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