@open-mercato/core 0.6.3-develop.3876.1.d40fe4ec2d → 0.6.3-develop.3894.1.352abf4240

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 (140) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/attachments/api/file/[id]/route.js +7 -2
  3. package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
  4. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +7 -4
  5. package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
  6. package/dist/modules/audit_logs/services/accessLogService.js +127 -8
  7. package/dist/modules/audit_logs/services/accessLogService.js.map +2 -2
  8. package/dist/modules/auth/backend/auth/profile/page.js +1 -1
  9. package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
  10. package/dist/modules/auth/backend/profile/change-password/page.js +1 -1
  11. package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
  12. package/dist/modules/auth/backend/users/[id]/edit/page.js +1 -1
  13. package/dist/modules/auth/backend/users/[id]/edit/page.js.map +2 -2
  14. package/dist/modules/auth/backend/users/create/page.js +6 -1
  15. package/dist/modules/auth/backend/users/create/page.js.map +2 -2
  16. package/dist/modules/auth/di.js +17 -3
  17. package/dist/modules/auth/di.js.map +2 -2
  18. package/dist/modules/auth/services/rbacDefaultCache.js +110 -0
  19. package/dist/modules/auth/services/rbacDefaultCache.js.map +7 -0
  20. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +8 -1
  21. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  22. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js +3 -2
  23. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.js.map +2 -2
  24. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js +3 -2
  25. package/dist/modules/catalog/backend/catalog/products/[productId]/variants/create/page.js.map +2 -2
  26. package/dist/modules/configs/cli.js +27 -14
  27. package/dist/modules/configs/cli.js.map +2 -2
  28. package/dist/modules/currencies/api/currencies/route.js +3 -4
  29. package/dist/modules/currencies/api/currencies/route.js.map +2 -2
  30. package/dist/modules/currencies/api/exchange-rates/route.js +3 -4
  31. package/dist/modules/currencies/api/exchange-rates/route.js.map +2 -2
  32. package/dist/modules/customers/api/people/route.js +26 -24
  33. package/dist/modules/customers/api/people/route.js.map +2 -2
  34. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js +26 -0
  35. package/dist/modules/directory/subscribers/invalidateOrgScopeCache.js.map +7 -0
  36. package/dist/modules/directory/utils/organizationScope.js +85 -0
  37. package/dist/modules/directory/utils/organizationScope.js.map +2 -2
  38. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js +1 -1
  39. package/dist/modules/resources/backend/resources/resource-types/[id]/edit/page.js.map +2 -2
  40. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js +1 -1
  41. package/dist/modules/sales/backend/sales/channels/[channelId]/edit/page.js.map +2 -2
  42. package/dist/modules/sales/components/channels/ChannelOfferForm.js +1 -1
  43. package/dist/modules/sales/components/channels/ChannelOfferForm.js.map +2 -2
  44. package/dist/modules/workflows/backend/definitions/[id]/page.js +2 -1
  45. package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
  46. package/dist/modules/workflows/backend/definitions/create/page.js +4 -2
  47. package/dist/modules/workflows/backend/definitions/create/page.js.map +2 -2
  48. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +20 -3
  49. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  50. package/dist/modules/workflows/components/ActivitiesEditor.js +34 -1
  51. package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
  52. package/dist/modules/workflows/components/NodeEditDialog.js +153 -17
  53. package/dist/modules/workflows/components/NodeEditDialog.js.map +2 -2
  54. package/dist/modules/workflows/components/StepsEditor.js +31 -0
  55. package/dist/modules/workflows/components/StepsEditor.js.map +2 -2
  56. package/dist/modules/workflows/components/WorkflowGraph.js +3 -2
  57. package/dist/modules/workflows/components/WorkflowGraph.js.map +2 -2
  58. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js +54 -0
  59. package/dist/modules/workflows/components/nodes/WaitForTimerNode.js.map +7 -0
  60. package/dist/modules/workflows/components/nodes/index.js +3 -1
  61. package/dist/modules/workflows/components/nodes/index.js.map +2 -2
  62. package/dist/modules/workflows/data/validators.js +117 -0
  63. package/dist/modules/workflows/data/validators.js.map +2 -2
  64. package/dist/modules/workflows/di.js +5 -1
  65. package/dist/modules/workflows/di.js.map +2 -2
  66. package/dist/modules/workflows/lib/activity-executor.js +42 -1
  67. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  68. package/dist/modules/workflows/lib/activity-queue-types.js.map +2 -2
  69. package/dist/modules/workflows/lib/activity-worker-handler.js +24 -0
  70. package/dist/modules/workflows/lib/activity-worker-handler.js.map +2 -2
  71. package/dist/modules/workflows/lib/duration.js +32 -0
  72. package/dist/modules/workflows/lib/duration.js.map +7 -0
  73. package/dist/modules/workflows/lib/event-logger.js +1 -0
  74. package/dist/modules/workflows/lib/event-logger.js.map +2 -2
  75. package/dist/modules/workflows/lib/format-validation-error.js +12 -0
  76. package/dist/modules/workflows/lib/format-validation-error.js.map +7 -0
  77. package/dist/modules/workflows/lib/graph-utils.js +6 -3
  78. package/dist/modules/workflows/lib/graph-utils.js.map +2 -2
  79. package/dist/modules/workflows/lib/node-type-icons.js +9 -5
  80. package/dist/modules/workflows/lib/node-type-icons.js.map +2 -2
  81. package/dist/modules/workflows/lib/signal-handler.js +55 -23
  82. package/dist/modules/workflows/lib/signal-handler.js.map +2 -2
  83. package/dist/modules/workflows/lib/step-handler.js +79 -29
  84. package/dist/modules/workflows/lib/step-handler.js.map +2 -2
  85. package/dist/modules/workflows/lib/timer-handler.js +159 -0
  86. package/dist/modules/workflows/lib/timer-handler.js.map +7 -0
  87. package/dist/modules/workflows/lib/workflow-executor.js +1 -1
  88. package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
  89. package/dist/modules/workflows/workers/workflow-activities.worker.js +20 -4
  90. package/dist/modules/workflows/workers/workflow-activities.worker.js.map +2 -2
  91. package/package.json +7 -7
  92. package/src/modules/attachments/api/file/[id]/route.ts +7 -2
  93. package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +7 -4
  94. package/src/modules/audit_logs/services/accessLogService.ts +179 -15
  95. package/src/modules/auth/backend/auth/profile/page.tsx +1 -1
  96. package/src/modules/auth/backend/profile/change-password/page.tsx +1 -1
  97. package/src/modules/auth/backend/users/[id]/edit/page.tsx +1 -1
  98. package/src/modules/auth/backend/users/create/page.tsx +6 -1
  99. package/src/modules/auth/di.ts +26 -3
  100. package/src/modules/auth/services/rbacDefaultCache.ts +145 -0
  101. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +8 -1
  102. package/src/modules/catalog/backend/catalog/products/[productId]/variants/[variantId]/page.tsx +3 -2
  103. package/src/modules/catalog/backend/catalog/products/[productId]/variants/create/page.tsx +3 -2
  104. package/src/modules/configs/cli.ts +34 -13
  105. package/src/modules/currencies/api/currencies/route.ts +3 -4
  106. package/src/modules/currencies/api/exchange-rates/route.ts +3 -4
  107. package/src/modules/customers/api/people/route.ts +27 -25
  108. package/src/modules/directory/subscribers/invalidateOrgScopeCache.ts +39 -0
  109. package/src/modules/directory/utils/organizationScope.ts +121 -0
  110. package/src/modules/resources/backend/resources/resource-types/[id]/edit/page.tsx +1 -1
  111. package/src/modules/sales/backend/sales/channels/[channelId]/edit/page.tsx +1 -1
  112. package/src/modules/sales/components/channels/ChannelOfferForm.tsx +1 -1
  113. package/src/modules/workflows/backend/definitions/[id]/page.tsx +3 -2
  114. package/src/modules/workflows/backend/definitions/create/page.tsx +4 -2
  115. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +18 -1
  116. package/src/modules/workflows/components/ActivitiesEditor.tsx +40 -0
  117. package/src/modules/workflows/components/NodeEditDialog.tsx +218 -30
  118. package/src/modules/workflows/components/StepsEditor.tsx +36 -0
  119. package/src/modules/workflows/components/WorkflowGraph.tsx +2 -1
  120. package/src/modules/workflows/components/nodes/WaitForTimerNode.tsx +70 -0
  121. package/src/modules/workflows/components/nodes/index.ts +3 -0
  122. package/src/modules/workflows/data/validators.ts +121 -0
  123. package/src/modules/workflows/di.ts +4 -0
  124. package/src/modules/workflows/i18n/de.json +10 -1
  125. package/src/modules/workflows/i18n/en.json +10 -1
  126. package/src/modules/workflows/i18n/es.json +10 -1
  127. package/src/modules/workflows/i18n/pl.json +10 -1
  128. package/src/modules/workflows/lib/activity-executor.ts +86 -2
  129. package/src/modules/workflows/lib/activity-queue-types.ts +18 -11
  130. package/src/modules/workflows/lib/activity-worker-handler.ts +29 -0
  131. package/src/modules/workflows/lib/duration.ts +51 -0
  132. package/src/modules/workflows/lib/event-logger.ts +1 -0
  133. package/src/modules/workflows/lib/format-validation-error.ts +30 -0
  134. package/src/modules/workflows/lib/graph-utils.ts +3 -0
  135. package/src/modules/workflows/lib/node-type-icons.ts +6 -2
  136. package/src/modules/workflows/lib/signal-handler.ts +62 -24
  137. package/src/modules/workflows/lib/step-handler.ts +107 -50
  138. package/src/modules/workflows/lib/timer-handler.ts +213 -0
  139. package/src/modules/workflows/lib/workflow-executor.ts +1 -1
  140. package/src/modules/workflows/workers/workflow-activities.worker.ts +33 -7
