@growthub/cli 0.13.5 → 0.13.7

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.
Files changed (39) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/QUICKSTART.md +19 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +172 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +531 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +274 -18
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +62 -7
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +554 -48
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +24 -14
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +2 -2
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +534 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +3 -1
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +82 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
  38. package/dist/index.js +127 -44
  39. package/package.json +1 -1
@@ -29,6 +29,7 @@ import {
29
29
  GitBranch,
30
30
  Hash,
31
31
  Home,
32
+ Hourglass,
32
33
  Import,
33
34
  Italic,
34
35
  Layers,
@@ -85,8 +86,10 @@ import {
85
86
  deriveWidgetDependencyContract
86
87
  } from "@/lib/workspace-chart-values";
87
88
  import { selectObjectFilterableFields, selectObjectSortableFields } from "@/lib/workspace-metadata-selectors";
89
+ import { deriveWorkspaceActivationState } from "@/lib/workspace-activation";
88
90
  import { HelperSidecar } from "./data-model/components/HelperSidecar.jsx";
89
91
  import { WorkspaceRail } from "./workspace-rail.jsx";
92
+ import { WorkspaceActivationPanel } from "./components/WorkspaceActivationPanel.jsx";
90
93
 
91
94
  // Workspace Metadata Graph V1 — typed dependency contracts.
92
95
  // Used by sidecar dependency summaries; the existing chart hydration path
@@ -110,6 +113,7 @@ const DATA_MODEL_SOURCE_TYPE = "workspace-data-model";
110
113
  const LIVE_SOURCE_TYPE = "workspace-source-records";
111
114
  const TESTED_SOURCE_STATUSES = new Set(["connected", "approved", "ok", "success"]);
112
115
  const HIDDEN_SANDBOX_OBJECT_IDS = new Set(["workspace-helper-sandbox"]);
116
+ const WORKSPACE_UI_CACHE_OBJECT_ID = "workspace-ui-cache";
113
117
 
114
118
  const SOURCE_TYPE_OBJECTS = [
115
119
  {
@@ -173,6 +177,26 @@ const CHART_TYPE_ICONS = {
173
177
 
174
178
  const VISIBLE_CHART_TYPES = KNOWN_CHART_TYPES.filter((type) => type !== "line");
175
179
 
180
+ const TABLE_VIEW_TYPES = ["gantt", "board", "calendar", "timeline"];
181
+ const TABLE_VIEW_LABELS = {
182
+ gantt: "Gantt",
183
+ board: "Board",
184
+ calendar: "Calendar",
185
+ timeline: "Timeline"
186
+ };
187
+ const TABLE_VIEW_HELP = {
188
+ gantt: "Track dependencies and baselines",
189
+ board: "Track work in a Kanban view",
190
+ calendar: "Plan weekly or monthly work",
191
+ timeline: "Schedule work over time"
192
+ };
193
+ const TABLE_VIEW_ICONS = {
194
+ gantt: GitBranch,
195
+ board: Columns3,
196
+ calendar: Calendar,
197
+ timeline: Rows3
198
+ };
199
+
176
200
  // User-facing labels for the Twenty-style Y-axis operation dropdown.
177
201
  // Keys must stay in sync with `lib/workspace-chart-values.js#KNOWN_AGGREGATIONS`
178
202
  // and the validator in `lib/workspace-schema.js`.
@@ -306,7 +330,7 @@ const GRID_COLUMNS = 12;
306
330
  const GRID_ROWS = 16;
307
331
  const GRID_CELL_COUNT = GRID_COLUMNS * GRID_ROWS;
308
332
  const DEFAULT_TAB_ID = "tab-default";
309
- const COLLAPSED_GRID_COLUMNS = "264px minmax(0, 1fr)";
333
+ const COLLAPSED_GRID_COLUMNS = "var(--workspace-rail-width, 264px) minmax(0, 1fr)";
310
334
 
311
335
  function generateId(prefix) {
312
336
  if (typeof globalThis !== "undefined" && globalThis.crypto?.randomUUID) {
@@ -407,6 +431,44 @@ function getWorkflowSandboxObject(config) {
407
431
  }) || null;
408
432
  }
409
433
 
434
+ function getWorkspaceUiCache(config) {
435
+ const object = getDataModelObject(config, WORKSPACE_UI_CACHE_OBJECT_ID);
436
+ const row = Array.isArray(object?.rows) ? object.rows.find((entry) => entry?.id === "activation") : null;
437
+ return row && typeof row === "object" ? row : {};
438
+ }
439
+
440
+ function setWorkspaceUiCacheFlag(config, key, value) {
441
+ const dataModel = config?.dataModel && typeof config.dataModel === "object" ? config.dataModel : {};
442
+ const objects = Array.isArray(dataModel.objects) ? dataModel.objects : [];
443
+ const cacheObject = objects.find((object) => object?.id === WORKSPACE_UI_CACHE_OBJECT_ID) || {
444
+ id: WORKSPACE_UI_CACHE_OBJECT_ID,
445
+ label: "Workspace UI Cache",
446
+ source: "Workspace UI Cache",
447
+ objectType: "custom",
448
+ icon: "Settings",
449
+ columns: ["id", key],
450
+ rows: [],
451
+ binding: { mode: "manual", source: "Workspace UI Cache" }
452
+ };
453
+ const columns = Array.from(new Set([...(Array.isArray(cacheObject.columns) ? cacheObject.columns : ["id"]), key]));
454
+ const rows = Array.isArray(cacheObject.rows) ? cacheObject.rows : [];
455
+ const hasActivationRow = rows.some((row) => row?.id === "activation");
456
+ const nextRows = hasActivationRow
457
+ ? rows.map((row) => row?.id === "activation" ? { ...row, [key]: value } : row)
458
+ : [...rows, { id: "activation", [key]: value }];
459
+ const nextCacheObject = { ...cacheObject, columns, rows: nextRows };
460
+ const nextObjects = objects.some((object) => object?.id === WORKSPACE_UI_CACHE_OBJECT_ID)
461
+ ? objects.map((object) => object?.id === WORKSPACE_UI_CACHE_OBJECT_ID ? nextCacheObject : object)
462
+ : [...objects, nextCacheObject];
463
+ return {
464
+ ...config,
465
+ dataModel: {
466
+ ...dataModel,
467
+ objects: nextObjects
468
+ }
469
+ };
470
+ }
471
+
410
472
  function listBuilderWorkflowItems(config) {
411
473
  const navFolders = getDataModelObject(config, "nav-folders");
412
474
  const rows = Array.isArray(navFolders?.rows) ? navFolders.rows : [];
@@ -476,11 +538,76 @@ function updateWorkflowFolderItemInConfig(config, workflow, updater) {
476
538
  }
477
539
 
478
540
  function createBlankWorkflowSandboxRow(rowId, nowIso) {
541
+ const registryId = "growthub-workspace-smoke-api";
479
542
  const draftGraph = JSON.stringify({
480
- version: "0",
543
+ version: "1",
481
544
  provider: "growthub-native",
482
- nodes: [],
483
- edges: []
545
+ nodes: [
546
+ {
547
+ id: "input",
548
+ type: "input",
549
+ label: "Input",
550
+ subtitle: "Manual or source payload",
551
+ config: { inputMode: "manual", samplePayload: {}, sourceType: "", sourceId: "", entityId: "", filterMode: "and", filters: [] }
552
+ },
553
+ {
554
+ id: "api-request",
555
+ type: "api-registry-call",
556
+ label: "API Registry",
557
+ subtitle: `${registryId} · GET /api/workspace`,
558
+ config: {
559
+ registryId,
560
+ integrationId: registryId,
561
+ baseUrl: "http://localhost:3000",
562
+ endpoint: "/api/workspace",
563
+ method: "GET",
564
+ authRef: "",
565
+ queryParams: {},
566
+ bodyTemplate: "",
567
+ requestHeadersMetadata: { authHeaderName: "x-api-key", authPrefix: "", contentType: "" },
568
+ timeoutMs: 30000
569
+ }
570
+ },
571
+ {
572
+ id: "transform",
573
+ type: "transform-filter",
574
+ label: "Transform",
575
+ subtitle: "Map fields and filter rows",
576
+ config: {
577
+ rootPath: "",
578
+ mode: "json",
579
+ responseMode: "json",
580
+ fieldMap: {},
581
+ includeFields: [],
582
+ excludeFields: [],
583
+ computedFields: {},
584
+ filters: [],
585
+ filterMode: "and",
586
+ maxRows: 0
587
+ }
588
+ },
589
+ {
590
+ id: "result",
591
+ type: "tool-result",
592
+ label: "Result",
593
+ subtitle: "Save status and response",
594
+ config: {
595
+ successStatusCodes: [200],
596
+ writeLastResponse: true,
597
+ writeSourceRecord: true,
598
+ sourceRecordId: "",
599
+ outputMode: "normalized-json",
600
+ previewFields: [],
601
+ statusField: "status",
602
+ lastTestedField: "lastTested"
603
+ }
604
+ }
605
+ ],
606
+ edges: [
607
+ { from: "input", to: "api-request", passes: "payload, filters, variables" },
608
+ { from: "api-request", to: "transform", passes: "provider-response" },
609
+ { from: "transform", to: "result", passes: "normalized-output" }
610
+ ]
484
611
  }, null, 2);
485
612
  return {
486
613
  Name: rowId,
@@ -520,11 +647,77 @@ function createBlankWorkflowSandboxRow(rowId, nowIso) {
520
647
  };
521
648
  }
522
649
 
650
+ function createWorkflowApiRegistryObject() {
651
+ const preset = OBJECT_TYPE_PRESETS["api-registry"] || {};
652
+ const columns = Array.isArray(preset.columns) ? [...preset.columns] : ["integrationId"];
653
+ return {
654
+ id: "workflow-api-registry",
655
+ label: preset.label || "API Registry",
656
+ source: preset.label || "API Registry",
657
+ objectType: "api-registry",
658
+ icon: preset.icon || "Code2",
659
+ columns,
660
+ rows: [
661
+ {
662
+ integrationId: "growthub-workspace-smoke-api",
663
+ authRef: "",
664
+ baseUrl: "http://localhost:3000",
665
+ endpoint: "/api/workspace",
666
+ method: "GET",
667
+ status: "draft",
668
+ lastTested: "",
669
+ lastResponse: "",
670
+ entityTypes: "workspace",
671
+ description: "Local workspace smoke endpoint for first workflow setup.",
672
+ connectorKind: "custom-http",
673
+ resolverTemplateId: "",
674
+ schemaVersion: "1",
675
+ capabilities: "read",
676
+ executionLane: "sandbox-local"
677
+ }
678
+ ],
679
+ binding: { mode: "manual", source: "Data Model" },
680
+ relations: Array.isArray(preset.relations) ? preset.relations.map((relation) => ({ ...relation })) : [],
681
+ fieldSettings: { hidden: [], order: columns }
682
+ };
683
+ }
684
+
685
+ function createWorkflowSandboxObject() {
686
+ const preset = OBJECT_TYPE_PRESETS["sandbox-environment"] || {};
687
+ const columns = Array.isArray(preset.columns) ? [...preset.columns] : ["Name"];
688
+ return {
689
+ id: "sandbox-environments",
690
+ label: preset.label || "Sandbox Environments",
691
+ source: preset.label || "Sandbox Environments",
692
+ objectType: "sandbox-environment",
693
+ icon: preset.icon || "Terminal",
694
+ columns,
695
+ rows: [],
696
+ binding: { mode: "manual", source: "Data Model" },
697
+ relations: Array.isArray(preset.relations) ? preset.relations.map((relation) => ({ ...relation })) : [],
698
+ fieldSettings: { hidden: [], order: columns }
699
+ };
700
+ }
701
+
523
702
  function addWorkflowFolderShortcut(dataModel, workflow) {
524
703
  const objects = Array.isArray(dataModel?.objects) ? dataModel.objects : [];
525
- const navIndex = objects.findIndex((object) => object?.id === "nav-folders");
526
- if (navIndex < 0) return dataModel;
527
- const navObject = objects[navIndex];
704
+ const seededObjects = objects.some((object) => object?.id === "nav-folders")
705
+ ? objects
706
+ : [
707
+ ...objects,
708
+ {
709
+ id: "nav-folders",
710
+ label: "Custom Folders",
711
+ source: "Custom Folders",
712
+ objectType: "custom",
713
+ icon: "Folder",
714
+ columns: ["name", "order", "collapsed", "items"],
715
+ rows: [],
716
+ binding: { mode: "manual", source: "Custom Folders" }
717
+ }
718
+ ];
719
+ const navIndex = seededObjects.findIndex((object) => object?.id === "nav-folders");
720
+ const navObject = seededObjects[navIndex];
528
721
  const rows = Array.isArray(navObject.rows) ? navObject.rows : [];
529
722
  const folderName = "Builder";
530
723
  const existingFolder = rows.find((row) => String(row?.name || "").trim().toLowerCase() === folderName.toLowerCase());
@@ -562,7 +755,7 @@ function addWorkflowFolderShortcut(dataModel, workflow) {
562
755
  ];
563
756
  return {
564
757
  ...dataModel,
565
- objects: objects.map((object, index) => index === navIndex ? { ...navObject, rows: nextRows } : object)
758
+ objects: seededObjects.map((object, index) => index === navIndex ? { ...navObject, rows: nextRows } : object)
566
759
  };
567
760
  }
568
761
 
@@ -939,6 +1132,43 @@ function getVisibleColumns(widget) {
939
1132
  return ordered.filter((name) => !hidden.has(name));
940
1133
  }
941
1134
 
1135
+ function getTableViewSettings(widget) {
1136
+ const settings = widget?.config?.fieldSettings?.tableView;
1137
+ return isPlainConfigObject(settings) ? settings : {};
1138
+ }
1139
+
1140
+ function getTableViewType(widget) {
1141
+ const type = getTableViewSettings(widget).type;
1142
+ return TABLE_VIEW_TYPES.includes(type) ? type : "";
1143
+ }
1144
+
1145
+ function getFieldValue(row, field, fallback = "") {
1146
+ if (!row || !field) return fallback;
1147
+ const value = row[field];
1148
+ if (value === null || value === undefined || value === "") return fallback;
1149
+ return String(value);
1150
+ }
1151
+
1152
+ function firstMatchingField(columns, candidates) {
1153
+ const lower = columns.map((column) => [column, String(column).toLowerCase()]);
1154
+ for (const candidate of candidates) {
1155
+ const found = lower.find(([, name]) => name.includes(candidate));
1156
+ if (found) return found[0];
1157
+ }
1158
+ return columns[0] || "";
1159
+ }
1160
+
1161
+ function resolveTableViewFields(widget) {
1162
+ const columns = getVisibleColumns(widget);
1163
+ const settings = getTableViewSettings(widget);
1164
+ return {
1165
+ titleField: settings.titleField || firstMatchingField(columns, ["title", "name", "task", "project"]),
1166
+ statusField: settings.statusField || firstMatchingField(columns, ["status", "stage", "state", "lane"]),
1167
+ startDateField: settings.startDateField || firstMatchingField(columns, ["start", "created", "date"]),
1168
+ endDateField: settings.endDateField || firstMatchingField(columns, ["end", "due", "deadline", "target"]),
1169
+ };
1170
+ }
1171
+
942
1172
  function withFieldSettings(config, patch) {
943
1173
  const current = isPlainConfigObject(config?.fieldSettings) ? config.fieldSettings : { hidden: [], order: [] };
944
1174
  return {
@@ -951,6 +1181,21 @@ function withFieldSettings(config, patch) {
951
1181
  };
952
1182
  }
953
1183
 
1184
+ function withTableViewSettings(config, patch) {
1185
+ const current = isPlainConfigObject(config?.fieldSettings) ? config.fieldSettings : {};
1186
+ const currentTableView = isPlainConfigObject(current.tableView) ? current.tableView : {};
1187
+ return {
1188
+ ...config,
1189
+ fieldSettings: {
1190
+ ...current,
1191
+ tableView: {
1192
+ ...currentTableView,
1193
+ ...patch
1194
+ }
1195
+ }
1196
+ };
1197
+ }
1198
+
954
1199
  function isPlainConfigObject(value) {
955
1200
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
956
1201
  }
@@ -1115,6 +1360,10 @@ function summarizeFields(widget) {
1115
1360
  return hidden ? `${total - hidden} of ${total} shown` : `${total} shown`;
1116
1361
  }
1117
1362
 
1363
+ function summarizeTableView(widget) {
1364
+ return TABLE_VIEW_LABELS[getTableViewType(widget)] || "Table";
1365
+ }
1366
+
1118
1367
  function summarizeSort(widget) {
1119
1368
  const sort = getSortClauses(widget);
1120
1369
  if (!sort.length) return "›";
@@ -2633,6 +2882,105 @@ function SortSubPanel({ widget, dataModelTable, onChange, onBack }) {
2633
2882
  </section>;
2634
2883
  }
2635
2884
 
2885
+ function TableViewConfig({ widget, dataModelTable, onChange, onSubPage }) {
2886
+ const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
2887
+ const columns = getVisibleColumns(viewWidget);
2888
+ const tableView = getTableViewSettings(widget);
2889
+ const activeType = getTableViewType(widget);
2890
+ const setTableView = (patch) => onChange(withTableViewSettings(widget.config, patch));
2891
+ const fieldOptions = columns.length ? columns : getColumnList(viewWidget);
2892
+ return (
2893
+ <div className="workspace-twenty-config workspace-table-view-config" role="group" aria-label="Table view widget settings">
2894
+ <p className="workspace-panel-label">Settings</p>
2895
+ <WidgetSettingsRow icon={Table2} label="Layout" value={summarizeTableView(widget)} disabled />
2896
+ <div className="workspace-chart-type-tabs workspace-table-view-tabs" role="tablist" aria-label="Table view type">
2897
+ {TABLE_VIEW_TYPES.map((type) => {
2898
+ const TypeIcon = TABLE_VIEW_ICONS[type];
2899
+ return (
2900
+ <button
2901
+ key={type}
2902
+ type="button"
2903
+ role="tab"
2904
+ aria-selected={activeType === type}
2905
+ className={activeType === type ? "active" : ""}
2906
+ onClick={() => setTableView({ type: activeType === type ? "" : type })}
2907
+ title={TABLE_VIEW_HELP[type]}
2908
+ >
2909
+ <IconGlyph icon={TypeIcon} size={17} />
2910
+ <em>{TABLE_VIEW_LABELS[type]}</em>
2911
+ </button>
2912
+ );
2913
+ })}
2914
+ </div>
2915
+ {activeType ? <p className="workspace-panel-hint">{TABLE_VIEW_HELP[activeType]}</p> : null}
2916
+
2917
+ <WidgetSettingsRow icon={Box} label="Source" value={summarizeSource(widget)} onClick={() => onSubPage("source")} />
2918
+ <WidgetSettingsRow icon={List} label="Fields" value={summarizeFields(viewWidget)} onClick={() => onSubPage("fields")} />
2919
+ <WidgetSettingsRow icon={Filter} label="Filter" value={summarizeFilter(viewWidget)} onClick={() => onSubPage("filter")} />
2920
+ <WidgetSettingsRow icon={SlidersHorizontal} label="Sort" value={summarizeSort(viewWidget)} onClick={() => onSubPage("sort")} />
2921
+
2922
+ {activeType ? (
2923
+ <>
2924
+ <p className="workspace-panel-label">View fields</p>
2925
+ <WidgetSelectRow icon={Type} label="Title">
2926
+ <FieldDropdown
2927
+ fields={fieldOptions}
2928
+ value={tableView.titleField || ""}
2929
+ onChange={(field) => setTableView({ titleField: field })}
2930
+ placeholder="Auto"
2931
+ disabled={!fieldOptions.length}
2932
+ />
2933
+ </WidgetSelectRow>
2934
+ {activeType === "board" ? (
2935
+ <WidgetSelectRow icon={Columns3} label="Status">
2936
+ <FieldDropdown
2937
+ fields={fieldOptions}
2938
+ value={tableView.statusField || ""}
2939
+ onChange={(field) => setTableView({ statusField: field })}
2940
+ placeholder="Auto"
2941
+ disabled={!fieldOptions.length}
2942
+ />
2943
+ </WidgetSelectRow>
2944
+ ) : null}
2945
+ {activeType === "gantt" || activeType === "timeline" ? (
2946
+ <>
2947
+ <WidgetSelectRow icon={Calendar} label="Start">
2948
+ <FieldDropdown
2949
+ fields={fieldOptions}
2950
+ value={tableView.startDateField || ""}
2951
+ onChange={(field) => setTableView({ startDateField: field })}
2952
+ placeholder="Auto"
2953
+ disabled={!fieldOptions.length}
2954
+ />
2955
+ </WidgetSelectRow>
2956
+ <WidgetSelectRow icon={Calendar} label="End">
2957
+ <FieldDropdown
2958
+ fields={fieldOptions}
2959
+ value={tableView.endDateField || ""}
2960
+ onChange={(field) => setTableView({ endDateField: field })}
2961
+ placeholder="Auto"
2962
+ disabled={!fieldOptions.length}
2963
+ />
2964
+ </WidgetSelectRow>
2965
+ </>
2966
+ ) : null}
2967
+ {activeType === "calendar" ? (
2968
+ <WidgetSelectRow icon={Calendar} label="Date">
2969
+ <FieldDropdown
2970
+ fields={fieldOptions}
2971
+ value={tableView.startDateField || ""}
2972
+ onChange={(field) => setTableView({ startDateField: field })}
2973
+ placeholder="Auto"
2974
+ disabled={!fieldOptions.length}
2975
+ />
2976
+ </WidgetSelectRow>
2977
+ ) : null}
2978
+ </>
2979
+ ) : null}
2980
+ </div>
2981
+ );
2982
+ }
2983
+
2636
2984
  function FilterSubPanel({ widget, integrations, dataModelTable, adapterConfig, onRefreshAndSave, onChange, onBack }) {
2637
2985
  const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
2638
2986
  const binding = widget.config?.binding || {};
@@ -3374,6 +3722,55 @@ function IframePreviewModal({ widget, onClose }) {
3374
3722
  </div>;
3375
3723
  }
3376
3724
 
3725
+ function TableTransformPreview({ widget, columns, rows }) {
3726
+ const type = getTableViewType(widget);
3727
+ const safeRows = Array.isArray(rows) ? rows : [];
3728
+ if (!type) {
3729
+ return <div
3730
+ className="workspace-view-table"
3731
+ aria-label={`${widget.title} preview`}
3732
+ style={{ "--workspace-view-columns": columns.length }}
3733
+ >
3734
+ <div>{columns.map((column) => <span key={column}>{column}</span>)}</div>
3735
+ {safeRows.slice(0, 6).map((row, rowIndex) => <div key={rowIndex}>
3736
+ {columns.map((column) => <span key={column}>{row?.[column] || ""}</span>)}
3737
+ </div>)}
3738
+ {!columns.length && !safeRows.length ? <div className="workspace-view-empty">Select a source</div> : null}
3739
+ <footer>Calculate</footer>
3740
+ </div>;
3741
+ }
3742
+ const fields = resolveTableViewFields(widget);
3743
+ if (type === "board") {
3744
+ const groups = safeRows.slice(0, 8).reduce((acc, row) => {
3745
+ const status = getFieldValue(row, fields.statusField, "Open");
3746
+ if (!acc[status]) acc[status] = [];
3747
+ acc[status].push(row);
3748
+ return acc;
3749
+ }, {});
3750
+ return <div className="workspace-table-transform-preview is-board">
3751
+ {Object.entries(groups).slice(0, 3).map(([status, groupRows]) => <section key={status}>
3752
+ <strong>{status}</strong>
3753
+ {groupRows.slice(0, 3).map((row, index) => <span key={index}>{getFieldValue(row, fields.titleField, `Row ${index + 1}`)}</span>)}
3754
+ </section>)}
3755
+ </div>;
3756
+ }
3757
+ if (type === "calendar") {
3758
+ return <div className="workspace-table-transform-preview is-calendar">
3759
+ {["Mon", "Tue", "Wed", "Thu", "Fri"].map((day, index) => <section key={day}>
3760
+ <strong>{day}</strong>
3761
+ {safeRows[index] ? <span>{getFieldValue(safeRows[index], fields.titleField, `Row ${index + 1}`)}</span> : null}
3762
+ </section>)}
3763
+ </div>;
3764
+ }
3765
+ return <div className={`workspace-table-transform-preview is-${type}`}>
3766
+ {safeRows.slice(0, 5).map((row, index) => <div key={index}>
3767
+ <span>{getFieldValue(row, fields.titleField, `Row ${index + 1}`)}</span>
3768
+ <i style={{ "--offset": `${(index % 4) * 14}%`, "--width": `${34 + (index % 3) * 12}%` }} />
3769
+ <em>{getFieldValue(row, fields.startDateField, getFieldValue(row, fields.endDateField, ""))}</em>
3770
+ </div>)}
3771
+ </div>;
3772
+ }
3773
+
3377
3774
  function WidgetPreview({ widget, branding, selected, onSelect, onMoveStart, onRemove, onResizeStart, onExpandIframe }) {
3378
3775
  const fallbackColumns = widget.config?.columns?.length ? widget.config.columns : [];
3379
3776
  const visibleColumns = widget.kind === "view" ? getVisibleColumns(widget) : fallbackColumns;
@@ -3416,18 +3813,7 @@ function WidgetPreview({ widget, branding, selected, onSelect, onMoveStart, onRe
3416
3813
  type="button"
3417
3814
  ><X size={13} /></button>
3418
3815
  </div>
3419
- {widget.kind === "view" ? <div
3420
- className="workspace-view-table"
3421
- aria-label={`${widget.title} preview`}
3422
- style={{ "--workspace-view-columns": viewColumns.length }}
3423
- >
3424
- <div>{viewColumns.map((column) => <span key={column}>{column}</span>)}</div>
3425
- {viewRows.slice(0, 6).map((row, rowIndex) => <div key={rowIndex}>
3426
- {viewColumns.map((column) => <span key={column}>{row?.[column] || ""}</span>)}
3427
- </div>)}
3428
- {!viewColumns.length && !viewRows.length ? <div className="workspace-view-empty">Select a source</div> : null}
3429
- <footer>Calculate</footer>
3430
- </div> : null}
3816
+ {widget.kind === "view" ? <TableTransformPreview widget={widget} columns={viewColumns} rows={viewRows} /> : null}
3431
3817
  {widget.kind === "iframe" ? <div className="workspace-iframe-preview">
3432
3818
  {isLikelyHttpUrl(widget.config?.url) ? <iframe title={`${widget.title} preview`} src={widget.config.url} /> : <span>Enter a valid http(s) URL</span>}
3433
3819
  <button type="button" onClick={(event) => {
@@ -3842,6 +4228,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
3842
4228
  const [editingTabId, setEditingTabId] = useState(null);
3843
4229
  const [editingTabDraft, setEditingTabDraft] = useState("");
3844
4230
  const [workspaceView, setWorkspaceView] = useState("dashboards");
4231
+ const [activationPanelOpen, setActivationPanelOpen] = useState(false);
3845
4232
  const [builderListFilter, setBuilderListFilter] = useState({ type: "all", query: "" });
3846
4233
  const [builderActionMenuId, setBuilderActionMenuId] = useState(null);
3847
4234
  const [builderActionMenuPlacement, setBuilderActionMenuPlacement] = useState(null);
@@ -3925,17 +4312,49 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
3925
4312
  ? initialSourceRecords
3926
4313
  : {})
3927
4314
  );
4315
+ const activationState = useMemo(() => deriveWorkspaceActivationState({
4316
+ workspaceConfig: config,
4317
+ workspaceSourceRecords,
4318
+ }), [config, workspaceSourceRecords]);
4319
+ const activationStarted = activationState.completedCount > 0;
4320
+ const activationComplete = Boolean(activationState.complete);
4321
+ const activationUiCache = useMemo(() => getWorkspaceUiCache(config), [config]);
4322
+ const activationButtonHidden = activationUiCache.finishSetupButtonHidden === true
4323
+ || String(activationUiCache.finishSetupButtonHidden || "") === "true";
4324
+ const showFinishSetupButton = workspaceView === "dashboards" && activationStarted && !activationButtonHidden;
4325
+ const showActivationPanel = workspaceView === "dashboards" && (
4326
+ !activationStarted || activationPanelOpen
4327
+ );
3928
4328
  const resizeDragRef = useRef(null);
3929
4329
  const moveDragRef = useRef(null);
3930
4330
  const importInputRef = useRef(null);
3931
- const addSlot = dragPreview || selectedPosition;
3932
4331
  const selectedWidgetLookupId = selectedWidgetId || pendingSelectedWidgetId;
4332
+ const addSlot = dragPreview || selectedPosition;
4333
+ const showAddWidgetSlot = dashboardDraftMode && !selectedWidgetLookupId && panelOpen && addSlot;
3933
4334
  const selectedWidget = activeWidgets.find((widget) => widget.id === selectedWidgetLookupId) || null;
3934
4335
  const availableIntegrations = useMemo(() => flattenIntegrationSettings(integrationSettings), [integrationSettings]);
3935
4336
  const dataModelTables = useMemo(
3936
4337
  () => listWorkspaceDataModelTables(config, { sourceRecords: workspaceSourceRecords }),
3937
4338
  [config, workspaceSourceRecords]
3938
4339
  );
4340
+
4341
+ const dismissFinishSetupButton = useCallback(async () => {
4342
+ const nextConfig = setWorkspaceUiCacheFlag(config, "finishSetupButtonHidden", true);
4343
+ setActivationPanelOpen(false);
4344
+ setConfig(nextConfig);
4345
+ try {
4346
+ const response = await fetch("/api/workspace", {
4347
+ method: "PATCH",
4348
+ headers: { "content-type": "application/json" },
4349
+ body: JSON.stringify({ dataModel: nextConfig.dataModel })
4350
+ });
4351
+ const payload = await response.json();
4352
+ if (!response.ok || !payload.workspaceConfig) throw new Error(payload.error || "Failed to save workspace preference");
4353
+ setConfig((prev) => ({ ...prev, dataModel: payload.workspaceConfig.dataModel }));
4354
+ } catch (error) {
4355
+ setConfigMessage(error.message || "Failed to save workspace preference");
4356
+ }
4357
+ }, [config]);
3939
4358
  const selectedResolvedWidget = selectedWidget ? resolveViewWidget(selectedWidget, dataModelTables) : null;
3940
4359
  const branding = config.branding || {};
3941
4360
  const occupiedCells = useMemo(() => {
@@ -4221,11 +4640,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4221
4640
  const createWorkflow = useCallback(async () => {
4222
4641
  if (saving) return;
4223
4642
  const nowIso = new Date().toISOString();
4224
- const existing = getWorkflowSandboxObject(config);
4225
- if (!existing) {
4226
- setConfigMessage("Workflow sandbox object is missing.");
4227
- return;
4228
- }
4643
+ const existing = getWorkflowSandboxObject(config) || createWorkflowSandboxObject();
4229
4644
  const sandboxObjectId = String(existing.id || "").trim();
4230
4645
  const rows = Array.isArray(existing.rows) ? existing.rows : [];
4231
4646
  const base = slugifyWorkflowName(`workflow-${rows.length + 1}`);
@@ -4239,11 +4654,20 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4239
4654
  const sandboxRow = createBlankWorkflowSandboxRow(rowId, nowIso);
4240
4655
  const nextDataModel = {
4241
4656
  ...(config.dataModel || {}),
4242
- objects: (Array.isArray(config.dataModel?.objects) ? config.dataModel.objects : []).map((object) =>
4243
- object?.id === sandboxObjectId
4244
- ? { ...object, rows: [...(Array.isArray(object.rows) ? object.rows : []), sandboxRow] }
4245
- : object
4246
- )
4657
+ objects: (() => {
4658
+ const objects = Array.isArray(config.dataModel?.objects) ? config.dataModel.objects : [];
4659
+ const hasSandboxObject = objects.some((object) => object?.id === sandboxObjectId);
4660
+ const hasSmokeRegistry = objects.some((object) => object?.id === "workflow-api-registry")
4661
+ || objects.some((object) =>
4662
+ object?.objectType === "api-registry"
4663
+ && (Array.isArray(object.rows) ? object.rows : []).some((row) => row?.integrationId === "growthub-workspace-smoke-api")
4664
+ );
4665
+ const nextSandboxObject = { ...existing, rows: [...rows, sandboxRow] };
4666
+ const nextObjects = hasSandboxObject
4667
+ ? objects.map((object) => object?.id === sandboxObjectId ? nextSandboxObject : object)
4668
+ : [...objects, nextSandboxObject];
4669
+ return hasSmokeRegistry ? nextObjects : [...nextObjects, createWorkflowApiRegistryObject()];
4670
+ })()
4247
4671
  };
4248
4672
  const finalDataModel = addWorkflowFolderShortcut(nextDataModel, {
4249
4673
  objectId: sandboxObjectId,
@@ -4306,6 +4730,36 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4306
4730
  selectDashboard(targetIndex);
4307
4731
  }, [dashboards, resolvedActiveDashboardId, searchParams, selectDashboard, workspaceView]);
4308
4732
 
4733
+ const openAddWidgetBuilder = useCallback(() => {
4734
+ const targetDashboard = activeDashboard || dashboards[0];
4735
+ if (!targetDashboard) return false;
4736
+ setConfig((prev) => {
4737
+ const synced = syncActiveDashboard(prev, activeDashboardId);
4738
+ const prevDashboards = synced.dashboards || [];
4739
+ const dashboard = prevDashboards.find((item) => item.id === targetDashboard.id) || prevDashboards[0];
4740
+ if (!dashboard) return prev;
4741
+ const normalized = normalizeDashboard(dashboard, dashboard.id === prevDashboards[0]?.id ? synced.canvas : undefined);
4742
+ const nextCanvas = dashboardCanvasFrom(normalized, synced.canvas);
4743
+ const nextActiveWidgets = getTabs(nextCanvas)[0]?.widgets || [];
4744
+ setSelectedWidgetId(null);
4745
+ setPendingSelectedWidgetId(null);
4746
+ setSelectedPosition(findFreePosition(nextActiveWidgets));
4747
+ setDragPreview(null);
4748
+ setActiveDashboardId(normalized.id);
4749
+ setWorkspaceView("builder");
4750
+ setDashboardLiveSnapshot(cloneConfig(normalized));
4751
+ setDashboardDraftMode(true);
4752
+ setPanelOpen(true);
4753
+ setConfigMessage(`Editing draft for ${normalized.name}`);
4754
+ return {
4755
+ ...synced,
4756
+ dashboards: prevDashboards.map((item) => item.id === normalized.id ? normalized : item),
4757
+ canvas: nextCanvas
4758
+ };
4759
+ });
4760
+ return true;
4761
+ }, [activeDashboard, activeDashboardId, dashboards]);
4762
+
4309
4763
  const enterDashboardTitleEdit = useCallback((dashboard) => {
4310
4764
  if (!dashboard) return;
4311
4765
  setEditingDashboardId(dashboard.id);
@@ -5149,6 +5603,12 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5149
5603
  return () => window.removeEventListener("keydown", handler);
5150
5604
  }, [addWidget, commandPaletteOpen, managementOpen, panelOpen, settingsOpen, templateGalleryOpen, workspaceView]);
5151
5605
 
5606
+ useEffect(() => {
5607
+ const openFromRail = () => setCommandPaletteOpen(true);
5608
+ window.addEventListener("growthub:open-command-palette", openFromRail);
5609
+ return () => window.removeEventListener("growthub:open-command-palette", openFromRail);
5610
+ }, []);
5611
+
5152
5612
  const builderStyle = workspaceView === "dashboards" || !panelOpen
5153
5613
  ? { gridTemplateColumns: COLLAPSED_GRID_COLUMNS }
5154
5614
  : undefined;
@@ -5317,6 +5777,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5317
5777
  <WorkspaceRail
5318
5778
  workspaceConfig={config}
5319
5779
  authority={integrationAdapter.authority}
5780
+ defaultCollapsed={workspaceView === "builder"}
5320
5781
  helperOpen={helperOpen}
5321
5782
  onOpenHelper={() => {
5322
5783
  if (helperOpen) { setHelperOpen(false); return; }
@@ -5354,11 +5815,11 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5354
5815
  )}
5355
5816
  />
5356
5817
 
5357
- <section className={`workspace-surface${workspaceView === "builder" ? " dm-workflow-surface workspace-dashboard-surface" : ""}`}>
5818
+ <section className={`workspace-surface${workspaceView === "builder" ? ` dm-workflow-surface workspace-dashboard-surface${dashboardDraftMode ? " is-dashboard-editing" : ""}` : ""}`}>
5358
5819
  <header className={`workspace-toolbar${workspaceView === "builder" ? " dm-workflow-toolbar" : ""}`}>
5359
5820
  <div className={workspaceView === "builder" ? "dm-workflow-titlebar" : undefined}>
5360
5821
  {workspaceView === "builder" ? <>
5361
- <span className="dm-workflow-title-muted">Dashboards</span>
5822
+ <button type="button" className="dm-workflow-breadcrumb-link" onClick={showDashboardHome}>Dashboards</button>
5362
5823
  <span className="dm-workflow-title-separator">/</span>
5363
5824
  <h1>{activeDashboard?.name || "Untitled"}</h1>
5364
5825
  <span className="dm-workflow-count">({activeWidgets.length}) · v{activeDashboard?.version || "1"} · {dashboardModeLabel}</span>
@@ -5376,6 +5837,33 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5376
5837
  <button type="button" className="dm-workflow-chip-btn" onClick={exportConfig}><Download size={13} />Export</button>
5377
5838
  <button type="button" className="dm-workflow-icon-btn" onClick={() => importInputRef.current?.click()} aria-label="Import"><Import size={14} /></button>
5378
5839
  </div> : <div className="workspace-toolbar-actions">
5840
+ {showFinishSetupButton ? (
5841
+ <span className={`workspace-finish-setup-control${activationComplete ? " is-complete" : ""}`}>
5842
+ <button
5843
+ type="button"
5844
+ className="workspace-finish-setup-trigger"
5845
+ onClick={() => {
5846
+ setWorkspaceView("dashboards");
5847
+ setActivationPanelOpen((open) => !open);
5848
+ }}
5849
+ >
5850
+ {activationComplete ? null : <Hourglass size={15} />}
5851
+ <span>{activationComplete ? "Setup Complete" : "Finish Workspace Setup"}</span>
5852
+ </button>
5853
+ {activationComplete ? (
5854
+ <button
5855
+ type="button"
5856
+ className="workspace-finish-setup-dismiss"
5857
+ aria-label="Hide completed workspace setup"
5858
+ title="Hide completed workspace setup"
5859
+ onClick={dismissFinishSetupButton}
5860
+ >
5861
+ <Check className="workspace-finish-setup-dismiss-check" size={15} aria-hidden="true" />
5862
+ <X className="workspace-finish-setup-dismiss-x" size={15} aria-hidden="true" />
5863
+ </button>
5864
+ ) : null}
5865
+ </span>
5866
+ ) : null}
5379
5867
  <button type="button" onClick={addDashboard}><Plus size={15} />New Dashboard</button>
5380
5868
  <button type="button" onClick={createWorkflow} disabled={saving}><GitBranch size={15} />New Workflow</button>
5381
5869
  <button type="button" onClick={() => importInputRef.current?.click()}><Import size={15} />Import</button>
@@ -5389,7 +5877,26 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5389
5877
  />
5390
5878
  </header>
5391
5879
 
5392
- {workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="Builder">
5880
+ {workspaceView === "dashboards" ? <>
5881
+ {showActivationPanel ? <WorkspaceActivationPanel
5882
+ workspaceConfig={config}
5883
+ workspaceSourceRecords={workspaceSourceRecords}
5884
+ onStepAction={(step) => {
5885
+ if (step?.id === "add-widget") return openAddWidgetBuilder();
5886
+ if (step?.id === "create-workflow") {
5887
+ createWorkflow();
5888
+ return true;
5889
+ }
5890
+ return false;
5891
+ }}
5892
+ onOpenHelper={() => {
5893
+ setHelperIntent("explain");
5894
+ setHelperInitialPrompt("Help me finish setting up this workspace.");
5895
+ setHelperInitialThread(null);
5896
+ setHelperOpen(true);
5897
+ }}
5898
+ /> : null}
5899
+ <section className="workspace-table" id="dashboards" aria-label="Builder">
5393
5900
  <div className="workspace-table-heading">
5394
5901
  <strong>Builder</strong>
5395
5902
  <span>{dashboards.length} dashboard{dashboards.length === 1 ? "" : "s"} · {workflows.length} workflow{workflows.length === 1 ? "" : "s"}</span>
@@ -5556,7 +6063,8 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5556
6063
  )}
5557
6064
  </span>
5558
6065
  </div>)}
5559
- </section> : null}
6066
+ </section>
6067
+ </> : null}
5560
6068
 
5561
6069
  {workspaceView === "builder" ? <section className="workspace-canvas" id="canvas" aria-label="Composable dashboard canvas">
5562
6070
  <div className="workspace-tabs">
@@ -5609,7 +6117,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5609
6117
  <button type="button" onClick={duplicateTab} disabled={!dashboardDraftMode}><Copy size={15} />Duplicate Tab</button>
5610
6118
  </div>
5611
6119
  <div
5612
- className={`workspace-grid${moveDrag ? " moving-widget" : ""}`}
6120
+ className={`workspace-grid${moveDrag ? " moving-widget" : ""}${dashboardDraftMode ? " is-edit-mode" : " is-view-mode"}`}
5613
6121
  ref={gridRef}
5614
6122
  onPointerMove={updatePointerDrag}
5615
6123
  onPointerUp={(event) => {
@@ -5627,7 +6135,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5627
6135
  }}
5628
6136
  style={{ "--workspace-columns": canvas.layout.columns, "--workspace-rows": GRID_ROWS }}
5629
6137
  >
5630
- {Array.from({ length: GRID_CELL_COUNT }).map((_, index) => {
6138
+ {dashboardDraftMode ? Array.from({ length: GRID_CELL_COUNT }).map((_, index) => {
5631
6139
  const x = index % GRID_COLUMNS;
5632
6140
  const y = Math.floor(index / GRID_COLUMNS);
5633
6141
  const isOccupied = occupiedCells.has(`${x}:${y}`);
@@ -5645,15 +6153,15 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5645
6153
  }}
5646
6154
  type="button"
5647
6155
  />;
5648
- })}
5649
- <button className={`workspace-add-widget${dragPreview ? " selecting" : ""}`} type="button" disabled={!dashboardDraftMode} onClick={() => dashboardDraftMode && setPanelOpen(true)} style={{
6156
+ }) : null}
6157
+ {showAddWidgetSlot ? <button className={`workspace-add-widget${dragPreview ? " selecting" : ""}`} type="button" onClick={() => setPanelOpen(true)} style={{
5650
6158
  gridColumn: `${addSlot.x + 1} / span ${addSlot.w}`,
5651
6159
  gridRow: `${addSlot.y + 1} / span ${addSlot.h}`
5652
6160
  }}>
5653
6161
  <span className="workspace-widget-icon" aria-hidden="true"><span /></span>
5654
6162
  <strong>Add widget</strong>
5655
6163
  <small>Click to add your first widget</small>
5656
- </button>
6164
+ </button> : null}
5657
6165
  {activeWidgets.map((widget) => <WidgetPreview
5658
6166
  key={widget.id}
5659
6167
  branding={branding}
@@ -5828,14 +6336,12 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5828
6336
  </small>
5829
6337
  </label> : null}
5830
6338
  {selectedWidget.kind === "view" ? <section className="workspace-field-stack">
5831
- <div className="workspace-twenty-config" role="group" aria-label="View widget settings">
5832
- <p className="workspace-panel-label">Settings</p>
5833
- <WidgetSettingsRow icon={Table2} label="Layout" value={selectedWidget.config?.layout || "Table"} disabled />
5834
- <WidgetSettingsRow icon={Box} label="Source" value={summarizeSource(selectedWidget)} onClick={() => setInspectorPath("source")} />
5835
- <WidgetSettingsRow icon={List} label="Fields" value={summarizeFields(selectedResolvedWidget || selectedWidget)} onClick={() => setInspectorPath("fields")} />
5836
- <WidgetSettingsRow icon={Filter} label="Filter" value={summarizeFilter(selectedResolvedWidget || selectedWidget)} onClick={() => setInspectorPath("filter")} />
5837
- <WidgetSettingsRow icon={SlidersHorizontal} label="Sort" value={summarizeSort(selectedResolvedWidget || selectedWidget)} onClick={() => setInspectorPath("sort")} />
5838
- </div>
6339
+ <TableViewConfig
6340
+ widget={selectedWidget}
6341
+ dataModelTable={resolveDataModelTable(dataModelTables, selectedWidget.config?.binding)}
6342
+ onChange={replaceSelectedWidgetConfig}
6343
+ onSubPage={(name) => setInspectorPath(name)}
6344
+ />
5839
6345
  </section> : null}
5840
6346
  </section> : null}
5841
6347
  {!selectedWidget ? <section>