@growthub/cli 0.13.6 → 0.13.8

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 (23) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +98 -34
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +106 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +189 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceContributionGraph.jsx +119 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +357 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +488 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensWalkthrough.jsx +69 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +37 -2
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +37 -2
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +437 -26
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +44 -0
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +592 -41
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-lens/page.jsx +76 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +148 -4
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1559 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +3 -3
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +24 -8
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +82 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +8 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +4 -4
  22. package/dist/index.js +5224 -5225
  23. 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,
@@ -58,6 +59,8 @@ import {
58
59
  Trash2,
59
60
  Type,
60
61
  Users,
62
+ Eye,
63
+ Wrench,
61
64
  X,
62
65
  Zap,
63
66
  } from "lucide-react";
@@ -85,8 +88,10 @@ import {
85
88
  deriveWidgetDependencyContract
86
89
  } from "@/lib/workspace-chart-values";
87
90
  import { selectObjectFilterableFields, selectObjectSortableFields } from "@/lib/workspace-metadata-selectors";
91
+ import { deriveWorkspaceActivationState } from "@/lib/workspace-activation";
88
92
  import { HelperSidecar } from "./data-model/components/HelperSidecar.jsx";
89
93
  import { WorkspaceRail } from "./workspace-rail.jsx";
94
+ import { WorkspaceActivationPanel } from "./components/WorkspaceActivationPanel.jsx";
90
95
 
91
96
  // Workspace Metadata Graph V1 — typed dependency contracts.
92
97
  // Used by sidecar dependency summaries; the existing chart hydration path
@@ -110,6 +115,7 @@ const DATA_MODEL_SOURCE_TYPE = "workspace-data-model";
110
115
  const LIVE_SOURCE_TYPE = "workspace-source-records";
111
116
  const TESTED_SOURCE_STATUSES = new Set(["connected", "approved", "ok", "success"]);
112
117
  const HIDDEN_SANDBOX_OBJECT_IDS = new Set(["workspace-helper-sandbox"]);
118
+ const WORKSPACE_UI_CACHE_OBJECT_ID = "workspace-ui-cache";
113
119
 
