@open-mercato/core 0.7.1-develop.7193.1.910a5b0a1e → 0.7.1-develop.7194.1.ab4fc81f82
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -2
- package/AGENTS.md +4 -3
- package/agentic/standalone-guide.md +1 -1
- package/build.mjs +16 -0
- package/dist/generated/entities/action_log/index.js +2 -0
- package/dist/generated/entities/action_log/index.js.map +2 -2
- package/dist/generated/entities/user/index.js +2 -0
- package/dist/generated/entities/user/index.js.map +2 -2
- package/dist/generated/entities/user_task/index.js +14 -0
- package/dist/generated/entities/user_task/index.js.map +2 -2
- package/dist/generated/entities/workflow_definition/index.js +6 -0
- package/dist/generated/entities/workflow_definition/index.js.map +2 -2
- package/dist/generated/entities/workflow_definition_draft/index.js +25 -0
- package/dist/generated/entities/workflow_definition_draft/index.js.map +7 -0
- package/dist/generated/entities/workflow_definition_metric_rollup/index.js +23 -0
- package/dist/generated/entities/workflow_definition_metric_rollup/index.js.map +7 -0
- package/dist/generated/entities/workflow_instance/index.js +4 -0
- package/dist/generated/entities/workflow_instance/index.js.map +2 -2
- package/dist/generated/entities.ids.generated.js +4 -1
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +39 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/workflowsFixtures.js +477 -2
- package/dist/helpers/integration/workflowsFixtures.js.map +2 -2
- package/dist/helpers/integration/workflowsUi.js +148 -0
- package/dist/helpers/integration/workflowsUi.js.map +7 -0
- package/dist/modules/attachments/lib/createFromBuffer.js +79 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +7 -0
- package/dist/modules/audit_logs/data/entities.js +5 -0
- package/dist/modules/audit_logs/data/entities.js.map +2 -2
- package/dist/modules/audit_logs/data/validators.js +1 -0
- package/dist/modules/audit_logs/data/validators.js.map +2 -2
- package/dist/modules/audit_logs/lib/projections.js +2 -1
- package/dist/modules/audit_logs/lib/projections.js.map +2 -2
- package/dist/modules/audit_logs/migrations/Migration20260625120000.js +15 -0
- package/dist/modules/audit_logs/migrations/Migration20260625120000.js.map +7 -0
- package/dist/modules/audit_logs/services/actionLogService.js +3 -0
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/auth/data/entities.js +6 -0
- package/dist/modules/auth/data/entities.js.map +2 -2
- package/dist/modules/auth/lib/executionPrincipal.js +140 -0
- package/dist/modules/auth/lib/executionPrincipal.js.map +7 -0
- package/dist/modules/auth/migrations/Migration20260625050000.js +13 -0
- package/dist/modules/auth/migrations/Migration20260625050000.js.map +7 -0
- package/dist/modules/business_rules/components/InlineRuleEditor.js +154 -0
- package/dist/modules/business_rules/components/InlineRuleEditor.js.map +7 -0
- package/dist/modules/business_rules/data/validators.js +6 -0
- package/dist/modules/business_rules/data/validators.js.map +2 -2
- package/dist/modules/business_rules/lib/rule-engine.js +5 -3
- package/dist/modules/business_rules/lib/rule-engine.js.map +2 -2
- package/dist/modules/catalog/commands/products.js +5 -0
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/catalog/workflows.js +19 -0
- package/dist/modules/catalog/workflows.js.map +7 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js +3 -1
- package/dist/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/cli.js +80 -0
- package/dist/modules/customer_accounts/cli.js.map +7 -0
- package/dist/modules/customer_accounts/lib/customerRoleAclSync.js +59 -0
- package/dist/modules/customer_accounts/lib/customerRoleAclSync.js.map +7 -0
- package/dist/modules/customer_accounts/setup.js +4 -2
- package/dist/modules/customer_accounts/setup.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +4 -0
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/components/detail/ChangelogEntryRow.js +1 -1
- package/dist/modules/customers/components/detail/ChangelogEntryRow.js.map +2 -2
- package/dist/modules/customers/components/detail/utils.js +2 -0
- package/dist/modules/customers/components/detail/utils.js.map +2 -2
- package/dist/modules/customers/events.js +13 -2
- package/dist/modules/customers/events.js.map +2 -2
- package/dist/modules/customers/lib/link-workflow-task-handler.js +64 -0
- package/dist/modules/customers/lib/link-workflow-task-handler.js.map +7 -0
- package/dist/modules/customers/lib/todoCompatibility.js +1 -1
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/lib/workflowTaskLink.js +9 -0
- package/dist/modules/customers/lib/workflowTaskLink.js.map +7 -0
- package/dist/modules/customers/subscribers/link-workflow-task.js +12 -0
- package/dist/modules/customers/subscribers/link-workflow-task.js.map +7 -0
- package/dist/modules/customers/workflows.js +29 -0
- package/dist/modules/customers/workflows.js.map +7 -0
- package/dist/modules/entities/api/records.js +13 -33
- package/dist/modules/entities/api/records.js.map +3 -3
- package/dist/modules/entities/lib/entityClassification.js +60 -0
- package/dist/modules/entities/lib/entityClassification.js.map +7 -0
- package/dist/modules/sales/commands/documents.js +31 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/events.js +12 -1
- package/dist/modules/sales/events.js.map +2 -2
- package/dist/modules/sales/workflows.js +12 -1
- package/dist/modules/sales/workflows.js.map +2 -2
- package/dist/modules/seeds/cli.js +199 -0
- package/dist/modules/seeds/cli.js.map +7 -0
- package/dist/modules/seeds/examples/sample-seed.json +26 -0
- package/dist/modules/seeds/index.js +12 -0
- package/dist/modules/seeds/index.js.map +7 -0
- package/dist/modules/workflows/acl.js +96 -0
- package/dist/modules/workflows/acl.js.map +2 -2
- package/dist/modules/workflows/ai-agents.js +50 -0
- package/dist/modules/workflows/ai-agents.js.map +7 -0
- package/dist/modules/workflows/ai-tools/authoring-pack.js +582 -0
- package/dist/modules/workflows/ai-tools/authoring-pack.js.map +7 -0
- package/dist/modules/workflows/ai-tools/types.js +43 -0
- package/dist/modules/workflows/ai-tools/types.js.map +7 -0
- package/dist/modules/workflows/ai-tools.js +10 -0
- package/dist/modules/workflows/ai-tools.js.map +7 -0
- package/dist/modules/workflows/api/command-settings/route.js +137 -0
- package/dist/modules/workflows/api/command-settings/route.js.map +7 -0
- package/dist/modules/workflows/api/commands/route.js +72 -0
- package/dist/modules/workflows/api/commands/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/callers/route.js +76 -0
- package/dist/modules/workflows/api/definitions/[id]/callers/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/context-schema/route.js +134 -0
- package/dist/modules/workflows/api/definitions/[id]/context-schema/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js +5 -1
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/draft/route.js +286 -0
- package/dist/modules/workflows/api/definitions/[id]/draft/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/publish/route.js +207 -0
- package/dist/modules/workflows/api/definitions/[id]/publish/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +5 -0
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js +93 -9
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +3 -3
- package/dist/modules/workflows/api/definitions/[id]/test-step/route.js +210 -0
- package/dist/modules/workflows/api/definitions/[id]/test-step/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/generate/route.js +233 -0
- package/dist/modules/workflows/api/definitions/generate/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/route.js +61 -113
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/serialize.js +9 -0
- package/dist/modules/workflows/api/definitions/serialize.js.map +2 -2
- package/dist/modules/workflows/api/endpoints/route.js +69 -0
- package/dist/modules/workflows/api/endpoints/route.js.map +7 -0
- package/dist/modules/workflows/api/functions/route.js +73 -0
- package/dist/modules/workflows/api/functions/route.js.map +7 -0
- package/dist/modules/workflows/api/grantable-features/route.js +75 -0
- package/dist/modules/workflows/api/grantable-features/route.js.map +7 -0
- package/dist/modules/workflows/api/instances/[id]/context/route.js +180 -0
- package/dist/modules/workflows/api/instances/[id]/context/route.js.map +7 -0
- package/dist/modules/workflows/api/instances/[id]/rerun-step/route.js +184 -0
- package/dist/modules/workflows/api/instances/[id]/rerun-step/route.js.map +7 -0
- package/dist/modules/workflows/api/instances/[id]/retry/route.js +12 -2
- package/dist/modules/workflows/api/instances/[id]/retry/route.js.map +2 -2
- package/dist/modules/workflows/api/instances/[id]/step-through/route.js +148 -0
- package/dist/modules/workflows/api/instances/[id]/step-through/route.js.map +7 -0
- package/dist/modules/workflows/api/instances/[id]/steps/route.js +123 -0
- package/dist/modules/workflows/api/instances/[id]/steps/route.js.map +7 -0
- package/dist/modules/workflows/api/instances/[id]/would-do/route.js +172 -0
- package/dist/modules/workflows/api/instances/[id]/would-do/route.js.map +7 -0
- package/dist/modules/workflows/api/instances/bulk-replay/route.js +117 -0
- package/dist/modules/workflows/api/instances/bulk-replay/route.js.map +7 -0
- package/dist/modules/workflows/api/instances/failure-queue/route.js +165 -0
- package/dist/modules/workflows/api/instances/failure-queue/route.js.map +7 -0
- package/dist/modules/workflows/api/instances/route.js +94 -3
- package/dist/modules/workflows/api/instances/route.js.map +2 -2
- package/dist/modules/workflows/api/metrics/definitions/route.js +188 -0
- package/dist/modules/workflows/api/metrics/definitions/route.js.map +7 -0
- package/dist/modules/workflows/api/openapi.js +415 -4
- package/dist/modules/workflows/api/openapi.js.map +2 -2
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +132 -0
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +7 -0
- package/dist/modules/workflows/api/portal/tasks/[id]/route.js +97 -0
- package/dist/modules/workflows/api/portal/tasks/[id]/route.js.map +7 -0
- package/dist/modules/workflows/api/portal/tasks/route.js +126 -0
- package/dist/modules/workflows/api/portal/tasks/route.js.map +7 -0
- package/dist/modules/workflows/api/rule-usage/route.js +80 -0
- package/dist/modules/workflows/api/rule-usage/route.js.map +7 -0
- package/dist/modules/workflows/api/task-settings/route.js +107 -0
- package/dist/modules/workflows/api/task-settings/route.js.map +7 -0
- package/dist/modules/workflows/api/tasks/[id]/claim/route.js +27 -16
- package/dist/modules/workflows/api/tasks/[id]/claim/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +40 -16
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/reassign/route.js +150 -0
- package/dist/modules/workflows/api/tasks/[id]/reassign/route.js.map +7 -0
- package/dist/modules/workflows/api/tasks/[id]/route.js +39 -6
- package/dist/modules/workflows/api/tasks/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/unclaim/route.js +100 -0
- package/dist/modules/workflows/api/tasks/[id]/unclaim/route.js.map +7 -0
- package/dist/modules/workflows/api/tasks/route.js +37 -6
- package/dist/modules/workflows/api/tasks/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/serialize.js +83 -0
- package/dist/modules/workflows/api/tasks/serialize.js.map +7 -0
- package/dist/modules/workflows/api/templates/route.js +69 -0
- package/dist/modules/workflows/api/templates/route.js.map +7 -0
- package/dist/modules/workflows/api/work-inbox/next/route.js +129 -0
- package/dist/modules/workflows/api/work-inbox/next/route.js.map +7 -0
- package/dist/modules/workflows/api/work-inbox/route.js +119 -0
- package/dist/modules/workflows/api/work-inbox/route.js.map +7 -0
- package/dist/modules/workflows/backend/config/workflows/page.js +10 -0
- package/dist/modules/workflows/backend/config/workflows/page.js.map +7 -0
- package/dist/modules/workflows/backend/config/workflows/page.meta.js +35 -0
- package/dist/modules/workflows/backend/config/workflows/page.meta.js.map +7 -0
- package/dist/modules/workflows/backend/definitions/[id]/page.js +6 -271
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +3 -3
- package/dist/modules/workflows/backend/definitions/create/page.js +7 -56
- package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/create/page.meta.js +1 -1
- package/dist/modules/workflows/backend/definitions/create/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +131 -51
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +2606 -677
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +3 -3
- package/dist/modules/workflows/backend/instances/[id]/page.js +532 -421
- package/dist/modules/workflows/backend/instances/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/[id]/page.meta.js +1 -1
- package/dist/modules/workflows/backend/instances/[id]/page.meta.js.map +1 -1
- package/dist/modules/workflows/backend/instances/failure-queue/page.js +211 -0
- package/dist/modules/workflows/backend/instances/failure-queue/page.js.map +7 -0
- package/dist/modules/workflows/backend/instances/failure-queue/page.meta.js +20 -0
- package/dist/modules/workflows/backend/instances/failure-queue/page.meta.js.map +7 -0
- package/dist/modules/workflows/backend/instances/page.js +159 -22
- package/dist/modules/workflows/backend/instances/page.js.map +2 -2
- package/dist/modules/workflows/backend/instances/page.meta.js +1 -1
- package/dist/modules/workflows/backend/instances/page.meta.js.map +1 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js +553 -392
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +3 -3
- package/dist/modules/workflows/backend/tasks/page.js +8 -250
- package/dist/modules/workflows/backend/tasks/page.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/page.meta.js +1 -0
- package/dist/modules/workflows/backend/tasks/page.meta.js.map +2 -2
- package/dist/modules/workflows/backend/work-inbox/page.js +399 -0
- package/dist/modules/workflows/backend/work-inbox/page.js.map +7 -0
- package/dist/modules/workflows/backend/work-inbox/page.meta.js +19 -0
- package/dist/modules/workflows/backend/work-inbox/page.meta.js.map +7 -0
- package/dist/modules/workflows/cli.js +1 -1
- package/dist/modules/workflows/cli.js.map +2 -2
- package/dist/modules/workflows/commands/tasks.js +81 -0
- package/dist/modules/workflows/commands/tasks.js.map +7 -0
- package/dist/modules/workflows/components/ActivitiesEditor.js +281 -287
- package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
- package/dist/modules/workflows/components/ActivityEditDialog.js +220 -0
- package/dist/modules/workflows/components/ActivityEditDialog.js.map +7 -0
- package/dist/modules/workflows/components/AgentSelector.js +115 -0
- package/dist/modules/workflows/components/AgentSelector.js.map +7 -0
- package/dist/modules/workflows/components/AnnotationEditDialog.js +110 -0
- package/dist/modules/workflows/components/AnnotationEditDialog.js.map +7 -0
- package/dist/modules/workflows/components/BusinessRuleUsagePanel.js +71 -0
- package/dist/modules/workflows/components/BusinessRuleUsagePanel.js.map +7 -0
- package/dist/modules/workflows/components/BusinessRulesSelector.js +151 -93
- package/dist/modules/workflows/components/BusinessRulesSelector.js.map +2 -2
- package/dist/modules/workflows/components/ConfigJsonTextarea.js +85 -0
- package/dist/modules/workflows/components/ConfigJsonTextarea.js.map +7 -0
- package/dist/modules/workflows/components/ContextSchemaEditor.js +150 -0
- package/dist/modules/workflows/components/ContextSchemaEditor.js.map +7 -0
- package/dist/modules/workflows/components/DefinitionErrorHandlerField.js +102 -0
- package/dist/modules/workflows/components/DefinitionErrorHandlerField.js.map +7 -0
- package/dist/modules/workflows/components/DefinitionMetadataDrawer.js +377 -0
- package/dist/modules/workflows/components/DefinitionMetadataDrawer.js.map +7 -0
- package/dist/modules/workflows/components/DefinitionMetricsCells.js +132 -0
- package/dist/modules/workflows/components/DefinitionMetricsCells.js.map +7 -0
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js +307 -216
- package/dist/modules/workflows/components/DefinitionTriggersEditor.js.map +2 -2
- package/dist/modules/workflows/components/EdgeEditDialog.js +11 -18
- package/dist/modules/workflows/components/EdgeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/EdgeEditDialogCrudForm.js +112 -72
- package/dist/modules/workflows/components/EdgeEditDialogCrudForm.js.map +2 -2
- package/dist/modules/workflows/components/InputDataPanel.js +128 -0
- package/dist/modules/workflows/components/InputDataPanel.js.map +7 -0
- package/dist/modules/workflows/components/InspectorPanel.js +161 -0
- package/dist/modules/workflows/components/InspectorPanel.js.map +7 -0
- package/dist/modules/workflows/components/NodeEditDialog.js +363 -41
- package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js +714 -159
- package/dist/modules/workflows/components/NodeEditDialogCrudForm.js.map +2 -2
- package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
- package/dist/modules/workflows/components/TemplateGalleryDialog.js +148 -0
- package/dist/modules/workflows/components/TemplateGalleryDialog.js.map +7 -0
- package/dist/modules/workflows/components/TransitionsEditor.js +80 -74
- package/dist/modules/workflows/components/TransitionsEditor.js.map +2 -2
- package/dist/modules/workflows/components/TriggersDialog.js +53 -0
- package/dist/modules/workflows/components/TriggersDialog.js.map +7 -0
- package/dist/modules/workflows/components/TriggersEditor.js +331 -0
- package/dist/modules/workflows/components/TriggersEditor.js.map +7 -0
- package/dist/modules/workflows/components/WorkflowAiDraftDialog.js +278 -0
- package/dist/modules/workflows/components/WorkflowAiDraftDialog.js.map +7 -0
- package/dist/modules/workflows/components/WorkflowCodeView.js +173 -0
- package/dist/modules/workflows/components/WorkflowCodeView.js.map +7 -0
- package/dist/modules/workflows/components/WorkflowCommandPalette.js +62 -0
- package/dist/modules/workflows/components/WorkflowCommandPalette.js.map +7 -0
- package/dist/modules/workflows/components/WorkflowCompensationGhostEdge.js +70 -0
- package/dist/modules/workflows/components/WorkflowCompensationGhostEdge.js.map +7 -0
- package/dist/modules/workflows/components/WorkflowDataMappingEdge.js +36 -0
- package/dist/modules/workflows/components/WorkflowDataMappingEdge.js.map +7 -0
- package/dist/modules/workflows/components/WorkflowGraph.js +4 -2
- package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowGraphImpl.js +324 -86
- package/dist/modules/workflows/components/WorkflowGraphImpl.js.map +3 -3
- package/dist/modules/workflows/components/WorkflowIconPicker.js +109 -0
- package/dist/modules/workflows/components/WorkflowIconPicker.js.map +7 -0
- package/dist/modules/workflows/components/WorkflowLegend.js +45 -29
- package/dist/modules/workflows/components/WorkflowLegend.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowNodeCard.js +221 -22
- package/dist/modules/workflows/components/WorkflowNodeCard.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowRouteChips.js +111 -0
- package/dist/modules/workflows/components/WorkflowRouteChips.js.map +7 -0
- package/dist/modules/workflows/components/WorkflowSelector.js +108 -88
- package/dist/modules/workflows/components/WorkflowSelector.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowTransitionEdge.js +61 -11
- package/dist/modules/workflows/components/WorkflowTransitionEdge.js.map +2 -2
- package/dist/modules/workflows/components/WorkflowTransitionLabel.js +15 -5
- package/dist/modules/workflows/components/WorkflowTransitionLabel.js.map +2 -2
- package/dist/modules/workflows/components/fields/ActivityArrayEditor.js +88 -21
- package/dist/modules/workflows/components/fields/ActivityArrayEditor.js.map +2 -2
- package/dist/modules/workflows/components/fields/ActivityConfigFields.js +615 -0
- package/dist/modules/workflows/components/fields/ActivityConfigFields.js.map +7 -0
- package/dist/modules/workflows/components/fields/ActivityTestPanel.js +191 -0
- package/dist/modules/workflows/components/fields/ActivityTestPanel.js.map +7 -0
- package/dist/modules/workflows/components/fields/AgentInvokeConfigField.js +488 -0
- package/dist/modules/workflows/components/fields/AgentInvokeConfigField.js.map +7 -0
- package/dist/modules/workflows/components/fields/AgentReviewOnBreachField.js +61 -0
- package/dist/modules/workflows/components/fields/AgentReviewOnBreachField.js.map +7 -0
- package/dist/modules/workflows/components/fields/BranchingRoutesEditor.js +227 -0
- package/dist/modules/workflows/components/fields/BranchingRoutesEditor.js.map +7 -0
- package/dist/modules/workflows/components/fields/BusinessRuleConditionsEditor.js +37 -4
- package/dist/modules/workflows/components/fields/BusinessRuleConditionsEditor.js.map +2 -2
- package/dist/modules/workflows/components/fields/CommandPicker.js +111 -0
- package/dist/modules/workflows/components/fields/CommandPicker.js.map +7 -0
- package/dist/modules/workflows/components/fields/EndpointPicker.js +235 -0
- package/dist/modules/workflows/components/fields/EndpointPicker.js.map +7 -0
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js +2 -2
- package/dist/modules/workflows/components/fields/FormFieldArrayEditor.js.map +2 -2
- package/dist/modules/workflows/components/fields/FunctionPicker.js +55 -0
- package/dist/modules/workflows/components/fields/FunctionPicker.js.map +7 -0
- package/dist/modules/workflows/components/fields/MappingArrayEditor.js +38 -15
- package/dist/modules/workflows/components/fields/MappingArrayEditor.js.map +2 -2
- package/dist/modules/workflows/components/fields/PortFieldArrayEditor.js +206 -0
- package/dist/modules/workflows/components/fields/PortFieldArrayEditor.js.map +7 -0
- package/dist/modules/workflows/components/fields/RolesMultiSelect.js +53 -0
- package/dist/modules/workflows/components/fields/RolesMultiSelect.js.map +7 -0
- package/dist/modules/workflows/components/fields/RouteOrderEditor.js +130 -0
- package/dist/modules/workflows/components/fields/RouteOrderEditor.js.map +7 -0
- package/dist/modules/workflows/components/fields/SchemaBuilderDialog.js +104 -0
- package/dist/modules/workflows/components/fields/SchemaBuilderDialog.js.map +7 -0
- package/dist/modules/workflows/components/fields/StartPreConditionsEditor.js +2 -2
- package/dist/modules/workflows/components/fields/StartPreConditionsEditor.js.map +2 -2
- package/dist/modules/workflows/components/fields/TaskAssignmentField.js +210 -0
- package/dist/modules/workflows/components/fields/TaskAssignmentField.js.map +7 -0
- package/dist/modules/workflows/components/fields/TaskDeadlineFields.js +126 -0
- package/dist/modules/workflows/components/fields/TaskDeadlineFields.js.map +7 -0
- package/dist/modules/workflows/components/fields/TaskDecisionsField.js +218 -0
- package/dist/modules/workflows/components/fields/TaskDecisionsField.js.map +7 -0
- package/dist/modules/workflows/components/fields/TaskEntityBindingsField.js +114 -0
- package/dist/modules/workflows/components/fields/TaskEntityBindingsField.js.map +7 -0
- package/dist/modules/workflows/components/fields/TemplateTextControl.js +64 -0
- package/dist/modules/workflows/components/fields/TemplateTextControl.js.map +7 -0
- package/dist/modules/workflows/components/fields/VariablePickerButton.js +121 -0
- package/dist/modules/workflows/components/fields/VariablePickerButton.js.map +7 -0
- package/dist/modules/workflows/components/fields/ledgerDropTarget.js +35 -0
- package/dist/modules/workflows/components/fields/ledgerDropTarget.js.map +7 -0
- package/dist/modules/workflows/components/fields/useActivityTypeOptions.js +16 -0
- package/dist/modules/workflows/components/fields/useActivityTypeOptions.js.map +7 -0
- package/dist/modules/workflows/components/formConfig.js +39 -10
- package/dist/modules/workflows/components/formConfig.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileDefinitionDetail.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileMetadataSheet.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileTaskForm.js +29 -2
- package/dist/modules/workflows/components/mobile/MobileTaskForm.js.map +2 -2
- package/dist/modules/workflows/components/mobile/MobileVisualEditor.js +64 -65
- package/dist/modules/workflows/components/mobile/MobileVisualEditor.js.map +2 -2
- package/dist/modules/workflows/components/nodes/AnnotationGroupNode.js +64 -0
- package/dist/modules/workflows/components/nodes/AnnotationGroupNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/AutomatedNode.js +24 -16
- package/dist/modules/workflows/components/nodes/AutomatedNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/EndNode.js +13 -12
- package/dist/modules/workflows/components/nodes/EndNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/ErrorOutputHandle.js +29 -0
- package/dist/modules/workflows/components/nodes/ErrorOutputHandle.js.map +7 -0
- package/dist/modules/workflows/components/nodes/IfElseNode.js +51 -0
- package/dist/modules/workflows/components/nodes/IfElseNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/InvokeAgentNode.js +122 -0
- package/dist/modules/workflows/components/nodes/InvokeAgentNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/NodeOutcomeRows.js +100 -0
- package/dist/modules/workflows/components/nodes/NodeOutcomeRows.js.map +7 -0
- package/dist/modules/workflows/components/nodes/ParallelForkNode.js +18 -14
- package/dist/modules/workflows/components/nodes/ParallelForkNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/ParallelJoinNode.js +18 -14
- package/dist/modules/workflows/components/nodes/ParallelJoinNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/StartNode.js +66 -15
- package/dist/modules/workflows/components/nodes/StartNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/StickyNoteNode.js +54 -0
- package/dist/modules/workflows/components/nodes/StickyNoteNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/SubWorkflowNode.js +107 -41
- package/dist/modules/workflows/components/nodes/SubWorkflowNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/SwitchNode.js +51 -0
- package/dist/modules/workflows/components/nodes/SwitchNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/TriggerCap.js +65 -0
- package/dist/modules/workflows/components/nodes/TriggerCap.js.map +7 -0
- package/dist/modules/workflows/components/nodes/TriggerNode.js +111 -0
- package/dist/modules/workflows/components/nodes/TriggerNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/UserTaskNode.js +43 -17
- package/dist/modules/workflows/components/nodes/UserTaskNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/WaitForConditionNode.js +53 -0
- package/dist/modules/workflows/components/nodes/WaitForConditionNode.js.map +7 -0
- package/dist/modules/workflows/components/nodes/WaitForSignalNode.js +21 -15
- package/dist/modules/workflows/components/nodes/WaitForSignalNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +21 -15
- package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +2 -2
- package/dist/modules/workflows/components/nodes/index.js +14 -0
- package/dist/modules/workflows/components/nodes/index.js.map +2 -2
- package/dist/modules/workflows/components/portal/PortalTaskForm.js +196 -0
- package/dist/modules/workflows/components/portal/PortalTaskForm.js.map +7 -0
- package/dist/modules/workflows/components/run/AgentActivityPanel.js +22 -0
- package/dist/modules/workflows/components/run/AgentActivityPanel.js.map +7 -0
- package/dist/modules/workflows/components/run/RerunStepDialog.js +106 -0
- package/dist/modules/workflows/components/run/RerunStepDialog.js.map +7 -0
- package/dist/modules/workflows/components/run/RunEventBadge.js +30 -0
- package/dist/modules/workflows/components/run/RunEventBadge.js.map +7 -0
- package/dist/modules/workflows/components/run/RunGantt.js +130 -0
- package/dist/modules/workflows/components/run/RunGantt.js.map +7 -0
- package/dist/modules/workflows/components/run/RunStatusBadge.js +82 -0
- package/dist/modules/workflows/components/run/RunStatusBadge.js.map +7 -0
- package/dist/modules/workflows/components/run/RunStepInspector.js +116 -0
- package/dist/modules/workflows/components/run/RunStepInspector.js.map +7 -0
- package/dist/modules/workflows/components/run/WouldDoReport.js +52 -0
- package/dist/modules/workflows/components/run/WouldDoReport.js.map +7 -0
- package/dist/modules/workflows/components/run/useAgentLabels.js +28 -0
- package/dist/modules/workflows/components/run/useAgentLabels.js.map +7 -0
- package/dist/modules/workflows/components/run/useLastRunOverlay.js +89 -0
- package/dist/modules/workflows/components/run/useLastRunOverlay.js.map +7 -0
- package/dist/modules/workflows/components/run/useLiveAgentActions.js +41 -0
- package/dist/modules/workflows/components/run/useLiveAgentActions.js.map +7 -0
- package/dist/modules/workflows/components/run/useLiveRunUpdates.js +57 -0
- package/dist/modules/workflows/components/run/useLiveRunUpdates.js.map +7 -0
- package/dist/modules/workflows/components/settings/CommandSettingsPageClient.js +156 -0
- package/dist/modules/workflows/components/settings/CommandSettingsPageClient.js.map +7 -0
- package/dist/modules/workflows/components/work-inbox/EntityContextPanel.js +72 -0
- package/dist/modules/workflows/components/work-inbox/EntityContextPanel.js.map +7 -0
- package/dist/modules/workflows/components/work-inbox/ReassignTaskDialog.js +156 -0
- package/dist/modules/workflows/components/work-inbox/ReassignTaskDialog.js.map +7 -0
- package/dist/modules/workflows/components/work-inbox/StatusChip.js +46 -0
- package/dist/modules/workflows/components/work-inbox/StatusChip.js.map +7 -0
- package/dist/modules/workflows/components/work-inbox/TaskActionNotice.js +53 -0
- package/dist/modules/workflows/components/work-inbox/TaskActionNotice.js.map +7 -0
- package/dist/modules/workflows/components/work-inbox/TaskFormFields.js +261 -0
- package/dist/modules/workflows/components/work-inbox/TaskFormFields.js.map +7 -0
- package/dist/modules/workflows/components/work-inbox/WorkInboxEmptyState.js +27 -0
- package/dist/modules/workflows/components/work-inbox/WorkInboxEmptyState.js.map +7 -0
- package/dist/modules/workflows/data/activity-config-schemas.js +191 -0
- package/dist/modules/workflows/data/activity-config-schemas.js.map +7 -0
- package/dist/modules/workflows/data/activity-config-warnings.js +42 -0
- package/dist/modules/workflows/data/activity-config-warnings.js.map +7 -0
- package/dist/modules/workflows/data/branching-route-warnings.js +65 -0
- package/dist/modules/workflows/data/branching-route-warnings.js.map +7 -0
- package/dist/modules/workflows/data/entities.js +144 -4
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/data/metrics-validators.js +35 -0
- package/dist/modules/workflows/data/metrics-validators.js.map +7 -0
- package/dist/modules/workflows/data/task-primitives.js +14 -0
- package/dist/modules/workflows/data/task-primitives.js.map +7 -0
- package/dist/modules/workflows/data/validators.js +504 -61
- package/dist/modules/workflows/data/validators.js.map +3 -3
- package/dist/modules/workflows/di.js +16 -1
- package/dist/modules/workflows/di.js.map +2 -2
- package/dist/modules/workflows/events.js +66 -8
- package/dist/modules/workflows/events.js.map +2 -2
- package/dist/modules/workflows/examples/oze-residential-install-60-nodes.json +1545 -0
- package/dist/modules/workflows/examples/sales-pipeline-definition.json +35 -35
- package/dist/modules/workflows/examples/templates/lead-to-install.json +160 -0
- package/dist/modules/workflows/examples/templates/order-approval.json +230 -0
- package/dist/modules/workflows/examples/templates/task-escalation.json +110 -0
- package/dist/modules/workflows/examples/templates/webhook-integration.json +113 -0
- package/dist/modules/workflows/extension-points.js +1 -1
- package/dist/modules/workflows/extension-points.js.map +2 -2
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js +144 -0
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.js.map +7 -0
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.meta.js +12 -0
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.meta.js.map +7 -0
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/page.js +114 -0
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/page.js.map +7 -0
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/page.meta.js +18 -0
- package/dist/modules/workflows/frontend/[orgSlug]/portal/tasks/page.meta.js.map +7 -0
- package/dist/modules/workflows/lib/activity-executor.js +505 -137
- package/dist/modules/workflows/lib/activity-executor.js.map +3 -3
- package/dist/modules/workflows/lib/activity-queue-types.js +3 -1
- package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
- package/dist/modules/workflows/lib/activity-registry-bootstrap.js +2 -0
- package/dist/modules/workflows/lib/activity-registry-bootstrap.js.map +7 -0
- package/dist/modules/workflows/lib/activity-registry.js +27 -0
- package/dist/modules/workflows/lib/activity-registry.js.map +7 -0
- package/dist/modules/workflows/lib/activity-types.js +291 -0
- package/dist/modules/workflows/lib/activity-types.js.map +7 -0
- package/dist/modules/workflows/lib/activity-worker-handler.js +456 -32
- package/dist/modules/workflows/lib/activity-worker-handler.js.map +3 -3
- package/dist/modules/workflows/lib/advanced-config.js +31 -0
- package/dist/modules/workflows/lib/advanced-config.js.map +7 -0
- package/dist/modules/workflows/lib/agent-disposition-task.js +198 -0
- package/dist/modules/workflows/lib/agent-disposition-task.js.map +7 -0
- package/dist/modules/workflows/lib/agent-outcome-paths.js +82 -0
- package/dist/modules/workflows/lib/agent-outcome-paths.js.map +7 -0
- package/dist/modules/workflows/lib/agent-result-mapping.js +54 -0
- package/dist/modules/workflows/lib/agent-result-mapping.js.map +7 -0
- package/dist/modules/workflows/lib/agent-review-inspector.js +61 -0
- package/dist/modules/workflows/lib/agent-review-inspector.js.map +7 -0
- package/dist/modules/workflows/lib/agent-review.js +66 -0
- package/dist/modules/workflows/lib/agent-review.js.map +7 -0
- package/dist/modules/workflows/lib/ai-authoring.js +155 -0
- package/dist/modules/workflows/lib/ai-authoring.js.map +7 -0
- package/dist/modules/workflows/lib/ai-draft-runner.js +43 -0
- package/dist/modules/workflows/lib/ai-draft-runner.js.map +7 -0
- package/dist/modules/workflows/lib/annotation-events.js +10 -0
- package/dist/modules/workflows/lib/annotation-events.js.map +7 -0
- package/dist/modules/workflows/lib/branching-routes.js +182 -0
- package/dist/modules/workflows/lib/branching-routes.js.map +7 -0
- package/dist/modules/workflows/lib/breach-routing.js +50 -0
- package/dist/modules/workflows/lib/breach-routing.js.map +7 -0
- package/dist/modules/workflows/lib/bulk-replay.js +101 -0
- package/dist/modules/workflows/lib/bulk-replay.js.map +7 -0
- package/dist/modules/workflows/lib/business-rule-usage.js +91 -0
- package/dist/modules/workflows/lib/business-rule-usage.js.map +7 -0
- package/dist/modules/workflows/lib/caller-graph.js +53 -0
- package/dist/modules/workflows/lib/caller-graph.js.map +7 -0
- package/dist/modules/workflows/lib/code-view-apply.js +86 -0
- package/dist/modules/workflows/lib/code-view-apply.js.map +7 -0
- package/dist/modules/workflows/lib/collect-validation-issues.js +164 -0
- package/dist/modules/workflows/lib/collect-validation-issues.js.map +7 -0
- package/dist/modules/workflows/lib/compensation-ghosts.js +72 -0
- package/dist/modules/workflows/lib/compensation-ghosts.js.map +7 -0
- package/dist/modules/workflows/lib/component-guard.js +7 -0
- package/dist/modules/workflows/lib/component-guard.js.map +7 -0
- package/dist/modules/workflows/lib/condition-handler.js +443 -0
- package/dist/modules/workflows/lib/condition-handler.js.map +7 -0
- package/dist/modules/workflows/lib/context-ledger.js +448 -0
- package/dist/modules/workflows/lib/context-ledger.js.map +7 -0
- package/dist/modules/workflows/lib/crud-form-dialogs-flag.js +8 -0
- package/dist/modules/workflows/lib/crud-form-dialogs-flag.js.map +7 -0
- package/dist/modules/workflows/lib/data-edge-mapping.js +63 -0
- package/dist/modules/workflows/lib/data-edge-mapping.js.map +7 -0
- package/dist/modules/workflows/lib/definition-context-schema.js +56 -0
- package/dist/modules/workflows/lib/definition-context-schema.js.map +7 -0
- package/dist/modules/workflows/lib/definition-edit-safety.js +123 -0
- package/dist/modules/workflows/lib/definition-edit-safety.js.map +7 -0
- package/dist/modules/workflows/lib/definition-error-body.js +33 -0
- package/dist/modules/workflows/lib/definition-error-body.js.map +7 -0
- package/dist/modules/workflows/lib/definition-evaluation.js +79 -0
- package/dist/modules/workflows/lib/definition-evaluation.js.map +7 -0
- package/dist/modules/workflows/lib/definition-grant.js +90 -0
- package/dist/modules/workflows/lib/definition-grant.js.map +7 -0
- package/dist/modules/workflows/lib/definition-json-locations.js +139 -0
- package/dist/modules/workflows/lib/definition-json-locations.js.map +7 -0
- package/dist/modules/workflows/lib/definition-payload.js +31 -0
- package/dist/modules/workflows/lib/definition-payload.js.map +7 -0
- package/dist/modules/workflows/lib/draft-restore.js +49 -0
- package/dist/modules/workflows/lib/draft-restore.js.map +7 -0
- package/dist/modules/workflows/lib/dry-run.js +106 -0
- package/dist/modules/workflows/lib/dry-run.js.map +7 -0
- package/dist/modules/workflows/lib/duration.js +66 -17
- package/dist/modules/workflows/lib/duration.js.map +2 -2
- package/dist/modules/workflows/lib/edge-reattachment.js +79 -0
- package/dist/modules/workflows/lib/edge-reattachment.js.map +7 -0
- package/dist/modules/workflows/lib/edgeFormTransforms.js +7 -11
- package/dist/modules/workflows/lib/edgeFormTransforms.js.map +2 -2
- package/dist/modules/workflows/lib/editor-annotations.js +195 -0
- package/dist/modules/workflows/lib/editor-annotations.js.map +7 -0
- package/dist/modules/workflows/lib/editor-commands.js +103 -0
- package/dist/modules/workflows/lib/editor-commands.js.map +7 -0
- package/dist/modules/workflows/lib/editor-history.js +51 -0
- package/dist/modules/workflows/lib/editor-history.js.map +7 -0
- package/dist/modules/workflows/lib/endpoint-catalog.js +181 -0
- package/dist/modules/workflows/lib/endpoint-catalog.js.map +7 -0
- package/dist/modules/workflows/lib/endpoint-path.js +90 -0
- package/dist/modules/workflows/lib/endpoint-path.js.map +7 -0
- package/dist/modules/workflows/lib/engine-version.js +42 -0
- package/dist/modules/workflows/lib/engine-version.js.map +7 -0
- package/dist/modules/workflows/lib/error-handler.js +132 -0
- package/dist/modules/workflows/lib/error-handler.js.map +7 -0
- package/dist/modules/workflows/lib/error-routing.js +104 -0
- package/dist/modules/workflows/lib/error-routing.js.map +7 -0
- package/dist/modules/workflows/lib/event-logger.js +23 -0
- package/dist/modules/workflows/lib/event-logger.js.map +2 -2
- package/dist/modules/workflows/lib/expression-refs.js +168 -0
- package/dist/modules/workflows/lib/expression-refs.js.map +7 -0
- package/dist/modules/workflows/lib/failure-grouping.js +62 -0
- package/dist/modules/workflows/lib/failure-grouping.js.map +7 -0
- package/dist/modules/workflows/lib/find-definition.js +1 -0
- package/dist/modules/workflows/lib/find-definition.js.map +2 -2
- package/dist/modules/workflows/lib/flow-logic-warnings.js +338 -0
- package/dist/modules/workflows/lib/flow-logic-warnings.js.map +7 -0
- package/dist/modules/workflows/lib/graph-utils.js +328 -77
- package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
- package/dist/modules/workflows/lib/icon-search.js +32 -0
- package/dist/modules/workflows/lib/icon-search.js.map +7 -0
- package/dist/modules/workflows/lib/insert-at-cursor.js +26 -0
- package/dist/modules/workflows/lib/insert-at-cursor.js.map +7 -0
- package/dist/modules/workflows/lib/instance-date-filter.js +28 -0
- package/dist/modules/workflows/lib/instance-date-filter.js.map +7 -0
- package/dist/modules/workflows/lib/interpolation-pipeline.js +271 -0
- package/dist/modules/workflows/lib/interpolation-pipeline.js.map +7 -0
- package/dist/modules/workflows/lib/ledger-drag.js +28 -0
- package/dist/modules/workflows/lib/ledger-drag.js.map +7 -0
- package/dist/modules/workflows/lib/ledger-entry-display.js +66 -0
- package/dist/modules/workflows/lib/ledger-entry-display.js.map +7 -0
- package/dist/modules/workflows/lib/ledger-schema-flatten.js +82 -0
- package/dist/modules/workflows/lib/ledger-schema-flatten.js.map +7 -0
- package/dist/modules/workflows/lib/metrics/definition-metrics.js +110 -0
- package/dist/modules/workflows/lib/metrics/definition-metrics.js.map +7 -0
- package/dist/modules/workflows/lib/metrics/metrics-display.js +26 -0
- package/dist/modules/workflows/lib/metrics/metrics-display.js.map +7 -0
- package/dist/modules/workflows/lib/metrics/rollup-schedule.js +7 -0
- package/dist/modules/workflows/lib/metrics/rollup-schedule.js.map +7 -0
- package/dist/modules/workflows/lib/metrics/rollup-service.js +160 -0
- package/dist/modules/workflows/lib/metrics/rollup-service.js.map +7 -0
- package/dist/modules/workflows/lib/node-config-summary.js +128 -0
- package/dist/modules/workflows/lib/node-config-summary.js.map +7 -0
- package/dist/modules/workflows/lib/node-geometry.js +53 -0
- package/dist/modules/workflows/lib/node-geometry.js.map +7 -0
- package/dist/modules/workflows/lib/node-nudge.js +44 -0
- package/dist/modules/workflows/lib/node-nudge.js.map +7 -0
- package/dist/modules/workflows/lib/node-outcome-rows.js +88 -0
- package/dist/modules/workflows/lib/node-outcome-rows.js.map +7 -0
- package/dist/modules/workflows/lib/node-placement.js +55 -0
- package/dist/modules/workflows/lib/node-placement.js.map +7 -0
- package/dist/modules/workflows/lib/node-type-icons.js +54 -13
- package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
- package/dist/modules/workflows/lib/nodeFormTransforms.js +329 -19
- package/dist/modules/workflows/lib/nodeFormTransforms.js.map +2 -2
- package/dist/modules/workflows/lib/outcome-routing.js +158 -0
- package/dist/modules/workflows/lib/outcome-routing.js.map +7 -0
- package/dist/modules/workflows/lib/owned-definition.js +107 -0
- package/dist/modules/workflows/lib/owned-definition.js.map +7 -0
- package/dist/modules/workflows/lib/owned-node-changes.js +10 -0
- package/dist/modules/workflows/lib/owned-node-changes.js.map +7 -0
- package/dist/modules/workflows/lib/palette-drag.js +36 -0
- package/dist/modules/workflows/lib/palette-drag.js.map +7 -0
- package/dist/modules/workflows/lib/palette-drop.js +45 -0
- package/dist/modules/workflows/lib/palette-drop.js.map +7 -0
- package/dist/modules/workflows/lib/parallel-handler.js +57 -2
- package/dist/modules/workflows/lib/parallel-handler.js.map +2 -2
- package/dist/modules/workflows/lib/port-contract.js +54 -0
- package/dist/modules/workflows/lib/port-contract.js.map +7 -0
- package/dist/modules/workflows/lib/portal-task-access.js +89 -0
- package/dist/modules/workflows/lib/portal-task-access.js.map +7 -0
- package/dist/modules/workflows/lib/rerun-from-step.js +65 -0
- package/dist/modules/workflows/lib/rerun-from-step.js.map +7 -0
- package/dist/modules/workflows/lib/route-chip-events.js +23 -0
- package/dist/modules/workflows/lib/route-chip-events.js.map +7 -0
- package/dist/modules/workflows/lib/route-chips.js +91 -0
- package/dist/modules/workflows/lib/route-chips.js.map +7 -0
- package/dist/modules/workflows/lib/route-edge.js +17 -0
- package/dist/modules/workflows/lib/route-edge.js.map +7 -0
- package/dist/modules/workflows/lib/route-kinds.js +67 -0
- package/dist/modules/workflows/lib/route-kinds.js.map +7 -0
- package/dist/modules/workflows/lib/route-priority.js +104 -0
- package/dist/modules/workflows/lib/route-priority.js.map +7 -0
- package/dist/modules/workflows/lib/run-duration.js +44 -0
- package/dist/modules/workflows/lib/run-duration.js.map +7 -0
- package/dist/modules/workflows/lib/run-event-tone.js +25 -0
- package/dist/modules/workflows/lib/run-event-tone.js.map +7 -0
- package/dist/modules/workflows/lib/run-execution.js +305 -0
- package/dist/modules/workflows/lib/run-execution.js.map +7 -0
- package/dist/modules/workflows/lib/run-gantt.js +180 -0
- package/dist/modules/workflows/lib/run-gantt.js.map +7 -0
- package/dist/modules/workflows/lib/run-outcome-evidence.js +33 -0
- package/dist/modules/workflows/lib/run-outcome-evidence.js.map +7 -0
- package/dist/modules/workflows/lib/run-outcome.js +48 -0
- package/dist/modules/workflows/lib/run-outcome.js.map +7 -0
- package/dist/modules/workflows/lib/sample-resolver.js +71 -0
- package/dist/modules/workflows/lib/sample-resolver.js.map +7 -0
- package/dist/modules/workflows/lib/seeds.js +1 -1
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/lib/server-output-contract.js +65 -0
- package/dist/modules/workflows/lib/server-output-contract.js.map +7 -0
- package/dist/modules/workflows/lib/set-variable.js +47 -0
- package/dist/modules/workflows/lib/set-variable.js.map +7 -0
- package/dist/modules/workflows/lib/signal-handler.js +63 -4
- package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
- package/dist/modules/workflows/lib/start-fixtures.js +42 -0
- package/dist/modules/workflows/lib/start-fixtures.js.map +7 -0
- package/dist/modules/workflows/lib/status-colors.js +153 -22
- package/dist/modules/workflows/lib/status-colors.js.map +2 -2
- package/dist/modules/workflows/lib/step-handler.js +529 -53
- package/dist/modules/workflows/lib/step-handler.js.map +3 -3
- package/dist/modules/workflows/lib/step-presentation.js +147 -0
- package/dist/modules/workflows/lib/step-presentation.js.map +7 -0
- package/dist/modules/workflows/lib/step-through.js +26 -0
- package/dist/modules/workflows/lib/step-through.js.map +7 -0
- package/dist/modules/workflows/lib/step-type-conversion.js +167 -0
- package/dist/modules/workflows/lib/step-type-conversion.js.map +7 -0
- package/dist/modules/workflows/lib/subgraph-clipboard.js +114 -0
- package/dist/modules/workflows/lib/subgraph-clipboard.js.map +7 -0
- package/dist/modules/workflows/lib/task-decision-context.js +38 -0
- package/dist/modules/workflows/lib/task-decision-context.js.map +7 -0
- package/dist/modules/workflows/lib/task-decisions.js +49 -0
- package/dist/modules/workflows/lib/task-decisions.js.map +7 -0
- package/dist/modules/workflows/lib/task-entity-access.js +62 -0
- package/dist/modules/workflows/lib/task-entity-access.js.map +7 -0
- package/dist/modules/workflows/lib/task-entity-aliases.js +39 -0
- package/dist/modules/workflows/lib/task-entity-aliases.js.map +7 -0
- package/dist/modules/workflows/lib/task-entity-types.js +36 -0
- package/dist/modules/workflows/lib/task-entity-types.js.map +7 -0
- package/dist/modules/workflows/lib/task-form-registry.js +33 -0
- package/dist/modules/workflows/lib/task-form-registry.js.map +7 -0
- package/dist/modules/workflows/lib/task-form-schema.js +75 -0
- package/dist/modules/workflows/lib/task-form-schema.js.map +7 -0
- package/dist/modules/workflows/lib/task-handler.js +251 -32
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-inspector-config.js +182 -0
- package/dist/modules/workflows/lib/task-inspector-config.js.map +7 -0
- package/dist/modules/workflows/lib/task-list-query.js +19 -0
- package/dist/modules/workflows/lib/task-list-query.js.map +7 -0
- package/dist/modules/workflows/lib/task-notifications.js +119 -0
- package/dist/modules/workflows/lib/task-notifications.js.map +7 -0
- package/dist/modules/workflows/lib/task-permission-settings.js +43 -0
- package/dist/modules/workflows/lib/task-permission-settings.js.map +7 -0
- package/dist/modules/workflows/lib/task-presets.js +35 -0
- package/dist/modules/workflows/lib/task-presets.js.map +7 -0
- package/dist/modules/workflows/lib/task-quick-action.js +45 -0
- package/dist/modules/workflows/lib/task-quick-action.js.map +7 -0
- package/dist/modules/workflows/lib/task-resolution.js +120 -0
- package/dist/modules/workflows/lib/task-resolution.js.map +7 -0
- package/dist/modules/workflows/lib/task-sla.js +357 -0
- package/dist/modules/workflows/lib/task-sla.js.map +7 -0
- package/dist/modules/workflows/lib/task-visibility-request.js +266 -0
- package/dist/modules/workflows/lib/task-visibility-request.js.map +7 -0
- package/dist/modules/workflows/lib/task-visibility.js +194 -0
- package/dist/modules/workflows/lib/task-visibility.js.map +7 -0
- package/dist/modules/workflows/lib/timer-handler.js +3 -1
- package/dist/modules/workflows/lib/timer-handler.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +148 -20
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/trigger-node.js +121 -0
- package/dist/modules/workflows/lib/trigger-node.js.map +7 -0
- package/dist/modules/workflows/lib/visual-editor-navigation.js +14 -0
- package/dist/modules/workflows/lib/visual-editor-navigation.js.map +7 -0
- package/dist/modules/workflows/lib/work-inbox/empty-state.js +52 -0
- package/dist/modules/workflows/lib/work-inbox/empty-state.js.map +7 -0
- package/dist/modules/workflows/lib/work-inbox/entity-links.js +63 -0
- package/dist/modules/workflows/lib/work-inbox/entity-links.js.map +7 -0
- package/dist/modules/workflows/lib/work-inbox/navigation.js +11 -0
- package/dist/modules/workflows/lib/work-inbox/navigation.js.map +7 -0
- package/dist/modules/workflows/lib/work-inbox/presentation.js +90 -0
- package/dist/modules/workflows/lib/work-inbox/presentation.js.map +7 -0
- package/dist/modules/workflows/lib/work-inbox/provider.js +118 -0
- package/dist/modules/workflows/lib/work-inbox/provider.js.map +7 -0
- package/dist/modules/workflows/lib/work-inbox/query.js +70 -0
- package/dist/modules/workflows/lib/work-inbox/query.js.map +7 -0
- package/dist/modules/workflows/lib/work-inbox/service.js +56 -0
- package/dist/modules/workflows/lib/work-inbox/service.js.map +7 -0
- package/dist/modules/workflows/lib/work-inbox/user-task-source.js +149 -0
- package/dist/modules/workflows/lib/work-inbox/user-task-source.js.map +7 -0
- package/dist/modules/workflows/lib/workflow-command-enablement.js +57 -0
- package/dist/modules/workflows/lib/workflow-command-enablement.js.map +7 -0
- package/dist/modules/workflows/lib/workflow-command-settings.js +54 -0
- package/dist/modules/workflows/lib/workflow-command-settings.js.map +7 -0
- package/dist/modules/workflows/lib/workflow-executor.js +658 -6
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-function-registry.js +40 -0
- package/dist/modules/workflows/lib/workflow-function-registry.js.map +7 -0
- package/dist/modules/workflows/lib/workflow-safe-commands.js +11 -1
- package/dist/modules/workflows/lib/workflow-safe-commands.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-templates.js +69 -0
- package/dist/modules/workflows/lib/workflow-templates.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260626214855_workflows.js +15 -0
- package/dist/modules/workflows/migrations/Migration20260626214855_workflows.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260626222424_workflows.js +17 -0
- package/dist/modules/workflows/migrations/Migration20260626222424_workflows.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260727074335_workflows.js +15 -0
- package/dist/modules/workflows/migrations/Migration20260727074335_workflows.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260728104038_workflows.js +13 -0
- package/dist/modules/workflows/migrations/Migration20260728104038_workflows.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260728163001_workflows.js +15 -0
- package/dist/modules/workflows/migrations/Migration20260728163001_workflows.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260729061736_workflows.js +13 -0
- package/dist/modules/workflows/migrations/Migration20260729061736_workflows.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260729192652_workflows.js +12 -0
- package/dist/modules/workflows/migrations/Migration20260729192652_workflows.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260730085000_workflows.js +13 -0
- package/dist/modules/workflows/migrations/Migration20260730085000_workflows.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260730092706_workflows.js +13 -0
- package/dist/modules/workflows/migrations/Migration20260730092706_workflows.js.map +7 -0
- package/dist/modules/workflows/notifications.js +38 -10
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/dist/modules/workflows/setup.js +81 -1
- package/dist/modules/workflows/setup.js.map +2 -2
- package/dist/modules/workflows/subscribers/task-assigned-notification.js +19 -27
- package/dist/modules/workflows/subscribers/task-assigned-notification.js.map +2 -2
- package/dist/modules/workflows/subscribers/task-deadline-breached-notification.js +24 -0
- package/dist/modules/workflows/subscribers/task-deadline-breached-notification.js.map +7 -0
- package/dist/modules/workflows/subscribers/task-reminder-notification.js +24 -0
- package/dist/modules/workflows/subscribers/task-reminder-notification.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/pending-work/widget.client.js +73 -0
- package/dist/modules/workflows/widgets/injection/pending-work/widget.client.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/pending-work/widget.js +17 -0
- package/dist/modules/workflows/widgets/injection/pending-work/widget.js.map +7 -0
- package/dist/modules/workflows/widgets/injection-table.js +26 -0
- package/dist/modules/workflows/widgets/injection-table.js.map +2 -2
- package/dist/modules/workflows/workers/workflow-activities.worker.js +61 -39
- package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
- package/dist/modules/workflows/workers/workflow-definition-metric-rollup.worker.js +40 -0
- package/dist/modules/workflows/workers/workflow-definition-metric-rollup.worker.js.map +7 -0
- package/dist/modules/workflows/workers/workflow-instance-bulk-replay.js +43 -0
- package/dist/modules/workflows/workers/workflow-instance-bulk-replay.js.map +7 -0
- package/dist/modules/workflows/workers/workflow-invoke-agent.worker.js +30 -0
- package/dist/modules/workflows/workers/workflow-invoke-agent.worker.js.map +7 -0
- package/generated/entities/action_log/index.ts +1 -0
- package/generated/entities/user/index.ts +1 -0
- package/generated/entities/user_task/index.ts +7 -0
- package/generated/entities/workflow_definition/index.ts +3 -0
- package/generated/entities/workflow_definition_draft/index.ts +11 -0
- package/generated/entities/workflow_definition_metric_rollup/index.ts +10 -0
- package/generated/entities/workflow_instance/index.ts +2 -0
- package/generated/entities.ids.generated.ts +4 -1
- package/generated/entity-fields-registry.ts +39 -0
- package/jest.setup.ts +13 -0
- package/package.json +8 -7
- package/src/helpers/integration/workflowsFixtures.ts +765 -5
- package/src/helpers/integration/workflowsUi.ts +318 -0
- package/src/modules/attachments/lib/createFromBuffer.ts +120 -0
- package/src/modules/audit_logs/data/entities.ts +12 -0
- package/src/modules/audit_logs/data/validators.ts +1 -0
- package/src/modules/audit_logs/lib/projections.ts +5 -1
- package/src/modules/audit_logs/migrations/.snapshot-open-mercato.json +26 -0
- package/src/modules/audit_logs/migrations/Migration20260625120000.ts +15 -0
- package/src/modules/audit_logs/services/actionLogService.ts +3 -0
- package/src/modules/auth/data/entities.ts +21 -0
- package/src/modules/auth/i18n/de.json +31 -0
- package/src/modules/auth/i18n/en.json +31 -0
- package/src/modules/auth/i18n/es.json +31 -0
- package/src/modules/auth/i18n/ko.json +31 -0
- package/src/modules/auth/i18n/pl.json +31 -0
- package/src/modules/auth/lib/executionPrincipal.ts +235 -0
- package/src/modules/auth/migrations/.snapshot-open-mercato.json +16 -0
- package/src/modules/auth/migrations/Migration20260625050000.ts +21 -0
- package/src/modules/business_rules/components/InlineRuleEditor.tsx +231 -0
- package/src/modules/business_rules/data/validators.ts +6 -0
- package/src/modules/business_rules/i18n/de.json +4 -0
- package/src/modules/business_rules/i18n/en.json +4 -0
- package/src/modules/business_rules/i18n/es.json +4 -0
- package/src/modules/business_rules/i18n/ko.json +4 -0
- package/src/modules/business_rules/i18n/pl.json +4 -0
- package/src/modules/business_rules/lib/rule-engine.ts +15 -1
- package/src/modules/catalog/commands/products.ts +12 -0
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/ko.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/catalog/workflows.ts +35 -0
- package/src/modules/customer_accounts/AGENTS.md +11 -0
- package/src/modules/customer_accounts/backend/customer_accounts/roles/[id]/page.tsx +2 -0
- package/src/modules/customer_accounts/cli.ts +96 -0
- package/src/modules/customer_accounts/i18n/de.json +5 -0
- package/src/modules/customer_accounts/i18n/en.json +5 -0
- package/src/modules/customer_accounts/i18n/es.json +5 -0
- package/src/modules/customer_accounts/i18n/ko.json +5 -0
- package/src/modules/customer_accounts/i18n/pl.json +5 -0
- package/src/modules/customer_accounts/lib/customerRoleAclSync.ts +92 -0
- package/src/modules/customer_accounts/setup.ts +15 -3
- package/src/modules/customers/commands/deals.ts +5 -0
- package/src/modules/customers/components/detail/ChangelogEntryRow.tsx +4 -2
- package/src/modules/customers/components/detail/utils.ts +2 -0
- package/src/modules/customers/events.ts +21 -3
- package/src/modules/customers/i18n/de.json +4 -0
- package/src/modules/customers/i18n/en.json +4 -0
- package/src/modules/customers/i18n/es.json +4 -0
- package/src/modules/customers/i18n/ko.json +4 -0
- package/src/modules/customers/i18n/pl.json +4 -0
- package/src/modules/customers/lib/link-workflow-task-handler.ts +136 -0
- package/src/modules/customers/lib/todoCompatibility.ts +4 -1
- package/src/modules/customers/lib/workflowTaskLink.ts +23 -0
- package/src/modules/customers/subscribers/link-workflow-task.ts +19 -0
- package/src/modules/customers/workflows.ts +57 -0
- package/src/modules/entities/api/records.ts +18 -62
- package/src/modules/entities/lib/entityClassification.ts +102 -0
- package/src/modules/sales/commands/documents.ts +46 -0
- package/src/modules/sales/events.ts +20 -2
- package/src/modules/sales/i18n/de.json +2 -1
- package/src/modules/sales/i18n/en.json +2 -1
- package/src/modules/sales/i18n/es.json +2 -1
- package/src/modules/sales/i18n/ko.json +2 -1
- package/src/modules/sales/i18n/pl.json +2 -1
- package/src/modules/sales/workflows.ts +12 -1
- package/src/modules/seeds/README.md +86 -0
- package/src/modules/seeds/cli.ts +220 -0
- package/src/modules/seeds/examples/sample-seed.json +26 -0
- package/src/modules/seeds/index.ts +11 -0
- package/src/modules/workflows/AGENTS.md +851 -10
- package/src/modules/workflows/acl.ts +96 -0
- package/src/modules/workflows/ai-agents.ts +80 -0
- package/src/modules/workflows/ai-tools/authoring-pack.ts +798 -0
- package/src/modules/workflows/ai-tools/types.ts +117 -0
- package/src/modules/workflows/ai-tools.ts +20 -0
- package/src/modules/workflows/api/command-settings/route.ts +171 -0
- package/src/modules/workflows/api/commands/route.ts +109 -0
- package/src/modules/workflows/api/definitions/[id]/callers/route.ts +95 -0
- package/src/modules/workflows/api/definitions/[id]/context-schema/route.ts +187 -0
- package/src/modules/workflows/api/definitions/[id]/customize/route.ts +6 -1
- package/src/modules/workflows/api/definitions/[id]/draft/route.ts +395 -0
- package/src/modules/workflows/api/definitions/[id]/publish/route.ts +250 -0
- package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +5 -0
- package/src/modules/workflows/api/definitions/[id]/route.ts +107 -7
- package/src/modules/workflows/api/definitions/[id]/test-step/route.ts +288 -0
- package/src/modules/workflows/api/definitions/generate/route.ts +335 -0
- package/src/modules/workflows/api/definitions/route.ts +88 -113
- package/src/modules/workflows/api/definitions/serialize.ts +9 -0
- package/src/modules/workflows/api/endpoints/route.ts +88 -0
- package/src/modules/workflows/api/functions/route.ts +91 -0
- package/src/modules/workflows/api/grantable-features/route.ts +102 -0
- package/src/modules/workflows/api/instances/[id]/context/route.ts +214 -0
- package/src/modules/workflows/api/instances/[id]/rerun-step/route.ts +226 -0
- package/src/modules/workflows/api/instances/[id]/retry/route.ts +18 -2
- package/src/modules/workflows/api/instances/[id]/step-through/route.ts +195 -0
- package/src/modules/workflows/api/instances/[id]/steps/route.ts +160 -0
- package/src/modules/workflows/api/instances/[id]/would-do/route.ts +216 -0
- package/src/modules/workflows/api/instances/bulk-replay/route.ts +141 -0
- package/src/modules/workflows/api/instances/failure-queue/route.ts +197 -0
- package/src/modules/workflows/api/instances/route.ts +135 -3
- package/src/modules/workflows/api/metrics/definitions/route.ts +262 -0
- package/src/modules/workflows/api/openapi.ts +549 -2
- package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +160 -0
- package/src/modules/workflows/api/portal/tasks/[id]/route.ts +124 -0
- package/src/modules/workflows/api/portal/tasks/route.ts +157 -0
- package/src/modules/workflows/api/rule-usage/route.ts +94 -0
- package/src/modules/workflows/api/task-settings/route.ts +138 -0
- package/src/modules/workflows/api/tasks/[id]/claim/route.ts +42 -17
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +47 -15
- package/src/modules/workflows/api/tasks/[id]/reassign/route.ts +197 -0
- package/src/modules/workflows/api/tasks/[id]/route.ts +57 -6
- package/src/modules/workflows/api/tasks/[id]/unclaim/route.ts +132 -0
- package/src/modules/workflows/api/tasks/route.ts +60 -5
- package/src/modules/workflows/api/tasks/serialize.ts +119 -0
- package/src/modules/workflows/api/templates/route.ts +87 -0
- package/src/modules/workflows/api/work-inbox/next/route.ts +170 -0
- package/src/modules/workflows/api/work-inbox/route.ts +153 -0
- package/src/modules/workflows/backend/config/workflows/page.meta.ts +33 -0
- package/src/modules/workflows/backend/config/workflows/page.tsx +12 -0
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +19 -343
- package/src/modules/workflows/backend/definitions/create/page.meta.ts +1 -1
- package/src/modules/workflows/backend/definitions/create/page.tsx +19 -59
- package/src/modules/workflows/backend/definitions/page.tsx +156 -76
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +3129 -584
- package/src/modules/workflows/backend/instances/[id]/page.meta.ts +1 -1
- package/src/modules/workflows/backend/instances/[id]/page.tsx +711 -539
- package/src/modules/workflows/backend/instances/failure-queue/page.meta.ts +16 -0
- package/src/modules/workflows/backend/instances/failure-queue/page.tsx +311 -0
- package/src/modules/workflows/backend/instances/page.meta.ts +1 -1
- package/src/modules/workflows/backend/instances/page.tsx +218 -38
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +639 -400
- package/src/modules/workflows/backend/tasks/page.meta.ts +7 -0
- package/src/modules/workflows/backend/tasks/page.tsx +20 -333
- package/src/modules/workflows/backend/work-inbox/page.meta.ts +15 -0
- package/src/modules/workflows/backend/work-inbox/page.tsx +506 -0
- package/src/modules/workflows/cli.ts +1 -1
- package/src/modules/workflows/commands/tasks.ts +146 -0
- package/src/modules/workflows/components/ActivitiesEditor.tsx +91 -114
- package/src/modules/workflows/components/ActivityEditDialog.tsx +244 -0
- package/src/modules/workflows/components/AgentSelector.tsx +200 -0
- package/src/modules/workflows/components/AnnotationEditDialog.tsx +157 -0
- package/src/modules/workflows/components/BusinessRuleUsagePanel.tsx +106 -0
- package/src/modules/workflows/components/BusinessRulesSelector.tsx +71 -17
- package/src/modules/workflows/components/ConfigJsonTextarea.tsx +111 -0
- package/src/modules/workflows/components/ContextSchemaEditor.tsx +165 -0
- package/src/modules/workflows/components/DefinitionErrorHandlerField.tsx +133 -0
- package/src/modules/workflows/components/DefinitionMetadataDrawer.tsx +456 -0
- package/src/modules/workflows/components/DefinitionMetricsCells.tsx +212 -0
- package/src/modules/workflows/components/DefinitionTriggersEditor.tsx +196 -95
- package/src/modules/workflows/components/EdgeEditDialog.tsx +15 -16
- package/src/modules/workflows/components/EdgeEditDialogCrudForm.tsx +130 -80
- package/src/modules/workflows/components/InputDataPanel.tsx +177 -0
- package/src/modules/workflows/components/InspectorPanel.tsx +250 -0
- package/src/modules/workflows/components/NodeEditDialog.tsx +444 -38
- package/src/modules/workflows/components/NodeEditDialogCrudForm.tsx +848 -178
- package/src/modules/workflows/components/StepsEditor.tsx +9 -2
- package/src/modules/workflows/components/TemplateGalleryDialog.tsx +205 -0
- package/src/modules/workflows/components/TransitionsEditor.tsx +98 -89
- package/src/modules/workflows/components/TriggersDialog.tsx +83 -0
- package/src/modules/workflows/components/TriggersEditor.tsx +363 -0
- package/src/modules/workflows/components/WorkflowAiDraftDialog.tsx +383 -0
- package/src/modules/workflows/components/WorkflowCodeView.tsx +257 -0
- package/src/modules/workflows/components/WorkflowCommandPalette.tsx +82 -0
- package/src/modules/workflows/components/WorkflowCompensationGhostEdge.tsx +78 -0
- package/src/modules/workflows/components/WorkflowDataMappingEdge.tsx +46 -0
- package/src/modules/workflows/components/WorkflowGraph.tsx +54 -1
- package/src/modules/workflows/components/WorkflowGraphImpl.tsx +463 -37
- package/src/modules/workflows/components/WorkflowIconPicker.tsx +144 -0
- package/src/modules/workflows/components/WorkflowLegend.tsx +59 -33
- package/src/modules/workflows/components/WorkflowNodeCard.tsx +355 -28
- package/src/modules/workflows/components/WorkflowRouteChips.tsx +145 -0
- package/src/modules/workflows/components/WorkflowSelector.tsx +33 -14
- package/src/modules/workflows/components/WorkflowTransitionEdge.tsx +83 -10
- package/src/modules/workflows/components/WorkflowTransitionLabel.tsx +13 -2
- package/src/modules/workflows/components/fields/ActivityArrayEditor.tsx +120 -30
- package/src/modules/workflows/components/fields/ActivityConfigFields.tsx +675 -0
- package/src/modules/workflows/components/fields/ActivityTestPanel.tsx +250 -0
- package/src/modules/workflows/components/fields/AgentInvokeConfigField.tsx +580 -0
- package/src/modules/workflows/components/fields/AgentReviewOnBreachField.tsx +88 -0
- package/src/modules/workflows/components/fields/BranchingRoutesEditor.tsx +323 -0
- package/src/modules/workflows/components/fields/BusinessRuleConditionsEditor.tsx +36 -4
- package/src/modules/workflows/components/fields/CommandPicker.tsx +158 -0
- package/src/modules/workflows/components/fields/EndpointPicker.tsx +330 -0
- package/src/modules/workflows/components/fields/FormFieldArrayEditor.tsx +2 -2
- package/src/modules/workflows/components/fields/FunctionPicker.tsx +68 -0
- package/src/modules/workflows/components/fields/MappingArrayEditor.tsx +46 -12
- package/src/modules/workflows/components/fields/PortFieldArrayEditor.tsx +255 -0
- package/src/modules/workflows/components/fields/RolesMultiSelect.tsx +68 -0
- package/src/modules/workflows/components/fields/RouteOrderEditor.tsx +169 -0
- package/src/modules/workflows/components/fields/SchemaBuilderDialog.tsx +139 -0
- package/src/modules/workflows/components/fields/StartPreConditionsEditor.tsx +2 -2
- package/src/modules/workflows/components/fields/TaskAssignmentField.tsx +355 -0
- package/src/modules/workflows/components/fields/TaskDeadlineFields.tsx +172 -0
- package/src/modules/workflows/components/fields/TaskDecisionsField.tsx +294 -0
- package/src/modules/workflows/components/fields/TaskEntityBindingsField.tsx +156 -0
- package/src/modules/workflows/components/fields/TemplateTextControl.tsx +85 -0
- package/src/modules/workflows/components/fields/VariablePickerButton.tsx +177 -0
- package/src/modules/workflows/components/fields/ledgerDropTarget.ts +55 -0
- package/src/modules/workflows/components/fields/useActivityTypeOptions.ts +19 -0
- package/src/modules/workflows/components/formConfig.tsx +84 -10
- package/src/modules/workflows/components/mobile/MobileDefinitionDetail.tsx +5 -0
- package/src/modules/workflows/components/mobile/MobileMetadataSheet.tsx +10 -0
- package/src/modules/workflows/components/mobile/MobileTaskForm.tsx +55 -19
- package/src/modules/workflows/components/mobile/MobileVisualEditor.tsx +49 -40
- package/src/modules/workflows/components/nodes/AnnotationGroupNode.tsx +81 -0
- package/src/modules/workflows/components/nodes/AutomatedNode.tsx +34 -17
- package/src/modules/workflows/components/nodes/EndNode.tsx +15 -14
- package/src/modules/workflows/components/nodes/ErrorOutputHandle.tsx +32 -0
- package/src/modules/workflows/components/nodes/IfElseNode.tsx +64 -0
- package/src/modules/workflows/components/nodes/InvokeAgentNode.tsx +180 -0
- package/src/modules/workflows/components/nodes/NodeOutcomeRows.tsx +143 -0
- package/src/modules/workflows/components/nodes/ParallelForkNode.tsx +28 -15
- package/src/modules/workflows/components/nodes/ParallelJoinNode.tsx +28 -15
- package/src/modules/workflows/components/nodes/StartNode.tsx +78 -18
- package/src/modules/workflows/components/nodes/StickyNoteNode.tsx +65 -0
- package/src/modules/workflows/components/nodes/SubWorkflowNode.tsx +124 -23
- package/src/modules/workflows/components/nodes/SwitchNode.tsx +64 -0
- package/src/modules/workflows/components/nodes/TriggerCap.tsx +103 -0
- package/src/modules/workflows/components/nodes/TriggerNode.tsx +151 -0
- package/src/modules/workflows/components/nodes/UserTaskNode.tsx +67 -22
- package/src/modules/workflows/components/nodes/WaitForConditionNode.tsx +74 -0
- package/src/modules/workflows/components/nodes/WaitForSignalNode.tsx +31 -17
- package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +31 -16
- package/src/modules/workflows/components/nodes/index.ts +17 -0
- package/src/modules/workflows/components/portal/PortalTaskForm.tsx +280 -0
- package/src/modules/workflows/components/run/AgentActivityPanel.tsx +48 -0
- package/src/modules/workflows/components/run/RerunStepDialog.tsx +133 -0
- package/src/modules/workflows/components/run/RunEventBadge.tsx +37 -0
- package/src/modules/workflows/components/run/RunGantt.tsx +180 -0
- package/src/modules/workflows/components/run/RunStatusBadge.tsx +112 -0
- package/src/modules/workflows/components/run/RunStepInspector.tsx +176 -0
- package/src/modules/workflows/components/run/WouldDoReport.tsx +93 -0
- package/src/modules/workflows/components/run/useAgentLabels.ts +42 -0
- package/src/modules/workflows/components/run/useLastRunOverlay.ts +150 -0
- package/src/modules/workflows/components/run/useLiveAgentActions.ts +70 -0
- package/src/modules/workflows/components/run/useLiveRunUpdates.ts +89 -0
- package/src/modules/workflows/components/settings/CommandSettingsPageClient.tsx +228 -0
- package/src/modules/workflows/components/work-inbox/EntityContextPanel.tsx +179 -0
- package/src/modules/workflows/components/work-inbox/ReassignTaskDialog.tsx +203 -0
- package/src/modules/workflows/components/work-inbox/StatusChip.tsx +61 -0
- package/src/modules/workflows/components/work-inbox/TaskActionNotice.tsx +77 -0
- package/src/modules/workflows/components/work-inbox/TaskFormFields.tsx +322 -0
- package/src/modules/workflows/components/work-inbox/WorkInboxEmptyState.tsx +43 -0
- package/src/modules/workflows/data/activity-config-schemas.ts +249 -0
- package/src/modules/workflows/data/activity-config-warnings.ts +71 -0
- package/src/modules/workflows/data/branching-route-warnings.ts +129 -0
- package/src/modules/workflows/data/entities.ts +397 -4
- package/src/modules/workflows/data/metrics-validators.ts +51 -0
- package/src/modules/workflows/data/task-primitives.ts +36 -0
- package/src/modules/workflows/data/types.ts +81 -2
- package/src/modules/workflows/data/validators.ts +729 -60
- package/src/modules/workflows/di.ts +22 -0
- package/src/modules/workflows/events.ts +68 -8
- package/src/modules/workflows/examples/oze-residential-install-60-nodes.json +1545 -0
- package/src/modules/workflows/examples/sales-pipeline-definition.json +35 -35
- package/src/modules/workflows/examples/templates/lead-to-install.json +160 -0
- package/src/modules/workflows/examples/templates/order-approval.json +230 -0
- package/src/modules/workflows/examples/templates/task-escalation.json +110 -0
- package/src/modules/workflows/examples/templates/webhook-integration.json +113 -0
- package/src/modules/workflows/extension-points.ts +1 -1
- package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.meta.ts +21 -0
- package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/[id]/page.tsx +212 -0
- package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/page.meta.ts +25 -0
- package/src/modules/workflows/frontend/[orgSlug]/portal/tasks/page.tsx +185 -0
- package/src/modules/workflows/i18n/de.json +1083 -24
- package/src/modules/workflows/i18n/en.json +1081 -22
- package/src/modules/workflows/i18n/es.json +1083 -24
- package/src/modules/workflows/i18n/ko.json +1072 -13
- package/src/modules/workflows/i18n/pl.json +1082 -23
- package/src/modules/workflows/lib/activity-executor.ts +855 -186
- package/src/modules/workflows/lib/activity-queue-types.ts +120 -1
- package/src/modules/workflows/lib/activity-registry-bootstrap.ts +7 -0
- package/src/modules/workflows/lib/activity-registry.ts +89 -0
- package/src/modules/workflows/lib/activity-types.ts +415 -0
- package/src/modules/workflows/lib/activity-worker-handler.ts +744 -35
- package/src/modules/workflows/lib/advanced-config.ts +41 -0
- package/src/modules/workflows/lib/agent-disposition-task.ts +343 -0
- package/src/modules/workflows/lib/agent-outcome-paths.ts +153 -0
- package/src/modules/workflows/lib/agent-result-mapping.ts +88 -0
- package/src/modules/workflows/lib/agent-review-inspector.ts +125 -0
- package/src/modules/workflows/lib/agent-review.ts +171 -0
- package/src/modules/workflows/lib/ai-authoring.ts +402 -0
- package/src/modules/workflows/lib/ai-draft-runner.ts +105 -0
- package/src/modules/workflows/lib/annotation-events.ts +15 -0
- package/src/modules/workflows/lib/branching-routes.ts +280 -0
- package/src/modules/workflows/lib/breach-routing.ts +142 -0
- package/src/modules/workflows/lib/bulk-replay.ts +185 -0
- package/src/modules/workflows/lib/business-rule-usage.ts +154 -0
- package/src/modules/workflows/lib/caller-graph.ts +104 -0
- package/src/modules/workflows/lib/code-view-apply.ts +178 -0
- package/src/modules/workflows/lib/collect-validation-issues.ts +239 -0
- package/src/modules/workflows/lib/compensation-ghosts.ts +134 -0
- package/src/modules/workflows/lib/component-guard.ts +13 -0
- package/src/modules/workflows/lib/condition-handler.ts +658 -0
- package/src/modules/workflows/lib/context-ledger.ts +814 -0
- package/src/modules/workflows/lib/crud-form-dialogs-flag.ts +5 -0
- package/src/modules/workflows/lib/data-edge-mapping.ts +112 -0
- package/src/modules/workflows/lib/definition-context-schema.ts +103 -0
- package/src/modules/workflows/lib/definition-edit-safety.ts +228 -0
- package/src/modules/workflows/lib/definition-error-body.ts +43 -0
- package/src/modules/workflows/lib/definition-evaluation.ts +156 -0
- package/src/modules/workflows/lib/definition-grant.ts +211 -0
- package/src/modules/workflows/lib/definition-json-locations.ts +226 -0
- package/src/modules/workflows/lib/definition-payload.ts +81 -0
- package/src/modules/workflows/lib/draft-restore.ts +72 -0
- package/src/modules/workflows/lib/dry-run.ts +203 -0
- package/src/modules/workflows/lib/duration.ts +138 -24
- package/src/modules/workflows/lib/edge-reattachment.ts +186 -0
- package/src/modules/workflows/lib/edgeFormTransforms.ts +15 -14
- package/src/modules/workflows/lib/editor-annotations.ts +301 -0
- package/src/modules/workflows/lib/editor-commands.ts +205 -0
- package/src/modules/workflows/lib/editor-history.ts +151 -0
- package/src/modules/workflows/lib/endpoint-catalog.ts +283 -0
- package/src/modules/workflows/lib/endpoint-path.ts +134 -0
- package/src/modules/workflows/lib/engine-version.ts +67 -0
- package/src/modules/workflows/lib/error-handler.ts +192 -0
- package/src/modules/workflows/lib/error-routing.ts +236 -0
- package/src/modules/workflows/lib/event-logger.ts +27 -0
- package/src/modules/workflows/lib/expression-refs.ts +291 -0
- package/src/modules/workflows/lib/failure-grouping.ts +134 -0
- package/src/modules/workflows/lib/find-definition.ts +4 -0
- package/src/modules/workflows/lib/flow-logic-warnings.ts +591 -0
- package/src/modules/workflows/lib/graph-utils.ts +620 -110
- package/src/modules/workflows/lib/icon-search.ts +65 -0
- package/src/modules/workflows/lib/insert-at-cursor.ts +53 -0
- package/src/modules/workflows/lib/instance-date-filter.ts +56 -0
- package/src/modules/workflows/lib/interpolation-pipeline.ts +351 -0
- package/src/modules/workflows/lib/ledger-drag.ts +61 -0
- package/src/modules/workflows/lib/ledger-entry-display.ts +89 -0
- package/src/modules/workflows/lib/ledger-schema-flatten.ts +137 -0
- package/src/modules/workflows/lib/metrics/definition-metrics.ts +268 -0
- package/src/modules/workflows/lib/metrics/metrics-display.ts +57 -0
- package/src/modules/workflows/lib/metrics/rollup-schedule.ts +26 -0
- package/src/modules/workflows/lib/metrics/rollup-service.ts +292 -0
- package/src/modules/workflows/lib/node-config-summary.ts +235 -0
- package/src/modules/workflows/lib/node-geometry.ts +168 -0
- package/src/modules/workflows/lib/node-nudge.ts +63 -0
- package/src/modules/workflows/lib/node-outcome-rows.ts +195 -0
- package/src/modules/workflows/lib/node-placement.ts +104 -0
- package/src/modules/workflows/lib/node-type-icons.ts +71 -10
- package/src/modules/workflows/lib/nodeFormTransforms.ts +519 -28
- package/src/modules/workflows/lib/outcome-routing.ts +346 -0
- package/src/modules/workflows/lib/owned-definition.ts +188 -0
- package/src/modules/workflows/lib/owned-node-changes.ts +29 -0
- package/src/modules/workflows/lib/palette-drag.ts +68 -0
- package/src/modules/workflows/lib/palette-drop.ts +114 -0
- package/src/modules/workflows/lib/parallel-handler.ts +79 -2
- package/src/modules/workflows/lib/port-contract.ts +107 -0
- package/src/modules/workflows/lib/portal-task-access.ts +220 -0
- package/src/modules/workflows/lib/rerun-from-step.ts +140 -0
- package/src/modules/workflows/lib/route-chip-events.ts +45 -0
- package/src/modules/workflows/lib/route-chips.ts +177 -0
- package/src/modules/workflows/lib/route-edge.ts +57 -0
- package/src/modules/workflows/lib/route-kinds.ts +166 -0
- package/src/modules/workflows/lib/route-priority.ts +197 -0
- package/src/modules/workflows/lib/run-duration.ts +70 -0
- package/src/modules/workflows/lib/run-event-tone.ts +46 -0
- package/src/modules/workflows/lib/run-execution.ts +598 -0
- package/src/modules/workflows/lib/run-gantt.ts +311 -0
- package/src/modules/workflows/lib/run-outcome-evidence.ts +60 -0
- package/src/modules/workflows/lib/run-outcome.ts +142 -0
- package/src/modules/workflows/lib/sample-resolver.ts +113 -0
- package/src/modules/workflows/lib/seeds.ts +3 -1
- package/src/modules/workflows/lib/server-output-contract.ts +120 -0
- package/src/modules/workflows/lib/set-variable.ts +94 -0
- package/src/modules/workflows/lib/signal-handler.ts +134 -4
- package/src/modules/workflows/lib/start-fixtures.ts +93 -0
- package/src/modules/workflows/lib/status-colors.ts +220 -21
- package/src/modules/workflows/lib/step-handler.ts +744 -68
- package/src/modules/workflows/lib/step-presentation.ts +295 -0
- package/src/modules/workflows/lib/step-through.ts +71 -0
- package/src/modules/workflows/lib/step-type-conversion.ts +263 -0
- package/src/modules/workflows/lib/subgraph-clipboard.ts +214 -0
- package/src/modules/workflows/lib/task-decision-context.ts +76 -0
- package/src/modules/workflows/lib/task-decisions.ts +117 -0
- package/src/modules/workflows/lib/task-entity-access.ts +181 -0
- package/src/modules/workflows/lib/task-entity-aliases.ts +77 -0
- package/src/modules/workflows/lib/task-entity-types.ts +66 -0
- package/src/modules/workflows/lib/task-form-registry.ts +123 -0
- package/src/modules/workflows/lib/task-form-schema.ts +129 -0
- package/src/modules/workflows/lib/task-handler.ts +492 -51
- package/src/modules/workflows/lib/task-inspector-config.ts +348 -0
- package/src/modules/workflows/lib/task-list-query.ts +46 -0
- package/src/modules/workflows/lib/task-notifications.ts +218 -0
- package/src/modules/workflows/lib/task-permission-settings.ts +117 -0
- package/src/modules/workflows/lib/task-presets.ts +95 -0
- package/src/modules/workflows/lib/task-quick-action.ts +111 -0
- package/src/modules/workflows/lib/task-resolution.ts +270 -0
- package/src/modules/workflows/lib/task-sla.ts +628 -0
- package/src/modules/workflows/lib/task-visibility-request.ts +668 -0
- package/src/modules/workflows/lib/task-visibility.ts +627 -0
- package/src/modules/workflows/lib/timer-handler.ts +8 -1
- package/src/modules/workflows/lib/transition-handler.ts +228 -20
- package/src/modules/workflows/lib/trigger-node.ts +272 -0
- package/src/modules/workflows/lib/visual-editor-navigation.ts +29 -0
- package/src/modules/workflows/lib/work-inbox/empty-state.ts +150 -0
- package/src/modules/workflows/lib/work-inbox/entity-links.ts +99 -0
- package/src/modules/workflows/lib/work-inbox/navigation.ts +28 -0
- package/src/modules/workflows/lib/work-inbox/presentation.ts +163 -0
- package/src/modules/workflows/lib/work-inbox/provider.ts +390 -0
- package/src/modules/workflows/lib/work-inbox/query.ts +103 -0
- package/src/modules/workflows/lib/work-inbox/service.ts +122 -0
- package/src/modules/workflows/lib/work-inbox/user-task-source.ts +240 -0
- package/src/modules/workflows/lib/workflow-command-enablement.ts +188 -0
- package/src/modules/workflows/lib/workflow-command-settings.ts +124 -0
- package/src/modules/workflows/lib/workflow-executor.ts +1036 -10
- package/src/modules/workflows/lib/workflow-function-registry.ts +44 -0
- package/src/modules/workflows/lib/workflow-safe-commands.ts +42 -1
- package/src/modules/workflows/lib/workflow-templates.ts +91 -0
- package/src/modules/workflows/migrations/.snapshot-open-mercato.json +2321 -698
- package/src/modules/workflows/migrations/Migration20260626214855_workflows.ts +15 -0
- package/src/modules/workflows/migrations/Migration20260626222424_workflows.ts +17 -0
- package/src/modules/workflows/migrations/Migration20260727074335_workflows.ts +15 -0
- package/src/modules/workflows/migrations/Migration20260728104038_workflows.ts +13 -0
- package/src/modules/workflows/migrations/Migration20260728163001_workflows.ts +36 -0
- package/src/modules/workflows/migrations/Migration20260729061736_workflows.ts +13 -0
- package/src/modules/workflows/migrations/Migration20260729192652_workflows.ts +11 -0
- package/src/modules/workflows/migrations/Migration20260730085000_workflows.ts +13 -0
- package/src/modules/workflows/migrations/Migration20260730092706_workflows.ts +13 -0
- package/src/modules/workflows/notifications.ts +40 -10
- package/src/modules/workflows/setup.ts +128 -0
- package/src/modules/workflows/subscribers/task-assigned-notification.ts +36 -45
- package/src/modules/workflows/subscribers/task-deadline-breached-notification.ts +31 -0
- package/src/modules/workflows/subscribers/task-reminder-notification.ts +31 -0
- package/src/modules/workflows/widgets/injection/pending-work/widget.client.tsx +119 -0
- package/src/modules/workflows/widgets/injection/pending-work/widget.ts +27 -0
- package/src/modules/workflows/widgets/injection-table.ts +52 -2
- package/src/modules/workflows/workers/workflow-activities.worker.ts +95 -42
- package/src/modules/workflows/workers/workflow-definition-metric-rollup.worker.ts +71 -0
- package/src/modules/workflows/workers/workflow-instance-bulk-replay.ts +51 -0
- package/src/modules/workflows/workers/workflow-invoke-agent.worker.ts +72 -0
|
@@ -1,18 +1,110 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { WorkflowGraph } from '../../../components/WorkflowGraph'
|
|
4
|
+
import { useLastRunOverlay } from '../../../components/run/useLastRunOverlay'
|
|
4
5
|
// Conditional imports based on feature flag
|
|
5
6
|
import { NodeEditDialog } from '../../../components/NodeEditDialog'
|
|
6
7
|
import { EdgeEditDialog } from '../../../components/EdgeEditDialog'
|
|
7
8
|
import { NodeEditDialogCrudForm } from '../../../components/NodeEditDialogCrudForm'
|
|
8
9
|
import { EdgeEditDialogCrudForm } from '../../../components/EdgeEditDialogCrudForm'
|
|
9
10
|
import type { Node, Edge, Connection } from '@xyflow/react'
|
|
10
|
-
import { useState, useCallback, useEffect } from 'react'
|
|
11
|
+
import { useState, useCallback, useEffect, useMemo, useRef } from 'react'
|
|
11
12
|
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
|
|
12
|
-
import { graphToDefinition, definitionToGraph, validateWorkflowGraph, generateStepId, generateTransitionId, appendWorkflowEdge, ValidationError } from '../../../lib/graph-utils'
|
|
13
|
+
import { graphToDefinition, definitionToGraph, applyAutoLayout, validateWorkflowGraph, generateStepId, generateTransitionId, appendWorkflowEdge, getBadgeForNodeType, ValidationError } from '../../../lib/graph-utils'
|
|
14
|
+
import { convertStepType, type ConvertibleStepType } from '../../../lib/step-type-conversion'
|
|
15
|
+
import { collectValidationIssues, countIssuesBySeverity, type WorkflowIssueTranslator, type WorkflowValidationIssue, type ZodIssueLike } from '../../../lib/collect-validation-issues'
|
|
16
|
+
import { formatWorkflowValidationError } from '../../../lib/format-validation-error'
|
|
17
|
+
import type { WorkflowGraphDropEvent, WorkflowGraphFocusTarget, WorkflowGraphNodesChangeMeta } from '../../../components/WorkflowGraph'
|
|
18
|
+
import { resolveNewNodePlacement } from '../../../lib/node-placement'
|
|
19
|
+
import {
|
|
20
|
+
commitEditorHistory,
|
|
21
|
+
createEditorHistory,
|
|
22
|
+
redoEditorHistory,
|
|
23
|
+
undoEditorHistory,
|
|
24
|
+
type EditorHistoryState,
|
|
25
|
+
type WorkflowEditorDocument,
|
|
26
|
+
type WorkflowEditorPanelState,
|
|
27
|
+
} from '../../../lib/editor-history'
|
|
28
|
+
import {
|
|
29
|
+
parseWorkflowSubgraph,
|
|
30
|
+
pasteWorkflowSubgraph,
|
|
31
|
+
serializeWorkflowSubgraph,
|
|
32
|
+
stringifyWorkflowSubgraph,
|
|
33
|
+
type SubgraphClipboardParseFailure,
|
|
34
|
+
type WorkflowSubgraphClipboard,
|
|
35
|
+
} from '../../../lib/subgraph-clipboard'
|
|
36
|
+
import {
|
|
37
|
+
annotationsFromNodes,
|
|
38
|
+
annotationsToNodes,
|
|
39
|
+
createGroupNode,
|
|
40
|
+
createNoteNode,
|
|
41
|
+
isAnnotationNode,
|
|
42
|
+
readEditorAnnotations,
|
|
43
|
+
removeAnnotationNode,
|
|
44
|
+
updateAnnotationNode,
|
|
45
|
+
type WorkflowGroupNodeData,
|
|
46
|
+
type WorkflowNoteNodeData,
|
|
47
|
+
} from '../../../lib/editor-annotations'
|
|
48
|
+
import { WORKFLOW_GROUP_TOGGLE_EVENT } from '../../../lib/annotation-events'
|
|
49
|
+
import { AnnotationEditDialog } from '../../../components/AnnotationEditDialog'
|
|
50
|
+
import { WorkflowCodeView } from '../../../components/WorkflowCodeView'
|
|
51
|
+
import { DefinitionMetadataDrawer, type DefinitionMetadataSection } from '../../../components/DefinitionMetadataDrawer'
|
|
52
|
+
import { TriggersDialog } from '../../../components/TriggersDialog'
|
|
53
|
+
import { describeCodeViewDraft, evaluateCodeViewDraft } from '../../../lib/code-view-apply'
|
|
54
|
+
import {
|
|
55
|
+
WorkflowAiDraftDialog,
|
|
56
|
+
type WorkflowAiDraftApplyRefusal,
|
|
57
|
+
} from '../../../components/WorkflowAiDraftDialog'
|
|
58
|
+
import { buildAiCheckpointLabel } from '../../../lib/ai-authoring'
|
|
59
|
+
import {
|
|
60
|
+
locateDefinitionJsonEntities,
|
|
61
|
+
locateIssues,
|
|
62
|
+
severityByLine,
|
|
63
|
+
} from '../../../lib/definition-json-locations'
|
|
64
|
+
import { WorkflowCommandPalette } from '../../../components/WorkflowCommandPalette'
|
|
65
|
+
import { buildWorkflowEditorCommands, type WorkflowEditorCommand } from '../../../lib/editor-commands'
|
|
66
|
+
import { NUDGE_COMMIT_DELAY_MS, nudgeOffset, nudgeSelectedNodes, resolveNudgeDirection, selectedNodeIds } from '../../../lib/node-nudge'
|
|
67
|
+
import { canRedoEditorHistory, canUndoEditorHistory } from '../../../lib/editor-history'
|
|
68
|
+
import { readPaletteDragItem, writePaletteDragPayload } from '../../../lib/palette-drag'
|
|
69
|
+
import { appendActivityToRoute, insertStepOnRoute, type PaletteDropRejectionCode } from '../../../lib/palette-drop'
|
|
70
|
+
import { useActivityTypeOptions } from '../../../components/fields/useActivityTypeOptions'
|
|
71
|
+
import { resolveActivityIcon } from '../../../components/WorkflowRouteChips'
|
|
13
72
|
import { performDeleteEdgeFlow, performDeleteNodeFlow } from '../../../lib/visual-editor-delete-flow'
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
73
|
+
import { resolveCrudFormDialogsEnabled } from '../../../lib/crud-form-dialogs-flag'
|
|
74
|
+
import { decideDraftRestore, isServerDraftEligible, stableSerializeDefinition } from '../../../lib/draft-restore'
|
|
75
|
+
import { buildDefinitionPayload, buildMetadataPayload } from '../../../lib/definition-payload'
|
|
76
|
+
import { resolveDefinitionInterpolationMode, type WorkflowInterpolationMode } from '../../../lib/interpolation-pipeline'
|
|
77
|
+
import { findRouteKindDescriptorForHandle } from '../../../lib/route-kinds'
|
|
78
|
+
import { buildWorkflowRouteEdge } from '../../../lib/route-edge'
|
|
79
|
+
import { isDecisionSourceHandle, type DecisionRowLike } from '../../../lib/node-outcome-rows'
|
|
80
|
+
import { STRUCTURAL_EDIT_CONFLICT_CODE } from '../../../lib/definition-edit-safety'
|
|
81
|
+
import type { WorkflowErrorHandlerConfig } from '../../../data/validators'
|
|
82
|
+
import { WORKFLOW_NODE_DELETE_EVENT } from '../../../components/WorkflowNodeCard'
|
|
83
|
+
import { WORKFLOW_ROUTE_CHIP_EVENT, WORKFLOW_ROUTE_ACTIVITY_EVENT, type RouteChipEventDetail, type RouteActivityEventDetail } from '../../../lib/route-chip-events'
|
|
84
|
+
import { ActivityEditDialog } from '../../../components/ActivityEditDialog'
|
|
85
|
+
import type { Activity } from '../../../components/fields/ActivityArrayEditor'
|
|
86
|
+
import { applyRouteOrder, canNormalizeRoutePriorities, normalizeRoutePriorities, readRouteOrder, type RouteOrderEntry } from '../../../lib/route-priority'
|
|
87
|
+
import { classifyConnection, applyInputMappingToNodes, buildDataMappingEdge } from '../../../lib/data-edge-mapping'
|
|
88
|
+
import { reattachWorkflowEdge, type EdgeReattachRejection } from '../../../lib/edge-reattachment'
|
|
89
|
+
import { workflowDefinitionDataSchema, type WorkflowIoContract } from '../../../data/validators'
|
|
90
|
+
import { collectActivityConfigWarnings } from '../../../data/activity-config-warnings'
|
|
91
|
+
import {
|
|
92
|
+
applyIfElseRoutes,
|
|
93
|
+
applySwitchRoutes,
|
|
94
|
+
isBranchingNodeType,
|
|
95
|
+
readBranchingRoutes,
|
|
96
|
+
readSwitchField,
|
|
97
|
+
type SwitchRoutesValue,
|
|
98
|
+
} from '../../../lib/branching-routes'
|
|
99
|
+
import {
|
|
100
|
+
buildTriggerPayloadContracts,
|
|
101
|
+
computeContextLedger,
|
|
102
|
+
type LedgerContract,
|
|
103
|
+
type LedgerWorkflowDefinition,
|
|
104
|
+
} from '../../../lib/context-ledger'
|
|
105
|
+
import { useAvailableEvents } from '@open-mercato/ui/backend/inputs/EventSelect'
|
|
106
|
+
import { toUnresolvedContextRefIssues } from '../../../lib/definition-evaluation'
|
|
107
|
+
import type { PinnedSampleEnvelope } from '../../../lib/sample-resolver'
|
|
16
108
|
import { Page } from '@open-mercato/ui/backend/Page'
|
|
17
109
|
import { Button } from '@open-mercato/ui/primitives/button'
|
|
18
110
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
@@ -20,36 +112,91 @@ import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
|
20
112
|
import { Label } from '@open-mercato/ui/primitives/label'
|
|
21
113
|
import { Switch } from '@open-mercato/ui/primitives/switch'
|
|
22
114
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
115
|
+
Drawer,
|
|
116
|
+
DrawerBody,
|
|
117
|
+
DrawerContent,
|
|
118
|
+
DrawerDescription,
|
|
119
|
+
DrawerFooter,
|
|
120
|
+
DrawerHeader,
|
|
121
|
+
DrawerTitle,
|
|
122
|
+
} from '@open-mercato/ui/primitives/drawer'
|
|
31
123
|
import { LoadingMessage } from '@open-mercato/ui/backend/detail'
|
|
32
|
-
import { Alert, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
33
|
-
import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
124
|
+
import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
|
|
125
|
+
import { ConfirmDialog, useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
|
|
34
126
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
35
|
-
import {
|
|
127
|
+
import { formatRelativeTime } from '@open-mercato/shared/lib/time'
|
|
128
|
+
import { FormHeader, ActionsDropdown, type ActionMenuEntry } from '@open-mercato/ui/backend/forms'
|
|
36
129
|
import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
|
|
130
|
+
import { apiFetch } from '@open-mercato/ui/backend/utils/api'
|
|
37
131
|
import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
|
|
38
132
|
import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
|
|
39
133
|
import { buildRecordInjectionContext, useSetCurrentRecordInjectionContext } from '@open-mercato/ui/backend/injection/recordContext'
|
|
134
|
+
import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
|
|
135
|
+
import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
|
|
136
|
+
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
137
|
+
import {
|
|
138
|
+
listStartFixtures,
|
|
139
|
+
removeStartFixture,
|
|
140
|
+
upsertStartFixture,
|
|
141
|
+
type WorkflowStartFixtures,
|
|
142
|
+
} from '../../../lib/start-fixtures'
|
|
40
143
|
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
41
|
-
import { CircleQuestionMark,
|
|
144
|
+
import { ChevronDown, ChevronRight, CircleAlert, CircleQuestionMark, Code, Command, Group, History, Maximize2, Minimize2, Network, PanelLeftClose, PanelLeftOpen, PanelRightOpen, Play, Save, ShieldMinus, Sparkles, StickyNote, Trash2, TriangleAlert, X } from 'lucide-react'
|
|
145
|
+
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
146
|
+
import { usePersistedBooleanFlag } from '@open-mercato/ui/backend/crud/usePersistedBooleanFlag'
|
|
147
|
+
import { useSidebarCollapse } from '@open-mercato/ui/backend/AppShell'
|
|
42
148
|
import { NODE_TYPE_ICONS, NODE_TYPE_COLORS, NODE_TYPE_LABELS } from '../../../lib/node-type-icons'
|
|
43
|
-
import {
|
|
149
|
+
import { TemplateGalleryDialog, type WorkflowTemplateGalleryItem } from '../../../components/TemplateGalleryDialog'
|
|
44
150
|
import { MobileVisualEditor } from '../../../components/mobile/MobileVisualEditor'
|
|
45
151
|
import { useIsMobile } from '@open-mercato/ui/hooks/useIsMobile'
|
|
46
|
-
import type { WorkflowDefinitionTrigger } from '../../../data/entities'
|
|
152
|
+
import type { WorkflowContextSchema, WorkflowDefinitionData, WorkflowDefinitionTrigger } from '../../../data/entities'
|
|
47
153
|
import type { WorkflowMetadataState, WorkflowMetadataHandlers } from '../../../data/types'
|
|
48
154
|
import * as React from 'react'
|
|
49
155
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
50
156
|
|
|
51
157
|
const logger = createLogger('workflows')
|
|
52
158
|
|
|
159
|
+
type WorkflowDefinitionDraftPayload = {
|
|
160
|
+
definition: Record<string, unknown>
|
|
161
|
+
metadata?: Record<string, unknown> | null
|
|
162
|
+
baseUpdatedAt: string | null
|
|
163
|
+
updatedAt: string | null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const DRAFT_AUTOSAVE_DEBOUNCE_MS = 2000
|
|
167
|
+
const DRAFT_SAVED_LABEL_REFRESH_MS = 30000
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Ledger-checked context-reference warnings for the Problems panel (spec
|
|
171
|
+
* section 3.5). The editor validates UNSAVED state, so the ledger is computed
|
|
172
|
+
* CLIENT-side with `resolveOutputContract` pinned to 'unknown' instead of
|
|
173
|
+
* fetching the server ledger from the context-schema API: the client ledger is
|
|
174
|
+
* type-poorer (activity outputs stay single `unknown` nodes rather than typed
|
|
175
|
+
* contract entries) but structurally identical, and since `unknown` entries
|
|
176
|
+
* resolve every sub-path in the checker, the degradation can only suppress
|
|
177
|
+
* warnings, never fabricate them. Warnings merge into the same ZodIssueLike
|
|
178
|
+
* channel as activity-config warnings, so `collectValidationIssues` maps them
|
|
179
|
+
* to nodes/edges and they never block saves.
|
|
180
|
+
*/
|
|
181
|
+
function computeClientContextLedger(
|
|
182
|
+
definitionData: WorkflowDefinitionData,
|
|
183
|
+
triggerPayloadContracts?: Record<string, LedgerContract>,
|
|
184
|
+
) {
|
|
185
|
+
return computeContextLedger(definitionData as unknown as LedgerWorkflowDefinition, {
|
|
186
|
+
resolveOutputContract: () => 'unknown',
|
|
187
|
+
triggerPayloadContracts,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function collectContextRefWarnings(
|
|
192
|
+
definitionData: WorkflowDefinitionData,
|
|
193
|
+
translate: ReturnType<typeof useT>,
|
|
194
|
+
triggerPayloadContracts?: Record<string, LedgerContract>,
|
|
195
|
+
): ZodIssueLike[] {
|
|
196
|
+
const ledger = computeClientContextLedger(definitionData, triggerPayloadContracts)
|
|
197
|
+
return toUnresolvedContextRefIssues(definitionData, ledger, translate)
|
|
198
|
+
}
|
|
199
|
+
|
|
53
200
|
/**
|
|
54
201
|
* VisualEditorPage - Visual workflow definition editor
|
|
55
202
|
*
|
|
@@ -62,12 +209,78 @@ const logger = createLogger('workflows')
|
|
|
62
209
|
* - Flash Messages: Top-right positioned validation messages
|
|
63
210
|
* - Edit Dialogs: Modal dialogs for editing steps and transitions
|
|
64
211
|
*/
|
|
212
|
+
/**
|
|
213
|
+
* Resolve the declared IO port contracts of every sub-workflow referenced by a
|
|
214
|
+
* definition, keyed by `subWorkflowId`, so SUB_WORKFLOW nodes can render the
|
|
215
|
+
* child's IN/OUT ports. Fail-open: any lookup error leaves that child without a
|
|
216
|
+
* contract and its node simply renders without ports.
|
|
217
|
+
*/
|
|
218
|
+
async function loadSubWorkflowContracts(
|
|
219
|
+
definition: { steps?: Array<{ stepType?: string; config?: { subWorkflowId?: string } }> } | null | undefined,
|
|
220
|
+
): Promise<Map<string, WorkflowIoContract>> {
|
|
221
|
+
const contracts = new Map<string, WorkflowIoContract>()
|
|
222
|
+
const subWorkflowIds = Array.from(
|
|
223
|
+
new Set(
|
|
224
|
+
(definition?.steps || [])
|
|
225
|
+
.filter((step) => step?.stepType === 'SUB_WORKFLOW' && step?.config?.subWorkflowId)
|
|
226
|
+
.map((step) => step.config!.subWorkflowId as string),
|
|
227
|
+
),
|
|
228
|
+
)
|
|
229
|
+
await Promise.all(
|
|
230
|
+
subWorkflowIds.map(async (workflowId) => {
|
|
231
|
+
try {
|
|
232
|
+
const res = await apiCall<{ data?: Array<{ definition?: { io?: WorkflowIoContract } }> }>(
|
|
233
|
+
`/api/workflows/definitions?workflowId=${encodeURIComponent(workflowId)}&limit=1`,
|
|
234
|
+
)
|
|
235
|
+
const io = res.ok ? res.result?.data?.[0]?.definition?.io : undefined
|
|
236
|
+
if (io) contracts.set(workflowId, io)
|
|
237
|
+
} catch {
|
|
238
|
+
// fail-open
|
|
239
|
+
}
|
|
240
|
+
}),
|
|
241
|
+
)
|
|
242
|
+
return contracts
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const PALETTE_NODE_TYPES = ['start', 'userTask', 'automated', 'invokeAgent', 'ifElse', 'switch', 'waitForSignal', 'waitForTimer', 'waitForCondition', 'subWorkflow', 'end'] as const
|
|
246
|
+
|
|
247
|
+
/** Body of a definition update — shared by explicit Save and the quiet autosave. */
|
|
248
|
+
type DefinitionUpdateBody = {
|
|
249
|
+
workflowName: string
|
|
250
|
+
description: string | null
|
|
251
|
+
version: number
|
|
252
|
+
definition: ReturnType<typeof buildDefinitionPayload>
|
|
253
|
+
metadata: ReturnType<typeof buildMetadataPayload>
|
|
254
|
+
enabled: boolean
|
|
255
|
+
effectiveFrom: string | null
|
|
256
|
+
effectiveTo: string | null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Edit-safety rule (spec 4.1): the definition PUT refuses a topology change
|
|
261
|
+
* while instances are still executing this version. The refusal is a dead end
|
|
262
|
+
* without its remedy, so the rejected payload rides along and is re-applied to
|
|
263
|
+
* the freshly minted version.
|
|
264
|
+
*/
|
|
265
|
+
type StructuralEditConflict = {
|
|
266
|
+
activeInstanceCount: number
|
|
267
|
+
payload: DefinitionUpdateBody
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function readStructuralEditConflictCount(status: number, body: unknown): number | null {
|
|
271
|
+
if (status !== 409) return null
|
|
272
|
+
const record = body && typeof body === 'object' ? (body as Record<string, unknown>) : null
|
|
273
|
+
if (!record || record.code !== STRUCTURAL_EDIT_CONFLICT_CODE) return null
|
|
274
|
+
return typeof record.activeInstanceCount === 'number' ? record.activeInstanceCount : 0
|
|
275
|
+
}
|
|
276
|
+
|
|
65
277
|
export default function VisualEditorPage() {
|
|
66
278
|
const t = useT()
|
|
67
279
|
const router = useRouter()
|
|
68
280
|
const searchParams = useSearchParams()
|
|
69
281
|
const pathname = usePathname()
|
|
70
282
|
const definitionId = searchParams.get('id')
|
|
283
|
+
const templateId = searchParams.get('template')
|
|
71
284
|
const isMobile = useIsMobile()
|
|
72
285
|
|
|
73
286
|
const { confirm, ConfirmDialogElement } = useConfirmDialog()
|
|
@@ -78,17 +291,96 @@ export default function VisualEditorPage() {
|
|
|
78
291
|
const [edges, setEdges] = useState<Edge[]>([])
|
|
79
292
|
const [selectedNode, setSelectedNode] = useState<Node | null>(null)
|
|
80
293
|
const [selectedEdge, setSelectedEdge] = useState<Edge | null>(null)
|
|
81
|
-
const [
|
|
294
|
+
const [edgeDialogFocusFieldId, setEdgeDialogFocusFieldId] = useState<string | null>(null)
|
|
295
|
+
// Latest edges, readable from window-event handlers without re-subscribing on
|
|
296
|
+
// every graph change (route chips resolve their edge through this).
|
|
297
|
+
const edgesRef = React.useRef<Edge[]>([])
|
|
298
|
+
edgesRef.current = edges
|
|
299
|
+
const nodesRef = React.useRef<Node[]>([])
|
|
300
|
+
nodesRef.current = nodes
|
|
301
|
+
// Definition metadata lives in a right-side Drawer, so it costs the canvas
|
|
302
|
+
// nothing and never needs hiding. It starts closed — the canvas is what the
|
|
303
|
+
// page is for — and `handleSave` opens it when the required id/name are
|
|
304
|
+
// still missing, which is the moment the author needs it.
|
|
305
|
+
const [metadataOpen, setMetadataOpen] = useState(false)
|
|
306
|
+
// Which section the drawer lands on. Only the canvas trigger pill sets it
|
|
307
|
+
// (fidelity gap #5) — every other way in wants the top of the form — so it is
|
|
308
|
+
// cleared whenever the drawer is opened by anything else.
|
|
309
|
+
const [metadataFocusSection, setMetadataFocusSection] = useState<DefinitionMetadataSection | null>(null)
|
|
310
|
+
const [triggersDialogOpen, setTriggersDialogOpen] = useState(false)
|
|
311
|
+
// The route activity chip's target: which edge + activity the focused
|
|
312
|
+
// single-activity modal is editing (fidelity gap #6 follow-up).
|
|
313
|
+
const [activityEdit, setActivityEdit] = useState<{ edgeId: string; activityId: string } | null>(null)
|
|
314
|
+
// The START node's trigger cap opens a focused Triggers modal (Direction A),
|
|
315
|
+
// NOT the five-section definition drawer — the author clicked "what starts
|
|
316
|
+
// this", so the affordance opens exactly that.
|
|
317
|
+
const handleOpenTriggers = useCallback(() => {
|
|
318
|
+
setTriggersDialogOpen(true)
|
|
319
|
+
}, [])
|
|
320
|
+
// The focus target is one-shot: every other way into the drawer wants the top
|
|
321
|
+
// of the form, so closing it clears the request.
|
|
322
|
+
const handleMetadataOpenChange = useCallback((open: boolean) => {
|
|
323
|
+
if (!open) setMetadataFocusSection(null)
|
|
324
|
+
setMetadataOpen(open)
|
|
325
|
+
}, [])
|
|
82
326
|
const [isCompactViewport, setIsCompactViewport] = useState(false)
|
|
327
|
+
const [autosaveState, setAutosaveState] = useState<'idle' | 'saving' | 'saved'>('idle')
|
|
328
|
+
const [structuralConflict, setStructuralConflict] = useState<StructuralEditConflict | null>(null)
|
|
329
|
+
const [isCreatingVersion, setIsCreatingVersion] = useState(false)
|
|
330
|
+
// Debounced autosave-on-drag plumbing. `performAutosaveRef` is reassigned each
|
|
331
|
+
// render so the debounced timer always runs the latest closure (no stale nodes).
|
|
332
|
+
const autosaveTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
333
|
+
const performAutosaveRef = React.useRef<() => Promise<void>>(async () => {})
|
|
334
|
+
// Last payload the quiet autosave actually persisted, so an unchanged graph
|
|
335
|
+
// never issues a redundant PUT (#4248). Cleared on failure so the next change
|
|
336
|
+
// retries the write.
|
|
337
|
+
const lastAutosavedBodyRef = React.useRef<string | null>(null)
|
|
338
|
+
const { value: paletteCollapsed, toggle: togglePaletteCollapsed, setValue: setPaletteCollapsed } = usePersistedBooleanFlag('om:wf-editor-palette', false)
|
|
339
|
+
const [showPaletteHowTo, setShowPaletteHowTo] = useState(false)
|
|
340
|
+
const { value: focusMode, setValue: setFocusMode, toggle: toggleFocus } = usePersistedBooleanFlag('om:wf-editor-focus', false)
|
|
341
|
+
// Compensation ghosts (spec §4.4) are OFF by default and remembered per
|
|
342
|
+
// author: they are a read-only overlay, never part of the document.
|
|
343
|
+
const { value: showCompensation, toggle: toggleCompensation } = usePersistedBooleanFlag('om:wf-editor-compensation', false)
|
|
344
|
+
// "Show last run" (spec §8.3). OFF by default and remembered per author, and
|
|
345
|
+
// it fetches nothing until it is on — an author editing a definition should
|
|
346
|
+
// not pay for three requests they never asked for.
|
|
347
|
+
const { value: showLastRun, toggle: toggleLastRun } = usePersistedBooleanFlag('om:wf-editor-last-run', false)
|
|
348
|
+
const { requestCollapse, releaseRequest } = useSidebarCollapse()
|
|
349
|
+
// Remember the palette state from before Focus mode took over so we can
|
|
350
|
+
// restore exactly what the author had when they exit.
|
|
351
|
+
const priorPaletteCollapsedRef = React.useRef<boolean | null>(null)
|
|
352
|
+
|
|
353
|
+
// Focus mode orchestrator: collapse the app sidebar + palette when entering
|
|
354
|
+
// and restore the author's prior palette state when leaving. Runs on mount
|
|
355
|
+
// too, so a persisted `focusMode === true` applies the collapses immediately.
|
|
356
|
+
// The metadata Drawer is not touched: it overlays rather than displaces the
|
|
357
|
+
// canvas, so hiding it would buy Focus mode nothing and would only make the
|
|
358
|
+
// palette's "Toggle the workflow details panel" command a silent no-op.
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
if (focusMode) {
|
|
361
|
+
if (priorPaletteCollapsedRef.current === null) priorPaletteCollapsedRef.current = paletteCollapsed
|
|
362
|
+
requestCollapse(true)
|
|
363
|
+
setPaletteCollapsed(true)
|
|
364
|
+
} else {
|
|
365
|
+
releaseRequest()
|
|
366
|
+
if (priorPaletteCollapsedRef.current !== null) {
|
|
367
|
+
setPaletteCollapsed(priorPaletteCollapsedRef.current)
|
|
368
|
+
priorPaletteCollapsedRef.current = null
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
372
|
+
}, [focusMode])
|
|
373
|
+
|
|
374
|
+
// Always release the app-sidebar request when the editor unmounts so the
|
|
375
|
+
// user's prior sidebar state is restored when they navigate away.
|
|
376
|
+
useEffect(() => () => releaseRequest(), [releaseRequest])
|
|
83
377
|
|
|
84
|
-
//
|
|
378
|
+
// Track the compact (<1280px) layout after hydration
|
|
85
379
|
useEffect(() => {
|
|
86
380
|
if (typeof window === 'undefined') return
|
|
87
381
|
const mediaQuery = window.matchMedia('(max-width: 1279px)')
|
|
88
382
|
const applyViewportMode = () => {
|
|
89
|
-
|
|
90
|
-
setIsCompactViewport(compact)
|
|
91
|
-
setShowMetadata(!compact)
|
|
383
|
+
setIsCompactViewport(mediaQuery.matches)
|
|
92
384
|
}
|
|
93
385
|
|
|
94
386
|
applyViewportMode()
|
|
@@ -100,10 +392,52 @@ export default function VisualEditorPage() {
|
|
|
100
392
|
}, [])
|
|
101
393
|
const [showNodeDialog, setShowNodeDialog] = useState(false)
|
|
102
394
|
const [showEdgeDialog, setShowEdgeDialog] = useState(false)
|
|
395
|
+
|
|
396
|
+
const crudFormDialogsEnabled = resolveCrudFormDialogsEnabled(process.env.NEXT_PUBLIC_WORKFLOW_CRUDFORM_ENABLED)
|
|
397
|
+
|
|
398
|
+
// Where the inspectors are MOUNTED. Both of them now open as the wide overlay
|
|
399
|
+
// Drawer (#4708 for the step form, its follow-up for the route form): the
|
|
400
|
+
// 384px rail could not hold either dense form. What survives of the rail
|
|
401
|
+
// decision is the mount point — on a roomy viewport the panels live inside the
|
|
402
|
+
// editor layout row, below it they ride the shared dialog stack — plus the
|
|
403
|
+
// shortcut ownership that keys off it. The legacy dialogs keep their own modal
|
|
404
|
+
// chrome; they are deprecated and are not being redesigned.
|
|
405
|
+
const inspectorsDocked = crudFormDialogsEnabled && !isMobile && !isCompactViewport
|
|
406
|
+
|
|
407
|
+
// Sticky notes and groups (spec 4.5) are canvas nodes with their own tiny
|
|
408
|
+
// inspector — they carry no step configuration, so they never open the step
|
|
409
|
+
// dialog.
|
|
410
|
+
const [selectedAnnotation, setSelectedAnnotation] = useState<Node | null>(null)
|
|
411
|
+
const [showAnnotationDialog, setShowAnnotationDialog] = useState(false)
|
|
412
|
+
// Cmd+K command palette (spec 4.6). Together with the inspector and the
|
|
413
|
+
// Problems panel it is the complete non-pointer authoring path, so every
|
|
414
|
+
// mutating action the toolbar offers is reachable through it.
|
|
415
|
+
const [showCommandPalette, setShowCommandPalette] = useState(false)
|
|
416
|
+
// Code view, stage 1 (spec §2.2): the assembled definition JSON read-only,
|
|
417
|
+
// beside the same structured issue list the Problems panel renders.
|
|
418
|
+
const [showCodeView, setShowCodeView] = useState(false)
|
|
103
419
|
const [showClearConfirm, setShowClearConfirm] = useState(false)
|
|
420
|
+
const [showTemplateGallery, setShowTemplateGallery] = useState(false)
|
|
421
|
+
const [problems, setProblems] = useState<WorkflowValidationIssue[]>([])
|
|
422
|
+
const [showProblems, setShowProblems] = useState(false)
|
|
423
|
+
const [problemsCollapsed, setProblemsCollapsed] = useState(false)
|
|
424
|
+
const [focusTarget, setFocusTarget] = useState<WorkflowGraphFocusTarget | null>(null)
|
|
425
|
+
const focusRequestRef = React.useRef(0)
|
|
426
|
+
|
|
427
|
+
// Error-severity issue counts per node id — drives the per-node error badges
|
|
428
|
+
// on the canvas; clearing the problems list clears every badge.
|
|
429
|
+
const nodeErrorCounts = React.useMemo(() => {
|
|
430
|
+
const counts: Record<string, number> = {}
|
|
431
|
+
for (const issue of problems) {
|
|
432
|
+
if (issue.severity !== 'error' || !issue.nodeId) continue
|
|
433
|
+
counts[issue.nodeId] = (counts[issue.nodeId] ?? 0) + 1
|
|
434
|
+
}
|
|
435
|
+
return counts
|
|
436
|
+
}, [problems])
|
|
104
437
|
|
|
105
438
|
// Workflow metadata state
|
|
106
439
|
const [workflowId, setWorkflowId] = useState('')
|
|
440
|
+
const lastRunOverlay = useLastRunOverlay({ workflowId, enabled: showLastRun && !!definitionId })
|
|
107
441
|
const [workflowName, setWorkflowName] = useState('')
|
|
108
442
|
const [description, setDescription] = useState('')
|
|
109
443
|
const [version, setVersion] = useState(1)
|
|
@@ -114,16 +448,143 @@ export default function VisualEditorPage() {
|
|
|
114
448
|
const [effectiveFrom, setEffectiveFrom] = useState('')
|
|
115
449
|
const [effectiveTo, setEffectiveTo] = useState('')
|
|
116
450
|
const [triggers, setTriggers] = useState<WorkflowDefinitionTrigger[]>([])
|
|
451
|
+
const [contextSchema, setContextSchema] = useState<WorkflowContextSchema | undefined>(undefined)
|
|
452
|
+
// The sub-workflow io port contract is not edited on this page, but it must
|
|
453
|
+
// survive the graph → definition rebuild on save/draft, so it is carried as
|
|
454
|
+
// pass-through state exactly like contextSchema.
|
|
455
|
+
const [definitionIo, setDefinitionIo] = useState<WorkflowIoContract | undefined>(undefined)
|
|
456
|
+
// Interpolation mode (spec §3.6): new definitions start strict (matching the
|
|
457
|
+
// POST create default); loaded definitions keep their stored value, and
|
|
458
|
+
// ABSENT stays absent through save round-trips so existing lenient
|
|
459
|
+
// definitions are never flipped by an unrelated edit.
|
|
460
|
+
const [interpolation, setInterpolation] = useState<WorkflowInterpolationMode | undefined>(undefined)
|
|
461
|
+
// Definition-level catch-all error handler (spec section 5.9). Pass-through
|
|
462
|
+
// state like contextSchema/io: absent stays absent through save round-trips.
|
|
463
|
+
const [errorHandler, setErrorHandler] = useState<WorkflowErrorHandlerConfig | undefined>(undefined)
|
|
464
|
+
const [loadedMetadata, setLoadedMetadata] = useState<Record<string, unknown> | null>(null)
|
|
117
465
|
const [source, setSource] = useState<'code' | 'code_override' | 'user' | null>(null)
|
|
118
466
|
const [updatedAt, setUpdatedAt] = useState<string | null>(null)
|
|
119
467
|
|
|
468
|
+
// Start-instance dialog state (mirrors the non-visual edit page UX)
|
|
469
|
+
const [startOpen, setStartOpen] = useState(false)
|
|
470
|
+
const [startContext, setStartContext] = useState('{}')
|
|
471
|
+
const [starting, setStarting] = useState(false)
|
|
472
|
+
// Spec section 8.2 test-run modes. Independent by design: a real run can be
|
|
473
|
+
// stepped through, and a dry run can be let loose end to end.
|
|
474
|
+
const [startDryRun, setStartDryRun] = useState(false)
|
|
475
|
+
const [startStepThrough, setStartStepThrough] = useState(false)
|
|
476
|
+
const [fixtureName, setFixtureName] = useState('')
|
|
477
|
+
|
|
478
|
+
// Undo/redo (spec §4.5). One stack over the whole editor document, so an
|
|
479
|
+
// inspector save undoes exactly like a deleted route. Snapshots are cheap
|
|
480
|
+
// because every mutating path already replaces the node/edge arrays instead
|
|
481
|
+
// of mutating them, so an entry only holds references.
|
|
482
|
+
const [history, setHistory] = useState<EditorHistoryState<WorkflowEditorDocument>>(createEditorHistory)
|
|
483
|
+
const historyRef = React.useRef(history)
|
|
484
|
+
historyRef.current = history
|
|
485
|
+
const loadedMetadataRef = React.useRef<Record<string, unknown> | null>(null)
|
|
486
|
+
loadedMetadataRef.current = loadedMetadata
|
|
487
|
+
|
|
488
|
+
const historyLabels = useMemo(() => ({
|
|
489
|
+
addStep: t('workflows.visualEditor.history.addStep', 'Add step'),
|
|
490
|
+
editStep: t('workflows.visualEditor.history.editStep', 'Edit step'),
|
|
491
|
+
editRoute: t('workflows.visualEditor.history.editRoute', 'Edit route'),
|
|
492
|
+
deleteStep: t('workflows.visualEditor.history.deleteStep', 'Delete step'),
|
|
493
|
+
deleteRoute: t('workflows.visualEditor.history.deleteRoute', 'Delete route'),
|
|
494
|
+
connect: t('workflows.visualEditor.history.connect', 'Connect steps'),
|
|
495
|
+
reattach: t('workflows.visualEditor.history.reattach', 'Re-target route'),
|
|
496
|
+
move: t('workflows.visualEditor.history.move', 'Move steps'),
|
|
497
|
+
convert: t('workflows.visualEditor.history.convert', 'Change step type'),
|
|
498
|
+
branchRoutes: t('workflows.visualEditor.history.branchRoutes', 'Edit branch routes'),
|
|
499
|
+
routeOrder: t('workflows.visualEditor.history.routeOrder', 'Reorder routes'),
|
|
500
|
+
pinSample: t('workflows.visualEditor.history.pinSample', 'Pin sample'),
|
|
501
|
+
unpinSample: t('workflows.visualEditor.history.unpinSample', 'Unpin sample'),
|
|
502
|
+
tidy: t('workflows.visualEditor.history.tidy', 'Tidy layout'),
|
|
503
|
+
paste: t('workflows.visualEditor.history.paste', 'Paste'),
|
|
504
|
+
duplicate: t('workflows.visualEditor.history.duplicate', 'Duplicate'),
|
|
505
|
+
insertOnRoute: t('workflows.visualEditor.history.insertOnRoute', 'Insert step on route'),
|
|
506
|
+
addActivity: t('workflows.visualEditor.history.addActivity', 'Add action to route'),
|
|
507
|
+
addNote: t('workflows.visualEditor.history.addNote', 'Add note'),
|
|
508
|
+
addGroup: t('workflows.visualEditor.history.addGroup', 'Add group'),
|
|
509
|
+
editAnnotation: t('workflows.visualEditor.history.editAnnotation', 'Edit annotation'),
|
|
510
|
+
deleteAnnotation: t('workflows.visualEditor.history.deleteAnnotation', 'Delete annotation'),
|
|
511
|
+
toggleGroup: t('workflows.visualEditor.history.toggleGroup', 'Collapse group'),
|
|
512
|
+
saveFixture: t('workflows.visualEditor.history.saveFixture', 'Save start fixture'),
|
|
513
|
+
deleteFixture: t('workflows.visualEditor.history.deleteFixture', 'Delete start fixture'),
|
|
514
|
+
applyCode: t('workflows.visualEditor.history.applyCode', 'Apply JSON from Code view'),
|
|
515
|
+
}), [t])
|
|
516
|
+
|
|
517
|
+
// The definition-panel fields ride ALONG with the document so the Code view's
|
|
518
|
+
// Apply — which replaces the whole definition, panel fields included — is one
|
|
519
|
+
// fully reversible action. Every other entry captures them too, which costs
|
|
520
|
+
// five references and makes an undo restore the state it claims to.
|
|
521
|
+
const panelStateRef = React.useRef<WorkflowEditorPanelState>({
|
|
522
|
+
triggers: [],
|
|
523
|
+
contextSchema: undefined,
|
|
524
|
+
io: undefined,
|
|
525
|
+
interpolation: undefined,
|
|
526
|
+
errorHandler: undefined,
|
|
527
|
+
})
|
|
528
|
+
useEffect(() => {
|
|
529
|
+
panelStateRef.current = {
|
|
530
|
+
triggers,
|
|
531
|
+
contextSchema,
|
|
532
|
+
io: definitionIo,
|
|
533
|
+
interpolation,
|
|
534
|
+
errorHandler,
|
|
535
|
+
}
|
|
536
|
+
}, [triggers, contextSchema, definitionIo, interpolation, errorHandler])
|
|
537
|
+
|
|
538
|
+
const captureDocument = useCallback((): WorkflowEditorDocument => ({
|
|
539
|
+
nodes: nodesRef.current,
|
|
540
|
+
edges: edgesRef.current,
|
|
541
|
+
metadata: loadedMetadataRef.current,
|
|
542
|
+
panel: panelStateRef.current,
|
|
543
|
+
}), [])
|
|
544
|
+
|
|
545
|
+
const commitCapturedDocument = useCallback((document: WorkflowEditorDocument, label: string) => {
|
|
546
|
+
setHistory((current) => commitEditorHistory(current, document, label))
|
|
547
|
+
}, [])
|
|
548
|
+
|
|
549
|
+
const commitHistory = useCallback((label: string) => {
|
|
550
|
+
commitCapturedDocument(captureDocument(), label)
|
|
551
|
+
}, [captureDocument, commitCapturedDocument])
|
|
552
|
+
|
|
553
|
+
// A whole-document replacement (draft restore, template load, clear) starts a
|
|
554
|
+
// fresh stack: those paths also rewrite the definition-panel fields, which the
|
|
555
|
+
// document deliberately does not version, so undoing into them would restore
|
|
556
|
+
// half a workflow.
|
|
557
|
+
const resetHistory = useCallback(() => {
|
|
558
|
+
setHistory(createEditorHistory<WorkflowEditorDocument>())
|
|
559
|
+
}, [])
|
|
560
|
+
|
|
561
|
+
const mutationContextId = `workflows.definitions.visual-editor:${definitionId ?? 'unknown'}`
|
|
562
|
+
const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
|
|
563
|
+
contextId: mutationContextId,
|
|
564
|
+
})
|
|
565
|
+
|
|
120
566
|
const isCodeOnly = source === 'code'
|
|
121
567
|
const isCodeOverride = source === 'code_override'
|
|
122
568
|
|
|
569
|
+
// Per-user draft layer (spec §4.7): drafts persist server-side only for
|
|
570
|
+
// SAVED definitions (uuid ids). Unsaved/new definitions and code-defined
|
|
571
|
+
// workflows keep their state client-side only — no persistence at all.
|
|
572
|
+
// Draft saves deliberately never participate in the definition's optimistic
|
|
573
|
+
// lock; only the explicit Save PUT sends the lock header.
|
|
574
|
+
const draftEligible = isServerDraftEligible(definitionId) && !isCodeOnly
|
|
575
|
+
const [pendingDraft, setPendingDraft] = useState<{ draft: WorkflowDefinitionDraftPayload; baseMismatch: boolean } | null>(null)
|
|
576
|
+
const [draftAutosaveReady, setDraftAutosaveReady] = useState(false)
|
|
577
|
+
const [draftSavedAt, setDraftSavedAt] = useState<string | null>(null)
|
|
578
|
+
const [draftSaveFailed, setDraftSaveFailed] = useState(false)
|
|
579
|
+
const [draftClock, setDraftClock] = useState(0)
|
|
580
|
+
const lastPersistedDraftRef = useRef<string | null>(null)
|
|
581
|
+
const draftSuspendedRef = useRef(false)
|
|
582
|
+
|
|
123
583
|
// Load existing definition if ID is provided
|
|
124
584
|
useEffect(() => {
|
|
125
585
|
const loadDefinition = async () => {
|
|
126
586
|
if (!definitionId) {
|
|
587
|
+
setInterpolation('strict')
|
|
127
588
|
setIsLoading(false)
|
|
128
589
|
return
|
|
129
590
|
}
|
|
@@ -151,19 +612,91 @@ export default function VisualEditorPage() {
|
|
|
151
612
|
setEffectiveFrom(definition.effectiveFrom || '')
|
|
152
613
|
setEffectiveTo(definition.effectiveTo || '')
|
|
153
614
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
615
|
+
// Resolve referenced sub-workflow port contracts so SUB_WORKFLOW nodes
|
|
616
|
+
// render IN/OUT ports without opening the child (fail-open).
|
|
617
|
+
const childContracts = await loadSubWorkflowContracts(definition.definition)
|
|
618
|
+
|
|
619
|
+
// Convert definition to graph. Annotations ride alongside the steps as
|
|
620
|
+
// canvas nodes so a note drags, undoes and autosaves like everything
|
|
621
|
+
// else — they are filtered back out on the way to `definition.steps`.
|
|
622
|
+
const graph = definitionToGraph(definition.definition, { childContracts })
|
|
623
|
+
const loadedMetadataBag = definition.metadata && typeof definition.metadata === 'object'
|
|
624
|
+
? (definition.metadata as Record<string, unknown>)
|
|
625
|
+
: null
|
|
626
|
+
const annotationNodes = annotationsToNodes(readEditorAnnotations(loadedMetadataBag))
|
|
627
|
+
setNodes([...graph.nodes, ...annotationNodes])
|
|
157
628
|
setEdges(graph.edges)
|
|
158
629
|
|
|
159
630
|
// Load embedded triggers from definition
|
|
160
|
-
|
|
631
|
+
const loadedTriggers = definition.definition?.triggers || []
|
|
632
|
+
setTriggers(loadedTriggers)
|
|
633
|
+
|
|
634
|
+
// Carry the declared context schema and the FULL metadata object so
|
|
635
|
+
// save/draft rebuilds cannot silently strip keys the editor does not
|
|
636
|
+
// edit (contextSchema, future metadata.editor.* keys).
|
|
637
|
+
const loadedContextSchema = (definition.definition?.contextSchema ?? undefined) as WorkflowContextSchema | undefined
|
|
638
|
+
setContextSchema(loadedContextSchema)
|
|
639
|
+
const loadedIo = (definition.definition?.io ?? undefined) as WorkflowIoContract | undefined
|
|
640
|
+
setDefinitionIo(loadedIo)
|
|
641
|
+
const loadedInterpolation = resolveDefinitionInterpolationMode(definition.definition)
|
|
642
|
+
setInterpolation(loadedInterpolation)
|
|
643
|
+
const loadedErrorHandler = (definition.definition?.errorHandler ?? undefined) as WorkflowErrorHandlerConfig | undefined
|
|
644
|
+
setErrorHandler(loadedErrorHandler)
|
|
645
|
+
const loadedMetadataObject = loadedMetadataBag ? { ...loadedMetadataBag } : null
|
|
646
|
+
setLoadedMetadata(loadedMetadataObject)
|
|
161
647
|
|
|
162
648
|
// Track source so the editor mirrors the non-visual edit page UX:
|
|
163
649
|
// code → read-only with Customize button; code_override → editable
|
|
164
650
|
// with Reset to code; user → editable, no banner.
|
|
165
|
-
|
|
166
|
-
|
|
651
|
+
const loadedSource = (definition.source as 'code' | 'code_override' | 'user') ?? null
|
|
652
|
+
setSource(loadedSource)
|
|
653
|
+
const loadedUpdatedAt = typeof definition.updatedAt === 'string' ? definition.updatedAt : null
|
|
654
|
+
setUpdatedAt(loadedUpdatedAt)
|
|
655
|
+
|
|
656
|
+
// Draft layer: compare against the ROUND-TRIPPED definition (graph →
|
|
657
|
+
// definition with the same normalization the autosave uses) so a mere
|
|
658
|
+
// load/serialize drift never looks like an unsaved draft.
|
|
659
|
+
const comparableDefinition = buildDefinitionPayload({
|
|
660
|
+
graphDefinition: graphToDefinition(graph.nodes, graph.edges, { includePositions: true }),
|
|
661
|
+
triggers: loadedTriggers,
|
|
662
|
+
contextSchema: loadedContextSchema,
|
|
663
|
+
io: loadedIo,
|
|
664
|
+
interpolation: loadedInterpolation,
|
|
665
|
+
errorHandler: loadedErrorHandler,
|
|
666
|
+
})
|
|
667
|
+
const loadedDraftMetadata = buildMetadataPayload({
|
|
668
|
+
loadedMetadata: loadedMetadataObject,
|
|
669
|
+
category: definition.metadata?.category || '',
|
|
670
|
+
tags: definition.metadata?.tags || [],
|
|
671
|
+
icon: definition.metadata?.icon || '',
|
|
672
|
+
annotations: annotationsFromNodes(annotationNodes),
|
|
673
|
+
})
|
|
674
|
+
lastPersistedDraftRef.current = stableSerializeDefinition({
|
|
675
|
+
definition: comparableDefinition,
|
|
676
|
+
metadata: loadedDraftMetadata,
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
if (loadedSource !== 'code' && isServerDraftEligible(definitionId)) {
|
|
680
|
+
const draftResult = await apiCall<{ data?: WorkflowDefinitionDraftPayload; error?: string }>(
|
|
681
|
+
`/api/workflows/definitions/${definitionId}/draft`,
|
|
682
|
+
)
|
|
683
|
+
const draft = draftResult.ok ? draftResult.result?.data : undefined
|
|
684
|
+
const decision = draft
|
|
685
|
+
? decideDraftRestore({
|
|
686
|
+
draftDefinition: draft.definition,
|
|
687
|
+
draftBaseUpdatedAt: draft.baseUpdatedAt,
|
|
688
|
+
loadedDefinition: comparableDefinition,
|
|
689
|
+
definitionUpdatedAt: loadedUpdatedAt,
|
|
690
|
+
})
|
|
691
|
+
: { offerRestore: false as const }
|
|
692
|
+
if (draft && decision.offerRestore) {
|
|
693
|
+
// Hold autosave until the user restores or discards, so editing
|
|
694
|
+
// with the banner open can never silently overwrite the draft.
|
|
695
|
+
setPendingDraft({ draft, baseMismatch: decision.baseMismatch })
|
|
696
|
+
} else {
|
|
697
|
+
setDraftAutosaveReady(true)
|
|
698
|
+
}
|
|
699
|
+
}
|
|
167
700
|
} catch (error) {
|
|
168
701
|
logger.error('Error loading workflow definition', { err: error })
|
|
169
702
|
flash('Failed to load workflow definition', 'error')
|
|
@@ -175,13 +708,310 @@ export default function VisualEditorPage() {
|
|
|
175
708
|
loadDefinition()
|
|
176
709
|
}, [definitionId])
|
|
177
710
|
|
|
711
|
+
// The canvas owns the annotations, so every persistence path derives them back
|
|
712
|
+
// out of the node array rather than tracking a second copy that could drift.
|
|
713
|
+
const annotations = useMemo(() => annotationsFromNodes(nodes), [nodes])
|
|
714
|
+
|
|
715
|
+
const draftMetadata = useMemo(
|
|
716
|
+
() => buildMetadataPayload({ loadedMetadata, tags, category, icon, annotations }),
|
|
717
|
+
[loadedMetadata, tags, category, icon, annotations],
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
// Debounced autosave-to-draft (~2s): watches the same editor-state dep set
|
|
721
|
+
// the save handler uses and PUTs the per-user draft. Never fires for
|
|
722
|
+
// unsaved/new or code-defined definitions, and failures stay quiet — the
|
|
723
|
+
// small indicator flips to "draft not saved" and the next change retries.
|
|
724
|
+
useEffect(() => {
|
|
725
|
+
if (!draftAutosaveReady || !draftEligible || !definitionId) return
|
|
726
|
+
if (draftSuspendedRef.current) return
|
|
727
|
+
let payload: { definition: ReturnType<typeof buildDefinitionPayload>; metadata: Record<string, unknown> | null; baseUpdatedAt: string | null }
|
|
728
|
+
try {
|
|
729
|
+
payload = {
|
|
730
|
+
definition: buildDefinitionPayload({
|
|
731
|
+
graphDefinition: graphToDefinition(nodes, edges, { includePositions: true }),
|
|
732
|
+
triggers,
|
|
733
|
+
contextSchema,
|
|
734
|
+
io: definitionIo,
|
|
735
|
+
interpolation,
|
|
736
|
+
errorHandler,
|
|
737
|
+
}),
|
|
738
|
+
metadata: draftMetadata,
|
|
739
|
+
baseUpdatedAt: updatedAt,
|
|
740
|
+
}
|
|
741
|
+
} catch {
|
|
742
|
+
return
|
|
743
|
+
}
|
|
744
|
+
const serialized = stableSerializeDefinition({ definition: payload.definition, metadata: payload.metadata })
|
|
745
|
+
if (serialized === lastPersistedDraftRef.current) return
|
|
746
|
+
const timer = window.setTimeout(async () => {
|
|
747
|
+
if (draftSuspendedRef.current) return
|
|
748
|
+
try {
|
|
749
|
+
const result = await apiCall<{ data?: WorkflowDefinitionDraftPayload; error?: string }>(
|
|
750
|
+
`/api/workflows/definitions/${definitionId}/draft`,
|
|
751
|
+
{
|
|
752
|
+
method: 'PUT',
|
|
753
|
+
headers: { 'Content-Type': 'application/json' },
|
|
754
|
+
body: JSON.stringify(payload),
|
|
755
|
+
},
|
|
756
|
+
)
|
|
757
|
+
if (result.ok) {
|
|
758
|
+
lastPersistedDraftRef.current = serialized
|
|
759
|
+
setDraftSavedAt(new Date().toISOString())
|
|
760
|
+
setDraftSaveFailed(false)
|
|
761
|
+
} else {
|
|
762
|
+
setDraftSaveFailed(true)
|
|
763
|
+
}
|
|
764
|
+
} catch {
|
|
765
|
+
setDraftSaveFailed(true)
|
|
766
|
+
}
|
|
767
|
+
}, DRAFT_AUTOSAVE_DEBOUNCE_MS)
|
|
768
|
+
return () => window.clearTimeout(timer)
|
|
769
|
+
}, [draftAutosaveReady, draftEligible, definitionId, nodes, edges, triggers, contextSchema, definitionIo, interpolation, errorHandler, draftMetadata, updatedAt, workflowName, description, version, enabled, effectiveFrom, effectiveTo])
|
|
770
|
+
|
|
771
|
+
// Keep the "Draft saved Xs ago" label fresh without re-rendering per second.
|
|
772
|
+
useEffect(() => {
|
|
773
|
+
if (!draftSavedAt) return
|
|
774
|
+
const interval = window.setInterval(() => setDraftClock((tick) => tick + 1), DRAFT_SAVED_LABEL_REFRESH_MS)
|
|
775
|
+
return () => window.clearInterval(interval)
|
|
776
|
+
}, [draftSavedAt])
|
|
777
|
+
|
|
778
|
+
const draftSavedLabel = useMemo(() => {
|
|
779
|
+
if (!draftSavedAt) return null
|
|
780
|
+
return formatRelativeTime(draftSavedAt, { translate: t })
|
|
781
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
782
|
+
}, [draftSavedAt, draftClock, t])
|
|
783
|
+
|
|
784
|
+
const handleRestoreDraft = useCallback(() => {
|
|
785
|
+
if (!pendingDraft) return
|
|
786
|
+
try {
|
|
787
|
+
const draftDefinition = pendingDraft.draft.definition as unknown as Parameters<typeof definitionToGraph>[0]
|
|
788
|
+
const graph = definitionToGraph(draftDefinition)
|
|
789
|
+
setNodes([...graph.nodes, ...annotationsToNodes(readEditorAnnotations(pendingDraft.draft.metadata ?? null))])
|
|
790
|
+
setEdges(graph.edges)
|
|
791
|
+
const draftTriggers = pendingDraft.draft.definition.triggers
|
|
792
|
+
setTriggers(Array.isArray(draftTriggers) ? (draftTriggers as WorkflowDefinitionTrigger[]) : [])
|
|
793
|
+
const draftContextSchema = pendingDraft.draft.definition.contextSchema
|
|
794
|
+
setContextSchema(draftContextSchema ? (draftContextSchema as WorkflowContextSchema) : undefined)
|
|
795
|
+
const draftIo = (pendingDraft.draft.definition as { io?: WorkflowIoContract }).io
|
|
796
|
+
setDefinitionIo(draftIo ?? undefined)
|
|
797
|
+
setInterpolation(resolveDefinitionInterpolationMode(pendingDraft.draft.definition))
|
|
798
|
+
const draftErrorHandler = (pendingDraft.draft.definition as { errorHandler?: WorkflowErrorHandlerConfig }).errorHandler
|
|
799
|
+
setErrorHandler(draftErrorHandler ?? undefined)
|
|
800
|
+
const restoredMetadata = pendingDraft.draft.metadata ?? null
|
|
801
|
+
setLoadedMetadata(restoredMetadata)
|
|
802
|
+
if (restoredMetadata) {
|
|
803
|
+
setCategory(typeof restoredMetadata.category === 'string' ? restoredMetadata.category : '')
|
|
804
|
+
setTags(Array.isArray(restoredMetadata.tags) ? restoredMetadata.tags.filter((tag): tag is string => typeof tag === 'string') : [])
|
|
805
|
+
setIcon(typeof restoredMetadata.icon === 'string' ? restoredMetadata.icon : '')
|
|
806
|
+
}
|
|
807
|
+
lastPersistedDraftRef.current = stableSerializeDefinition({
|
|
808
|
+
definition: pendingDraft.draft.definition,
|
|
809
|
+
metadata: pendingDraft.draft.metadata ?? null,
|
|
810
|
+
})
|
|
811
|
+
resetHistory()
|
|
812
|
+
flash(t('workflows.visualEditor.draft.restored', 'Draft restored'), 'success')
|
|
813
|
+
} catch (error) {
|
|
814
|
+
logger.error('Error restoring workflow definition draft', { err: error })
|
|
815
|
+
flash(t('workflows.visualEditor.draft.restoreFailed', 'Failed to restore draft'), 'error')
|
|
816
|
+
} finally {
|
|
817
|
+
setPendingDraft(null)
|
|
818
|
+
setDraftAutosaveReady(true)
|
|
819
|
+
}
|
|
820
|
+
}, [pendingDraft, resetHistory, t])
|
|
821
|
+
|
|
822
|
+
const handleDiscardDraft = useCallback(async () => {
|
|
823
|
+
setPendingDraft(null)
|
|
824
|
+
setDraftAutosaveReady(true)
|
|
825
|
+
if (!definitionId) return
|
|
826
|
+
try {
|
|
827
|
+
await apiCall(`/api/workflows/definitions/${definitionId}/draft`, { method: 'DELETE' })
|
|
828
|
+
} catch (error) {
|
|
829
|
+
logger.error('Error discarding workflow definition draft', { err: error })
|
|
830
|
+
}
|
|
831
|
+
}, [definitionId])
|
|
832
|
+
|
|
833
|
+
// Schedule a quiet debounced autosave (~900ms). Only for already-saved,
|
|
834
|
+
// editable definitions; new unsaved drafts fall back to an explicit Save.
|
|
835
|
+
const scheduleAutosave = useCallback(() => {
|
|
836
|
+
if (!definitionId || isCodeOnly) return
|
|
837
|
+
if (autosaveTimerRef.current) clearTimeout(autosaveTimerRef.current)
|
|
838
|
+
autosaveTimerRef.current = setTimeout(() => {
|
|
839
|
+
autosaveTimerRef.current = null
|
|
840
|
+
void performAutosaveRef.current()
|
|
841
|
+
}, 900)
|
|
842
|
+
}, [definitionId, isCodeOnly])
|
|
843
|
+
|
|
844
|
+
// Clear any pending autosave timer on unmount.
|
|
845
|
+
useEffect(() => () => {
|
|
846
|
+
if (autosaveTimerRef.current) clearTimeout(autosaveTimerRef.current)
|
|
847
|
+
}, [])
|
|
848
|
+
|
|
849
|
+
const applyHistoryDocument = useCallback((document: WorkflowEditorDocument) => {
|
|
850
|
+
setNodes(document.nodes)
|
|
851
|
+
setEdges(document.edges)
|
|
852
|
+
setLoadedMetadata(document.metadata)
|
|
853
|
+
// Entries captured before the panel fields were versioned carry none; then
|
|
854
|
+
// the panel is left exactly as it is, which is the previous behaviour.
|
|
855
|
+
if (document.panel) {
|
|
856
|
+
setTriggers(document.panel.triggers as WorkflowDefinitionTrigger[])
|
|
857
|
+
setContextSchema(document.panel.contextSchema as WorkflowContextSchema | undefined)
|
|
858
|
+
setDefinitionIo(document.panel.io as WorkflowIoContract | undefined)
|
|
859
|
+
setInterpolation(document.panel.interpolation as WorkflowInterpolationMode | undefined)
|
|
860
|
+
setErrorHandler(document.panel.errorHandler as WorkflowErrorHandlerConfig | undefined)
|
|
861
|
+
}
|
|
862
|
+
scheduleAutosave()
|
|
863
|
+
}, [scheduleAutosave])
|
|
864
|
+
|
|
865
|
+
const handleUndo = useCallback(() => {
|
|
866
|
+
if (isCodeOnly) return
|
|
867
|
+
const step = undoEditorHistory(historyRef.current, captureDocument())
|
|
868
|
+
if (!step) {
|
|
869
|
+
flash(t('workflows.visualEditor.history.nothingToUndo', 'Nothing to undo'), 'info')
|
|
870
|
+
return
|
|
871
|
+
}
|
|
872
|
+
setHistory(step.state)
|
|
873
|
+
applyHistoryDocument(step.document)
|
|
874
|
+
flash(t('workflows.visualEditor.history.undone', 'Undone: {label}', { label: step.label }), 'success')
|
|
875
|
+
}, [isCodeOnly, captureDocument, applyHistoryDocument, t])
|
|
876
|
+
|
|
877
|
+
const handleRedo = useCallback(() => {
|
|
878
|
+
if (isCodeOnly) return
|
|
879
|
+
const step = redoEditorHistory(historyRef.current, captureDocument())
|
|
880
|
+
if (!step) {
|
|
881
|
+
flash(t('workflows.visualEditor.history.nothingToRedo', 'Nothing to redo'), 'info')
|
|
882
|
+
return
|
|
883
|
+
}
|
|
884
|
+
setHistory(step.state)
|
|
885
|
+
applyHistoryDocument(step.document)
|
|
886
|
+
flash(t('workflows.visualEditor.history.redone', 'Redone: {label}', { label: step.label }), 'success')
|
|
887
|
+
}, [isCodeOnly, captureDocument, applyHistoryDocument, t])
|
|
888
|
+
|
|
889
|
+
// Copy / paste / duplicate of a selected subgraph (spec §4.5). The payload on
|
|
890
|
+
// the system clipboard is portable JSON in the DEFINITION vocabulary, so it
|
|
891
|
+
// travels to another workflow and to the Code view. When the browser denies
|
|
892
|
+
// clipboard access (insecure context, no permission) the editor falls back to
|
|
893
|
+
// an in-page buffer and says so, rather than failing silently.
|
|
894
|
+
const localClipboardRef = React.useRef<string | null>(null)
|
|
895
|
+
|
|
896
|
+
const selectedSubgraph = useCallback(() => {
|
|
897
|
+
const selectedIds = nodesRef.current.filter((node) => node.selected).map((node) => node.id)
|
|
898
|
+
return serializeWorkflowSubgraph(nodesRef.current, edgesRef.current, selectedIds)
|
|
899
|
+
}, [])
|
|
900
|
+
|
|
901
|
+
const handleCopySelection = useCallback(async () => {
|
|
902
|
+
const payload = selectedSubgraph()
|
|
903
|
+
if (!payload) {
|
|
904
|
+
flash(t('workflows.visualEditor.clipboard.nothingSelected', 'Select one or more steps first'), 'info')
|
|
905
|
+
return
|
|
906
|
+
}
|
|
907
|
+
const serialized = stringifyWorkflowSubgraph(payload)
|
|
908
|
+
localClipboardRef.current = serialized
|
|
909
|
+
try {
|
|
910
|
+
await navigator.clipboard.writeText(serialized)
|
|
911
|
+
flash(
|
|
912
|
+
t('workflows.visualEditor.clipboard.copied', 'Copied {count} step(s)', { count: String(payload.steps.length) }),
|
|
913
|
+
'success',
|
|
914
|
+
)
|
|
915
|
+
} catch (error) {
|
|
916
|
+
logger.warn('Workflow subgraph copy fell back to the in-page buffer', { err: error })
|
|
917
|
+
flash(
|
|
918
|
+
t(
|
|
919
|
+
'workflows.visualEditor.clipboard.copyUnavailable',
|
|
920
|
+
'The browser blocked clipboard access, so the selection was kept in this editor only — pasting into another tab will not work.',
|
|
921
|
+
),
|
|
922
|
+
'warning',
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
}, [selectedSubgraph, t])
|
|
926
|
+
|
|
927
|
+
const describeClipboardFailure = useCallback((code: SubgraphClipboardParseFailure['code']) => {
|
|
928
|
+
switch (code) {
|
|
929
|
+
case 'invalidJson':
|
|
930
|
+
return t('workflows.visualEditor.clipboard.invalidJson', 'The clipboard does not contain valid JSON.')
|
|
931
|
+
case 'empty':
|
|
932
|
+
return t('workflows.visualEditor.clipboard.empty', 'The copied selection contains no steps.')
|
|
933
|
+
default:
|
|
934
|
+
return t('workflows.visualEditor.clipboard.unsupportedFormat', 'The clipboard does not contain a copied workflow selection.')
|
|
935
|
+
}
|
|
936
|
+
}, [t])
|
|
937
|
+
|
|
938
|
+
const spliceSubgraph = useCallback((payload: WorkflowSubgraphClipboard, label: string) => {
|
|
939
|
+
const result = pasteWorkflowSubgraph(nodesRef.current, edgesRef.current, payload)
|
|
940
|
+
commitHistory(label)
|
|
941
|
+
setNodes(result.nodes)
|
|
942
|
+
setEdges(result.edges)
|
|
943
|
+
scheduleAutosave()
|
|
944
|
+
flash(
|
|
945
|
+
t('workflows.visualEditor.clipboard.pasted', 'Pasted {count} step(s)', { count: String(result.pastedNodeIds.length) }),
|
|
946
|
+
'success',
|
|
947
|
+
)
|
|
948
|
+
}, [commitHistory, scheduleAutosave, t])
|
|
949
|
+
|
|
950
|
+
const handlePaste = useCallback(async () => {
|
|
951
|
+
if (isCodeOnly) return
|
|
952
|
+
let text: string | null = null
|
|
953
|
+
try {
|
|
954
|
+
text = await navigator.clipboard.readText()
|
|
955
|
+
} catch (error) {
|
|
956
|
+
logger.warn('Workflow subgraph paste fell back to the in-page buffer', { err: error })
|
|
957
|
+
text = localClipboardRef.current
|
|
958
|
+
if (!text) {
|
|
959
|
+
flash(
|
|
960
|
+
t(
|
|
961
|
+
'workflows.visualEditor.clipboard.pasteUnavailable',
|
|
962
|
+
'The browser blocked clipboard access, so there is nothing to paste. Copy a selection in this editor first.',
|
|
963
|
+
),
|
|
964
|
+
'error',
|
|
965
|
+
)
|
|
966
|
+
return
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const parsed = parseWorkflowSubgraph(text ?? '')
|
|
970
|
+
if (!parsed.ok) {
|
|
971
|
+
flash(describeClipboardFailure(parsed.code), 'error')
|
|
972
|
+
return
|
|
973
|
+
}
|
|
974
|
+
spliceSubgraph(parsed.payload, historyLabels.paste)
|
|
975
|
+
}, [isCodeOnly, describeClipboardFailure, spliceSubgraph, historyLabels.paste, t])
|
|
976
|
+
|
|
977
|
+
const handleDuplicateSelection = useCallback(() => {
|
|
978
|
+
if (isCodeOnly) return
|
|
979
|
+
const payload = selectedSubgraph()
|
|
980
|
+
if (!payload) {
|
|
981
|
+
flash(t('workflows.visualEditor.clipboard.nothingSelected', 'Select one or more steps first'), 'info')
|
|
982
|
+
return
|
|
983
|
+
}
|
|
984
|
+
spliceSubgraph(payload, historyLabels.duplicate)
|
|
985
|
+
}, [isCodeOnly, selectedSubgraph, spliceSubgraph, historyLabels.duplicate, t])
|
|
986
|
+
|
|
178
987
|
// Handle node changes from ReactFlow. The lazy graph applies React Flow's
|
|
179
|
-
// change reducers internally (#3169) and hands back the resolved nodes
|
|
180
|
-
// this page never imports the @xyflow/react
|
|
181
|
-
|
|
988
|
+
// change reducers internally (#3169) and hands back the resolved nodes plus a
|
|
989
|
+
// note on what the batch meant, so this page never imports the @xyflow/react
|
|
990
|
+
// runtime. A drag persists once, on drag end (#4248); selecting or measuring
|
|
991
|
+
// a node changes nothing worth saving and must not touch the stored row.
|
|
992
|
+
// A drag reports one change batch per frame, so the undoable "before" state is
|
|
993
|
+
// the arrangement captured when the drag STARTED — not the previous frame.
|
|
994
|
+
const dragBaselineRef = React.useRef<WorkflowEditorDocument | null>(null)
|
|
995
|
+
|
|
996
|
+
const handleNodesChange = useCallback((nextNodes: Node[], meta?: WorkflowGraphNodesChangeMeta) => {
|
|
182
997
|
if (isCodeOnly) return
|
|
998
|
+
if ((meta?.dragging || meta?.resizing) && !dragBaselineRef.current) dragBaselineRef.current = captureDocument()
|
|
999
|
+
const baseline = dragBaselineRef.current ?? captureDocument()
|
|
183
1000
|
setNodes(nextNodes)
|
|
184
|
-
|
|
1001
|
+
if (meta && !meta.persistable) return
|
|
1002
|
+
dragBaselineRef.current = null
|
|
1003
|
+
commitCapturedDocument(baseline, historyLabels.move)
|
|
1004
|
+
scheduleAutosave()
|
|
1005
|
+
}, [isCodeOnly, captureDocument, commitCapturedDocument, historyLabels.move, scheduleAutosave])
|
|
1006
|
+
|
|
1007
|
+
// Auto-arrange / Tidy: the single intentional full re-layout. Re-runs dagre
|
|
1008
|
+
// (LR) over the current graph, overwrites positions, and persists via autosave.
|
|
1009
|
+
const handleAutoArrange = useCallback(() => {
|
|
1010
|
+
if (isCodeOnly) return
|
|
1011
|
+
commitHistory(historyLabels.tidy)
|
|
1012
|
+
setNodes((nds) => applyAutoLayout(nds, edges))
|
|
1013
|
+
scheduleAutosave()
|
|
1014
|
+
}, [isCodeOnly, edges, commitHistory, historyLabels.tidy, scheduleAutosave])
|
|
185
1015
|
|
|
186
1016
|
// Handle edge changes from ReactFlow (resolved edges from the lazy graph).
|
|
187
1017
|
const handleEdgesChange = useCallback((nextEdges: Edge[]) => {
|
|
@@ -189,33 +1019,147 @@ export default function VisualEditorPage() {
|
|
|
189
1019
|
setEdges(nextEdges)
|
|
190
1020
|
}, [isCodeOnly])
|
|
191
1021
|
|
|
192
|
-
|
|
193
|
-
|
|
1022
|
+
const buildPaletteNode = useCallback((nodeType: string, dropPosition?: { x: number; y: number } | null): Node => ({
|
|
1023
|
+
id: generateStepId(nodeType),
|
|
1024
|
+
type: nodeType,
|
|
1025
|
+
position: resolveNewNodePlacement(nodesRef.current, { dropPosition }),
|
|
1026
|
+
data: {
|
|
1027
|
+
label: getDefaultLabel(nodeType),
|
|
1028
|
+
description: '',
|
|
1029
|
+
badge: getDefaultBadge(nodeType),
|
|
1030
|
+
status: 'pending',
|
|
1031
|
+
},
|
|
1032
|
+
}), [])
|
|
1033
|
+
|
|
1034
|
+
// Handle adding new node from palette. A drop position (drag-from-palette)
|
|
1035
|
+
// places the card under the cursor; the click-append path lands after the
|
|
1036
|
+
// right-most card. Both are nudged clear of existing cards (#4248) and both
|
|
1037
|
+
// count as a manual arrangement, so the placement is persisted like a drag.
|
|
1038
|
+
const handleAddNode = useCallback((nodeType: string, dropPosition?: { x: number; y: number } | null) => {
|
|
194
1039
|
if (isCodeOnly) return
|
|
195
|
-
const newNode
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
1040
|
+
const newNode = buildPaletteNode(nodeType, dropPosition)
|
|
1041
|
+
commitHistory(historyLabels.addStep)
|
|
1042
|
+
setNodes((nds) => [...nds, newNode])
|
|
1043
|
+
scheduleAutosave()
|
|
1044
|
+
}, [isCodeOnly, buildPaletteNode, commitHistory, historyLabels.addStep, scheduleAutosave])
|
|
1045
|
+
|
|
1046
|
+
// Notes and groups are added from the palette exactly like steps: a click
|
|
1047
|
+
// appends one at a free spot and opens its inspector, so the keyboard path is
|
|
1048
|
+
// the same path.
|
|
1049
|
+
const handleAddAnnotation = useCallback((kind: 'note' | 'group') => {
|
|
1050
|
+
if (isCodeOnly) return
|
|
1051
|
+
const position = resolveNewNodePlacement(nodesRef.current, {})
|
|
1052
|
+
const node = kind === 'note'
|
|
1053
|
+
? createNoteNode(position, '')
|
|
1054
|
+
: createGroupNode(position, t('workflows.annotations.group.untitled', 'Untitled group'))
|
|
1055
|
+
commitHistory(kind === 'note' ? historyLabels.addNote : historyLabels.addGroup)
|
|
1056
|
+
setNodes((nds) => [...nds, node])
|
|
1057
|
+
setSelectedAnnotation(node)
|
|
1058
|
+
setShowAnnotationDialog(true)
|
|
1059
|
+
scheduleAutosave()
|
|
1060
|
+
}, [isCodeOnly, commitHistory, historyLabels.addNote, historyLabels.addGroup, scheduleAutosave, t])
|
|
1061
|
+
|
|
1062
|
+
const activityTypeOptions = useActivityTypeOptions()
|
|
1063
|
+
|
|
1064
|
+
const describePaletteDropRejection = useCallback((code: PaletteDropRejectionCode) => {
|
|
1065
|
+
switch (code) {
|
|
1066
|
+
case 'dataMappingRoute':
|
|
1067
|
+
return t('workflows.visualEditor.palette.rejected.dataMappingRoute', 'Data mapping links are edited in the step configuration, not on the canvas.')
|
|
1068
|
+
default:
|
|
1069
|
+
return t('workflows.visualEditor.palette.rejected.unknownRoute', 'That route no longer exists.')
|
|
208
1070
|
}
|
|
1071
|
+
}, [t])
|
|
1072
|
+
|
|
1073
|
+
// Drag-from-palette (spec §4.2). Dropping on empty canvas places the step at
|
|
1074
|
+
// the cursor; dropping on a route splices it between that route's endpoints;
|
|
1075
|
+
// dropping an action on a route appends it to the route's activities. The
|
|
1076
|
+
// graph resolved the flow-space position and the route under the cursor — the
|
|
1077
|
+
// effect itself is pure (`lib/palette-drop.ts`).
|
|
1078
|
+
const handleCanvasDrop = useCallback((event: WorkflowGraphDropEvent) => {
|
|
1079
|
+
if (isCodeOnly) return
|
|
1080
|
+
const item = readPaletteDragItem(event.dataTransfer)
|
|
1081
|
+
if (!item) return
|
|
209
1082
|
|
|
210
|
-
|
|
211
|
-
|
|
1083
|
+
if (item.kind === 'step') {
|
|
1084
|
+
if (!event.edgeId) {
|
|
1085
|
+
handleAddNode(item.nodeType, event.position)
|
|
1086
|
+
return
|
|
1087
|
+
}
|
|
1088
|
+
const result = insertStepOnRoute(
|
|
1089
|
+
nodesRef.current,
|
|
1090
|
+
edgesRef.current,
|
|
1091
|
+
event.edgeId,
|
|
1092
|
+
buildPaletteNode(item.nodeType, event.position),
|
|
1093
|
+
)
|
|
1094
|
+
if (!result.ok) {
|
|
1095
|
+
flash(describePaletteDropRejection(result.code), 'error')
|
|
1096
|
+
return
|
|
1097
|
+
}
|
|
1098
|
+
commitHistory(historyLabels.insertOnRoute)
|
|
1099
|
+
setNodes(result.nodes)
|
|
1100
|
+
setEdges(result.edges)
|
|
1101
|
+
scheduleAutosave()
|
|
1102
|
+
flash(t('workflows.visualEditor.palette.insertedOnRoute', 'Step inserted on the route'), 'success')
|
|
1103
|
+
return
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (!event.edgeId) {
|
|
1107
|
+
flash(t('workflows.visualEditor.palette.dragActionHint', 'Drop an action onto a route to add it there'), 'info')
|
|
1108
|
+
return
|
|
1109
|
+
}
|
|
1110
|
+
const activityLabel = activityTypeOptions.find((option) => option.value === item.activityType)?.label ?? item.activityType
|
|
1111
|
+
const result = appendActivityToRoute(edgesRef.current, event.edgeId, {
|
|
1112
|
+
activityId: `activity_${Date.now()}`,
|
|
1113
|
+
activityName: activityLabel,
|
|
1114
|
+
activityType: item.activityType,
|
|
1115
|
+
config: {},
|
|
1116
|
+
})
|
|
1117
|
+
if (!result.ok) {
|
|
1118
|
+
flash(describePaletteDropRejection(result.code), 'error')
|
|
1119
|
+
return
|
|
1120
|
+
}
|
|
1121
|
+
commitHistory(historyLabels.addActivity)
|
|
1122
|
+
setEdges(result.edges)
|
|
1123
|
+
scheduleAutosave()
|
|
1124
|
+
flash(t('workflows.visualEditor.palette.activityAdded', '{activity} added to the route', { activity: activityLabel }), 'success')
|
|
1125
|
+
}, [
|
|
1126
|
+
isCodeOnly,
|
|
1127
|
+
handleAddNode,
|
|
1128
|
+
buildPaletteNode,
|
|
1129
|
+
activityTypeOptions,
|
|
1130
|
+
describePaletteDropRejection,
|
|
1131
|
+
commitHistory,
|
|
1132
|
+
historyLabels.insertOnRoute,
|
|
1133
|
+
historyLabels.addActivity,
|
|
1134
|
+
scheduleAutosave,
|
|
1135
|
+
t,
|
|
1136
|
+
])
|
|
1137
|
+
|
|
1138
|
+
const handlePaletteStepDragStart = useCallback((event: React.DragEvent<HTMLButtonElement>, nodeType: string) => {
|
|
1139
|
+
writePaletteDragPayload(event.dataTransfer, { kind: 'step', nodeType }, NODE_TYPE_LABELS[nodeType as keyof typeof NODE_TYPE_LABELS]?.title ?? nodeType)
|
|
1140
|
+
}, [])
|
|
1141
|
+
|
|
1142
|
+
const handlePaletteActivityDragStart = useCallback((event: React.DragEvent<HTMLButtonElement>, activityType: string, label: string) => {
|
|
1143
|
+
writePaletteDragPayload(event.dataTransfer, { kind: 'activity', activityType }, label)
|
|
1144
|
+
}, [])
|
|
212
1145
|
|
|
213
1146
|
// Handle node selection - open edit dialog (suppressed in read-only mode
|
|
214
1147
|
// so users can't open the node editor on a code-defined workflow).
|
|
215
1148
|
const handleNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
|
|
216
1149
|
if (isCodeOnly) return
|
|
1150
|
+
if (isAnnotationNode(node)) {
|
|
1151
|
+
setSelectedAnnotation(node)
|
|
1152
|
+
setSelectedNode(null)
|
|
1153
|
+
setSelectedEdge(null)
|
|
1154
|
+
setShowAnnotationDialog(true)
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
217
1157
|
setSelectedNode(node)
|
|
218
1158
|
setSelectedEdge(null)
|
|
1159
|
+
// With a docked rail the canvas stays clickable, so a step click can arrive
|
|
1160
|
+
// while the ROUTE inspector is open. Closing the other one keeps a single
|
|
1161
|
+
// rail; the modal variant could never reach this state.
|
|
1162
|
+
setShowEdgeDialog(false)
|
|
219
1163
|
setShowNodeDialog(true)
|
|
220
1164
|
}, [isCodeOnly])
|
|
221
1165
|
|
|
@@ -224,11 +1168,53 @@ export default function VisualEditorPage() {
|
|
|
224
1168
|
if (isCodeOnly) return
|
|
225
1169
|
setSelectedEdge(edge)
|
|
226
1170
|
setSelectedNode(null)
|
|
1171
|
+
setEdgeDialogFocusFieldId(null)
|
|
1172
|
+
setShowNodeDialog(false)
|
|
1173
|
+
setShowEdgeDialog(true)
|
|
1174
|
+
}, [isCodeOnly])
|
|
1175
|
+
|
|
1176
|
+
// Route chips (#4244) open the edge dialog on the section behind the chip.
|
|
1177
|
+
// Chips render inside React Flow's label portal, so they announce on `window`
|
|
1178
|
+
// instead of bubbling to `onEdgeClick` — same bridge as the node delete button.
|
|
1179
|
+
const handleRouteChipOpen = useCallback((edgeId: string, section: RouteChipEventDetail['section']) => {
|
|
1180
|
+
if (isCodeOnly) return
|
|
1181
|
+
const edge = edgesRef.current.find((candidate) => candidate.id === edgeId)
|
|
1182
|
+
if (!edge) return
|
|
1183
|
+
setSelectedEdge(edge)
|
|
1184
|
+
setSelectedNode(null)
|
|
1185
|
+
setEdgeDialogFocusFieldId(section === 'condition' ? 'condition' : 'activities')
|
|
227
1186
|
setShowEdgeDialog(true)
|
|
228
1187
|
}, [isCodeOnly])
|
|
229
1188
|
|
|
230
1189
|
// Save node updates
|
|
231
1190
|
const handleSaveNode = useCallback((nodeId: string, updates: Partial<Node['data']>) => {
|
|
1191
|
+
commitHistory(historyLabels.editStep)
|
|
1192
|
+
if (Object.prototype.hasOwnProperty.call(updates, 'decisions')) {
|
|
1193
|
+
const previousNode = nodesRef.current.find((node) => node.id === nodeId)
|
|
1194
|
+
const previousDecisions = previousNode?.data.decisions as DecisionRowLike[] | undefined
|
|
1195
|
+
const nextDecisions = updates.decisions as DecisionRowLike[] | undefined
|
|
1196
|
+
const previousIds = new Set(
|
|
1197
|
+
(previousDecisions ?? []).flatMap((decision) =>
|
|
1198
|
+
typeof decision.transitionId === 'string' ? [decision.transitionId] : []
|
|
1199
|
+
),
|
|
1200
|
+
)
|
|
1201
|
+
const nextIds = new Set(
|
|
1202
|
+
(nextDecisions ?? []).flatMap((decision) =>
|
|
1203
|
+
typeof decision.transitionId === 'string' ? [decision.transitionId] : []
|
|
1204
|
+
),
|
|
1205
|
+
)
|
|
1206
|
+
setEdges((currentEdges) =>
|
|
1207
|
+
currentEdges.map((edge) => {
|
|
1208
|
+
if (edge.source !== nodeId) return edge
|
|
1209
|
+
if (nextIds.has(edge.id)) return { ...edge, sourceHandle: edge.id }
|
|
1210
|
+
if (previousIds.has(edge.id) && edge.sourceHandle === edge.id) {
|
|
1211
|
+
const { sourceHandle: _removed, ...remainingEdge } = edge
|
|
1212
|
+
return remainingEdge
|
|
1213
|
+
}
|
|
1214
|
+
return edge
|
|
1215
|
+
}),
|
|
1216
|
+
)
|
|
1217
|
+
}
|
|
232
1218
|
setNodes((nds) =>
|
|
233
1219
|
nds.map((node) =>
|
|
234
1220
|
node.id === nodeId
|
|
@@ -237,10 +1223,59 @@ export default function VisualEditorPage() {
|
|
|
237
1223
|
)
|
|
238
1224
|
)
|
|
239
1225
|
flash('Node updated successfully', 'success')
|
|
240
|
-
}, [])
|
|
1226
|
+
}, [commitHistory, historyLabels.editStep])
|
|
1227
|
+
|
|
1228
|
+
// Branching (IF_ELSE / SWITCH) routes are read from and written back to the
|
|
1229
|
+
// node's outgoing edges — the inspector never introduces a bespoke shape.
|
|
1230
|
+
const branchingRoutesValue = useMemo<SwitchRoutesValue | undefined>(() => {
|
|
1231
|
+
if (!selectedNode || !isBranchingNodeType(selectedNode.type)) return undefined
|
|
1232
|
+
return {
|
|
1233
|
+
field: readSwitchField(edges, selectedNode.id),
|
|
1234
|
+
routes: readBranchingRoutes(edges, selectedNode.id),
|
|
1235
|
+
}
|
|
1236
|
+
}, [selectedNode, edges])
|
|
1237
|
+
|
|
1238
|
+
const handleSaveBranchingRoutes = useCallback((nodeId: string, value: SwitchRoutesValue) => {
|
|
1239
|
+
const nodeType = nodes.find((node) => node.id === nodeId)?.type
|
|
1240
|
+
commitHistory(historyLabels.branchRoutes)
|
|
1241
|
+
setEdges((eds) => (nodeType === 'switch' ? applySwitchRoutes(eds, nodeId, value) : applyIfElseRoutes(eds, nodeId, value.routes)))
|
|
1242
|
+
scheduleAutosave()
|
|
1243
|
+
}, [nodes, commitHistory, historyLabels.branchRoutes, scheduleAutosave])
|
|
1244
|
+
|
|
1245
|
+
// Route order (spec 4.4): a non-branching step's outgoing routes are ordered
|
|
1246
|
+
// in the inspector and the priority number is derived from that order.
|
|
1247
|
+
const routeOrderValue = useMemo<RouteOrderEntry[] | undefined>(() => {
|
|
1248
|
+
if (!selectedNode || isBranchingNodeType(selectedNode.type)) return undefined
|
|
1249
|
+
return readRouteOrder(edges, selectedNode.id)
|
|
1250
|
+
}, [selectedNode, edges])
|
|
1251
|
+
|
|
1252
|
+
// Nodes whose legacy priorities have already been rewritten this session. The
|
|
1253
|
+
// normalization pass runs on the FIRST edit of a node's routes and is
|
|
1254
|
+
// idempotent afterwards.
|
|
1255
|
+
const normalizedRouteNodesRef = React.useRef<Set<string>>(new Set())
|
|
1256
|
+
|
|
1257
|
+
// A `source: 'code'` definition is read-only in the editor, so normalization
|
|
1258
|
+
// simply cannot run for it. Customize mints an editable override (source
|
|
1259
|
+
// becomes 'code_override') and the pass runs there on the first route edit —
|
|
1260
|
+
// which is exactly the spec's "only on Customize".
|
|
1261
|
+
const normalizeRoutesOnFirstEdit = useCallback((nodeId: string) => {
|
|
1262
|
+
if (!canNormalizeRoutePriorities(source)) return
|
|
1263
|
+
if (normalizedRouteNodesRef.current.has(nodeId)) return
|
|
1264
|
+
normalizedRouteNodesRef.current.add(nodeId)
|
|
1265
|
+
setEdges((eds) => normalizeRoutePriorities(eds, nodeId))
|
|
1266
|
+
}, [source])
|
|
1267
|
+
|
|
1268
|
+
const handleSaveRouteOrder = useCallback((nodeId: string, entries: RouteOrderEntry[]) => {
|
|
1269
|
+
if (isCodeOnly) return
|
|
1270
|
+
commitHistory(historyLabels.routeOrder)
|
|
1271
|
+
normalizeRoutesOnFirstEdit(nodeId)
|
|
1272
|
+
setEdges((eds) => applyRouteOrder(eds, nodeId, entries))
|
|
1273
|
+
scheduleAutosave()
|
|
1274
|
+
}, [isCodeOnly, normalizeRoutesOnFirstEdit, commitHistory, historyLabels.routeOrder, scheduleAutosave])
|
|
241
1275
|
|
|
242
1276
|
// Save edge updates
|
|
243
1277
|
const handleSaveEdge = useCallback((edgeId: string, updates: Partial<Edge['data']>) => {
|
|
1278
|
+
commitHistory(historyLabels.editRoute)
|
|
244
1279
|
setEdges((eds) =>
|
|
245
1280
|
eds.map((edge) =>
|
|
246
1281
|
edge.id === edgeId
|
|
@@ -249,11 +1284,14 @@ export default function VisualEditorPage() {
|
|
|
249
1284
|
)
|
|
250
1285
|
)
|
|
251
1286
|
flash('Transition updated successfully', 'success')
|
|
252
|
-
}, [])
|
|
1287
|
+
}, [commitHistory, historyLabels.editRoute])
|
|
253
1288
|
|
|
254
|
-
// Delete edge
|
|
1289
|
+
// Delete edge. The undo snapshot is taken before the confirmation and only
|
|
1290
|
+
// committed once the delete actually lands, so a cancelled confirm leaves no
|
|
1291
|
+
// entry on the stack.
|
|
255
1292
|
const handleDeleteEdge = useCallback(async (edgeId: string) => {
|
|
256
|
-
|
|
1293
|
+
const before = captureDocument()
|
|
1294
|
+
const deleted = await performDeleteEdgeFlow(edgeId, {
|
|
257
1295
|
confirm,
|
|
258
1296
|
t,
|
|
259
1297
|
setShowEdgeDialog,
|
|
@@ -261,11 +1299,62 @@ export default function VisualEditorPage() {
|
|
|
261
1299
|
setEdges,
|
|
262
1300
|
notifyDeleted: () => flash('Transition deleted successfully', 'success'),
|
|
263
1301
|
})
|
|
264
|
-
|
|
1302
|
+
if (deleted) commitCapturedDocument(before, historyLabels.deleteRoute)
|
|
1303
|
+
}, [confirm, t, captureDocument, commitCapturedDocument, historyLabels.deleteRoute])
|
|
1304
|
+
|
|
1305
|
+
// Annotation inspector (spec 4.5). Notes and groups edit a single field each
|
|
1306
|
+
// and commit one undo entry, exactly like a step inspector save.
|
|
1307
|
+
const handleSaveAnnotation = useCallback((nodeId: string, updates: Partial<WorkflowNoteNodeData & WorkflowGroupNodeData>) => {
|
|
1308
|
+
if (isCodeOnly) return
|
|
1309
|
+
commitHistory(historyLabels.editAnnotation)
|
|
1310
|
+
setNodes((nds) => updateAnnotationNode(nds, nodeId, updates))
|
|
1311
|
+
scheduleAutosave()
|
|
1312
|
+
}, [isCodeOnly, commitHistory, historyLabels.editAnnotation, scheduleAutosave])
|
|
1313
|
+
|
|
1314
|
+
const handleDeleteAnnotation = useCallback(async (nodeId: string) => {
|
|
1315
|
+
if (isCodeOnly) return
|
|
1316
|
+
const before = captureDocument()
|
|
1317
|
+
setShowAnnotationDialog(false)
|
|
1318
|
+
setSelectedAnnotation(null)
|
|
1319
|
+
const confirmed = await confirm({
|
|
1320
|
+
title: t('workflows.annotations.confirmDeleteTitle', 'Delete annotation?'),
|
|
1321
|
+
text: t('workflows.annotations.confirmDeleteText', 'The note or group is removed from the canvas. Nothing the workflow executes changes.'),
|
|
1322
|
+
variant: 'destructive',
|
|
1323
|
+
})
|
|
1324
|
+
if (!confirmed) return
|
|
1325
|
+
commitCapturedDocument(before, historyLabels.deleteAnnotation)
|
|
1326
|
+
setNodes((nds) => removeAnnotationNode(nds, nodeId))
|
|
1327
|
+
scheduleAutosave()
|
|
1328
|
+
}, [isCodeOnly, captureDocument, commitCapturedDocument, historyLabels.deleteAnnotation, confirm, scheduleAutosave, t])
|
|
1329
|
+
|
|
1330
|
+
const handleToggleGroupCollapsed = useCallback((nodeId: string) => {
|
|
1331
|
+
if (isCodeOnly) return
|
|
1332
|
+
const node = nodesRef.current.find((candidate) => candidate.id === nodeId)
|
|
1333
|
+
if (!node) return
|
|
1334
|
+
const collapsed = (node.data as Partial<WorkflowGroupNodeData> | undefined)?.collapsed === true
|
|
1335
|
+
commitHistory(historyLabels.toggleGroup)
|
|
1336
|
+
setNodes((nds) => updateAnnotationNode(nds, nodeId, { collapsed: !collapsed }))
|
|
1337
|
+
scheduleAutosave()
|
|
1338
|
+
}, [isCodeOnly, commitHistory, historyLabels.toggleGroup, scheduleAutosave])
|
|
1339
|
+
|
|
1340
|
+
useEffect(() => {
|
|
1341
|
+
const onToggle = (event: Event) => {
|
|
1342
|
+
const nodeId = (event as CustomEvent<{ nodeId?: string }>).detail?.nodeId
|
|
1343
|
+
if (typeof nodeId === 'string') handleToggleGroupCollapsed(nodeId)
|
|
1344
|
+
}
|
|
1345
|
+
window.addEventListener(WORKFLOW_GROUP_TOGGLE_EVENT, onToggle)
|
|
1346
|
+
return () => window.removeEventListener(WORKFLOW_GROUP_TOGGLE_EVENT, onToggle)
|
|
1347
|
+
}, [handleToggleGroupCollapsed])
|
|
265
1348
|
|
|
266
1349
|
// Delete node
|
|
267
1350
|
const handleDeleteNode = useCallback(async (nodeId: string) => {
|
|
268
|
-
|
|
1351
|
+
const annotation = nodesRef.current.find((candidate) => candidate.id === nodeId && isAnnotationNode(candidate))
|
|
1352
|
+
if (annotation) {
|
|
1353
|
+
await handleDeleteAnnotation(nodeId)
|
|
1354
|
+
return
|
|
1355
|
+
}
|
|
1356
|
+
const before = captureDocument()
|
|
1357
|
+
const deleted = await performDeleteNodeFlow(nodeId, {
|
|
269
1358
|
nodes,
|
|
270
1359
|
confirm,
|
|
271
1360
|
t,
|
|
@@ -275,102 +1364,746 @@ export default function VisualEditorPage() {
|
|
|
275
1364
|
setEdges,
|
|
276
1365
|
notifyDeleted: () => flash('Step deleted successfully', 'success'),
|
|
277
1366
|
})
|
|
278
|
-
|
|
1367
|
+
if (deleted) commitCapturedDocument(before, historyLabels.deleteStep)
|
|
1368
|
+
}, [confirm, nodes, t, captureDocument, commitCapturedDocument, historyLabels.deleteStep, handleDeleteAnnotation])
|
|
1369
|
+
|
|
1370
|
+
// In-place step type conversion (spec 4.5, #4237). The step keeps its id,
|
|
1371
|
+
// name, position and every incoming/outgoing route; config the new type
|
|
1372
|
+
// cannot execute is quarantined by `convertStepType` and stays visible in the
|
|
1373
|
+
// inspector. The dialog closes before the confirm so only one modal is on
|
|
1374
|
+
// screen (same reason as the delete flow).
|
|
1375
|
+
const handleConvertNodeType = useCallback(async (nodeId: string, targetType: ConvertibleStepType) => {
|
|
1376
|
+
if (isCodeOnly) return
|
|
1377
|
+
const node = nodesRef.current.find((candidate) => candidate.id === nodeId)
|
|
1378
|
+
if (!node) return
|
|
1379
|
+
|
|
1380
|
+
const before = captureDocument()
|
|
1381
|
+
const result = convertStepType({ type: node.type, data: node.data as Record<string, unknown> }, targetType)
|
|
1382
|
+
if (!result.ok) {
|
|
1383
|
+
flash(t('workflows.stepConversion.unavailable', 'This step type cannot be changed.'), 'error')
|
|
1384
|
+
return
|
|
1385
|
+
}
|
|
279
1386
|
|
|
280
|
-
|
|
1387
|
+
setShowNodeDialog(false)
|
|
1388
|
+
setSelectedNode(null)
|
|
1389
|
+
const confirmed = await confirm({
|
|
1390
|
+
title: t('workflows.stepConversion.confirmTitle', 'Change step type?'),
|
|
1391
|
+
text: result.quarantined.length > 0
|
|
1392
|
+
? t(
|
|
1393
|
+
'workflows.stepConversion.confirmWithQuarantine',
|
|
1394
|
+
'The step keeps its name, position and routes. {count} setting(s) the new type cannot execute move to Unmapped configuration: {keys}',
|
|
1395
|
+
{ count: String(result.quarantined.length), keys: result.quarantined.join(', ') },
|
|
1396
|
+
)
|
|
1397
|
+
: t('workflows.stepConversion.confirm', 'The step keeps its name, position and routes.'),
|
|
1398
|
+
})
|
|
1399
|
+
if (!confirmed) return
|
|
1400
|
+
|
|
1401
|
+
commitCapturedDocument(before, historyLabels.convert)
|
|
1402
|
+
setNodes((nds) =>
|
|
1403
|
+
nds.map((candidate) =>
|
|
1404
|
+
candidate.id === nodeId
|
|
1405
|
+
? { ...candidate, type: result.type, data: { ...result.data, badge: getBadgeForNodeType(result.type) } }
|
|
1406
|
+
: candidate,
|
|
1407
|
+
),
|
|
1408
|
+
)
|
|
1409
|
+
scheduleAutosave()
|
|
1410
|
+
flash(
|
|
1411
|
+
result.quarantined.length > 0
|
|
1412
|
+
? t('workflows.stepConversion.convertedWithQuarantine', 'Step type changed; {count} setting(s) parked as unmapped configuration', { count: String(result.quarantined.length) })
|
|
1413
|
+
: t('workflows.stepConversion.converted', 'Step type changed'),
|
|
1414
|
+
result.quarantined.length > 0 ? 'warning' : 'success',
|
|
1415
|
+
)
|
|
1416
|
+
}, [isCodeOnly, confirm, captureDocument, commitCapturedDocument, historyLabels.convert, scheduleAutosave, t])
|
|
1417
|
+
|
|
1418
|
+
// Inline node delete: a node's trash button dispatches WORKFLOW_NODE_DELETE_EVENT
|
|
1419
|
+
// (decoupled from the node component); route it through the same confirm +
|
|
1420
|
+
// edge-cleanup flow as the edit dialog's delete. No-op in read-only mode.
|
|
1421
|
+
useEffect(() => {
|
|
1422
|
+
if (isCodeOnly) return
|
|
1423
|
+
const onNodeDelete = (event: Event) => {
|
|
1424
|
+
const nodeId = (event as CustomEvent<{ nodeId?: string }>).detail?.nodeId
|
|
1425
|
+
if (typeof nodeId === 'string') void handleDeleteNode(nodeId)
|
|
1426
|
+
}
|
|
1427
|
+
window.addEventListener(WORKFLOW_NODE_DELETE_EVENT, onNodeDelete)
|
|
1428
|
+
return () => window.removeEventListener(WORKFLOW_NODE_DELETE_EVENT, onNodeDelete)
|
|
1429
|
+
}, [isCodeOnly, handleDeleteNode])
|
|
1430
|
+
|
|
1431
|
+
useEffect(() => {
|
|
1432
|
+
const onChipOpen = (event: Event) => {
|
|
1433
|
+
const detail = (event as CustomEvent<RouteChipEventDetail>).detail
|
|
1434
|
+
if (detail?.edgeId) handleRouteChipOpen(detail.edgeId, detail.section)
|
|
1435
|
+
}
|
|
1436
|
+
window.addEventListener(WORKFLOW_ROUTE_CHIP_EVENT, onChipOpen)
|
|
1437
|
+
return () => window.removeEventListener(WORKFLOW_ROUTE_CHIP_EVENT, onChipOpen)
|
|
1438
|
+
}, [handleRouteChipOpen])
|
|
1439
|
+
|
|
1440
|
+
useEffect(() => {
|
|
1441
|
+
const onActivityOpen = (event: Event) => {
|
|
1442
|
+
if (isCodeOnly) return
|
|
1443
|
+
const detail = (event as CustomEvent<RouteActivityEventDetail>).detail
|
|
1444
|
+
if (detail?.edgeId && detail?.activityId) {
|
|
1445
|
+
setActivityEdit({ edgeId: detail.edgeId, activityId: detail.activityId })
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
window.addEventListener(WORKFLOW_ROUTE_ACTIVITY_EVENT, onActivityOpen)
|
|
1449
|
+
return () => window.removeEventListener(WORKFLOW_ROUTE_ACTIVITY_EVENT, onActivityOpen)
|
|
1450
|
+
}, [isCodeOnly])
|
|
1451
|
+
|
|
1452
|
+
// Handle new connections. A drop onto a sub-workflow IN port authors a field
|
|
1453
|
+
// mapping (written to the target step's config.inputMapping + a distinct data
|
|
1454
|
+
// edge); a plain handle-to-handle connection stays a control-flow transition.
|
|
281
1455
|
const handleConnect = useCallback((connection: Connection) => {
|
|
282
|
-
const
|
|
283
|
-
|
|
1456
|
+
const classification = classifyConnection(connection)
|
|
1457
|
+
|
|
1458
|
+
if (classification.kind === 'data-ignored') {
|
|
1459
|
+
return
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
if (classification.kind === 'data-mapping') {
|
|
1463
|
+
const { targetNodeId, childPortKey, parentPath } = classification
|
|
1464
|
+
commitHistory(historyLabels.connect)
|
|
1465
|
+
setNodes((nds) => applyInputMappingToNodes(nds, targetNodeId, childPortKey, parentPath))
|
|
1466
|
+
const dataEdge = buildDataMappingEdge(connection, childPortKey)
|
|
1467
|
+
setEdges((eds) => appendWorkflowEdge(eds.filter((e) => e.id !== dataEdge.id), dataEdge))
|
|
1468
|
+
return
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// A connection drawn from one of a node's kinded output handles authors that
|
|
1472
|
+
// kind of route (spec 5.9 error routes, SLA-breach routes, …): normal
|
|
1473
|
+
// routing never selects one, the engine follows it only down its own path.
|
|
1474
|
+
const routeKind = findRouteKindDescriptorForHandle(connection.sourceHandle)
|
|
1475
|
+
const sourceNode = nodesRef.current.find((node) => node.id === connection.source)
|
|
1476
|
+
const decisions = sourceNode?.data.decisions as DecisionRowLike[] | undefined
|
|
1477
|
+
const decisionTransitionId = isDecisionSourceHandle(decisions, connection.sourceHandle)
|
|
1478
|
+
? connection.sourceHandle
|
|
1479
|
+
: null
|
|
1480
|
+
if (
|
|
1481
|
+
decisionTransitionId &&
|
|
1482
|
+
edgesRef.current.some(
|
|
1483
|
+
(edge) =>
|
|
1484
|
+
edge.source === connection.source &&
|
|
1485
|
+
(edge.id === decisionTransitionId || edge.sourceHandle === decisionTransitionId),
|
|
1486
|
+
)
|
|
1487
|
+
) {
|
|
1488
|
+
return
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const routeData = routeKind
|
|
1492
|
+
? {
|
|
1493
|
+
kind: routeKind.kind,
|
|
1494
|
+
...routeKind.discriminatorFields({
|
|
1495
|
+
sourceHandle: connection.sourceHandle,
|
|
1496
|
+
}),
|
|
1497
|
+
}
|
|
1498
|
+
: {}
|
|
1499
|
+
|
|
1500
|
+
// A drawn route is the same object a saved one loads as, so it takes the
|
|
1501
|
+
// workflow edge renderer (a curve) immediately rather than on the next
|
|
1502
|
+
// reload — see `lib/route-edge.ts`.
|
|
1503
|
+
const newEdge: Edge = buildWorkflowRouteEdge({
|
|
1504
|
+
id: decisionTransitionId ?? generateTransitionId(),
|
|
284
1505
|
source: connection.source!,
|
|
285
1506
|
target: connection.target!,
|
|
286
|
-
|
|
1507
|
+
sourceHandle: routeKind ? connection.sourceHandle : decisionTransitionId,
|
|
287
1508
|
data: {
|
|
288
1509
|
trigger: 'auto',
|
|
289
1510
|
preConditions: [],
|
|
290
1511
|
postConditions: [],
|
|
291
1512
|
activities: [],
|
|
292
1513
|
label: '',
|
|
1514
|
+
...routeData,
|
|
293
1515
|
},
|
|
294
|
-
}
|
|
1516
|
+
})
|
|
295
1517
|
|
|
1518
|
+
commitHistory(historyLabels.connect)
|
|
296
1519
|
setEdges((eds) => appendWorkflowEdge(eds, newEdge))
|
|
297
|
-
}, [])
|
|
1520
|
+
}, [commitHistory, historyLabels.connect])
|
|
1521
|
+
|
|
1522
|
+
// Route reattachment (#4233): dropping an existing route endpoint on another
|
|
1523
|
+
// node re-targets it. The route keeps its durable transitionId, so its label,
|
|
1524
|
+
// condition, activities and priority travel with it. A refused target leaves
|
|
1525
|
+
// the edge list untouched, which is what snaps the endpoint back — the reason
|
|
1526
|
+
// is surfaced instead of silently reverting.
|
|
1527
|
+
const describeReattachRejection = useCallback((rejection: EdgeReattachRejection) => {
|
|
1528
|
+
switch (rejection.code) {
|
|
1529
|
+
case 'selfLoop':
|
|
1530
|
+
return t('workflows.reattach.rejected.selfLoop', 'A route cannot start and end on the same step.')
|
|
1531
|
+
case 'duplicateRoute':
|
|
1532
|
+
return t('workflows.reattach.rejected.duplicateRoute', 'A route already connects those two steps.')
|
|
1533
|
+
case 'dataMappingRoute':
|
|
1534
|
+
return t('workflows.reattach.rejected.dataMappingRoute', 'Data mapping links are edited in the step configuration, not on the canvas.')
|
|
1535
|
+
case 'errorHandleUnsupported':
|
|
1536
|
+
return t('workflows.reattach.rejected.errorHandleUnsupported', 'An error route can only start at a step that can fail.')
|
|
1537
|
+
case 'kindHandleUnsupported':
|
|
1538
|
+
return t('workflows.reattach.rejected.kindHandleUnsupported', 'This route cannot start at that type of step.')
|
|
1539
|
+
case 'graphInvalid':
|
|
1540
|
+
return t('workflows.reattach.rejected.graphInvalid', 'That target would break the workflow: {reason}', { reason: rejection.detail ?? '' })
|
|
1541
|
+
case 'forkJoinInvalid':
|
|
1542
|
+
return t('workflows.reattach.rejected.forkJoinInvalid', 'That target would break the parallel branch structure: {reason}', { reason: rejection.detail ?? '' })
|
|
1543
|
+
default:
|
|
1544
|
+
return t('workflows.reattach.rejected.unsupported', 'That route cannot be re-targeted.')
|
|
1545
|
+
}
|
|
1546
|
+
}, [t])
|
|
1547
|
+
|
|
1548
|
+
const handleReconnect = useCallback((oldEdge: Edge, connection: Connection) => {
|
|
1549
|
+
if (isCodeOnly) return
|
|
1550
|
+
const result = reattachWorkflowEdge(nodesRef.current, edgesRef.current, oldEdge.id, connection)
|
|
1551
|
+
if (!result.ok) {
|
|
1552
|
+
flash(describeReattachRejection(result), 'error')
|
|
1553
|
+
return
|
|
1554
|
+
}
|
|
1555
|
+
commitHistory(historyLabels.reattach)
|
|
1556
|
+
setEdges(result.edges)
|
|
1557
|
+
scheduleAutosave()
|
|
1558
|
+
flash(t('workflows.reattach.retargeted', 'Route re-targeted'), 'success')
|
|
1559
|
+
}, [isCodeOnly, describeReattachRejection, commitHistory, historyLabels.reattach, scheduleAutosave, t])
|
|
1560
|
+
|
|
1561
|
+
// Typed trigger contextMapping targets (spec section 3.1, step 1.9): the
|
|
1562
|
+
// ledger stays pure, so trigger event payload contracts are pre-resolved
|
|
1563
|
+
// here from the already-fetched declared-events list (which carries
|
|
1564
|
+
// payloadSchema since step 1.7) and passed in as plain data. Schema-less or
|
|
1565
|
+
// wildcard triggers get no contract and their mapping targets stay unknown.
|
|
1566
|
+
// Handler-step candidates for the definition-level error handler: every step
|
|
1567
|
+
// that can actually receive the run (START/END are not recovery targets).
|
|
1568
|
+
const errorHandlerStepOptions = useMemo(
|
|
1569
|
+
() =>
|
|
1570
|
+
nodes
|
|
1571
|
+
.filter((node) => node.type !== 'start' && node.type !== 'end')
|
|
1572
|
+
.map((node) => ({
|
|
1573
|
+
stepId: node.id,
|
|
1574
|
+
label: typeof node.data?.label === 'string' && node.data.label ? node.data.label : node.id,
|
|
1575
|
+
})),
|
|
1576
|
+
[nodes],
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
const { events: availableEvents } = useAvailableEvents()
|
|
1580
|
+
const triggerPayloadContracts = useMemo(
|
|
1581
|
+
() => buildTriggerPayloadContracts(triggers, availableEvents),
|
|
1582
|
+
[triggers, availableEvents],
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
// Ledger entries for the variable picker in the open edit dialog (spec
|
|
1586
|
+
// section 3.5, step 3.2). Computed lazily — only while a dialog is open —
|
|
1587
|
+
// with the same client-side 'unknown'-contract ledger the Problems warnings
|
|
1588
|
+
// use, so the picker never offers a path the ref checker would then flag.
|
|
1589
|
+
// Node dialogs get the edited step's incoming view; edge dialogs get the
|
|
1590
|
+
// TARGET step's incoming view, matching the transition scope rule in
|
|
1591
|
+
// lib/expression-refs.ts.
|
|
1592
|
+
const dialogLedger = useMemo(() => {
|
|
1593
|
+
if (!showNodeDialog && !showEdgeDialog) return null
|
|
1594
|
+
try {
|
|
1595
|
+
const definitionData = buildDefinitionPayload({
|
|
1596
|
+
graphDefinition: graphToDefinition(nodes, edges),
|
|
1597
|
+
triggers,
|
|
1598
|
+
contextSchema,
|
|
1599
|
+
io: definitionIo,
|
|
1600
|
+
})
|
|
1601
|
+
return computeClientContextLedger(definitionData, triggerPayloadContracts)
|
|
1602
|
+
} catch {
|
|
1603
|
+
return null
|
|
1604
|
+
}
|
|
1605
|
+
}, [showNodeDialog, showEdgeDialog, nodes, edges, triggers, contextSchema, definitionIo, triggerPayloadContracts])
|
|
1606
|
+
|
|
1607
|
+
// A step nobody has wired yet has no incoming route, so the fixpoint leaves
|
|
1608
|
+
// its ledger empty and the Input data panel opened on nothing at all (issue
|
|
1609
|
+
// #5988). What such a step will receive once it is wired is exactly what the
|
|
1610
|
+
// run starts with — the START step's ledger IS that initial context, since it
|
|
1611
|
+
// has no incoming transitions either — so an unwired step borrows it rather
|
|
1612
|
+
// than showing the author an empty picker.
|
|
1613
|
+
const nodeDialogLedgerEntries = useMemo(() => {
|
|
1614
|
+
if (!dialogLedger || !selectedNode) return undefined
|
|
1615
|
+
const stepEntries = dialogLedger.steps[selectedNode.id]?.entries
|
|
1616
|
+
if (stepEntries && stepEntries.length > 0) return stepEntries
|
|
1617
|
+
const startNode = nodes.find((candidate) => candidate.type === 'start')
|
|
1618
|
+
const startEntries = startNode ? dialogLedger.steps[startNode.id]?.entries : undefined
|
|
1619
|
+
return startEntries && startEntries.length > 0 ? startEntries : stepEntries
|
|
1620
|
+
}, [dialogLedger, nodes, selectedNode])
|
|
1621
|
+
|
|
1622
|
+
// Pinned per-step samples carried inside metadata.editor.samples (spec
|
|
1623
|
+
// section 3.6, step 4.4). The page owns the metadata object, so pin/unpin
|
|
1624
|
+
// write through setLoadedMetadata and the explicit Save (and draft
|
|
1625
|
+
// autosave) persist them via buildMetadataPayload's spread.
|
|
1626
|
+
const editorSamples = useMemo(() => {
|
|
1627
|
+
const editorValue = loadedMetadata?.editor
|
|
1628
|
+
if (!editorValue || typeof editorValue !== 'object' || Array.isArray(editorValue)) return undefined
|
|
1629
|
+
const samplesValue = (editorValue as Record<string, unknown>).samples
|
|
1630
|
+
if (!samplesValue || typeof samplesValue !== 'object' || Array.isArray(samplesValue)) return undefined
|
|
1631
|
+
return samplesValue as Record<string, PinnedSampleEnvelope>
|
|
1632
|
+
}, [loadedMetadata])
|
|
1633
|
+
|
|
1634
|
+
const handlePinSample = useCallback((stepId: string, data: unknown) => {
|
|
1635
|
+
commitHistory(historyLabels.pinSample)
|
|
1636
|
+
setLoadedMetadata((previous) => {
|
|
1637
|
+
const base: Record<string, unknown> = { ...(previous ?? {}) }
|
|
1638
|
+
const editorValue = base.editor
|
|
1639
|
+
const editor: Record<string, unknown> =
|
|
1640
|
+
editorValue && typeof editorValue === 'object' && !Array.isArray(editorValue)
|
|
1641
|
+
? { ...(editorValue as Record<string, unknown>) }
|
|
1642
|
+
: {}
|
|
1643
|
+
const samplesValue = editor.samples
|
|
1644
|
+
const samples: Record<string, unknown> =
|
|
1645
|
+
samplesValue && typeof samplesValue === 'object' && !Array.isArray(samplesValue)
|
|
1646
|
+
? { ...(samplesValue as Record<string, unknown>) }
|
|
1647
|
+
: {}
|
|
1648
|
+
samples[stepId] = { pinnedAt: new Date().toISOString(), source: 'test', data }
|
|
1649
|
+
editor.samples = samples
|
|
1650
|
+
base.editor = editor
|
|
1651
|
+
return base
|
|
1652
|
+
})
|
|
1653
|
+
flash(t('workflows.testStep.pinnedFlash', 'Sample pinned — it is stored with the definition on save'), 'success')
|
|
1654
|
+
}, [commitHistory, historyLabels.pinSample, t])
|
|
1655
|
+
|
|
1656
|
+
const handleUnpinSample = useCallback((stepId: string) => {
|
|
1657
|
+
commitHistory(historyLabels.unpinSample)
|
|
1658
|
+
setLoadedMetadata((previous) => {
|
|
1659
|
+
if (!previous) return previous
|
|
1660
|
+
const editorValue = previous.editor
|
|
1661
|
+
if (!editorValue || typeof editorValue !== 'object' || Array.isArray(editorValue)) return previous
|
|
1662
|
+
const editor = { ...(editorValue as Record<string, unknown>) }
|
|
1663
|
+
const samplesValue = editor.samples
|
|
1664
|
+
if (!samplesValue || typeof samplesValue !== 'object' || Array.isArray(samplesValue)) return previous
|
|
1665
|
+
const samples = { ...(samplesValue as Record<string, unknown>) }
|
|
1666
|
+
if (!(stepId in samples)) return previous
|
|
1667
|
+
delete samples[stepId]
|
|
1668
|
+
if (Object.keys(samples).length > 0) editor.samples = samples
|
|
1669
|
+
else delete editor.samples
|
|
1670
|
+
const base = { ...previous }
|
|
1671
|
+
if (Object.keys(editor).length > 0) base.editor = editor
|
|
1672
|
+
else delete base.editor
|
|
1673
|
+
return Object.keys(base).length > 0 ? base : null
|
|
1674
|
+
})
|
|
1675
|
+
}, [commitHistory, historyLabels.unpinSample])
|
|
1676
|
+
|
|
1677
|
+
// Named START contexts (spec section 8.1). They live beside samples in
|
|
1678
|
+
// metadata.editor, but they are a DIFFERENT thing — a whole start context,
|
|
1679
|
+
// not per-step data — so they get their own key and their own cap.
|
|
1680
|
+
const startFixtures = useMemo(() => {
|
|
1681
|
+
const editorValue = loadedMetadata?.editor
|
|
1682
|
+
if (!editorValue || typeof editorValue !== 'object' || Array.isArray(editorValue)) return undefined
|
|
1683
|
+
const fixturesValue = (editorValue as Record<string, unknown>).fixtures
|
|
1684
|
+
if (!fixturesValue || typeof fixturesValue !== 'object' || Array.isArray(fixturesValue)) return undefined
|
|
1685
|
+
return fixturesValue as WorkflowStartFixtures
|
|
1686
|
+
}, [loadedMetadata])
|
|
1687
|
+
|
|
1688
|
+
const startFixtureList = useMemo(() => listStartFixtures(startFixtures), [startFixtures])
|
|
1689
|
+
|
|
1690
|
+
const handleSaveFixture = useCallback(() => {
|
|
1691
|
+
let context: Record<string, unknown>
|
|
1692
|
+
try {
|
|
1693
|
+
const parsed = startContext.trim() ? JSON.parse(startContext) : {}
|
|
1694
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
1695
|
+
throw new Error('[internal] context must be a JSON object')
|
|
1696
|
+
}
|
|
1697
|
+
context = parsed as Record<string, unknown>
|
|
1698
|
+
} catch {
|
|
1699
|
+
flash(t('workflows.startInstance.invalidJson'), 'error')
|
|
1700
|
+
return
|
|
1701
|
+
}
|
|
298
1702
|
|
|
299
|
-
|
|
300
|
-
|
|
1703
|
+
const result = upsertStartFixture(startFixtures, fixtureName, context, new Date().toISOString())
|
|
1704
|
+
if (!result.ok) {
|
|
1705
|
+
flash(t(`workflows.fixtures.errors.${result.reason}`), 'error')
|
|
1706
|
+
return
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
commitHistory(historyLabels.saveFixture)
|
|
1710
|
+
setLoadedMetadata((previous) => {
|
|
1711
|
+
const base: Record<string, unknown> = { ...(previous ?? {}) }
|
|
1712
|
+
const editorValue = base.editor
|
|
1713
|
+
const editor: Record<string, unknown> =
|
|
1714
|
+
editorValue && typeof editorValue === 'object' && !Array.isArray(editorValue)
|
|
1715
|
+
? { ...(editorValue as Record<string, unknown>) }
|
|
1716
|
+
: {}
|
|
1717
|
+
editor.fixtures = result.fixtures
|
|
1718
|
+
base.editor = editor
|
|
1719
|
+
return base
|
|
1720
|
+
})
|
|
1721
|
+
setFixtureName('')
|
|
1722
|
+
flash(t('workflows.fixtures.saved'), 'success')
|
|
1723
|
+
}, [startContext, startFixtures, fixtureName, commitHistory, historyLabels.saveFixture, t])
|
|
1724
|
+
|
|
1725
|
+
const handleApplyFixture = useCallback((name: string) => {
|
|
1726
|
+
const fixture = startFixtures?.[name]
|
|
1727
|
+
if (!fixture) return
|
|
1728
|
+
setStartContext(JSON.stringify(fixture.context, null, 2))
|
|
1729
|
+
setFixtureName(fixture.name)
|
|
1730
|
+
}, [startFixtures])
|
|
1731
|
+
|
|
1732
|
+
const handleDeleteFixture = useCallback((name: string) => {
|
|
1733
|
+
commitHistory(historyLabels.deleteFixture)
|
|
1734
|
+
setLoadedMetadata((previous) => {
|
|
1735
|
+
if (!previous) return previous
|
|
1736
|
+
const editorValue = previous.editor
|
|
1737
|
+
if (!editorValue || typeof editorValue !== 'object' || Array.isArray(editorValue)) return previous
|
|
1738
|
+
const editor = { ...(editorValue as Record<string, unknown>) }
|
|
1739
|
+
const next = removeStartFixture(editor.fixtures as WorkflowStartFixtures | undefined, name)
|
|
1740
|
+
if (Object.keys(next).length > 0) editor.fixtures = next
|
|
1741
|
+
else delete editor.fixtures
|
|
1742
|
+
const base = { ...previous }
|
|
1743
|
+
if (Object.keys(editor).length > 0) base.editor = editor
|
|
1744
|
+
else delete base.editor
|
|
1745
|
+
return Object.keys(base).length > 0 ? base : null
|
|
1746
|
+
})
|
|
1747
|
+
}, [commitHistory, historyLabels.deleteFixture])
|
|
1748
|
+
|
|
1749
|
+
const edgeDialogLedgerEntries = useMemo(
|
|
1750
|
+
() => (dialogLedger && selectedEdge ? dialogLedger.steps[selectedEdge.target]?.entries : undefined),
|
|
1751
|
+
[dialogLedger, selectedEdge],
|
|
1752
|
+
)
|
|
1753
|
+
|
|
1754
|
+
// Bridge the pure issue collector to the page's i18n. `t` already accepts a
|
|
1755
|
+
// fallback plus params, so the flow-logic messages stay translatable without
|
|
1756
|
+
// the collector depending on React.
|
|
1757
|
+
const translateIssue = useCallback<WorkflowIssueTranslator>(
|
|
1758
|
+
(key, fallback, params) => t(key, fallback, params),
|
|
1759
|
+
[t],
|
|
1760
|
+
)
|
|
1761
|
+
|
|
1762
|
+
// Collect every graph and schema issue for the current document. Shared by the
|
|
1763
|
+
// explicit Validate action and the Code view's JSON-schema validation display
|
|
1764
|
+
// (spec §2.2), so both surfaces can never disagree about what is wrong.
|
|
1765
|
+
const evaluateWorkflowIssues = useCallback((): WorkflowValidationIssue[] => {
|
|
301
1766
|
const graphErrors = validateWorkflowGraph(nodes, edges)
|
|
302
|
-
|
|
1767
|
+
let zodIssues: ZodIssueLike[] = []
|
|
1768
|
+
let configWarnings: ZodIssueLike[] = []
|
|
1769
|
+
let schemaFailureMessage: string | null = null
|
|
1770
|
+
let flowLogicDefinition: WorkflowDefinitionData | null = null
|
|
1771
|
+
let flowLogicLedger: ReturnType<typeof computeClientContextLedger> | undefined
|
|
303
1772
|
|
|
304
|
-
// Run Zod schema validation
|
|
305
1773
|
try {
|
|
306
1774
|
const definitionData = graphToDefinition(nodes, edges, { includePositions: true })
|
|
307
1775
|
const result = workflowDefinitionDataSchema.safeParse(definitionData)
|
|
308
|
-
|
|
309
1776
|
if (!result.success) {
|
|
310
|
-
|
|
311
|
-
result.error.issues.forEach((issue) => {
|
|
312
|
-
allErrors.push({
|
|
313
|
-
type: 'error',
|
|
314
|
-
message: `Schema validation: ${issue.path.join('.')} - ${issue.message}`,
|
|
315
|
-
})
|
|
316
|
-
})
|
|
1777
|
+
zodIssues = result.error.issues
|
|
317
1778
|
}
|
|
1779
|
+
const ledgerDefinition = buildDefinitionPayload({ graphDefinition: definitionData, triggers, contextSchema, io: definitionIo, interpolation, errorHandler })
|
|
1780
|
+
flowLogicDefinition = ledgerDefinition
|
|
1781
|
+
flowLogicLedger = computeClientContextLedger(ledgerDefinition, triggerPayloadContracts)
|
|
1782
|
+
configWarnings = [
|
|
1783
|
+
...collectActivityConfigWarnings(definitionData),
|
|
1784
|
+
...collectContextRefWarnings(ledgerDefinition, t, triggerPayloadContracts),
|
|
1785
|
+
]
|
|
318
1786
|
} catch (error) {
|
|
319
|
-
|
|
320
|
-
type: 'error',
|
|
321
|
-
message: `Schema validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
322
|
-
})
|
|
1787
|
+
schemaFailureMessage = error instanceof Error ? error.message : String(error)
|
|
323
1788
|
}
|
|
324
1789
|
|
|
325
|
-
|
|
326
|
-
|
|
1790
|
+
const issues = collectValidationIssues({
|
|
1791
|
+
graphErrors,
|
|
1792
|
+
zodIssues,
|
|
1793
|
+
configWarnings,
|
|
1794
|
+
nodes,
|
|
1795
|
+
edges,
|
|
1796
|
+
definition: flowLogicDefinition,
|
|
1797
|
+
ledger: flowLogicLedger,
|
|
1798
|
+
translate: translateIssue,
|
|
1799
|
+
})
|
|
1800
|
+
if (schemaFailureMessage) {
|
|
1801
|
+
issues.unshift({
|
|
1802
|
+
id: 'schema-exception',
|
|
1803
|
+
severity: 'error',
|
|
1804
|
+
message: t('workflows.visualEditor.problems.schemaValidationFailed', 'Schema validation failed: {message}', { message: schemaFailureMessage }),
|
|
1805
|
+
})
|
|
1806
|
+
}
|
|
1807
|
+
return issues
|
|
1808
|
+
}, [nodes, edges, triggers, contextSchema, definitionIo, interpolation, errorHandler, triggerPayloadContracts, translateIssue, t])
|
|
1809
|
+
|
|
1810
|
+
// Validate workflow — collect every graph and schema issue into the problems
|
|
1811
|
+
// panel. Shared by the explicit Validate action and by the "Problems pass
|
|
1812
|
+
// already run" an applied AI draft owes the author (spec §9).
|
|
1813
|
+
const runProblemsPass = useCallback(() => {
|
|
1814
|
+
const issues = evaluateWorkflowIssues()
|
|
1815
|
+
setProblems(issues)
|
|
1816
|
+
|
|
1817
|
+
if (issues.length === 0) {
|
|
1818
|
+
setShowProblems(false)
|
|
1819
|
+
flash(t('workflows.visualEditor.problems.validationPassed', 'Validation passed! Your workflow is valid and ready to save.'), 'success')
|
|
327
1820
|
} else {
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
:
|
|
334
|
-
|
|
1821
|
+
setShowProblems(true)
|
|
1822
|
+
setProblemsCollapsed(false)
|
|
1823
|
+
const { errors, warnings } = countIssuesBySeverity(issues)
|
|
1824
|
+
flash(
|
|
1825
|
+
t('workflows.visualEditor.problems.summary', 'Validation found {errors} error(s) and {warnings} warning(s).', { errors, warnings }),
|
|
1826
|
+
errors > 0 ? 'error' : 'warning',
|
|
1827
|
+
)
|
|
1828
|
+
}
|
|
1829
|
+
}, [evaluateWorkflowIssues, t])
|
|
1830
|
+
|
|
1831
|
+
const handleValidate = runProblemsPass
|
|
1832
|
+
|
|
1833
|
+
// Code view, stage 1 (spec §2.2). The JSON is assembled through the same
|
|
1834
|
+
// builders Save uses, so what an author reads is exactly what is persisted;
|
|
1835
|
+
// it is only computed while the panel is open.
|
|
1836
|
+
const codeViewJson = useMemo(() => {
|
|
1837
|
+
if (!showCodeView) return ''
|
|
1838
|
+
try {
|
|
1839
|
+
const definitionData = buildDefinitionPayload({
|
|
1840
|
+
graphDefinition: graphToDefinition(nodes, edges, { includePositions: true }),
|
|
1841
|
+
triggers,
|
|
1842
|
+
contextSchema,
|
|
1843
|
+
io: definitionIo,
|
|
1844
|
+
interpolation,
|
|
1845
|
+
errorHandler,
|
|
1846
|
+
})
|
|
1847
|
+
return JSON.stringify(definitionData, null, 2)
|
|
1848
|
+
} catch (error) {
|
|
1849
|
+
return t(
|
|
1850
|
+
'workflows.visualEditor.codeView.assemblyFailed',
|
|
1851
|
+
'The definition could not be assembled: {message}',
|
|
1852
|
+
{ message: error instanceof Error ? error.message : String(error) },
|
|
1853
|
+
)
|
|
335
1854
|
}
|
|
336
|
-
}, [nodes, edges])
|
|
1855
|
+
}, [showCodeView, nodes, edges, triggers, contextSchema, definitionIo, interpolation, errorHandler, t])
|
|
1856
|
+
|
|
1857
|
+
const codeViewIssues = useMemo(
|
|
1858
|
+
() => (showCodeView ? evaluateWorkflowIssues() : []),
|
|
1859
|
+
[showCodeView, evaluateWorkflowIssues],
|
|
1860
|
+
)
|
|
1861
|
+
|
|
1862
|
+
// Code view stage 2 (spec §2.2). Safety model, stated once:
|
|
1863
|
+
// canvas → code is LIVE (the panel re-renders from the canvas whenever the
|
|
1864
|
+
// author has not started editing); code → canvas needs an explicit Apply.
|
|
1865
|
+
// Parsing, validation and the gutter markers stay live as you type, so the
|
|
1866
|
+
// feedback loop is immediate even though the commit is not — mutating the
|
|
1867
|
+
// canvas per keystroke would delete every node the moment "steps" is
|
|
1868
|
+
// mid-rename, and push one undo entry per character.
|
|
1869
|
+
const [codeDraft, setCodeDraft] = useState<string | null>(null)
|
|
1870
|
+
const codeDraftText = codeDraft ?? codeViewJson
|
|
1871
|
+
|
|
1872
|
+
// The ONE gate a whole definition passes before it may replace the document.
|
|
1873
|
+
// Extracted so the Code view's Apply and the AI draft's Apply (spec §9) share
|
|
1874
|
+
// it verbatim — an AI-authored graph has earned exactly as much trust as a
|
|
1875
|
+
// pasted one, which is to say none.
|
|
1876
|
+
const definitionApplyGate = useMemo(() => ({
|
|
1877
|
+
parseDefinition: (parsed: unknown) => {
|
|
1878
|
+
const result = workflowDefinitionDataSchema.safeParse(parsed)
|
|
1879
|
+
if (result.success) return { ok: true as const, definition: result.data as WorkflowDefinitionData }
|
|
1880
|
+
return {
|
|
1881
|
+
ok: false as const,
|
|
1882
|
+
messages: result.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`),
|
|
1883
|
+
}
|
|
1884
|
+
},
|
|
1885
|
+
// Warnings never block, exactly as they never block a Save; only graph
|
|
1886
|
+
// ERRORS do, because a canvas holding a graph the engine rejects is worse
|
|
1887
|
+
// than no apply — the author has lost the text they typed.
|
|
1888
|
+
// `autoLayout: false` on purpose: whether a definition is VALID cannot
|
|
1889
|
+
// depend on running a layout engine over it, and skipping dagre keeps this
|
|
1890
|
+
// memo cheap enough to run on every keystroke.
|
|
1891
|
+
validateGraph: (definition: WorkflowDefinitionData) => {
|
|
1892
|
+
const graph = definitionToGraph(definition, { autoLayout: false })
|
|
1893
|
+
return validateWorkflowGraph(graph.nodes, graph.edges)
|
|
1894
|
+
.filter((issue) => issue.type === 'error')
|
|
1895
|
+
.map((issue) => issue.message)
|
|
1896
|
+
},
|
|
1897
|
+
}), [])
|
|
1898
|
+
|
|
1899
|
+
const codeApplyDecision = useMemo(
|
|
1900
|
+
() => evaluateCodeViewDraft<WorkflowDefinitionData>({
|
|
1901
|
+
draftText: codeDraftText,
|
|
1902
|
+
canvasText: codeViewJson,
|
|
1903
|
+
...definitionApplyGate,
|
|
1904
|
+
}),
|
|
1905
|
+
[codeDraftText, codeViewJson, definitionApplyGate],
|
|
1906
|
+
)
|
|
1907
|
+
|
|
1908
|
+
const codeDraftStatus = useMemo(() => describeCodeViewDraft(codeApplyDecision), [codeApplyDecision])
|
|
1909
|
+
|
|
1910
|
+
const codeIssueLocations = useMemo(() => {
|
|
1911
|
+
if (!showCodeView) return { severityByLine: new Map<number, 'error' | 'warning'>(), lineByIssueId: new Map<string, number>() }
|
|
1912
|
+
let parsed: unknown
|
|
1913
|
+
try {
|
|
1914
|
+
parsed = JSON.parse(codeDraftText)
|
|
1915
|
+
} catch {
|
|
1916
|
+
// Unparseable text has no entity positions to point at; the parse-error
|
|
1917
|
+
// line is marked on its own by `codeDraftStatus`.
|
|
1918
|
+
return { severityByLine: new Map<number, 'error' | 'warning'>(), lineByIssueId: new Map<string, number>() }
|
|
1919
|
+
}
|
|
1920
|
+
const locations = locateDefinitionJsonEntities(codeDraftText, parsed)
|
|
1921
|
+
const located = locateIssues(codeViewIssues, locations)
|
|
1922
|
+
return {
|
|
1923
|
+
severityByLine: severityByLine(located),
|
|
1924
|
+
lineByIssueId: new Map(located.map((issue) => [issue.issueId, issue.line])),
|
|
1925
|
+
}
|
|
1926
|
+
}, [showCodeView, codeDraftText, codeViewIssues])
|
|
1927
|
+
|
|
1928
|
+
// Replace the whole editor document from a validated definition, as ONE undo
|
|
1929
|
+
// entry named `label`. Shared by the Code view's Apply and the AI draft's
|
|
1930
|
+
// Apply so a twenty-node generation is exactly as reversible as a paste.
|
|
1931
|
+
const applyDefinitionDocument = useCallback((definition: WorkflowDefinitionData, label: string) => {
|
|
1932
|
+
// Committed BEFORE the replacement, so undo restores the graph AND the
|
|
1933
|
+
// panel fields the applied definition also carried.
|
|
1934
|
+
commitHistory(label)
|
|
1935
|
+
const graph = definitionToGraph(definition, { autoLayout: true })
|
|
1936
|
+
setNodes(graph.nodes)
|
|
1937
|
+
setEdges(graph.edges)
|
|
1938
|
+
setTriggers(definition.triggers ?? [])
|
|
1939
|
+
// The zod-parsed shapes leave the optional field flags optional; the editor
|
|
1940
|
+
// state types have them resolved. The same narrowing the draft-restore path
|
|
1941
|
+
// uses applies here.
|
|
1942
|
+
setContextSchema(definition.contextSchema as WorkflowContextSchema | undefined)
|
|
1943
|
+
setDefinitionIo(definition.io as WorkflowIoContract | undefined)
|
|
1944
|
+
setInterpolation(definition.interpolation)
|
|
1945
|
+
setErrorHandler(definition.errorHandler)
|
|
1946
|
+
scheduleAutosave()
|
|
1947
|
+
}, [commitHistory, scheduleAutosave])
|
|
1948
|
+
|
|
1949
|
+
const handleApplyCodeDraft = useCallback(() => {
|
|
1950
|
+
if (!codeApplyDecision.ok) return
|
|
1951
|
+
applyDefinitionDocument(codeApplyDecision.definition, historyLabels.applyCode)
|
|
1952
|
+
setCodeDraft(null)
|
|
1953
|
+
flash(t('workflows.visualEditor.codeView.applied'), 'success')
|
|
1954
|
+
}, [codeApplyDecision, applyDefinitionDocument, historyLabels.applyCode, t])
|
|
1955
|
+
|
|
1956
|
+
// ---------------------------------------------------------------------
|
|
1957
|
+
// Prompt-to-draft (spec §9)
|
|
1958
|
+
//
|
|
1959
|
+
// The generated definition NEVER reaches the canvas by a path of its own: it
|
|
1960
|
+
// goes through `evaluateCodeViewDraft`, the same gate a pasted definition
|
|
1961
|
+
// passes, and lands through `applyDefinitionDocument`, the same replacement
|
|
1962
|
+
// the Code view's Apply performs. So a refused draft leaves the canvas
|
|
1963
|
+
// byte-identical, and an applied one — however many nodes it minted — is ONE
|
|
1964
|
+
// named entry on the existing undo stack.
|
|
1965
|
+
//
|
|
1966
|
+
// Nothing here writes to the server. The route that generated the draft
|
|
1967
|
+
// persisted nothing either; Save and Enable stay exactly where they were.
|
|
1968
|
+
// ---------------------------------------------------------------------
|
|
1969
|
+
const [showAiDraft, setShowAiDraft] = useState(false)
|
|
1970
|
+
const [problemsPassToken, setProblemsPassToken] = useState(0)
|
|
1971
|
+
|
|
1972
|
+
// The pass has to observe the APPLIED graph, and `evaluateWorkflowIssues`
|
|
1973
|
+
// closes over the node/edge state, so it runs on the commit after the apply
|
|
1974
|
+
// rather than inside it. The ref keeps it the page's ONE evaluator.
|
|
1975
|
+
const runProblemsPassRef = React.useRef(runProblemsPass)
|
|
1976
|
+
runProblemsPassRef.current = runProblemsPass
|
|
1977
|
+
useEffect(() => {
|
|
1978
|
+
if (problemsPassToken === 0) return
|
|
1979
|
+
runProblemsPassRef.current()
|
|
1980
|
+
}, [problemsPassToken])
|
|
1981
|
+
|
|
1982
|
+
const handleApplyAiDraft = useCallback((input: {
|
|
1983
|
+
definition: Record<string, unknown>
|
|
1984
|
+
prompt: string
|
|
1985
|
+
}): WorkflowAiDraftApplyRefusal | null => {
|
|
1986
|
+
const decision = evaluateCodeViewDraft<WorkflowDefinitionData>({
|
|
1987
|
+
// Serialized so the AI path and the Code view path are literally the same
|
|
1988
|
+
// call. It also proves the draft survives the JSON round trip the Code
|
|
1989
|
+
// view and every save will put it through.
|
|
1990
|
+
draftText: JSON.stringify(input.definition, null, 2),
|
|
1991
|
+
...definitionApplyGate,
|
|
1992
|
+
})
|
|
1993
|
+
|
|
1994
|
+
if (!decision.ok) {
|
|
1995
|
+
switch (decision.reason) {
|
|
1996
|
+
case 'parseError':
|
|
1997
|
+
return { reason: 'parseError', messages: [decision.message] }
|
|
1998
|
+
case 'schemaError':
|
|
1999
|
+
case 'graphError':
|
|
2000
|
+
return { reason: decision.reason, messages: decision.messages }
|
|
2001
|
+
default:
|
|
2002
|
+
// `unchanged` cannot occur — no `canvasText` is supplied — but a
|
|
2003
|
+
// refusal must never fall through into a silent apply.
|
|
2004
|
+
return {
|
|
2005
|
+
reason: 'schemaError',
|
|
2006
|
+
messages: [t('workflows.aiDraft.refusedUnknown', 'The generated definition was refused.')],
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
applyDefinitionDocument(
|
|
2012
|
+
decision.definition,
|
|
2013
|
+
buildAiCheckpointLabel(input.prompt, {
|
|
2014
|
+
prefix: t('workflows.visualEditor.history.aiDraft', 'AI draft'),
|
|
2015
|
+
}),
|
|
2016
|
+
)
|
|
2017
|
+
setProblemsPassToken((token) => token + 1)
|
|
2018
|
+
flash(
|
|
2019
|
+
t('workflows.aiDraft.applied', 'AI draft applied. Review the problems, then save when you are happy with it.'),
|
|
2020
|
+
'success',
|
|
2021
|
+
)
|
|
2022
|
+
return null
|
|
2023
|
+
}, [definitionApplyGate, applyDefinitionDocument, t])
|
|
2024
|
+
|
|
2025
|
+
const handleRevertCodeDraft = useCallback(() => {
|
|
2026
|
+
setCodeDraft(null)
|
|
2027
|
+
}, [])
|
|
2028
|
+
|
|
2029
|
+
// Closing the panel discards an unapplied draft: keeping it would let the
|
|
2030
|
+
// author reopen the view onto text that no longer describes the canvas, with
|
|
2031
|
+
// no signal that the two had diverged.
|
|
2032
|
+
const handleCloseCodeView = useCallback(() => {
|
|
2033
|
+
setCodeDraft(null)
|
|
2034
|
+
setShowCodeView(false)
|
|
2035
|
+
}, [])
|
|
2036
|
+
|
|
2037
|
+
const handleCopyDefinitionJson = useCallback(async () => {
|
|
2038
|
+
try {
|
|
2039
|
+
await navigator.clipboard.writeText(codeViewJson)
|
|
2040
|
+
flash(t('workflows.visualEditor.codeView.copied', 'Definition JSON copied'), 'success')
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
logger.warn('Workflow definition JSON copy was blocked by the browser', { err: error })
|
|
2043
|
+
flash(
|
|
2044
|
+
t(
|
|
2045
|
+
'workflows.visualEditor.codeView.copyUnavailable',
|
|
2046
|
+
'The browser blocked clipboard access, so nothing was copied — select the JSON and copy it manually.',
|
|
2047
|
+
),
|
|
2048
|
+
'warning',
|
|
2049
|
+
)
|
|
2050
|
+
}
|
|
2051
|
+
}, [codeViewJson, t])
|
|
2052
|
+
|
|
2053
|
+
// Focus the offending node or edge on the canvas when a problem row is clicked
|
|
2054
|
+
const handleProblemClick = useCallback((issue: WorkflowValidationIssue) => {
|
|
2055
|
+
if (!issue.nodeId && !issue.edgeId) return
|
|
2056
|
+
focusRequestRef.current += 1
|
|
2057
|
+
setFocusTarget({
|
|
2058
|
+
...(issue.nodeId ? { nodeId: issue.nodeId } : { edgeId: issue.edgeId }),
|
|
2059
|
+
requestId: focusRequestRef.current,
|
|
2060
|
+
})
|
|
2061
|
+
}, [])
|
|
337
2062
|
|
|
338
2063
|
// Save workflow definition
|
|
339
2064
|
const handleSave = useCallback(async () => {
|
|
340
|
-
// Validate required fields
|
|
2065
|
+
// Validate required fields. They live in the details drawer, so open it —
|
|
2066
|
+
// an error toast pointing at fields the author cannot see is a dead end.
|
|
341
2067
|
if (!workflowId || !workflowName) {
|
|
342
|
-
|
|
2068
|
+
setMetadataOpen(true)
|
|
2069
|
+
flash(t('workflows.visualEditor.metadata.requiredFields', 'Workflow ID and name are required.'), 'error')
|
|
343
2070
|
return
|
|
344
2071
|
}
|
|
345
2072
|
|
|
346
|
-
// Validate workflow structure
|
|
347
|
-
const
|
|
348
|
-
const criticalErrors = errors.filter(e => e.type === 'error')
|
|
349
|
-
if (criticalErrors.length > 0) {
|
|
350
|
-
flash(`Cannot save: ${criticalErrors.length} validation error(s) found. Please fix them first.`, 'error')
|
|
351
|
-
return
|
|
352
|
-
}
|
|
2073
|
+
// Validate workflow structure and schema, surfacing every issue in the problems panel
|
|
2074
|
+
const graphErrors = validateWorkflowGraph(nodes, edges)
|
|
353
2075
|
|
|
354
|
-
// Generate definition data and
|
|
355
|
-
|
|
356
|
-
const definitionData = {
|
|
357
|
-
|
|
358
|
-
triggers
|
|
359
|
-
|
|
2076
|
+
// Generate definition data and re-attach triggers, contextSchema, io, and
|
|
2077
|
+
// the interpolation mode
|
|
2078
|
+
const definitionData = buildDefinitionPayload({
|
|
2079
|
+
graphDefinition: graphToDefinition(nodes, edges, { includePositions: true }),
|
|
2080
|
+
triggers,
|
|
2081
|
+
contextSchema,
|
|
2082
|
+
io: definitionIo,
|
|
2083
|
+
interpolation,
|
|
2084
|
+
errorHandler,
|
|
2085
|
+
})
|
|
360
2086
|
|
|
361
|
-
// Run Zod schema validation before saving. Report every issue, not just the
|
|
362
|
-
// first: a save rejected for one missing activity field used to look like
|
|
363
|
-
// "nothing happened" once the operator fixed that field and hit a second
|
|
364
|
-
// one (#4232). Paths are humanized (steps.2.activities.0.config.endpoint →
|
|
365
|
-
// step 3 › activity 1 › endpoint) so the message points at the node to open.
|
|
366
2087
|
const schemaResult = workflowDefinitionDataSchema.safeParse(definitionData)
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
2088
|
+
const issues = collectValidationIssues({
|
|
2089
|
+
graphErrors,
|
|
2090
|
+
zodIssues: schemaResult.success ? [] : schemaResult.error.issues,
|
|
2091
|
+
configWarnings: [
|
|
2092
|
+
...collectActivityConfigWarnings(definitionData),
|
|
2093
|
+
...collectContextRefWarnings(definitionData, t, triggerPayloadContracts),
|
|
2094
|
+
],
|
|
2095
|
+
nodes,
|
|
2096
|
+
edges,
|
|
2097
|
+
definition: definitionData,
|
|
2098
|
+
ledger: computeClientContextLedger(definitionData, triggerPayloadContracts),
|
|
2099
|
+
translate: translateIssue,
|
|
2100
|
+
})
|
|
2101
|
+
setProblems(issues)
|
|
2102
|
+
const { errors } = countIssuesBySeverity(issues)
|
|
2103
|
+
if (errors > 0) {
|
|
2104
|
+
setShowProblems(true)
|
|
2105
|
+
setProblemsCollapsed(false)
|
|
2106
|
+
flash(t('workflows.visualEditor.problems.saveBlocked', 'Cannot save: {count} validation error(s) found. Please fix them first.', { count: errors }), 'error')
|
|
374
2107
|
return
|
|
375
2108
|
}
|
|
376
2109
|
|
|
@@ -378,35 +2111,34 @@ export default function VisualEditorPage() {
|
|
|
378
2111
|
|
|
379
2112
|
try {
|
|
380
2113
|
|
|
381
|
-
const
|
|
382
|
-
if (category) metadata.category = category
|
|
383
|
-
if (tags.length > 0) metadata.tags = tags
|
|
384
|
-
if (icon) metadata.icon = icon
|
|
2114
|
+
const metadataPayload = buildMetadataPayload({ loadedMetadata, tags, category, icon, definition: definitionData, annotations })
|
|
385
2115
|
|
|
386
2116
|
// Determine if creating new or updating existing
|
|
387
2117
|
const isUpdate = !!definitionId
|
|
388
2118
|
|
|
389
2119
|
let result
|
|
2120
|
+
let updateBody: DefinitionUpdateBody | null = null
|
|
390
2121
|
if (isUpdate) {
|
|
391
2122
|
// Update existing definition — send the full editable payload so metadata
|
|
392
2123
|
// edits (name, description, version, category, tags, icon, effective
|
|
393
2124
|
// dates) actually persist. Previously only `definition` + `enabled`
|
|
394
2125
|
// were sent, silently dropping every other field.
|
|
2126
|
+
updateBody = {
|
|
2127
|
+
workflowName,
|
|
2128
|
+
description: description || null,
|
|
2129
|
+
version,
|
|
2130
|
+
definition: definitionData,
|
|
2131
|
+
metadata: metadataPayload,
|
|
2132
|
+
enabled,
|
|
2133
|
+
effectiveFrom: effectiveFrom || null,
|
|
2134
|
+
effectiveTo: effectiveTo || null,
|
|
2135
|
+
}
|
|
395
2136
|
result = await withScopedApiRequestHeaders(
|
|
396
2137
|
buildOptimisticLockHeader(updatedAt),
|
|
397
2138
|
() => apiCall<{ data: any; error?: string }>(`/api/workflows/definitions/${definitionId}`, {
|
|
398
2139
|
method: 'PUT',
|
|
399
2140
|
headers: { 'Content-Type': 'application/json' },
|
|
400
|
-
body: JSON.stringify(
|
|
401
|
-
workflowName,
|
|
402
|
-
description: description || null,
|
|
403
|
-
version,
|
|
404
|
-
definition: definitionData,
|
|
405
|
-
metadata: Object.keys(metadata).length > 0 ? metadata : null,
|
|
406
|
-
enabled,
|
|
407
|
-
effectiveFrom: effectiveFrom || null,
|
|
408
|
-
effectiveTo: effectiveTo || null,
|
|
409
|
-
}),
|
|
2141
|
+
body: JSON.stringify(updateBody),
|
|
410
2142
|
}),
|
|
411
2143
|
)
|
|
412
2144
|
} else {
|
|
@@ -420,7 +2152,7 @@ export default function VisualEditorPage() {
|
|
|
420
2152
|
description: description || null,
|
|
421
2153
|
version,
|
|
422
2154
|
definition: definitionData,
|
|
423
|
-
metadata:
|
|
2155
|
+
metadata: metadataPayload,
|
|
424
2156
|
enabled,
|
|
425
2157
|
effectiveFrom: effectiveFrom || null,
|
|
426
2158
|
effectiveTo: effectiveTo || null,
|
|
@@ -429,32 +2161,475 @@ export default function VisualEditorPage() {
|
|
|
429
2161
|
}
|
|
430
2162
|
|
|
431
2163
|
if (!result.ok) {
|
|
432
|
-
const
|
|
2164
|
+
const activeInstanceCount = readStructuralEditConflictCount(result.status, result.result)
|
|
2165
|
+
if (activeInstanceCount !== null && updateBody) {
|
|
2166
|
+
setStructuralConflict({ activeInstanceCount, payload: updateBody })
|
|
2167
|
+
return
|
|
2168
|
+
}
|
|
2169
|
+
const conflictError = Object.assign(new Error(t('workflows.messages.saveFailed', 'Failed to save')), {
|
|
2170
|
+
status: result.status,
|
|
2171
|
+
...(result.result && typeof result.result === 'object' ? result.result : {}),
|
|
2172
|
+
})
|
|
2173
|
+
if (!surfaceRecordConflict(conflictError, t)) {
|
|
2174
|
+
flash(formatWorkflowValidationError(result.result, t('workflows.messages.saveFailed', 'Failed to save')), 'error')
|
|
2175
|
+
}
|
|
2176
|
+
return
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
setStructuralConflict(null)
|
|
2180
|
+
if (updateBody) lastAutosavedBodyRef.current = stableSerializeDefinition(updateBody)
|
|
2181
|
+
|
|
2182
|
+
const savedDefinition = result.result?.data
|
|
2183
|
+
|
|
2184
|
+
// Explicit Save promoted the working copy: drop the per-user draft
|
|
2185
|
+
// (fire-and-forget) and suspend autosave so a pending debounce can't
|
|
2186
|
+
// recreate it before the redirect.
|
|
2187
|
+
draftSuspendedRef.current = true
|
|
2188
|
+
setPendingDraft(null)
|
|
2189
|
+
setDraftSavedAt(null)
|
|
2190
|
+
setDraftSaveFailed(false)
|
|
2191
|
+
if (isUpdate && isServerDraftEligible(definitionId)) {
|
|
2192
|
+
void apiCall(`/api/workflows/definitions/${definitionId}/draft`, { method: 'DELETE' }).catch(() => undefined)
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
flash(
|
|
2196
|
+
isUpdate
|
|
2197
|
+
? t('workflows.messages.workflowUpdated', 'Workflow updated successfully')
|
|
2198
|
+
: t('workflows.messages.workflowCreated', 'Workflow created successfully'),
|
|
2199
|
+
'success',
|
|
2200
|
+
)
|
|
2201
|
+
|
|
2202
|
+
// Stay on the visual editor after saving. On update, refresh the local
|
|
2203
|
+
// optimistic-lock token so the next save keeps working. On create, switch
|
|
2204
|
+
// the editor into edit mode by pointing the URL at the new id — the load
|
|
2205
|
+
// effect then re-syncs from the persisted definition without leaving the
|
|
2206
|
+
// canvas.
|
|
2207
|
+
if (isUpdate) {
|
|
2208
|
+
if (typeof savedDefinition?.updatedAt === 'string') {
|
|
2209
|
+
setUpdatedAt(savedDefinition.updatedAt)
|
|
2210
|
+
}
|
|
2211
|
+
} else if (savedDefinition?.id) {
|
|
2212
|
+
router.replace(`/backend/definitions/visual-editor?id=${encodeURIComponent(savedDefinition.id)}`)
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
} catch (error) {
|
|
2216
|
+
logger.error('Error saving workflow definition', { err: error })
|
|
2217
|
+
flash(t('workflows.messages.saveFailed', 'Failed to save'), 'error')
|
|
2218
|
+
} finally {
|
|
2219
|
+
setIsSaving(false)
|
|
2220
|
+
}
|
|
2221
|
+
}, [nodes, edges, workflowId, workflowName, description, version, enabled, category, tags, icon, effectiveFrom, effectiveTo, triggers, contextSchema, definitionIo, interpolation, errorHandler, loadedMetadata, annotations, definitionId, updatedAt, router, t])
|
|
2222
|
+
|
|
2223
|
+
// ── Non-pointer authoring path (spec §4.6) ────────────────────────────────
|
|
2224
|
+
// Every canvas operation is reachable from the keyboard: the command palette
|
|
2225
|
+
// reaches all of them, and the direct bindings below cover the ones an author
|
|
2226
|
+
// performs constantly (open, delete, nudge).
|
|
2227
|
+
|
|
2228
|
+
const focusNode = useCallback((nodeId: string) => {
|
|
2229
|
+
focusRequestRef.current += 1
|
|
2230
|
+
setFocusTarget({ nodeId, requestId: focusRequestRef.current })
|
|
2231
|
+
}, [])
|
|
2232
|
+
|
|
2233
|
+
const openSelectedInspector = useCallback(() => {
|
|
2234
|
+
const selected = nodesRef.current.find((node) => node.selected)
|
|
2235
|
+
if (!selected) {
|
|
2236
|
+
const selectedEdgeNode = edgesRef.current.find((edge) => edge.selected)
|
|
2237
|
+
if (!selectedEdgeNode || isCodeOnly) return
|
|
2238
|
+
setSelectedEdge(selectedEdgeNode)
|
|
2239
|
+
setSelectedNode(null)
|
|
2240
|
+
setEdgeDialogFocusFieldId(null)
|
|
2241
|
+
setShowEdgeDialog(true)
|
|
2242
|
+
return
|
|
2243
|
+
}
|
|
2244
|
+
handleNodeClick({} as React.MouseEvent, selected)
|
|
2245
|
+
}, [isCodeOnly, handleNodeClick])
|
|
2246
|
+
|
|
2247
|
+
// Del removes whatever is selected. Each removal runs its own confirm +
|
|
2248
|
+
// cleanup flow, so a multi-selection deletes exactly as clicking each trash
|
|
2249
|
+
// button would — and each one is undoable.
|
|
2250
|
+
const handleDeleteSelection = useCallback(async () => {
|
|
2251
|
+
if (isCodeOnly) return
|
|
2252
|
+
const nodeIds = selectedNodeIds(nodesRef.current)
|
|
2253
|
+
const edgeIds = edgesRef.current.filter((edge) => edge.selected).map((edge) => edge.id)
|
|
2254
|
+
if (nodeIds.length === 0 && edgeIds.length === 0) {
|
|
2255
|
+
flash(t('workflows.visualEditor.keyboard.nothingSelected', 'Select a step or a route first'), 'info')
|
|
2256
|
+
return
|
|
2257
|
+
}
|
|
2258
|
+
for (const edgeId of edgeIds) await handleDeleteEdge(edgeId)
|
|
2259
|
+
for (const nodeId of nodeIds) await handleDeleteNode(nodeId)
|
|
2260
|
+
}, [isCodeOnly, handleDeleteEdge, handleDeleteNode, t])
|
|
2261
|
+
|
|
2262
|
+
// Arrow-key nudging follows the drag rule (#4248): a burst of keystrokes is
|
|
2263
|
+
// ONE arrangement, so the baseline is captured on the first press and the undo
|
|
2264
|
+
// entry plus the autosave land once the burst settles.
|
|
2265
|
+
const nudgeBaselineRef = React.useRef<WorkflowEditorDocument | null>(null)
|
|
2266
|
+
const nudgeCommitTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
2267
|
+
|
|
2268
|
+
const handleNudge = useCallback((key: string, coarse: boolean) => {
|
|
2269
|
+
if (isCodeOnly) return false
|
|
2270
|
+
const direction = resolveNudgeDirection(key)
|
|
2271
|
+
if (!direction) return false
|
|
2272
|
+
if (selectedNodeIds(nodesRef.current).length === 0) return false
|
|
2273
|
+
if (!nudgeBaselineRef.current) nudgeBaselineRef.current = captureDocument()
|
|
2274
|
+
setNodes((nds) => nudgeSelectedNodes(nds, nudgeOffset(direction, coarse)))
|
|
2275
|
+
if (nudgeCommitTimerRef.current) clearTimeout(nudgeCommitTimerRef.current)
|
|
2276
|
+
nudgeCommitTimerRef.current = setTimeout(() => {
|
|
2277
|
+
nudgeCommitTimerRef.current = null
|
|
2278
|
+
const baseline = nudgeBaselineRef.current
|
|
2279
|
+
nudgeBaselineRef.current = null
|
|
2280
|
+
if (baseline) commitCapturedDocument(baseline, historyLabels.move)
|
|
2281
|
+
scheduleAutosave()
|
|
2282
|
+
}, NUDGE_COMMIT_DELAY_MS)
|
|
2283
|
+
return true
|
|
2284
|
+
}, [isCodeOnly, captureDocument, commitCapturedDocument, historyLabels.move, scheduleAutosave])
|
|
2285
|
+
|
|
2286
|
+
useEffect(() => () => {
|
|
2287
|
+
if (nudgeCommitTimerRef.current) clearTimeout(nudgeCommitTimerRef.current)
|
|
2288
|
+
}, [])
|
|
2289
|
+
|
|
2290
|
+
const commandPaletteCommands = useMemo<WorkflowEditorCommand[]>(() => buildWorkflowEditorCommands({
|
|
2291
|
+
readOnly: isCodeOnly,
|
|
2292
|
+
canUndo: canUndoEditorHistory(history),
|
|
2293
|
+
canRedo: canRedoEditorHistory(history),
|
|
2294
|
+
hasNodes: nodes.length > 0,
|
|
2295
|
+
hasSelection: nodes.some((node) => node.selected) || edges.some((edge) => edge.selected),
|
|
2296
|
+
canRunTest: !!definitionId,
|
|
2297
|
+
stepTypes: PALETTE_NODE_TYPES.map((nodeType) => ({
|
|
2298
|
+
nodeType,
|
|
2299
|
+
typeLabel: NODE_TYPE_LABELS[nodeType]?.title ?? nodeType,
|
|
2300
|
+
})),
|
|
2301
|
+
steps: nodes
|
|
2302
|
+
.filter((node) => !isAnnotationNode(node))
|
|
2303
|
+
.map((node) => ({
|
|
2304
|
+
id: node.id,
|
|
2305
|
+
label: typeof node.data?.label === 'string' && node.data.label ? node.data.label : node.id,
|
|
2306
|
+
typeLabel: NODE_TYPE_LABELS[(node.type ?? '') as keyof typeof NODE_TYPE_LABELS]?.title ?? node.type ?? '',
|
|
2307
|
+
})),
|
|
2308
|
+
labels: {
|
|
2309
|
+
undo: t('workflows.commandPalette.undo', 'Undo'),
|
|
2310
|
+
redo: t('workflows.commandPalette.redo', 'Redo'),
|
|
2311
|
+
deleteSelection: t('workflows.commandPalette.delete', 'Delete selection'),
|
|
2312
|
+
copy: t('workflows.commandPalette.copy', 'Copy selection'),
|
|
2313
|
+
paste: t('workflows.commandPalette.paste', 'Paste'),
|
|
2314
|
+
duplicate: t('workflows.commandPalette.duplicate', 'Duplicate selection'),
|
|
2315
|
+
addStep: (typeLabel: string) => t('workflows.commandPalette.addStep', 'Add step: {type}', { type: typeLabel }),
|
|
2316
|
+
addNote: t('workflows.commandPalette.addNote', 'Add note'),
|
|
2317
|
+
addGroup: t('workflows.commandPalette.addGroup', 'Add group'),
|
|
2318
|
+
goToStep: t('workflows.commandPalette.goToStep', 'Go to step'),
|
|
2319
|
+
tidy: t('workflows.visualEditor.autoArrange'),
|
|
2320
|
+
togglePalette: t('workflows.commandPalette.togglePalette', 'Toggle the step palette'),
|
|
2321
|
+
toggleMetadata: t('workflows.commandPalette.toggleMetadata', 'Toggle the workflow details panel'),
|
|
2322
|
+
toggleFocus: t('workflows.commandPalette.toggleFocus', 'Toggle focus mode'),
|
|
2323
|
+
toggleProblems: t('workflows.commandPalette.toggleProblems', 'Toggle the Problems panel'),
|
|
2324
|
+
toggleCodeView: t('workflows.commandPalette.toggleCodeView', 'Toggle the Code view'),
|
|
2325
|
+
toggleCompensation: t('workflows.commandPalette.toggleCompensation', 'Toggle compensation paths'),
|
|
2326
|
+
validate: t('workflows.visualEditor.validate'),
|
|
2327
|
+
runTest: t('workflows.actions.startInstance'),
|
|
2328
|
+
save: t('workflows.common.save'),
|
|
2329
|
+
aiDraft: t('workflows.aiDraft.open', 'Draft this workflow with AI'),
|
|
2330
|
+
},
|
|
2331
|
+
actions: {
|
|
2332
|
+
undo: handleUndo,
|
|
2333
|
+
redo: handleRedo,
|
|
2334
|
+
deleteSelection: () => { void handleDeleteSelection() },
|
|
2335
|
+
copy: () => { void handleCopySelection() },
|
|
2336
|
+
paste: () => { void handlePaste() },
|
|
2337
|
+
duplicate: handleDuplicateSelection,
|
|
2338
|
+
addStep: (nodeType: string) => handleAddNode(nodeType),
|
|
2339
|
+
addNote: () => handleAddAnnotation('note'),
|
|
2340
|
+
addGroup: () => handleAddAnnotation('group'),
|
|
2341
|
+
goToStep: focusNode,
|
|
2342
|
+
tidy: handleAutoArrange,
|
|
2343
|
+
togglePalette: togglePaletteCollapsed,
|
|
2344
|
+
toggleMetadata: () => setMetadataOpen((open) => !open),
|
|
2345
|
+
toggleFocus,
|
|
2346
|
+
toggleProblems: () => setShowProblems((visible) => !visible),
|
|
2347
|
+
toggleCodeView: () => setShowCodeView((visible) => !visible),
|
|
2348
|
+
toggleCompensation,
|
|
2349
|
+
validate: handleValidate,
|
|
2350
|
+
runTest: () => setStartOpen(true),
|
|
2351
|
+
save: () => { void handleSave() },
|
|
2352
|
+
aiDraft: () => setShowAiDraft(true),
|
|
2353
|
+
},
|
|
2354
|
+
}), [
|
|
2355
|
+
isCodeOnly, history, nodes, edges, definitionId, t,
|
|
2356
|
+
handleUndo, handleRedo, handleDeleteSelection, handleCopySelection, handlePaste, handleDuplicateSelection,
|
|
2357
|
+
handleAddNode, handleAddAnnotation, focusNode, handleAutoArrange, togglePaletteCollapsed, toggleFocus,
|
|
2358
|
+
toggleCompensation, handleValidate, handleSave,
|
|
2359
|
+
])
|
|
2360
|
+
|
|
2361
|
+
// Keyboard shortcuts: Cmd/Ctrl+K opens the command palette, Cmd/Ctrl+Z undoes,
|
|
2362
|
+
// Cmd/Ctrl+Shift+Z redoes, Cmd/Ctrl+C, +V and +D copy, paste and duplicate the
|
|
2363
|
+
// selected subgraph, Enter opens the inspector for the selection, Del removes
|
|
2364
|
+
// it, the arrows nudge it, `F` toggles Focus mode and `Esc` exits it.
|
|
2365
|
+
//
|
|
2366
|
+
// Everything except the palette is suppressed while the user is typing in a
|
|
2367
|
+
// field (a text input keeps its own native undo, copy/paste and caret keys) or
|
|
2368
|
+
// while a dialog is open, so a shortcut never hijacks form input or the
|
|
2369
|
+
// dialog's own Escape-to-close. Cmd+K is deliberately exempt from the typing
|
|
2370
|
+
// guard: it is the way out of any focus, which is the whole point of a palette.
|
|
2371
|
+
useEffect(() => {
|
|
2372
|
+
if (isMobile) return
|
|
2373
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
2374
|
+
const active = document.activeElement as HTMLElement | null
|
|
2375
|
+
const tag = (active?.tagName || '').toLowerCase()
|
|
2376
|
+
const isEditing = tag === 'input' || tag === 'textarea' || tag === 'select' || !!active?.isContentEditable
|
|
2377
|
+
// Docking is a SPATIAL change, not a keyboard one: an open inspector
|
|
2378
|
+
// still owns the shortcuts, because it holds a form with unsaved values
|
|
2379
|
+
// and every canvas binding here either mutates the document under it
|
|
2380
|
+
// (undo, delete, paste) or would save a graph that omits the edit sitting
|
|
2381
|
+
// in the rail. What docking buys is that the canvas stays visible and
|
|
2382
|
+
// clickable — re-targeting is a click, and Escape closes the rail.
|
|
2383
|
+
const isInspectorOpen = showNodeDialog || showEdgeDialog
|
|
2384
|
+
const isDialogOpen = isInspectorOpen || showAnnotationDialog || showClearConfirm || startOpen || showCodeView
|
|
2385
|
+
// The details drawer is a modal overlay, so the canvas bindings must not
|
|
2386
|
+
// fire behind it. Cmd+S is the deliberate exception: the drawer has no
|
|
2387
|
+
// submit of its own to protect — saving the workflow IS its primary
|
|
2388
|
+
// action — so the shortcut keeps working while it is open.
|
|
2389
|
+
const isOverlayOpen = isDialogOpen || metadataOpen || showAiDraft
|
|
2390
|
+
const isCommandKey = (event.metaKey || event.ctrlKey) && !event.altKey
|
|
2391
|
+
|
|
2392
|
+
if (isCommandKey && (event.key === 'k' || event.key === 'K')) {
|
|
2393
|
+
if (isOverlayOpen) return
|
|
2394
|
+
event.preventDefault()
|
|
2395
|
+
setShowCommandPalette((open) => !open)
|
|
2396
|
+
return
|
|
2397
|
+
}
|
|
2398
|
+
if (isCommandKey && !event.shiftKey && (event.key === 's' || event.key === 'S')) {
|
|
2399
|
+
if (isDialogOpen || isCodeOnly) return
|
|
2400
|
+
event.preventDefault()
|
|
2401
|
+
void handleSave()
|
|
2402
|
+
return
|
|
2403
|
+
}
|
|
2404
|
+
if (isEditing || showCommandPalette) return
|
|
2405
|
+
|
|
2406
|
+
if (isCommandKey && (event.key === 'z' || event.key === 'Z')) {
|
|
2407
|
+
if (isOverlayOpen) return
|
|
2408
|
+
event.preventDefault()
|
|
2409
|
+
if (event.shiftKey) handleRedo()
|
|
2410
|
+
else handleUndo()
|
|
2411
|
+
return
|
|
2412
|
+
}
|
|
2413
|
+
if (isCommandKey && !event.shiftKey && !isOverlayOpen) {
|
|
2414
|
+
if (event.key === 'c' || event.key === 'C') {
|
|
2415
|
+
event.preventDefault()
|
|
2416
|
+
void handleCopySelection()
|
|
2417
|
+
return
|
|
2418
|
+
}
|
|
2419
|
+
if (event.key === 'v' || event.key === 'V') {
|
|
2420
|
+
event.preventDefault()
|
|
2421
|
+
void handlePaste()
|
|
2422
|
+
return
|
|
2423
|
+
}
|
|
2424
|
+
if (event.key === 'd' || event.key === 'D') {
|
|
2425
|
+
event.preventDefault()
|
|
2426
|
+
handleDuplicateSelection()
|
|
2427
|
+
return
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
if (event.metaKey || event.ctrlKey || event.altKey) return
|
|
2431
|
+
if (event.key === 'Escape') {
|
|
2432
|
+
// Escape raised inside the rail is claimed by the rail itself. Reaching
|
|
2433
|
+
// here means focus is on the canvas, where Escape should still close the
|
|
2434
|
+
// inspector before it falls through to leaving Focus mode.
|
|
2435
|
+
if (inspectorsDocked && isInspectorOpen) {
|
|
2436
|
+
event.preventDefault()
|
|
2437
|
+
setShowNodeDialog(false)
|
|
2438
|
+
setShowEdgeDialog(false)
|
|
2439
|
+
return
|
|
2440
|
+
}
|
|
2441
|
+
if (focusMode && !isOverlayOpen) {
|
|
2442
|
+
event.preventDefault()
|
|
2443
|
+
setFocusMode(false)
|
|
2444
|
+
}
|
|
2445
|
+
return
|
|
2446
|
+
}
|
|
2447
|
+
if (isOverlayOpen) return
|
|
2448
|
+
if (event.key === 'Enter') {
|
|
2449
|
+
event.preventDefault()
|
|
2450
|
+
openSelectedInspector()
|
|
2451
|
+
return
|
|
2452
|
+
}
|
|
2453
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
2454
|
+
event.preventDefault()
|
|
2455
|
+
void handleDeleteSelection()
|
|
2456
|
+
return
|
|
2457
|
+
}
|
|
2458
|
+
if (resolveNudgeDirection(event.key)) {
|
|
2459
|
+
if (handleNudge(event.key, event.shiftKey)) event.preventDefault()
|
|
2460
|
+
return
|
|
2461
|
+
}
|
|
2462
|
+
if (event.key === 'f' || event.key === 'F') {
|
|
2463
|
+
event.preventDefault()
|
|
2464
|
+
toggleFocus()
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
window.addEventListener('keydown', onKeyDown)
|
|
2468
|
+
return () => window.removeEventListener('keydown', onKeyDown)
|
|
2469
|
+
}, [
|
|
2470
|
+
isMobile, focusMode, inspectorsDocked, showNodeDialog, showEdgeDialog, showAnnotationDialog, showClearConfirm, startOpen,
|
|
2471
|
+
showCodeView, metadataOpen, showAiDraft, showCommandPalette, toggleFocus, setFocusMode, handleUndo, handleRedo, handleCopySelection, handlePaste,
|
|
2472
|
+
handleDuplicateSelection, openSelectedInspector, handleDeleteSelection, handleNudge,
|
|
2473
|
+
handleSave, isCodeOnly,
|
|
2474
|
+
])
|
|
2475
|
+
|
|
2476
|
+
// Quiet autosave routine (no redirect, no success flash). Mirrors the update
|
|
2477
|
+
// branch of `handleSave` exactly — same payload and the same optimistic-lock
|
|
2478
|
+
// header — so dragged positions persist without an explicit Save. Reassigned
|
|
2479
|
+
// every render and invoked through `performAutosaveRef` by the debounced timer
|
|
2480
|
+
// so it always sees the latest nodes/edges/metadata.
|
|
2481
|
+
performAutosaveRef.current = async () => {
|
|
2482
|
+
if (!definitionId || isCodeOnly) return
|
|
2483
|
+
if (!workflowId || !workflowName) return
|
|
2484
|
+
|
|
2485
|
+
const criticalErrors = validateWorkflowGraph(nodes, edges).filter((e) => e.type === 'error')
|
|
2486
|
+
if (criticalErrors.length > 0) return
|
|
2487
|
+
|
|
2488
|
+
// Same payload builders as the explicit Save: the quiet autosave must not
|
|
2489
|
+
// strip contextSchema, io, interpolation, or unedited metadata keys
|
|
2490
|
+
// (editor samples).
|
|
2491
|
+
const definitionData = buildDefinitionPayload({
|
|
2492
|
+
graphDefinition: graphToDefinition(nodes, edges, { includePositions: true }),
|
|
2493
|
+
triggers,
|
|
2494
|
+
contextSchema,
|
|
2495
|
+
io: definitionIo,
|
|
2496
|
+
interpolation,
|
|
2497
|
+
errorHandler,
|
|
2498
|
+
})
|
|
2499
|
+
if (!workflowDefinitionDataSchema.safeParse(definitionData).success) return
|
|
2500
|
+
|
|
2501
|
+
const metadataPayload = buildMetadataPayload({ loadedMetadata, tags, category, icon, definition: definitionData, annotations })
|
|
2502
|
+
|
|
2503
|
+
const updateBody: DefinitionUpdateBody = {
|
|
2504
|
+
workflowName,
|
|
2505
|
+
description: description || null,
|
|
2506
|
+
version,
|
|
2507
|
+
definition: definitionData,
|
|
2508
|
+
metadata: metadataPayload,
|
|
2509
|
+
enabled,
|
|
2510
|
+
effectiveFrom: effectiveFrom || null,
|
|
2511
|
+
effectiveTo: effectiveTo || null,
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// Autosave only when the payload actually differs from what the row already
|
|
2515
|
+
// holds. Without this a selection or a re-render would PUT an identical body
|
|
2516
|
+
// and bump the optimistic-lock token for nothing.
|
|
2517
|
+
const serializedBody = stableSerializeDefinition(updateBody)
|
|
2518
|
+
if (serializedBody === lastAutosavedBodyRef.current) return
|
|
2519
|
+
lastAutosavedBodyRef.current = serializedBody
|
|
2520
|
+
|
|
2521
|
+
setAutosaveState('saving')
|
|
2522
|
+
try {
|
|
2523
|
+
const result = await withScopedApiRequestHeaders(
|
|
2524
|
+
buildOptimisticLockHeader(updatedAt),
|
|
2525
|
+
() => apiCall<{ data: any; error?: string }>(`/api/workflows/definitions/${definitionId}`, {
|
|
2526
|
+
method: 'PUT',
|
|
2527
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2528
|
+
body: JSON.stringify(updateBody),
|
|
2529
|
+
}),
|
|
2530
|
+
)
|
|
2531
|
+
|
|
2532
|
+
if (!result.ok) {
|
|
2533
|
+
setAutosaveState('idle')
|
|
2534
|
+
lastAutosavedBodyRef.current = null
|
|
2535
|
+
// A quiet autosave carrying a structural change hits the same edit-safety
|
|
2536
|
+
// rule as Save. Surfacing the banner here keeps the author from watching
|
|
2537
|
+
// "Saved" never appear with no explanation.
|
|
2538
|
+
const activeInstanceCount = readStructuralEditConflictCount(result.status, result.result)
|
|
2539
|
+
if (activeInstanceCount !== null) {
|
|
2540
|
+
setStructuralConflict({ activeInstanceCount, payload: updateBody })
|
|
2541
|
+
return
|
|
2542
|
+
}
|
|
2543
|
+
const conflictError = Object.assign(new Error('[internal] workflow autosave failed'), {
|
|
433
2544
|
status: result.status,
|
|
434
2545
|
...(result.result && typeof result.result === 'object' ? result.result : {}),
|
|
435
2546
|
})
|
|
436
|
-
|
|
437
|
-
flash(`Failed to save: ${result.result?.error || 'Unknown error'}`, 'error')
|
|
438
|
-
}
|
|
2547
|
+
surfaceRecordConflict(conflictError, t)
|
|
439
2548
|
return
|
|
440
2549
|
}
|
|
441
2550
|
|
|
2551
|
+
setStructuralConflict(null)
|
|
2552
|
+
|
|
442
2553
|
const savedDefinition = result.result?.data
|
|
2554
|
+
if (typeof savedDefinition?.updatedAt === 'string') {
|
|
2555
|
+
setUpdatedAt(savedDefinition.updatedAt)
|
|
2556
|
+
}
|
|
2557
|
+
setAutosaveState('saved')
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
logger.error('workflow autosave failed', {
|
|
2560
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2561
|
+
})
|
|
2562
|
+
lastAutosavedBodyRef.current = null
|
|
2563
|
+
setAutosaveState('idle')
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
443
2566
|
|
|
444
|
-
|
|
2567
|
+
// Fade the "Saved" affordance back to idle shortly after a successful autosave.
|
|
2568
|
+
useEffect(() => {
|
|
2569
|
+
if (autosaveState !== 'saved') return
|
|
2570
|
+
const timer = setTimeout(() => setAutosaveState('idle'), 2000)
|
|
2571
|
+
return () => clearTimeout(timer)
|
|
2572
|
+
}, [autosaveState])
|
|
2573
|
+
|
|
2574
|
+
// Remedy for the edit-safety rule: mint the next version from the stored
|
|
2575
|
+
// definition (the same machinery Publish uses), apply the rejected edit to
|
|
2576
|
+
// that fresh row, and continue editing it. Instances that pinned the previous
|
|
2577
|
+
// row keep executing the definition they started on.
|
|
2578
|
+
const handleCreateVersion = useCallback(async () => {
|
|
2579
|
+
if (!definitionId || !structuralConflict) return
|
|
2580
|
+
setIsCreatingVersion(true)
|
|
2581
|
+
try {
|
|
2582
|
+
const publishResult = await apiCall<{ data?: { id?: string; version?: number; updatedAt?: string }; error?: string }>(
|
|
2583
|
+
`/api/workflows/definitions/${definitionId}/publish`,
|
|
2584
|
+
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) },
|
|
2585
|
+
)
|
|
2586
|
+
const mintedVersion = publishResult.result?.data
|
|
2587
|
+
if (!publishResult.ok || !mintedVersion?.id) {
|
|
2588
|
+
flash(
|
|
2589
|
+
publishResult.result?.error
|
|
2590
|
+
|| t('workflows.visualEditor.editSafety.createVersionFailed', 'Could not create a new version.'),
|
|
2591
|
+
'error',
|
|
2592
|
+
)
|
|
2593
|
+
return
|
|
2594
|
+
}
|
|
445
2595
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
2596
|
+
const nextVersion = mintedVersion.version ?? version
|
|
2597
|
+
const saveResult = await withScopedApiRequestHeaders(
|
|
2598
|
+
buildOptimisticLockHeader(mintedVersion.updatedAt ?? null),
|
|
2599
|
+
() => apiCall<{ data?: { updatedAt?: string }; error?: string }>(
|
|
2600
|
+
`/api/workflows/definitions/${mintedVersion.id}`,
|
|
2601
|
+
{
|
|
2602
|
+
method: 'PUT',
|
|
2603
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2604
|
+
body: JSON.stringify({ ...structuralConflict.payload, version: nextVersion }),
|
|
2605
|
+
},
|
|
2606
|
+
),
|
|
2607
|
+
)
|
|
2608
|
+
if (!saveResult.ok) {
|
|
2609
|
+
flash(
|
|
2610
|
+
formatWorkflowValidationError(
|
|
2611
|
+
saveResult.result,
|
|
2612
|
+
t('workflows.visualEditor.editSafety.createVersionFailed', 'Could not create a new version.'),
|
|
2613
|
+
),
|
|
2614
|
+
'error',
|
|
2615
|
+
)
|
|
2616
|
+
return
|
|
2617
|
+
}
|
|
450
2618
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
2619
|
+
setStructuralConflict(null)
|
|
2620
|
+
setVersion(nextVersion)
|
|
2621
|
+
setUpdatedAt(saveResult.result?.data?.updatedAt ?? null)
|
|
2622
|
+
flash(
|
|
2623
|
+
t('workflows.visualEditor.editSafety.createVersionSucceeded', 'Version {version} created. Your changes were saved there.', {
|
|
2624
|
+
version: String(nextVersion),
|
|
2625
|
+
}),
|
|
2626
|
+
'success',
|
|
2627
|
+
)
|
|
2628
|
+
router.replace(`/backend/definitions/visual-editor?id=${encodeURIComponent(mintedVersion.id)}`)
|
|
454
2629
|
} finally {
|
|
455
|
-
|
|
2630
|
+
setIsCreatingVersion(false)
|
|
456
2631
|
}
|
|
457
|
-
}, [
|
|
2632
|
+
}, [definitionId, structuralConflict, version, router, t])
|
|
458
2633
|
|
|
459
2634
|
// Customize a code-defined workflow → creates an override and reloads the
|
|
460
2635
|
// editor pointed at the new UUID. Mirrors the non-visual edit page button.
|
|
@@ -510,101 +2685,121 @@ export default function VisualEditorPage() {
|
|
|
510
2685
|
}
|
|
511
2686
|
}, [definitionId, workflowId, router, confirm, t])
|
|
512
2687
|
|
|
513
|
-
//
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
2688
|
+
// Start a workflow instance with an initial JSON context, mirroring the
|
|
2689
|
+
// non-visual edit page. Requires a persisted definition (the executor
|
|
2690
|
+
// resolves the definition by workflowId + version).
|
|
2691
|
+
const handleStartInstance = useCallback(async () => {
|
|
2692
|
+
let initialContext: Record<string, unknown>
|
|
2693
|
+
try {
|
|
2694
|
+
const parsed = startContext.trim() ? JSON.parse(startContext) : {}
|
|
2695
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
2696
|
+
throw new Error('[internal] context must be a JSON object')
|
|
2697
|
+
}
|
|
2698
|
+
initialContext = parsed as Record<string, unknown>
|
|
2699
|
+
} catch {
|
|
2700
|
+
flash(t('workflows.startInstance.invalidJson'), 'error')
|
|
520
2701
|
return
|
|
521
2702
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
position: { x: 250, y: 50 },
|
|
543
|
-
data: {
|
|
544
|
-
label: 'Start',
|
|
545
|
-
description: 'Workflow begins',
|
|
546
|
-
status: 'pending',
|
|
547
|
-
badge: 'Start',
|
|
548
|
-
},
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
id: 'step1',
|
|
552
|
-
type: 'userTask',
|
|
553
|
-
position: { x: 250, y: 250 },
|
|
554
|
-
data: {
|
|
555
|
-
label: 'Review Request',
|
|
556
|
-
description: 'User reviews the incoming request',
|
|
557
|
-
status: 'pending',
|
|
558
|
-
stepNumber: 1,
|
|
559
|
-
badge: 'User Task',
|
|
560
|
-
assignedToRoles: ['Reviewer'],
|
|
561
|
-
},
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
id: 'end',
|
|
565
|
-
type: 'end',
|
|
566
|
-
position: { x: 250, y: 450 },
|
|
567
|
-
data: {
|
|
568
|
-
label: 'Complete',
|
|
569
|
-
description: 'Workflow ends',
|
|
570
|
-
status: 'pending',
|
|
571
|
-
badge: 'End',
|
|
572
|
-
},
|
|
573
|
-
},
|
|
574
|
-
]
|
|
575
|
-
|
|
576
|
-
const exampleEdges: Edge[] = [
|
|
577
|
-
{
|
|
578
|
-
id: 'e-start-step1',
|
|
579
|
-
source: 'start',
|
|
580
|
-
target: 'step1',
|
|
581
|
-
type: 'smoothstep',
|
|
582
|
-
data: {
|
|
583
|
-
trigger: 'auto',
|
|
584
|
-
preConditions: [],
|
|
585
|
-
postConditions: [],
|
|
586
|
-
activities: [],
|
|
2703
|
+
setStarting(true)
|
|
2704
|
+
try {
|
|
2705
|
+
const result = await runMutation({
|
|
2706
|
+
operation: async () => {
|
|
2707
|
+
const response = await apiFetch('/api/workflows/instances', {
|
|
2708
|
+
method: 'POST',
|
|
2709
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2710
|
+
body: JSON.stringify({
|
|
2711
|
+
workflowId,
|
|
2712
|
+
version,
|
|
2713
|
+
initialContext,
|
|
2714
|
+
...(startDryRun ? { dryRun: true } : {}),
|
|
2715
|
+
...(startStepThrough ? { stepThrough: true } : {}),
|
|
2716
|
+
}),
|
|
2717
|
+
})
|
|
2718
|
+
if (!response.ok) {
|
|
2719
|
+
const errorBody = await readJsonSafe<{ error?: string }>(response, null)
|
|
2720
|
+
throw new Error(errorBody?.error || t('workflows.startInstance.failed'))
|
|
2721
|
+
}
|
|
2722
|
+
return readJsonSafe<{ data?: { instance?: { id?: string } } }>(response, null)
|
|
587
2723
|
},
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
trigger: 'auto',
|
|
596
|
-
preConditions: [],
|
|
597
|
-
postConditions: [],
|
|
598
|
-
activities: [],
|
|
2724
|
+
mutationPayload: { resourceId: definitionId, operation: 'start-instance' },
|
|
2725
|
+
context: {
|
|
2726
|
+
formId: mutationContextId,
|
|
2727
|
+
resourceKind: 'workflows.instance',
|
|
2728
|
+
resourceId: definitionId,
|
|
2729
|
+
operation: 'start-instance',
|
|
2730
|
+
retryLastMutation,
|
|
599
2731
|
},
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
2732
|
+
})
|
|
2733
|
+
flash(t('workflows.startInstance.started'), 'success')
|
|
2734
|
+
setStartOpen(false)
|
|
2735
|
+
const instanceId = result?.data?.instance?.id
|
|
2736
|
+
if (instanceId) {
|
|
2737
|
+
router.push(`/backend/instances/${instanceId}`)
|
|
2738
|
+
router.refresh()
|
|
2739
|
+
}
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
flash(error instanceof Error ? error.message : t('workflows.startInstance.failed'), 'error')
|
|
2742
|
+
} finally {
|
|
2743
|
+
setStarting(false)
|
|
2744
|
+
}
|
|
2745
|
+
}, [startContext, startDryRun, startStepThrough, workflowId, version, definitionId, mutationContextId, retryLastMutation, router, t])
|
|
2746
|
+
|
|
2747
|
+
// Apply a gallery template to the canvas: populate metadata from the
|
|
2748
|
+
// template's i18n keys and convert its definition into graph nodes/edges.
|
|
2749
|
+
const applyTemplate = useCallback((template: WorkflowTemplateGalleryItem) => {
|
|
2750
|
+
setWorkflowId(template.id.replace(/-/g, '_'))
|
|
2751
|
+
setWorkflowName(t(template.nameKey))
|
|
2752
|
+
setDescription(t(template.descriptionKey))
|
|
2753
|
+
setVersion(1)
|
|
2754
|
+
setEnabled(true)
|
|
2755
|
+
setCategory(template.category)
|
|
2756
|
+
setTags([])
|
|
2757
|
+
setIcon(template.icon)
|
|
2758
|
+
|
|
2759
|
+
const graph = definitionToGraph(template.definition)
|
|
2760
|
+
setNodes(graph.nodes)
|
|
2761
|
+
setEdges(graph.edges)
|
|
2762
|
+
setTriggers(template.definition.triggers || [])
|
|
2763
|
+
setContextSchema(template.definition.contextSchema ?? undefined)
|
|
2764
|
+
setDefinitionIo((template.definition.io ?? undefined) as WorkflowIoContract | undefined)
|
|
2765
|
+
setInterpolation(resolveDefinitionInterpolationMode(template.definition) ?? 'strict')
|
|
2766
|
+
setErrorHandler((template.definition as { errorHandler?: WorkflowErrorHandlerConfig }).errorHandler ?? undefined)
|
|
2767
|
+
setLoadedMetadata(null)
|
|
2768
|
+
resetHistory()
|
|
2769
|
+
flash(t('workflows.visualEditor.templateLoaded', 'Template loaded'), 'success')
|
|
2770
|
+
}, [resetHistory, t])
|
|
2771
|
+
|
|
2772
|
+
// Open the template gallery (replaces the old hardcoded inline example).
|
|
2773
|
+
const handleOpenTemplateGallery = useCallback(() => {
|
|
2774
|
+
setShowTemplateGallery(true)
|
|
606
2775
|
}, [])
|
|
607
2776
|
|
|
2777
|
+
const handleTemplateSelect = useCallback((template: WorkflowTemplateGalleryItem | null) => {
|
|
2778
|
+
if (template) applyTemplate(template)
|
|
2779
|
+
}, [applyTemplate])
|
|
2780
|
+
|
|
2781
|
+
// Populate the canvas from ?template=<id> when creating a new workflow.
|
|
2782
|
+
useEffect(() => {
|
|
2783
|
+
if (definitionId || !templateId) return
|
|
2784
|
+
let cancelled = false
|
|
2785
|
+
const loadTemplate = async () => {
|
|
2786
|
+
const result = await apiCall<{ items?: WorkflowTemplateGalleryItem[]; error?: string }>('/api/workflows/templates')
|
|
2787
|
+
if (cancelled) return
|
|
2788
|
+
const template = result.ok
|
|
2789
|
+
? (result.result?.items || []).find((item) => item.id === templateId)
|
|
2790
|
+
: undefined
|
|
2791
|
+
if (template) {
|
|
2792
|
+
applyTemplate(template)
|
|
2793
|
+
} else {
|
|
2794
|
+
flash(t('workflows.templates.gallery.notFound', 'Template not found'), 'error')
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
void loadTemplate()
|
|
2798
|
+
return () => {
|
|
2799
|
+
cancelled = true
|
|
2800
|
+
}
|
|
2801
|
+
}, [definitionId, templateId, applyTemplate, t])
|
|
2802
|
+
|
|
608
2803
|
// Clear canvas
|
|
609
2804
|
const handleClear = useCallback(() => {
|
|
610
2805
|
if (nodes.length > 0 || edges.length > 0 || workflowId || workflowName) {
|
|
@@ -627,9 +2822,13 @@ export default function VisualEditorPage() {
|
|
|
627
2822
|
setEffectiveFrom('')
|
|
628
2823
|
setEffectiveTo('')
|
|
629
2824
|
setTriggers([])
|
|
2825
|
+
setContextSchema(undefined)
|
|
2826
|
+
setDefinitionIo(undefined)
|
|
2827
|
+
setLoadedMetadata(null)
|
|
630
2828
|
setShowClearConfirm(false)
|
|
2829
|
+
resetHistory()
|
|
631
2830
|
flash('Canvas cleared', 'success')
|
|
632
|
-
}, [])
|
|
2831
|
+
}, [resetHistory])
|
|
633
2832
|
|
|
634
2833
|
// Publish page-load record context to the AppShell-owned `backend:record:current`
|
|
635
2834
|
// mount so the enterprise record_locks widget resolves `workflows.definition` + id
|
|
@@ -660,44 +2859,385 @@ export default function VisualEditorPage() {
|
|
|
660
2859
|
workflowId, workflowName, description, version,
|
|
661
2860
|
enabled, category, tags, icon,
|
|
662
2861
|
effectiveFrom, effectiveTo, triggers,
|
|
2862
|
+
contextSchema, interpolation, errorHandler,
|
|
663
2863
|
}
|
|
664
2864
|
|
|
665
2865
|
const metadataHandlers: WorkflowMetadataHandlers = {
|
|
666
2866
|
setWorkflowId, setWorkflowName, setDescription, setVersion,
|
|
667
2867
|
setEnabled, setCategory, setTags, setIcon,
|
|
668
2868
|
setEffectiveFrom, setEffectiveTo, setTriggers,
|
|
2869
|
+
setContextSchema,
|
|
2870
|
+
setInterpolation,
|
|
2871
|
+
setErrorHandler: (next) => setErrorHandler(next ?? undefined),
|
|
669
2872
|
}
|
|
670
2873
|
|
|
671
|
-
|
|
2874
|
+
// The required identity fields now live behind the details drawer, so the
|
|
2875
|
+
// toolbar has to say when they are still blank.
|
|
2876
|
+
const metadataIncomplete = !isCodeOnly && (!workflowId || !workflowName)
|
|
2877
|
+
|
|
2878
|
+
const inspectorPanels = (
|
|
672
2879
|
<>
|
|
673
|
-
{
|
|
674
|
-
|
|
2880
|
+
{crudFormDialogsEnabled ? (
|
|
2881
|
+
// Both forms are dense (Invoke-Agent, timeout, sub-editors on the step;
|
|
2882
|
+
// condition, mapping and activities on the route) and the 384px docked
|
|
2883
|
+
// rail made them unusable, so each opens as the wide overlay Drawer that
|
|
2884
|
+
// mirrors the definition metadata drawer, with its ledger in a sticky
|
|
2885
|
+
// side column.
|
|
2886
|
+
<NodeEditDialogCrudForm node={selectedNode} isOpen={showNodeDialog} onClose={() => setShowNodeDialog(false)} onSave={handleSaveNode} onDelete={handleDeleteNode} ledgerEntries={nodeDialogLedgerEntries} definitionId={definitionId} samples={editorSamples} onPinSample={handlePinSample} onUnpinSample={handleUnpinSample} branchingRoutes={branchingRoutesValue} onSaveBranchingRoutes={handleSaveBranchingRoutes} routeOrder={routeOrderValue} onSaveRouteOrder={handleSaveRouteOrder} onConvertType={handleConvertNodeType} variant="overlay" wide />
|
|
675
2887
|
) : (
|
|
676
2888
|
<NodeEditDialog node={selectedNode} isOpen={showNodeDialog} onClose={() => setShowNodeDialog(false)} onSave={handleSaveNode} onDelete={handleDeleteNode} />
|
|
677
2889
|
)}
|
|
678
|
-
{
|
|
679
|
-
<EdgeEditDialogCrudForm edge={selectedEdge} isOpen={showEdgeDialog} onClose={() => setShowEdgeDialog(false)} onSave={handleSaveEdge} onDelete={handleDeleteEdge} />
|
|
2890
|
+
{crudFormDialogsEnabled ? (
|
|
2891
|
+
<EdgeEditDialogCrudForm edge={selectedEdge} isOpen={showEdgeDialog} onClose={() => setShowEdgeDialog(false)} onSave={handleSaveEdge} onDelete={handleDeleteEdge} ledgerEntries={edgeDialogLedgerEntries} focusFieldId={edgeDialogFocusFieldId} variant="overlay" wide />
|
|
680
2892
|
) : (
|
|
681
2893
|
<EdgeEditDialog edge={selectedEdge} isOpen={showEdgeDialog} onClose={() => setShowEdgeDialog(false)} onSave={handleSaveEdge} onDelete={handleDeleteEdge} />
|
|
682
2894
|
)}
|
|
683
|
-
<Dialog open={showClearConfirm} onOpenChange={setShowClearConfirm}>
|
|
684
|
-
<DialogContent className="sm:max-w-md">
|
|
685
|
-
<DialogHeader>
|
|
686
|
-
<DialogTitle>{t('workflows.visualEditor.clearTitle')}</DialogTitle>
|
|
687
|
-
<DialogDescription>{t('workflows.visualEditor.clearDescription')}</DialogDescription>
|
|
688
|
-
</DialogHeader>
|
|
689
|
-
<DialogFooter>
|
|
690
|
-
<Button variant="outline" onClick={() => setShowClearConfirm(false)}>{t('common.cancel', 'Cancel')}</Button>
|
|
691
|
-
<Button variant="destructive-solid" onClick={confirmClear}>{t('common.clear', 'Clear')}</Button>
|
|
692
|
-
</DialogFooter>
|
|
693
|
-
</DialogContent>
|
|
694
|
-
</Dialog>
|
|
695
2895
|
</>
|
|
696
2896
|
)
|
|
697
2897
|
|
|
2898
|
+
const sharedDialogs = (
|
|
2899
|
+
<>
|
|
2900
|
+
{/* Docked inspectors render inside the editor's layout row instead, so
|
|
2901
|
+
they shrink the canvas rather than covering it. */}
|
|
2902
|
+
{inspectorsDocked ? null : inspectorPanels}
|
|
2903
|
+
<WorkflowCommandPalette
|
|
2904
|
+
open={showCommandPalette}
|
|
2905
|
+
onOpenChange={setShowCommandPalette}
|
|
2906
|
+
commands={commandPaletteCommands}
|
|
2907
|
+
/>
|
|
2908
|
+
<DefinitionMetadataDrawer
|
|
2909
|
+
open={metadataOpen}
|
|
2910
|
+
onOpenChange={handleMetadataOpenChange}
|
|
2911
|
+
focusSection={metadataFocusSection}
|
|
2912
|
+
definitionId={definitionId}
|
|
2913
|
+
readOnly={isCodeOnly}
|
|
2914
|
+
metadata={metadata}
|
|
2915
|
+
handlers={metadataHandlers}
|
|
2916
|
+
errorHandlerStepOptions={errorHandlerStepOptions}
|
|
2917
|
+
onSave={() => { void handleSave() }}
|
|
2918
|
+
isSaving={isSaving}
|
|
2919
|
+
/>
|
|
2920
|
+
<TriggersDialog
|
|
2921
|
+
open={triggersDialogOpen}
|
|
2922
|
+
onOpenChange={setTriggersDialogOpen}
|
|
2923
|
+
triggers={triggers}
|
|
2924
|
+
onSave={setTriggers}
|
|
2925
|
+
/>
|
|
2926
|
+
{(() => {
|
|
2927
|
+
const edge = activityEdit ? edges.find((candidate) => candidate.id === activityEdit.edgeId) ?? null : null
|
|
2928
|
+
const activities = (edge?.data?.activities as Activity[] | undefined) ?? []
|
|
2929
|
+
const activity = activityEdit ? activities.find((item) => item.activityId === activityEdit.activityId) ?? null : null
|
|
2930
|
+
const labelFor = (nodeId: string) =>
|
|
2931
|
+
(nodes.find((node) => node.id === nodeId)?.data?.label as string | undefined) ?? nodeId
|
|
2932
|
+
const routeLabel = edge ? `${labelFor(edge.source)} → ${labelFor(edge.target)}` : undefined
|
|
2933
|
+
return (
|
|
2934
|
+
<ActivityEditDialog
|
|
2935
|
+
open={!!activity}
|
|
2936
|
+
onOpenChange={(open) => { if (!open) setActivityEdit(null) }}
|
|
2937
|
+
activity={activity}
|
|
2938
|
+
routeLabel={routeLabel}
|
|
2939
|
+
onSave={(updated) => {
|
|
2940
|
+
if (!activityEdit) return
|
|
2941
|
+
handleSaveEdge(activityEdit.edgeId, {
|
|
2942
|
+
activities: activities.map((item) => (item.activityId === updated.activityId ? updated : item)),
|
|
2943
|
+
})
|
|
2944
|
+
}}
|
|
2945
|
+
onRemove={() => {
|
|
2946
|
+
if (!activityEdit) return
|
|
2947
|
+
handleSaveEdge(activityEdit.edgeId, {
|
|
2948
|
+
activities: activities.filter((item) => item.activityId !== activityEdit.activityId),
|
|
2949
|
+
})
|
|
2950
|
+
}}
|
|
2951
|
+
onOpenFullTransition={() => {
|
|
2952
|
+
if (activityEdit) handleRouteChipOpen(activityEdit.edgeId, 'activities')
|
|
2953
|
+
}}
|
|
2954
|
+
/>
|
|
2955
|
+
)
|
|
2956
|
+
})()}
|
|
2957
|
+
<WorkflowCodeView
|
|
2958
|
+
isOpen={showCodeView}
|
|
2959
|
+
onClose={handleCloseCodeView}
|
|
2960
|
+
definitionJson={codeViewJson}
|
|
2961
|
+
issues={codeViewIssues}
|
|
2962
|
+
onCopy={() => { void handleCopyDefinitionJson() }}
|
|
2963
|
+
onPasteSubgraph={() => { void handlePaste() }}
|
|
2964
|
+
canPaste={!isCodeOnly}
|
|
2965
|
+
onIssueClick={handleProblemClick}
|
|
2966
|
+
draftText={codeDraftText}
|
|
2967
|
+
onDraftChange={setCodeDraft}
|
|
2968
|
+
onApply={handleApplyCodeDraft}
|
|
2969
|
+
onRevert={handleRevertCodeDraft}
|
|
2970
|
+
draftStatus={codeDraftStatus}
|
|
2971
|
+
severityByLine={codeIssueLocations.severityByLine}
|
|
2972
|
+
lineByIssueId={codeIssueLocations.lineByIssueId}
|
|
2973
|
+
/>
|
|
2974
|
+
<WorkflowAiDraftDialog
|
|
2975
|
+
open={showAiDraft}
|
|
2976
|
+
onOpenChange={setShowAiDraft}
|
|
2977
|
+
onApply={handleApplyAiDraft}
|
|
2978
|
+
canvasHasContent={nodes.length > 0}
|
|
2979
|
+
/>
|
|
2980
|
+
<AnnotationEditDialog
|
|
2981
|
+
node={selectedAnnotation}
|
|
2982
|
+
isOpen={showAnnotationDialog}
|
|
2983
|
+
onClose={() => setShowAnnotationDialog(false)}
|
|
2984
|
+
onSave={handleSaveAnnotation}
|
|
2985
|
+
onDelete={handleDeleteAnnotation}
|
|
2986
|
+
/>
|
|
2987
|
+
<TemplateGalleryDialog
|
|
2988
|
+
open={showTemplateGallery}
|
|
2989
|
+
onOpenChange={setShowTemplateGallery}
|
|
2990
|
+
onSelect={handleTemplateSelect}
|
|
2991
|
+
/>
|
|
2992
|
+
{/* Clearing the canvas is a yes/no interruption that must block — a rail
|
|
2993
|
+
would make an irreversible action easy to miss. */}
|
|
2994
|
+
<ConfirmDialog
|
|
2995
|
+
open={showClearConfirm}
|
|
2996
|
+
onOpenChange={setShowClearConfirm}
|
|
2997
|
+
title={t('workflows.visualEditor.clearTitle')}
|
|
2998
|
+
text={t('workflows.visualEditor.clearDescription')}
|
|
2999
|
+
confirmText={t('common.clear', 'Clear')}
|
|
3000
|
+
cancelText={t('common.cancel', 'Cancel')}
|
|
3001
|
+
variant="destructive"
|
|
3002
|
+
onConfirm={confirmClear}
|
|
3003
|
+
/>
|
|
3004
|
+
{/* Starting a run is a FORM — a context payload, saved fixtures and two
|
|
3005
|
+
run-mode switches — authored against the canvas it will execute, so it
|
|
3006
|
+
is a rail rather than a modal that hides the graph. */}
|
|
3007
|
+
<Drawer open={startOpen} onOpenChange={setStartOpen}>
|
|
3008
|
+
<DrawerContent
|
|
3009
|
+
data-testid="workflow-start-instance-drawer"
|
|
3010
|
+
closeAriaLabel={t('workflows.startInstance.close')}
|
|
3011
|
+
onKeyDown={(event) => {
|
|
3012
|
+
if (!(event.metaKey || event.ctrlKey) || event.key !== 'Enter') return
|
|
3013
|
+
event.preventDefault()
|
|
3014
|
+
event.stopPropagation()
|
|
3015
|
+
if (starting) return
|
|
3016
|
+
void handleStartInstance()
|
|
3017
|
+
}}
|
|
3018
|
+
>
|
|
3019
|
+
<DrawerHeader>
|
|
3020
|
+
<DrawerTitle>{t('workflows.startInstance.title')}</DrawerTitle>
|
|
3021
|
+
<DrawerDescription>{t('workflows.startInstance.description')}</DrawerDescription>
|
|
3022
|
+
</DrawerHeader>
|
|
3023
|
+
<DrawerBody className="space-y-3 py-2">
|
|
3024
|
+
|
|
3025
|
+
{startFixtureList.length > 0 ? (
|
|
3026
|
+
<div className="space-y-1">
|
|
3027
|
+
<span className="text-overline text-muted-foreground">{t('workflows.fixtures.label')}</span>
|
|
3028
|
+
<div className="flex flex-wrap gap-1">
|
|
3029
|
+
{startFixtureList.map((fixture) => (
|
|
3030
|
+
<span key={fixture.name} className="inline-flex items-center gap-1 rounded-md border border-border bg-muted px-2 py-1">
|
|
3031
|
+
<button
|
|
3032
|
+
type="button"
|
|
3033
|
+
className="text-xs font-medium hover:underline"
|
|
3034
|
+
onClick={() => handleApplyFixture(fixture.name)}
|
|
3035
|
+
>
|
|
3036
|
+
{fixture.name}
|
|
3037
|
+
</button>
|
|
3038
|
+
<button
|
|
3039
|
+
type="button"
|
|
3040
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
3041
|
+
aria-label={t('workflows.fixtures.delete', { name: fixture.name })}
|
|
3042
|
+
onClick={() => handleDeleteFixture(fixture.name)}
|
|
3043
|
+
>
|
|
3044
|
+
×
|
|
3045
|
+
</button>
|
|
3046
|
+
</span>
|
|
3047
|
+
))}
|
|
3048
|
+
</div>
|
|
3049
|
+
</div>
|
|
3050
|
+
) : null}
|
|
3051
|
+
|
|
3052
|
+
<label className="text-sm font-medium" htmlFor="workflow-start-context">
|
|
3053
|
+
{t('workflows.startInstance.contextLabel')}
|
|
3054
|
+
</label>
|
|
3055
|
+
<Textarea
|
|
3056
|
+
id="workflow-start-context"
|
|
3057
|
+
value={startContext}
|
|
3058
|
+
onChange={(e) => setStartContext(e.target.value)}
|
|
3059
|
+
rows={9}
|
|
3060
|
+
spellCheck={false}
|
|
3061
|
+
className="font-mono text-sm"
|
|
3062
|
+
/>
|
|
3063
|
+
|
|
3064
|
+
<div className="flex items-end gap-2">
|
|
3065
|
+
<div className="flex-1 space-y-1">
|
|
3066
|
+
<Label htmlFor="workflow-fixture-name" className="text-xs">
|
|
3067
|
+
{t('workflows.fixtures.nameLabel')}
|
|
3068
|
+
</Label>
|
|
3069
|
+
<Input
|
|
3070
|
+
id="workflow-fixture-name"
|
|
3071
|
+
value={fixtureName}
|
|
3072
|
+
onChange={(e) => setFixtureName(e.target.value)}
|
|
3073
|
+
placeholder={t('workflows.fixtures.namePlaceholder')}
|
|
3074
|
+
/>
|
|
3075
|
+
</div>
|
|
3076
|
+
<Button variant="outline" onClick={handleSaveFixture} disabled={!fixtureName.trim()}>
|
|
3077
|
+
{t('workflows.fixtures.save')}
|
|
3078
|
+
</Button>
|
|
3079
|
+
</div>
|
|
3080
|
+
<p className="text-xs text-muted-foreground">{t('workflows.fixtures.storageWarning')}</p>
|
|
3081
|
+
|
|
3082
|
+
<div className="space-y-2 rounded-md border border-border p-2">
|
|
3083
|
+
<div className="flex items-center justify-between gap-2">
|
|
3084
|
+
<Label htmlFor="workflow-start-dry-run" className="text-sm font-medium">
|
|
3085
|
+
{t('workflows.startInstance.dryRunLabel')}
|
|
3086
|
+
</Label>
|
|
3087
|
+
<Switch
|
|
3088
|
+
id="workflow-start-dry-run"
|
|
3089
|
+
checked={startDryRun}
|
|
3090
|
+
onCheckedChange={(checked) => setStartDryRun(checked === true)}
|
|
3091
|
+
/>
|
|
3092
|
+
</div>
|
|
3093
|
+
<p className="text-xs text-muted-foreground">{t('workflows.startInstance.dryRunHint')}</p>
|
|
3094
|
+
<div className="flex items-center justify-between gap-2">
|
|
3095
|
+
<Label htmlFor="workflow-start-step-through" className="text-sm font-medium">
|
|
3096
|
+
{t('workflows.startInstance.stepThroughLabel')}
|
|
3097
|
+
</Label>
|
|
3098
|
+
<Switch
|
|
3099
|
+
id="workflow-start-step-through"
|
|
3100
|
+
checked={startStepThrough}
|
|
3101
|
+
onCheckedChange={(checked) => setStartStepThrough(checked === true)}
|
|
3102
|
+
/>
|
|
3103
|
+
</div>
|
|
3104
|
+
<p className="text-xs text-muted-foreground">{t('workflows.startInstance.stepThroughHint')}</p>
|
|
3105
|
+
</div>
|
|
3106
|
+
</DrawerBody>
|
|
3107
|
+
<DrawerFooter>
|
|
3108
|
+
<Button variant="outline" onClick={() => setStartOpen(false)} disabled={starting}>
|
|
3109
|
+
{t('workflows.startInstance.cancel')}
|
|
3110
|
+
</Button>
|
|
3111
|
+
<Button onClick={() => void handleStartInstance()} disabled={starting}>
|
|
3112
|
+
{starting ? <Spinner className="h-4 w-4" /> : t('workflows.startInstance.start')}
|
|
3113
|
+
</Button>
|
|
3114
|
+
</DrawerFooter>
|
|
3115
|
+
</DrawerContent>
|
|
3116
|
+
</Drawer>
|
|
3117
|
+
</>
|
|
3118
|
+
)
|
|
3119
|
+
|
|
3120
|
+
const structuralConflictBanner = structuralConflict ? (
|
|
3121
|
+
<div className="shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3">
|
|
3122
|
+
<Alert variant="warning">
|
|
3123
|
+
<AlertTitle>
|
|
3124
|
+
{t('workflows.visualEditor.editSafety.bannerTitle', 'This change needs a new version')}
|
|
3125
|
+
</AlertTitle>
|
|
3126
|
+
<AlertDescription>
|
|
3127
|
+
{t(
|
|
3128
|
+
'workflows.visualEditor.editSafety.bannerDescription',
|
|
3129
|
+
'{count} instance(s) are still running on this version. Changing the steps or routes would re-point them mid-flight, so the change is saved to a new version instead.',
|
|
3130
|
+
{ count: String(structuralConflict.activeInstanceCount) },
|
|
3131
|
+
)}
|
|
3132
|
+
</AlertDescription>
|
|
3133
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
3134
|
+
<Button size="sm" onClick={handleCreateVersion} disabled={isCreatingVersion} className="h-8 text-xs">
|
|
3135
|
+
{t('workflows.visualEditor.editSafety.createVersion', 'Create version')}
|
|
3136
|
+
</Button>
|
|
3137
|
+
<Button
|
|
3138
|
+
size="sm"
|
|
3139
|
+
variant="outline"
|
|
3140
|
+
onClick={() => setStructuralConflict(null)}
|
|
3141
|
+
disabled={isCreatingVersion}
|
|
3142
|
+
className="h-8 text-xs"
|
|
3143
|
+
>
|
|
3144
|
+
{t('workflows.visualEditor.editSafety.keepEditing', 'Keep editing')}
|
|
3145
|
+
</Button>
|
|
3146
|
+
</div>
|
|
3147
|
+
</Alert>
|
|
3148
|
+
</div>
|
|
3149
|
+
) : null
|
|
3150
|
+
|
|
3151
|
+
const draftRestoreBanner = pendingDraft ? (
|
|
3152
|
+
<div className="shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3">
|
|
3153
|
+
<Alert variant="info">
|
|
3154
|
+
<AlertTitle>
|
|
3155
|
+
{t('workflows.visualEditor.draft.bannerTitle', 'You have an unsaved draft from {time}', {
|
|
3156
|
+
time: formatRelativeTime(pendingDraft.draft.updatedAt, { translate: t }) ?? '',
|
|
3157
|
+
})}
|
|
3158
|
+
</AlertTitle>
|
|
3159
|
+
{pendingDraft.baseMismatch && (
|
|
3160
|
+
<AlertDescription>
|
|
3161
|
+
{t('workflows.visualEditor.draft.bannerConflict', 'The workflow definition has changed since this draft was made. Restoring will apply your draft over the newer version.')}
|
|
3162
|
+
</AlertDescription>
|
|
3163
|
+
)}
|
|
3164
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
3165
|
+
<Button size="sm" onClick={handleRestoreDraft} className="h-8 text-xs">
|
|
3166
|
+
{t('workflows.visualEditor.draft.restore', 'Restore draft')}
|
|
3167
|
+
</Button>
|
|
3168
|
+
<Button size="sm" variant="outline" onClick={handleDiscardDraft} className="h-8 text-xs">
|
|
3169
|
+
{t('workflows.visualEditor.draft.discard', 'Discard')}
|
|
3170
|
+
</Button>
|
|
3171
|
+
</div>
|
|
3172
|
+
</Alert>
|
|
3173
|
+
</div>
|
|
3174
|
+
) : null
|
|
3175
|
+
|
|
3176
|
+
const { errors: problemErrorCount, warnings: problemWarningCount } = countIssuesBySeverity(problems)
|
|
3177
|
+
|
|
3178
|
+
const problemsPanel = showProblems && problems.length > 0 ? (
|
|
3179
|
+
<div className="shrink-0 border-t border-border bg-background px-3 py-2 md:px-6 md:py-3">
|
|
3180
|
+
<div className={`rounded-lg border bg-card ${problemErrorCount > 0 ? 'border-status-error-border' : 'border-status-warning-border'}`}>
|
|
3181
|
+
<div className="flex items-center justify-between gap-2 px-3 py-2">
|
|
3182
|
+
<Button
|
|
3183
|
+
variant="ghost"
|
|
3184
|
+
size="sm"
|
|
3185
|
+
onClick={() => setProblemsCollapsed((collapsed) => !collapsed)}
|
|
3186
|
+
aria-expanded={!problemsCollapsed}
|
|
3187
|
+
className="h-auto gap-2 p-0 text-sm font-semibold text-foreground hover:bg-transparent"
|
|
3188
|
+
>
|
|
3189
|
+
{problemsCollapsed ? <ChevronRight className="h-4 w-4" aria-hidden="true" /> : <ChevronDown className="h-4 w-4" aria-hidden="true" />}
|
|
3190
|
+
{t('workflows.visualEditor.problems.title', 'Problems')}
|
|
3191
|
+
<span className="text-xs font-normal text-muted-foreground">
|
|
3192
|
+
{t('workflows.visualEditor.problems.counts', '{errors} error(s) · {warnings} warning(s)', { errors: problemErrorCount, warnings: problemWarningCount })}
|
|
3193
|
+
</span>
|
|
3194
|
+
</Button>
|
|
3195
|
+
<Button
|
|
3196
|
+
variant="ghost"
|
|
3197
|
+
size="sm"
|
|
3198
|
+
onClick={() => setShowProblems(false)}
|
|
3199
|
+
aria-label={t('workflows.visualEditor.problems.dismiss', 'Dismiss problems')}
|
|
3200
|
+
className="h-7 w-7 p-0"
|
|
3201
|
+
>
|
|
3202
|
+
<X className="h-4 w-4" aria-hidden="true" />
|
|
3203
|
+
</Button>
|
|
3204
|
+
</div>
|
|
3205
|
+
{!problemsCollapsed && (
|
|
3206
|
+
<ul className="max-h-56 overflow-y-auto border-t border-border">
|
|
3207
|
+
{problems.map((issue) => {
|
|
3208
|
+
const isNavigable = Boolean(issue.nodeId || issue.edgeId)
|
|
3209
|
+
return (
|
|
3210
|
+
<li key={issue.id}>
|
|
3211
|
+
<Button
|
|
3212
|
+
variant="ghost"
|
|
3213
|
+
onClick={() => handleProblemClick(issue)}
|
|
3214
|
+
disabled={!isNavigable}
|
|
3215
|
+
className={`flex h-auto w-full items-start justify-start gap-2 rounded-none px-3 py-1.5 text-left text-sm font-normal ${isNavigable ? 'hover:bg-muted' : 'cursor-default hover:bg-transparent'}`}
|
|
3216
|
+
>
|
|
3217
|
+
{issue.severity === 'error' ? (
|
|
3218
|
+
<CircleAlert className="mt-0.5 h-4 w-4 shrink-0 text-status-error-text" aria-hidden="true" />
|
|
3219
|
+
) : (
|
|
3220
|
+
<TriangleAlert className="mt-0.5 h-4 w-4 shrink-0 text-status-warning-text" aria-hidden="true" />
|
|
3221
|
+
)}
|
|
3222
|
+
<span className="min-w-0 flex-1 text-foreground">{issue.message}</span>
|
|
3223
|
+
{issue.nodeLabel && (
|
|
3224
|
+
<span className="shrink-0 text-xs text-muted-foreground">{issue.nodeLabel}</span>
|
|
3225
|
+
)}
|
|
3226
|
+
</Button>
|
|
3227
|
+
</li>
|
|
3228
|
+
)
|
|
3229
|
+
})}
|
|
3230
|
+
</ul>
|
|
3231
|
+
)}
|
|
3232
|
+
</div>
|
|
3233
|
+
</div>
|
|
3234
|
+
) : null
|
|
3235
|
+
|
|
698
3236
|
if (isMobile) {
|
|
699
3237
|
return (
|
|
700
3238
|
<Page className="flex h-[100svh] flex-col space-y-0 overflow-hidden">
|
|
3239
|
+
{structuralConflictBanner}
|
|
3240
|
+
{draftRestoreBanner}
|
|
701
3241
|
<MobileVisualEditor
|
|
702
3242
|
definitionId={definitionId}
|
|
703
3243
|
isSaving={isSaving}
|
|
@@ -708,14 +3248,15 @@ export default function VisualEditorPage() {
|
|
|
708
3248
|
onNodeClick={handleNodeClick}
|
|
709
3249
|
onEdgeClick={handleEdgeClick}
|
|
710
3250
|
onConnect={handleConnect}
|
|
3251
|
+
onReconnect={handleReconnect}
|
|
711
3252
|
onAddNode={handleAddNode}
|
|
712
3253
|
onSave={handleSave}
|
|
713
3254
|
onValidate={handleValidate}
|
|
714
|
-
|
|
715
|
-
onLoadExample={
|
|
3255
|
+
onStartInstance={() => setStartOpen(true)}
|
|
3256
|
+
onLoadExample={handleOpenTemplateGallery}
|
|
716
3257
|
onClear={handleClear}
|
|
717
3258
|
metadata={metadata}
|
|
718
|
-
|
|
3259
|
+
onMetadataOpenChange={handleMetadataOpenChange}
|
|
719
3260
|
/>
|
|
720
3261
|
{sharedDialogs}
|
|
721
3262
|
{ConfirmDialogElement}
|
|
@@ -723,89 +3264,168 @@ export default function VisualEditorPage() {
|
|
|
723
3264
|
)
|
|
724
3265
|
}
|
|
725
3266
|
|
|
3267
|
+
// Break the editor out of `main`'s centered `max-w-screen-2xl` so the canvas
|
|
3268
|
+
// uses the full content column on wide screens (the side gutters the author
|
|
3269
|
+
// saw). Width = the content column (viewport minus the sidebar offset exposed
|
|
3270
|
+
// by AppShell); the negative margins cancel `main`'s auto-centering
|
|
3271
|
+
// (`max(0, (col-1536)/2)`) and its `lg:p-6` padding (24px). Desktop-only — the
|
|
3272
|
+
// compact (<1280px) layout keeps the normal centered container.
|
|
3273
|
+
const fullBleedSideMargin =
|
|
3274
|
+
'calc(-1 * (max(0px, (100vw - var(--app-content-offset, 0px) - 1536px) / 2) + 24px))'
|
|
3275
|
+
const fullBleedStyle: React.CSSProperties = !isCompactViewport
|
|
3276
|
+
? {
|
|
3277
|
+
width: 'calc(100vw - var(--app-content-offset, 0px))',
|
|
3278
|
+
marginLeft: fullBleedSideMargin,
|
|
3279
|
+
marginRight: fullBleedSideMargin,
|
|
3280
|
+
}
|
|
3281
|
+
: {}
|
|
3282
|
+
|
|
726
3283
|
return (
|
|
727
|
-
<Page
|
|
3284
|
+
<Page
|
|
3285
|
+
className="flex min-h-[calc(100svh-7rem)] flex-col space-y-0 overflow-x-hidden"
|
|
3286
|
+
style={fullBleedStyle}
|
|
3287
|
+
>
|
|
728
3288
|
{/* Page Header */}
|
|
729
|
-
<div className=
|
|
3289
|
+
<div className={`shrink-0 border-b border-border bg-background ${focusMode ? 'px-3 py-1.5 md:px-4' : 'px-3 py-2 md:px-6 md:py-3'}`}>
|
|
730
3290
|
<FormHeader
|
|
731
3291
|
mode="detail"
|
|
732
3292
|
backHref="/backend/definitions"
|
|
733
3293
|
backLabel={t('workflows.definitions.backToList', 'Back to definitions')}
|
|
734
3294
|
title={definitionId ? (workflowName || t('workflows.definitions.singular')) : t('workflows.backend.definitions.visual_editor.title')}
|
|
735
|
-
subtitle={
|
|
736
|
-
?
|
|
737
|
-
:
|
|
3295
|
+
subtitle={focusMode
|
|
3296
|
+
? undefined
|
|
3297
|
+
: definitionId
|
|
3298
|
+
? t('workflows.definitions.detail.summary', 'Editing workflow definition')
|
|
3299
|
+
: t('workflows.definitions.create.summary', 'Create and edit workflow definitions visually with a drag-and-drop interface')
|
|
738
3300
|
}
|
|
739
3301
|
actionsContent={
|
|
740
3302
|
<div className="flex flex-wrap items-center justify-end gap-1 md:gap-2">
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
aria-label={showMetadata ? t('workflows.visualEditor.hideMetadata') : t('workflows.visualEditor.showMetadata')}
|
|
748
|
-
>
|
|
749
|
-
{showMetadata ? <PanelTopClose className="mr-1.5 h-4 w-4" /> : <PanelTopOpen className="mr-1.5 h-4 w-4" />}
|
|
750
|
-
{showMetadata ? t('workflows.visualEditor.hideMetadata') : t('workflows.visualEditor.showMetadata')}
|
|
751
|
-
</Button>
|
|
752
|
-
{!isCodeOnly && (
|
|
753
|
-
<Button
|
|
754
|
-
variant="outline"
|
|
755
|
-
size="sm"
|
|
756
|
-
onClick={handleLoadExample}
|
|
757
|
-
disabled={isSaving}
|
|
758
|
-
className="h-8 text-xs"
|
|
759
|
-
>
|
|
760
|
-
{t('workflows.visualEditor.loadExample')}
|
|
761
|
-
</Button>
|
|
3303
|
+
{draftEligible && (draftSaveFailed || draftSavedLabel) && (
|
|
3304
|
+
<span role="status" className="text-xs text-muted-foreground">
|
|
3305
|
+
{draftSaveFailed
|
|
3306
|
+
? t('workflows.visualEditor.draft.saveFailed', 'Draft not saved')
|
|
3307
|
+
: t('workflows.visualEditor.draft.saved', 'Draft saved {time}', { time: draftSavedLabel ?? '' })}
|
|
3308
|
+
</span>
|
|
762
3309
|
)}
|
|
763
|
-
{!isCodeOnly && (
|
|
764
|
-
<
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
onClick={handleClear}
|
|
768
|
-
disabled={isSaving}
|
|
769
|
-
className="h-8 px-2 text-xs"
|
|
770
|
-
aria-label={t('workflows.visualEditor.clear')}
|
|
771
|
-
>
|
|
772
|
-
<Trash2 className="mr-1.5 h-4 w-4" />
|
|
773
|
-
{t('workflows.visualEditor.clear')}
|
|
774
|
-
</Button>
|
|
3310
|
+
{!isCodeOnly && definitionId && autosaveState !== 'idle' && (
|
|
3311
|
+
<span className="mr-1 text-xs text-muted-foreground" aria-live="polite">
|
|
3312
|
+
{autosaveState === 'saving' ? t('workflows.visualEditor.autosaving') : t('workflows.visualEditor.autosaved')}
|
|
3313
|
+
</span>
|
|
775
3314
|
)}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
disabled={isSaving}
|
|
781
|
-
className="h-8 px-2 text-xs"
|
|
782
|
-
aria-label={t('workflows.visualEditor.validate')}
|
|
783
|
-
>
|
|
784
|
-
<CircleQuestionMark className="mr-1.5 h-4 w-4" />
|
|
785
|
-
{t('workflows.visualEditor.validate')}
|
|
786
|
-
</Button>
|
|
787
|
-
{!isCodeOnly && (
|
|
3315
|
+
{focusMode ? (
|
|
3316
|
+
// Focus mode collapses the toolbar to the essentials; the toggle
|
|
3317
|
+
// stays reachable so the author can leave, and Cmd+K still opens
|
|
3318
|
+
// every command.
|
|
788
3319
|
<Button
|
|
789
3320
|
variant="outline"
|
|
790
3321
|
size="sm"
|
|
791
|
-
onClick={
|
|
3322
|
+
onClick={toggleFocus}
|
|
792
3323
|
disabled={isSaving}
|
|
793
|
-
className="h-8 text-xs"
|
|
3324
|
+
className="h-8 px-2 text-xs"
|
|
3325
|
+
aria-label={t('workflows.visualEditor.exitFocusMode')}
|
|
794
3326
|
>
|
|
795
|
-
<
|
|
796
|
-
{t('workflows.visualEditor.
|
|
3327
|
+
<Minimize2 className="mr-1.5 h-4 w-4" />
|
|
3328
|
+
{t('workflows.visualEditor.exitFocusMode')}
|
|
797
3329
|
</Button>
|
|
798
|
-
)
|
|
799
|
-
|
|
3330
|
+
) : (
|
|
3331
|
+
<>
|
|
3332
|
+
{/* Validate — surfaces the last Problems pass's error count so the
|
|
3333
|
+
author sees "3 errors" without opening the panel; icon + count,
|
|
3334
|
+
never colour alone (spec §4.6). */}
|
|
800
3335
|
<Button
|
|
801
3336
|
variant="outline"
|
|
802
3337
|
size="sm"
|
|
803
|
-
onClick={
|
|
3338
|
+
onClick={handleValidate}
|
|
804
3339
|
disabled={isSaving}
|
|
805
|
-
className="h-8 text-xs"
|
|
3340
|
+
className="h-8 px-2 text-xs"
|
|
3341
|
+
aria-label={problemErrorCount > 0
|
|
3342
|
+
? t('workflows.visualEditor.validateWithErrors', '{count} validation errors', { count: problemErrorCount })
|
|
3343
|
+
: t('workflows.visualEditor.validate')}
|
|
806
3344
|
>
|
|
807
|
-
{
|
|
3345
|
+
{problemErrorCount > 0 ? (
|
|
3346
|
+
<CircleAlert className="mr-1.5 h-4 w-4 text-destructive" aria-hidden="true" />
|
|
3347
|
+
) : (
|
|
3348
|
+
<CircleQuestionMark className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
|
3349
|
+
)}
|
|
3350
|
+
{t('workflows.visualEditor.validate')}
|
|
3351
|
+
{problemErrorCount > 0 && (
|
|
3352
|
+
<span className="ml-1.5 inline-flex h-4 min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-xs font-semibold text-white">
|
|
3353
|
+
{problemErrorCount}
|
|
3354
|
+
</span>
|
|
3355
|
+
)}
|
|
808
3356
|
</Button>
|
|
3357
|
+
{/* One grouped overflow keeps only the essentials — Validate,
|
|
3358
|
+
Start instance, Update — in the row. The metadata-incomplete
|
|
3359
|
+
warning rides the trigger (attention dot) and the Settings
|
|
3360
|
+
item, so hiding Settings here never buries it. */}
|
|
3361
|
+
{(() => {
|
|
3362
|
+
const moreActions: ActionMenuEntry[] = []
|
|
3363
|
+
moreActions.push({ id: 'group-tools', header: t('workflows.visualEditor.menuGroups.tools', 'Tools') })
|
|
3364
|
+
moreActions.push({ id: 'commands', label: t('workflows.commandPalette.buttonLabel', 'Commands'), icon: Command, onSelect: () => setShowCommandPalette(true) })
|
|
3365
|
+
if (!isCodeOnly) moreActions.push({ id: 'ai-draft', label: t('workflows.aiDraft.toggleShort', 'AI draft'), icon: Sparkles, onSelect: () => setShowAiDraft(true) })
|
|
3366
|
+
moreActions.push({ id: 'group-view', header: t('workflows.visualEditor.menuGroups.view', 'View') })
|
|
3367
|
+
moreActions.push({ id: 'focus', label: t('workflows.visualEditor.enterFocusMode'), icon: Maximize2, onSelect: toggleFocus, disabled: isSaving })
|
|
3368
|
+
moreActions.push({ id: 'settings', label: t('workflows.visualEditor.metadata.buttonLabel', 'Settings'), icon: metadataIncomplete ? TriangleAlert : PanelRightOpen, onSelect: () => setMetadataOpen(true), disabled: isSaving })
|
|
3369
|
+
moreActions.push({ id: 'code', label: t('workflows.visualEditor.codeView.title', 'Code'), icon: Code, onSelect: () => setShowCodeView(true) })
|
|
3370
|
+
moreActions.push({ id: 'group-canvas', header: t('workflows.visualEditor.menuGroups.canvas', 'Canvas') })
|
|
3371
|
+
if (!isCodeOnly) moreActions.push({ id: 'load-example', label: t('workflows.visualEditor.loadExample'), onSelect: handleOpenTemplateGallery, disabled: isSaving })
|
|
3372
|
+
if (!isCodeOnly) moreActions.push({ id: 'auto-arrange', label: t('workflows.visualEditor.autoArrange'), icon: Network, onSelect: handleAutoArrange, disabled: isSaving || nodes.length === 0 })
|
|
3373
|
+
moreActions.push({ id: 'compensation', label: t('workflows.compensation.toggleShort', 'Compensation'), icon: ShieldMinus, onSelect: toggleCompensation })
|
|
3374
|
+
if (isCodeOverride) moreActions.push({ id: 'reset-to-code', label: t('workflows.actions.resetToCode'), onSelect: handleResetToCode, disabled: isSaving })
|
|
3375
|
+
if (!isCodeOnly) {
|
|
3376
|
+
moreActions.push({ separator: true, id: 'sep-destructive' })
|
|
3377
|
+
moreActions.push({ id: 'clear', label: t('workflows.visualEditor.clear'), icon: Trash2, onSelect: handleClear, disabled: isSaving, destructive: true })
|
|
3378
|
+
}
|
|
3379
|
+
return (
|
|
3380
|
+
<ActionsDropdown
|
|
3381
|
+
items={moreActions}
|
|
3382
|
+
label={t('workflows.visualEditor.more', 'More')}
|
|
3383
|
+
ariaLabel={t('workflows.visualEditor.more', 'More')}
|
|
3384
|
+
triggerClassName="h-8 px-2 text-xs"
|
|
3385
|
+
triggerIcon={false}
|
|
3386
|
+
attention={metadataIncomplete}
|
|
3387
|
+
/>
|
|
3388
|
+
)
|
|
3389
|
+
})()}
|
|
3390
|
+
{/* Execution overlay (spec §8.3): "Show last run" paints node
|
|
3391
|
+
states with DS status tokens and the taken path. It stays a
|
|
3392
|
+
toolbar button rather than joining the More menu because it
|
|
3393
|
+
is a STATEFUL toggle with something to report: a menu entry
|
|
3394
|
+
can neither announce `aria-pressed` nor keep the "(never
|
|
3395
|
+
run)" caption visible once the menu closes, and §8.3 requires
|
|
3396
|
+
a definition that has never run to say so. Read-only —
|
|
3397
|
+
derived at render time, never part of the document. */}
|
|
3398
|
+
{definitionId && (
|
|
3399
|
+
<Button
|
|
3400
|
+
variant="outline"
|
|
3401
|
+
size="sm"
|
|
3402
|
+
onClick={toggleLastRun}
|
|
3403
|
+
aria-pressed={showLastRun}
|
|
3404
|
+
className="h-8 px-2 text-xs"
|
|
3405
|
+
aria-label={t('workflows.lastRun.toggle', 'Show the last run on the canvas')}
|
|
3406
|
+
>
|
|
3407
|
+
<History className="mr-1.5 h-4 w-4" aria-hidden="true" />
|
|
3408
|
+
{t('workflows.lastRun.toggleShort', 'Last run')}
|
|
3409
|
+
{showLastRun && lastRunOverlay.isUnavailable ? (
|
|
3410
|
+
<span className="ml-1.5 text-muted-foreground">
|
|
3411
|
+
{t('workflows.lastRun.never', '(never run)')}
|
|
3412
|
+
</span>
|
|
3413
|
+
) : null}
|
|
3414
|
+
</Button>
|
|
3415
|
+
)}
|
|
3416
|
+
{definitionId && (
|
|
3417
|
+
<Button
|
|
3418
|
+
variant="outline"
|
|
3419
|
+
size="sm"
|
|
3420
|
+
onClick={() => setStartOpen(true)}
|
|
3421
|
+
disabled={isSaving}
|
|
3422
|
+
className="h-8 text-xs"
|
|
3423
|
+
>
|
|
3424
|
+
<Play className="mr-1.5 h-4 w-4" />
|
|
3425
|
+
{t('workflows.actions.startInstance')}
|
|
3426
|
+
</Button>
|
|
3427
|
+
)}
|
|
3428
|
+
</>
|
|
809
3429
|
)}
|
|
810
3430
|
{isCodeOnly ? (
|
|
811
3431
|
<Button
|
|
@@ -849,150 +3469,11 @@ export default function VisualEditorPage() {
|
|
|
849
3469
|
</div>
|
|
850
3470
|
)}
|
|
851
3471
|
|
|
852
|
-
{/*
|
|
853
|
-
{
|
|
854
|
-
<div className={isCompactViewport
|
|
855
|
-
? 'shrink-0 border-b border-border bg-background px-3 py-2 max-h-[60svh] overflow-y-auto overscroll-contain md:px-6 md:py-3'
|
|
856
|
-
: 'shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3'
|
|
857
|
-
}>
|
|
858
|
-
<fieldset disabled={isCodeOnly} className="rounded-lg border bg-card p-3 disabled:opacity-70 md:p-4">
|
|
859
|
-
<h2 className="mb-3 text-xs font-semibold uppercase text-muted-foreground">{t('workflows.visualEditor.workflowMetadata')}</h2>
|
|
860
|
-
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 md:gap-4">
|
|
861
|
-
{/* Workflow ID */}
|
|
862
|
-
<div className="min-w-0 space-y-1">
|
|
863
|
-
<Label htmlFor="workflowId" className="text-xs">{t('workflows.form.workflowId')} *</Label>
|
|
864
|
-
<Input
|
|
865
|
-
id="workflowId"
|
|
866
|
-
value={workflowId}
|
|
867
|
-
onChange={(e) => setWorkflowId(e.target.value)}
|
|
868
|
-
placeholder="checkout_workflow"
|
|
869
|
-
disabled={!!definitionId}
|
|
870
|
-
className="h-8 text-sm"
|
|
871
|
-
/>
|
|
872
|
-
{definitionId && <p className="text-overline text-muted-foreground">{t('workflows.visualEditor.readOnly')}</p>}
|
|
873
|
-
</div>
|
|
874
|
-
|
|
875
|
-
{/* Workflow Name */}
|
|
876
|
-
<div className="min-w-0 space-y-1">
|
|
877
|
-
<Label htmlFor="workflowName" className="text-xs">{t('workflows.form.workflowName')} *</Label>
|
|
878
|
-
<Input
|
|
879
|
-
id="workflowName"
|
|
880
|
-
value={workflowName}
|
|
881
|
-
onChange={(e) => setWorkflowName(e.target.value)}
|
|
882
|
-
placeholder="Checkout Process"
|
|
883
|
-
className="h-8 text-sm"
|
|
884
|
-
/>
|
|
885
|
-
</div>
|
|
886
|
-
|
|
887
|
-
{/* Category */}
|
|
888
|
-
<div className="min-w-0 space-y-1">
|
|
889
|
-
<Label htmlFor="category" className="text-xs">{t('workflows.form.category')}</Label>
|
|
890
|
-
<Input
|
|
891
|
-
id="category"
|
|
892
|
-
value={category}
|
|
893
|
-
onChange={(e) => setCategory(e.target.value)}
|
|
894
|
-
placeholder="E-Commerce"
|
|
895
|
-
className="h-8 text-sm"
|
|
896
|
-
/>
|
|
897
|
-
</div>
|
|
898
|
-
|
|
899
|
-
{/* Description */}
|
|
900
|
-
<div className="min-w-0 space-y-1 sm:col-span-2 lg:col-span-3">
|
|
901
|
-
<Label htmlFor="description" className="text-xs">{t('workflows.form.description')}</Label>
|
|
902
|
-
<Textarea
|
|
903
|
-
id="description"
|
|
904
|
-
value={description}
|
|
905
|
-
onChange={(e) => setDescription(e.target.value)}
|
|
906
|
-
placeholder={t('workflows.form.placeholders.description')}
|
|
907
|
-
rows={2}
|
|
908
|
-
className="min-h-[60px] text-sm"
|
|
909
|
-
/>
|
|
910
|
-
</div>
|
|
911
|
-
|
|
912
|
-
{/* Version */}
|
|
913
|
-
<div className="min-w-0 space-y-1">
|
|
914
|
-
<Label htmlFor="version" className="text-xs">{t('workflows.form.version')} *</Label>
|
|
915
|
-
<Input
|
|
916
|
-
id="version"
|
|
917
|
-
type="number"
|
|
918
|
-
value={version}
|
|
919
|
-
onChange={(e) => setVersion(parseInt(e.target.value) || 1)}
|
|
920
|
-
min={1}
|
|
921
|
-
disabled={!!definitionId}
|
|
922
|
-
className="h-8 text-sm"
|
|
923
|
-
/>
|
|
924
|
-
</div>
|
|
925
|
-
|
|
926
|
-
{/* Enabled */}
|
|
927
|
-
<div className="min-w-0 space-y-1">
|
|
928
|
-
<Label className="text-xs">{t('common.enabled', 'Enabled')}</Label>
|
|
929
|
-
<div className="flex h-8 items-center gap-2">
|
|
930
|
-
<Switch
|
|
931
|
-
id="enabled"
|
|
932
|
-
checked={enabled}
|
|
933
|
-
onCheckedChange={setEnabled}
|
|
934
|
-
/>
|
|
935
|
-
<Label htmlFor="enabled" className="cursor-pointer text-xs font-normal">
|
|
936
|
-
{enabled ? t('common.on', 'On') : t('common.off', 'Off')}
|
|
937
|
-
</Label>
|
|
938
|
-
</div>
|
|
939
|
-
</div>
|
|
940
|
-
|
|
941
|
-
{/* Tags */}
|
|
942
|
-
<div className="min-w-0 space-y-1">
|
|
943
|
-
<Label className="text-xs">{t('workflows.form.tags')}</Label>
|
|
944
|
-
<TagsInput
|
|
945
|
-
value={tags}
|
|
946
|
-
onChange={setTags}
|
|
947
|
-
placeholder={t('workflows.form.placeholders.tags')}
|
|
948
|
-
/>
|
|
949
|
-
</div>
|
|
950
|
-
|
|
951
|
-
{/* Icon */}
|
|
952
|
-
<div className="min-w-0 space-y-1">
|
|
953
|
-
<Label htmlFor="icon" className="text-xs">{t('workflows.form.icon')}</Label>
|
|
954
|
-
<Input
|
|
955
|
-
id="icon"
|
|
956
|
-
value={icon}
|
|
957
|
-
onChange={(e) => setIcon(e.target.value)}
|
|
958
|
-
placeholder="ShoppingCart"
|
|
959
|
-
className="h-8 text-sm"
|
|
960
|
-
/>
|
|
961
|
-
</div>
|
|
962
|
-
|
|
963
|
-
<div className="min-w-0 space-y-1">
|
|
964
|
-
<Label htmlFor="effectiveFrom" className="text-xs">{t('workflows.form.effectiveFrom')}</Label>
|
|
965
|
-
<Input
|
|
966
|
-
id="effectiveFrom"
|
|
967
|
-
type="date"
|
|
968
|
-
value={effectiveFrom}
|
|
969
|
-
onChange={(e) => setEffectiveFrom(e.target.value)}
|
|
970
|
-
className="h-8 text-sm"
|
|
971
|
-
/>
|
|
972
|
-
</div>
|
|
973
|
-
|
|
974
|
-
<div className="min-w-0 space-y-1">
|
|
975
|
-
<Label htmlFor="effectiveTo" className="text-xs">{t('workflows.form.effectiveTo')}</Label>
|
|
976
|
-
<Input
|
|
977
|
-
id="effectiveTo"
|
|
978
|
-
type="date"
|
|
979
|
-
value={effectiveTo}
|
|
980
|
-
onChange={(e) => setEffectiveTo(e.target.value)}
|
|
981
|
-
className="h-8 text-sm"
|
|
982
|
-
/>
|
|
983
|
-
</div>
|
|
984
|
-
</div>
|
|
985
|
-
</fieldset>
|
|
3472
|
+
{/* Edit-safety rule: structural change refused while instances run (spec §4.1) */}
|
|
3473
|
+
{structuralConflictBanner}
|
|
986
3474
|
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
<DefinitionTriggersEditor
|
|
990
|
-
value={triggers}
|
|
991
|
-
onChange={setTriggers}
|
|
992
|
-
/>
|
|
993
|
-
</fieldset>
|
|
994
|
-
</div>
|
|
995
|
-
)}
|
|
3475
|
+
{/* Per-user draft restore banner (spec §4.7) */}
|
|
3476
|
+
{draftRestoreBanner}
|
|
996
3477
|
|
|
997
3478
|
{/* Main Content */}
|
|
998
3479
|
{isCompactViewport ? (
|
|
@@ -1007,8 +3488,18 @@ export default function VisualEditorPage() {
|
|
|
1007
3488
|
onNodeClick={handleNodeClick}
|
|
1008
3489
|
onEdgeClick={handleEdgeClick}
|
|
1009
3490
|
onConnect={handleConnect}
|
|
3491
|
+
onReconnect={handleReconnect}
|
|
3492
|
+
onCanvasDrop={handleCanvasDrop}
|
|
1010
3493
|
editable={!isCodeOnly}
|
|
1011
3494
|
height="100%"
|
|
3495
|
+
focusTarget={focusTarget}
|
|
3496
|
+
nodeErrorCounts={nodeErrorCounts}
|
|
3497
|
+
showCompensation={showCompensation}
|
|
3498
|
+
runOverlay={lastRunOverlay.execution}
|
|
3499
|
+
runAgentLabels={lastRunOverlay.agentLabels}
|
|
3500
|
+
triggers={triggers}
|
|
3501
|
+
definitionEnabled={enabled}
|
|
3502
|
+
onOpenTriggers={handleOpenTriggers}
|
|
1012
3503
|
/>
|
|
1013
3504
|
</div>
|
|
1014
3505
|
|
|
@@ -1018,7 +3509,7 @@ export default function VisualEditorPage() {
|
|
|
1018
3509
|
<h2 className="mb-2 text-lg font-semibold text-foreground">{t('workflows.visualEditor.startBuilding')}</h2>
|
|
1019
3510
|
<p className="mb-4 text-sm text-muted-foreground">{t('workflows.visualEditor.tapToAddBelow')}</p>
|
|
1020
3511
|
<button
|
|
1021
|
-
onClick={
|
|
3512
|
+
onClick={handleOpenTemplateGallery}
|
|
1022
3513
|
className="pointer-events-auto text-sm text-primary hover:underline"
|
|
1023
3514
|
>
|
|
1024
3515
|
{t('workflows.visualEditor.loadExampleWorkflow')}
|
|
@@ -1034,11 +3525,13 @@ export default function VisualEditorPage() {
|
|
|
1034
3525
|
<p className="mb-3 text-xs text-muted-foreground">{t('workflows.visualEditor.tapToAdd')}</p>
|
|
1035
3526
|
|
|
1036
3527
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
|
1037
|
-
{(['start', 'userTask', 'automated', 'waitForSignal', 'waitForTimer', 'subWorkflow', 'end'] as const).map((nodeType) => {
|
|
3528
|
+
{(['start', 'userTask', 'automated', 'invokeAgent', 'waitForSignal', 'waitForTimer', 'subWorkflow', 'end'] as const).map((nodeType) => {
|
|
1038
3529
|
const Icon = NODE_TYPE_ICONS[nodeType]
|
|
1039
3530
|
return (
|
|
1040
3531
|
<button
|
|
1041
3532
|
key={nodeType}
|
|
3533
|
+
draggable
|
|
3534
|
+
onDragStart={(event) => handlePaletteStepDragStart(event, nodeType)}
|
|
1042
3535
|
onClick={() => handleAddNode(nodeType)}
|
|
1043
3536
|
className="flex shrink-0 items-center gap-1 rounded-md border bg-background px-2 py-1 text-xs hover:bg-muted active:bg-muted/50"
|
|
1044
3537
|
>
|
|
@@ -1052,143 +3545,172 @@ export default function VisualEditorPage() {
|
|
|
1052
3545
|
)}
|
|
1053
3546
|
</div>
|
|
1054
3547
|
) : (
|
|
1055
|
-
<div className="flex min-h-
|
|
1056
|
-
{/* Left Sidebar - Step Palette (hidden in read-only mode) */}
|
|
3548
|
+
<div data-testid="workflow-editor-row" className="flex min-h-0 min-w-0 flex-1 border-t border-border pl-3 md:pl-6">
|
|
3549
|
+
{/* Left Sidebar - Step Palette rail (hidden in read-only mode) */}
|
|
1057
3550
|
{!isCodeOnly && (
|
|
1058
|
-
<div className=
|
|
1059
|
-
<div className=
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
return <Icon className="h-4 w-4" />
|
|
1075
|
-
})()}
|
|
1076
|
-
</div>
|
|
1077
|
-
<div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.start.title}</div>
|
|
1078
|
-
<div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.start.description}</div>
|
|
1079
|
-
</button>
|
|
1080
|
-
|
|
1081
|
-
{/* USER_TASK Step */}
|
|
1082
|
-
<button
|
|
1083
|
-
onClick={() => handleAddNode('userTask')}
|
|
1084
|
-
className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
|
|
1085
|
-
>
|
|
1086
|
-
<div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.userTask} opacity-60 transition-opacity group-hover:opacity-100`}>
|
|
1087
|
-
{(() => {
|
|
1088
|
-
const Icon = NODE_TYPE_ICONS.userTask
|
|
1089
|
-
return <Icon className="h-4 w-4" />
|
|
1090
|
-
})()}
|
|
1091
|
-
</div>
|
|
1092
|
-
<div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.userTask.title}</div>
|
|
1093
|
-
<div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.userTask.description}</div>
|
|
1094
|
-
</button>
|
|
3551
|
+
<div className={`${paletteCollapsed ? 'w-14' : 'w-48'} shrink-0 overflow-y-auto border-r border-border bg-background p-2`}>
|
|
3552
|
+
<div className={`mb-2 flex items-center ${paletteCollapsed ? 'justify-center' : 'justify-between'}`}>
|
|
3553
|
+
{!paletteCollapsed && (
|
|
3554
|
+
<h2 className="px-1 text-xs font-semibold uppercase text-muted-foreground">{t('workflows.visualEditor.stepPalette')}</h2>
|
|
3555
|
+
)}
|
|
3556
|
+
<IconButton
|
|
3557
|
+
type="button"
|
|
3558
|
+
variant="ghost"
|
|
3559
|
+
size="sm"
|
|
3560
|
+
onClick={togglePaletteCollapsed}
|
|
3561
|
+
title={paletteCollapsed ? t('workflows.visualEditor.expandPalette') : t('workflows.visualEditor.collapsePalette')}
|
|
3562
|
+
aria-label={paletteCollapsed ? t('workflows.visualEditor.expandPalette') : t('workflows.visualEditor.collapsePalette')}
|
|
3563
|
+
>
|
|
3564
|
+
{paletteCollapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
|
|
3565
|
+
</IconButton>
|
|
3566
|
+
</div>
|
|
1095
3567
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
className="group relative w-full cursor-pointer rounded-xl border-2 border-border bg-background px-4 py-3 text-left transition-all hover:border-muted-foreground/30 hover:shadow-md"
|
|
1100
|
-
>
|
|
1101
|
-
<div className={`absolute right-2 top-2 ${NODE_TYPE_COLORS.automated} opacity-60 transition-opacity group-hover:opacity-100`}>
|
|
1102
|
-
{(() => {
|
|
1103
|
-
const Icon = NODE_TYPE_ICONS.automated
|
|
1104
|
-
return <Icon className="h-4 w-4" />
|
|
1105
|
-
})()}
|
|
1106
|
-
</div>
|
|
1107
|
-
<div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.automated.title}</div>
|
|
1108
|
-
<div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.automated.description}</div>
|
|
1109
|
-
</button>
|
|
3568
|
+
{!paletteCollapsed && (
|
|
3569
|
+
<p className="mb-2 px-1 text-xs text-muted-foreground">{t('workflows.visualEditor.clickToAdd')}</p>
|
|
3570
|
+
)}
|
|
1110
3571
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
3572
|
+
<div className={`flex flex-col gap-1 ${paletteCollapsed ? 'items-center' : ''}`}>
|
|
3573
|
+
{PALETTE_NODE_TYPES.map((nodeType) => {
|
|
3574
|
+
const Icon = NODE_TYPE_ICONS[nodeType]
|
|
3575
|
+
const label = NODE_TYPE_LABELS[nodeType]
|
|
3576
|
+
const tooltip = `${label.title} — ${label.description}`
|
|
3577
|
+
if (paletteCollapsed) {
|
|
3578
|
+
return (
|
|
3579
|
+
<button
|
|
3580
|
+
key={nodeType}
|
|
3581
|
+
type="button"
|
|
3582
|
+
draggable
|
|
3583
|
+
onDragStart={(event) => handlePaletteStepDragStart(event, nodeType)}
|
|
3584
|
+
onClick={() => handleAddNode(nodeType)}
|
|
3585
|
+
title={tooltip}
|
|
3586
|
+
aria-label={tooltip}
|
|
3587
|
+
className="flex h-9 w-9 items-center justify-center rounded-md border hover:bg-muted"
|
|
3588
|
+
>
|
|
3589
|
+
<Icon className={`h-4 w-4 ${NODE_TYPE_COLORS[nodeType]}`} />
|
|
3590
|
+
</button>
|
|
3591
|
+
)
|
|
3592
|
+
}
|
|
3593
|
+
return (
|
|
3594
|
+
<button
|
|
3595
|
+
key={nodeType}
|
|
3596
|
+
type="button"
|
|
3597
|
+
draggable
|
|
3598
|
+
onDragStart={(event) => handlePaletteStepDragStart(event, nodeType)}
|
|
3599
|
+
onClick={() => handleAddNode(nodeType)}
|
|
3600
|
+
title={label.description}
|
|
3601
|
+
className="flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs hover:bg-muted"
|
|
3602
|
+
>
|
|
3603
|
+
<Icon className={`h-4 w-4 shrink-0 ${NODE_TYPE_COLORS[nodeType]}`} />
|
|
3604
|
+
<span className="truncate font-medium text-foreground">{label.title}</span>
|
|
3605
|
+
</button>
|
|
3606
|
+
)
|
|
3607
|
+
})}
|
|
3608
|
+
</div>
|
|
1125
3609
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
3610
|
+
{/* Actions (spec §4.4): drag one onto a route to append it to that
|
|
3611
|
+
route's activities — the chips (#4244) then render it. Clicking
|
|
3612
|
+
explains where the keyboard path is, so the entry is never dead. */}
|
|
3613
|
+
{!paletteCollapsed && (
|
|
3614
|
+
<div className="mt-4">
|
|
3615
|
+
<h2 className="mb-1 px-1 text-xs font-semibold uppercase text-muted-foreground">
|
|
3616
|
+
{t('workflows.visualEditor.palette.actionsTitle', 'Actions')}
|
|
3617
|
+
</h2>
|
|
3618
|
+
<p className="mb-2 px-1 text-xs text-muted-foreground">
|
|
3619
|
+
{t('workflows.visualEditor.palette.dragActionHint', 'Drop an action onto a route to add it there')}
|
|
3620
|
+
</p>
|
|
3621
|
+
<div className="flex flex-col gap-1">
|
|
3622
|
+
{activityTypeOptions.map((option) => {
|
|
3623
|
+
const ActionIcon = resolveActivityIcon(option.value)
|
|
3624
|
+
return (
|
|
3625
|
+
<button
|
|
3626
|
+
key={option.value}
|
|
3627
|
+
type="button"
|
|
3628
|
+
draggable
|
|
3629
|
+
onDragStart={(event) => handlePaletteActivityDragStart(event, option.value, option.label)}
|
|
3630
|
+
onClick={() => flash(t('workflows.visualEditor.palette.dragActionHint', 'Drop an action onto a route to add it there'), 'info')}
|
|
3631
|
+
title={t('workflows.visualEditor.palette.dragActionHint', 'Drop an action onto a route to add it there')}
|
|
3632
|
+
className="flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs hover:bg-muted"
|
|
3633
|
+
>
|
|
3634
|
+
<ActionIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
3635
|
+
<span className="truncate font-medium text-foreground">{option.label}</span>
|
|
3636
|
+
</button>
|
|
3637
|
+
)
|
|
3638
|
+
})}
|
|
3639
|
+
</div>
|
|
3640
|
+
</div>
|
|
3641
|
+
)}
|
|
1140
3642
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
>
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
<
|
|
1153
|
-
|
|
1154
|
-
|
|
3643
|
+
{/* Annotations (spec §4.5): documentation only — filtered out of the
|
|
3644
|
+
saved definition, so they can never change what the engine runs. */}
|
|
3645
|
+
{!paletteCollapsed && (
|
|
3646
|
+
<div className="mt-4">
|
|
3647
|
+
<h2 className="mb-1 px-1 text-xs font-semibold uppercase text-muted-foreground">
|
|
3648
|
+
{t('workflows.annotations.paletteTitle', 'Annotations')}
|
|
3649
|
+
</h2>
|
|
3650
|
+
<p className="mb-2 px-1 text-xs text-muted-foreground">
|
|
3651
|
+
{t('workflows.annotations.paletteHint', 'Documentation only — never executed')}
|
|
3652
|
+
</p>
|
|
3653
|
+
<div className="flex flex-col gap-1">
|
|
3654
|
+
<button
|
|
3655
|
+
type="button"
|
|
3656
|
+
onClick={() => handleAddAnnotation('note')}
|
|
3657
|
+
title={t('workflows.annotations.note.addHint', 'Add a markdown sticky note to the canvas')}
|
|
3658
|
+
className="flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs hover:bg-muted"
|
|
3659
|
+
>
|
|
3660
|
+
<StickyNote className="h-4 w-4 shrink-0 text-status-warning-text" aria-hidden="true" />
|
|
3661
|
+
<span className="truncate font-medium text-foreground">{t('workflows.annotations.note.add', 'Note')}</span>
|
|
3662
|
+
</button>
|
|
3663
|
+
<button
|
|
3664
|
+
type="button"
|
|
3665
|
+
onClick={() => handleAddAnnotation('group')}
|
|
3666
|
+
title={t('workflows.annotations.group.addHint', 'Add a named region around part of the canvas')}
|
|
3667
|
+
className="flex w-full items-center gap-2 rounded-md border px-2 py-1.5 text-left text-xs hover:bg-muted"
|
|
3668
|
+
>
|
|
3669
|
+
<Group className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
3670
|
+
<span className="truncate font-medium text-foreground">{t('workflows.annotations.group.add', 'Group')}</span>
|
|
3671
|
+
</button>
|
|
3672
|
+
</div>
|
|
3673
|
+
</div>
|
|
3674
|
+
)}
|
|
1155
3675
|
|
|
1156
|
-
|
|
3676
|
+
{!paletteCollapsed && (
|
|
3677
|
+
<div className="mt-3">
|
|
1157
3678
|
<button
|
|
1158
|
-
|
|
1159
|
-
|
|
3679
|
+
type="button"
|
|
3680
|
+
onClick={() => setShowPaletteHowTo((prev) => !prev)}
|
|
3681
|
+
aria-expanded={showPaletteHowTo}
|
|
3682
|
+
className="flex w-full items-center gap-1.5 rounded-md px-1 py-1 text-xs text-muted-foreground hover:text-foreground"
|
|
1160
3683
|
>
|
|
1161
|
-
<
|
|
1162
|
-
|
|
1163
|
-
const Icon = NODE_TYPE_ICONS.end
|
|
1164
|
-
return <Icon className="h-4 w-4" />
|
|
1165
|
-
})()}
|
|
1166
|
-
</div>
|
|
1167
|
-
<div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.end.title}</div>
|
|
1168
|
-
<div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.end.description}</div>
|
|
3684
|
+
<CircleQuestionMark className="h-3.5 w-3.5" />
|
|
3685
|
+
<span>{t('workflows.visualEditor.howToUse', 'How to use:')}</span>
|
|
1169
3686
|
</button>
|
|
3687
|
+
{showPaletteHowTo && (
|
|
3688
|
+
<Alert variant="info" className="mt-2">
|
|
3689
|
+
<AlertTitle className="text-xs">{t('workflows.visualEditor.howToUse', 'How to use:')}</AlertTitle>
|
|
3690
|
+
<div className="mt-2">
|
|
3691
|
+
<ul className="list-inside list-disc space-y-1 text-xs">
|
|
3692
|
+
<li>{t('workflows.visualEditor.hint.addSteps', 'Click step types to add them')}</li>
|
|
3693
|
+
<li>{t('workflows.visualEditor.hint.dragFromPalette', 'Drag a step onto the canvas, or onto a route to insert it there')}</li>
|
|
3694
|
+
<li>{t('workflows.visualEditor.hint.dragSteps', 'Drag steps to position them')}</li>
|
|
3695
|
+
<li>{t('workflows.visualEditor.hint.connectSteps', 'Connect steps by dragging from handles')}</li>
|
|
3696
|
+
<li>{t('workflows.visualEditor.hint.editSteps', 'Click steps and transitions to edit them')}</li>
|
|
3697
|
+
<li>{t('workflows.visualEditor.hint.copyPaste', 'Select steps and copy, paste or duplicate them with Cmd/Ctrl+C, +V and +D')}</li>
|
|
3698
|
+
<li>{t('workflows.visualEditor.hint.commandPalette', 'Press Cmd/Ctrl+K for every command without the mouse')}</li>
|
|
3699
|
+
<li>{t('workflows.visualEditor.hint.keyboardCanvas', 'With a step selected: Enter opens it, Del removes it, arrows nudge it (hold Shift for bigger steps)')}</li>
|
|
3700
|
+
<li>{t('workflows.visualEditor.hint.validate', 'Validate before saving')}</li>
|
|
3701
|
+
</ul>
|
|
3702
|
+
</div>
|
|
3703
|
+
</Alert>
|
|
3704
|
+
)}
|
|
1170
3705
|
</div>
|
|
1171
|
-
|
|
1172
|
-
{/* Instructions */}
|
|
1173
|
-
<Alert status="information" className="mt-6">
|
|
1174
|
-
<AlertTitle className="text-xs">{t('workflows.visualEditor.howToUse', 'How to use:')}</AlertTitle>
|
|
1175
|
-
<div className="mt-2">
|
|
1176
|
-
<ul className="list-inside list-disc space-y-1 text-xs">
|
|
1177
|
-
<li>{t('workflows.visualEditor.hint.addSteps', 'Click step types to add them')}</li>
|
|
1178
|
-
<li>{t('workflows.visualEditor.hint.dragSteps', 'Drag steps to position them')}</li>
|
|
1179
|
-
<li>{t('workflows.visualEditor.hint.connectSteps', 'Connect steps by dragging from handles')}</li>
|
|
1180
|
-
<li>{t('workflows.visualEditor.hint.editSteps', 'Click steps and transitions to edit them')}</li>
|
|
1181
|
-
<li>{t('workflows.visualEditor.hint.validate', 'Validate before saving')}</li>
|
|
1182
|
-
</ul>
|
|
1183
|
-
</div>
|
|
1184
|
-
</Alert>
|
|
1185
|
-
</div>
|
|
3706
|
+
)}
|
|
1186
3707
|
</div>
|
|
1187
3708
|
)}
|
|
1188
3709
|
|
|
1189
|
-
{/* Main Canvas
|
|
3710
|
+
{/* Main Canvas — the header already carries the Focus toggle, so no
|
|
3711
|
+
separate in-canvas Exit-focus button (avoids two identical controls). */}
|
|
1190
3712
|
<div className="min-w-0 flex-1 p-6">
|
|
1191
|
-
<div className="relative h-
|
|
3713
|
+
<div className="relative h-full min-h-[480px]">
|
|
1192
3714
|
<div className="h-full rounded-lg border bg-card">
|
|
1193
3715
|
<WorkflowGraph
|
|
1194
3716
|
initialNodes={nodes}
|
|
@@ -1198,8 +3720,18 @@ export default function VisualEditorPage() {
|
|
|
1198
3720
|
onNodeClick={handleNodeClick}
|
|
1199
3721
|
onEdgeClick={handleEdgeClick}
|
|
1200
3722
|
onConnect={handleConnect}
|
|
3723
|
+
onReconnect={handleReconnect}
|
|
3724
|
+
onCanvasDrop={handleCanvasDrop}
|
|
1201
3725
|
editable={!isCodeOnly}
|
|
1202
3726
|
height="100%"
|
|
3727
|
+
focusTarget={focusTarget}
|
|
3728
|
+
nodeErrorCounts={nodeErrorCounts}
|
|
3729
|
+
showCompensation={showCompensation}
|
|
3730
|
+
runOverlay={lastRunOverlay.execution}
|
|
3731
|
+
runAgentLabels={lastRunOverlay.agentLabels}
|
|
3732
|
+
triggers={triggers}
|
|
3733
|
+
definitionEnabled={enabled}
|
|
3734
|
+
onOpenTriggers={handleOpenTriggers}
|
|
1203
3735
|
/>
|
|
1204
3736
|
</div>
|
|
1205
3737
|
|
|
@@ -1214,7 +3746,7 @@ export default function VisualEditorPage() {
|
|
|
1214
3746
|
{t('workflows.visualEditor.clickToAddFromPalette')}
|
|
1215
3747
|
</p>
|
|
1216
3748
|
<button
|
|
1217
|
-
onClick={
|
|
3749
|
+
onClick={handleOpenTemplateGallery}
|
|
1218
3750
|
className="pointer-events-auto text-sm text-primary hover:underline"
|
|
1219
3751
|
>
|
|
1220
3752
|
{t('workflows.visualEditor.loadExampleWorkflow')}
|
|
@@ -1224,8 +3756,14 @@ export default function VisualEditorPage() {
|
|
|
1224
3756
|
)}
|
|
1225
3757
|
</div>
|
|
1226
3758
|
</div>
|
|
3759
|
+
|
|
3760
|
+
{/* Docked step / route inspector (spec §4.1). It is a sibling of the
|
|
3761
|
+
canvas, not an overlay on it, so opening it narrows the graph and
|
|
3762
|
+
leaves it visible and clickable. */}
|
|
3763
|
+
{inspectorsDocked ? inspectorPanels : null}
|
|
1227
3764
|
</div>
|
|
1228
3765
|
)}
|
|
3766
|
+
{problemsPanel}
|
|
1229
3767
|
{sharedDialogs}
|
|
1230
3768
|
{ConfirmDialogElement}
|
|
1231
3769
|
</Page>
|
|
@@ -1242,6 +3780,10 @@ function getDefaultLabel(nodeType: string): string {
|
|
|
1242
3780
|
decision: 'Decision Point',
|
|
1243
3781
|
waitForSignal: 'Wait for Signal',
|
|
1244
3782
|
waitForTimer: 'Wait for Timer',
|
|
3783
|
+
waitForCondition: 'Wait for Condition',
|
|
3784
|
+
invokeAgent: 'Invoke Agent',
|
|
3785
|
+
ifElse: 'If / Else',
|
|
3786
|
+
switch: 'Switch',
|
|
1245
3787
|
}
|
|
1246
3788
|
return labels[nodeType] || 'New Step'
|
|
1247
3789
|
}
|
|
@@ -1255,7 +3797,10 @@ function getDefaultBadge(nodeType: string): string {
|
|
|
1255
3797
|
decision: 'Decision',
|
|
1256
3798
|
waitForSignal: 'Wait for Signal',
|
|
1257
3799
|
waitForTimer: 'Wait for Timer',
|
|
3800
|
+
waitForCondition: 'Wait for Condition',
|
|
3801
|
+
invokeAgent: 'Invoke Agent',
|
|
3802
|
+
ifElse: 'If / Else',
|
|
3803
|
+
switch: 'Switch',
|
|
1258
3804
|
}
|
|
1259
3805
|
return badges[nodeType] || 'Task'
|
|
1260
3806
|
}
|
|
1261
|
-
|