@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
@@ -18,8 +18,9 @@ import {
18
18
  updateWorkflowDefinitionInputSchema,
19
19
  type UpdateWorkflowDefinitionApiInput,
20
20
  } from '../../../data/validators'
21
- import { serializeWorkflowDefinition } from '../serialize'
21
+ import { serializeWorkflowDefinition, serializeCodeWorkflowDefinition } from '../serialize'
22
22
  import { invalidateTriggerCache } from '../../../lib/event-trigger-service'
23
+ import { getCodeWorkflow } from '../../../lib/code-registry'
23
24
 
24
25
  export const metadata = {
25
26
  requireAuth: true,
@@ -55,6 +56,21 @@ export async function GET(
55
56
  const tenantId = auth.tenantId
56
57
  const orgFilter = resolveOrganizationScopeFilter(scope, auth)
57
58
 
59
+ // Handle code-based workflow definitions (code:<workflowId>)
60
+ if (params.id.startsWith('code:')) {
61
+ const workflowId = params.id.slice(5)
62
+ const codeDef = getCodeWorkflow(workflowId)
63
+ if (!codeDef) {
64
+ return NextResponse.json(
65
+ { error: 'Workflow definition not found' },
66
+ { status: 404 }
67
+ )
68
+ }
69
+ return NextResponse.json({
70
+ data: serializeCodeWorkflowDefinition(codeDef, params.id),
71
+ })
72
+ }
73
+
58
74
  const definition = await em.findOne(WorkflowDefinition, {
59
75
  id: params.id,
60
76
  tenantId,
@@ -136,6 +152,90 @@ export async function PUT(
136
152
 
137
153
  const input: UpdateWorkflowDefinitionApiInput = validation.data
138
154
 
155
+ // Handle customizing a code-based workflow definition
156
+ if (params.id.startsWith('code:')) {
157
+ const workflowId = params.id.slice(5)
158
+ const codeDef = getCodeWorkflow(workflowId)
159
+ if (!codeDef) {
160
+ return NextResponse.json(
161
+ { error: 'Workflow definition not found' },
162
+ { status: 404 }
163
+ )
164
+ }
165
+
166
+ // Check if an override already exists (including soft-deleted, due to unique constraint on workflowId+tenantId)
167
+ const existingOverride = await em.findOne(WorkflowDefinition, {
168
+ workflowId: codeDef.workflowId,
169
+ tenantId,
170
+ })
171
+
172
+ let savedOverride: WorkflowDefinition
173
+ if (existingOverride) {
174
+ // Revive if soft-deleted, then apply updates
175
+ existingOverride.deletedAt = null
176
+ existingOverride.workflowName = codeDef.workflowName
177
+ existingOverride.description = codeDef.description
178
+ existingOverride.version = codeDef.version
179
+ existingOverride.definition = input.definition ?? codeDef.definition
180
+ existingOverride.metadata = codeDef.metadata ?? null
181
+ existingOverride.enabled = input.enabled ?? codeDef.enabled
182
+ existingOverride.codeWorkflowId = codeDef.workflowId
183
+ existingOverride.updatedBy = auth.sub
184
+ existingOverride.updatedAt = new Date()
185
+ await em.flush()
186
+ savedOverride = existingOverride
187
+ } else {
188
+ // Create DB override row from code definition + user changes
189
+ const override = em.create(WorkflowDefinition, {
190
+ workflowId: codeDef.workflowId,
191
+ workflowName: codeDef.workflowName,
192
+ description: codeDef.description,
193
+ version: codeDef.version,
194
+ definition: input.definition ?? codeDef.definition,
195
+ metadata: codeDef.metadata,
196
+ enabled: input.enabled ?? codeDef.enabled,
197
+ codeWorkflowId: codeDef.workflowId,
198
+ tenantId,
199
+ organizationId,
200
+ createdBy: auth.sub,
201
+ updatedBy: auth.sub,
202
+ createdAt: new Date(),
203
+ updatedAt: new Date(),
204
+ })
205
+
206
+ em.persist(override)
207
+ await em.flush()
208
+ savedOverride = override
209
+ }
210
+
211
+ try {
212
+ const eventBus = container.resolve('eventBus') as
213
+ | { emitEvent(event: string, payload: unknown, options?: unknown): Promise<void> }
214
+ | undefined
215
+ if (eventBus && typeof eventBus.emitEvent === 'function') {
216
+ await eventBus.emitEvent(
217
+ 'workflows.definition.customized',
218
+ {
219
+ id: savedOverride.id,
220
+ workflowId: savedOverride.workflowId,
221
+ codeWorkflowId: savedOverride.codeWorkflowId ?? null,
222
+ tenantId: savedOverride.tenantId,
223
+ organizationId: savedOverride.organizationId,
224
+ userId: auth.sub ?? null,
225
+ },
226
+ { tenantId: savedOverride.tenantId, organizationId: savedOverride.organizationId, persistent: true },
227
+ )
228
+ }
229
+ } catch (eventError) {
230
+ console.error('Failed to emit workflows.definition.customized event:', eventError)
231
+ }
232
+
233
+ return NextResponse.json({
234
+ data: serializeWorkflowDefinition(savedOverride),
235
+ message: 'Workflow definition customized successfully',
236
+ })
237
+ }
238
+
139
239
  // Find existing definition
140
240
  const definition = await em.findOne(WorkflowDefinition, {
141
241
  id: params.id,
@@ -151,14 +251,18 @@ export async function PUT(
151
251
  )
152
252
  }
153
253
 
154
- // Update fields. workflowId and version are intentionally ignored —
155
- // they identify the row and bumping versions is handled elsewhere.
254
+ // Update fields. workflowId is intentionally ignored — it identifies the
255
+ // row and renaming would break references. Version is user-managed and
256
+ // applied when supplied so the edit form can bump it explicitly.
156
257
  if (input.workflowName !== undefined) {
157
258
  definition.workflowName = input.workflowName
158
259
  }
159
260
  if (input.description !== undefined) {
160
261
  definition.description = input.description
161
262
  }
263
+ if (input.version !== undefined) {
264
+ definition.version = input.version
265
+ }
162
266
  if (input.definition !== undefined) {
163
267
  definition.definition = input.definition
164
268
  }
@@ -295,7 +399,7 @@ export const openApi = {
295
399
  description: 'Get a single workflow definition by ID. Returns the complete workflow structure including steps and transitions (with embedded activities).',
296
400
  tags: ['Workflows'],
297
401
  pathParams: z.object({
298
- id: z.string().uuid(),
402
+ id: z.string().describe('UUID for DB definitions, or "code:<workflowId>" for code-based definitions'),
299
403
  }),
300
404
  responses: [
301
405
  {
@@ -17,8 +17,9 @@ import {
17
17
  createWorkflowDefinitionInputSchema,
18
18
  type CreateWorkflowDefinitionApiInput,
19
19
  } from '../../data/validators'
20
- import { serializeWorkflowDefinition } from './serialize'
20
+ import { serializeWorkflowDefinition, serializeCodeWorkflowDefinition } from './serialize'
21
21
  import { invalidateTriggerCache } from '../../lib/event-trigger-service'
22
+ import { getAllCodeWorkflows } from '../../lib/code-registry'
22
23
 
23
24
  export const metadata = {
24
25
  requireAuth: true,
@@ -97,18 +98,61 @@ export async function GET(request: NextRequest) {
97
98
  ]
98
99
  }
99
100
 
100
- const [definitions, total] = await em.findAndCount(
101
- WorkflowDefinition,
102
- where,
103
- {
104
- orderBy: { createdAt: 'DESC' },
105
- limit,
106
- offset,
107
- }
101
+ // Determine which code workflows are shadowed by a DB row (so we can
102
+ // exclude them from the code-only list) without loading all DB rows.
103
+ const enabledFilter = enabled !== null ? enabled === 'true' : null
104
+ const searchLower = search ? search.toLowerCase() : null
105
+ const allCodeIds = getAllCodeWorkflows().map((cw) => cw.workflowId)
106
+ const shadowed = allCodeIds.length > 0
107
+ ? new Set(
108
+ (
109
+ await em.find(
110
+ WorkflowDefinition,
111
+ { ...where, workflowId: { $in: allCodeIds } },
112
+ { fields: ['workflowId'] as const },
113
+ )
114
+ ).map((d: WorkflowDefinition) => d.workflowId),
115
+ )
116
+ : new Set<string>()
117
+
118
+ const codeOnly = getAllCodeWorkflows()
119
+ .filter((cw) => !shadowed.has(cw.workflowId))
120
+ .filter((cw) => {
121
+ if (searchLower) {
122
+ const matches =
123
+ cw.workflowId.toLowerCase().includes(searchLower) ||
124
+ cw.workflowName.toLowerCase().includes(searchLower)
125
+ if (!matches) return false
126
+ }
127
+ if (enabledFilter !== null && cw.enabled !== enabledFilter) return false
128
+ if (workflowId && cw.workflowId !== workflowId) return false
129
+ return true
130
+ })
131
+ .map((cw) => serializeCodeWorkflowDefinition(cw, `code:${cw.workflowId}`))
132
+
133
+ const dbCount = await em.count(WorkflowDefinition, where)
134
+ const total = dbCount + codeOnly.length
135
+
136
+ // Fetch only the prefix of DB rows we might need to fill the requested
137
+ // page after merging with the (already-filtered) code-only list.
138
+ const dbWindowLimit = offset + limit
139
+ const dbWindow = dbWindowLimit > 0
140
+ ? await em.find(WorkflowDefinition, where, {
141
+ orderBy: { workflowName: 'ASC' },
142
+ limit: dbWindowLimit,
143
+ })
144
+ : []
145
+
146
+ const serializedDb = dbWindow.map(serializeWorkflowDefinition)
147
+
148
+ const merged = [...serializedDb, ...codeOnly].sort((a, b) =>
149
+ a.workflowName.localeCompare(b.workflowName),
108
150
  )
109
151
 
152
+ const paginated = merged.slice(offset, offset + limit)
153
+
110
154
  return NextResponse.json({
111
- data: definitions.map(serializeWorkflowDefinition),
155
+ data: paginated,
112
156
  pagination: {
113
157
  total,
114
158
  limit,
@@ -1,6 +1,20 @@
1
1
  import type { WorkflowDefinition } from '../../data/entities'
2
+ import type { CodeWorkflowDefinition } from '@open-mercato/shared/modules/workflows'
3
+ import { getCodeWorkflow } from '../../lib/code-registry'
4
+
5
+ export type WorkflowDefinitionSource = 'code' | 'code_override' | 'user'
6
+
7
+ function resolveSource(definition: WorkflowDefinition): WorkflowDefinitionSource {
8
+ if (definition.codeWorkflowId) return 'code_override'
9
+ return 'user'
10
+ }
2
11
 
3
12
  export function serializeWorkflowDefinition(definition: WorkflowDefinition) {
13
+ const source = resolveSource(definition)
14
+ const codeModuleId =
15
+ source === 'code_override' && definition.codeWorkflowId
16
+ ? getCodeWorkflow(definition.codeWorkflowId)?.moduleId ?? null
17
+ : null
4
18
  return {
5
19
  id: definition.id,
6
20
  workflowId: definition.workflowId,
@@ -19,5 +33,33 @@ export function serializeWorkflowDefinition(definition: WorkflowDefinition) {
19
33
  createdAt: definition.createdAt,
20
34
  updatedAt: definition.updatedAt,
21
35
  deletedAt: definition.deletedAt ?? null,
36
+ source,
37
+ isCodeBased: source === 'code' || source === 'code_override',
38
+ codeModuleId,
39
+ }
40
+ }
41
+
42
+ export function serializeCodeWorkflowDefinition(codeDef: CodeWorkflowDefinition, syntheticId: string) {
43
+ return {
44
+ id: syntheticId,
45
+ workflowId: codeDef.workflowId,
46
+ workflowName: codeDef.workflowName,
47
+ description: codeDef.description ?? null,
48
+ version: codeDef.version,
49
+ definition: codeDef.definition,
50
+ metadata: codeDef.metadata ?? null,
51
+ enabled: codeDef.enabled,
52
+ effectiveFrom: null,
53
+ effectiveTo: null,
54
+ tenantId: null,
55
+ organizationId: null,
56
+ createdBy: null,
57
+ updatedBy: null,
58
+ createdAt: null,
59
+ updatedAt: null,
60
+ deletedAt: null,
61
+ source: 'code' as const,
62
+ isCodeBased: true,
63
+ codeModuleId: codeDef.moduleId,
22
64
  }
23
65
  }
@@ -7,8 +7,13 @@ import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { CrudForm } from '@open-mercato/ui/backend/CrudForm'
8
8
  import { Spinner } from '@open-mercato/ui/primitives/spinner'
9
9
  import { Button } from '@open-mercato/ui/primitives/button'
10
+ import { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'
10
11
  import { apiFetch } from '@open-mercato/ui/backend/utils/api'
12
+ import { readJsonSafe } from '@open-mercato/ui/backend/utils/serverErrors'
13
+ import { flash } from '@open-mercato/ui/backend/FlashMessages'
11
14
  import { useT } from '@open-mercato/shared/lib/i18n/context'
15
+ import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
16
+ import { useGuardedMutation } from '@open-mercato/ui/backend/injection/useGuardedMutation'
12
17
  import {
13
18
  workflowDefinitionFormSchema,
14
19
  createFormGroups,
@@ -64,24 +69,121 @@ export default function EditWorkflowDefinitionPage() {
64
69
  setTriggers(initialValues?.triggers ?? [])
65
70
  }, [initialValues])
66
71
 
72
+ const source = definition?.source as 'code' | 'code_override' | 'user' | undefined
73
+ const isCodeOnly = source === 'code'
74
+ const isCodeOverride = source === 'code_override'
75
+
76
+ const { confirm, ConfirmDialogElement } = useConfirmDialog()
77
+
78
+ const mutationContextId = React.useMemo(
79
+ () => `workflows.definitions.detail:${definitionId ?? 'unknown'}`,
80
+ [definitionId],
81
+ )
82
+ const { runMutation, retryLastMutation } = useGuardedMutation<Record<string, unknown>>({
83
+ contextId: mutationContextId,
84
+ })
85
+
67
86
  const handleSubmit = async (values: WorkflowDefinitionFormValues) => {
68
87
  const payload = buildWorkflowPayload({ ...values, triggers })
69
88
 
70
- const response = await apiFetch(`/api/workflows/definitions/${definitionId}`, {
71
- method: 'PUT',
72
- headers: { 'Content-Type': 'application/json' },
73
- body: JSON.stringify(payload),
89
+ await runMutation({
90
+ operation: async () => {
91
+ const response = await apiFetch(`/api/workflows/definitions/${definitionId}`, {
92
+ method: 'PUT',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify(payload),
95
+ })
96
+ if (!response.ok) {
97
+ const errorBody = await readJsonSafe<{ error?: string }>(response, null)
98
+ throw new Error(errorBody?.error || t('workflows.errors.updateFailed'))
99
+ }
100
+ return response
101
+ },
102
+ mutationPayload: { resourceId: definitionId, operation: 'update' },
103
+ context: {
104
+ formId: mutationContextId,
105
+ resourceKind: 'workflows.definition',
106
+ resourceId: definitionId,
107
+ operation: 'update',
108
+ retryLastMutation,
109
+ },
74
110
  })
75
111
 
76
- if (!response.ok) {
77
- const error = await response.json()
78
- throw new Error(error.error || t('workflows.errors.updateFailed'))
79
- }
80
-
81
112
  router.push('/backend/definitions')
82
113
  router.refresh()
83
114
  }
84
115
 
116
+ const handleCustomize = async () => {
117
+ try {
118
+ const result = await runMutation({
119
+ operation: async () => {
120
+ const response = await apiFetch(`/api/workflows/definitions/${definitionId}/customize`, {
121
+ method: 'POST',
122
+ })
123
+ if (!response.ok) {
124
+ const errorBody = await readJsonSafe<{ error?: string }>(response, null)
125
+ throw new Error(errorBody?.error || t('workflows.errors.updateFailed'))
126
+ }
127
+ return readJsonSafe<{ data?: { id?: string } }>(response, null)
128
+ },
129
+ mutationPayload: { resourceId: definitionId, operation: 'customize' },
130
+ context: {
131
+ formId: mutationContextId,
132
+ resourceKind: 'workflows.definition',
133
+ resourceId: definitionId,
134
+ operation: 'customize',
135
+ retryLastMutation,
136
+ },
137
+ })
138
+ if (result?.data?.id) {
139
+ router.push(`/backend/definitions/${result.data.id}`)
140
+ router.refresh()
141
+ }
142
+ } catch (error) {
143
+ const message = error instanceof Error ? error.message : t('workflows.errors.updateFailed')
144
+ flash(message, 'error')
145
+ }
146
+ }
147
+
148
+ const handleResetToCode = async () => {
149
+ const confirmed = await confirm({
150
+ title: t('workflows.actions.resetToCode'),
151
+ description: t('workflows.actions.resetConfirm'),
152
+ confirmText: t('workflows.actions.resetToCode'),
153
+ variant: 'destructive',
154
+ })
155
+ if (!confirmed) return
156
+
157
+ try {
158
+ const result = await runMutation({
159
+ operation: async () => {
160
+ const response = await apiFetch(`/api/workflows/definitions/${definitionId}/reset-to-code`, {
161
+ method: 'POST',
162
+ })
163
+ if (!response.ok) {
164
+ const errorBody = await readJsonSafe<{ error?: string }>(response, null)
165
+ throw new Error(errorBody?.error || t('workflows.messages.updateFailed'))
166
+ }
167
+ return readJsonSafe<{ data?: { id?: string } }>(response, null)
168
+ },
169
+ mutationPayload: { resourceId: definitionId, operation: 'reset-to-code' },
170
+ context: {
171
+ formId: mutationContextId,
172
+ resourceKind: 'workflows.definition',
173
+ resourceId: definitionId,
174
+ operation: 'reset-to-code',
175
+ retryLastMutation,
176
+ },
177
+ })
178
+ flash(t('workflows.messages.updated'), 'success')
179
+ const codeId = result?.data?.id || `code:${definition?.workflowId}`
180
+ router.push(`/backend/definitions/${codeId}`)
181
+ router.refresh()
182
+ } catch {
183
+ flash(t('workflows.messages.updateFailed'), 'error')
184
+ }
185
+ }
186
+
85
187
  const fields = React.useMemo(() => createFieldDefinitions(t), [t])
86
188
 
87
189
  const formGroups = React.useMemo(
@@ -128,6 +230,26 @@ export default function EditWorkflowDefinitionPage() {
128
230
  return (
129
231
  <Page>
130
232
  <PageBody>
233
+ {isCodeOnly && (
234
+ <Alert variant="info" className="mb-4">
235
+ <AlertDescription className="flex items-center justify-between">
236
+ <span>{t('workflows.source.code.readonlyBanner')}</span>
237
+ <Button variant="outline" size="sm" onClick={handleCustomize}>
238
+ {t('workflows.actions.customize')}
239
+ </Button>
240
+ </AlertDescription>
241
+ </Alert>
242
+ )}
243
+ {isCodeOverride && (
244
+ <Alert variant="warning" className="mb-4">
245
+ <AlertDescription className="flex items-center justify-between">
246
+ <span>{t('workflows.source.code_override.banner')}</span>
247
+ <Button variant="outline" size="sm" onClick={handleResetToCode}>
248
+ {t('workflows.actions.resetToCode')}
249
+ </Button>
250
+ </AlertDescription>
251
+ </Alert>
252
+ )}
131
253
  <div className="mb-4 p-4 bg-status-info-bg border border-status-info-border rounded-lg flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
132
254
  <div className="flex items-start gap-3">
133
255
  <svg className="w-5 h-5 text-status-info-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -150,7 +272,7 @@ export default function EditWorkflowDefinitionPage() {
150
272
  </div>
151
273
  <CrudForm
152
274
  key={definitionId}
153
- title={t('workflows.edit.title')}
275
+ title={isCodeOnly ? definition?.workflowName || t('workflows.edit.title') : t('workflows.edit.title')}
154
276
  backHref="/backend/definitions"
155
277
  schema={workflowDefinitionFormSchema}
156
278
  fields={fields}
@@ -158,7 +280,8 @@ export default function EditWorkflowDefinitionPage() {
158
280
  onSubmit={handleSubmit}
159
281
  cancelHref="/backend/definitions"
160
282
  groups={formGroups}
161
- submitLabel={t('workflows.form.update')}
283
+ submitLabel={isCodeOnly ? t('workflows.actions.customize') : t('workflows.form.update')}
284
+ {...(isCodeOnly ? { readOnly: true } : {})}
162
285
  />
163
286
 
164
287
  {/* Mobile Steps & Transitions View */}
@@ -184,6 +307,7 @@ export default function EditWorkflowDefinitionPage() {
184
307
  />
185
308
  </div>
186
309
  </PageBody>
310
+ {ConfirmDialogElement}
187
311
  </Page>
188
312
  )
189
313
  }
@@ -25,6 +25,8 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
25
25
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
26
26
  import { Trash2 } from 'lucide-react'
27
27
 
28
+ type WorkflowDefinitionSource = 'code' | 'code_override' | 'user'
29
+
28
30
  type WorkflowDefinition = {
29
31
  id: string
30
32
  workflowId: string
@@ -45,6 +47,8 @@ type WorkflowDefinition = {
45
47
  createdAt: string
46
48
  updatedAt: string
47
49
  createdBy: string | null
50
+ source?: WorkflowDefinitionSource
51
+ isCodeBased?: boolean
48
52
  }
49
53
 
50
54
  type DefinitionsResponse = {
@@ -241,7 +245,15 @@ export default function WorkflowDefinitionsListPage() {
241
245
  meta: { truncate: false },
242
246
  cell: ({ row }) => (
243
247
  <div>
244
- <div className="font-medium">{row.original.workflowName}</div>
248
+ <div className="flex items-center gap-2">
249
+ <span className="font-medium">{row.original.workflowName}</span>
250
+ {row.original.source === 'code' && (
251
+ <Badge variant="secondary">{t('workflows.source.code')}</Badge>
252
+ )}
253
+ {row.original.source === 'code_override' && (
254
+ <Badge variant="outline">{t('workflows.source.code_override')}</Badge>
255
+ )}
256
+ </div>
245
257
  {row.original.description && (
246
258
  <div className="text-xs text-gray-500">
247
259
  {row.original.description}
@@ -316,38 +328,38 @@ export default function WorkflowDefinitionsListPage() {
316
328
  {
317
329
  id: 'actions',
318
330
  header: '',
319
- cell: ({ row }) => (
320
- <RowActions
321
- items={[
322
- {
323
- id: 'edit',
324
- label: t('common.edit'),
325
- href: `/backend/definitions/${row.original.id}`,
326
- },
327
- {
328
- id: 'edit-visual',
329
- label: t('workflows.actions.editVisually'),
330
- href: `/backend/definitions/visual-editor?id=${row.original.id}`,
331
- },
332
- {
333
- id: row.original.enabled ? 'disable' : 'enable',
334
- label: row.original.enabled ? t('common.disable') : t('common.enable'),
335
- onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled),
336
- },
337
- {
338
- id: 'duplicate',
339
- label: t('common.duplicate'),
340
- onSelect: () => handleDuplicate(row.original),
341
- },
342
- {
343
- id: 'delete',
344
- label: t('common.delete'),
345
- onSelect: () => handleDelete(row.original.id, row.original.workflowName),
346
- destructive: true,
347
- },
348
- ]}
349
- />
350
- ),
331
+ cell: ({ row }) => {
332
+ const isCodeOnly = row.original.source === 'code'
333
+ const items = [
334
+ {
335
+ id: 'edit',
336
+ label: isCodeOnly ? t('common.view') : t('common.edit'),
337
+ href: `/backend/definitions/${row.original.id}`,
338
+ },
339
+ ...(!isCodeOnly ? [{
340
+ id: 'edit-visual',
341
+ label: t('workflows.actions.editVisually'),
342
+ href: `/backend/definitions/visual-editor?id=${row.original.id}`,
343
+ }] : []),
344
+ ...(!isCodeOnly ? [{
345
+ id: row.original.enabled ? 'disable' : 'enable',
346
+ label: row.original.enabled ? t('common.disable') : t('common.enable'),
347
+ onSelect: () => handleToggleEnabled(row.original.id, row.original.enabled),
348
+ }] : []),
349
+ ...(!isCodeOnly ? [{
350
+ id: 'duplicate',
351
+ label: t('common.duplicate'),
352
+ onSelect: () => handleDuplicate(row.original),
353
+ }] : []),
354
+ ...(!isCodeOnly ? [{
355
+ id: 'delete',
356
+ label: t('common.delete'),
357
+ onSelect: () => handleDelete(row.original.id, row.original.workflowName),
358
+ destructive: true,
359
+ }] : []),
360
+ ]
361
+ return <RowActions items={items} />
362
+ },
351
363
  },
352
364
  ]
353
365