Press ESC to close

Arjan ter HeegdeArjan ter Heegde Welcome to my blog about Microsoft Power Platform and Dynamics 365

Change Fields That Are Not on the Form in Dynamics 365 with This Level Up Command

Dynamics 365 admins run into this all the time: a field exists on the entity, but it is not available on the form. That makes simple updates slower and more frustrating than they should be.

This custom Level Up command solves that by opening a field editor directly on the current record. It allows users to view and update fields that are not on the form, without leaving the page or switching forms.

The command supports:

  • fields not included on the current form
  • all fields on the entity
  • lookup selection
  • filtering by field type and access level
  • direct save with automatic form refresh

For Dynamics admins, this is a practical way to make record maintenance easier without overloading forms with extra fields. It helps keep forms cleaner while still giving power users fast access to the data they need.

DateChange
17-03-2026Small change for lookup and choice fields.
17-03-2026Init

Note: this script is vibecoded.

You can add the JSON below as a command in Level Up:

Import command in Level Up

JavaScript source code (Compressed due to the 500-line limit in Level Up):

(function () {
    var ROOT_ID = "lu_missing_editor_panel";
    var STYLE_ID = "lu_missing_editor_style";
    var Xrm = window.Xrm || (window.parent && window.parent.Xrm) || (window.top && window.top.Xrm);
    var formContext = Xrm && Xrm.Page;
    var state = {filterAccess: "all",filterType: "all",scope: "missing",search: "",rows: [],entityName: "",entitySetName: "",entityId: "",relationshipMap: {},entitySetCache: {},formMap: {},loading: false,hasChanges: false,primaryNameAttr: "",recordName: ""};
    if (!Xrm || !formContext || !formContext.data || !formContext.ui) {alert("No supported form context found.");return;}
    if (document.getElementById(ROOT_ID)) {removePanel();return;}
    state.entityName = formContext.data.entity.getEntityName();
    state.entityId = cleanGuid(formContext.data.entity.getId() || "");
    if (!state.entityId) {alert("Save the record first. Fields can only be edited on a saved record.");return;}
    addStyles();
    renderShell();
    setStatus("Loading metadata...");
    loadAll().then(function () {renderRows();setStatus("Ready");}, function (err) {setStatus("Error");alert("Load failed: " + getErrorMessage(err));});
    function removePanel() {var el = document.getElementById(ROOT_ID);if (el && el.parentNode) el.parentNode.removeChild(el);}
    function cleanGuid(v) {return String(v || "").replace(/[{}]/g, "");}
    function esc(v) {return String(v == null ? "" : v).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);}
    function getErrorMessage(err) {if (!err) return "Unknown error";if (typeof err === "string") return err;if (err.message) return err.message;try { return JSON.stringify(err); } catch (e) { return "Unknown error"; }}
    function labelText(labelObj, fallback) {if (!labelObj) return fallback || "";if (typeof labelObj === "string") return labelObj;if (labelObj.UserLocalizedLabel && labelObj.UserLocalizedLabel.Label) return labelObj.UserLocalizedLabel.Label;if (labelObj.LocalizedLabels && labelObj.LocalizedLabels.length && labelObj.LocalizedLabels[0].Label) return labelObj.LocalizedLabels[0].Label;return fallback || "";}
    function typeName(a) {if (!a) return "Unknown";if (a.AttributeTypeName && a.AttributeTypeName.Value) return a.AttributeTypeName.Value;return a.AttributeType || "Unknown";}
    function sortLabel(a, b) {return String(a.label || "").localeCompare(String(b.label || ""), "en");}
    function split(list, size) {var out = [];for (var i = 0; i < list.length; i += size) out.push(list.slice(i, i + size));return out;}
    function cssEsc(v) {return String(v || "").replace(/\\/g, "\\\\").replace(/"/g, "\\\"");}
    function trimOrNull(v) {var s = String(v == null ? "" : v).trim();return s === "" ? null : s;}
    function parseInteger(v) {var s = trimOrNull(v);if (s === null) return null;var n = parseInt(s, 10);if (isNaN(n)) throw new Error("Invalid integer: " + s);return n;}
    function parseDecimal(v) {var s = trimOrNull(v);if (s === null) return null;s = s.replace(",", ".");var n = parseFloat(s);if (isNaN(n)) throw new Error("Invalid number: " + s);return n;}
    function parseDateInput(v) {var s = trimOrNull(v);if (s === null) return null;if (s.indexOf("T") >= 0) {if (s.indexOf("Z") < 0 && s.indexOf("+") < 0) {var parts = s.split("T");if (parts[1] && parts[1].length === 5) s += ":00";return s + ".000Z";}var d = new Date(s);if (isNaN(d.getTime())) throw new Error("Invalid date/time: " + s);return d.toISOString();}return s;}
    function toLocalDateTime(isoString) {if (!isoString) return "";var s = String(isoString);if (s.indexOf("T") < 0) return "";var parts = s.split("T");var datePart = parts[0];var timePart = parts[1] ? parts[1].split(".")[0].split("Z")[0].split("+")[0].substring(0, 5) : "00:00";return datePart + "T" + timePart;}
    function existingFormMap() {var map = {};var attrs = formContext.data.entity.attributes.get() || [];for (var i = 0; i < attrs.length; i++)map[String(attrs[i].getName() || "").toLowerCase()] = true;return map;}
    function fetchJson(url) {
        return fetch(url, {
            method: "GET",
            headers: {"Accept": "application/json","OData-Version": "4.0","OData-MaxVersion": "4.0","Prefer": "odata.include-annotations=\"OData.Community.Display.V1.FormattedValue,Microsoft.Dynamics.CRM.lookuplogicalname,Microsoft.Dynamics.CRM.associatednavigationproperty\""},
            credentials: "same-origin"
        }).then(function (r) {
            if (!r.ok) {
                return r.text().then(function (txt) {
                    var msg = txt || ("HTTP " + r.status);
                    try {var obj = JSON.parse(txt);if (obj && obj.error && obj.error.message) msg = obj.error.message;} catch (e) {}
                    throw new Error(msg);
                });
            }
            return r.json();
        });
    }
    function fetchEntityMeta() {
        var base = Xrm.Utility.getGlobalContext().getClientUrl() + "/api/data/v9.2/";
        var key = "EntityDefinitions(LogicalName=%27" + encodeURIComponent(state.entityName) + "%27)";
        var entityUrl = base + key + "?$select=LogicalName,EntitySetName,PrimaryIdAttribute,PrimaryNameAttribute";
        var attrUrl = base + key + "/Attributes?$select=LogicalName,AttributeType,AttributeTypeName,DisplayName,IsValidForRead,IsValidForUpdate,IsValidODataAttribute,IsLogical,AttributeOf";
        var relUrl = base + key + "/ManyToOneRelationships?$select=ReferencingAttribute,ReferencingEntityNavigationPropertyName,ReferencedEntity";
        return Promise.all([fetchJson(entityUrl),fetchJson(attrUrl),fetchJson(relUrl)]).then(function (all) {
            return {entity: all[0],attributes: all[1] && all[1].value ? all[1].value : [],relationships: all[2] && all[2].value ? all[2].value : []};
        });
    }
    function buildRelationshipMap(list) {
        var map = {};
        for (var i = 0; i < list.length; i++) {
            var r = list[i];
            if (!r || !r.ReferencingAttribute || !r.ReferencingEntityNavigationPropertyName) continue;
            var key = String(r.ReferencingAttribute).toLowerCase();
            if (!map[key]) map[key] = [];
            map[key].push({attribute: r.ReferencingAttribute,nav: r.ReferencingEntityNavigationPropertyName,target: r.ReferencedEntity});
        }
        return map;
    }
    function rowKind(a) {
        var t = String(typeName(a)).toLowerCase();
        if (t.indexOf("lookup") >= 0 || t.indexOf("customer") >= 0 || t.indexOf("owner") >= 0) return "lookup";
        if (t.indexOf("memo") >= 0) return "memo";
        if (t.indexOf("boolean") >= 0) return "boolean";
        if (t.indexOf("datetime") >= 0) return "datetime";
        if (t.indexOf("picklist") >= 0 || t.indexOf("status") >= 0 || t.indexOf("state") >= 0) return "choice";
        if (t.indexOf("multiselectpicklist") >= 0) return "readonly";
        if (t.indexOf("integer") >= 0 || t.indexOf("bigint") >= 0) return "integer";
        if (t.indexOf("decimal") >= 0 || t.indexOf("double") >= 0 || t.indexOf("money") >= 0) return "decimal";
        if (t.indexOf("uniqueidentifier") >= 0) return "readonly";
        if (t.indexOf("virtual") >= 0 || t.indexOf("file") >= 0 || t.indexOf("image") >= 0 || t.indexOf("partylist") >= 0 || t.indexOf("managedproperty") >= 0) return "readonly";
        return "text";
    }
    function isReadable(a) {if (!a || !a.LogicalName) return false;if (a.AttributeOf) return false;if (a.IsLogical) return false;if (a.IsValidForRead === false) return false;if (a.IsValidODataAttribute === false) return false;return true;}
    function isEditable(a, kind) {if (!a) return false;if (a.IsValidForUpdate === false) return false;if (kind === "readonly") return false;return true;}
    function buildRows(attrs, formMap) {
        var rows = [];
        var blocked = {"stageid": true, "processid": true, "traversedpath": true,"entityimage_timestamp": true, "entityimage_url": true,"versionnumber": true, "timezoneruleversionnumber": true,"utcconversiontimezonecode": true, "importsequencenumber": true,"overriddencreatedon": true, "yominame": true, "isprivate": true};
        for (var i = 0; i < attrs.length; i++) {
            var a = attrs[i];
            if (!isReadable(a)) continue;
            var name = a.LogicalName;
            if (blocked[String(name).toLowerCase()]) continue;
            var rels = state.relationshipMap[String(name).toLowerCase()] || [];
            var kind = rowKind(a);
            if (kind !== "lookup") rels = [];
            rows.push({name: name,label: labelText(a.DisplayName, name),type: typeName(a),kind: kind,editable: isEditable(a, kind),relationships: rels,targets: [],onForm: !!formMap[String(name).toLowerCase()],currentValue: null,currentDisplay: "",originalKey: "",lookupValue: null,lookupNavOriginal: "",options: [],optionMap: {}});
        }
        rows.sort(sortLabel);
        return rows;
    }
    function fetchChoiceOptions(row) {
        var t = String(row.type || "").toLowerCase();
        var cast = "";
        if (t.indexOf("status") >= 0) cast = "StatusAttributeMetadata";
        else if (t.indexOf("state") >= 0) cast = "StateAttributeMetadata";
        else cast = "PicklistAttributeMetadata";
        var base = Xrm.Utility.getGlobalContext().getClientUrl() + "/api/data/v9.2/";
        var key = "EntityDefinitions(LogicalName=%27" + encodeURIComponent(state.entityName) + "%27)";
        var attr = "Attributes(LogicalName=%27" + encodeURIComponent(row.name) + "%27)";
        var url = base + key + "/" + attr + "/Microsoft.Dynamics.CRM." + cast + "?$select=LogicalName&$expand=OptionSet($select=Options),GlobalOptionSet($select=Options)";
        return fetchJson(url).then(function (obj) {
            var src = [];
            if (obj && obj.OptionSet && obj.OptionSet.Options) src = obj.OptionSet.Options;
            if ((!src || !src.length) && obj && obj.GlobalOptionSet && obj.GlobalOptionSet.Options) src = obj.GlobalOptionSet.Options;
            var options = [];
            var map = {};
            for (var i = 0; i < src.length; i++) {
                var o = src[i];
                if (!o || o.Value == null) continue;
                var item = {value: o.Value,label: labelText(o.Label, String(o.Value)),state: o.State};
                options.push(item);
                map[String(item.value)] = item;
            }
            options.sort(function (a, b) {return String(a.label || "").localeCompare(String(b.label || ""), "en");});
            row.options = options;
            row.optionMap = map;
        }, function () {row.options = [];row.optionMap = {};});
    }
    function loadChoiceMetadata() {var jobs = [];for (var i = 0; i < state.rows.length; i++) {if (state.rows[i].kind === "choice") jobs.push(fetchChoiceOptions(state.rows[i]));}return Promise.all(jobs);}
    function loadAll() {
        return fetchEntityMeta().then(function (meta) {
            state.entitySetName = meta.entity && meta.entity.EntitySetName ? meta.entity.EntitySetName : state.entityName;
            state.primaryNameAttr = meta.entity && meta.entity.PrimaryNameAttribute ? meta.entity.PrimaryNameAttribute : "";
            state.relationshipMap = buildRelationshipMap(meta.relationships || []);
            state.formMap = existingFormMap();
            state.rows = buildRows(meta.attributes || [], state.formMap);
            updateSubtitle();
            return loadChoiceMetadata().then(function () {return reloadValues();});
        });
    }
    function updateSubtitle() {
        var editable = 0, readonly = 0, onForm = 0, missing = 0;
        for (var i = 0; i < state.rows.length; i++) {
            if (state.rows[i].editable) editable++; else readonly++;
            if (state.rows[i].onForm) onForm++; else missing++;
        }
        var sub = document.getElementById("lu_subtitle");
        var nameText = state.recordName ? state.recordName + " (" + state.entityName + ")" : state.entityName;
        if (sub) sub.textContent = nameText + " | Total: " + state.rows.length + " | On form: " + onForm + " | Missing: " + missing + " | Editable: " + editable + " | Read-only: " + readonly;
    }
    function hydrateRows(obj) {
        if (state.primaryNameAttr && obj[state.primaryNameAttr]) {state.recordName = String(obj[state.primaryNameAttr]);updateSubtitle();}
        for (var i = 0; i < state.rows.length; i++) {
            var row = state.rows[i];
            if (row.kind === "lookup") {
                var rawKey = "_" + row.name + "_value";
                var fmtKey = rawKey + "@OData.Community.Display.V1.FormattedValue";
                var typeKey = rawKey + "@Microsoft.Dynamics.CRM.lookuplogicalname";
                var navKey = rawKey + "@Microsoft.Dynamics.CRM.associatednavigationproperty";
                var rawId = obj[rawKey] ? cleanGuid(obj[rawKey]) : "";
                var rawName = obj[fmtKey] || "";
                var rawType = obj[typeKey] || "";
                var rawNav = obj[navKey] || "";
                row.lookupValue = rawId ? {id: rawId,name: rawName || rawId,entityType: rawType || "",nav: rawNav || ""} : null;
                row.lookupNavOriginal = rawNav || "";
                row.currentValue = row.lookupValue ? row.lookupValue.id : null;
                row.currentDisplay = row.lookupValue ? row.lookupValue.name : "";
                row.originalKey = row.lookupValue ? (row.lookupValue.entityType + "|" + row.lookupValue.id) : "";
            } else {
                var fmt = row.name + "@OData.Community.Display.V1.FormattedValue";
                var raw = Object.prototype.hasOwnProperty.call(obj, row.name) ? obj[row.name] : null;
                row.currentValue = raw == null ? null : raw;
                row.currentDisplay = obj[fmt] != null ? String(obj[fmt]) : (raw == null ? "" : String(raw));
                if (row.kind === "datetime" && raw) {row.originalKey = String(raw).split(".")[0].split("Z")[0].split("+")[0] + ".000Z";} else {row.originalKey = raw == null ? "" : String(raw);}
            }
        }
    }
    function fetchColumnsSafe(baseUrl, columns) {
        var merged = {};
        function absorb(obj) {for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) merged[k] = obj[k];}
        function loadGroup(cols) {
            if (!cols || !cols.length) return Promise.resolve();
            return fetchJson(baseUrl + "?$select=" + cols.join(",")).then(function (obj) {absorb(obj);}, function () {
                if (cols.length === 1) return Promise.resolve();
                var mid = Math.ceil(cols.length / 2);
                return loadGroup(cols.slice(0, mid)).then(function () {return loadGroup(cols.slice(mid));});
            });
        }
        return loadGroup(columns).then(function () { return merged; });
    }
    function reloadValues() {
        setStatus("Loading values...");
        var selects = [];
        for (var i = 0; i < state.rows.length; i++) {
            if (state.rows[i].kind === "lookup") selects.push("_" + state.rows[i].name + "_value");
            else selects.push(state.rows[i].name);
        }
        if (!selects.length) {renderRows();setStatus("No fields found");return Promise.resolve();}
        var base = Xrm.Utility.getGlobalContext().getClientUrl() + "/api/data/v9.2/" + state.entitySetName + "(" + state.entityId + ")";
        var parts = split(selects, 20);
        var merged = {};
        var chain = Promise.resolve();
        for (var p = 0; p < parts.length; p++) {
            (function (cols) {
                chain = chain.then(function () {
                    return fetchColumnsSafe(base, cols).then(function (obj) {
                        for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) merged[k] = obj[k];
                    });
                });
            })(parts[p]);
        }
        return chain.then(function () {hydrateRows(merged);renderRows();setStatus("Values refreshed");}, function () {hydrateRows({});renderRows();setStatus("Values loaded with skips");});
    }
    function addStyles() {
        if (document.getElementById(STYLE_ID)) return;
        var css = "#" + ROOT_ID + "{position:fixed;top:64px;right:12px;width:620px;max-width:calc(100vw - 24px);height:calc(100vh - 76px);background:#ffffff;border:1px solid #d1d5db;border-radius:16px;box-shadow:0 20px 50px rgba(0,0,0,.2);z-index:2147483647;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif;display:flex;flex-direction:column;overflow:hidden;}#" + ROOT_ID + " *{box-sizing:border-box;}#" + ROOT_ID + " .hd{padding:20px 22px 22px;border-bottom:1px solid #e5e7eb;background:linear-gradient(180deg,#fafbfc 0%,#f5f7fa 100%);}#" + ROOT_ID + " .top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px;}#" + ROOT_ID + " .title{font-size:18px;font-weight:700;line-height:1.3;color:#0d1421;letter-spacing:-.01em;}#" + ROOT_ID + " .sub{font-size:12px;color:#6b7280;margin-top:5px;line-height:1.5;}#" + ROOT_ID + " .close{border:1px solid #d1d5db;background:#fff;border-radius:8px;padding:7px 13px;font-size:12px;font-weight:600;cursor:pointer;appearance:none;-webkit-appearance:none;transition:all .15s ease;color:#374151;}#" + ROOT_ID + " .close:hover{background:#f9fafb;border-color:#9ca3af;}#" + ROOT_ID + " .search{width:100%;margin-bottom:16px;padding:10px 13px;border:1px solid #d1d5db;border-radius:10px;font-size:13px;outline:none;transition:border-color .15s ease;}#" + ROOT_ID + " .search:focus{border-color:#0f6cbd;box-shadow:0 0 0 3px rgba(15,108,189,.1);}#" + ROOT_ID + " .lu-switch-row{display:flex;align-items:center;gap:12px;margin-bottom:18px;}#" + ROOT_ID + " .lu-switch-label{font-size:13px;color:#374151;font-weight:600;text-transform:uppercase;letter-spacing:.02em;font-size:11px;}#" + ROOT_ID + " .lu-segmented{display:inline-flex;align-items:stretch;gap:0;padding:3px;border:1px solid #d1dae6;background:#e8edf5;border-radius:10px;box-shadow:inset 0 1px 2px rgba(16,24,40,.06);}#" + ROOT_ID + " .lu-segment{appearance:none;-webkit-appearance:none;border:0 !important;outline:none;background:transparent;color:#4b5563;border-radius:7px;padding:8px 16px;font-size:12px;font-weight:600;line-height:1;cursor:pointer;transition:all .2s cubic-bezier(.4,0,.2,1);white-space:nowrap;position:relative;}#" + ROOT_ID + " .lu-segment:hover:not(.active){background:rgba(255,255,255,.5);}#" + ROOT_ID + " .lu-segment.active{background:#ffffff;color:#0f6cbd;box-shadow:0 2px 4px rgba(16,24,40,.1),0 1px 2px rgba(16,24,40,.06),inset 0 0 0 1px rgba(15,108,189,.1);}#" + ROOT_ID + " .filter-section{margin-bottom:24px;}#" + ROOT_ID + " .filter-section:last-child{margin-bottom:8px;}#" + ROOT_ID + " .filter-label{font-size:11px;color:#374151;font-weight:700;text-transform:uppercase;letter-spacing:.03em;margin-bottom:10px;}#" + ROOT_ID + " .filters{display:flex;gap:7px;flex-wrap:wrap;}#" + ROOT_ID + " .f{border:1px solid #d1d5db;background:#fff;border-radius:999px;padding:7px 14px;font-size:12px;font-weight:600;cursor:pointer;appearance:none;-webkit-appearance:none;transition:all .15s ease;color:#4b5563;}#" + ROOT_ID + " .f:hover:not(.a){background:#f9fafb;border-color:#9ca3af;}#" + ROOT_ID + " .f.a{background:#0f6cbd;color:#fff;border-color:#0f6cbd;box-shadow:0 1px 3px rgba(15,108,189,.3);}#" + ROOT_ID + " .toolbar{display:flex;gap:8px;flex-wrap:wrap;padding:13px 18px;border-bottom:1px solid #e5e7eb;background:#fafbfc;}#" + ROOT_ID + " .btn{border:1px solid #d1d5db;background:#fff;border-radius:8px;padding:9px 15px;font-size:12px;font-weight:600;cursor:pointer;appearance:none;-webkit-appearance:none;transition:all .15s ease;color:#374151;}#" + ROOT_ID + " .btn:hover{background:#f3f4f6;}#" + ROOT_ID + " .btn.primary{background:#0f6cbd;color:#fff;border-color:#0f6cbd;}#" + ROOT_ID + " .btn.primary:hover{background:#0c5ba6;}#" + ROOT_ID + " .btn.primary:disabled{background:#e5e7eb;border-color:#d1d5db;color:#9ca3af;cursor:not-allowed;box-shadow:none;}#" + ROOT_ID + " .btn.primary:disabled:hover{background:#e5e7eb;}#" + ROOT_ID + " .status{padding:11px 18px;border-bottom:1px solid #e5e7eb;background:#f9fafb;font-size:12px;color:#374151;font-weight:500;}#" + ROOT_ID + " .list{flex:1;overflow:auto;background:#f5f7fa;padding:16px;}#" + ROOT_ID + " .row{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px;margin-bottom:12px;box-shadow:0 1px 2px rgba(0,0,0,.04);transition:box-shadow .15s ease;}#" + ROOT_ID + " .row:hover{box-shadow:0 2px 8px rgba(0,0,0,.08);}#" + ROOT_ID + " .rowhd{margin-bottom:12px;}#" + ROOT_ID + " .lbl{font-size:14px;font-weight:600;color:#111827;line-height:1.4;margin-bottom:4px;}#" + ROOT_ID + " .meta{font-size:11px;color:#6b7280;margin-bottom:10px;word-break:break-word;font-family:Monaco,Consolas,monospace;background:#f9fafb;padding:3px 8px;border-radius:5px;display:inline-block;}#" + ROOT_ID + " .lu-chip-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}#" + ROOT_ID + " .lu-chip{display:inline-flex;align-items:center;gap:6px;font-size:11px;font-weight:600;border-radius:6px;padding:5px 11px;white-space:nowrap;border:1px solid;line-height:1.3;text-transform:capitalize;}#" + ROOT_ID + " .lu-chip-dot{width:5px;height:5px;border-radius:999px;background:currentColor;display:inline-block;}#" + ROOT_ID + " .lu-chip-scope.onform{background:#d1fae5;border-color:#6ee7b7;color:#065f46;}#" + ROOT_ID + " .lu-chip-scope.missing{background:#dbeafe;border-color:#93c5fd;color:#1e40af;}#" + ROOT_ID + " .lu-chip-type{background:#fef3c7;border-color:#fcd34d;color:#92400e;}#" + ROOT_ID + " .lu-chip-kind{background:#e0e7ff;border-color:#c7d2fe;color:#3730a3;}#" + ROOT_ID + " .ro{display:inline-block;margin-top:10px;font-size:11px;font-weight:600;color:#92400e;background:#fef3c7;border:1px solid #fcd34d;border-radius:6px;padding:5px 11px;}#" + ROOT_ID + " .fld{margin-top:12px;}#" + ROOT_ID + " .input,#" + ROOT_ID + " .select,#" + ROOT_ID + " .ta{width:100%;padding:10px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:13px;background:#fff;color:#111827;transition:border-color .15s ease;}#" + ROOT_ID + " .input:focus,#" + ROOT_ID + " .select:focus,#" + ROOT_ID + " .ta:focus{outline:none;border-color:#0f6cbd;box-shadow:0 0 0 3px rgba(15,108,189,.1);}#" + ROOT_ID + " .ta{min-height:90px;resize:vertical;font-family:inherit;}#" + ROOT_ID + " .input[disabled],#" + ROOT_ID + " .select[disabled],#" + ROOT_ID + " .ta[disabled]{background:#f9fafb;color:#9ca3af;cursor:not-allowed;}#" + ROOT_ID + " .lookupwrap{display:flex;gap:8px;align-items:center;}#" + ROOT_ID + " .lookupname{flex:1;padding:10px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:13px;background:#fff;min-height:40px;display:flex;align-items:center;}#" + ROOT_ID + " .lookupbtn{border:1px solid #d1d5db;background:#fff;border-radius:8px;padding:9px 13px;font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;appearance:none;-webkit-appearance:none;transition:all .15s ease;color:#374151;}#" + ROOT_ID + " .lookupbtn:hover{background:#f3f4f6;}#" + ROOT_ID + " .help{margin-top:7px;font-size:11px;color:#6b7280;line-height:1.4;}#" + ROOT_ID + " .empty{text-align:center;color:#6b7280;padding:40px 20px;font-size:13px;}#" + ROOT_ID + " .chk{display:flex;align-items:center;gap:8px;font-size:13px;color:#111827;padding:10px 0;}#" + ROOT_ID + " .chk input{width:17px;height:17px;cursor:pointer;}#" + ROOT_ID + ".lookup-open{visibility:hidden;}";
        var st = document.createElement("style");
        st.id = STYLE_ID;
        st.appendChild(document.createTextNode(css));
        document.head.appendChild(st);
    }
    function renderShell() {
        var host = document.createElement("div");
        host.id = ROOT_ID;
        host.innerHTML = "<div class=\"hd\"><div class=\"top\"><div><div class=\"title\">Missing fields editor</div><div class=\"sub\" id=\"lu_subtitle\">" + esc(state.entityName) + "</div></div><button class=\"close\" type=\"button\" id=\"lu_close\">Close</button></div><input class=\"search\" id=\"lu_search\" type=\"text\" placeholder=\"Search by label, logical name or type\"><div class=\"lu-switch-row\"><div class=\"lu-switch-label\">Show</div><div class=\"lu-segmented\"><button class=\"lu-segment active\" data-scope=\"missing\" type=\"button\">Missing only</button><button class=\"lu-segment\" data-scope=\"allfields\" type=\"button\">All fields</button></div></div><div class=\"filter-section\"><div class=\"filter-label\">Access Level</div><div class=\"filters\"><button class=\"f a\" data-filter-access=\"all\" type=\"button\">All</button><button class=\"f\" data-filter-access=\"editable\" type=\"button\">Editable</button><button class=\"f\" data-filter-access=\"readonly\" type=\"button\">Read-only</button></div></div><div class=\"filter-section\"><div class=\"filter-label\">Field Type</div><div class=\"filters\"><button class=\"f a\" data-filter-type=\"all\" type=\"button\">All</button><button class=\"f\" data-filter-type=\"lookup\" type=\"button\">Lookup</button><button class=\"f\" data-filter-type=\"boolean\" type=\"button\">Boolean</button><button class=\"f\" data-filter-type=\"choice\" type=\"button\">Choice</button><button class=\"f\" data-filter-type=\"text\" type=\"button\">Text</button><button class=\"f\" data-filter-type=\"integer\" type=\"button\">Integer</button><button class=\"f\" data-filter-type=\"decimal\" type=\"button\">Decimal</button><button class=\"f\" data-filter-type=\"datetime\" type=\"button\">DateTime</button><button class=\"f\" data-filter-type=\"memo\" type=\"button\">Memo</button></div></div></div><div class=\"toolbar\"><button class=\"btn primary\" type=\"button\" id=\"lu_save\">Save</button><button class=\"btn\" type=\"button\" id=\"lu_refresh\">Refresh</button></div><div class=\"status\" id=\"lu_status\">Loading...</div><div class=\"list\" id=\"lu_list\"></div>";
        document.body.appendChild(host);
        document.getElementById("lu_close").onclick = removePanel;
        document.getElementById("lu_search").oninput = function () {state.search = String(this.value || "").toLowerCase();renderRows();};
        var saveBtn = document.getElementById("lu_save");
        saveBtn.onclick = saveAll;
        saveBtn.disabled = true;
        document.getElementById("lu_refresh").onclick = function () {formContext.data.refresh(false).then(function () { return reloadValues(); }, function () { return reloadValues(); });};
        host.onclick = function (e) {
            var t = e.target || e.srcElement;
            if (!t) return;
            var scope = t.getAttribute("data-scope");
            if (scope) {
                state.scope = scope;
                var sbs = host.querySelectorAll(".lu-segment");
                for (var si = 0; si < sbs.length; si++) sbs[si].className = "lu-segment";
                t.className = "lu-segment active";
                renderRows();
                return;
            }
            var filterAccess = t.getAttribute("data-filter-access");
            if (filterAccess) {
                state.filterAccess = filterAccess;
                var fs = host.querySelectorAll("[data-filter-access]");
                for (var i = 0; i < fs.length; i++) fs[i].className = "f";
                t.className = "f a";
                renderRows();
                return;
            }
            var filterType = t.getAttribute("data-filter-type");
            if (filterType) {
                state.filterType = filterType;
                var fs = host.querySelectorAll("[data-filter-type]");
                for (var i = 0; i < fs.length; i++) fs[i].className = "f";
                t.className = "f a";
                renderRows();
                return;
            }
            var act = t.getAttribute("data-act");
            var name = t.getAttribute("data-name");
            if (act === "picklookup" && name) openLookup(name);
            if (act === "clearlookup" && name) clearLookup(name);
        };
    }
    function setStatus(text) {var el = document.getElementById("lu_status");if (el) el.textContent = text;}
    function setLookupOverlay(open) {var el = document.getElementById(ROOT_ID);if (!el) return;el.className = open ? "lookup-open" : "";}
    function checkForChanges() {var changed = false;for (var i = 0; i < state.rows.length; i++) {var row = state.rows[i];if (!row.editable) continue;if (row.kind === "lookup") {var currentKey = row.lookupValue ? (row.lookupValue.entityType + "|" + row.lookupValue.id) : "";if (currentKey !== row.originalKey) {changed = true;break;}} else {var el = document.querySelector("[data-name=\"" + cssEsc(row.name) + "\"]");if (!el) continue;var currentVal = "";if (row.kind === "boolean") {currentVal = el.checked ? "true" : "false";var origVal = row.currentValue === true ? "true" : "false";if (currentVal !== origVal) {changed = true;break;}} else if (row.kind === "datetime") {var parsed = parseDateInput(el.value);if (parsed !== row.originalKey) {changed = true;break;}} else {currentVal = el.value == null ? "" : String(el.value);if (row.kind === "choice" || row.kind === "integer") {var parsedNum = parseInteger(el.value);currentVal = parsedNum == null ? "" : String(parsedNum);}if (currentVal !== row.originalKey) {changed = true;break;}}}}state.hasChanges = changed;var saveBtn = document.getElementById("lu_save");if (saveBtn) saveBtn.disabled = !changed;}
    function matchRow(row) {
        if (state.scope === "missing" && row.onForm) return false;
        if (state.filterAccess === "editable" && !row.editable) return false;
        if (state.filterAccess === "readonly" && row.editable) return false;
        if (state.filterType !== "all" && row.kind !== state.filterType) return false;
        if (!state.search) return true;
        var hay = (row.label + " " + row.name + " " + row.type + " " + row.kind + " " + (row.onForm ? "onform" : "missing")).toLowerCase();
        return hay.indexOf(state.search) >= 0;
    }
    function renderChoiceOptions(row) {
        var html = ["<option value=\"\">-- Empty --</option>"];
        for (var i = 0; i < row.options.length; i++) {
            var o = row.options[i];
            html.push("<option value=\"" + esc(o.value) + "\">" + esc(o.label) + " (" + esc(o.value) + ")</option>");
        }
        return html.join("");
    }
    function renderInput(row) {
        var disabled = row.editable ? "" : " disabled";
        if (row.kind === "memo") return "<textarea class=\"ta\" data-kind=\"memo\" data-name=\"" + esc(row.name) + "\"" + disabled + ">" + esc(row.currentDisplay || "") + "</textarea>";
        if (row.kind === "boolean") {var checked = row.currentValue === true ? " checked" : "";return "<label class=\"chk\"><input type=\"checkbox\" data-kind=\"boolean\" data-name=\"" + esc(row.name) + "\"" + checked + disabled + "> Yes</label>";}
        if (row.kind === "choice") return "<select class=\"select\" data-kind=\"choice\" data-name=\"" + esc(row.name) + "\"" + disabled + ">" + renderChoiceOptions(row) + "</select>";
        if (row.kind === "integer") return "<input class=\"input\" data-kind=\"integer\" data-name=\"" + esc(row.name) + "\" type=\"text\" value=\"" + esc(row.currentValue != null ? String(row.currentValue) : "") + "\"" + disabled + ">";
        if (row.kind === "decimal") return "<input class=\"input\" data-kind=\"decimal\" data-name=\"" + esc(row.name) + "\" type=\"text\" value=\"" + esc(row.currentValue != null ? String(row.currentValue) : "") + "\"" + disabled + ">";
        if (row.kind === "datetime") return "<input class=\"input\" data-kind=\"datetime\" data-name=\"" + esc(row.name) + "\" type=\"datetime-local\" value=\"" + toLocalDateTime(row.currentValue) + "\"" + disabled + "><div class=\"help\">Use the calendar picker or type manually</div>";
        if (row.kind === "lookup") {
            var text = row.lookupValue ? row.lookupValue.name + (row.lookupValue.entityType ? " (" + row.lookupValue.entityType + ")" : "") : "";
            var targets = [];
            for (var i = 0; i < row.relationships.length; i++)
                if (row.relationships[i].target && targets.indexOf(row.relationships[i].target) < 0) targets.push(row.relationships[i].target);
            return "<div class=\"lookupwrap\"><div class=\"lookupname\" data-name=\"" + esc(row.name) + "\" id=\"lk_" + esc(row.name) + "\">" + esc(text) + "</div><button class=\"lookupbtn\" type=\"button\" data-act=\"picklookup\" data-name=\"" + esc(row.name) + "\"" + disabled + ">Select</button><button class=\"lookupbtn\" type=\"button\" data-act=\"clearlookup\" data-name=\"" + esc(row.name) + "\"" + disabled + ">Clear</button></div><div class=\"help\">Targets: " + esc(targets.join(", ")) + "</div>";
        }
        return "<input class=\"input\" data-kind=\"text\" data-name=\"" + esc(row.name) + "\" type=\"text\" value=\"" + esc(row.currentDisplay || "") + "\"" + disabled + ">";
    }
    function renderRow(row) {
        var html = [];
        html.push("<div class=\"row\">");
        html.push("<div class=\"rowhd\">");
        html.push("<div class=\"lbl\">" + esc(row.label || row.name) + "</div>");
        html.push("<div class=\"meta\">" + esc(row.name) + "</div>");
        html.push("<div class=\"lu-chip-row\">");
        html.push("<span class=\"lu-chip lu-chip-scope " + (row.onForm ? "onform" : "missing") + "\"><span class=\"lu-chip-dot\"></span>" + (row.onForm ? "On form" : "Missing") + "</span>");
        html.push("<span class=\"lu-chip lu-chip-type\">" + esc(row.type) + "</span>");
        html.push("<span class=\"lu-chip lu-chip-kind\">" + esc(row.kind) + "</span>");
        html.push("</div>");
        if (!row.editable) html.push("<div class=\"ro\">Read-only</div>");
        html.push("</div>");
        html.push("<div class=\"fld\">" + renderInput(row) + "</div>");
        html.push("</div>");
        return html.join("");
    }
    function renderRows() {
        var list = document.getElementById("lu_list");
        if (!list) return;
        var html = [];
        var shown = 0;
        for (var i = 0; i < state.rows.length; i++) {
            if (!matchRow(state.rows[i])) continue;
            shown++;
            html.push(renderRow(state.rows[i]));
        }
        if (!shown) html.push("<div class=\"empty\">No fields found for the current filter.</div>");
        list.innerHTML = html.join("");
        syncInputs();
        attachChangeListeners();
    }
    function attachChangeListeners() {
        var inputs = document.querySelectorAll("#" + ROOT_ID + " .input, #" + ROOT_ID + " .select, #" + ROOT_ID + " .ta, #" + ROOT_ID + " input[type='checkbox']");
        for (var i = 0; i < inputs.length; i++) {
            inputs[i].removeEventListener("input", checkForChanges);
            inputs[i].removeEventListener("change", checkForChanges);
            inputs[i].addEventListener("input", checkForChanges);
            inputs[i].addEventListener("change", checkForChanges);
        }
    }
    function syncInputs() {
        for (var i = 0; i < state.rows.length; i++) {
            var row = state.rows[i];
            if (row.kind === "lookup") {
                var lk = document.getElementById("lk_" + row.name);
                if (lk) lk.textContent = row.lookupValue ? row.lookupValue.name + (row.lookupValue.entityType ? " (" + row.lookupValue.entityType + ")" : "") : "";
            }
            if (row.kind === "choice") {
                var sel = document.querySelector("select[data-name=\"" + cssEsc(row.name) + "\"]");
                if (sel) sel.value = row.currentValue == null ? "" : String(row.currentValue);
            }
            if (row.kind === "boolean") {
                var chk = document.querySelector("input[type=\"checkbox\"][data-name=\"" + cssEsc(row.name) + "\"]");
                if (chk) chk.checked = row.currentValue === true;
            }
        }
    }
    function findRow(name) {var key = String(name || "").toLowerCase();for (var i = 0; i < state.rows.length; i++)if (String(state.rows[i].name || "").toLowerCase() === key) return state.rows[i];return null;}
    function openLookup(name) {
        var row = findRow(name);
        if (!row || !row.editable) return;
        var entityTypes = [];
        for (var i = 0; i < row.relationships.length; i++)
            if (row.relationships[i].target && entityTypes.indexOf(row.relationships[i].target) < 0) entityTypes.push(row.relationships[i].target);
        if (!entityTypes.length) {setStatus("No lookup targets found for " + row.name);return;}
        setLookupOverlay(true);
        Xrm.Utility.lookupObjects({allowMultiSelect: false,entityTypes: entityTypes,defaultEntityType: row.lookupValue && row.lookupValue.entityType ? row.lookupValue.entityType : entityTypes[0]}).then(function (results) {
            setLookupOverlay(false);
            if (!results || !results.length) return;
            var first = results[0];
            row.lookupValue = {id: cleanGuid(first.id),name: first.name || cleanGuid(first.id),entityType: first.entityType || entityTypes[0],nav: ""};
            syncInputs();
            setStatus("Lookup selected for " + row.label);
            checkForChanges();
        }, function (err) {
            setLookupOverlay(false);
            if (err && err.message) setStatus("Lookup failed: " + getErrorMessage(err));
            else setStatus("Lookup closed");
        });
    }
    function clearLookup(name) {var row = findRow(name);if (!row || !row.editable) return;row.lookupValue = null;syncInputs();setStatus("Lookup cleared for " + row.label);checkForChanges();}
    function collectRowValue(row) {
        if (!row.editable) return { supported: false };
        if (row.kind === "lookup") return { supported: true, kind: "lookup", value: row.lookupValue };
        var el = document.querySelector("[data-name=\"" + cssEsc(row.name) + "\"]");
        if (!el) return { supported: false };
        if (row.kind === "memo" || row.kind === "text") return { supported: true, kind: row.kind, value: trimOrNull(el.value) };
        if (row.kind === "boolean") return { supported: true, kind: "boolean", value: !!el.checked };
        if (row.kind === "integer") return { supported: true, kind: "integer", value: parseInteger(el.value) };
        if (row.kind === "decimal") return { supported: true, kind: "decimal", value: parseDecimal(el.value) };
        if (row.kind === "choice") return { supported: true, kind: "choice", value: parseInteger(el.value) };
        if (row.kind === "datetime") return { supported: true, kind: "datetime", value: parseDateInput(el.value) };
        return { supported: true, kind: "text", value: trimOrNull(el.value) };
    }
    function findRelationshipForValue(row, lookupValue) {
        if (!lookupValue) {
            if (row.lookupNavOriginal) return { nav: row.lookupNavOriginal, target: "" };
            return row.relationships && row.relationships.length ? row.relationships[0] : null;
        }
        for (var i = 0; i < row.relationships.length; i++) {
            if (String(row.relationships[i].target || "").toLowerCase() === String(lookupValue.entityType || "").toLowerCase())
                return row.relationships[i];
        }
        return row.relationships && row.relationships.length ? row.relationships[0] : null;
    }
    function getEntitySetNameFor(logicalName) {
        var key = String(logicalName || "").toLowerCase();
        if (state.entitySetCache[key]) return Promise.resolve(state.entitySetCache[key]);
        return Xrm.Utility.getEntityMetadata(logicalName).then(function (meta) {
            var setName = meta && meta.EntitySetName ? meta.EntitySetName : logicalName;
            state.entitySetCache[key] = setName;
            return setName;
        });
    }
    function buildPayload() {
        var payload = {};
        var tasks = [];
        for (var i = 0; i < state.rows.length; i++) {
            (function (row) {
                if (!row.editable) return;
                var data = collectRowValue(row);
                if (!data.supported) return;
                if (row.kind === "lookup") {
                    var newKey = data.value ? ((data.value.entityType || "") + "|" + cleanGuid(data.value.id || "")) : "";
                    if (newKey === row.originalKey) return;
                    tasks.push(new Promise(function (resolve, reject) {
                        var rel = findRelationshipForValue(row, data.value);
                        if (!rel || !rel.nav) {resolve();return;}
                        if (!data.value) {payload[rel.nav] = null;resolve();return;}
                        getEntitySetNameFor(data.value.entityType).then(function (setName) {
                            payload[rel.nav + "@odata.bind"] = "/" + setName + "(" + cleanGuid(data.value.id) + ")";
                            resolve();
                        }, reject);
                    }));
                } else {
                    var compare = data.value == null ? "" : String(data.value);
                    if (compare === row.originalKey) return;
                    payload[row.name] = data.value;
                    if (String(row.name).toLowerCase() === "statuscode" && row.optionMap[String(data.value)] && row.optionMap[String(data.value)].state != null && !Object.prototype.hasOwnProperty.call(payload, "statecode"))
                        payload.statecode = row.optionMap[String(data.value)].state;
                }
            })(state.rows[i]);
        }
        return Promise.all(tasks).then(function () { return payload; });
    }
    function refreshFormAndPanel(statusText) {
        return formContext.data.refresh(false).then(function () {setStatus(statusText);return reloadValues();}, function () {setStatus(statusText);return reloadValues();});
    }
    function saveAll() {
        if (state.loading) return;
        state.loading = true;
        setStatus("Saving...");
        buildPayload().then(function (payload) {
            var keys = [];
            for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) keys.push(k);
            if (!keys.length) {return refreshFormAndPanel("No changes to save");}
            return Xrm.WebApi.updateRecord(state.entityName, state.entityId, payload).then(function () {return refreshFormAndPanel("Saved");});
        }).then(function () {state.loading = false;state.hasChanges = false;var saveBtn = document.getElementById("lu_save");if (saveBtn) saveBtn.disabled = true;}, function (err) {
            state.loading = false;
            setStatus("Save failed: " + getErrorMessage(err));
            alert("Save failed: " + getErrorMessage(err));
        });
    }
})();
JavaScript

Arjan ter Heegde

Arjan ter Heegde

Leave a Reply

Your email address will not be published. Required fields are marked *