@growthub/cli 0.13.0 → 0.13.2
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/workspace/sandbox-run/route.js +50 -25
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +522 -35
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +242 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +52 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +1203 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +163 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +190 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +64 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +376 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1062 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +492 -28
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +114 -30
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +21 -1
- package/package.json +1 -1
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
Globe,
|
|
27
27
|
GripVertical,
|
|
28
28
|
Grid2X2,
|
|
29
|
+
GitBranch,
|
|
29
30
|
Hash,
|
|
30
31
|
Home,
|
|
31
32
|
Import,
|
|
@@ -37,6 +38,7 @@ import {
|
|
|
37
38
|
List,
|
|
38
39
|
Mail,
|
|
39
40
|
Maximize2,
|
|
41
|
+
MoreVertical,
|
|
40
42
|
Pencil,
|
|
41
43
|
PieChart,
|
|
42
44
|
Plus,
|
|
@@ -90,6 +92,7 @@ const CUSTOM_API_SOURCE_TYPE = "custom-api-webhooks";
|
|
|
90
92
|
const DATA_MODEL_SOURCE_TYPE = "workspace-data-model";
|
|
91
93
|
const LIVE_SOURCE_TYPE = "workspace-source-records";
|
|
92
94
|
const TESTED_SOURCE_STATUSES = new Set(["connected", "approved", "ok", "success"]);
|
|
95
|
+
const HIDDEN_SANDBOX_OBJECT_IDS = new Set(["workspace-helper-sandbox"]);
|
|
93
96
|
|
|
94
97
|
const SOURCE_TYPE_OBJECTS = [
|
|
95
98
|
{
|
|
@@ -347,6 +350,188 @@ function createDashboardRecord(name = "Untitled") {
|
|
|
347
350
|
};
|
|
348
351
|
}
|
|
349
352
|
|
|
353
|
+
function slugifyWorkflowName(name) {
|
|
354
|
+
const slug = String(name || "workflow")
|
|
355
|
+
.trim()
|
|
356
|
+
.toLowerCase()
|
|
357
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
358
|
+
.replace(/^-+|-+$/g, "");
|
|
359
|
+
return slug || "workflow";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function getDataModelObject(config, objectId) {
|
|
363
|
+
const objects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
|
|
364
|
+
return objects.find((object) => object?.id === objectId) || null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function getWorkflowSandboxObject(config) {
|
|
368
|
+
const objects = Array.isArray(config?.dataModel?.objects) ? config.dataModel.objects : [];
|
|
369
|
+
return objects.find((object) => {
|
|
370
|
+
if (object?.objectType !== "sandbox-environment") return false;
|
|
371
|
+
const id = String(object?.id || "").trim();
|
|
372
|
+
return id && !HIDDEN_SANDBOX_OBJECT_IDS.has(id);
|
|
373
|
+
}) || null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function listBuilderWorkflowItems(config) {
|
|
377
|
+
const navFolders = getDataModelObject(config, "nav-folders");
|
|
378
|
+
const rows = Array.isArray(navFolders?.rows) ? navFolders.rows : [];
|
|
379
|
+
return rows.flatMap((folder) => {
|
|
380
|
+
const folderId = String(folder?.id || folder?.name || "").trim();
|
|
381
|
+
const folderName = String(folder?.name || "").trim();
|
|
382
|
+
const items = Array.isArray(folder?.items) ? folder.items : [];
|
|
383
|
+
return items
|
|
384
|
+
.filter((item) => item?.type === "workflow" && item?.objectId && item?.rowId)
|
|
385
|
+
.map((item) => {
|
|
386
|
+
const objectId = String(item.objectId);
|
|
387
|
+
const rowId = String(item.rowId);
|
|
388
|
+
const itemId = String(item.id || `${objectId}:${rowId}`);
|
|
389
|
+
const sandboxObject = getDataModelObject(config, objectId);
|
|
390
|
+
const sandboxRows = Array.isArray(sandboxObject?.rows) ? sandboxObject.rows : [];
|
|
391
|
+
const sandboxRow = sandboxRows.find((row) => String(row?.Name || row?.name || row?.slug || row?.id || "").trim() === rowId);
|
|
392
|
+
return {
|
|
393
|
+
id: itemId,
|
|
394
|
+
folderId,
|
|
395
|
+
objectId,
|
|
396
|
+
rowId,
|
|
397
|
+
fieldName: String(item.fieldName || "orchestrationConfig"),
|
|
398
|
+
label: String(item.label || rowId),
|
|
399
|
+
folderName: folderName || "Builder",
|
|
400
|
+
lifecycleStatus: String(item.lifecycleStatus || item.status || sandboxRow?.lifecycleStatus || sandboxRow?.status || "draft").trim(),
|
|
401
|
+
version: String(sandboxRow?.version || "0").trim(),
|
|
402
|
+
updatedAt: String(sandboxRow?.orchestrationPublishedAt || sandboxRow?.orchestrationDraftUpdatedAt || sandboxRow?.lastTested || "").trim()
|
|
403
|
+
};
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function formatBuilderTimestamp(value) {
|
|
409
|
+
const raw = String(value || "").trim();
|
|
410
|
+
if (!raw || raw === "new") return raw;
|
|
411
|
+
const date = new Date(raw);
|
|
412
|
+
if (Number.isNaN(date.getTime())) return raw;
|
|
413
|
+
const yyyy = date.getFullYear();
|
|
414
|
+
const mm = String(date.getMonth() + 1).padStart(2, "0");
|
|
415
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
416
|
+
const hh = String(date.getHours()).padStart(2, "0");
|
|
417
|
+
const min = String(date.getMinutes()).padStart(2, "0");
|
|
418
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${min}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function updateWorkflowFolderItemInConfig(config, workflow, updater) {
|
|
422
|
+
const workflowId = String(workflow?.id || "").trim();
|
|
423
|
+
if (!workflowId) return config;
|
|
424
|
+
return {
|
|
425
|
+
...config,
|
|
426
|
+
dataModel: {
|
|
427
|
+
...(config.dataModel || {}),
|
|
428
|
+
objects: (Array.isArray(config.dataModel?.objects) ? config.dataModel.objects : []).map((object) => {
|
|
429
|
+
if (object?.id !== "nav-folders") return object;
|
|
430
|
+
return {
|
|
431
|
+
...object,
|
|
432
|
+
rows: (Array.isArray(object.rows) ? object.rows : []).map((folder) => ({
|
|
433
|
+
...folder,
|
|
434
|
+
items: (Array.isArray(folder.items) ? folder.items : []).map((item) =>
|
|
435
|
+
String(item?.id || `${item?.objectId}:${item?.rowId}`) === workflowId ? updater(item) : item
|
|
436
|
+
)
|
|
437
|
+
}))
|
|
438
|
+
};
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function createBlankWorkflowSandboxRow(rowId, nowIso) {
|
|
445
|
+
const draftGraph = JSON.stringify({
|
|
446
|
+
version: "0",
|
|
447
|
+
provider: "growthub-native",
|
|
448
|
+
nodes: [],
|
|
449
|
+
edges: []
|
|
450
|
+
}, null, 2);
|
|
451
|
+
return {
|
|
452
|
+
Name: rowId,
|
|
453
|
+
lifecycleStatus: "draft",
|
|
454
|
+
version: "0",
|
|
455
|
+
runLocality: "local",
|
|
456
|
+
schedulerRegistryId: "",
|
|
457
|
+
runtime: "node",
|
|
458
|
+
adapter: "local-agent-host",
|
|
459
|
+
agentHost: "claude_local",
|
|
460
|
+
intelligenceType: "agent-host",
|
|
461
|
+
localModel: "",
|
|
462
|
+
localEndpoint: "",
|
|
463
|
+
intelligenceAdapterMode: "ollama",
|
|
464
|
+
envRefs: "",
|
|
465
|
+
networkAllow: "false",
|
|
466
|
+
allowList: "",
|
|
467
|
+
instructions: "Draft workflow created from Builder. Configure nodes, save draft, test successfully, then publish to v1.",
|
|
468
|
+
command: "",
|
|
469
|
+
orchestrationConfig: "",
|
|
470
|
+
orchestrationDraftConfig: draftGraph,
|
|
471
|
+
orchestrationDraftStatus: "draft",
|
|
472
|
+
orchestrationDraftUpdatedAt: nowIso,
|
|
473
|
+
orchestrationDraftBaseVersion: "0",
|
|
474
|
+
orchestrationDraftTestPassed: false,
|
|
475
|
+
orchestrationDraftTestedConfig: "",
|
|
476
|
+
orchestrationDeltas: [],
|
|
477
|
+
timeoutMs: "180000",
|
|
478
|
+
resolverTemplateId: "",
|
|
479
|
+
connectorKind: "local-agent-host",
|
|
480
|
+
executionLane: "workflow",
|
|
481
|
+
status: "draft",
|
|
482
|
+
lastTested: "",
|
|
483
|
+
lastRunId: "",
|
|
484
|
+
lastSourceId: "",
|
|
485
|
+
lastResponse: ""
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function addWorkflowFolderShortcut(dataModel, workflow) {
|
|
490
|
+
const objects = Array.isArray(dataModel?.objects) ? dataModel.objects : [];
|
|
491
|
+
const navIndex = objects.findIndex((object) => object?.id === "nav-folders");
|
|
492
|
+
if (navIndex < 0) return dataModel;
|
|
493
|
+
const navObject = objects[navIndex];
|
|
494
|
+
const rows = Array.isArray(navObject.rows) ? navObject.rows : [];
|
|
495
|
+
const folderName = "Builder";
|
|
496
|
+
const existingFolder = rows.find((row) => String(row?.name || "").trim().toLowerCase() === folderName.toLowerCase());
|
|
497
|
+
const item = {
|
|
498
|
+
id: generateId("item"),
|
|
499
|
+
type: "workflow",
|
|
500
|
+
objectId: workflow.objectId,
|
|
501
|
+
rowId: workflow.rowId,
|
|
502
|
+
fieldName: "orchestrationConfig",
|
|
503
|
+
label: workflow.label,
|
|
504
|
+
builderManaged: true,
|
|
505
|
+
icon: "GitBranch",
|
|
506
|
+
color: "#111827",
|
|
507
|
+
iconBg: "#f3f4f6"
|
|
508
|
+
};
|
|
509
|
+
const nextRows = existingFolder
|
|
510
|
+
? rows.map((row) => {
|
|
511
|
+
if (row !== existingFolder) return row;
|
|
512
|
+
const items = Array.isArray(row.items) ? row.items : [];
|
|
513
|
+
const exists = items.some((entry) => entry?.type === "workflow" && entry?.objectId === item.objectId && entry?.rowId === item.rowId);
|
|
514
|
+
return exists ? row : { ...row, collapsed: false, items: [...items, item] };
|
|
515
|
+
})
|
|
516
|
+
: [
|
|
517
|
+
...rows,
|
|
518
|
+
{
|
|
519
|
+
id: generateId("folder"),
|
|
520
|
+
name: folderName,
|
|
521
|
+
order: rows.length,
|
|
522
|
+
collapsed: false,
|
|
523
|
+
icon: "Folder",
|
|
524
|
+
color: "#f97316",
|
|
525
|
+
iconBg: "#fff7ed",
|
|
526
|
+
items: [item]
|
|
527
|
+
}
|
|
528
|
+
];
|
|
529
|
+
return {
|
|
530
|
+
...dataModel,
|
|
531
|
+
objects: objects.map((object, index) => index === navIndex ? { ...navObject, rows: nextRows } : object)
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
350
535
|
function createEmptyTab(name = "Untitled") {
|
|
351
536
|
return {
|
|
352
537
|
id: generateId("tab"),
|
|
@@ -3329,7 +3514,12 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3329
3514
|
const [previewTemplateId, setPreviewTemplateId] = useState(null);
|
|
3330
3515
|
const [editingDashboardId, setEditingDashboardId] = useState(null);
|
|
3331
3516
|
const [editingDashboardDraft, setEditingDashboardDraft] = useState("");
|
|
3517
|
+
const [editingWorkflowId, setEditingWorkflowId] = useState(null);
|
|
3518
|
+
const [editingWorkflowDraft, setEditingWorkflowDraft] = useState("");
|
|
3332
3519
|
const [workspaceView, setWorkspaceView] = useState("dashboards");
|
|
3520
|
+
const [builderListFilter, setBuilderListFilter] = useState({ type: "all", query: "" });
|
|
3521
|
+
const [builderActionMenuId, setBuilderActionMenuId] = useState(null);
|
|
3522
|
+
const [builderActionMenuPlacement, setBuilderActionMenuPlacement] = useState(null);
|
|
3333
3523
|
const [activeDashboardId, setActiveDashboardId] = useState(() =>
|
|
3334
3524
|
getActiveDashboardId(
|
|
3335
3525
|
Array.isArray(initialConfig.dashboards) && initialConfig.dashboards.length ? initialConfig.dashboards : [],
|
|
@@ -3339,6 +3529,34 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3339
3529
|
const gridRef = useRef(null);
|
|
3340
3530
|
const canvas = config.canvas;
|
|
3341
3531
|
const dashboards = config.dashboards || [];
|
|
3532
|
+
const workflows = useMemo(() => listBuilderWorkflowItems(config), [config]);
|
|
3533
|
+
const builderItems = useMemo(() => {
|
|
3534
|
+
const dashboardItems = dashboards.map((dashboard, index) => ({
|
|
3535
|
+
type: "dashboard",
|
|
3536
|
+
id: dashboard.id,
|
|
3537
|
+
title: dashboard.name,
|
|
3538
|
+
itemKind: "Dashboard",
|
|
3539
|
+
updatedAt: formatBuilderTimestamp(dashboard.updatedAt || ""),
|
|
3540
|
+
status: dashboard.status || "draft",
|
|
3541
|
+
index,
|
|
3542
|
+
dashboard
|
|
3543
|
+
}));
|
|
3544
|
+
const workflowItems = workflows.map((workflow) => ({
|
|
3545
|
+
type: "workflow",
|
|
3546
|
+
id: workflow.id || `${workflow.objectId}:${workflow.rowId}`,
|
|
3547
|
+
title: workflow.label,
|
|
3548
|
+
itemKind: "Workflow",
|
|
3549
|
+
updatedAt: formatBuilderTimestamp(workflow.updatedAt) || `v${workflow.version || "0"}`,
|
|
3550
|
+
status: workflow.lifecycleStatus || "draft",
|
|
3551
|
+
workflow
|
|
3552
|
+
}));
|
|
3553
|
+
const q = builderListFilter.query.trim().toLowerCase();
|
|
3554
|
+
return [...dashboardItems, ...workflowItems].filter((item) => {
|
|
3555
|
+
if (builderListFilter.type !== "all" && item.type !== builderListFilter.type) return false;
|
|
3556
|
+
if (!q) return true;
|
|
3557
|
+
return [item.title, item.itemKind, item.status, item.type].some((part) => String(part || "").toLowerCase().includes(q));
|
|
3558
|
+
});
|
|
3559
|
+
}, [builderListFilter, dashboards, workflows]);
|
|
3342
3560
|
const resolvedActiveDashboardId = getActiveDashboardId(dashboards, activeDashboardId);
|
|
3343
3561
|
const resolvedActiveDashboardIndex = activeDashboardIndex(dashboards, resolvedActiveDashboardId);
|
|
3344
3562
|
const widgetTypes = config.widgetTypes;
|
|
@@ -3506,6 +3724,59 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3506
3724
|
});
|
|
3507
3725
|
}, [activeDashboardId]);
|
|
3508
3726
|
|
|
3727
|
+
const createWorkflow = useCallback(async () => {
|
|
3728
|
+
if (saving) return;
|
|
3729
|
+
const nowIso = new Date().toISOString();
|
|
3730
|
+
const existing = getWorkflowSandboxObject(config);
|
|
3731
|
+
if (!existing) {
|
|
3732
|
+
setConfigMessage("Workflow sandbox object is missing.");
|
|
3733
|
+
return;
|
|
3734
|
+
}
|
|
3735
|
+
const sandboxObjectId = String(existing.id || "").trim();
|
|
3736
|
+
const rows = Array.isArray(existing.rows) ? existing.rows : [];
|
|
3737
|
+
const base = slugifyWorkflowName(`workflow-${rows.length + 1}`);
|
|
3738
|
+
const existingIds = new Set(rows.map((row) => String(row?.Name || row?.name || row?.id || "").trim()));
|
|
3739
|
+
let rowId = base;
|
|
3740
|
+
let suffix = 2;
|
|
3741
|
+
while (existingIds.has(rowId)) {
|
|
3742
|
+
rowId = `${base}-${suffix}`;
|
|
3743
|
+
suffix += 1;
|
|
3744
|
+
}
|
|
3745
|
+
const sandboxRow = createBlankWorkflowSandboxRow(rowId, nowIso);
|
|
3746
|
+
const nextDataModel = {
|
|
3747
|
+
...(config.dataModel || {}),
|
|
3748
|
+
objects: (Array.isArray(config.dataModel?.objects) ? config.dataModel.objects : []).map((object) =>
|
|
3749
|
+
object?.id === sandboxObjectId
|
|
3750
|
+
? { ...object, rows: [...(Array.isArray(object.rows) ? object.rows : []), sandboxRow] }
|
|
3751
|
+
: object
|
|
3752
|
+
)
|
|
3753
|
+
};
|
|
3754
|
+
const finalDataModel = addWorkflowFolderShortcut(nextDataModel, {
|
|
3755
|
+
objectId: sandboxObjectId,
|
|
3756
|
+
rowId,
|
|
3757
|
+
label: rowId
|
|
3758
|
+
});
|
|
3759
|
+
setSaving(true);
|
|
3760
|
+
try {
|
|
3761
|
+
const response = await fetch("/api/workspace", {
|
|
3762
|
+
method: "PATCH",
|
|
3763
|
+
headers: { "content-type": "application/json" },
|
|
3764
|
+
body: JSON.stringify({ dataModel: finalDataModel })
|
|
3765
|
+
});
|
|
3766
|
+
const payload = await response.json();
|
|
3767
|
+
if (!response.ok || !payload.workspaceConfig) {
|
|
3768
|
+
throw new Error(payload.error || "Failed to create workflow");
|
|
3769
|
+
}
|
|
3770
|
+
setConfig((prev) => ({ ...prev, dataModel: payload.workspaceConfig.dataModel }));
|
|
3771
|
+
setConfigMessage(`Created workflow ${rowId}`);
|
|
3772
|
+
window.open(`/workflows?object=${sandboxObjectId}&row=${encodeURIComponent(rowId)}&field=orchestrationConfig`, "_self");
|
|
3773
|
+
} catch (error) {
|
|
3774
|
+
setConfigMessage(error.message || "Failed to create workflow");
|
|
3775
|
+
} finally {
|
|
3776
|
+
setSaving(false);
|
|
3777
|
+
}
|
|
3778
|
+
}, [config, saving]);
|
|
3779
|
+
|
|
3509
3780
|
const selectDashboard = useCallback((index) => {
|
|
3510
3781
|
setConfig((prev) => {
|
|
3511
3782
|
const synced = syncActiveDashboard(prev, activeDashboardId);
|
|
@@ -3843,6 +4114,96 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3843
4114
|
await persistWorkspaceConfig(nextConfig, activeDashboardId);
|
|
3844
4115
|
}, [activeDashboardId, config, editingDashboardDraft, persistWorkspaceConfig]);
|
|
3845
4116
|
|
|
4117
|
+
const enterWorkflowTitleEdit = useCallback((workflow) => {
|
|
4118
|
+
if (!workflow) return;
|
|
4119
|
+
setEditingWorkflowId(workflow.id);
|
|
4120
|
+
setEditingWorkflowDraft(workflow.label || workflow.rowId || "Workflow");
|
|
4121
|
+
setWorkspaceView("dashboards");
|
|
4122
|
+
}, []);
|
|
4123
|
+
|
|
4124
|
+
const confirmWorkflowTitleEdit = useCallback(async (workflow) => {
|
|
4125
|
+
if (!workflow) return;
|
|
4126
|
+
const nextLabel = editingWorkflowDraft.trim() || workflow.label || workflow.rowId || "Workflow";
|
|
4127
|
+
const nextConfig = updateWorkflowFolderItemInConfig(config, workflow, (item) => ({
|
|
4128
|
+
...item,
|
|
4129
|
+
label: nextLabel,
|
|
4130
|
+
updatedAt: new Date().toISOString()
|
|
4131
|
+
}));
|
|
4132
|
+
setEditingWorkflowId(null);
|
|
4133
|
+
setEditingWorkflowDraft("");
|
|
4134
|
+
setConfig(nextConfig);
|
|
4135
|
+
await persistWorkspaceConfig(nextConfig, activeDashboardId);
|
|
4136
|
+
}, [activeDashboardId, config, editingWorkflowDraft, persistWorkspaceConfig]);
|
|
4137
|
+
|
|
4138
|
+
const cancelWorkflowTitleEdit = useCallback((workflow) => {
|
|
4139
|
+
if (!workflow) return;
|
|
4140
|
+
if (editingWorkflowDraft.trim() !== String(workflow.label || workflow.rowId || "Workflow")) {
|
|
4141
|
+
const discard = window.confirm("Discard workflow title changes?");
|
|
4142
|
+
if (!discard) {
|
|
4143
|
+
requestAnimationFrame(() => {
|
|
4144
|
+
document.querySelector(`[data-workflow-title-input="${workflow.id}"]`)?.focus();
|
|
4145
|
+
});
|
|
4146
|
+
return;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
setEditingWorkflowId(null);
|
|
4150
|
+
setEditingWorkflowDraft("");
|
|
4151
|
+
}, [editingWorkflowDraft]);
|
|
4152
|
+
|
|
4153
|
+
const closeBuilderActionMenu = useCallback(() => {
|
|
4154
|
+
setBuilderActionMenuId(null);
|
|
4155
|
+
setBuilderActionMenuPlacement(null);
|
|
4156
|
+
}, []);
|
|
4157
|
+
|
|
4158
|
+
const openBuilderActionMenu = useCallback((item, event) => {
|
|
4159
|
+
const itemId = item?.id;
|
|
4160
|
+
if (!itemId) return;
|
|
4161
|
+
if (builderActionMenuId === itemId) {
|
|
4162
|
+
closeBuilderActionMenu();
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4165
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
4166
|
+
const menuWidth = 148;
|
|
4167
|
+
const menuHeight = item?.type === "dashboard" ? 136 : 76;
|
|
4168
|
+
const margin = 8;
|
|
4169
|
+
const left = Math.min(
|
|
4170
|
+
Math.max(margin, rect.right - menuWidth),
|
|
4171
|
+
Math.max(margin, window.innerWidth - menuWidth - margin)
|
|
4172
|
+
);
|
|
4173
|
+
const preferredTop = rect.bottom + 6;
|
|
4174
|
+
const top = preferredTop + menuHeight > window.innerHeight - margin
|
|
4175
|
+
? Math.max(margin, rect.top - menuHeight - 6)
|
|
4176
|
+
: preferredTop;
|
|
4177
|
+
setBuilderActionMenuId(itemId);
|
|
4178
|
+
setBuilderActionMenuPlacement({
|
|
4179
|
+
left: `${Math.round(left)}px`,
|
|
4180
|
+
top: `${Math.round(top)}px`
|
|
4181
|
+
});
|
|
4182
|
+
}, [builderActionMenuId, closeBuilderActionMenu]);
|
|
4183
|
+
|
|
4184
|
+
useEffect(() => {
|
|
4185
|
+
if (!builderActionMenuId) return undefined;
|
|
4186
|
+
const close = () => closeBuilderActionMenu();
|
|
4187
|
+
const closeOnPointerDown = (event) => {
|
|
4188
|
+
const target = event.target;
|
|
4189
|
+
if (target?.closest?.(".workspace-row-action-menu, .workspace-row-action-trigger")) return;
|
|
4190
|
+
closeBuilderActionMenu();
|
|
4191
|
+
};
|
|
4192
|
+
const closeOnKeyDown = (event) => {
|
|
4193
|
+
if (event.key === "Escape") closeBuilderActionMenu();
|
|
4194
|
+
};
|
|
4195
|
+
document.addEventListener("pointerdown", closeOnPointerDown);
|
|
4196
|
+
document.addEventListener("keydown", closeOnKeyDown);
|
|
4197
|
+
window.addEventListener("resize", close);
|
|
4198
|
+
window.addEventListener("scroll", close, true);
|
|
4199
|
+
return () => {
|
|
4200
|
+
document.removeEventListener("pointerdown", closeOnPointerDown);
|
|
4201
|
+
document.removeEventListener("keydown", closeOnKeyDown);
|
|
4202
|
+
window.removeEventListener("resize", close);
|
|
4203
|
+
window.removeEventListener("scroll", close, true);
|
|
4204
|
+
};
|
|
4205
|
+
}, [builderActionMenuId, closeBuilderActionMenu]);
|
|
4206
|
+
|
|
3846
4207
|
const cancelDashboardTitleEdit = useCallback((dashboard) => {
|
|
3847
4208
|
if (!dashboard) return;
|
|
3848
4209
|
if (editingDashboardDraft.trim() !== dashboard.name) {
|
|
@@ -4096,6 +4457,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4096
4457
|
const showDashboardHome = useCallback(() => {
|
|
4097
4458
|
setEditingDashboardId(null);
|
|
4098
4459
|
setEditingDashboardDraft("");
|
|
4460
|
+
setBuilderActionMenuId(null);
|
|
4099
4461
|
setWorkspaceView("dashboards");
|
|
4100
4462
|
}, []);
|
|
4101
4463
|
const resetWidgetSelectionOnOutsidePointer = useCallback((event) => {
|
|
@@ -4307,7 +4669,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4307
4669
|
run: () => setManagementOpen(true)
|
|
4308
4670
|
});
|
|
4309
4671
|
list.push({
|
|
4310
|
-
id: "workspace.
|
|
4672
|
+
id: "workspace.builder", group: "Navigation", icon: Home, label: "Go to Builder",
|
|
4311
4673
|
run: () => showDashboardHome()
|
|
4312
4674
|
});
|
|
4313
4675
|
|
|
@@ -4358,7 +4720,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4358
4720
|
className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"}
|
|
4359
4721
|
onClick={showDashboardHome}
|
|
4360
4722
|
>
|
|
4361
|
-
|
|
4723
|
+
Builder
|
|
4362
4724
|
</button>
|
|
4363
4725
|
)}
|
|
4364
4726
|
managementSlot={(
|
|
@@ -4380,12 +4742,13 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4380
4742
|
<h1>{activeDashboard?.name || "Untitled"}</h1>
|
|
4381
4743
|
</> : <>
|
|
4382
4744
|
<p>Workspace home</p>
|
|
4383
|
-
<h1>
|
|
4745
|
+
<h1>Builder</h1>
|
|
4384
4746
|
</>}
|
|
4385
4747
|
</div>
|
|
4386
4748
|
<div className="workspace-toolbar-actions">
|
|
4387
4749
|
<button type="button" onClick={() => setTemplateGalleryOpen(true)}><Grid2X2 size={15} />Templates</button>
|
|
4388
4750
|
<button type="button" onClick={addDashboard}><Plus size={15} />New Dashboard</button>
|
|
4751
|
+
<button type="button" onClick={createWorkflow} disabled={saving}><GitBranch size={15} />New Workflow</button>
|
|
4389
4752
|
<button type="button" onClick={duplicateDashboard}><Copy size={15} />Duplicate Dashboard</button>
|
|
4390
4753
|
<button type="button" onClick={() => importInputRef.current?.click()}><Import size={15} />Import</button>
|
|
4391
4754
|
<button type="button" onClick={exportConfig}><Download size={15} />Export</button>
|
|
@@ -4400,70 +4763,171 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4400
4763
|
/>
|
|
4401
4764
|
</header>
|
|
4402
4765
|
|
|
4403
|
-
{workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="
|
|
4766
|
+
{workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="Builder">
|
|
4404
4767
|
<div className="workspace-table-heading">
|
|
4405
|
-
<strong>
|
|
4406
|
-
<span>{dashboards.length} dashboard{dashboards.length === 1 ? "" : "s"}</span>
|
|
4768
|
+
<strong>Builder</strong>
|
|
4769
|
+
<span>{dashboards.length} dashboard{dashboards.length === 1 ? "" : "s"} · {workflows.length} workflow{workflows.length === 1 ? "" : "s"}</span>
|
|
4770
|
+
</div>
|
|
4771
|
+
<div className="workspace-builder-filterbar">
|
|
4772
|
+
<div className="workspace-builder-filterbar__segments" role="group" aria-label="Builder item type">
|
|
4773
|
+
{[
|
|
4774
|
+
["all", "All"],
|
|
4775
|
+
["dashboard", "Dashboards"],
|
|
4776
|
+
["workflow", "Workflows"]
|
|
4777
|
+
].map(([type, label]) => (
|
|
4778
|
+
<button
|
|
4779
|
+
key={type}
|
|
4780
|
+
type="button"
|
|
4781
|
+
className={builderListFilter.type === type ? "is-active" : ""}
|
|
4782
|
+
onClick={() => setBuilderListFilter((prev) => ({ ...prev, type }))}
|
|
4783
|
+
>
|
|
4784
|
+
{label}
|
|
4785
|
+
</button>
|
|
4786
|
+
))}
|
|
4787
|
+
</div>
|
|
4788
|
+
<label className="workspace-builder-filterbar__search">
|
|
4789
|
+
<Search size={14} aria-hidden="true" />
|
|
4790
|
+
<input
|
|
4791
|
+
value={builderListFilter.query}
|
|
4792
|
+
placeholder="Filter builder items"
|
|
4793
|
+
onChange={(event) => setBuilderListFilter((prev) => ({ ...prev, query: event.target.value }))}
|
|
4794
|
+
/>
|
|
4795
|
+
</label>
|
|
4407
4796
|
</div>
|
|
4408
4797
|
<div className="workspace-table-row workspace-table-head">
|
|
4409
4798
|
<span>Title</span>
|
|
4410
|
-
<span>
|
|
4799
|
+
<span>Type</span>
|
|
4411
4800
|
<span>Last update</span>
|
|
4412
4801
|
<span>Status</span>
|
|
4413
4802
|
<span>Actions</span>
|
|
4414
4803
|
</div>
|
|
4415
|
-
{
|
|
4804
|
+
{builderItems.map((item) => item.type === "dashboard" ? <div className="workspace-table-row" key={item.id}>
|
|
4416
4805
|
<span className="workspace-dashboard-title">
|
|
4417
|
-
{editingDashboardId === dashboard.id ? <span className="workspace-dashboard-title-editor">
|
|
4806
|
+
{editingDashboardId === item.dashboard.id ? <span className="workspace-dashboard-title-editor">
|
|
4418
4807
|
<input
|
|
4419
|
-
aria-label={`Rename ${dashboard.name}`}
|
|
4808
|
+
aria-label={`Rename ${item.dashboard.name}`}
|
|
4420
4809
|
autoFocus
|
|
4421
|
-
data-dashboard-title-input={dashboard.id}
|
|
4422
|
-
onBlur={() => cancelDashboardTitleEdit(dashboard)}
|
|
4810
|
+
data-dashboard-title-input={item.dashboard.id}
|
|
4811
|
+
onBlur={() => cancelDashboardTitleEdit(item.dashboard)}
|
|
4423
4812
|
onChange={(event) => setEditingDashboardDraft(event.target.value)}
|
|
4424
4813
|
onKeyDown={(event) => {
|
|
4425
4814
|
if (event.key === "Enter") {
|
|
4426
4815
|
event.preventDefault();
|
|
4427
|
-
confirmDashboardTitleEdit(dashboard.id);
|
|
4816
|
+
confirmDashboardTitleEdit(item.dashboard.id);
|
|
4428
4817
|
}
|
|
4429
4818
|
if (event.key === "Escape") {
|
|
4430
4819
|
event.preventDefault();
|
|
4431
|
-
cancelDashboardTitleEdit(dashboard);
|
|
4820
|
+
cancelDashboardTitleEdit(item.dashboard);
|
|
4432
4821
|
}
|
|
4433
4822
|
}}
|
|
4434
4823
|
value={editingDashboardDraft}
|
|
4435
4824
|
/>
|
|
4436
4825
|
<button
|
|
4437
|
-
aria-label={`Confirm ${dashboard.name} title`}
|
|
4826
|
+
aria-label={`Confirm ${item.dashboard.name} title`}
|
|
4827
|
+
className="workspace-dashboard-title-confirm"
|
|
4828
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
4829
|
+
onClick={() => confirmDashboardTitleEdit(item.dashboard.id)}
|
|
4830
|
+
type="button"
|
|
4831
|
+
>✓</button>
|
|
4832
|
+
</span> : <button
|
|
4833
|
+
className={item.index === resolvedActiveDashboardIndex ? "active" : ""}
|
|
4834
|
+
onClick={() => enterDashboardTitleEdit(item.dashboard)}
|
|
4835
|
+
type="button"
|
|
4836
|
+
>{item.dashboard.name}</button>}
|
|
4837
|
+
</span>
|
|
4838
|
+
<span>{item.itemKind}</span>
|
|
4839
|
+
<span>{item.updatedAt}</span>
|
|
4840
|
+
<span>
|
|
4841
|
+
<select
|
|
4842
|
+
aria-label={`Status for ${item.dashboard.name}`}
|
|
4843
|
+
onChange={(event) => updateDashboardStatus(item.dashboard.id, event.target.value)}
|
|
4844
|
+
value={item.dashboard.status}
|
|
4845
|
+
>
|
|
4846
|
+
<option value="draft">draft</option>
|
|
4847
|
+
<option value="active">active</option>
|
|
4848
|
+
<option value="archived">archived</option>
|
|
4849
|
+
</select>
|
|
4850
|
+
</span>
|
|
4851
|
+
<span className="workspace-dashboard-actions">
|
|
4852
|
+
<button
|
|
4853
|
+
type="button"
|
|
4854
|
+
className="workspace-row-action-trigger"
|
|
4855
|
+
aria-label={`Actions for ${item.dashboard.name}`}
|
|
4856
|
+
onClick={(event) => openBuilderActionMenu(item, event)}
|
|
4857
|
+
>
|
|
4858
|
+
<MoreVertical size={16} aria-hidden="true" />
|
|
4859
|
+
</button>
|
|
4860
|
+
{builderActionMenuId === item.id && (
|
|
4861
|
+
<span className="workspace-row-action-menu" style={builderActionMenuPlacement || undefined}>
|
|
4862
|
+
<button type="button" onClick={() => { closeBuilderActionMenu(); selectDashboard(item.index); }}>Edit</button>
|
|
4863
|
+
<button type="button" onClick={() => { closeBuilderActionMenu(); enterDashboardTitleEdit(item.dashboard); }}>Rename</button>
|
|
4864
|
+
<button type="button" onClick={() => { closeBuilderActionMenu(); cloneDashboard(item.index); }}>Clone</button>
|
|
4865
|
+
<button type="button" onClick={() => { closeBuilderActionMenu(); deleteDashboard(item.index); }}>Delete</button>
|
|
4866
|
+
</span>
|
|
4867
|
+
)}
|
|
4868
|
+
</span>
|
|
4869
|
+
</div> : <div className="workspace-table-row" key={item.id}>
|
|
4870
|
+
<span className="workspace-dashboard-title">
|
|
4871
|
+
{editingWorkflowId === item.workflow.id ? <span className="workspace-dashboard-title-editor">
|
|
4872
|
+
<input
|
|
4873
|
+
aria-label={`Rename ${item.title}`}
|
|
4874
|
+
autoFocus
|
|
4875
|
+
data-workflow-title-input={item.workflow.id}
|
|
4876
|
+
onBlur={() => cancelWorkflowTitleEdit(item.workflow)}
|
|
4877
|
+
onChange={(event) => setEditingWorkflowDraft(event.target.value)}
|
|
4878
|
+
onKeyDown={(event) => {
|
|
4879
|
+
if (event.key === "Enter") {
|
|
4880
|
+
event.preventDefault();
|
|
4881
|
+
confirmWorkflowTitleEdit(item.workflow);
|
|
4882
|
+
}
|
|
4883
|
+
if (event.key === "Escape") {
|
|
4884
|
+
event.preventDefault();
|
|
4885
|
+
cancelWorkflowTitleEdit(item.workflow);
|
|
4886
|
+
}
|
|
4887
|
+
}}
|
|
4888
|
+
value={editingWorkflowDraft}
|
|
4889
|
+
/>
|
|
4890
|
+
<button
|
|
4891
|
+
aria-label={`Confirm ${item.title} title`}
|
|
4438
4892
|
className="workspace-dashboard-title-confirm"
|
|
4439
4893
|
onMouseDown={(event) => event.preventDefault()}
|
|
4440
|
-
onClick={() =>
|
|
4894
|
+
onClick={() => confirmWorkflowTitleEdit(item.workflow)}
|
|
4441
4895
|
type="button"
|
|
4442
4896
|
>✓</button>
|
|
4443
4897
|
</span> : <button
|
|
4444
|
-
className={index === resolvedActiveDashboardIndex ? "active" : ""}
|
|
4445
|
-
onClick={() => enterDashboardTitleEdit(dashboard)}
|
|
4446
4898
|
type="button"
|
|
4447
|
-
|
|
4899
|
+
onClick={() => enterWorkflowTitleEdit(item.workflow)}
|
|
4900
|
+
>{item.title}</button>}
|
|
4448
4901
|
</span>
|
|
4449
|
-
<span>{
|
|
4450
|
-
<span>{
|
|
4902
|
+
<span>{item.itemKind}</span>
|
|
4903
|
+
<span>{item.updatedAt}</span>
|
|
4451
4904
|
<span>
|
|
4452
4905
|
<select
|
|
4453
|
-
aria-label={`Status for ${
|
|
4454
|
-
|
|
4455
|
-
|
|
4906
|
+
aria-label={`Status for ${item.title}`}
|
|
4907
|
+
value={item.status}
|
|
4908
|
+
disabled
|
|
4456
4909
|
>
|
|
4457
4910
|
<option value="draft">draft</option>
|
|
4458
4911
|
<option value="active">active</option>
|
|
4912
|
+
<option value="live">live</option>
|
|
4459
4913
|
<option value="archived">archived</option>
|
|
4460
4914
|
</select>
|
|
4461
4915
|
</span>
|
|
4462
4916
|
<span className="workspace-dashboard-actions">
|
|
4463
|
-
<button
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4917
|
+
<button
|
|
4918
|
+
type="button"
|
|
4919
|
+
className="workspace-row-action-trigger"
|
|
4920
|
+
aria-label={`Actions for ${item.title}`}
|
|
4921
|
+
onClick={(event) => openBuilderActionMenu(item, event)}
|
|
4922
|
+
>
|
|
4923
|
+
<MoreVertical size={16} aria-hidden="true" />
|
|
4924
|
+
</button>
|
|
4925
|
+
{builderActionMenuId === item.id && (
|
|
4926
|
+
<span className="workspace-row-action-menu" style={builderActionMenuPlacement || undefined}>
|
|
4927
|
+
<button type="button" onClick={() => { closeBuilderActionMenu(); window.open(`/workflows?object=${item.workflow.objectId}&row=${encodeURIComponent(item.workflow.rowId)}&field=${encodeURIComponent(item.workflow.fieldName || "orchestrationConfig")}`, "_self"); }}>Edit</button>
|
|
4928
|
+
<button type="button" onClick={() => { closeBuilderActionMenu(); enterWorkflowTitleEdit(item.workflow); }}>Rename</button>
|
|
4929
|
+
</span>
|
|
4930
|
+
)}
|
|
4467
4931
|
</span>
|
|
4468
4932
|
</div>)}
|
|
4469
4933
|
</section> : null}
|