@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.
Files changed (31) 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 +556 -248
  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/components/dm-shared.jsx +8 -2
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +2897 -934
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/views/[viewId]/page.jsx +206 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +493 -28
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +1363 -8
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +13 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +96 -0
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +122 -0
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +1 -0
  31. 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
+ }
@@ -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.dashboards", group: "Navigation", icon: Home, label: "Go to Dashboards",
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
- Dashboards
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>Dashboards</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="Dashboards">
4756
+ {workspaceView === "dashboards" ? <section className="workspace-table" id="dashboards" aria-label="Builder">
4393
4757
  <div className="workspace-table-heading">
4394
- <strong>Dashboards</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>Created by</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
- {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}>
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={() => confirmDashboardTitleEdit(dashboard.id)}
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
- >{dashboard.name}</button>}
4889
+ onClick={() => enterWorkflowTitleEdit(item.workflow)}
4890
+ >{item.title}</button>}
4437
4891
  </span>
4438
- <span>{dashboard.createdBy}</span>
4439
- <span>{dashboard.updatedAt}</span>
4892
+ <span>{item.itemKind}</span>
4893
+ <span>{item.updatedAt}</span>
4440
4894
  <span>
4441
4895
  <select
4442
- aria-label={`Status for ${dashboard.name}`}
4443
- onChange={(event) => updateDashboardStatus(dashboard.id, event.target.value)}
4444
- value={dashboard.status}
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 type="button" onClick={() => selectDashboard(index)}>Edit</button>
4453
- <button type="button" onClick={() => enterDashboardTitleEdit(dashboard)}>Rename</button>
4454
- <button type="button" onClick={() => cloneDashboard(index)}>Clone</button>
4455
- <button type="button" onClick={() => deleteDashboard(index)}>Delete</button>
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}