@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/workflow_definition/index.js +2 -0
- package/dist/generated/entities/workflow_definition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/sales/workflows.js +180 -0
- package/dist/modules/sales/workflows.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js +172 -0
- package/dist/modules/workflows/api/definitions/[id]/customize/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js +205 -0
- package/dist/modules/workflows/api/definitions/[id]/reset-to-code/route.js.map +7 -0
- package/dist/modules/workflows/api/definitions/[id]/route.js +91 -2
- package/dist/modules/workflows/api/definitions/[id]/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/route.js +32 -9
- package/dist/modules/workflows/api/definitions/route.js.map +2 -2
- package/dist/modules/workflows/api/definitions/serialize.js +36 -1
- package/dist/modules/workflows/api/definitions/serialize.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/[id]/page.js +161 -52
- package/dist/modules/workflows/backend/definitions/[id]/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/page.js +37 -34
- package/dist/modules/workflows/backend/definitions/page.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +134 -41
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/cli.js +19 -121
- package/dist/modules/workflows/cli.js.map +2 -2
- package/dist/modules/workflows/components/formConfig.js +1 -1
- package/dist/modules/workflows/components/formConfig.js.map +2 -2
- package/dist/modules/workflows/data/entities.js +3 -0
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +2 -2
- package/dist/modules/workflows/data/validators.js.map +2 -2
- package/dist/modules/workflows/events.js +2 -0
- package/dist/modules/workflows/events.js.map +2 -2
- package/dist/modules/workflows/examples/guard-rules-example.json +6 -6
- package/dist/modules/workflows/examples/order-approval-guard-rules.json +2 -2
- package/dist/modules/workflows/frontend/checkout-demo/page.js +4 -4
- package/dist/modules/workflows/frontend/checkout-demo/page.js.map +2 -2
- package/dist/modules/workflows/lib/code-registry.js +40 -0
- package/dist/modules/workflows/lib/code-registry.js.map +7 -0
- package/dist/modules/workflows/lib/find-definition.js +58 -0
- package/dist/modules/workflows/lib/find-definition.js.map +7 -0
- package/dist/modules/workflows/lib/seeds.js +0 -3
- package/dist/modules/workflows/lib/seeds.js.map +2 -2
- package/dist/modules/workflows/lib/start-validator.js +1 -20
- package/dist/modules/workflows/lib/start-validator.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +1 -19
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260414120000.js +13 -0
- package/dist/modules/workflows/migrations/Migration20260414120000.js.map +7 -0
- package/dist/modules/workflows/migrations/Migration20260428102318.js +32 -0
- package/dist/modules/workflows/migrations/Migration20260428102318.js.map +7 -0
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +1 -1
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +1 -1
- package/dist/modules/workflows/workflows.js +260 -0
- package/dist/modules/workflows/workflows.js.map +7 -0
- package/generated/entities/workflow_definition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/modules/sales/workflows.ts +184 -0
- package/src/modules/workflows/api/definitions/[id]/customize/route.ts +197 -0
- package/src/modules/workflows/api/definitions/[id]/reset-to-code/route.ts +252 -0
- package/src/modules/workflows/api/definitions/[id]/route.ts +108 -4
- package/src/modules/workflows/api/definitions/route.ts +54 -10
- package/src/modules/workflows/api/definitions/serialize.ts +42 -0
- package/src/modules/workflows/backend/definitions/[id]/page.tsx +135 -11
- package/src/modules/workflows/backend/definitions/page.tsx +45 -33
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +222 -97
- package/src/modules/workflows/cli.ts +32 -155
- package/src/modules/workflows/components/formConfig.tsx +2 -2
- package/src/modules/workflows/data/entities.ts +4 -1
- package/src/modules/workflows/data/validators.ts +4 -3
- package/src/modules/workflows/events.ts +2 -0
- package/src/modules/workflows/examples/guard-rules-example.json +6 -6
- package/src/modules/workflows/examples/order-approval-guard-rules.json +2 -2
- package/src/modules/workflows/frontend/checkout-demo/page.tsx +4 -4
- package/src/modules/workflows/i18n/de.json +16 -0
- package/src/modules/workflows/i18n/en.json +16 -0
- package/src/modules/workflows/i18n/es.json +16 -0
- package/src/modules/workflows/i18n/pl.json +16 -0
- package/src/modules/workflows/lib/code-registry.ts +65 -0
- package/src/modules/workflows/lib/find-definition.ts +100 -0
- package/src/modules/workflows/lib/seeds.ts +4 -3
- package/src/modules/workflows/lib/start-validator.ts +2 -35
- package/src/modules/workflows/lib/workflow-executor.ts +2 -39
- package/src/modules/workflows/migrations/.snapshot-open-mercato.json +10 -0
- package/src/modules/workflows/migrations/Migration20260414120000.ts +13 -0
- package/src/modules/workflows/migrations/Migration20260428102318.ts +57 -0
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +1 -1
- package/src/modules/workflows/workflows.ts +278 -0
- package/dist/modules/workflows/examples/checkout-demo-definition.json +0 -486
- package/dist/modules/workflows/examples/order-approval-definition.json +0 -257
- package/dist/modules/workflows/examples/simple-approval-definition.json +0 -134
- package/src/modules/workflows/examples/checkout-demo-definition.json +0 -486
- package/src/modules/workflows/examples/order-approval-definition.json +0 -257
- 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
|
|
155
|
-
//
|
|
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().
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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:
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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="
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|