114
120
  const SOURCE_TYPE_OBJECTS = [
115
121
  {
@@ -173,6 +179,26 @@ const CHART_TYPE_ICONS = {
173
179
 
174
180
  const VISIBLE_CHART_TYPES = KNOWN_CHART_TYPES.filter((type) => type !== "line");
175
181
 
182
+ const TABLE_VIEW_TYPES = ["gantt", "board", "calendar", "timeline"];
183
+ const TABLE_VIEW_LABELS = {
184
+ gantt: "Gantt",
185
+ board: "Board",
186
+ calendar: "Calendar",
187
+ timeline: "Timeline"
188
+ };
189
+ const TABLE_VIEW_HELP = {
190
+ gantt: "Track dependencies and baselines",
191
+ board: "Track work in a Kanban view",
192
+ calendar: "Plan weekly or monthly work",
193
+ timeline: "Schedule work over time"
194
+ };
195
+ const TABLE_VIEW_ICONS = {
196
+ gantt: GitBranch,
197
+ board: Columns3,
198
+ calendar: Calendar,
199
+ timeline: Rows3
200
+ };
201
+
176
202
  // User-facing labels for the Twenty-style Y-axis operation dropdown.
177
203
  // Keys must stay in sync with `lib/workspace-chart-values.js#KNOWN_AGGREGATIONS`
178
204
  // and the validator in `lib/workspace-schema.js`.
@@ -407,6 +433,44 @@ function getWorkflowSandboxObject(config) {
407
433
  }) || null;
408
434
  }
409
435
 
436
+ function getWorkspaceUiCache(config) {
437
+ const object = getDataModelObject(config, WORKSPACE_UI_CACHE_OBJECT_ID);
438
+ const row = Array.isArray(object?.rows) ? object.rows.find((entry) => entry?.id === "activation") : null;
439
+ return row && typeof row === "object" ? row : {};
440
+ }
441
+
442
+ function setWorkspaceUiCacheFlag(config, key, value) {
443
+ const dataModel = config?.dataModel && typeof config.dataModel === "object" ? config.dataModel : {};
444
+ const objects = Array.isArray(dataModel.objects) ? dataModel.objects : [];
445
+ const cacheObject = objects.find((object) => object?.id === WORKSPACE_UI_CACHE_OBJECT_ID) || {
446
+ id: WORKSPACE_UI_CACHE_OBJECT_ID,
447
+ label: "Workspace UI Cache",
448
+ source: "Workspace UI Cache",
449
+ objectType: "custom",
450
+ icon: "Settings",
451
+ columns: ["id", key],
452
+ rows: [],
453
+ binding: { mode: "manual", source: "Workspace UI Cache" }
454
+ };
455
+ const columns = Array.from(new Set([...(Array.isArray(cacheObject.columns) ? cacheObject.columns : ["id"]), key]));
456
+ const rows = Array.isArray(cacheObject.rows) ? cacheObject.rows : [];
457
+ const hasActivationRow = rows.some((row) => row?.id === "activation");
458
+ const nextRows = hasActivationRow
459
+ ? rows.map((row) => row?.id === "activation" ? { ...row, [key]: value } : row)
460
+ : [...rows, { id: "activation", [key]: value }];
461
+ const nextCacheObject = { ...cacheObject, columns, rows: nextRows };
462
+ const nextObjects = objects.some((object) => object?.id === WORKSPACE_UI_CACHE_OBJECT_ID)
463
+ ? objects.map((object) => object?.id === WORKSPACE_UI_CACHE_OBJECT_ID ? nextCacheObject : object)
464
+ : [...objects, nextCacheObject];
465
+ return {
466
+ ...config,
467
+ dataModel: {
468
+ ...dataModel,
469
+ objects: nextObjects
470
+ }
471
+ };
472
+ }
473
+
410
474
  function listBuilderWorkflowItems(config) {
411
475
  const navFolders = getDataModelObject(config, "nav-folders");
412
476
  const rows = Array.isArray(navFolders?.rows) ? navFolders.rows : [];
@@ -476,11 +540,76 @@ function updateWorkflowFolderItemInConfig(config, workflow, updater) {
476
540
  }
477
541
 
478
542
  function createBlankWorkflowSandboxRow(rowId, nowIso) {
543
+ const registryId = "growthub-workspace-smoke-api";
479
544
  const draftGraph = JSON.stringify({
480
- version: "0",
545
+ version: "1",
481
546
  provider: "growthub-native",
482
- nodes: [],
483
- edges: []
547
+ nodes: [
548
+ {
549
+ id: "input",
550
+ type: "input",
551
+ label: "Input",
552
+ subtitle: "Manual or source payload",
553
+ config: { inputMode: "manual", samplePayload: {}, sourceType: "", sourceId: "", entityId: "", filterMode: "and", filters: [] }
554
+ },
555
+ {
556
+ id: "api-request",
557
+ type: "api-registry-call",
558
+ label: "API Registry",
559
+ subtitle: `${registryId} · GET /api/workspace`,
560
+ config: {
561
+ registryId,
562
+ integrationId: registryId,
563
+ baseUrl: "http://localhost:3000",
564
+ endpoint: "/api/workspace",
565
+ method: "GET",
566
+ authRef: "",
567
+ queryParams: {},
568
+ bodyTemplate: "",
569
+ requestHeadersMetadata: { authHeaderName: "x-api-key", authPrefix: "", contentType: "" },
570
+ timeoutMs: 30000
571
+ }
572
+ },
573
+ {
574
+ id: "transform",
575
+ type: "transform-filter",
576
+ label: "Transform",
577
+ subtitle: "Map fields and filter rows",
578
+ config: {
579
+ rootPath: "",
580
+ mode: "json",
581
+ responseMode: "json",
582
+ fieldMap: {},
583
+ includeFields: [],
584
+ excludeFields: [],
585
+ computedFields: {},
586
+ filters: [],
587
+ filterMode: "and",
588
+ maxRows: 0
589
+ }
590
+ },
591
+ {
592
+ id: "result",
593
+ type: "tool-result",
594
+ label: "Result",
595
+ subtitle: "Save status and response",
596
+ config: {
597
+ successStatusCodes: [200],
598
+ writeLastResponse: true,
599
+ writeSourceRecord: true,
600
+ sourceRecordId: "",
601
+ outputMode: "normalized-json",
602
+ previewFields: [],
603
+ statusField: "status",
604
+ lastTestedField: "lastTested"
605
+ }
606
+ }
607
+ ],
608
+ edges: [
609
+ { from: "input", to: "api-request", passes: "payload, filters, variables" },
610
+ { from: "api-request", to: "transform", passes: "provider-response" },
611
+ { from: "transform", to: "result", passes: "normalized-output" }
612
+ ]
484
613
  }, null, 2);
485
614
  return {
486
615
  Name: rowId,
@@ -520,11 +649,77 @@ function createBlankWorkflowSandboxRow(rowId, nowIso) {
520
649
  };
521
650
  }
522
651
 
652
+ function createWorkflowApiRegistryObject() {
653
+ const preset = OBJECT_TYPE_PRESETS["api-registry"] || {};
654
+ const columns = Array.isArray(preset.columns) ? [...preset.columns] : ["integrationId"];
655
+ return {
656
+ id: "workflow-api-registry",
657
+ label: preset.label || "API Registry",
658
+ source: preset.label || "API Registry",
659
+ objectType: "api-registry",
660
+ icon: preset.icon || "Code2",
661
+ columns,
662
+ rows: [
663
+ {
664
+ integrationId: "growthub-workspace-smoke-api",
665
+ authRef: "",
666
+ baseUrl: "http://localhost:3000",
667
+ endpoint: "/api/workspace",
668
+ method: "GET",
669
+ status: "draft",
670
+ lastTested: "",
671
+ lastResponse: "",
672
+ entityTypes: "workspace",
673
+ description: "Local workspace smoke endpoint for first workflow setup.",
674
+ connectorKind: "custom-http",
675
+ resolverTemplateId: "",
676
+ schemaVersion: "1",
677
+ capabilities: "read",
678
+ executionLane: "sandbox-local"
679
+ }
680
+ ],
681
+ binding: { mode: "manual", source: "Data Model" },
682
+ relations: Array.isArray(preset.relations) ? preset.relations.map((relation) => ({ ...relation })) : [],
683
+ fieldSettings: { hidden: [], order: columns }
684
+ };
685
+ }
686
+
687
+ function createWorkflowSandboxObject() {
688
+ const preset = OBJECT_TYPE_PRESETS["sandbox-environment"] || {};
689
+ const columns = Array.isArray(preset.columns) ? [...preset.columns] : ["Name"];
690
+ return {
691
+ id: "sandbox-environments",
692
+ label: preset.label || "Sandbox Environments",
693
+ source: preset.label || "Sandbox Environments",
694
+ objectType: "sandbox-environment",
695
+ icon: preset.icon || "Terminal",
696
+ columns,
697
+ rows: [],
698
+ binding: { mode: "manual", source: "Data Model" },
699
+ relations: Array.isArray(preset.relations) ? preset.relations.map((relation) => ({ ...relation })) : [],
700
+ fieldSettings: { hidden: [], order: columns }
701
+ };
702
+ }
703
+
523
704
  function addWorkflowFolderShortcut(dataModel, workflow) {
524
705
  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];
706
+ const seededObjects = objects.some((object) => object?.id === "nav-folders")
707
+ ? objects
708
+ : [
709
+ ...objects,
710
+ {
711
+ id: "nav-folders",
712
+ label: "Custom Folders",
713
+ source: "Custom Folders",
714
+ objectType: "custom",
715
+ icon: "Folder",
716
+ columns: ["name", "order", "collapsed", "items"],
717
+ rows: [],
718
+ binding: { mode: "manual", source: "Custom Folders" }
719
+ }
720
+ ];
721
+ const navIndex = seededObjects.findIndex((object) => object?.id === "nav-folders");
722
+ const navObject = seededObjects[navIndex];
528
723
  const rows = Array.isArray(navObject.rows) ? navObject.rows : [];
529
724
  const folderName = "Builder";
530
725
  const existingFolder = rows.find((row) => String(row?.name || "").trim().toLowerCase() === folderName.toLowerCase());
@@ -562,7 +757,7 @@ function addWorkflowFolderShortcut(dataModel, workflow) {
562
757
  ];
563
758
  return {
564
759
  ...dataModel,
565
- objects: objects.map((object, index) => index === navIndex ? { ...navObject, rows: nextRows } : object)
760
+ objects: seededObjects.map((object, index) => index === navIndex ? { ...navObject, rows: nextRows } : object)
566
761
  };
567
762
  }
568
763
 
@@ -939,6 +1134,43 @@ function getVisibleColumns(widget) {
939
1134
  return ordered.filter((name) => !hidden.has(name));
940
1135
  }
941
1136
 
1137
+ function getTableViewSettings(widget) {
1138
+ const settings = widget?.config?.fieldSettings?.tableView;
1139
+ return isPlainConfigObject(settings) ? settings : {};
1140
+ }
1141
+
1142
+ function getTableViewType(widget) {
1143
+ const type = getTableViewSettings(widget).type;
1144
+ return TABLE_VIEW_TYPES.includes(type) ? type : "";
1145
+ }
1146
+
1147
+ function getFieldValue(row, field, fallback = "") {
1148
+ if (!row || !field) return fallback;
1149
+ const value = row[field];
1150
+ if (value === null || value === undefined || value === "") return fallback;
1151
+ return String(value);
1152
+ }
1153
+
1154
+ function firstMatchingField(columns, candidates) {
1155
+ const lower = columns.map((column) => [column, String(column).toLowerCase()]);
1156
+ for (const candidate of candidates) {
1157
+ const found = lower.find(([, name]) => name.includes(candidate));
1158
+ if (found) return found[0];
1159
+ }
1160
+ return columns[0] || "";
1161
+ }
1162
+
1163
+ function resolveTableViewFields(widget) {
1164
+ const columns = getVisibleColumns(widget);
1165
+ const settings = getTableViewSettings(widget);
1166
+ return {
1167
+ titleField: settings.titleField || firstMatchingField(columns, ["title", "name", "task", "project"]),
1168
+ statusField: settings.statusField || firstMatchingField(columns, ["status", "stage", "state", "lane"]),
1169
+ startDateField: settings.startDateField || firstMatchingField(columns, ["start", "created", "date"]),
1170
+ endDateField: settings.endDateField || firstMatchingField(columns, ["end", "due", "deadline", "target"]),
1171
+ };
1172
+ }
1173
+
942
1174
  function withFieldSettings(config, patch) {
943
1175
  const current = isPlainConfigObject(config?.fieldSettings) ? config.fieldSettings : { hidden: [], order: [] };
944
1176
  return {
@@ -951,6 +1183,21 @@ function withFieldSettings(config, patch) {
951
1183
  };
952
1184
  }
953
1185
 
1186
+ function withTableViewSettings(config, patch) {
1187
+ const current = isPlainConfigObject(config?.fieldSettings) ? config.fieldSettings : {};
1188
+ const currentTableView = isPlainConfigObject(current.tableView) ? current.tableView : {};
1189
+ return {
1190
+ ...config,
1191
+ fieldSettings: {
1192
+ ...current,
1193
+ tableView: {
1194
+ ...currentTableView,
1195
+ ...patch
1196
+ }
1197
+ }
1198
+ };
1199
+ }
1200
+
954
1201
  function isPlainConfigObject(value) {
955
1202
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
956
1203
  }
@@ -1115,6 +1362,10 @@ function summarizeFields(widget) {
1115
1362
  return hidden ? `${total - hidden} of ${total} shown` : `${total} shown`;
1116
1363
  }
1117
1364
 
1365
+ function summarizeTableView(widget) {
1366
+ return TABLE_VIEW_LABELS[getTableViewType(widget)] || "Table";
1367
+ }
1368
+
1118
1369
  function summarizeSort(widget) {
1119
1370
  const sort = getSortClauses(widget);
1120
1371
  if (!sort.length) return "›";
@@ -2633,6 +2884,105 @@ function SortSubPanel({ widget, dataModelTable, onChange, onBack }) {
2633
2884
  </section>;
2634
2885
  }
2635
2886
 
2887
+ function TableViewConfig({ widget, dataModelTable, onChange, onSubPage }) {
2888
+ const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
2889
+ const columns = getVisibleColumns(viewWidget);
2890
+ const tableView = getTableViewSettings(widget);
2891
+ const activeType = getTableViewType(widget);
2892
+ const setTableView = (patch) => onChange(withTableViewSettings(widget.config, patch));
2893
+ const fieldOptions = columns.length ? columns : getColumnList(viewWidget);
2894
+ return (
2895
+ <div className="workspace-twenty-config workspace-table-view-config" role="group" aria-label="Table view widget settings">
2896
+ <p className="workspace-panel-label">Settings</p>
2897
+ <WidgetSettingsRow icon={Table2} label="Layout" value={summarizeTableView(widget)} disabled />
2898
+ <div className="workspace-chart-type-tabs workspace-table-view-tabs" role="tablist" aria-label="Table view type">
2899
+ {TABLE_VIEW_TYPES.map((type) => {
2900
+ const TypeIcon = TABLE_VIEW_ICONS[type];
2901
+ return (
2902
+ <button
2903
+ key={type}
2904
+ type="button"
2905
+ role="tab"
2906
+ aria-selected={activeType === type}
2907
+ className={activeType === type ? "active" : ""}
2908
+ onClick={() => setTableView({ type: activeType === type ? "" : type })}
2909
+ title={TABLE_VIEW_HELP[type]}
2910
+ >
2911
+ <IconGlyph icon={TypeIcon} size={17} />
2912
+ <em>{TABLE_VIEW_LABELS[type]}</em>
2913
+ </button>
2914
+ );
2915
+ })}
2916
+ </div>
2917
+ {activeType ? <p className="workspace-panel-hint">{TABLE_VIEW_HELP[activeType]}</p> : null}
2918
+
2919
+ <WidgetSettingsRow icon={Box} label="Source" value={summarizeSource(widget)} onClick={() => onSubPage("source")} />
2920
+ <WidgetSettingsRow icon={List} label="Fields" value={summarizeFields(viewWidget)} onClick={() => onSubPage("fields")} />
2921
+ <WidgetSettingsRow icon={Filter} label="Filter" value={summarizeFilter(viewWidget)} onClick={() => onSubPage("filter")} />
2922
+ <WidgetSettingsRow icon={SlidersHorizontal} label="Sort" value={summarizeSort(viewWidget)} onClick={() => onSubPage("sort")} />
2923
+
2924
+ {activeType ? (
2925
+ <>
2926
+ <p className="workspace-panel-label">View fields</p>
2927
+ <WidgetSelectRow icon={Type} label="Title">
2928
+ <FieldDropdown
2929
+ fields={fieldOptions}
2930
+ value={tableView.titleField || ""}
2931
+ onChange={(field) => setTableView({ titleField: field })}
2932
+ placeholder="Auto"
2933
+ disabled={!fieldOptions.length}
2934
+ />
2935
+ </WidgetSelectRow>
2936
+ {activeType === "board" ? (
2937
+ <WidgetSelectRow icon={Columns3} label="Status">
2938
+ <FieldDropdown
2939
+ fields={fieldOptions}
2940
+ value={tableView.statusField || ""}
2941
+ onChange={(field) => setTableView({ statusField: field })}
2942
+ placeholder="Auto"
2943
+ disabled={!fieldOptions.length}
2944
+ />
2945
+ </WidgetSelectRow>
2946
+ ) : null}
2947
+ {activeType === "gantt" || activeType === "timeline" ? (
2948
+ <>
2949
+ <WidgetSelectRow icon={Calendar} label="Start">
2950
+ <FieldDropdown
2951
+ fields={fieldOptions}
2952
+ value={tableView.startDateField || ""}
2953
+ onChange={(field) => setTableView({ startDateField: field })}
2954
+ placeholder="Auto"
2955
+ disabled={!fieldOptions.length}
2956
+ />
2957
+ </WidgetSelectRow>
2958
+ <WidgetSelectRow icon={Calendar} label="End">
2959
+ <FieldDropdown
2960
+ fields={fieldOptions}
2961
+ value={tableView.endDateField || ""}
2962
+ onChange={(field) => setTableView({ endDateField: field })}
2963
+ placeholder="Auto"
2964
+ disabled={!fieldOptions.length}
2965
+ />
2966
+ </WidgetSelectRow>
2967
+ </>
2968
+ ) : null}
2969
+ {activeType === "calendar" ? (
2970
+ <WidgetSelectRow icon={Calendar} label="Date">
2971
+ <FieldDropdown
2972
+ fields={fieldOptions}
2973
+ value={tableView.startDateField || ""}
2974
+ onChange={(field) => setTableView({ startDateField: field })}
2975
+ placeholder="Auto"
2976
+ disabled={!fieldOptions.length}
2977
+ />
2978
+ </WidgetSelectRow>
2979
+ ) : null}
2980
+ </>
2981
+ ) : null}
2982
+ </div>
2983
+ );
2984
+ }
2985
+
2636
2986
  function FilterSubPanel({ widget, integrations, dataModelTable, adapterConfig, onRefreshAndSave, onChange, onBack }) {
2637
2987
  const viewWidget = dataModelTable ? resolveViewWidget(widget, [dataModelTable]) : widget;
2638
2988
  const binding = widget.config?.binding || {};
@@ -3374,6 +3724,55 @@ function IframePreviewModal({ widget, onClose }) {
3374
3724
  </div>;
3375
3725
  }
3376
3726
 
3727
+ function TableTransformPreview({ widget, columns, rows }) {
3728
+ const type = getTableViewType(widget);
3729
+ const safeRows = Array.isArray(rows) ? rows : [];
3730
+ if (!type) {
3731
+ return <div
3732
+ className="workspace-view-table"
3733
+ aria-label={`${widget.title} preview`}
3734
+ style={{ "--workspace-view-columns": columns.length }}
3735
+ >
3736
+ <div>{columns.map((column) => <span key={column}>{column}</span>)}</div>
3737
+ {safeRows.slice(0, 6).map((row, rowIndex) => <div key={rowIndex}>
3738
+ {columns.map((column) => <span key={column}>{row?.[column] || ""}</span>)}
3739
+ </div>)}
3740
+ {!columns.length && !safeRows.length ? <div className="workspace-view-empty">Select a source</div> : null}
3741
+ <footer>Calculate</footer>
3742
+ </div>;
3743
+ }
3744
+ const fields = resolveTableViewFields(widget);
3745
+ if (type === "board") {
3746
+ const groups = safeRows.slice(0, 8).reduce((acc, row) => {
3747
+ const status = getFieldValue(row, fields.statusField, "Open");
3748
+ if (!acc[status]) acc[status] = [];
3749
+ acc[status].push(row);
3750
+ return acc;
3751
+ }, {});
3752
+ return <div className="workspace-table-transform-preview is-board">
3753
+ {Object.entries(groups).slice(0, 3).map(([status, groupRows]) => <section key={status}>
3754
+ <strong>{status}</strong>
3755
+ {groupRows.slice(0, 3).map((row, index) => <span key={index}>{getFieldValue(row, fields.titleField, `Row ${index + 1}`)}</span>)}
3756
+ </section>)}
3757
+ </div>;
3758
+ }
3759
+ if (type === "calendar") {
3760
+ return <div className="workspace-table-transform-preview is-calendar">
3761
+ {["Mon", "Tue", "Wed", "Thu", "Fri"].map((day, index) => <section key={day}>
3762
+ <strong>{day}</strong>
3763
+ {safeRows[index] ? <span>{getFieldValue(safeRows[index], fields.titleField, `Row ${index + 1}`)}</span> : null}
3764
+ </section>)}
3765
+ </div>;
3766
+ }
3767
+ return <div className={`workspace-table-transform-preview is-${type}`}>
3768
+ {safeRows.slice(0, 5).map((row, index) => <div key={index}>
3769
+ <span>{getFieldValue(row, fields.titleField, `Row ${index + 1}`)}</span>
3770
+ <i style={{ "--offset": `${(index % 4) * 14}%`, "--width": `${34 + (index % 3) * 12}%` }} />
3771
+ <em>{getFieldValue(row, fields.startDateField, getFieldValue(row, fields.endDateField, ""))}</em>
3772
+ </div>)}
3773
+ </div>;
3774
+ }
3775
+
3377
3776
  function WidgetPreview({ widget, branding, selected, onSelect, onMoveStart, onRemove, onResizeStart, onExpandIframe }) {
3378
3777
  const fallbackColumns = widget.config?.columns?.length ? widget.config.columns : [];
3379
3778
  const visibleColumns = widget.kind === "view" ? getVisibleColumns(widget) : fallbackColumns;
@@ -3416,18 +3815,7 @@ function WidgetPreview({ widget, branding, selected, onSelect, onMoveStart, onRe
3416
3815
  type="button"
3417
3816
  ><X size={13} /></button>
3418
3817
  </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}
3818
+ {widget.kind === "view" ? <TableTransformPreview widget={widget} columns={viewColumns} rows={viewRows} /> : null}
3431
3819
  {widget.kind === "iframe" ? <div className="workspace-iframe-preview">
3432
3820
  {isLikelyHttpUrl(widget.config?.url) ? <iframe title={`${widget.title} preview`} src={widget.config.url} /> : <span>Enter a valid http(s) URL</span>}
3433
3821
  <button type="button" onClick={(event) => {
@@ -3842,6 +4230,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
3842
4230
  const [editingTabId, setEditingTabId] = useState(null);
3843
4231
  const [editingTabDraft, setEditingTabDraft] = useState("");
3844
4232
  const [workspaceView, setWorkspaceView] = useState("dashboards");
4233
+ const [activationPanelOpen, setActivationPanelOpen] = useState(false);
3845
4234
  const [builderListFilter, setBuilderListFilter] = useState({ type: "all", query: "" });
3846
4235
  const [builderActionMenuId, setBuilderActionMenuId] = useState(null);
3847
4236
  const [builderActionMenuPlacement, setBuilderActionMenuPlacement] = useState(null);
@@ -3925,6 +4314,31 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
3925
4314
  ? initialSourceRecords
3926
4315
  : {})
3927
4316
  );
4317
+ const activationState = useMemo(() => deriveWorkspaceActivationState({
4318
+ workspaceConfig: config,
4319
+ workspaceSourceRecords,
4320
+ }), [config, workspaceSourceRecords]);
4321
+ // Safe runtime descriptor for the secondary readiness lenses — assembled from
4322
+ // the persistence/adapter props the builder already receives (no fetch, no
4323
+ // secrets; booleans only).
4324
+ const lensMetadataGraph = useMemo(() => ({
4325
+ runtime: {
4326
+ persistenceMode: persistence?.mode || "",
4327
+ persistenceAdapter: persistence?.mode === "database" ? (adapterConfig?.dataAdapter || null) : null,
4328
+ allowFsWrite: persistence?.mode === "filesystem" && persistence?.canSave === true,
4329
+ nangoConfigured: Boolean(adapterConfig?.nango?.hasSecretKey),
4330
+ deploy: { target: adapterConfig?.deployTarget || "" },
4331
+ },
4332
+ }), [persistence, adapterConfig]);
4333
+ const activationStarted = activationState.completedCount > 0;
4334
+ const activationComplete = Boolean(activationState.complete);
4335
+ const activationUiCache = useMemo(() => getWorkspaceUiCache(config), [config]);
4336
+ const activationButtonHidden = activationUiCache.finishSetupButtonHidden === true
4337
+ || String(activationUiCache.finishSetupButtonHidden || "") === "true";
4338
+ const showFinishSetupButton = workspaceView === "dashboards" && activationStarted && !activationButtonHidden;
4339
+ const showActivationPanel = workspaceView === "dashboards" && (
4340
+ !activationStarted || activationPanelOpen
4341
+ );
3928
4342
  const resizeDragRef = useRef(null);
3929
4343
  const moveDragRef = useRef(null);
3930
4344
  const importInputRef = useRef(null);
@@ -3937,6 +4351,24 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
3937
4351
  () => listWorkspaceDataModelTables(config, { sourceRecords: workspaceSourceRecords }),
3938
4352
  [config, workspaceSourceRecords]
3939
4353
  );
4354
+
4355
+ const dismissFinishSetupButton = useCallback(async () => {
4356
+ const nextConfig = setWorkspaceUiCacheFlag(config, "finishSetupButtonHidden", true);
4357
+ setActivationPanelOpen(false);
4358
+ setConfig(nextConfig);
4359
+ try {
4360
+ const response = await fetch("/api/workspace", {
4361
+ method: "PATCH",
4362
+ headers: { "content-type": "application/json" },
4363
+ body: JSON.stringify({ dataModel: nextConfig.dataModel })
4364
+ });
4365
+ const payload = await response.json();
4366
+ if (!response.ok || !payload.workspaceConfig) throw new Error(payload.error || "Failed to save workspace preference");
4367
+ setConfig((prev) => ({ ...prev, dataModel: payload.workspaceConfig.dataModel }));
4368
+ } catch (error) {
4369
+ setConfigMessage(error.message || "Failed to save workspace preference");
4370
+ }
4371
+ }, [config]);
3940
4372
  const selectedResolvedWidget = selectedWidget ? resolveViewWidget(selectedWidget, dataModelTables) : null;
3941
4373
  const branding = config.branding || {};
3942
4374
  const occupiedCells = useMemo(() => {
@@ -4222,11 +4654,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4222
4654
  const createWorkflow = useCallback(async () => {
4223
4655
  if (saving) return;
4224
4656
  const nowIso = new Date().toISOString();
4225
- const existing = getWorkflowSandboxObject(config);
4226
- if (!existing) {
4227
- setConfigMessage("Workflow sandbox object is missing.");
4228
- return;
4229
- }
4657
+ const existing = getWorkflowSandboxObject(config) || createWorkflowSandboxObject();
4230
4658
  const sandboxObjectId = String(existing.id || "").trim();
4231
4659
  const rows = Array.isArray(existing.rows) ? existing.rows : [];
4232
4660
  const base = slugifyWorkflowName(`workflow-${rows.length + 1}`);
@@ -4240,11 +4668,20 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4240
4668
  const sandboxRow = createBlankWorkflowSandboxRow(rowId, nowIso);
4241
4669
  const nextDataModel = {
4242
4670
  ...(config.dataModel || {}),
4243
- objects: (Array.isArray(config.dataModel?.objects) ? config.dataModel.objects : []).map((object) =>
4244
- object?.id === sandboxObjectId
4245
- ? { ...object, rows: [...(Array.isArray(object.rows) ? object.rows : []), sandboxRow] }
4246
- : object
4247
- )
4671
+ objects: (() => {
4672
+ const objects = Array.isArray(config.dataModel?.objects) ? config.dataModel.objects : [];
4673
+ const hasSandboxObject = objects.some((object) => object?.id === sandboxObjectId);
4674
+ const hasSmokeRegistry = objects.some((object) => object?.id === "workflow-api-registry")
4675
+ || objects.some((object) =>
4676
+ object?.objectType === "api-registry"
4677
+ && (Array.isArray(object.rows) ? object.rows : []).some((row) => row?.integrationId === "growthub-workspace-smoke-api")
4678
+ );
4679
+ const nextSandboxObject = { ...existing, rows: [...rows, sandboxRow] };
4680
+ const nextObjects = hasSandboxObject
4681
+ ? objects.map((object) => object?.id === sandboxObjectId ? nextSandboxObject : object)
4682
+ : [...objects, nextSandboxObject];
4683
+ return hasSmokeRegistry ? nextObjects : [...nextObjects, createWorkflowApiRegistryObject()];
4684
+ })()
4248
4685
  };
4249
4686
  const finalDataModel = addWorkflowFolderShortcut(nextDataModel, {
4250
4687
  objectId: sandboxObjectId,
@@ -4307,6 +4744,36 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4307
4744
  selectDashboard(targetIndex);
4308
4745
  }, [dashboards, resolvedActiveDashboardId, searchParams, selectDashboard, workspaceView]);
4309
4746
 
4747
+ const openAddWidgetBuilder = useCallback(() => {
4748
+ const targetDashboard = activeDashboard || dashboards[0];
4749
+ if (!targetDashboard) return false;
4750
+ setConfig((prev) => {
4751
+ const synced = syncActiveDashboard(prev, activeDashboardId);
4752
+ const prevDashboards = synced.dashboards || [];
4753
+ const dashboard = prevDashboards.find((item) => item.id === targetDashboard.id) || prevDashboards[0];
4754
+ if (!dashboard) return prev;
4755
+ const normalized = normalizeDashboard(dashboard, dashboard.id === prevDashboards[0]?.id ? synced.canvas : undefined);
4756
+ const nextCanvas = dashboardCanvasFrom(normalized, synced.canvas);
4757
+ const nextActiveWidgets = getTabs(nextCanvas)[0]?.widgets || [];
4758
+ setSelectedWidgetId(null);
4759
+ setPendingSelectedWidgetId(null);
4760
+ setSelectedPosition(findFreePosition(nextActiveWidgets));
4761
+ setDragPreview(null);
4762
+ setActiveDashboardId(normalized.id);
4763
+ setWorkspaceView("builder");
4764
+ setDashboardLiveSnapshot(cloneConfig(normalized));
4765
+ setDashboardDraftMode(true);
4766
+ setPanelOpen(true);
4767
+ setConfigMessage(`Editing draft for ${normalized.name}`);
4768
+ return {
4769
+ ...synced,
4770
+ dashboards: prevDashboards.map((item) => item.id === normalized.id ? normalized : item),
4771
+ canvas: nextCanvas
4772
+ };
4773
+ });
4774
+ return true;
4775
+ }, [activeDashboard, activeDashboardId, dashboards]);
4776
+
4310
4777
  const enterDashboardTitleEdit = useCallback((dashboard) => {
4311
4778
  if (!dashboard) return;
4312
4779
  setEditingDashboardId(dashboard.id);
@@ -5299,6 +5766,40 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5299
5766
  id: "workspace.builder", group: "Navigation", icon: Home, label: "Go to Builder",
5300
5767
  run: () => showDashboardHome()
5301
5768
  });
5769
+ list.push({
5770
+ id: "nav.data-model", group: "Navigation", icon: Database, label: "Go to Management (Data Model)",
5771
+ run: () => { window.location.href = "/data-model"; }
5772
+ });
5773
+
5774
+ // Workspace Lens — fast navigation into the post-activation operating
5775
+ // surface and its filtered views. Unlocks once activation completes.
5776
+ const lensReady = Boolean(activationState?.complete);
5777
+ list.push({
5778
+ id: "lens.open", group: "Workspace Lens", icon: Eye,
5779
+ label: lensReady ? "Open Workspace Lens" : "Workspace Lens (finish setup to unlock)",
5780
+ disabled: !lensReady,
5781
+ run: () => { window.location.href = "/workspace-lens"; }
5782
+ });
5783
+ list.push({
5784
+ id: "lens.blocked", group: "Workspace Lens", icon: Eye, label: "Workspace Lens — Blocked",
5785
+ disabled: !lensReady,
5786
+ run: () => { window.location.href = "/workspace-lens?filter=blocked"; }
5787
+ });
5788
+ list.push({
5789
+ id: "lens.ready", group: "Workspace Lens", icon: Eye, label: "Workspace Lens — Ready",
5790
+ disabled: !lensReady,
5791
+ run: () => { window.location.href = "/workspace-lens?filter=ready"; }
5792
+ });
5793
+ list.push({
5794
+ id: "lens.assignable", group: "Workspace Lens", icon: Eye, label: "Workspace Lens — Agent-assignable",
5795
+ disabled: !lensReady,
5796
+ run: () => { window.location.href = "/workspace-lens?filter=assignable"; }
5797
+ });
5798
+ list.push({
5799
+ id: "lens.runs", group: "Workspace Lens", icon: Eye, label: "Workspace Lens — Runs",
5800
+ disabled: !lensReady,
5801
+ run: () => { window.location.href = "/workspace-lens?filter=runs"; }
5802
+ });
5302
5803
 
5303
5804
  return list;
5304
5805
  }, [
@@ -5317,7 +5818,8 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5317
5818
  saving,
5318
5819
  selectedWidget,
5319
5820
  showDashboardHome,
5320
- workspaceView
5821
+ workspaceView,
5822
+ activationState
5321
5823
  ]);
5322
5824
 
5323
5825
  return <main className="workspace-builder" onPointerDownCapture={resetWidgetSelectionOnOutsidePointer} style={builderStyle}>
@@ -5345,10 +5847,12 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5345
5847
  dashboardsSlot={(
5346
5848
  <button
5347
5849
  type="button"
5850
+ title="Builder"
5348
5851
  className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"}
5349
5852
  onClick={showDashboardHome}
5350
5853
  >
5351
- Builder
5854
+ <Wrench size={15} aria-hidden="true" />
5855
+ <span className="workspace-nav-label">Builder</span>
5352
5856
  </button>
5353
5857
  )}
5354
5858
  managementSlot={(
@@ -5384,6 +5888,33 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5384
5888
  <button type="button" className="dm-workflow-chip-btn" onClick={exportConfig}><Download size={13} />Export</button>
5385
5889
  <button type="button" className="dm-workflow-icon-btn" onClick={() => importInputRef.current?.click()} aria-label="Import"><Import size={14} /></button>
5386
5890
  </div> : <div className="workspace-toolbar-actions">
5891
+ {showFinishSetupButton ? (
5892
+ <span className={`workspace-finish-setup-control${activationComplete ? " is-complete" : ""}`}>
5893
+ <button
5894
+ type="button"
5895
+ className="workspace-finish-setup-trigger"
5896
+ onClick={() => {
5897
+ setWorkspaceView("dashboards");
5898
+ setActivationPanelOpen((open) => !open);
5899
+ }}
5900
+ >
5901
+ {activationComplete ? null : <Hourglass size={15} />}
5902
+ <span>{activationComplete ? "Setup Complete" : "Finish Workspace Setup"}</span>
5903
+ </button>
5904
+ {activationComplete ? (
5905
+ <button
5906
+ type="button"
5907
+ className="workspace-finish-setup-dismiss"
5908
+ aria-label="Hide completed workspace setup"
5909
+ title="Hide completed workspace setup"
5910
+ onClick={dismissFinishSetupButton}
5911
+ >
5912
+ <Check className="workspace-finish-setup-dismiss-check" size={15} aria-hidden="true" />
5913
+ <X className="workspace-finish-setup-dismiss-x" size={15} aria-hidden="true" />
5914
+ </button>
5915
+ ) : null}
5916
+ </span>
5917
+ ) : null}
5387
5918
  <button type="button" onClick={addDashboard}><Plus size={15} />New Dashboard</button>
5388
5919
  <button type="button" onClick={createWorkflow} disabled={saving}><GitBranch size={15} />New Workflow</button>
5389
5920
  <button type="button" onClick={() => importInputRef.current?.click()}><Import size={15} />Import</button>
@@ -5397,7 +5928,28 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5397
5928
  />
5398
5929
  </header>
5399
5930
 
5400
- {workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="Builder">
5931
+ {workspaceView === "dashboards" ? <>
5932
+ {showActivationPanel ? <WorkspaceActivationPanel
5933
+ workspaceConfig={config}
5934
+ workspaceSourceRecords={workspaceSourceRecords}
5935
+ metadataGraph={lensMetadataGraph}
5936
+ showLenses={true}
5937
+ onStepAction={(step) => {
5938
+ if (step?.id === "add-widget") return openAddWidgetBuilder();
5939
+ if (step?.id === "create-workflow") {
5940
+ createWorkflow();
5941
+ return true;
5942
+ }
5943
+ return false;
5944
+ }}
5945
+ onOpenHelper={() => {
5946
+ setHelperIntent("explain");
5947
+ setHelperInitialPrompt("Help me finish setting up this workspace.");
5948
+ setHelperInitialThread(null);
5949
+ setHelperOpen(true);
5950
+ }}
5951
+ /> : null}
5952
+ <section className="workspace-table" id="dashboards" aria-label="Builder">
5401
5953
  <div className="workspace-table-heading">
5402
5954
  <strong>Builder</strong>
5403
5955
  <span>{dashboards.length} dashboard{dashboards.length === 1 ? "" : "s"} · {workflows.length} workflow{workflows.length === 1 ? "" : "s"}</span>
@@ -5564,7 +6116,8 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5564
6116
  )}
5565
6117
  </span>
5566
6118
  </div>)}
5567
- </section> : null}
6119
+ </section>
6120
+ </> : null}
5568
6121
 
5569
6122
  {workspaceView === "builder" ? <section className="workspace-canvas" id="canvas" aria-label="Composable dashboard canvas">
5570
6123
  <div className="workspace-tabs">
@@ -5836,14 +6389,12 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5836
6389
  </small>
5837
6390
  </label> : null}
5838
6391
  {selectedWidget.kind === "view" ? <section className="workspace-field-stack">
5839
- <div className="workspace-twenty-config" role="group" aria-label="View widget settings">
5840
- <p className="workspace-panel-label">Settings</p>
5841
- <WidgetSettingsRow icon={Table2} label="Layout" value={selectedWidget.config?.layout || "Table"} disabled />
5842
- <WidgetSettingsRow icon={Box} label="Source" value={summarizeSource(selectedWidget)} onClick={() => setInspectorPath("source")} />
5843
- <WidgetSettingsRow icon={List} label="Fields" value={summarizeFields(selectedResolvedWidget || selectedWidget)} onClick={() => setInspectorPath("fields")} />
5844
- <WidgetSettingsRow icon={Filter} label="Filter" value={summarizeFilter(selectedResolvedWidget || selectedWidget)} onClick={() => setInspectorPath("filter")} />
5845
- <WidgetSettingsRow icon={SlidersHorizontal} label="Sort" value={summarizeSort(selectedResolvedWidget || selectedWidget)} onClick={() => setInspectorPath("sort")} />
5846
- </div>
6392
+ <TableViewConfig
6393
+ widget={selectedWidget}
6394
+ dataModelTable={resolveDataModelTable(dataModelTables, selectedWidget.config?.binding)}
6395
+ onChange={replaceSelectedWidgetConfig}
6396
+ onSubPage={(name) => setInspectorPath(name)}
6397
+ />
5847
6398
  </section> : null}
5848
6399
  </section> : null}
5849
6400
  {!selectedWidget ? <section>