@growthub/cli 0.9.11 → 0.9.13
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/apps/workspace/app/api/settings/apis-webhooks/route.js +59 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/settings/workspace/route.js +70 -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 +406 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/global-error.jsx +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +767 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apis-webhooks/apis-webhooks-form.jsx +208 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apis-webhooks/page.jsx +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/apps-list.jsx +43 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +109 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/general/general-settings-form.jsx +134 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/general/page.jsx +25 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +23 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/page.jsx +25 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +33 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +139 -28
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +189 -2
- 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 +58 -7
- package/dist/index.js +3 -1
- package/package.json +1 -1
|
@@ -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
|
+
};
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* - `widgetTypes` palette of allowed widget kinds (label/icon)
|
|
13
13
|
* - `canvas` active canvas: layout, single-tab `widgets[]`, or
|
|
14
14
|
* multi-tab `tabs[]` + `activeTabId`, plus `bindings`
|
|
15
|
+
* - `dataModel` governed manual data objects, never dashboard widgets
|
|
15
16
|
*
|
|
16
17
|
* Other top-level fields (`id`, `name`, `description`, `capabilities`,
|
|
17
18
|
* `branding`, `pipelines`, `integrations`, `provenance`) are preserved
|
|
18
19
|
* round-trip but cannot be mutated through PATCH. The validator rejects
|
|
19
|
-
* unknown fields inside the
|
|
20
|
+
* unknown fields inside the allowlisted sections.
|
|
20
21
|
*
|
|
21
22
|
* Canonical canvas shape (mutually exclusive — never both at once):
|
|
22
23
|
*
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
const GRID_COLUMNS = 12;
|
|
40
41
|
const GRID_ROWS = 16;
|
|
41
42
|
const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
|
|
42
|
-
const KNOWN_FIELDS = ["dashboards", "widgetTypes", "canvas"];
|
|
43
|
+
const KNOWN_FIELDS = ["dashboards", "widgetTypes", "canvas", "dataModel"];
|
|
43
44
|
const KNOWN_DATA_BINDING_MODES = ["manual", "json", "csv", "integration"];
|
|
44
45
|
const KNOWN_CHART_TYPES = ["bar-vertical", "bar-horizontal", "line", "pie", "sum", "gauge"];
|
|
45
46
|
const KNOWN_FILTER_OPERATORS = ["eq", "ne", "contains", "gt", "lt", "isEmpty", "isNotEmpty"];
|
|
@@ -202,11 +203,11 @@ function defaultConfigFor(kind) {
|
|
|
202
203
|
return { values: [58, 36, 72, 48, 64], binding: SAMPLE_DATA_BINDINGS.reportingJson };
|
|
203
204
|
case "view":
|
|
204
205
|
return {
|
|
205
|
-
source: "
|
|
206
|
+
source: "",
|
|
206
207
|
layout: "Table",
|
|
207
|
-
columns: [
|
|
208
|
-
rows:
|
|
209
|
-
binding:
|
|
208
|
+
columns: [],
|
|
209
|
+
rows: [],
|
|
210
|
+
binding: { mode: "manual", source: "Static rows", rows: [] }
|
|
210
211
|
};
|
|
211
212
|
case "iframe":
|
|
212
213
|
return { url: "" };
|
|
@@ -247,7 +248,13 @@ const DASHBOARD_TEMPLATES = [
|
|
|
247
248
|
dashboard: { name: "Client Portal", status: "draft" },
|
|
248
249
|
widgets: [
|
|
249
250
|
createWidget("rich-text", "Client Summary", { x: 0, y: 0, w: 4, h: 4 }, { text: "Current client priorities, owner notes, and next milestone.", binding: { mode: "manual", source: "Manual text", rows: [] } }),
|
|
250
|
-
createWidget("view", "Companies", { x: 4, y: 0, w: 5, h: 5 }
|
|
251
|
+
createWidget("view", "Companies", { x: 4, y: 0, w: 5, h: 5 }, {
|
|
252
|
+
source: "Companies",
|
|
253
|
+
layout: "Table",
|
|
254
|
+
columns: ["Name", "Domain Name"],
|
|
255
|
+
rows: SAMPLE_VIEW_ROWS,
|
|
256
|
+
binding: SAMPLE_DATA_BINDINGS.companiesManual
|
|
257
|
+
}),
|
|
251
258
|
createWidget("iframe", "Client Portal Embed", { x: 9, y: 0, w: 3, h: 5 }, { url: "" }),
|
|
252
259
|
createWidget("chart", "Delivery Health", { x: 0, y: 4, w: 4, h: 4 }, { values: [72, 64, 81, 58, 76], binding: SAMPLE_DATA_BINDINGS.reportingJson })
|
|
253
260
|
]
|
|
@@ -414,6 +421,9 @@ function validateStaticDataBinding(binding, path, errors) {
|
|
|
414
421
|
if (binding.endpointRef !== undefined && typeof binding.endpointRef !== "string") {
|
|
415
422
|
errors.push(`${path}.endpointRef must be a string`);
|
|
416
423
|
}
|
|
424
|
+
if (binding.objectId !== undefined && typeof binding.objectId !== "string") {
|
|
425
|
+
errors.push(`${path}.objectId must be a string`);
|
|
426
|
+
}
|
|
417
427
|
if (binding.integrationId !== undefined && typeof binding.integrationId !== "string") {
|
|
418
428
|
errors.push(`${path}.integrationId must be a string`);
|
|
419
429
|
}
|
|
@@ -768,6 +778,46 @@ function validateCanvasConfig(canvas, errors) {
|
|
|
768
778
|
}
|
|
769
779
|
}
|
|
770
780
|
|
|
781
|
+
function validateDataModelConfig(dataModel, errors) {
|
|
782
|
+
if (dataModel === undefined) return;
|
|
783
|
+
if (!isPlainObject(dataModel)) {
|
|
784
|
+
errors.push("dataModel must be a plain object");
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (dataModel.objects === undefined) return;
|
|
788
|
+
if (!Array.isArray(dataModel.objects)) {
|
|
789
|
+
errors.push("dataModel.objects must be an array");
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const ids = new Set();
|
|
793
|
+
dataModel.objects.forEach((object, index) => {
|
|
794
|
+
const prefix = `dataModel.objects[${index}]`;
|
|
795
|
+
if (!isPlainObject(object)) {
|
|
796
|
+
errors.push(`${prefix} must be an object`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (typeof object.id !== "string" || !object.id.trim()) {
|
|
800
|
+
errors.push(`${prefix}.id must be a non-empty string`);
|
|
801
|
+
} else if (ids.has(object.id)) {
|
|
802
|
+
errors.push(`${prefix}.id duplicates an earlier object id`);
|
|
803
|
+
} else {
|
|
804
|
+
ids.add(object.id);
|
|
805
|
+
}
|
|
806
|
+
if (typeof object.label !== "string" || !object.label.trim()) errors.push(`${prefix}.label must be a non-empty string`);
|
|
807
|
+
if (object.source !== undefined && typeof object.source !== "string") errors.push(`${prefix}.source must be a string`);
|
|
808
|
+
validateStringArray(object.columns, `${prefix}.columns`, errors);
|
|
809
|
+
if (!Array.isArray(object.rows)) {
|
|
810
|
+
errors.push(`${prefix}.rows must be an array`);
|
|
811
|
+
} else {
|
|
812
|
+
object.rows.forEach((row, rowIndex) => {
|
|
813
|
+
if (!isPlainObject(row)) errors.push(`${prefix}.rows[${rowIndex}] must be a plain object`);
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
validateStaticDataBinding(object.binding, `${prefix}.binding`, errors);
|
|
817
|
+
validateFieldSettings(object.fieldSettings, `${prefix}.fieldSettings`, errors);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
|
|
771
821
|
function validateTemplateWidgetArray(widgets, contextPath, errors) {
|
|
772
822
|
if (!Array.isArray(widgets)) {
|
|
773
823
|
errors.push(`${contextPath} must be an array`);
|
|
@@ -996,6 +1046,7 @@ function validateWorkspaceConfig(nextConfig) {
|
|
|
996
1046
|
if (nextConfig.dashboards !== undefined) validateDashboardArray(nextConfig.dashboards, errors);
|
|
997
1047
|
if (nextConfig.widgetTypes !== undefined && !Array.isArray(nextConfig.widgetTypes)) errors.push("widgetTypes must be an array");
|
|
998
1048
|
if (nextConfig.canvas !== undefined) validateCanvasConfig(nextConfig.canvas, errors);
|
|
1049
|
+
if (nextConfig.dataModel !== undefined) validateDataModelConfig(nextConfig.dataModel, errors);
|
|
999
1050
|
if (errors.length) {
|
|
1000
1051
|
const error = new Error(`invalid workspace config: ${errors.join("; ")}`);
|
|
1001
1052
|
error.code = "INVALID_WORKSPACE_CONFIG";
|
package/dist/index.js
CHANGED
|
@@ -14199,7 +14199,9 @@ async function addAllowedHostname(host, opts) {
|
|
|
14199
14199
|
return;
|
|
14200
14200
|
}
|
|
14201
14201
|
const normalized = normalizeHostnameInput(host);
|
|
14202
|
-
const current = new Set(
|
|
14202
|
+
const current = new Set(
|
|
14203
|
+
(config.server.allowedHostnames ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
14204
|
+
);
|
|
14203
14205
|
const existed = current.has(normalized);
|
|
14204
14206
|
current.add(normalized);
|
|
14205
14207
|
config.server.allowedHostnames = Array.from(current).sort();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthub/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
4
4
|
"description": "Growthub Local is a control plane for forked worker kits. The CLI is the executor, the hosted app is the identity authority, the worker kit is the unit of portable agent infrastructure, and the fork is the operator's personal branch of that infrastructure — policy-governed, trace-backed, and self-healing.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|