@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.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/QUICKSTART.md +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +172 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +531 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +274 -18
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +62 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +554 -48
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +24 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +534 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +82 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
- package/dist/index.js +127 -44
- 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: "
|
|
543
|
+
version: "1",
|
|
481
544
|
provider: "growthub-native",
|
|
482
|
-
nodes: [
|
|
483
|
-
|
|
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
|
|
526
|
-
|
|
527
|
-
|
|
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:
|
|
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" ? <
|
|
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: (
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
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" ?
|
|
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
|
-
<
|
|
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" ?
|
|
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>
|
|
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"
|
|
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
|
-
<
|
|
5832
|
-
|
|
5833
|
-
|
|
5834
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
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>
|