@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.
Files changed (27) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +50 -25
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +38 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +522 -35
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +242 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +52 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +1203 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +163 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +190 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +64 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +376 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1062 -2
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +492 -28
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +114 -30
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +21 -1
  27. package/package.json +1 -1
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import { Suspense } from "react";
4
+ import WorkflowSurface from "./WorkflowSurface.jsx";
5
+
6
+ export default function WorkflowsPage() {
7
+ return (
8
+ <Suspense fallback={null}>
9
+ <WorkflowSurface />
10
+ </Suspense>
11
+ );
12
+ }
@@ -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.dashboards", group: "Navigation", icon: Home, label: "Go to Dashboards",
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
- Dashboards
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>Dashboards</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="Dashboards">
4766
+ {workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="Builder">
4404
4767
  <div className="workspace-table-heading">
4405
- <strong>Dashboards</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>Created by</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
- {dashboards.map((dashboard, index) => <div className="workspace-table-row" key={dashboard.id}>
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={() => confirmDashboardTitleEdit(dashboard.id)}
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
- >{dashboard.name}</button>}
4899
+ onClick={() => enterWorkflowTitleEdit(item.workflow)}
4900
+ >{item.title}</button>}
4448
4901
  </span>
4449
- <span>{dashboard.createdBy}</span>
4450
- <span>{dashboard.updatedAt}</span>
4902
+ <span>{item.itemKind}</span>
4903
+ <span>{item.updatedAt}</span>
4451
4904
  <span>
4452
4905
  <select
4453
- aria-label={`Status for ${dashboard.name}`}
4454
- onChange={(event) => updateDashboardStatus(dashboard.id, event.target.value)}
4455
- value={dashboard.status}
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 type="button" onClick={() => selectDashboard(index)}>Edit</button>
4464
- <button type="button" onClick={() => enterDashboardTitleEdit(dashboard)}>Rename</button>
4465
- <button type="button" onClick={() => cloneDashboard(index)}>Clone</button>
4466
- <button type="button" onClick={() => deleteDashboard(index)}>Delete</button>
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}