@growthub/cli 0.12.2 → 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 +556 -248
- 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/components/dm-shared.jsx +8 -2
- 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 +2897 -934
- 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/views/[viewId]/page.jsx +206 -0
- 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 +493 -28
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +1363 -8
- 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 +13 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +96 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +122 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import Link from "next/link";
|
|
4
|
+
import { useSearchParams } from "next/navigation";
|
|
4
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
6
|
import {
|
|
6
7
|
Activity,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
Globe,
|
|
26
27
|
GripVertical,
|
|
27
28
|
Grid2X2,
|
|
29
|
+
GitBranch,
|
|
28
30
|
Hash,
|
|
29
31
|
Home,
|
|
30
32
|
Import,
|
|
@@ -36,6 +38,7 @@ import {
|
|
|
36
38
|
List,
|
|
37
39
|
Mail,
|
|
38
40
|
Maximize2,
|
|
41
|
+
MoreVertical,
|
|
39
42
|
Pencil,
|
|
40
43
|
PieChart,
|
|
41
44
|
Plus,
|
|
@@ -346,6 +349,179 @@ function createDashboardRecord(name = "Untitled") {
|
|
|
346
349
|
};
|
|
347
350
|
}
|
|
348
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
|
+
|
|
349
525
|
function createEmptyTab(name = "Untitled") {
|
|
350
526
|
return {
|
|
351
527
|
id: generateId("tab"),
|
|
@@ -3307,6 +3483,7 @@ function WorkspaceManagementPanel({ config, persistence, adapterConfig, onClose
|
|
|
3307
3483
|
}
|
|
3308
3484
|
|
|
3309
3485
|
function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, integrationSettings, persistence }) {
|
|
3486
|
+
const searchParams = useSearchParams();
|
|
3310
3487
|
const [config, setConfig] = useState(() => {
|
|
3311
3488
|
const dashboards = Array.isArray(initialConfig.dashboards) && initialConfig.dashboards.length
|
|
3312
3489
|
? initialConfig.dashboards.map((dashboard, index) =>
|
|
@@ -3327,7 +3504,12 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3327
3504
|
const [previewTemplateId, setPreviewTemplateId] = useState(null);
|
|
3328
3505
|
const [editingDashboardId, setEditingDashboardId] = useState(null);
|
|
3329
3506
|
const [editingDashboardDraft, setEditingDashboardDraft] = useState("");
|
|
3507
|
+
const [editingWorkflowId, setEditingWorkflowId] = useState(null);
|
|
3508
|
+
const [editingWorkflowDraft, setEditingWorkflowDraft] = useState("");
|
|
3330
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);
|
|
3331
3513
|
const [activeDashboardId, setActiveDashboardId] = useState(() =>
|
|
3332
3514
|
getActiveDashboardId(
|
|
3333
3515
|
Array.isArray(initialConfig.dashboards) && initialConfig.dashboards.length ? initialConfig.dashboards : [],
|
|
@@ -3337,6 +3519,34 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3337
3519
|
const gridRef = useRef(null);
|
|
3338
3520
|
const canvas = config.canvas;
|
|
3339
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]);
|
|
3340
3550
|
const resolvedActiveDashboardId = getActiveDashboardId(dashboards, activeDashboardId);
|
|
3341
3551
|
const resolvedActiveDashboardIndex = activeDashboardIndex(dashboards, resolvedActiveDashboardId);
|
|
3342
3552
|
const widgetTypes = config.widgetTypes;
|
|
@@ -3504,6 +3714,59 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3504
3714
|
});
|
|
3505
3715
|
}, [activeDashboardId]);
|
|
3506
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
|
+
|
|
3507
3770
|
const selectDashboard = useCallback((index) => {
|
|
3508
3771
|
setConfig((prev) => {
|
|
3509
3772
|
const synced = syncActiveDashboard(prev, activeDashboardId);
|
|
@@ -3527,6 +3790,15 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3527
3790
|
});
|
|
3528
3791
|
}, [activeDashboardId]);
|
|
3529
3792
|
|
|
3793
|
+
useEffect(() => {
|
|
3794
|
+
const dashboardParam = searchParams?.get("dashboard");
|
|
3795
|
+
if (!dashboardParam || dashboards.length === 0) return;
|
|
3796
|
+
const targetIndex = dashboards.findIndex((dashboard) => dashboard.id === dashboardParam);
|
|
3797
|
+
if (targetIndex < 0) return;
|
|
3798
|
+
if (dashboards[targetIndex]?.id === resolvedActiveDashboardId && workspaceView === "builder") return;
|
|
3799
|
+
selectDashboard(targetIndex);
|
|
3800
|
+
}, [dashboards, resolvedActiveDashboardId, searchParams, selectDashboard, workspaceView]);
|
|
3801
|
+
|
|
3530
3802
|
const enterDashboardTitleEdit = useCallback((dashboard) => {
|
|
3531
3803
|
if (!dashboard) return;
|
|
3532
3804
|
setEditingDashboardId(dashboard.id);
|
|
@@ -3832,6 +4104,96 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3832
4104
|
await persistWorkspaceConfig(nextConfig, activeDashboardId);
|
|
3833
4105
|
}, [activeDashboardId, config, editingDashboardDraft, persistWorkspaceConfig]);
|
|
3834
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
|
+
|
|
3835
4197
|
const cancelDashboardTitleEdit = useCallback((dashboard) => {
|
|
3836
4198
|
if (!dashboard) return;
|
|
3837
4199
|
if (editingDashboardDraft.trim() !== dashboard.name) {
|
|
@@ -4085,6 +4447,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4085
4447
|
const showDashboardHome = useCallback(() => {
|
|
4086
4448
|
setEditingDashboardId(null);
|
|
4087
4449
|
setEditingDashboardDraft("");
|
|
4450
|
+
setBuilderActionMenuId(null);
|
|
4088
4451
|
setWorkspaceView("dashboards");
|
|
4089
4452
|
}, []);
|
|
4090
4453
|
const resetWidgetSelectionOnOutsidePointer = useCallback((event) => {
|
|
@@ -4296,7 +4659,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4296
4659
|
run: () => setManagementOpen(true)
|
|
4297
4660
|
});
|
|
4298
4661
|
list.push({
|
|
4299
|
-
id: "workspace.
|
|
4662
|
+
id: "workspace.builder", group: "Navigation", icon: Home, label: "Go to Builder",
|
|
4300
4663
|
run: () => showDashboardHome()
|
|
4301
4664
|
});
|
|
4302
4665
|
|
|
@@ -4347,7 +4710,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4347
4710
|
className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"}
|
|
4348
4711
|
onClick={showDashboardHome}
|
|
4349
4712
|
>
|
|
4350
|
-
|
|
4713
|
+
Builder
|
|
4351
4714
|
</button>
|
|
4352
4715
|
)}
|
|
4353
4716
|
managementSlot={(
|
|
@@ -4369,12 +4732,13 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4369
4732
|
<h1>{activeDashboard?.name || "Untitled"}</h1>
|
|
4370
4733
|
</> : <>
|
|
4371
4734
|
<p>Workspace home</p>
|
|
4372
|
-
<h1>
|
|
4735
|
+
<h1>Builder</h1>
|
|
4373
4736
|
</>}
|
|
4374
4737
|
</div>
|
|
4375
4738
|
<div className="workspace-toolbar-actions">
|
|
4376
4739
|
<button type="button" onClick={() => setTemplateGalleryOpen(true)}><Grid2X2 size={15} />Templates</button>
|
|
4377
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>
|
|
4378
4742
|
<button type="button" onClick={duplicateDashboard}><Copy size={15} />Duplicate Dashboard</button>
|
|
4379
4743
|
<button type="button" onClick={() => importInputRef.current?.click()}><Import size={15} />Import</button>
|
|
4380
4744
|
<button type="button" onClick={exportConfig}><Download size={15} />Export</button>
|
|
@@ -4389,70 +4753,171 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
4389
4753
|
/>
|
|
4390
4754
|
</header>
|
|
4391
4755
|
|
|
4392
|
-
{workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="
|
|
4756
|
+
{workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="Builder">
|
|
4393
4757
|
<div className="workspace-table-heading">
|
|
4394
|
-
<strong>
|
|
4395
|
-
<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>
|
|
4396
4786
|
</div>
|
|
4397
4787
|
<div className="workspace-table-row workspace-table-head">
|
|
4398
4788
|
<span>Title</span>
|
|
4399
|
-
<span>
|
|
4789
|
+
<span>Type</span>
|
|
4400
4790
|
<span>Last update</span>
|
|
4401
4791
|
<span>Status</span>
|
|
4402
4792
|
<span>Actions</span>
|
|
4403
4793
|
</div>
|
|
4404
|
-
{
|
|
4794
|
+
{builderItems.map((item) => item.type === "dashboard" ? <div className="workspace-table-row" key={item.id}>
|
|
4405
4795
|
<span className="workspace-dashboard-title">
|
|
4406
|
-
{editingDashboardId === dashboard.id ? <span className="workspace-dashboard-title-editor">
|
|
4796
|
+
{editingDashboardId === item.dashboard.id ? <span className="workspace-dashboard-title-editor">
|
|
4407
4797
|
<input
|
|
4408
|
-
aria-label={`Rename ${dashboard.name}`}
|
|
4798
|
+
aria-label={`Rename ${item.dashboard.name}`}
|
|
4409
4799
|
autoFocus
|
|
4410
|
-
data-dashboard-title-input={dashboard.id}
|
|
4411
|
-
onBlur={() => cancelDashboardTitleEdit(dashboard)}
|
|
4800
|
+
data-dashboard-title-input={item.dashboard.id}
|
|
4801
|
+
onBlur={() => cancelDashboardTitleEdit(item.dashboard)}
|
|
4412
4802
|
onChange={(event) => setEditingDashboardDraft(event.target.value)}
|
|
4413
4803
|
onKeyDown={(event) => {
|
|
4414
4804
|
if (event.key === "Enter") {
|
|
4415
4805
|
event.preventDefault();
|
|
4416
|
-
confirmDashboardTitleEdit(dashboard.id);
|
|
4806
|
+
confirmDashboardTitleEdit(item.dashboard.id);
|
|
4417
4807
|
}
|
|
4418
4808
|
if (event.key === "Escape") {
|
|
4419
4809
|
event.preventDefault();
|
|
4420
|
-
cancelDashboardTitleEdit(dashboard);
|
|
4810
|
+
cancelDashboardTitleEdit(item.dashboard);
|
|
4421
4811
|
}
|
|
4422
4812
|
}}
|
|
4423
4813
|
value={editingDashboardDraft}
|
|
4424
4814
|
/>
|
|
4425
4815
|
<button
|
|
4426
|
-
aria-label={`Confirm ${dashboard.name} title`}
|
|
4816
|
+
aria-label={`Confirm ${item.dashboard.name} title`}
|
|
4817
|
+
className="workspace-dashboard-title-confirm"
|
|
4818
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
4819
|
+
onClick={() => confirmDashboardTitleEdit(item.dashboard.id)}
|
|
4820
|
+
type="button"
|
|
4821
|
+
>✓</button>
|
|
4822
|
+
</span> : <button
|
|
4823
|
+
className={item.index === resolvedActiveDashboardIndex ? "active" : ""}
|
|
4824
|
+
onClick={() => enterDashboardTitleEdit(item.dashboard)}
|
|
4825
|
+
type="button"
|
|
4826
|
+
>{item.dashboard.name}</button>}
|
|
4827
|
+
</span>
|
|
4828
|
+
<span>{item.itemKind}</span>
|
|
4829
|
+
<span>{item.updatedAt}</span>
|
|
4830
|
+
<span>
|
|
4831
|
+
<select
|
|
4832
|
+
aria-label={`Status for ${item.dashboard.name}`}
|
|
4833
|
+
onChange={(event) => updateDashboardStatus(item.dashboard.id, event.target.value)}
|
|
4834
|
+
value={item.dashboard.status}
|
|
4835
|
+
>
|
|
4836
|
+
<option value="draft">draft</option>
|
|
4837
|
+
<option value="active">active</option>
|
|
4838
|
+
<option value="archived">archived</option>
|
|
4839
|
+
</select>
|
|
4840
|
+
</span>
|
|
4841
|
+
<span className="workspace-dashboard-actions">
|
|
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`}
|
|
4427
4882
|
className="workspace-dashboard-title-confirm"
|
|
4428
4883
|
onMouseDown={(event) => event.preventDefault()}
|
|
4429
|
-
onClick={() =>
|
|
4884
|
+
onClick={() => confirmWorkflowTitleEdit(item.workflow)}
|
|
4430
4885
|
type="button"
|
|
4431
4886
|
>✓</button>
|
|
4432
4887
|
</span> : <button
|
|
4433
|
-
className={index === resolvedActiveDashboardIndex ? "active" : ""}
|
|
4434
|
-
onClick={() => enterDashboardTitleEdit(dashboard)}
|
|
4435
4888
|
type="button"
|
|
4436
|
-
|
|
4889
|
+
onClick={() => enterWorkflowTitleEdit(item.workflow)}
|
|
4890
|
+
>{item.title}</button>}
|
|
4437
4891
|
</span>
|
|
4438
|
-
<span>{
|
|
4439
|
-
<span>{
|
|
4892
|
+
<span>{item.itemKind}</span>
|
|
4893
|
+
<span>{item.updatedAt}</span>
|
|
4440
4894
|
<span>
|
|
4441
4895
|
<select
|
|
4442
|
-
aria-label={`Status for ${
|
|
4443
|
-
|
|
4444
|
-
|
|
4896
|
+
aria-label={`Status for ${item.title}`}
|
|
4897
|
+
value={item.status}
|
|
4898
|
+
disabled
|
|
4445
4899
|
>
|
|
4446
4900
|
<option value="draft">draft</option>
|
|
4447
4901
|
<option value="active">active</option>
|
|
4902
|
+
<option value="live">live</option>
|
|
4448
4903
|
<option value="archived">archived</option>
|
|
4449
4904
|
</select>
|
|
4450
4905
|
</span>
|
|
4451
4906
|
<span className="workspace-dashboard-actions">
|
|
4452
|
-
<button
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
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
|
+
)}
|
|
4456
4921
|
</span>
|
|
4457
4922
|
</div>)}
|
|
4458
4923
|
</section> : null}
|