@open-mercato/core 0.6.2-develop.3400.2.d948268f50 → 0.6.2-develop.3406.1.2b403f40da

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/workflow_definition/index.js +2 -0
  3. package/dist/generated/entities/workflow_definition/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/sales/workflows.js +180 -0
  7. package/dist/modules/sales/workflows.js.map +7 -0
  8. package/dist/modules/workflows/api/definitions/[id]/customize/route.js +172 -0
  9. package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +7 -0
  10. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +205 -0
  11. package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +7 -0
  12. package/dist/modules/workflows/api/definitions/[id]/route.js +91 -2
  13. package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
  14. package/dist/modules/workflows/api/definitions/route.js +32 -9
  15. package/dist/modules/workflows/api/definitions/route.js.map +2 -2
  16. package/dist/modules/workflows/api/definitions/serialize.js +36 -1
  17. package/dist/modules/workflows/api/definitions/serialize.js.map +2 -2
  18. package/dist/modules/workflows/backend/definitions/[id]/page.js +161 -52
  19. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  20. package/dist/modules/workflows/backend/definitions/page.js +37 -34
  21. package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
  22. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +134 -41
  23. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  24. package/dist/modules/workflows/cli.js +19 -121
  25. package/dist/modules/workflows/cli.js.map +2 -2
  26. package/dist/modules/workflows/components/formConfig.js +1 -1
  27. package/dist/modules/workflows/components/formConfig.js.map +2 -2
  28. package/dist/modules/workflows/data/entities.js +3 -0
  29. package/dist/modules/workflows/data/entities.js.map +2 -2
  30. package/dist/modules/workflows/data/validators.js +2 -2
  31. package/dist/modules/workflows/data/validators.js.map +2 -2
  32. package/dist/modules/workflows/events.js +2 -0
  33. package/dist/modules/workflows/events.js.map +2 -2
  34. package/dist/modules/workflows/examples/guard-rules-example.json +6 -6
  35. package/dist/modules/workflows/examples/order-approval-guard-rules.json +2 -2
  36. package/dist/modules/workflows/frontend/checkout-demo/page.js +4 -4
  37. package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
  38. package/dist/modules/workflows/lib/code-registry.js +40 -0
  39. package/dist/modules/workflows/lib/code-registry.js.map +7 -0
  40. package/dist/modules/workflows/lib/find-definition.js +58 -0
  41. package/dist/modules/workflows/lib/find-definition.js.map +7 -0
  42. package/dist/modules/workflows/lib/seeds.js +0 -3
  43. package/dist/modules/workflows/lib/seeds.js.map +2 -2
  44. package/dist/modules/workflows/lib/start-validator.js +1 -20
  45. package/dist/modules/workflows/lib/start-validator.js.map +2 -2
  46. package/dist/modules/workflows/lib/workflow-executor.js +1 -19
  47. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  48. package/dist/modules/workflows/migrations/Migration20260414120000.js +13 -0
  49. package/dist/modules/workflows/migrations/Migration20260414120000.js.map +7 -0
  50. package/dist/modules/workflows/migrations/Migration20260428102318.js +32 -0
  51. package/dist/modules/workflows/migrations/Migration20260428102318.js.map +7 -0
  52. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +1 -1
  53. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +1 -1
  54. package/dist/modules/workflows/workflows.js +260 -0
  55. package/dist/modules/workflows/workflows.js.map +7 -0
  56. package/generated/entities/workflow_definition/index.ts +1 -0
  57. package/generated/entity-fields-registry.ts +1 -0
  58. package/package.json +7 -7
  59. package/src/modules/sales/workflows.ts +184 -0
  60. package/src/modules/workflows/api/definitions/[id]/customize/route.ts +197 -0
  61. package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +252 -0
  62. package/src/modules/workflows/api/definitions/[id]/route.ts +108 -4
  63. package/src/modules/workflows/api/definitions/route.ts +54 -10
  64. package/src/modules/workflows/api/definitions/serialize.ts +42 -0
  65. package/src/modules/workflows/backend/definitions/[id]/page.tsx +135 -11
  66. package/src/modules/workflows/backend/definitions/page.tsx +45 -33
  67. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +222 -97
  68. package/src/modules/workflows/cli.ts +32 -155
  69. package/src/modules/workflows/components/formConfig.tsx +2 -2
  70. package/src/modules/workflows/data/entities.ts +4 -1
  71. package/src/modules/workflows/data/validators.ts +4 -3
  72. package/src/modules/workflows/events.ts +2 -0
  73. package/src/modules/workflows/examples/guard-rules-example.json +6 -6
  74. package/src/modules/workflows/examples/order-approval-guard-rules.json +2 -2
  75. package/src/modules/workflows/frontend/checkout-demo/page.tsx +4 -4
  76. package/src/modules/workflows/i18n/de.json +16 -0
  77. package/src/modules/workflows/i18n/en.json +16 -0
  78. package/src/modules/workflows/i18n/es.json +16 -0
  79. package/src/modules/workflows/i18n/pl.json +16 -0
  80. package/src/modules/workflows/lib/code-registry.ts +65 -0
  81. package/src/modules/workflows/lib/find-definition.ts +100 -0
  82. package/src/modules/workflows/lib/seeds.ts +4 -3
  83. package/src/modules/workflows/lib/start-validator.ts +2 -35
  84. package/src/modules/workflows/lib/workflow-executor.ts +2 -39
  85. package/src/modules/workflows/migrations/.snapshot-open-mercato.json +10 -0
  86. package/src/modules/workflows/migrations/Migration20260414120000.ts +13 -0
  87. package/src/modules/workflows/migrations/Migration20260428102318.ts +57 -0
  88. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +1 -1
  89. package/src/modules/workflows/workflows.ts +278 -0
  90. package/dist/modules/workflows/examples/checkout-demo-definition.json +0 -486
  91. package/dist/modules/workflows/examples/order-approval-definition.json +0 -257
  92. package/dist/modules/workflows/examples/simple-approval-definition.json +0 -134
  93. package/src/modules/workflows/examples/checkout-demo-definition.json +0 -486
  94. package/src/modules/workflows/examples/order-approval-definition.json +0 -257
  95. package/src/modules/workflows/examples/simple-approval-definition.json +0 -134
