From 55130c0570b3f0151082bde7f9d7b89676248caa Mon Sep 17 00:00:00 2001 From: Abe Wheeler Date: Thu, 15 Oct 2026 13:22:57 -0500 Subject: [PATCH] Render nested model details and release 0.18.10 --- package-lock.json | 8 ++--- package.json | 2 +- packages/create-filegrc/package.json | 2 +- packages/create-filegrc/src/index.js | 4 +-- packages/create-filegrc/template/package.json | 2 +- packages/filegrc/package.json | 2 +- packages/filegrc/src/web.js | 32 +++++++++++++------ packages/filegrc/test/render.test.js | 29 +++++++++++++++++ 8 files changed, 61 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 739d7f5..1a61a28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "filegrc-monorepo", - "version": "0.18.9", + "version": "0.18.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "filegrc-monorepo", - "version": "0.18.9", + "version": "0.18.10", "workspaces": [ "packages/filegrc", "packages/create-filegrc" @@ -24,7 +24,7 @@ "link": true }, "packages/create-filegrc": { - "version": "0.18.9", + "version": "0.18.10", "license": "MIT", "bin": { "create-filegrc": "bin/create-filegrc.js" @@ -34,7 +34,7 @@ } }, "packages/filegrc": { - "version": "0.18.9", + "version": "0.18.10", "license": "MIT", "bin": { "filegrc": "bin/filegrc.js" diff --git a/package.json b/package.json index 14751bd..1579764 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "filegrc-monorepo", - "version": "0.18.9", + "version": "0.18.10", "private": true, "workspaces": [ "packages/filegrc", diff --git a/packages/create-filegrc/package.json b/packages/create-filegrc/package.json index 09eb7ef..964f744 100644 --- a/packages/create-filegrc/package.json +++ b/packages/create-filegrc/package.json @@ -1,6 +1,6 @@ { "name": "create-filegrc", - "version": "0.18.9", + "version": "0.18.10", "description": "Create a filegrc workspace for a SOC 2 program", "license": "MIT", "repository": { diff --git a/packages/create-filegrc/src/index.js b/packages/create-filegrc/src/index.js index b46a98a..3921f4f 100644 --- a/packages/create-filegrc/src/index.js +++ b/packages/create-filegrc/src/index.js @@ -524,13 +524,13 @@ async function runCombinedSetup(target, input) { async function writeMinimalLockfile(target, name, versionRange) { const lock = { name, - version: "0.18.9", + version: "0.18.10", lockfileVersion: 3, requires: true, packages: { "": { name, - version: "0.18.9", + version: "0.18.10", dependencies: { filegrc: versionRange } } } diff --git a/packages/create-filegrc/template/package.json b/packages/create-filegrc/template/package.json index a5346b5..63483d3 100644 --- a/packages/create-filegrc/template/package.json +++ b/packages/create-filegrc/template/package.json @@ -1,6 +1,6 @@ { "name": "{{project_name}}", - "version": "0.18.9", + "version": "0.18.10", "private": true, "description": "filegrc workspace for a SOC 2 program", "type": "module", diff --git a/packages/filegrc/package.json b/packages/filegrc/package.json index cb3956d..d5a16bf 100644 --- a/packages/filegrc/package.json +++ b/packages/filegrc/package.json @@ -1,6 +1,6 @@ { "name": "filegrc", - "version": "0.18.9", + "version": "0.18.10", "description": "Zero-dependency Git-native GRC engine", "license": "MIT", "repository": { diff --git a/packages/filegrc/src/web.js b/packages/filegrc/src/web.js index 91febc7..1bf30ea 100644 --- a/packages/filegrc/src/web.js +++ b/packages/filegrc/src/web.js @@ -6405,18 +6405,30 @@ function formatObjectArray(items, objectType, compact = false) { return []; } const label = property.label || humanize(name.replace(/Ids?$/, "")); - let display; - if (property.relation && property.type === "id") display = formatReference(value); - else if (property.relation && property.type === "array") display = value.map((id) => formatReference(id)).join(" "); - else if (Array.isArray(value)) display = esc(value.map((entry) => properCase(entry)).join(", ")); - else if (property.type === "enum") display = esc(properCase(value)); - else if (property.type === "date") display = esc(formatCalendarDate(value)); - else display = esc(String(value)); - return ['' + esc(label) + '' + display + '']; + const display = formatObjectProperty(value, property, compact); + return ['
' + esc(label) + '' + display + '
']; }); - return '
' + facts.join("") + '' + (!compact && notes.length ? '' + esc(notes.join(" ")) + '' : "") + '
'; + return '
' + facts.join("") + '
' + (!compact && notes.length ? '' + esc(notes.join(" ")) + '' : "") + '
'; }).join("") + ''; } +function formatObjectProperty(value, property, compact) { + if (property.relation && property.type === "id") return formatReference(value); + if (Array.isArray(value)) { + if (property.itemObjectType) return formatObjectArray(value, property.itemObjectType, compact); + if (property.relation) return value.map((id) => formatReference(id)).join(" "); + return value.map((item) => formatObjectProperty(item, { type: property.items }, compact)).join(", "); + } + if (value && typeof value === "object") { + if (state.model.objectTypes[property.objectType]?.properties) return formatObjectArray([value], property.objectType, compact); + return '
' + esc(JSON.stringify(value, null, 2)) + '
'; + } + if (property.type === "enum") return esc(properCase(value)); + if (property.type === "date") return esc(formatCalendarDate(value)); + if (property.type === "timestamp") return esc(formatLocalDateTime(value)); + if (typeof value === "boolean") return value ? "Yes" : "No"; + return esc(String(value)); +} + function workQueueScheduleStatus(obligation) { const scheduleReview = state.collectionReviews?.obligation; const disposition = scheduleReview?.complete && scheduleReview.review?.scheduleDecisions?.find(({ obligationId }) => obligationId === obligation.id)?.disposition; @@ -6957,7 +6969,7 @@ dialog::backdrop{background:rgba(0,0,24,.62)} .activation-review{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0}.activation-review>div{padding:10px 12px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.activation-review .full{grid-column:1/-1}.activation-review span,.activation-review strong,.activation-review small{display:block}.activation-review span{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.06em}.activation-review strong{margin-top:4px;font-size:12px}.activation-review small{margin-top:3px;color:var(--muted);font-size:9.6px}.confirmation{display:flex!important;align-items:flex-start;gap:8px}.confirmation input{flex:none;width:auto!important;margin-top:2px}.cutover-note{margin:12px 0;padding:9px 11px;border-radius:7px;background:#fff7df;color:#765410;font-size:10.8px} .event-dialog label{display:block;margin-top:13px}.event-dialog label[hidden]{display:none}.event-dialog label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.event-dialog input,.event-dialog select,.event-dialog textarea{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:14.4px}.event-dialog textarea{resize:vertical}.commit-dialog .form-grid label.full{grid-column:1/-1}.workflow-preview{margin-top:14px;padding:0 12px;border-radius:7px;background:var(--surface-soft)}.workflow-preview:not(:empty){padding-top:10px;padding-bottom:10px}.workflow-preview strong,.workflow-preview p{display:block;margin:0}.workflow-preview p{margin-top:5px;color:var(--muted);font-size:12px;line-height:1.5}.event-dialog-steps{display:grid;gap:6px;margin-top:15px;padding:10px;background:var(--surface-soft);border-radius:7px}.event-dialog-steps strong,.event-dialog-steps small{display:block}.event-dialog-steps strong{font-size:12px}.event-dialog-steps small{font-size:9.6px;color:var(--muted);margin-top:2px} .applicability-dialog{width:min(980px,calc(100vw - 30px));max-height:calc(100vh - 32px);border:0;border-radius:12px;padding:0;background:var(--panel);color:var(--ink);box-shadow:0 25px 80px rgba(0,0,24,.28)}.applicability-dialog form{padding:23px}.applicability-dialog form>p{color:var(--muted);font-size:13.2px}.applicability-dialog form>.applicability-baseline-note{padding:10px 12px;border-radius:7px;background:var(--accent-soft);color:var(--ink)}.review-context label>span{display:block;font-size:12px;font-weight:720;margin-bottom:6px}.review-context input,.review-context select,.applicability-row input,.applicability-row select{width:100%;min-height:38px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:8px 9px;font-size:13.2px}.review-context label.full{grid-column:1/-1}.applicability-rows{display:grid;gap:7px;max-height:46vh;overflow:auto;margin-top:16px;padding-right:4px}.applicability-row{display:grid;grid-template-columns:minmax(210px,1fr) 180px minmax(240px,1.3fr);gap:9px;align-items:center;padding:9px;border:1px solid var(--line);border-radius:8px}.applicability-row strong,.applicability-row small{display:block}.applicability-row small{margin-top:3px;color:var(--muted);font-size:10.8px}.applicability-row .applicability-constraint{color:var(--accent);line-height:1.35} -.object-value-list{display:grid;gap:7px}.object-value{min-width:0;padding:7px 8px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.object-value-facts{display:flex;flex-wrap:wrap;gap:5px 10px}.object-value-facts>span{display:flex;align-items:baseline;gap:5px;min-width:0;font-size:11px}.object-value-facts b{color:var(--muted);font-size:8.8px;text-transform:uppercase;letter-spacing:.05em}.object-value>small{display:block;margin-top:6px;color:var(--muted);font-size:10px;line-height:1.45}.object-value-list.compact .object-value{padding:0;border:0;background:none}.object-value-list.compact .object-value-facts{display:grid;gap:5px}.object-value-list.compact .object-value-facts>span{display:grid;gap:2px;font-size:10px} +.object-value-list{display:grid;gap:7px}.object-value{min-width:0;padding:7px 8px;border:1px solid var(--line);border-radius:7px;background:var(--surface-soft)}.object-value-facts{display:flex;flex-wrap:wrap;gap:5px 10px}.object-value-facts>.object-value-fact{display:flex;align-items:baseline;gap:5px;min-width:0;font-size:11px}.object-value-fact:has(>.object-value-list){display:grid;flex-basis:100%;align-items:start}.object-value-facts b{color:var(--muted);font-size:8.8px;text-transform:uppercase;letter-spacing:.05em}.object-value>small{display:block;margin-top:6px;color:var(--muted);font-size:10px;line-height:1.45}.object-value-list.compact .object-value{padding:0;border:0;background:none}.object-value-list.compact .object-value-facts{display:grid;gap:5px}.object-value-list.compact .object-value-facts>.object-value-fact{display:grid;gap:2px;font-size:10px} .resource-review-criteria.compact{margin:12px 0;padding:10px 12px;background:var(--surface-soft)}.resource-review-criteria.compact summary{cursor:pointer;font-size:11px;font-weight:750}.resource-review-criteria.compact ul{margin-bottom:0}.evidence-map-more{justify-self:center;margin:2px 0 8px} .packet-builder form{display:grid;grid-template-columns:repeat(3,minmax(0,1fr)) auto;gap:12px;align-items:end}.packet-builder label>span{display:block;font-size:10.8px;font-weight:720;margin-bottom:6px}.packet-builder input,.packet-builder select{width:100%;min-height:40px;border:1px solid var(--line);border-radius:7px;background:var(--field);color:var(--ink);padding:9px 10px;font-size:13.2px}.packet-note,.packet-output>p{font-size:12px;color:var(--muted);margin:12px 0 0}.packet-output{margin:14px 0}.packet-output h3{overflow-wrap:anywhere}.packet-gaps{display:grid}.packet-gaps>div{display:grid;grid-template-columns:max-content 1fr;align-items:start;gap:10px;border-top:1px solid var(--line);padding:10px 0}.packet-gaps>div:first-child{border-top:0}.packet-gaps .badge{display:inline-flex;align-items:center;justify-content:center;line-height:1.2}.packet-gaps p{font-size:12px;margin:0}.packet-list{display:grid}.packet-list a{display:block;text-decoration:none;border-top:1px solid var(--line);padding:9px 0}.packet-list a:first-child{border-top:0}.packet-list strong,.packet-list small{display:block}.packet-list strong{font-size:12px}.packet-list small{font-size:9.6px;color:var(--muted);margin-top:2px} .packet-preflight{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px;margin-bottom:12px}.packet-preflight a{display:flex;align-items:center;gap:9px;padding:10px 12px;border:1px solid var(--line);border-radius:8px;background:var(--panel);text-decoration:none}.packet-preflight small,.packet-preflight strong{display:block}.packet-preflight small{color:var(--muted);font-size:9.6px;text-transform:uppercase;letter-spacing:.07em}.packet-preflight strong{margin-top:3px;font-size:12px}.audit-evidence-paths{margin-bottom:12px}.audit-evidence-paths .panel-head p:not(.kicker){margin:4px 0 0;color:var(--muted);font-size:10.8px}.audit-evidence-path-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:12px}.audit-evidence-path-grid>a{display:block;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--surface-soft);text-decoration:none}.audit-evidence-path-grid h4{margin:7px 0 5px;font-size:13.2px}.audit-evidence-path-grid p{margin:0;color:var(--muted);font-size:10.8px;line-height:1.55} diff --git a/packages/filegrc/test/render.test.js b/packages/filegrc/test/render.test.js index 19675aa..4063876 100644 --- a/packages/filegrc/test/render.test.js +++ b/packages/filegrc/test/render.test.js @@ -1546,6 +1546,35 @@ test("renders nested relationship values without exposing raw JSON", () => { assert.match(APP_STYLES, /\.object-value-list\{display:grid;gap:7px\}/); }); +test("nested model detail renders account decisions, typed facts and escaped text recursively", async () => { + const source = APP_SCRIPT.slice(APP_SCRIPT.indexOf("function formatObjectArray"), APP_SCRIPT.indexOf("function workQueueScheduleStatus")); + const model = JSON.parse(await readFile(new URL("../model/v13.json", import.meta.url), "utf8")); + const escape = (value) => String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll('"', """); + const context = { + state: { model }, esc: escape, humanize: value => value, properCase: value => value, + pluralize: value => value, formatReference: value => '' + escape(value) + '', + formatCalendarDate: value => 'date:' + value, formatLocalDateTime: value => 'time:' + value + }; + const rows = [{ componentId: "component-provider", boundary: "direct", populationEvidenceId: "evidence-population", accounts: [ + { accountReference: "row-1