@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.
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 +482 -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,
@@ -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.dashboards", group: "Navigation", icon: Home, label: "Go to Dashboards",
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
- Dashboards
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>Dashboards</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="Dashboards">
4756
+ {workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="Builder">
4404
4757
  <div className="workspace-table-heading">
4405
- <strong>Dashboards</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>Created by</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
- {dashboards.map((dashboard, index) => <div className="workspace-table-row" key={dashboard.id}>
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>{dashboard.createdBy}</span>
4450
- <span>{dashboard.updatedAt}</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 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>
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}