@growthub/cli 0.9.10 → 0.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/assets/worker-kits/creative-strategist-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-agency-portal-starter-v1/kit.json +4 -1
- package/assets/worker-kits/growthub-ai-website-cloner-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-creative-video-pipeline-v1/kit.json +4 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integration-entities/route.js +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +389 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +362 -15
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +5 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +5 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +625 -56
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/growthub-connection-normalizer.js +12 -16
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/index.js +61 -11
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/domain/integrations.js +31 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +3 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +433 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +112 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-email-marketing-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-geo-seo-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-hyperframes-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-marketing-skills-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-open-higgsfield-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-open-montage-studio-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-postiz-social-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-twenty-crm-v1/kit.json +6 -3
- package/assets/worker-kits/growthub-video-use-studio-v1/kit.json +5 -2
- package/assets/worker-kits/growthub-zernio-social-v1/kit.json +5 -2
- package/dist/index.js +1750 -433
- package/package.json +1 -1
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
const providerAliases = {
|
|
2
|
-
ga4: "google-analytics",
|
|
3
|
-
google_analytics: "google-analytics",
|
|
4
|
-
google_drive: "google-drive",
|
|
5
|
-
ghl: "go-high-level",
|
|
6
|
-
gohighlevel: "go-high-level",
|
|
7
|
-
meta: "meta-ads",
|
|
8
|
-
meta_ads: "meta-ads"
|
|
9
|
-
};
|
|
10
1
|
function normalizeProviderId(provider) {
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
return provider.trim().toLowerCase().replaceAll("_", "-");
|
|
3
|
+
}
|
|
4
|
+
function providerLabel(provider) {
|
|
5
|
+
return normalizeProviderId(provider)
|
|
6
|
+
.split("-")
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
|
|
9
|
+
.join(" ");
|
|
13
10
|
}
|
|
14
11
|
function isHostedRecord(row) {
|
|
15
12
|
return "provider" in row && ("ready" in row || "connectedAt" in row || "scopes" in row || "handle" in row);
|
|
@@ -41,10 +38,10 @@ function normalizeMcpAccount(account) {
|
|
|
41
38
|
const isVerified = account.isVerified === true;
|
|
42
39
|
const isConnected = isActive;
|
|
43
40
|
return {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
id: provider,
|
|
42
|
+
provider,
|
|
43
|
+
label: providerLabel(provider),
|
|
44
|
+
name: providerLabel(provider),
|
|
48
45
|
authType: normalizeConnectionType(account.connectionType),
|
|
49
46
|
status: isConnected ? "connected" : "needs-connection",
|
|
50
47
|
isConnected,
|
|
@@ -54,7 +51,6 @@ function normalizeMcpAccount(account) {
|
|
|
54
51
|
setupMode: "hosted-authority",
|
|
55
52
|
connectionMetadata: {
|
|
56
53
|
source: "growthub-mcp-accounts",
|
|
57
|
-
accountId: account.id,
|
|
58
54
|
connectionName: account.connectionName,
|
|
59
55
|
connectionType: account.connectionType,
|
|
60
56
|
isVerified,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
2
2
|
import {
|
|
3
|
-
governedWorkspaceIntegrationCatalog
|
|
3
|
+
governedWorkspaceIntegrationCatalog,
|
|
4
|
+
normalizeIntegrationEntities
|
|
4
5
|
} from "@/lib/domain/integrations";
|
|
5
6
|
import {
|
|
6
7
|
normalizeGrowthubBridgePayload
|
|
@@ -136,10 +137,10 @@ function mergeBridgeRows(rows) {
|
|
|
136
137
|
});
|
|
137
138
|
if (!row) return catalogItem;
|
|
138
139
|
seenProviders.add(row.provider || row.id || catalogItem.provider);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
return {
|
|
141
|
+
...catalogItem,
|
|
142
|
+
label: catalogItem.label,
|
|
143
|
+
name: catalogItem.name,
|
|
143
144
|
icon: row.icon || catalogItem.icon,
|
|
144
145
|
description: row.description || catalogItem.description,
|
|
145
146
|
category: row.category || catalogItem.category,
|
|
@@ -150,7 +151,6 @@ function mergeBridgeRows(rows) {
|
|
|
150
151
|
setupMode: row.setupMode || catalogItem.setupMode,
|
|
151
152
|
status: row.status || (row.isConnected || row.isActive ? "connected" : catalogItem.status),
|
|
152
153
|
connectionId: row.connectionId,
|
|
153
|
-
accountId: row.accountId,
|
|
154
154
|
secretEnvName: row.secretEnvName,
|
|
155
155
|
connectionMetadata: row.connectionMetadata || row.metadata,
|
|
156
156
|
metadata: row.metadata || row.connectionMetadata
|
|
@@ -167,8 +167,9 @@ function mergeBridgeRows(rows) {
|
|
|
167
167
|
function toDiscoveredIntegration(row) {
|
|
168
168
|
const provider = row.provider || row.id || "unknown-provider";
|
|
169
169
|
const label = row.label || row.name || provider;
|
|
170
|
-
const isDataPipeline = ["windsor-ai", "google-sheets", "google-analytics", "shopify", "meta-ads"].includes(provider);
|
|
171
170
|
const isConnected = row.isConnected ?? row.status === "connected";
|
|
171
|
+
const lane = typeof row.lane === "string" && row.lane ? row.lane : "workspace-integration";
|
|
172
|
+
const objectType = typeof row.objectType === "string" && row.objectType ? row.objectType : "mcp-connection";
|
|
172
173
|
return {
|
|
173
174
|
id: row.id || provider,
|
|
174
175
|
label,
|
|
@@ -180,19 +181,68 @@ function toDiscoveredIntegration(row) {
|
|
|
180
181
|
authType: row.authType || "oauth_first_party",
|
|
181
182
|
isConnected,
|
|
182
183
|
isActive: row.isActive ?? isConnected,
|
|
183
|
-
lane
|
|
184
|
-
objectType
|
|
184
|
+
lane,
|
|
185
|
+
objectType,
|
|
185
186
|
status: row.status || (isConnected ? "connected" : "needs-connection"),
|
|
186
187
|
authPath: row.authPath || "growthub-mcp-bridge",
|
|
187
188
|
setupMode: row.setupMode || "hosted-authority",
|
|
188
189
|
connectionId: row.connectionId,
|
|
189
|
-
accountId: row.accountId,
|
|
190
190
|
secretEnvName: row.secretEnvName,
|
|
191
191
|
connectionMetadata: row.connectionMetadata || row.metadata,
|
|
192
192
|
metadata: row.metadata || row.connectionMetadata
|
|
193
193
|
};
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Governed Integration Reference Binding — entity metadata resolution.
|
|
197
|
+
*
|
|
198
|
+
* Returns NormalizedIntegrationEntity[] for the requested integration when a
|
|
199
|
+
* server-side object resolver is available. Bridge connection discovery alone
|
|
200
|
+
* does not fabricate provider objects.
|
|
201
|
+
*
|
|
202
|
+
* Authority invariant: this function runs server-side only (API route).
|
|
203
|
+
* The browser NEVER calls provider APIs, holds tokens, or resolves entities.
|
|
204
|
+
*/
|
|
205
|
+
async function listEntityMetadataForIntegration(integrationId) {
|
|
206
|
+
if (!integrationId) return [];
|
|
207
|
+
const config = readAdapterConfig();
|
|
208
|
+
|
|
209
|
+
if (config.integrationAdapter === "growthub-bridge" &&
|
|
210
|
+
config.growthubBridge?.baseUrl &&
|
|
211
|
+
process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN) {
|
|
212
|
+
try {
|
|
213
|
+
const baseUrl = config.growthubBridge.baseUrl;
|
|
214
|
+
const entitiesPath = `/api/integrations/${encodeURIComponent(integrationId)}/entities`;
|
|
215
|
+
const url = new URL(entitiesPath, baseUrl);
|
|
216
|
+
const headers = {
|
|
217
|
+
accept: "application/json",
|
|
218
|
+
authorization: `Bearer ${process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN}`
|
|
219
|
+
};
|
|
220
|
+
if (config.growthubBridge.userId) {
|
|
221
|
+
headers["x-user-id"] = config.growthubBridge.userId;
|
|
222
|
+
}
|
|
223
|
+
const response = await fetch(url, {
|
|
224
|
+
headers,
|
|
225
|
+
next: { revalidate: 30 }
|
|
226
|
+
});
|
|
227
|
+
if (response.ok) {
|
|
228
|
+
const payload = await response.json();
|
|
229
|
+
const entities = Array.isArray(payload.entities) ? payload.entities :
|
|
230
|
+
Array.isArray(payload.objects) ? payload.objects :
|
|
231
|
+
Array.isArray(payload.data) ? payload.data :
|
|
232
|
+
Array.isArray(payload) ? payload : [];
|
|
233
|
+
const normalized = normalizeIntegrationEntities(entities);
|
|
234
|
+
if (normalized.length) return normalized;
|
|
235
|
+
}
|
|
236
|
+
} catch {
|
|
237
|
+
// No fallback object data. The UI must surface the missing resolver.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
|
|
195
244
|
export {
|
|
196
245
|
describeIntegrationAdapter,
|
|
197
|
-
listGovernedWorkspaceIntegrations
|
|
246
|
+
listGovernedWorkspaceIntegrations,
|
|
247
|
+
listEntityMetadataForIntegration
|
|
198
248
|
};
|
|
@@ -173,13 +173,43 @@ const workspaceIntegrations = [
|
|
|
173
173
|
}
|
|
174
174
|
];
|
|
175
175
|
const governedWorkspaceIntegrationCatalog = [...dataSources, ...workspaceIntegrations];
|
|
176
|
+
|
|
176
177
|
function groupIntegrationsByLane(integrations) {
|
|
177
178
|
return {
|
|
178
179
|
dataSources: integrations.filter((item) => item.lane === "data-source"),
|
|
179
180
|
workspaceIntegrations: integrations.filter((item) => item.lane === "workspace-integration")
|
|
180
181
|
};
|
|
181
182
|
}
|
|
183
|
+
|
|
184
|
+
function normalizeIntegrationEntity(entity) {
|
|
185
|
+
if (!entity || typeof entity !== "object" || Array.isArray(entity)) return null;
|
|
186
|
+
const id = typeof entity.id === "string" ? entity.id.trim() : "";
|
|
187
|
+
const label = typeof entity.label === "string" && entity.label.trim()
|
|
188
|
+
? entity.label.trim()
|
|
189
|
+
: id;
|
|
190
|
+
if (!id || !label) return null;
|
|
191
|
+
const normalized = {
|
|
192
|
+
id,
|
|
193
|
+
label,
|
|
194
|
+
secondaryLabel: typeof entity.secondaryLabel === "string" ? entity.secondaryLabel : id,
|
|
195
|
+
entityType: typeof entity.entityType === "string" ? entity.entityType : undefined,
|
|
196
|
+
provider: typeof entity.provider === "string" ? entity.provider : undefined,
|
|
197
|
+
lane: typeof entity.lane === "string" ? entity.lane : undefined,
|
|
198
|
+
status: typeof entity.status === "string" ? entity.status : undefined,
|
|
199
|
+
metadata: entity.metadata && typeof entity.metadata === "object" && !Array.isArray(entity.metadata)
|
|
200
|
+
? entity.metadata
|
|
201
|
+
: undefined
|
|
202
|
+
};
|
|
203
|
+
return Object.fromEntries(Object.entries(normalized).filter(([, value]) => value !== undefined));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function normalizeIntegrationEntities(entities) {
|
|
207
|
+
if (!Array.isArray(entities)) return [];
|
|
208
|
+
return entities.map(normalizeIntegrationEntity).filter(Boolean);
|
|
209
|
+
}
|
|
210
|
+
|
|
182
211
|
export {
|
|
183
212
|
governedWorkspaceIntegrationCatalog,
|
|
184
|
-
groupIntegrationsByLane
|
|
213
|
+
groupIntegrationsByLane,
|
|
214
|
+
normalizeIntegrationEntities
|
|
185
215
|
};
|
|
@@ -93,6 +93,7 @@ function applyPatch(currentConfig, patch) {
|
|
|
93
93
|
const next = { ...currentConfig };
|
|
94
94
|
if (patch.dashboards !== undefined) next.dashboards = patch.dashboards;
|
|
95
95
|
if (patch.widgetTypes !== undefined) next.widgetTypes = patch.widgetTypes;
|
|
96
|
+
if (patch.dataModel !== undefined) next.dataModel = patch.dataModel;
|
|
96
97
|
if (patch.canvas !== undefined && patch.canvas !== null) {
|
|
97
98
|
const patchCanvas = { ...patch.canvas };
|
|
98
99
|
if (Array.isArray(patchCanvas.tabs)) {
|
|
@@ -140,7 +141,8 @@ async function writeWorkspaceConfig(patch) {
|
|
|
140
141
|
validateWorkspaceConfig({
|
|
141
142
|
dashboards: next.dashboards,
|
|
142
143
|
widgetTypes: next.widgetTypes,
|
|
143
|
-
canvas: next.canvas
|
|
144
|
+
canvas: next.canvas,
|
|
145
|
+
dataModel: next.dataModel
|
|
144
146
|
});
|
|
145
147
|
const configPath = resolveWorkspaceConfigPath();
|
|
146
148
|
const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
function parseCsv(text) {
|
|
2
|
+
const lines = String(text || "").trim().split("\n").filter(Boolean);
|
|
3
|
+
if (!lines.length) return { columns: [], rows: [] };
|
|
4
|
+
const parseLine = (line) => {
|
|
5
|
+
const cells = [];
|
|
6
|
+
let value = "";
|
|
7
|
+
let quoted = false;
|
|
8
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
9
|
+
const char = line[index];
|
|
10
|
+
if (char === '"') {
|
|
11
|
+
if (quoted && line[index + 1] === '"') {
|
|
12
|
+
value += '"';
|
|
13
|
+
index += 1;
|
|
14
|
+
} else {
|
|
15
|
+
quoted = !quoted;
|
|
16
|
+
}
|
|
17
|
+
} else if (char === "," && !quoted) {
|
|
18
|
+
cells.push(value);
|
|
19
|
+
value = "";
|
|
20
|
+
} else {
|
|
21
|
+
value += char;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
cells.push(value);
|
|
25
|
+
return cells;
|
|
26
|
+
};
|
|
27
|
+
const columns = parseLine(lines[0]).map((cell) => cell.trim()).filter(Boolean);
|
|
28
|
+
const rows = lines.slice(1).map((line) => {
|
|
29
|
+
const cells = parseLine(line);
|
|
30
|
+
return columns.reduce((record, column, index) => {
|
|
31
|
+
record[column] = (cells[index] || "").trim();
|
|
32
|
+
return record;
|
|
33
|
+
}, {});
|
|
34
|
+
});
|
|
35
|
+
return { columns, rows };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toCsv(columns, rows) {
|
|
39
|
+
const escape = (value) => {
|
|
40
|
+
const text = String(value ?? "");
|
|
41
|
+
return /[",\n\r]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
42
|
+
};
|
|
43
|
+
const header = columns.map(escape).join(",");
|
|
44
|
+
const body = rows.map((row) => columns.map((column) => escape(row?.[column])).join(",")).join("\n");
|
|
45
|
+
return body ? `${header}\n${body}` : header;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseJsonRows(text) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(text || "[]");
|
|
51
|
+
const rows = Array.isArray(parsed) ? parsed.filter((row) => row && typeof row === "object" && !Array.isArray(row)) : [];
|
|
52
|
+
const columns = Array.from(rows.reduce((set, row) => {
|
|
53
|
+
Object.keys(row).forEach((key) => set.add(key));
|
|
54
|
+
return set;
|
|
55
|
+
}, new Set()));
|
|
56
|
+
return { columns, rows };
|
|
57
|
+
} catch {
|
|
58
|
+
return { columns: [], rows: [] };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function listWidgetEntries(workspaceConfig) {
|
|
63
|
+
const entries = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
const push = (widget, location) => {
|
|
66
|
+
if (!widget?.id || seen.has(widget.id)) return;
|
|
67
|
+
seen.add(widget.id);
|
|
68
|
+
entries.push({ widget, location });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
for (const dashboard of workspaceConfig?.dashboards || []) {
|
|
72
|
+
for (const tab of dashboard.tabs || []) {
|
|
73
|
+
for (const widget of tab.widgets || []) {
|
|
74
|
+
push(widget, {
|
|
75
|
+
dashboardId: dashboard.id,
|
|
76
|
+
dashboardName: dashboard.name,
|
|
77
|
+
tabId: tab.id,
|
|
78
|
+
tabName: tab.name,
|
|
79
|
+
widgetId: widget.id
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const canvas = workspaceConfig?.canvas;
|
|
86
|
+
for (const tab of canvas?.tabs || []) {
|
|
87
|
+
for (const widget of tab.widgets || []) {
|
|
88
|
+
push(widget, { dashboardId: null, dashboardName: null, tabId: tab.id, tabName: tab.name, widgetId: widget.id });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const widget of canvas?.widgets || []) {
|
|
92
|
+
push(widget, { dashboardId: null, dashboardName: null, tabId: null, tabName: canvas.name || "Tab 1", widgetId: widget.id });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return entries;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function bindingColumns(binding) {
|
|
99
|
+
const fields = Array.isArray(binding?.fields) ? binding.fields : [];
|
|
100
|
+
return Array.from(new Set([...fields, "id", "label", "entityType", "provider", "lane", "status"])).filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function deriveWidgetTable(widget, location) {
|
|
104
|
+
const config = widget.config || {};
|
|
105
|
+
const binding = config.binding && typeof config.binding === "object" && !Array.isArray(config.binding) ? config.binding : null;
|
|
106
|
+
|
|
107
|
+
if (widget.kind === "view") {
|
|
108
|
+
if (binding?.sourceType === "workspace-data-model") return null;
|
|
109
|
+
const source = config.source || widget.title || "Untitled";
|
|
110
|
+
const integration = binding?.mode === "integration";
|
|
111
|
+
const columns = integration ? bindingColumns(binding) : (Array.isArray(config.columns) ? config.columns : []);
|
|
112
|
+
const rows = integration && (binding.entityId || binding.entityLabel)
|
|
113
|
+
? [{ id: binding.entityId || "", label: binding.entityLabel || binding.entityId || "", entityType: binding.entityType || "", provider: binding.provider || "", lane: binding.lane || "", status: binding.status || "" }]
|
|
114
|
+
: (Array.isArray(config.rows) ? config.rows : []);
|
|
115
|
+
return { source, columns, rows, binding: binding || { mode: "manual", source: "Manual rows" }, mutable: !integration, storage: "view" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (binding?.mode === "integration") {
|
|
119
|
+
const source = binding.entityLabel || binding.source || widget.title || "Integration reference";
|
|
120
|
+
const rows = binding.entityId || binding.entityLabel
|
|
121
|
+
? [{ id: binding.entityId || "", label: binding.entityLabel || binding.entityId || "", entityType: binding.entityType || "", provider: binding.provider || "", lane: binding.lane || "", status: binding.status || "" }]
|
|
122
|
+
: [];
|
|
123
|
+
return { source, columns: bindingColumns(binding), rows, binding, mutable: false, storage: "integration" };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (binding?.mode === "json" && typeof binding.json === "string") {
|
|
127
|
+
const parsed = parseJsonRows(binding.json);
|
|
128
|
+
return { source: binding.source || widget.title || "JSON binding", columns: parsed.columns, rows: parsed.rows, binding, mutable: true, storage: "json" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (binding?.mode === "csv" && typeof binding.csv === "string") {
|
|
132
|
+
const parsed = parseCsv(binding.csv);
|
|
133
|
+
return { source: binding.source || widget.title || "CSV binding", columns: parsed.columns, rows: parsed.rows, binding, mutable: true, storage: "csv" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (binding?.mode === "manual" && Array.isArray(binding.rows)) {
|
|
137
|
+
const columns = Array.from(binding.rows.reduce((set, row) => {
|
|
138
|
+
Object.keys(row || {}).forEach((key) => set.add(key));
|
|
139
|
+
return set;
|
|
140
|
+
}, new Set()));
|
|
141
|
+
return { source: binding.source || widget.title || "Manual rows", columns, rows: binding.rows, binding, mutable: true, storage: "manual-binding" };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (widget.kind === "chart" && Array.isArray(config.values)) {
|
|
145
|
+
return {
|
|
146
|
+
source: widget.title || "Chart values",
|
|
147
|
+
columns: ["Index", "Value"],
|
|
148
|
+
rows: config.values.map((value, index) => ({ Index: index + 1, Value: value })),
|
|
149
|
+
binding: binding || { mode: "manual", source: "Chart values" },
|
|
150
|
+
mutable: true,
|
|
151
|
+
storage: "chart-values"
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function tableId(source, columns) {
|
|
159
|
+
return `table:${source}:${columns.join("\0")}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeManualObjects(workspaceConfig) {
|
|
163
|
+
return Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function deriveManualObjectTable(object) {
|
|
167
|
+
const columns = Array.isArray(object.columns) ? object.columns.filter(Boolean) : [];
|
|
168
|
+
const rows = Array.isArray(object.rows) ? object.rows.filter((row) => row && typeof row === "object" && !Array.isArray(row)) : [];
|
|
169
|
+
const source = object.source || object.label || object.name || "Manual object";
|
|
170
|
+
return {
|
|
171
|
+
id: `manual-object:${object.id || source}`,
|
|
172
|
+
label: object.label || object.name || source,
|
|
173
|
+
source,
|
|
174
|
+
columns,
|
|
175
|
+
rows,
|
|
176
|
+
binding: object.binding || { mode: "manual", source: "Data Model" },
|
|
177
|
+
mutable: true,
|
|
178
|
+
storage: "manual-object",
|
|
179
|
+
objectId: object.id,
|
|
180
|
+
widgetRefs: [],
|
|
181
|
+
fieldSettings: {
|
|
182
|
+
hidden: Array.isArray(object.fieldSettings?.hidden) ? object.fieldSettings.hidden : [],
|
|
183
|
+
order: Array.isArray(object.fieldSettings?.order) ? object.fieldSettings.order : columns
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function listWorkspaceDataModelTables(workspaceConfig) {
|
|
189
|
+
const widgetEntries = listWidgetEntries(workspaceConfig);
|
|
190
|
+
const refsByObjectId = widgetEntries.reduce((map, { widget, location }) => {
|
|
191
|
+
const binding = widget?.config?.binding;
|
|
192
|
+
if (binding?.sourceType !== "workspace-data-model" || !binding.objectId) return map;
|
|
193
|
+
const refs = map.get(binding.objectId) || [];
|
|
194
|
+
refs.push({
|
|
195
|
+
...location,
|
|
196
|
+
widgetTitle: widget.title,
|
|
197
|
+
widgetKind: widget.kind
|
|
198
|
+
});
|
|
199
|
+
map.set(binding.objectId, refs);
|
|
200
|
+
return map;
|
|
201
|
+
}, new Map());
|
|
202
|
+
const manualObjects = normalizeManualObjects(workspaceConfig).map((object) => {
|
|
203
|
+
const table = deriveManualObjectTable(object);
|
|
204
|
+
return { ...table, widgetRefs: refsByObjectId.get(object.id) || [] };
|
|
205
|
+
});
|
|
206
|
+
const widgetTables = widgetEntries
|
|
207
|
+
.map(({ widget, location }) => {
|
|
208
|
+
const table = deriveWidgetTable(widget, location);
|
|
209
|
+
if (!table) return null;
|
|
210
|
+
return {
|
|
211
|
+
id: tableId(table.source, table.columns),
|
|
212
|
+
label: table.source,
|
|
213
|
+
source: table.source,
|
|
214
|
+
columns: table.columns,
|
|
215
|
+
rows: table.rows,
|
|
216
|
+
binding: table.binding,
|
|
217
|
+
mutable: table.mutable,
|
|
218
|
+
storage: table.storage,
|
|
219
|
+
widgetRefs: [{
|
|
220
|
+
...location,
|
|
221
|
+
widgetTitle: widget.title,
|
|
222
|
+
widgetKind: widget.kind
|
|
223
|
+
}],
|
|
224
|
+
fieldSettings: {
|
|
225
|
+
hidden: Array.isArray(widget.config?.fieldSettings?.hidden) ? widget.config.fieldSettings.hidden : [],
|
|
226
|
+
order: Array.isArray(widget.config?.fieldSettings?.order) ? widget.config.fieldSettings.order : table.columns
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
})
|
|
230
|
+
.filter(Boolean);
|
|
231
|
+
return [...manualObjects, ...widgetTables];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function writeTableConfig(config, storage, columns, rows) {
|
|
235
|
+
if (storage === "view") {
|
|
236
|
+
const binding = config.binding?.mode === "manual" ? { ...config.binding, rows } : config.binding;
|
|
237
|
+
return { ...config, columns, rows, binding, fieldSettings: { hidden: config.fieldSettings?.hidden || [], order: columns } };
|
|
238
|
+
}
|
|
239
|
+
if (storage === "json") {
|
|
240
|
+
return { ...config, binding: { ...config.binding, json: JSON.stringify(rows, null, 2) } };
|
|
241
|
+
}
|
|
242
|
+
if (storage === "csv") {
|
|
243
|
+
return { ...config, binding: { ...config.binding, csv: toCsv(columns, rows) } };
|
|
244
|
+
}
|
|
245
|
+
if (storage === "manual-binding") {
|
|
246
|
+
return { ...config, binding: { ...config.binding, rows } };
|
|
247
|
+
}
|
|
248
|
+
if (storage === "chart-values") {
|
|
249
|
+
return { ...config, values: rows.map((row) => Number(row.Value)).filter((value) => Number.isFinite(value)) };
|
|
250
|
+
}
|
|
251
|
+
return config;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function applyTableMutation(workspaceConfig, table, mutate) {
|
|
255
|
+
if (table.storage === "manual-object") {
|
|
256
|
+
const objects = normalizeManualObjects(workspaceConfig);
|
|
257
|
+
const dataModel = workspaceConfig.dataModel && typeof workspaceConfig.dataModel === "object" && !Array.isArray(workspaceConfig.dataModel)
|
|
258
|
+
? workspaceConfig.dataModel
|
|
259
|
+
: {};
|
|
260
|
+
return {
|
|
261
|
+
...workspaceConfig,
|
|
262
|
+
dataModel: {
|
|
263
|
+
...dataModel,
|
|
264
|
+
objects: objects.map((object) => {
|
|
265
|
+
if (object.id !== table.objectId) return object;
|
|
266
|
+
const current = deriveManualObjectTable(object);
|
|
267
|
+
const next = mutate({ columns: current.columns, rows: current.rows });
|
|
268
|
+
return {
|
|
269
|
+
...object,
|
|
270
|
+
columns: next.columns,
|
|
271
|
+
rows: next.rows,
|
|
272
|
+
fieldSettings: { ...(object.fieldSettings || {}), order: next.columns }
|
|
273
|
+
};
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const ids = new Set((table.widgetRefs || []).map((ref) => ref.widgetId));
|
|
280
|
+
const mutateWidgets = (widgets) => (widgets || []).map((widget) => {
|
|
281
|
+
if (!ids.has(widget.id)) return widget;
|
|
282
|
+
const current = deriveWidgetTable(widget, { widgetId: widget.id });
|
|
283
|
+
if (!current?.mutable) return widget;
|
|
284
|
+
const next = mutate({ columns: current.columns, rows: current.rows });
|
|
285
|
+
return { ...widget, config: writeTableConfig(widget.config || {}, current.storage, next.columns, next.rows) };
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const dashboards = (workspaceConfig.dashboards || []).map((dashboard) => ({
|
|
289
|
+
...dashboard,
|
|
290
|
+
tabs: (dashboard.tabs || []).map((tab) => ({ ...tab, widgets: mutateWidgets(tab.widgets) }))
|
|
291
|
+
}));
|
|
292
|
+
let canvas = workspaceConfig.canvas ? { ...workspaceConfig.canvas } : {};
|
|
293
|
+
if (Array.isArray(canvas.widgets)) canvas = { ...canvas, widgets: mutateWidgets(canvas.widgets) };
|
|
294
|
+
if (Array.isArray(canvas.tabs)) canvas = { ...canvas, tabs: canvas.tabs.map((tab) => ({ ...tab, widgets: mutateWidgets(tab.widgets) })) };
|
|
295
|
+
return { ...workspaceConfig, dashboards, canvas };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function slugifyObjectName(name) {
|
|
299
|
+
return String(name || "")
|
|
300
|
+
.trim()
|
|
301
|
+
.toLowerCase()
|
|
302
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
303
|
+
.replace(/^-+|-+$/g, "")
|
|
304
|
+
|| "object";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function uniqueObjectId(workspaceConfig, name) {
|
|
308
|
+
const base = slugifyObjectName(name);
|
|
309
|
+
const used = new Set(normalizeManualObjects(workspaceConfig).map((object) => object.id));
|
|
310
|
+
if (!used.has(base)) return base;
|
|
311
|
+
let index = 2;
|
|
312
|
+
while (used.has(`${base}-${index}`)) index += 1;
|
|
313
|
+
return `${base}-${index}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function createManualBusinessObject(workspaceConfig, { name, fields } = {}) {
|
|
317
|
+
const label = String(name || "").trim();
|
|
318
|
+
const columns = Array.from(new Set((Array.isArray(fields) ? fields : String(fields || "").split(","))
|
|
319
|
+
.map((field) => String(field || "").trim())
|
|
320
|
+
.filter(Boolean)));
|
|
321
|
+
if (!label || !columns.length) return workspaceConfig;
|
|
322
|
+
const dataModel = workspaceConfig.dataModel && typeof workspaceConfig.dataModel === "object" && !Array.isArray(workspaceConfig.dataModel)
|
|
323
|
+
? workspaceConfig.dataModel
|
|
324
|
+
: {};
|
|
325
|
+
const id = uniqueObjectId(workspaceConfig, label);
|
|
326
|
+
const object = {
|
|
327
|
+
id,
|
|
328
|
+
label,
|
|
329
|
+
source: label,
|
|
330
|
+
columns,
|
|
331
|
+
rows: [],
|
|
332
|
+
binding: { mode: "manual", source: "Data Model" },
|
|
333
|
+
fieldSettings: { hidden: [], order: columns }
|
|
334
|
+
};
|
|
335
|
+
return {
|
|
336
|
+
...workspaceConfig,
|
|
337
|
+
dataModel: {
|
|
338
|
+
...dataModel,
|
|
339
|
+
objects: [...normalizeManualObjects(workspaceConfig), object]
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function addTableField(workspaceConfig, table, fieldName) {
|
|
345
|
+
const name = String(fieldName || "").trim();
|
|
346
|
+
if (!name || !table.mutable) return workspaceConfig;
|
|
347
|
+
return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => {
|
|
348
|
+
if (columns.includes(name)) return { columns, rows };
|
|
349
|
+
return { columns: [...columns, name], rows: rows.map((row) => ({ ...row, [name]: "" })) };
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function addTableRow(workspaceConfig, table) {
|
|
354
|
+
if (!table.mutable) return workspaceConfig;
|
|
355
|
+
return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => ({
|
|
356
|
+
columns,
|
|
357
|
+
rows: [...rows, Object.fromEntries(columns.map((column) => [column, ""]))]
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function updateTableCell(workspaceConfig, table, rowIndex, fieldName, value) {
|
|
362
|
+
if (!table.mutable) return workspaceConfig;
|
|
363
|
+
return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => ({
|
|
364
|
+
columns,
|
|
365
|
+
rows: rows.map((row, index) => index === rowIndex ? { ...row, [fieldName]: value } : row)
|
|
366
|
+
}));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function deleteTableRow(workspaceConfig, table, rowIndex) {
|
|
370
|
+
if (!table.mutable) return workspaceConfig;
|
|
371
|
+
return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => ({
|
|
372
|
+
columns,
|
|
373
|
+
rows: rows.filter((_, index) => index !== rowIndex)
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function duplicateTableRow(workspaceConfig, table, rowIndex) {
|
|
378
|
+
if (!table.mutable) return workspaceConfig;
|
|
379
|
+
return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => {
|
|
380
|
+
const next = [...rows];
|
|
381
|
+
if (rows[rowIndex]) next.splice(rowIndex + 1, 0, { ...rows[rowIndex] });
|
|
382
|
+
return { columns, rows: next };
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function appendRowsToTable(workspaceConfig, table, rowsToAppend) {
|
|
387
|
+
if (!table.mutable || !Array.isArray(rowsToAppend)) return workspaceConfig;
|
|
388
|
+
return applyTableMutation(workspaceConfig, table, ({ columns, rows }) => ({ columns, rows: [...rows, ...rowsToAppend] }));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function replaceTableContent(workspaceConfig, table, { columns = [], rows = [] } = {}) {
|
|
392
|
+
if (!table.mutable) return workspaceConfig;
|
|
393
|
+
return applyTableMutation(workspaceConfig, table, () => ({ columns, rows }));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function exportTableAsCsv(table) {
|
|
397
|
+
return toCsv(table.columns || [], table.rows || []);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function importTableFromCsv(text) {
|
|
401
|
+
return parseCsv(text);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function describeBindingLane(binding) {
|
|
405
|
+
if (binding?.mode === "integration" && binding.lane === "data-source") return "data-source";
|
|
406
|
+
if (binding?.mode === "integration" && binding.lane === "workspace-integration") return "workspace-integration";
|
|
407
|
+
if (binding?.mode === "integration") return "integration";
|
|
408
|
+
return "manual";
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function describeBindingMode(binding) {
|
|
412
|
+
const lane = describeBindingLane(binding);
|
|
413
|
+
if (lane === "data-source") return { label: "Data source scope", description: "Integration reference selected in the existing widget source flow. Dynamic data resolves through the governed server-side integration path." };
|
|
414
|
+
if (lane === "workspace-integration") return { label: "Workspace tool scope", description: "Workspace integration reference selected in the existing widget source flow." };
|
|
415
|
+
if (lane === "integration") return { label: "Integration scope", description: "Integration reference stored on widget.config.binding." };
|
|
416
|
+
return { label: "Manual local table", description: "Rows and fields live in the existing widget config and travel with workspace export/import." };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export {
|
|
420
|
+
addTableField,
|
|
421
|
+
addTableRow,
|
|
422
|
+
appendRowsToTable,
|
|
423
|
+
createManualBusinessObject,
|
|
424
|
+
deleteTableRow,
|
|
425
|
+
describeBindingLane,
|
|
426
|
+
describeBindingMode,
|
|
427
|
+
duplicateTableRow,
|
|
428
|
+
exportTableAsCsv,
|
|
429
|
+
importTableFromCsv,
|
|
430
|
+
listWorkspaceDataModelTables,
|
|
431
|
+
replaceTableContent,
|
|
432
|
+
updateTableCell
|
|
433
|
+
};
|