@@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'
5
5
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
6
6
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
7
7
  import { apiFetch } from '@open-mercato/ui/backend/utils/api'
8
+ import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
8
9
  import { useT } from '@open-mercato/shared/lib/i18n/context'
9
10
  import {
10
11
  workflowDefinitionFormSchema,
@@ -18,6 +19,7 @@ import { StepsEditor } from '../../../components/StepsEditor'
18
19
  import { TransitionsEditor } from '../../../components/TransitionsEditor'
19
20
  import { Alert, AlertDescription, AlertTitle } from '@open-mercato/ui/primitives/alert'
20
21
  import { Zap } from 'lucide-react'
22
+ import { formatWorkflowValidationError } from '../../../lib/format-validation-error'
21
23
 
22
24
  export default function CreateWorkflowDefinitionPage() {
23
25
  const router = useRouter()
@@ -33,8 +35,8 @@ export default function CreateWorkflowDefinitionPage() {
33
35
  })
34
36
 
35
37
  if (!response.ok) {
36
- const error = await response.json()
37
- throw new Error(error.error || t('workflows.errors.createFailed'))
38
+ const errorBody = await readJsonSafe<{ error?: string; details?: Array<{ path?: Array<string | number>; message?: string }> }>(response, null)
39
+ throw new Error(formatWorkflowValidationError(errorBody, t('workflows.errors.createFailed')))
38
40
  }
39
41
 
40
42
  router.push('/backend/definitions')
@@ -989,7 +989,7 @@ export default function VisualEditorPage() {
989
989
  <p className="mb-3 text-xs text-muted-foreground">{t('workflows.visualEditor.tapToAdd')}</p>
990
990
 
991
991
  <div className="flex gap-2 overflow-x-auto pb-1">
992
- {(['start', 'userTask', 'automated', 'waitForSignal', 'subWorkflow', 'end'] as const).map((nodeType) => {
992
+ {(['start', 'userTask', 'automated', 'waitForSignal', 'waitForTimer', 'subWorkflow', 'end'] as const).map((nodeType) => {
993
993
  const Icon = NODE_TYPE_ICONS[nodeType]
994
994
  return (
995
995
  <button
@@ -1078,6 +1078,21 @@ export default function VisualEditorPage() {
1078
1078
  <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.waitForSignal.description}</div>
1079
1079
  </button>
1080
1080
 
1081
+ {/* WAIT_FOR_TIMER Step */}
1082
+ <button
1083
+ onClick={() => handleAddNode('waitForTimer')}
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.waitForTimer} opacity-60 transition-opacity group-hover:opacity-100`}>
1087
+ {(() => {
1088
+ const Icon = NODE_TYPE_ICONS.waitForTimer
1089
+ return <Icon className="h-4 w-4" />
1090
+ })()}
1091
+ </div>
1092
+ <div className="text-sm font-semibold text-foreground">{NODE_TYPE_LABELS.waitForTimer.title}</div>
1093
+ <div className="mt-0.5 text-xs text-muted-foreground">{NODE_TYPE_LABELS.waitForTimer.description}</div>
1094
+ </button>
1095
+
1081
1096
  {/* SUB_WORKFLOW Step */}
1082
1097
  <button
1083
1098
  onClick={() => handleAddNode('subWorkflow')}
@@ -1182,6 +1197,7 @@ function getDefaultLabel(nodeType: string): string {
1182
1197
  automated: 'New Automated Task',
1183
1198
  decision: 'Decision Point',
1184
1199
  waitForSignal: 'Wait for Signal',
1200
+ waitForTimer: 'Wait for Timer',
1185
1201
  }
1186
1202
  return labels[nodeType] || 'New Step'
1187
1203
  }
@@ -1194,6 +1210,7 @@ function getDefaultBadge(nodeType: string): string {
1194
1210
  automated: 'Automated',
1195
1211
  decision: 'Decision',
1196
1212
  waitForSignal: 'Wait for Signal',
1213
+ waitForTimer: 'Wait for Timer',
1197
1214
  }
1198
1215
  return badges[nodeType] || 'Task'
1199
1216
  }
@@ -273,6 +273,45 @@ export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEdit
273
273
  </div>
274
274
  </div>
275
275
 
276
+ {activity.activityType === 'WAIT' && (
277
+ <div className="space-y-3">
278
+ <div>
279
+ <Label htmlFor={`activity-${index}-duration`} className="text-xs">
280
+ {t('workflows.activities.waitDuration')}
281
+ </Label>
282
+ <Input
283
+ id={`activity-${index}-duration`}
284
+ value={activity.config?.duration || ''}
285
+ onChange={(e) => updateActivity(index, 'config', { ...activity.config, duration: e.target.value, until: undefined })}
286
+ placeholder={t('workflows.activities.waitDurationPlaceholder')}
287
+ disabled={!!activity.config?.until}
288
+ className="mt-1"
289
+ />
290
+ <p className="text-xs text-muted-foreground mt-1">
291
+ {t('workflows.activities.waitDurationDescription')}
292
+ </p>
293
+ </div>
294
+ <div className="text-xs text-center text-muted-foreground">{t('workflows.activities.waitOr')}</div>
295
+ <div>
296
+ <Label htmlFor={`activity-${index}-until`} className="text-xs">
297
+ {t('workflows.activities.waitUntil')}
298
+ </Label>
299
+ <Input
300
+ id={`activity-${index}-until`}
301
+ type="datetime-local"
302
+ value={activity.config?.until ? activity.config.until.slice(0, 16) : ''}
303
+ onChange={(e) => updateActivity(index, 'config', { ...activity.config, until: e.target.value ? new Date(e.target.value).toISOString() : undefined, duration: undefined })}
304
+ disabled={!!activity.config?.duration}
305
+ className="mt-1"
306
+ />
307
+ <p className="text-xs text-muted-foreground mt-1">
308
+ {t('workflows.activities.waitUntilDescription')}
309
+ </p>
310
+ </div>
311
+ </div>
312
+ )}
313
+
314
+ {activity.activityType !== 'WAIT' && (
276
315
  <div>
277
316
  <Label htmlFor={`activity-${index}-config`} className="text-xs">
278
317
  {t('workflows.activities.config')} (JSON)
@@ -293,6 +332,7 @@ export function ActivitiesEditor({ value = [], onChange, error }: ActivitiesEdit
293
332
  className="mt-1 font-mono text-xs"
294
333
  />
295
334
  </div>
335
+ )}
296
336
  </div>
297
337
  </div>
298
338
  ))}
@@ -21,6 +21,7 @@ import {JsonBuilder} from '@open-mercato/ui/backend/JsonBuilder'
21
21
  import {StartPreConditionsEditor, type StartPreCondition} from './fields/StartPreConditionsEditor'
22
22
  import {useT} from '@open-mercato/shared/lib/i18n/context'
23
23
  import {useConfirmDialog} from '@open-mercato/ui/backend/confirm-dialog'
24
+ import {isFutureIsoDateString, isValidDurationString} from '../data/validators'
24
25
 
25
26
  export interface NodeEditDialogProps {
26
27
  node: Node | null
@@ -81,6 +82,10 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
81
82
  const [signalName, setSignalName] = useState('')
82
83
  const [signalTimeout, setSignalTimeout] = useState('')
83
84
 
85
+ // Wait for timer configuration fields
86
+ const [timerDuration, setTimerDuration] = useState('')
87
+ const [timerUntil, setTimerUntil] = useState('')
88
+
84
89
  // Step activities state (for AUTOMATED steps)
85
90
  const [stepActivities, setStepActivities] = useState<any[]>([])
86
91
  const [expandedStepActivities, setExpandedStepActivities] = useState<Set<number>>(new Set())
@@ -88,6 +93,9 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
88
93
  // Pre-conditions state (for START steps)
89
94
  const [preConditions, setPreConditions] = useState<StartPreCondition[]>([])
90
95
 
96
+ // Inline validation errors keyed by field name
97
+ const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
98
+
91
99
  // Convert JSON Schema to our custom format
92
100
  const convertJsonSchemaToFields = (schema: any): FormField[] => {
93
101
  if (!schema || !schema.properties) return []
@@ -211,6 +219,15 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
211
219
  setSignalTimeout('')
212
220
  }
213
221
 
222
+ // Load timer configuration
223
+ if (node.type === 'waitForTimer') {
224
+ setTimerDuration(nodeData?.config?.duration || '')
225
+ setTimerUntil(nodeData?.config?.until || '')
226
+ } else {
227
+ setTimerDuration('')
228
+ setTimerUntil('')
229
+ }
230
+
214
231
  // Load step activities (for AUTOMATED steps)
215
232
  if (node.type === 'automated' && nodeData?.activities) {
216
233
  setStepActivities(nodeData.activities)
@@ -257,6 +274,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
257
274
  }
258
275
  setAdvancedConfig(advancedFields)
259
276
  setExpandedFields(new Set())
277
+ setFieldErrors({})
260
278
  }
261
279
  }, [node, isOpen])
262
280
 
@@ -313,6 +331,30 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
313
331
  const handleSave = () => {
314
332
  if (!node) return
315
333
 
334
+ // Pre-save validation for wait-related fields. Surface inline errors instead
335
+ // of silently saving an invalid value that will only blow up later when the
336
+ // whole workflow is serialized through the API zod schema.
337
+ const errors: Record<string, string> = {}
338
+
339
+ if (node.type === 'waitForTimer') {
340
+ if (timerDuration && !isValidDurationString(timerDuration)) {
341
+ errors.timerDuration = t('workflows.validation.invalidDuration')
342
+ }
343
+ if (timerUntil && !isFutureIsoDateString(timerUntil)) {
344
+ errors.timerUntil = t('workflows.validation.untilMustBeFuture')
345
+ }
346
+ }
347
+
348
+ if (node.type === 'waitForSignal' && signalTimeout && !isValidDurationString(signalTimeout)) {
349
+ errors.signalTimeout = t('workflows.validation.invalidDuration')
350
+ }
351
+
352
+ if (Object.keys(errors).length > 0) {
353
+ setFieldErrors(errors)
354
+ return
355
+ }
356
+ setFieldErrors({})
357
+
316
358
  // Validate and sanitize step ID
317
359
  const sanitizedId = sanitizeId(node.id)
318
360
  if (sanitizedId !== node.id) {
@@ -407,6 +449,17 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
407
449
  }
408
450
  }
409
451
 
452
+ // Wait for timer specific fields (duration XOR until)
453
+ if (node.type === 'waitForTimer') {
454
+ const config: any = {}
455
+ if (timerDuration) {
456
+ config.duration = timerDuration
457
+ } else if (timerUntil) {
458
+ config.until = timerUntil
459
+ }
460
+ updates.config = Object.keys(config).length > 0 ? config : undefined
461
+ }
462
+
410
463
  // Step activities (for AUTOMATED steps)
411
464
  if (node.type === 'automated' && stepActivities.length > 0) {
412
465
  updates.activities = stepActivities
@@ -449,6 +502,9 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
449
502
  userTask: t('workflows.nodeTypes.userTask'),
450
503
  automated: t('workflows.nodeTypes.automated'),
451
504
  decision: t('workflows.nodeTypes.decision'),
505
+ waitForSignal: t('workflows.nodeTypes.waitForSignal'),
506
+ waitForTimer: t('workflows.nodeTypes.waitForTimer'),
507
+ subWorkflow: t('workflows.nodeTypes.subWorkflow'),
452
508
  }[node.type || 'automated']
453
509
 
454
510
  // START nodes are partially editable (pre-conditions only), END nodes are not editable
@@ -544,27 +600,30 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
544
600
  </p>
545
601
  </div>
546
602
 
547
- {/* Timeout */}
548
- <div>
549
- <label className="block text-sm font-medium text-gray-700 mb-1">
550
- {t('workflows.form.timeout')}
551
- </label>
552
- <Input
553
- type="text"
554
- value={timeout}
555
- onChange={(e) => setTimeout(e.target.value)}
556
- placeholder={t('workflows.form.placeholders.timeout')}
557
- />
558
- <p className="text-xs text-gray-500 mt-1">
559
- {t('workflows.form.descriptions.timeout')}
560
- </p>
561
- </div>
603
+ {/* Timeout — hidden for wait nodes that already expose their own time-bound config
604
+ (waitForTimer uses duration/until, waitForSignal uses signalConfig.timeout). */}
605
+ {node.type !== 'waitForSignal' && node.type !== 'waitForTimer' && (
606
+ <div>
607
+ <label className="block text-sm font-medium text-gray-700 mb-1">
608
+ {t('workflows.form.timeout')}
609
+ </label>
610
+ <Input
611
+ type="text"
612
+ value={timeout}
613
+ onChange={(e) => setTimeout(e.target.value)}
614
+ placeholder={t('workflows.form.placeholders.timeout')}
615
+ />
616
+ <p className="text-xs text-gray-500 mt-1">
617
+ {t('workflows.form.descriptions.timeout')}
618
+ </p>
619
+ </div>
620
+ )}
562
621
 
563
622
  {/* User Task Configuration */}
564
623
  {node.type === 'userTask' && (
565
624
  <>
566
625
  <div className="border-t border-gray-200 pt-4 mt-4">
567
- <h3 className="text-sm font-semibold text-gray-900 mb-3">
626
+ <h3 className="text-sm font-semibold text-foreground mb-3">
568
627
  {t('workflows.nodeEditor.userTaskConfig')}
569
628
  </h3>
570
629
  </div>
@@ -618,7 +677,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
618
677
  <div className="border-t border-gray-200 pt-4 mt-4">
619
678
  <div className="flex items-center justify-between mb-3">
620
679
  <div>
621
- <h3 className="text-sm font-semibold text-gray-900">
680
+ <h3 className="text-sm font-semibold text-foreground">
622
681
  {t('workflows.form.formFields', { count: formFields.length })}
623
682
  </h3>
624
683
  <p className="text-xs text-gray-500 mt-0.5">
@@ -663,7 +722,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
663
722
  >
664
723
  <div className="flex-1">
665
724
  <div className="flex items-center gap-2">
666
- <span className="text-sm font-semibold text-gray-900">
725
+ <span className="text-sm font-semibold text-foreground">
667
726
  {field.label || field.name}
668
727
  </span>
669
728
  <Badge variant="secondary" className="text-xs">
@@ -819,7 +878,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
819
878
  <div className="border-t border-gray-200 pt-4 mt-4">
820
879
  <div className="flex items-center justify-between mb-3">
821
880
  <div>
822
- <h3 className="text-sm font-semibold text-gray-900">
881
+ <h3 className="text-sm font-semibold text-foreground">
823
882
  {t('workflows.form.stepActivities', { count: stepActivities.length })}
824
883
  </h3>
825
884
  <p className="text-xs text-gray-500 mt-0.5">
@@ -874,7 +933,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
874
933
  >
875
934
  <div className="flex-1">
876
935
  <div className="flex items-center gap-2">
877
- <span className="text-sm font-semibold text-gray-900">
936
+ <span className="text-sm font-semibold text-foreground">
878
937
  {activity.activityName || activity.activityId || `Activity ${index + 1}`}
879
938
  </span>
880
939
  <Badge variant="secondary" className="text-xs">
@@ -957,6 +1016,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
957
1016
  <SelectItem value="EMIT_EVENT">{t('workflows.activities.types.EMIT_EVENT')}</SelectItem>
958
1017
  <SelectItem value="CALL_WEBHOOK">{t('workflows.activities.types.CALL_WEBHOOK')}</SelectItem>
959
1018
  <SelectItem value="EXECUTE_FUNCTION">{t('workflows.activities.types.EXECUTE_FUNCTION')}</SelectItem>
1019
+ <SelectItem value="WAIT">{t('workflows.activities.types.WAIT')}</SelectItem>
960
1020
  </SelectContent>
961
1021
  </Select>
962
1022
  </div>
@@ -1065,7 +1125,48 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
1065
1125
  </label>
1066
1126
  </div>
1067
1127
 
1068
- {/* Activity Config JSON */}
1128
+ {/* WAIT Activity: Duration / Until fields */}
1129
+ {activity.activityType === 'WAIT' && (
1130
+ <div className="space-y-3">
1131
+ <div>
1132
+ <label className="block text-xs font-medium text-gray-700 mb-1">
1133
+ {t('workflows.activities.waitDuration')}
1134
+ </label>
1135
+ <Input
1136
+ size="sm"
1137
+ type="text"
1138
+ value={activity.config?.duration || ''}
1139
+ onChange={(e) => {
1140
+ const updated = [...stepActivities]
1141
+ updated[index].config = { ...updated[index].config, duration: e.target.value, until: undefined }
1142
+ setStepActivities(updated)
1143
+ }}
1144
+ placeholder={t('workflows.activities.waitDurationPlaceholder')}
1145
+ />
1146
+ <p className="text-xs text-muted-foreground mt-1">{t('workflows.activities.waitDurationDescription')}</p>
1147
+ </div>
1148
+ <div className="text-xs text-center text-muted-foreground">{t('workflows.activities.waitOr')}</div>
1149
+ <div>
1150
+ <label className="block text-xs font-medium text-gray-700 mb-1">
1151
+ {t('workflows.activities.waitUntil')}
1152
+ </label>
1153
+ <Input
1154
+ size="sm"
1155
+ type="datetime-local"
1156
+ value={activity.config?.until ? activity.config.until.slice(0, 16) : ''}
1157
+ onChange={(e) => {
1158
+ const updated = [...stepActivities]
1159
+ updated[index].config = { ...updated[index].config, until: e.target.value ? new Date(e.target.value).toISOString() : undefined, duration: undefined }
1160
+ setStepActivities(updated)
1161
+ }}
1162
+ />
1163
+ <p className="text-xs text-muted-foreground mt-1">{t('workflows.activities.waitUntilDescription')}</p>
1164
+ </div>
1165
+ </div>
1166
+ )}
1167
+
1168
+ {/* Activity Config JSON (hidden for WAIT) */}
1169
+ {activity.activityType !== 'WAIT' && (
1069
1170
  <div>
1070
1171
  <label className="block text-xs font-medium text-gray-700 mb-1">
1071
1172
  {t('workflows.form.configuration')}
@@ -1082,6 +1183,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
1082
1183
  {t('workflows.form.descriptions.activityConfig')}
1083
1184
  </p>
1084
1185
  </div>
1186
+ )}
1085
1187
 
1086
1188
  {/* Delete Button */}
1087
1189
  <div className="pt-3 border-t border-gray-100">
@@ -1114,7 +1216,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
1114
1216
  {node.type === 'subWorkflow' && (
1115
1217
  <>
1116
1218
  <div className="border-t border-gray-200 pt-4 mt-4">
1117
- <h3 className="text-sm font-semibold text-gray-900 mb-3">
1219
+ <h3 className="text-sm font-semibold text-foreground mb-3">
1118
1220
  {t('workflows.form.subWorkflowConfig')}
1119
1221
  </h3>
1120
1222
  </div>
@@ -1164,7 +1266,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
1164
1266
  <div className="border-t border-gray-200 pt-4 mt-4">
1165
1267
  <div className="flex items-center justify-between mb-3">
1166
1268
  <div>
1167
- <h4 className="text-sm font-semibold text-gray-900">
1269
+ <h4 className="text-sm font-semibold text-foreground">
1168
1270
  {t('workflows.form.inputMapping', { count: inputMappings.length })}
1169
1271
  </h4>
1170
1272
  <p className="text-xs text-gray-500 mt-0.5">
@@ -1237,7 +1339,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
1237
1339
  <div className="border-t border-gray-200 pt-4 mt-4">
1238
1340
  <div className="flex items-center justify-between mb-3">
1239
1341
  <div>
1240
- <h4 className="text-sm font-semibold text-gray-900">
1342
+ <h4 className="text-sm font-semibold text-foreground">
1241
1343
  {t('workflows.form.outputMapping', { count: outputMappings.length })}
1242
1344
  </h4>
1243
1345
  <p className="text-xs text-gray-500 mt-0.5">
@@ -1312,7 +1414,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
1312
1414
  {node.type === 'waitForSignal' && (
1313
1415
  <>
1314
1416
  <div className="border-t border-gray-200 pt-4 mt-4">
1315
- <h3 className="text-sm font-semibold text-gray-900 mb-3">
1417
+ <h3 className="text-sm font-semibold text-foreground mb-3">
1316
1418
  {t('workflows.form.signalConfig')}
1317
1419
  </h3>
1318
1420
  </div>
@@ -1339,12 +1441,98 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
1339
1441
  <Input
1340
1442
  type="text"
1341
1443
  value={signalTimeout}
1342
- onChange={(e) => setSignalTimeout(e.target.value)}
1444
+ onChange={(e) => {
1445
+ setSignalTimeout(e.target.value)
1446
+ if (fieldErrors.signalTimeout) {
1447
+ const next = { ...fieldErrors }
1448
+ delete next.signalTimeout
1449
+ setFieldErrors(next)
1450
+ }
1451
+ }}
1343
1452
  placeholder={t('workflows.form.placeholders.signalTimeout')}
1453
+ aria-invalid={fieldErrors.signalTimeout ? true : undefined}
1344
1454
  />
1345
- <p className="text-xs text-gray-500 mt-1">
1346
- {t('workflows.form.descriptions.signalTimeout')}
1347
- </p>
1455
+ {fieldErrors.signalTimeout ? (
1456
+ <p className="text-xs text-destructive mt-1">
1457
+ {fieldErrors.signalTimeout}
1458
+ </p>
1459
+ ) : (
1460
+ <p className="text-xs text-gray-500 mt-1">
1461
+ {t('workflows.form.descriptions.signalTimeout')}
1462
+ </p>
1463
+ )}
1464
+ </div>
1465
+ </>
1466
+ )}
1467
+
1468
+ {/* Wait for Timer Configuration */}
1469
+ {node.type === 'waitForTimer' && (
1470
+ <>
1471
+ <div className="border-t border-gray-200 pt-4 mt-4">
1472
+ <h3 className="text-sm font-semibold text-foreground mb-3">
1473
+ {t('workflows.steps.types.WAIT_FOR_TIMER')}
1474
+ </h3>
1475
+ </div>
1476
+
1477
+ <div>
1478
+ <label className="block text-sm font-medium text-gray-700 mb-1">
1479
+ {t('workflows.activities.waitDuration')}
1480
+ </label>
1481
+ <Input
1482
+ type="text"
1483
+ value={timerDuration}
1484
+ onChange={(e) => {
1485
+ setTimerDuration(e.target.value)
1486
+ if (e.target.value) setTimerUntil('')
1487
+ if (fieldErrors.timerDuration) {
1488
+ const next = { ...fieldErrors }
1489
+ delete next.timerDuration
1490
+ setFieldErrors(next)
1491
+ }
1492
+ }}
1493
+ placeholder={t('workflows.activities.waitDurationPlaceholder')}
1494
+ aria-invalid={fieldErrors.timerDuration ? true : undefined}
1495
+ />
1496
+ {fieldErrors.timerDuration ? (
1497
+ <p className="text-xs text-destructive mt-1">
1498
+ {fieldErrors.timerDuration}
1499
+ </p>
1500
+ ) : (
1501
+ <p className="text-xs text-gray-500 mt-1">
1502
+ {t('workflows.activities.waitDurationDescription')}
1503
+ </p>
1504
+ )}
1505
+ </div>
1506
+
1507
+ <div>
1508
+ <label className="block text-sm font-medium text-gray-700 mb-1">
1509
+ {t('workflows.activities.waitUntil')}
1510
+ </label>
1511
+ <Input
1512
+ type="datetime-local"
1513
+ value={timerUntil ? timerUntil.slice(0, 16) : ''}
1514
+ min={new Date(Date.now() - new Date().getTimezoneOffset() * 60000).toISOString().slice(0, 16)}
1515
+ onChange={(e) => {
1516
+ const next = e.target.value ? new Date(e.target.value).toISOString() : ''
1517
+ setTimerUntil(next)
1518
+ if (next) setTimerDuration('')
1519
+ if (fieldErrors.timerUntil) {
1520
+ const nextErrors = { ...fieldErrors }
1521
+ delete nextErrors.timerUntil
1522
+ setFieldErrors(nextErrors)
1523
+ }
1524
+ }}
1525
+ aria-invalid={fieldErrors.timerUntil ? true : undefined}
1526
+ />
1527
+ {fieldErrors.timerUntil ? (
1528
+ <p className="text-xs text-destructive mt-1">
1529
+ {fieldErrors.timerUntil}
1530
+ </p>
1531
+ ) : (
1532
+ <p className="text-xs text-gray-500 mt-1">
1533
+ {t('workflows.activities.waitUntilDescription')}
1534
+ </p>
1535
+ )}
1348
1536
  </div>
1349
1537
  </>
1350
1538
  )}
@@ -1356,7 +1544,7 @@ export function NodeEditDialog({ node, isOpen, onClose, onSave, onDelete }: Node
1356
1544
  onClick={() => setShowAdvanced(!showAdvanced)}
1357
1545
  className="flex items-center justify-between w-full text-left"
1358
1546
  >
1359
- <h3 className="text-sm font-semibold text-gray-900">
1547
+ <h3 className="text-sm font-semibold text-foreground">
1360
1548
  {t('workflows.form.advancedConfiguration')}
1361
1549
  </h3>
1362
1550
  <svg
@@ -203,6 +203,42 @@ export function StepsEditor({ value = [], onChange, error }: StepsEditorProps) {
203
203
  </div>
204
204
  </div>
205
205
 
206
+ {step.stepType === 'WAIT_FOR_TIMER' && (
207
+ <div className="grid grid-cols-1 sm:grid-cols-[1fr_auto_1fr] gap-3 items-end">
208
+ <div>
209
+ <Label htmlFor={`step-${index}-duration`} className="text-xs">
210
+ {t('workflows.activities.waitDuration')}
211
+ </Label>
212
+ <Input
213
+ id={`step-${index}-duration`}
214
+ value={step.config?.duration || ''}
215
+ onChange={(e) => updateStep(index, 'config', { ...step.config, duration: e.target.value, until: undefined })}
216
+ placeholder={t('workflows.activities.waitDurationPlaceholder')}
217
+ className="mt-1"
218
+ />
219
+ <p className="text-xs text-muted-foreground mt-1">
220
+ {t('workflows.activities.waitDurationDescription')}
221
+ </p>
222
+ </div>
223
+ <span className="text-xs text-muted-foreground pb-6">{t('workflows.activities.waitOr')}</span>
224
+ <div>
225
+ <Label htmlFor={`step-${index}-until`} className="text-xs">
226
+ {t('workflows.activities.waitUntil')}
227
+ </Label>
228
+ <Input
229
+ id={`step-${index}-until`}
230
+ type="datetime-local"
231
+ value={step.config?.until ? step.config.until.slice(0, 16) : ''}
232
+ onChange={(e) => updateStep(index, 'config', { ...step.config, until: e.target.value ? new Date(e.target.value).toISOString() : undefined, duration: undefined })}
233
+ className="mt-1"
234
+ />
235
+ <p className="text-xs text-muted-foreground mt-1">
236
+ {t('workflows.activities.waitUntilDescription')}
237
+ </p>
238
+ </div>
239
+ </div>
240
+ )}
241
+
206
242
  <div>
207
243
  <Label htmlFor={`step-${index}-description`} className="text-xs">
208
244
  {t('workflows.steps.singular')} {t('workflows.definitions.description')}
@@ -17,7 +17,7 @@ import {
17
17
  ConnectionMode,
18
18
  MarkerType,
19
19
  } from '@xyflow/react'
20
- import {StartNode, EndNode, UserTaskNode, AutomatedNode, SubWorkflowNode, WaitForSignalNode} from './nodes'
20
+ import {StartNode, EndNode, UserTaskNode, AutomatedNode, SubWorkflowNode, WaitForSignalNode, WaitForTimerNode} from './nodes'
21
21
  import { WorkflowTransitionEdge } from './WorkflowTransitionEdge'
22
22
  import { STATUS_COLORS } from '../lib/status-colors'
23
23
  import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
@@ -151,6 +151,7 @@ export function WorkflowGraph({
151
151
  automated: AutomatedNode,
152
152
  subWorkflow: SubWorkflowNode,
153
153
  waitForSignal: WaitForSignalNode,
154
+ waitForTimer: WaitForTimerNode,
154
155
  }),
155
156
  []
156
157
  )