@@ -106,6 +106,10 @@ export default function VisualEditorPage() {
106
106
  const [effectiveFrom, setEffectiveFrom] = useState('')
107
107
  const [effectiveTo, setEffectiveTo] = useState('')
108
108
  const [triggers, setTriggers] = useState<WorkflowDefinitionTrigger[]>([])
109
+ const [source, setSource] = useState<'code' | 'code_override' | 'user' | null>(null)
110
+
111
+ const isCodeOnly = source === 'code'
112
+ const isCodeOverride = source === 'code_override'
109
113
 
110
114
  // Load existing definition if ID is provided
111
115
  useEffect(() => {
@@ -146,7 +150,10 @@ export default function VisualEditorPage() {
146
150
  // Load embedded triggers from definition
147
151
  setTriggers(definition.definition?.triggers || [])
148
152
 
149
- flash('Workflow loaded successfully', 'success')
153
+ // Track source so the editor mirrors the non-visual edit page UX:
154
+ // code → read-only with Customize button; code_override → editable
155
+ // with Reset to code; user → editable, no banner.
156
+ setSource((definition.source as 'code' | 'code_override' | 'user') ?? null)
150
157
  } catch (error) {
151
158
  console.error('Error loading workflow definition:', error)
152
159
  flash('Failed to load workflow definition', 'error')
@@ -160,16 +167,19 @@ export default function VisualEditorPage() {
160
167
 
161
168
  // Handle node changes from ReactFlow
162
169
  const handleNodesChange = useCallback((changes: NodeChange[]) => {
170
+ if (isCodeOnly) return
163
171
  setNodes((nds) => applyNodeChanges(changes, nds))
164
- }, [])
172
+ }, [isCodeOnly])
165
173
 
166
174
  // Handle edge changes from ReactFlow
167
175
  const handleEdgesChange = useCallback((changes: EdgeChange[]) => {
176
+ if (isCodeOnly) return
168
177
  setEdges((eds) => applyEdgeChanges(changes, eds))
169
- }, [])
178
+ }, [isCodeOnly])
170
179
 
171
180
  // Handle adding new node from palette
172
181
  const handleAddNode = useCallback((nodeType: string) => {
182
+ if (isCodeOnly) return
173
183
  const newNode: Node = {
174
184
  id: generateStepId(nodeType),
175
185
  type: nodeType,
@@ -186,21 +196,24 @@ export default function VisualEditorPage() {
186
196
  }
187
197
 
188
198
  setNodes((nds) => [...nds, newNode])
189
- }, [nodes.length])
199
+ }, [nodes.length, isCodeOnly])
190
200
 
