@growthub/cli 0.9.11 → 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,7 @@ import {
57
57
  wrapWorkspaceTemplateExport
58
58
  } from "@/lib/workspace-schema";
59
59
  import { governedWorkspaceIntegrationCatalog } from "@/lib/domain/integrations";
60
+ import { listWorkspaceDataModelTables } from "@/lib/workspace-data-model";
60
61
 
61
62
  const DEFAULT_CHART_TYPE = "bar-vertical";
62
63
  const DEFAULT_FILTER_OP = "and";
@@ -65,6 +66,7 @@ const DEFAULT_SORT_DIRECTION = "asc";
65
66
  const SUB_PANEL_ROOT = "root";
66
67
  const MANAGED_INTEGRATION_SOURCE_TYPE = "managed-integrations";
67
68
  const CUSTOM_API_SOURCE_TYPE = "custom-api-webhooks";
69
+ const DATA_MODEL_SOURCE_TYPE = "workspace-data-model";
68
70
 
69
71
  const SOURCE_TYPE_OBJECTS = [
70
72
  {
@@ -147,7 +149,7 @@ function generateId(prefix) {
147
149
  function defaultTitleFor(kind) {
148
150
  switch (kind) {
149
151
  case "chart": return "Untitled chart";
150
- case "view": return "Companies";
152
+ case "view": return "Untitled view";
151
153
  case "iframe": return "Untitled iFrame";
152
154
  case "rich-text": return "Untitled Rich Text";
153
155
  default: return "Untitled widget";
@@ -641,6 +643,7 @@ function getChartStyle(widget) {
641
643
 
642
644
  function summarizeSource(widget) {
643
645
  const binding = widget?.config?.binding;
646
+ if (binding?.sourceType === DATA_MODEL_SOURCE_TYPE) return binding.source || widget?.config?.source || "Data Model object";
644
647
  if (binding?.mode === "integration") {
645
648
  const source = binding.source || "Integration";
646
649
  if (binding.entityLabel) return `${source} · ${binding.entityLabel}`;
@@ -652,6 +655,7 @@ function summarizeSource(widget) {
652
655
  }
653
656
 
654
657
  function summarizeSourceType(binding) {
658
+ if (binding?.sourceType === DATA_MODEL_SOURCE_TYPE) return "Data Model";
655
659
  if (binding?.sourceType === CUSTOM_API_SOURCE_TYPE) return "Custom APIs/Webhooks";
656
660
  if (binding?.mode === "integration" || binding?.sourceType === MANAGED_INTEGRATION_SOURCE_TYPE) return "Managed Integrations";
657
661
  return "Static data";
@@ -663,6 +667,27 @@ function resolveBindingSourceType(binding) {
663
667
  return "static";
664
668
  }
665
669
 
670
+ function resolveDataModelTable(dataModelTables, binding) {
671
+ if (binding?.sourceType !== DATA_MODEL_SOURCE_TYPE) return null;
672
+ const tables = Array.isArray(dataModelTables) ? dataModelTables : [];
673
+ return tables.find((table) => table.objectId === binding.objectId || table.id === binding.objectId || table.source === binding.source) || null;
674
+ }
675
+
676
+ function resolveViewWidget(widget, dataModelTables) {
677
+ if (widget?.kind !== "view") return widget;
678
+ const table = resolveDataModelTable(dataModelTables, widget.config?.binding);
679
+ if (!table) return widget;
680
+ return {
681
+ ...widget,
682
+ config: {
683
+ ...(widget.config || {}),
684
+ source: table.source,
685
+ columns: table.columns,
686
+ rows: table.rows
687
+ }
688
+ };
689
+ }
690
+
666
691
  function summarizeFields(widget) {
667
692
  const total = getColumnList(widget).length;
668
693
  const hidden = getHiddenColumnSet(widget).size;
@@ -1061,7 +1086,7 @@ function EntitySelector({ integration, entities, selectedEntityId, selectedEntit
1061
1086
  </div>;
1062
1087
  }
1063
1088
 
1064
- function SourceSubPanel({ widget, integrations, onChange, onBack }) {
1089
+ function SourceSubPanel({ widget, integrations, dataModelTables, onChange, onBack }) {
1065
1090
  const binding = widget.config?.binding || {};
1066
1091
  const currentMode = binding.mode || (widget.kind === "view" ? "manual" : "json");
1067
1092
  const activeSourceType = resolveBindingSourceType(binding);
@@ -1070,6 +1095,7 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
1070
1095
  const hasConnectedSource = Boolean(
1071
1096
  binding.integrationId ||
1072
1097
  binding.endpointRef ||
1098
+ binding.sourceType === DATA_MODEL_SOURCE_TYPE ||
1073
1099
  binding.sourceType === MANAGED_INTEGRATION_SOURCE_TYPE ||
1074
1100
  binding.sourceType === CUSTOM_API_SOURCE_TYPE
1075
1101
  );
@@ -1098,6 +1124,16 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
1098
1124
  };
1099
1125
  }, [integrations, laneFilter, query]);
1100
1126
 
1127
+ const availableDataObjects = useMemo(() => {
1128
+ const list = Array.isArray(dataModelTables) ? dataModelTables : [];
1129
+ const trimmed = query.trim().toLowerCase();
1130
+ return list.filter((table) => {
1131
+ if (table.storage !== "manual-object") return false;
1132
+ if (!trimmed) return true;
1133
+ return `${table.label} ${table.source}`.toLowerCase().includes(trimmed);
1134
+ });
1135
+ }, [dataModelTables, query]);
1136
+
1101
1137
  const selectStatic = useCallback(() => {
1102
1138
  if (!confirmSourceChange("Static rows")) return;
1103
1139
  if (widget.kind === "chart") {
@@ -1105,12 +1141,34 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
1105
1141
  } else {
1106
1142
  onChange({
1107
1143
  ...widget.config,
1108
- source: widget.config?.source || "Companies",
1109
- binding: SAMPLE_DATA_BINDINGS.companiesManual
1144
+ source: widget.config?.source || "Static rows",
1145
+ binding: { mode: "manual", source: "Static rows", rows: Array.isArray(widget.config?.rows) ? widget.config.rows : [] }
1110
1146
  });
1111
1147
  }
1112
1148
  }, [confirmSourceChange, onChange, widget.config, widget.kind]);
1113
1149
 
1150
+ const selectDataModelObject = useCallback((table) => {
1151
+ if (!table || !confirmSourceChange(table.label)) return;
1152
+ onChange({
1153
+ ...widget.config,
1154
+ source: table.source,
1155
+ columns: table.columns,
1156
+ rows: [],
1157
+ binding: {
1158
+ mode: "manual",
1159
+ source: table.source,
1160
+ sourceType: DATA_MODEL_SOURCE_TYPE,
1161
+ sourceAuthority: "workspace-config",
1162
+ objectId: table.objectId,
1163
+ rows: []
1164
+ },
1165
+ fieldSettings: {
1166
+ hidden: [],
1167
+ order: table.columns
1168
+ }
1169
+ });
1170
+ }, [confirmSourceChange, onChange, widget.config]);
1171
+
1114
1172
  const selectCustomApi = useCallback(() => {
1115
1173
  if (!confirmSourceChange("Custom APIs/Webhooks")) return;
1116
1174
  onChange({
@@ -1238,6 +1296,27 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
1238
1296
  {activeSourceType === "static" ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
1239
1297
  </button>
1240
1298
  </div>
1299
+ {widget.kind === "view" ? <>
1300
+ <p className="workspace-panel-label">Data Model objects</p>
1301
+ <div className="workspace-source-list">
1302
+ {availableDataObjects.length ? availableDataObjects.map((table) => {
1303
+ const isActive = binding.sourceType === DATA_MODEL_SOURCE_TYPE && binding.objectId === table.objectId;
1304
+ return <button
1305
+ key={table.id}
1306
+ type="button"
1307
+ className={`workspace-source-row${isActive ? " active" : ""}`}
1308
+ onClick={() => selectDataModelObject(table)}
1309
+ >
1310
+ <span className="workspace-source-icon" aria-hidden="true"><Database size={15} /></span>
1311
+ <span className="workspace-source-meta">
1312
+ <strong>{table.label}</strong>
1313
+ <em>{table.columns.length} fields · {table.rows.length} records · workspace config</em>
1314
+ </span>
1315
+ {isActive ? <span className="workspace-source-tick" aria-hidden="true"><Check size={16} strokeWidth={2.4} /></span> : null}
1316
+ </button>;
1317
+ }) : <p className="workspace-entity-empty">No manual Data Model objects yet.</p>}
1318
+ </div>
1319
+ </> : null}
1241
1320
  {Object.entries(groups).map(([lane, items]) => items.length ? <div key={lane}>
1242
1321
  <p className="workspace-panel-label">{lane === "data-source" ? "Data Sources" : "Workspace Tools"}</p>
1243
1322
  <div className="workspace-source-list">
@@ -1289,22 +1368,24 @@ function SourceSubPanel({ widget, integrations, onChange, onBack }) {
1289
1368
  </section>;
1290
1369
  }
1291
1370
 
1292
- function FieldsSubPanel({ widget, onChange, onBack }) {
1293
- const ordered = getOrderedColumns(widget);
1294
- const hidden = getHiddenColumnSet(widget);
1371
+ function FieldsSubPanel({ widget, dataModelTable, onChange, onBack }) {
1372
+ const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
1373
+ const ordered = getOrderedColumns(viewWidget);
1374
+ const hidden = getHiddenColumnSet(viewWidget);
1295
1375
  const visible = ordered.filter((name) => !hidden.has(name));
1296
1376
  const hiddenList = ordered.filter((name) => hidden.has(name));
1297
1377
  const [hiddenOpen, setHiddenOpen] = useState(true);
1298
1378
  const [draftField, setDraftField] = useState("");
1299
1379
  const move = (fieldId, direction) => {
1300
- const next = reorderColumn(widget, fieldId, direction);
1380
+ const next = reorderColumn(viewWidget, fieldId, direction);
1301
1381
  onChange({ ...widget.config, fieldSettings: next });
1302
1382
  };
1303
1383
  const toggle = (fieldId) => {
1304
- const next = toggleColumnHidden(widget, fieldId);
1384
+ const next = toggleColumnHidden(viewWidget, fieldId);
1305
1385
  onChange({ ...widget.config, fieldSettings: next });
1306
1386
  };
1307
1387
  const removeColumn = (fieldId) => {
1388
+ if (dataModelTable) return;
1308
1389
  const nextColumns = ordered.filter((name) => name !== fieldId);
1309
1390
  const fs = widget.config?.fieldSettings || {};
1310
1391
  onChange({
@@ -1317,6 +1398,7 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
1317
1398
  });
1318
1399
  };
1319
1400
  const addColumn = () => {
1401
+ if (dataModelTable) return;
1320
1402
  const trimmed = draftField.trim();
1321
1403
  if (!trimmed || ordered.includes(trimmed)) return;
1322
1404
  onChange({ ...widget.config, columns: [...ordered, trimmed] });
@@ -1324,6 +1406,7 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
1324
1406
  };
1325
1407
  return <section className="workspace-widget-subpanel">
1326
1408
  <SubPanelHeader title="Fields" breadcrumb={widget.title} onBack={onBack} />
1409
+ {dataModelTable ? <p className="workspace-panel-hint">This View is bound to a Data Model object. Field order and visibility are widget-local; add or remove object fields on the Data Model page.</p> : null}
1327
1410
  <p className="workspace-panel-label">Visible fields</p>
1328
1411
  <div className="workspace-field-rows">
1329
1412
  {visible.length === 0 ? <p className="workspace-panel-hint">No visible fields. Add one below or unhide an existing field.</p> : null}
@@ -1335,7 +1418,7 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
1335
1418
  <button type="button" aria-label={`Move ${name} up`} disabled={index === 0} onClick={() => move(name, "up")}>↑</button>
1336
1419
  <button type="button" aria-label={`Move ${name} down`} disabled={index === visible.length - 1} onClick={() => move(name, "down")}>↓</button>
1337
1420
  <button type="button" aria-label={`Hide ${name}`} onClick={() => toggle(name)}>👁</button>
1338
- <button type="button" aria-label={`Remove ${name}`} onClick={() => removeColumn(name)}>✕</button>
1421
+ <button type="button" aria-label={`Remove ${name}`} disabled={Boolean(dataModelTable)} onClick={() => removeColumn(name)}>✕</button>
1339
1422
  </span>
1340
1423
  </div>)}
1341
1424
  </div>
@@ -1355,7 +1438,7 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
1355
1438
  <span className="workspace-field-row-name">{name}</span>
1356
1439
  <span className="workspace-field-row-actions">
1357
1440
  <button type="button" aria-label={`Show ${name}`} onClick={() => toggle(name)}>👁</button>
1358
- <button type="button" aria-label={`Remove ${name}`} onClick={() => removeColumn(name)}>✕</button>
1441
+ <button type="button" aria-label={`Remove ${name}`} disabled={Boolean(dataModelTable)} onClick={() => removeColumn(name)}>✕</button>
1359
1442
  </span>
1360
1443
  </div>)}
1361
1444
  </div> : null}
@@ -1372,14 +1455,15 @@ function FieldsSubPanel({ widget, onChange, onBack }) {
1372
1455
  }
1373
1456
  }}
1374
1457
  />
1375
- <button type="button" onClick={addColumn} disabled={!draftField.trim()}>Add</button>
1458
+ <button type="button" onClick={addColumn} disabled={Boolean(dataModelTable) || !draftField.trim()}>Add</button>
1376
1459
  </div>
1377
1460
  </section>;
1378
1461
  }
1379
1462
 
1380
- function SortSubPanel({ widget, onChange, onBack }) {
1463
+ function SortSubPanel({ widget, dataModelTable, onChange, onBack }) {
1464
+ const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
1381
1465
  const sort = getSortClauses(widget);
1382
- const columns = getColumnList(widget);
1466
+ const columns = getColumnList(viewWidget);
1383
1467
  const updateSort = (next) => onChange({ ...widget.config, sort: next });
1384
1468
  const addClause = () => {
1385
1469
  const fieldId = columns[0] || "";
@@ -1423,12 +1507,13 @@ function SortSubPanel({ widget, onChange, onBack }) {
1423
1507
  </section>;
1424
1508
  }
1425
1509
 
1426
- function FilterSubPanel({ widget, integrations, onChange, onBack }) {
1510
+ function FilterSubPanel({ widget, integrations, dataModelTable, onChange, onBack }) {
1511
+ const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
1427
1512
  const binding = widget.config?.binding || {};
1428
1513
  const filter = getFilterConfig(widget);
1429
1514
  const [entities, setEntities] = useState([]);
1430
1515
  const [entitiesLoading, setEntitiesLoading] = useState(false);
1431
- const fieldChoices = getFilterFieldChoices(widget, entities);
1516
+ const fieldChoices = getFilterFieldChoices(viewWidget, entities);
1432
1517
  const columns = fieldChoices.map((field) => field.id);
1433
1518
  const setFilter = (next) => onChange({ ...widget.config, filter: next });
1434
1519
  const setOp = (op) => setFilter({ ...filter, op });
@@ -2000,7 +2085,7 @@ function WorkspaceSettingsPanel({ config, persistence, adapterConfig, integratio
2000
2085
  Inspect-only. Sourced from <code>growthub.config.json</code> + <code>GET /api/workspace</code>.
2001
2086
  Edit branding by updating <code>growthub.config.json</code> inside your governed fork.
2002
2087
  The builder itself never holds tokens, never executes hosted workflows, and never bypasses the PATCH allowlist
2003
- (<code>dashboards</code>, <code>widgetTypes</code>, <code>canvas</code>).
2088
+ (<code>dashboards</code>, <code>widgetTypes</code>, <code>canvas</code>, <code>dataModel</code>).
2004
2089
  </p>
2005
2090
  <div className="workspace-readiness">
2006
2091
  <article className="workspace-readiness-section">
@@ -2081,7 +2166,7 @@ function WorkspaceManagementPanel({ config, persistence, adapterConfig, onClose
2081
2166
  </article>
2082
2167
  <article className="workspace-readiness-section">
2083
2168
  <h3>API</h3>
2084
- <div className="workspace-readiness-row"><span>PATCH allowlist</span><code>dashboards | widgetTypes | canvas</code></div>
2169
+ <div className="workspace-readiness-row"><span>PATCH allowlist</span><code>dashboards | widgetTypes | canvas | dataModel</code></div>
2085
2170
  <div className="workspace-readiness-row"><span>Unknown field</span><code>400</code></div>
2086
2171
  <div className="workspace-readiness-row"><span>Read-only runtime</span><code>409 + guidance</code></div>
2087
2172
  <div className="workspace-readiness-row"><span>Can save now</span>
@@ -2175,6 +2260,8 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
2175
2260
  const addSlot = dragPreview || selectedPosition;
2176
2261
  const selectedWidget = activeWidgets.find((widget) => widget.id === selectedWidgetId) || null;
2177
2262
  const availableIntegrations = useMemo(() => flattenIntegrationSettings(integrationSettings), [integrationSettings]);
2263
+ const dataModelTables = useMemo(() => listWorkspaceDataModelTables(config), [config]);
2264
+ const selectedResolvedWidget = selectedWidget ? resolveViewWidget(selectedWidget, dataModelTables) : null;
2178
2265
  const branding = config.branding || {};
2179
2266
  const occupiedCells = useMemo(() => {
2180
2267
  const cells = new Set();
@@ -3055,6 +3142,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
3055
3142
  </div>
3056
3143
  <nav className="workspace-nav">
3057
3144
  <button type="button" className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"} onClick={showDashboardHome}>Dashboards</button>
3145
+ <Link href="/data-model">Data Model</Link>
3058
3146
  <Link href="/settings/integrations">Integrations</Link>
3059
3147
  <button type="button" className="workspace-nav-button" onClick={() => setSettingsOpen(true)}>Workspace Settings</button>
3060
3148
  <button type="button" className="workspace-nav-button" onClick={() => setManagementOpen(true)}>Management</button>
@@ -3246,7 +3334,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
3246
3334
  onSelect={() => selectWidget(widget.id)}
3247
3335
  onExpandIframe={setExpandedIframeWidget}
3248
3336
  selected={widget.id === selectedWidgetId}
3249
- widget={widget}
3337
+ widget={resolveViewWidget(widget, dataModelTables)}
3250
3338
  />)}
3251
3339
  </div>
3252
3340
  </section> : null}
@@ -3292,22 +3380,26 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
3292
3380
  {selectedWidget && inspectorPath === "source" ? <SourceSubPanel
3293
3381
  widget={selectedWidget}
3294
3382
  integrations={availableIntegrations}
3383
+ dataModelTables={dataModelTables}
3295
3384
  onChange={replaceSelectedWidgetConfig}
3296
3385
  onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
3297
3386
  /> : null}
3298
3387
  {selectedWidget && inspectorPath === "fields" ? <FieldsSubPanel
3299
3388
  widget={selectedWidget}
3389
+ dataModelTable={resolveDataModelTable(dataModelTables, selectedWidget.config?.binding)}
3300
3390
  onChange={replaceSelectedWidgetConfig}
3301
3391
  onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
3302
3392
  /> : null}
3303
3393
  {selectedWidget && inspectorPath === "sort" ? <SortSubPanel
3304
3394
  widget={selectedWidget}
3395
+ dataModelTable={resolveDataModelTable(dataModelTables, selectedWidget.config?.binding)}
3305
3396
  onChange={replaceSelectedWidgetConfig}
3306
3397
  onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
3307
3398
  /> : null}
3308
3399
  {selectedWidget && inspectorPath === "filter" ? <FilterSubPanel
3309
3400
  widget={selectedWidget}
3310
3401
  integrations={availableIntegrations}
3402
+ dataModelTable={resolveDataModelTable(dataModelTables, selectedWidget.config?.binding)}
3311
3403
  onChange={replaceSelectedWidgetConfig}
3312
3404
  onBack={() => setInspectorPath(SUB_PANEL_ROOT)}
3313
3405
  /> : null}
@@ -3370,7 +3462,11 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
3370
3462
  </small>
3371
3463
  </label> : null}
3372
3464
  {selectedWidget.kind === "view" ? <section className="workspace-field-stack">
3373
- <label>
3465
+ {selectedWidget.config?.binding?.sourceType === DATA_MODEL_SOURCE_TYPE ? <div className="workspace-active-source-state">
3466
+ <span>Data Model object</span>
3467
+ <strong>{summarizeSource(selectedWidget)}</strong>
3468
+ <code>{selectedWidget.config?.binding?.objectId || "workspace-config"}</code>
3469
+ </div> : <label>
3374
3470
  <span>Manual Rows</span>
3375
3471
  <textarea
3376
3472
  value={serializeManualRows(selectedWidget.config?.rows || [], selectedWidget.config?.columns || [])}
@@ -3382,7 +3478,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
3382
3478
  });
3383
3479
  }}
3384
3480
  />
3385
- </label>
3481
+ </label>}
3386
3482
  <div className="workspace-settings-list" role="group" aria-label="View widget settings">
3387
3483
  <p className="workspace-panel-label">Settings</p>
3388
3484
  <button type="button" className="workspace-settings-row" disabled>
@@ -3392,13 +3488,13 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
3392
3488
  <span>Source</span><code>{summarizeSource(selectedWidget)}</code>
3393
3489
  </button>
3394
3490
  <button type="button" className="workspace-settings-row" onClick={() => setInspectorPath("fields")}>
3395
- <span>Fields</span><code>{summarizeFields(selectedWidget)}</code>
3491
+ <span>Fields</span><code>{summarizeFields(selectedResolvedWidget || selectedWidget)}</code>
3396
3492
  </button>
3397
3493
  <button type="button" className="workspace-settings-row" onClick={() => setInspectorPath("filter")}>
3398
- <span>Filter</span><code>{summarizeFilter(selectedWidget)}</code>
3494
+ <span>Filter</span><code>{summarizeFilter(selectedResolvedWidget || selectedWidget)}</code>
3399
3495
  </button>
3400
3496
  <button type="button" className="workspace-settings-row" onClick={() => setInspectorPath("sort")}>
3401
- <span>Sort</span><code>{summarizeSort(selectedWidget)}</code>
3497
+ <span>Sort</span><code>{summarizeSort(selectedResolvedWidget || selectedWidget)}</code>
3402
3498
  </button>
3403
3499
  </div>
3404
3500
  </section> : null}
@@ -93,6 +93,7 @@ function applyPatch(currentConfig, patch) {
93
93
  const next = { ...currentConfig };
94
94
  if (patch.dashboards !== undefined) next.dashboards = patch.dashboards;
95
95
  if (patch.widgetTypes !== undefined) next.widgetTypes = patch.widgetTypes;
96
+ if (patch.dataModel !== undefined) next.dataModel = patch.dataModel;
96
97
  if (patch.canvas !== undefined && patch.canvas !== null) {
97
98
  const patchCanvas = { ...patch.canvas };
98
99
  if (Array.isArray(patchCanvas.tabs)) {
@@ -140,7 +141,8 @@ async function writeWorkspaceConfig(patch) {
140
141
  validateWorkspaceConfig({
141
142
  dashboards: next.dashboards,
142
143
  widgetTypes: next.widgetTypes,
143
- canvas: next.canvas
144
+ canvas: next.canvas,
145
+ dataModel: next.dataModel
144
146
  });
145
147
  const configPath = resolveWorkspaceConfigPath();
146
148
  const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());