191
- // Handle node selection - open edit dialog
201
+ // Handle node selection - open edit dialog (suppressed in read-only mode
202
+ // so users can't open the node editor on a code-defined workflow).
192
203
  const handleNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
204
+ if (isCodeOnly) return
193
205
  setSelectedNode(node)
194
206
  setSelectedEdge(null)
195
207
  setShowNodeDialog(true)
196
- }, [])
208
+ }, [isCodeOnly])
197
209
 
198
210
  // Handle edge selection - open edit dialog
199
211
  const handleEdgeClick = useCallback((_event: React.MouseEvent, edge: Edge) => {
212
+ if (isCodeOnly) return
200
213
  setSelectedEdge(edge)
201
214
  setSelectedNode(null)
202
215
  setShowEdgeDialog(true)
203
- }, [])
216
+ }, [isCodeOnly])
204
217
 
205
218
  // Save node updates
206
219
  const handleSaveNode = useCallback((nodeId: string, updates: Partial<Node['data']>) => {
@@ -355,13 +368,22 @@ export default function VisualEditorPage() {
355
368
 
356
369
  let result
357
370
  if (isUpdate) {
358
- // Update existing definition
371
+ // Update existing definition — send the full editable payload so metadata
372
+ // edits (name, description, version, category, tags, icon, effective
373
+ // dates) actually persist. Previously only `definition` + `enabled`
374
+ // were sent, silently dropping every other field.
359
375
  result = await apiCall<{ data: any; error?: string }>(`/api/workflows/definitions/${definitionId}`, {
360
376
  method: 'PUT',
361
377
  headers: { 'Content-Type': 'application/json' },
362
378
  body: JSON.stringify({
379
+ workflowName,
380
+ description: description || null,
381
+ version,
363
382
  definition: definitionData,
383
+ metadata: Object.keys(metadata).length > 0 ? metadata : null,
364
384
  enabled,
385
+ effectiveFrom: effectiveFrom || null,
386
+ effectiveTo: effectiveTo || null,
365
387
  }),
366
388
  })
367
389
  } else {
@@ -405,6 +427,60 @@ export default function VisualEditorPage() {
405
427
  }
406
428
  }, [nodes, edges, workflowId, workflowName, description, version, enabled, category, tags, icon, effectiveFrom, effectiveTo, triggers, definitionId, router])
407
429
 
430
+ // Customize a code-defined workflow → creates an override and reloads the
431
+ // editor pointed at the new UUID. Mirrors the non-visual edit page button.
432
+ const handleCustomize = useCallback(async () => {
433
+ if (!definitionId) return
434
+ setIsSaving(true)
435
+ try {
436
+ const result = await apiCall<{ data?: { id?: string }; error?: string }>(
437
+ `/api/workflows/definitions/${definitionId}/customize`,
438
+ { method: 'POST' },
439
+ )
440
+ if (!result.ok) {
441
+ flash(result.result?.error || 'Failed to customize workflow', 'error')
442
+ return
443
+ }
444
+ const newId = result.result?.data?.id
445
+ if (!newId) return
446
+ router.push(`/backend/definitions/visual-editor?id=${encodeURIComponent(newId)}`)
447
+ router.refresh()
448
+ } finally {
449
+ setIsSaving(false)
450
+ }
451
+ }, [definitionId, router])
452
+
453
+ // Reset a code-override back to its code definition. Mirrors the
454
+ // non-visual edit page action, with the same confirm dialog.
455
+ const handleResetToCode = useCallback(async () => {
456
+ if (!definitionId) return
457
+ const confirmed = await confirm({
458
+ title: t('workflows.actions.resetToCode'),
459
+ description: t('workflows.actions.resetConfirm'),
460
+ confirmText: t('workflows.actions.resetToCode'),
461
+ variant: 'destructive',
462
+ })
463
+ if (!confirmed) return
464
+
465
+ setIsSaving(true)
466
+ try {
467
+ const result = await apiCall<{ data?: { id?: string }; error?: string }>(
468
+ `/api/workflows/definitions/${definitionId}/reset-to-code`,
469
+ { method: 'POST' },
470
+ )
471
+ if (!result.ok) {
472
+ flash(result.result?.error || 'Failed to reset workflow', 'error')
473
+ return
474
+ }
475
+ const codeId = result.result?.data?.id || (workflowId ? `code:${workflowId}` : null)
476
+ if (!codeId) return
477
+ router.push(`/backend/definitions/visual-editor?id=${encodeURIComponent(codeId)}`)
478
+ router.refresh()
479
+ } finally {
480
+ setIsSaving(false)
481
+ }
482
+ }, [definitionId, workflowId, router, confirm, t])
483
+
408
484
  // Test workflow
409
485
  const handleTest = useCallback(() => {
410
486
  // First validate
@@ -628,26 +704,30 @@ export default function VisualEditorPage() {
628
704
  {showMetadata ? <PanelTopClose className="mr-1.5 h-4 w-4" /> : <PanelTopOpen className="mr-1.5 h-4 w-4" />}
629
705
  {showMetadata ? t('workflows.visualEditor.hideMetadata') : t('workflows.visualEditor.showMetadata')}
630
706
  </Button>
631
- <Button
632
- variant="outline"
633
- size="sm"
634
- onClick={handleLoadExample}
635
- disabled={isSaving}
636
- className="h-8 text-xs"
637
- >
638
- {t('workflows.visualEditor.loadExample')}
639
- </Button>
640
- <Button
641
- variant="destructive"
642
- size="sm"
643
- onClick={handleClear}
644
- disabled={isSaving}
645
- className="h-8 px-2 text-xs"
646
- aria-label={t('workflows.visualEditor.clear')}
647
- >
648
- <Trash2 className="mr-1.5 h-4 w-4" />
649
- {t('workflows.visualEditor.clear')}
650
- </Button>
707
+ {!isCodeOnly && (
708
+ <Button
709
+ variant="outline"
710
+ size="sm"
711
+ onClick={handleLoadExample}
712
+ disabled={isSaving}
713
+ className="h-8 text-xs"
714
+ >
715
+ {t('workflows.visualEditor.loadExample')}
716
+ </Button>
717
+ )}
718
+ {!isCodeOnly && (
719
+ <Button
720
+ variant="destructive"
721
+ size="sm"
722
+ onClick={handleClear}
723
+ disabled={isSaving}
724
+ className="h-8 px-2 text-xs"
725
+ aria-label={t('workflows.visualEditor.clear')}
726
+ >
727
+ <Trash2 className="mr-1.5 h-4 w-4" />
728
+ {t('workflows.visualEditor.clear')}
729
+ </Button>
730
+ )}
651
731
  <Button
652
732
  variant="outline"
653
733
  size="sm"
@@ -659,43 +739,83 @@ export default function VisualEditorPage() {
659
739
  <CircleQuestionMark className="mr-1.5 h-4 w-4" />
660
740
  {t('workflows.visualEditor.validate')}
661
741
  </Button>
662
- <Button
663
- variant="outline"
664
- size="sm"
665
- onClick={handleTest}
666
- disabled={isSaving}
667
- className="h-8 text-xs"
668
- >
669
- <Play className="mr-1.5 h-4 w-4" />
670
- {t('workflows.visualEditor.runTest')}
671
- </Button>
672
- <Button
673
- size="sm"
674
- onClick={handleSave}
675
- disabled={isSaving}
676
- className="h-8 px-2 text-xs md:px-3"
677
- aria-label={isSaving ? t('workflows.mobile.saving') : definitionId ? t('workflows.common.update') : t('workflows.common.save')}
678
- >
679
- <Save className="mr-1.5 h-4 w-4" />
680
- {isSaving ? t('workflows.mobile.saving') : definitionId ? t('workflows.common.update') : t('workflows.common.save')}
681
- </Button>
742
+ {!isCodeOnly && (
743
+ <Button
744
+ variant="outline"
745
+ size="sm"
746
+ onClick={handleTest}
747
+ disabled={isSaving}
748
+ className="h-8 text-xs"
749
+ >
750
+ <Play className="mr-1.5 h-4 w-4" />
751
+ {t('workflows.visualEditor.runTest')}
752
+ </Button>
753
+ )}
754
+ {isCodeOverride && (
755
+ <Button
756
+ variant="outline"
757
+ size="sm"
758
+ onClick={handleResetToCode}
759
+ disabled={isSaving}
760
+ className="h-8 text-xs"
761
+ >
762
+ {t('workflows.actions.resetToCode')}
763
+ </Button>
764
+ )}
765
+ {isCodeOnly ? (
766
+ <Button
767
+ size="sm"
768
+ onClick={handleCustomize}
769
+ disabled={isSaving}
770
+ className="h-8 px-2 text-xs md:px-3"
771
+ >
772
+ {t('workflows.actions.customize')}
773
+ </Button>
774
+ ) : (
775
+ <Button
776
+ size="sm"
777
+ onClick={handleSave}
778
+ disabled={isSaving}
779
+ className="h-8 px-2 text-xs md:px-3"
780
+ aria-label={isSaving ? t('workflows.mobile.saving') : definitionId ? t('workflows.common.update') : t('workflows.common.save')}
781
+ >
782
+ <Save className="mr-1.5 h-4 w-4" />
783
+ {isSaving ? t('workflows.mobile.saving') : definitionId ? t('workflows.common.update') : t('workflows.common.save')}
784
+ </Button>
785
+ )}
682
786
  </div>
683
787
  }
684
788
  />
685
789
  </div>
686
790
 
791
+ {/* Source banner (code-defined / customized) */}
792
+ {(isCodeOnly || isCodeOverride) && (
793
+ <div className="shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3">
794
+ {isCodeOnly && (
795
+ <Alert variant="info">
796
+ <AlertTitle>{t('workflows.source.code.readonlyBanner')}</AlertTitle>
797
+ </Alert>
798
+ )}
799
+ {isCodeOverride && (
800
+ <Alert variant="warning">
801
+ <AlertTitle>{t('workflows.source.code_override.banner')}</AlertTitle>
802
+ </Alert>
803
+ )}
804
+ </div>
805
+ )}
806
+
687
807
  {/* Workflow Metadata Form */}
688
808
  {showMetadata && (
689
809
  <div className={isCompactViewport
690
810
  ? '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'
691
811
  : 'shrink-0 border-b border-border bg-background px-3 py-2 md:px-6 md:py-3'
692
812
  }>
693
- <div className="rounded-lg border bg-card p-3 md:p-4">
813
+ <fieldset disabled={isCodeOnly} className="rounded-lg border bg-card p-3 disabled:opacity-70 md:p-4">
694
814
  <h2 className="mb-3 text-xs font-semibold uppercase text-muted-foreground">{t('workflows.visualEditor.workflowMetadata')}</h2>
695
815
  <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 md:gap-4">
696
816
  {/* Workflow ID */}
697
817
  <div className="min-w-0 space-y-1">
698
- <Label htmlFor="workflowId" className="text-xs">Workflow ID *</Label>
818
+ <Label htmlFor="workflowId" className="text-xs">{t('workflows.form.workflowId')} *</Label>
699
819
  <Input
700
820
  id="workflowId"
701
821
  value={workflowId}
@@ -704,12 +824,12 @@ export default function VisualEditorPage() {
704
824
  disabled={!!definitionId}
705
825
  className="h-8 text-sm"
706
826
  />
707
- {definitionId && <p className="text-overline text-muted-foreground">Read-only</p>}
827
+ {definitionId && <p className="text-overline text-muted-foreground">{t('workflows.visualEditor.readOnly')}</p>}
708
828
  </div>
709
829
 
710
830
  {/* Workflow Name */}
711
831
  <div className="min-w-0 space-y-1">
712
- <Label htmlFor="workflowName" className="text-xs">Name *</Label>
832
+ <Label htmlFor="workflowName" className="text-xs">{t('workflows.form.workflowName')} *</Label>
713
833
  <Input
714
834
  id="workflowName"
715
835
  value={workflowName}
@@ -721,7 +841,7 @@ export default function VisualEditorPage() {
721
841
 
722
842
  {/* Category */}
723
843
  <div className="min-w-0 space-y-1">
724
- <Label htmlFor="category" className="text-xs">Category</Label>
844
+ <Label htmlFor="category" className="text-xs">{t('workflows.form.category')}</Label>
725
845
  <Input
726
846
  id="category"
727
847
  value={category}
@@ -733,12 +853,12 @@ export default function VisualEditorPage() {
733
853
 
734
854
  {/* Description */}
735
855
  <div className="min-w-0 space-y-1 sm:col-span-2 lg:col-span-3">
736
- <Label htmlFor="description" className="text-xs">Description</Label>
856
+ <Label htmlFor="description" className="text-xs">{t('workflows.form.description')}</Label>
737
857
  <Textarea
738
858
  id="description"
739
859
  value={description}
740
860
  onChange={(e) => setDescription(e.target.value)}
741
- placeholder="Describe the purpose of this workflow..."
861
+ placeholder={t('workflows.form.placeholders.description')}
742
862
  rows={2}
743
863
  className="min-h-[60px] text-sm"
744
864
  />
@@ -746,7 +866,7 @@ export default function VisualEditorPage() {
746
866
 
747
867
  {/* Version */}
748
868
  <div className="min-w-0 space-y-1">
749
- <Label htmlFor="version" className="text-xs">Version *</Label>
869
+ <Label htmlFor="version" className="text-xs">{t('workflows.form.version')} *</Label>
750
870
  <Input
751
871
  id="version"
752
872
  type="number"
@@ -775,7 +895,7 @@ export default function VisualEditorPage() {
775
895
 
776
896
  {/* Tags */}
777
897
  <div className="min-w-0 space-y-1">
778
- <Label className="text-xs">Tags</Label>
898
+ <Label className="text-xs">{t('workflows.form.tags')}</Label>
779
899
  <TagsInput
780
900
  value={tags}
781
901
  onChange={setTags}
@@ -785,7 +905,7 @@ export default function VisualEditorPage() {
785
905
 
786
906
  {/* Icon */}
787
907
  <div className="min-w-0 space-y-1">
788
- <Label htmlFor="icon" className="text-xs">Icon</Label>
908
+ <Label htmlFor="icon" className="text-xs">{t('workflows.form.icon')}</Label>
789
909
  <Input
790
910
  id="icon"
791
911
  value={icon}
@@ -796,7 +916,7 @@ export default function VisualEditorPage() {
796
916
  </div>
797
917
 
798
918
  <div className="min-w-0 space-y-1">
799
- <Label htmlFor="effectiveFrom" className="text-xs">Effective From</Label>
919
+ <Label htmlFor="effectiveFrom" className="text-xs">{t('workflows.form.effectiveFrom')}</Label>
800
920
  <Input
801
921
  id="effectiveFrom"
802
922
  type="date"
@@ -807,7 +927,7 @@ export default function VisualEditorPage() {
807
927
  </div>
808
928
 
809
929
  <div className="min-w-0 space-y-1">
810
- <Label htmlFor="effectiveTo" className="text-xs">Effective To</Label>
930
+ <Label htmlFor="effectiveTo" className="text-xs">{t('workflows.form.effectiveTo')}</Label>
811
931
  <Input
812
932
  id="effectiveTo"
813
933
  type="date"
@@ -817,14 +937,15 @@ export default function VisualEditorPage() {
817
937
  />
818
938
  </div>
819
939
  </div>
820
- </div>
821
-
822
- {/* Event Triggers */}
823
- <DefinitionTriggersEditor
824
- value={triggers}
825
- onChange={setTriggers}
826
- className="mt-3"
827
- />
940
+ </fieldset>
941
+
942
+ {/* Event Triggers — also locked when the workflow is code-defined */}
943
+ <fieldset disabled={isCodeOnly} className="mt-3 disabled:opacity-70">
944
+ <DefinitionTriggersEditor
945
+ value={triggers}
946
+ onChange={setTriggers}
947
+ />
948
+ </fieldset>
828
949
  </div>
829
950
  )}
830
951
 
@@ -841,7 +962,7 @@ export default function VisualEditorPage() {
841
962
  onNodeClick={handleNodeClick}
842
963
  onEdgeClick={handleEdgeClick}
843
964
  onConnect={handleConnect}
844
- editable={true}
965
+ editable={!isCodeOnly}
845
966
  height="100%"
846
967
  />
847
968
  </div>
@@ -849,48 +970,51 @@ export default function VisualEditorPage() {
849
970
  {nodes.length === 0 && (
850
971
  <div className="pointer-events-none absolute inset-0 flex items-center justify-center px-4">
851
972
  <div className="text-center">
852
- <h2 className="mb-2 text-lg font-semibold text-foreground">Start Building Your Workflow</h2>
853
- <p className="mb-4 text-sm text-muted-foreground">Tap a step type below to add it to the canvas</p>
973
+ <h2 className="mb-2 text-lg font-semibold text-foreground">{t('workflows.visualEditor.startBuilding')}</h2>
974
+ <p className="mb-4 text-sm text-muted-foreground">{t('workflows.visualEditor.tapToAddBelow')}</p>
854
975
  <button
855
976
  onClick={handleLoadExample}
856
977
  className="pointer-events-auto text-sm text-primary hover:underline"
857
978
  >
858
- Load an example workflow
979
+ {t('workflows.visualEditor.loadExampleWorkflow')}
859
980
  </button>
860
981
  </div>
861
982
  </div>
862
983
  )}
863
984
  </div>
864
985
 
865
- <div className="mt-3 rounded-lg border bg-card p-3">
866
- <h2 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">Step Palette</h2>
867
- <p className="mb-3 text-xs text-muted-foreground">Tap a step type to add it to the canvas</p>
986
+ {!isCodeOnly && (
987
+ <div className="mt-3 rounded-lg border bg-card p-3">
988
+ <h2 className="mb-2 text-xs font-semibold uppercase text-muted-foreground">{t('workflows.visualEditor.stepPalette')}</h2>
989
+ <p className="mb-3 text-xs text-muted-foreground">{t('workflows.visualEditor.tapToAdd')}</p>
868
990
 
869
- <div className="flex gap-2 overflow-x-auto pb-1">
870
- {(['start', 'userTask', 'automated', 'waitForSignal', 'subWorkflow', 'end'] as const).map((nodeType) => {
871
- const Icon = NODE_TYPE_ICONS[nodeType]
872
- return (
873
- <button
874
- key={nodeType}
875
- onClick={() => handleAddNode(nodeType)}
876
- 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"
877
- >
878
- <Icon className="h-3.5 w-3.5" />
879
- <span>{NODE_TYPE_LABELS[nodeType].title}</span>
880
- </button>
881
- )
882
- })}
991
+ <div className="flex gap-2 overflow-x-auto pb-1">
992
+ {(['start', 'userTask', 'automated', 'waitForSignal', 'subWorkflow', 'end'] as const).map((nodeType) => {
993
+ const Icon = NODE_TYPE_ICONS[nodeType]
994
+ return (
995
+ <button
996
+ key={nodeType}
997
+ onClick={() => handleAddNode(nodeType)}
998
+ 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"
999
+ >
1000
+ <Icon className="h-3.5 w-3.5" />
1001
+ <span>{NODE_TYPE_LABELS[nodeType].title}</span>
1002
+ </button>
1003
+ )
1004
+ })}
1005
+ </div>
883
1006
  </div>
884
- </div>
1007
+ )}
885
1008
  </div>
886
1009
  ) : (
887
1010
  <div className="flex min-h-[72svh] min-w-0 flex-1 border-t border-border">
888
- {/* Left Sidebar - Step Palette */}
1011
+ {/* Left Sidebar - Step Palette (hidden in read-only mode) */}
1012
+ {!isCodeOnly && (
889
1013
  <div className="w-[24rem] shrink-0 overflow-y-auto border-r border-border bg-background p-6">
890
1014
  <div className="rounded-lg border bg-card p-4">
891
- <h2 className="mb-2 text-sm font-semibold uppercase text-muted-foreground">Step Palette</h2>
1015
+ <h2 className="mb-2 text-sm font-semibold uppercase text-muted-foreground">{t('workflows.visualEditor.stepPalette')}</h2>
892
1016
  <p className="mb-4 text-xs text-muted-foreground">
893
- Click a step type to add it to the canvas
1017
+ {t('workflows.visualEditor.clickToAdd')}
894
1018
  </p>
895
1019
 
896
1020
  <div className="space-y-3">
@@ -1001,6 +1125,7 @@ export default function VisualEditorPage() {
1001
1125
  </Alert>
1002
1126
  </div>
1003
1127
  </div>
1128
+ )}
1004
1129
 
1005
1130
  {/* Main Canvas */}
1006
1131
  <div className="min-w-0 flex-1 p-6">
@@ -1014,7 +1139,7 @@ export default function VisualEditorPage() {
1014
1139
  onNodeClick={handleNodeClick}
1015
1140
  onEdgeClick={handleEdgeClick}
1016
1141
  onConnect={handleConnect}
1017
- editable={true}
1142
+ editable={!isCodeOnly}
1018
1143
  height="100%"
1019
1144
  />
1020
1145
  </div>
@@ -1024,16 +1149,16 @@ export default function VisualEditorPage() {
1024
1149
  <div className="pointer-events-none absolute inset-0 flex items-center justify-center px-4">
1025
1150
  <div className="text-center">
1026
1151
  <h2 className="mb-2 text-xl font-semibold text-foreground">
1027
- Start Building Your Workflow
1152
+ {t('workflows.visualEditor.startBuilding')}
1028
1153
  </h2>
1029
1154
  <p className="mb-4 text-muted-foreground">
1030
- Click a step type from the palette to add it to the canvas
1155
+ {t('workflows.visualEditor.clickToAddFromPalette')}
1031
1156
  </p>
1032
1157
  <button
1033
1158
  onClick={handleLoadExample}
1034
1159
  className="pointer-events-auto text-sm text-primary hover:underline"
1035
1160
  >
1036
- Load an example workflow
1161
+ {t('workflows.visualEditor.loadExampleWorkflow')}
1037
1162
  </button>
1038
1163
  </div>
1039
1164
  </div>