@rytass/bpm-core-shared 0.3.0 → 0.5.0
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/CHANGELOG.md +12 -0
- package/eslint.config.mjs +19 -0
- package/jest.config.cts +10 -0
- package/package.json +2 -3
- package/project.json +49 -0
- package/src/lib/condition.ts +55 -0
- package/src/lib/form.ts +77 -0
- package/src/lib/identity.ts +25 -0
- package/src/lib/organization.ts +52 -0
- package/src/lib/shared.spec.ts +13 -0
- package/src/lib/status.ts +29 -0
- package/src/lib/workflow-command.spec.ts +278 -0
- package/src/lib/workflow-command.ts +848 -0
- package/src/lib/workflow-graph.ts +974 -0
- package/src/lib/workflow-toolset.spec.ts +230 -0
- package/src/lib/workflow-toolset.ts +1083 -0
- package/src/lib/workflow.ts +216 -0
- package/tsconfig.json +23 -0
- package/tsconfig.lib.json +16 -0
- package/tsconfig.spec.json +16 -0
- package/src/index.js +0 -13
- package/src/index.js.map +0 -1
- package/src/lib/condition.d.ts +0 -43
- package/src/lib/condition.js +0 -3
- package/src/lib/condition.js.map +0 -1
- package/src/lib/form.d.ts +0 -48
- package/src/lib/form.js +0 -3
- package/src/lib/form.js.map +0 -1
- package/src/lib/identity.d.ts +0 -24
- package/src/lib/identity.js +0 -3
- package/src/lib/identity.js.map +0 -1
- package/src/lib/organization.d.ts +0 -47
- package/src/lib/organization.js +0 -3
- package/src/lib/organization.js.map +0 -1
- package/src/lib/status.d.ts +0 -7
- package/src/lib/status.js +0 -3
- package/src/lib/status.js.map +0 -1
- package/src/lib/workflow-command.d.ts +0 -156
- package/src/lib/workflow-command.js +0 -292
- package/src/lib/workflow-command.js.map +0 -1
- package/src/lib/workflow-graph.d.ts +0 -96
- package/src/lib/workflow-graph.js +0 -605
- package/src/lib/workflow-graph.js.map +0 -1
- package/src/lib/workflow-toolset.d.ts +0 -105
- package/src/lib/workflow-toolset.js +0 -702
- package/src/lib/workflow-toolset.js.map +0 -1
- package/src/lib/workflow.d.ts +0 -160
- package/src/lib/workflow.js +0 -3
- package/src/lib/workflow.js.map +0 -1
- /package/src/{index.d.ts → index.ts} +0 -0
|
@@ -0,0 +1,974 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FormDefinitionSchema,
|
|
3
|
+
FormFieldDefinition,
|
|
4
|
+
FormFieldOption,
|
|
5
|
+
} from './form';
|
|
6
|
+
import {
|
|
7
|
+
ApproverResolver,
|
|
8
|
+
ServiceAction,
|
|
9
|
+
WorkflowDefinition,
|
|
10
|
+
WorkflowEdge,
|
|
11
|
+
WorkflowEdgeConditionOperator,
|
|
12
|
+
WorkflowEdgeData,
|
|
13
|
+
WorkflowNode,
|
|
14
|
+
WorkflowNodeTriggerMode,
|
|
15
|
+
} from './workflow';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pure, framework-agnostic structural operations over a {@link WorkflowDefinition}.
|
|
19
|
+
*
|
|
20
|
+
* This module is the canonical home for every transform that the template
|
|
21
|
+
* designer performs on the workflow graph. It intentionally contains **no**
|
|
22
|
+
* React, DOM, or layout (dagre) dependency so that both the frontend designer
|
|
23
|
+
* and the backend can share the exact same semantics, and so the LLM command
|
|
24
|
+
* layer ({@link ./workflow-command}) can be built on top of it.
|
|
25
|
+
*
|
|
26
|
+
* Pixel layout (dagre) and ReactFlow node/edge mapping stay in the React lib;
|
|
27
|
+
* the structures here only describe the logical graph.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Node types a user can add from the palette (start/end are implicit). */
|
|
31
|
+
export type NodePaletteType = 'exclusiveGateway' | 'serviceTask' | 'userTask';
|
|
32
|
+
|
|
33
|
+
export const WORKFLOW_INPUT_HANDLE_ID = 'input';
|
|
34
|
+
export const WORKFLOW_OUTPUT_HANDLE_ID = 'output';
|
|
35
|
+
|
|
36
|
+
export const WORKFLOW_NODE_TYPE_LABELS: Readonly<
|
|
37
|
+
Record<WorkflowNode['type'], string>
|
|
38
|
+
> = {
|
|
39
|
+
endEvent: '結束',
|
|
40
|
+
exclusiveGateway: '條件分流',
|
|
41
|
+
parallelGateway: '並行處理',
|
|
42
|
+
serviceTask: '系統',
|
|
43
|
+
startEvent: '開始',
|
|
44
|
+
userTask: '簽核',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface ConditionOperatorOption {
|
|
48
|
+
readonly id: WorkflowEdgeConditionOperator;
|
|
49
|
+
readonly name: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const CONDITION_OPERATOR_OPTIONS: readonly ConditionOperatorOption[] = [
|
|
53
|
+
{ id: 'EQUALS', name: '等於' },
|
|
54
|
+
{ id: 'NOT_EQUALS', name: '不等於' },
|
|
55
|
+
{ id: 'GREATER_THAN', name: '大於' },
|
|
56
|
+
{ id: 'GREATER_THAN_OR_EQUALS', name: '大於等於' },
|
|
57
|
+
{ id: 'LESS_THAN', name: '小於' },
|
|
58
|
+
{ id: 'LESS_THAN_OR_EQUALS', name: '小於等於' },
|
|
59
|
+
{ id: 'IS_FILLED', name: '已填寫' },
|
|
60
|
+
{ id: 'IS_EMPTY', name: '未填寫' },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
export const CONDITION_OPERATORS_REQUIRING_VALUE: readonly WorkflowEdgeConditionOperator[] =
|
|
64
|
+
[
|
|
65
|
+
'EQUALS',
|
|
66
|
+
'GREATER_THAN',
|
|
67
|
+
'GREATER_THAN_OR_EQUALS',
|
|
68
|
+
'LESS_THAN',
|
|
69
|
+
'LESS_THAN_OR_EQUALS',
|
|
70
|
+
'NOT_EQUALS',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
export interface WorkflowConnectionCandidate {
|
|
74
|
+
readonly source?: string | null;
|
|
75
|
+
readonly sourceHandle?: string | null;
|
|
76
|
+
readonly target?: string | null;
|
|
77
|
+
readonly targetHandle?: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Result of inserting a node, including the post-insert selection intent. */
|
|
81
|
+
export interface WorkflowNodeInsertResult {
|
|
82
|
+
readonly definition: WorkflowDefinition;
|
|
83
|
+
readonly editingEdgeId: string | null;
|
|
84
|
+
readonly selectedEdgeIds: readonly string[];
|
|
85
|
+
readonly selectedNodeId: string | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Factory used to mint workflow edge ids. Injectable so callers (e.g. tests or
|
|
90
|
+
* a deterministic reducer run) can supply stable ids; defaults to a
|
|
91
|
+
* time + random suffix to match the original designer behaviour.
|
|
92
|
+
*/
|
|
93
|
+
export type WorkflowEdgeIdFactory = (source: string, target: string) => string;
|
|
94
|
+
|
|
95
|
+
export function defaultWorkflowEdgeId(source: string, target: string): string {
|
|
96
|
+
return `edge_${source}_${target}_${Date.now()}_${Math.random()
|
|
97
|
+
.toString(36)
|
|
98
|
+
.slice(2, 8)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Node transforms ────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
export function renameWorkflowNode(
|
|
104
|
+
node: WorkflowNode,
|
|
105
|
+
label: string,
|
|
106
|
+
): WorkflowNode {
|
|
107
|
+
if (node.type === 'startEvent') {
|
|
108
|
+
return { ...node, data: { ...node.data, label } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (node.type === 'endEvent') {
|
|
112
|
+
return { ...node, data: { ...node.data, label } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (node.type === 'userTask') {
|
|
116
|
+
return { ...node, data: { ...node.data, label } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (node.type === 'serviceTask') {
|
|
120
|
+
return { ...node, data: { ...node.data, label } };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (node.type === 'exclusiveGateway') {
|
|
124
|
+
return { ...node, data: { ...node.data, label } };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { ...node, data: { ...node.data, label } };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function applyWorkflowNodeTriggerMode(
|
|
131
|
+
node: WorkflowNode,
|
|
132
|
+
triggerMode: WorkflowNodeTriggerMode,
|
|
133
|
+
): WorkflowNode {
|
|
134
|
+
if (node.type === 'startEvent') {
|
|
135
|
+
return node;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (node.type === 'endEvent') {
|
|
139
|
+
return { ...node, data: { ...node.data, triggerMode } };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (node.type === 'userTask') {
|
|
143
|
+
return { ...node, data: { ...node.data, triggerMode } };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (node.type === 'serviceTask') {
|
|
147
|
+
return { ...node, data: { ...node.data, triggerMode } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (node.type === 'exclusiveGateway') {
|
|
151
|
+
return { ...node, data: { ...node.data, triggerMode } };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { ...node, data: { ...node.data, triggerMode } };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function normalizeDesignerWorkflowDefinition(
|
|
158
|
+
definition: WorkflowDefinition,
|
|
159
|
+
): WorkflowDefinition {
|
|
160
|
+
return normalizeSingleIncomingTriggerModes(
|
|
161
|
+
removeAsyncNotifyOutgoingEdges(definition),
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function removeAsyncNotifyOutgoingEdges(
|
|
166
|
+
definition: WorkflowDefinition,
|
|
167
|
+
): WorkflowDefinition {
|
|
168
|
+
const asyncNotifyNodeIds = new Set(
|
|
169
|
+
definition.nodes
|
|
170
|
+
.filter((node) => isAsyncNotifyServiceTask(node))
|
|
171
|
+
.map((node) => node.id),
|
|
172
|
+
);
|
|
173
|
+
const edges = definition.edges.filter(
|
|
174
|
+
(edge) => !asyncNotifyNodeIds.has(edge.source),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return edges.length === definition.edges.length
|
|
178
|
+
? definition
|
|
179
|
+
: { ...definition, edges };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function normalizeSingleIncomingTriggerModes(
|
|
183
|
+
definition: WorkflowDefinition,
|
|
184
|
+
): WorkflowDefinition {
|
|
185
|
+
const incomingEdgeCounts = definition.edges.reduce<Record<string, number>>(
|
|
186
|
+
(counts, edge) => ({
|
|
187
|
+
...counts,
|
|
188
|
+
[edge.target]: (counts[edge.target] ?? 0) + 1,
|
|
189
|
+
}),
|
|
190
|
+
{},
|
|
191
|
+
);
|
|
192
|
+
const nodes = definition.nodes.map((node) => {
|
|
193
|
+
if (node.type === 'startEvent') {
|
|
194
|
+
return node;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const incomingEdgeCount = incomingEdgeCounts[node.id] ?? 0;
|
|
198
|
+
|
|
199
|
+
return incomingEdgeCount < 2 && node.data.triggerMode !== 'AND'
|
|
200
|
+
? applyWorkflowNodeTriggerMode(node, 'AND')
|
|
201
|
+
: node;
|
|
202
|
+
});
|
|
203
|
+
const hasNodeChanges = nodes.some(
|
|
204
|
+
(node, index) => node !== definition.nodes[index],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
return hasNodeChanges ? { ...definition, nodes } : definition;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ── Node / edge factories ──────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
export function createWorkflowNode(
|
|
213
|
+
type: NodePaletteType,
|
|
214
|
+
index: number,
|
|
215
|
+
): WorkflowNode {
|
|
216
|
+
const id = `${type}_${index}`;
|
|
217
|
+
const base = {
|
|
218
|
+
id,
|
|
219
|
+
position: { x: 260 + index * 48, y: 120 + index * 42 },
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (type === 'userTask') {
|
|
223
|
+
return {
|
|
224
|
+
...base,
|
|
225
|
+
data: {
|
|
226
|
+
allowAddSigner: false,
|
|
227
|
+
allowReject: true,
|
|
228
|
+
allowTransfer: true,
|
|
229
|
+
approverResolver: { memberIds: ['member-001'], type: 'DIRECT' },
|
|
230
|
+
decisionPolicy: { type: 'SINGLE' },
|
|
231
|
+
label: `簽核節點 ${index}`,
|
|
232
|
+
returnBehavior: {
|
|
233
|
+
allowReturn: true,
|
|
234
|
+
allowedTargets: 'INITIATOR',
|
|
235
|
+
resubmitStrategy: 'RESTART',
|
|
236
|
+
},
|
|
237
|
+
triggerMode: 'AND',
|
|
238
|
+
},
|
|
239
|
+
type,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (type === 'serviceTask') {
|
|
244
|
+
return {
|
|
245
|
+
...base,
|
|
246
|
+
data: {
|
|
247
|
+
action: {
|
|
248
|
+
channels: ['IN_APP'],
|
|
249
|
+
recipients: { memberIds: ['member-001'], type: 'DIRECT' },
|
|
250
|
+
type: 'NOTIFY',
|
|
251
|
+
},
|
|
252
|
+
label: `知會節點 ${index}`,
|
|
253
|
+
triggerMode: 'AND',
|
|
254
|
+
},
|
|
255
|
+
type,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
...base,
|
|
261
|
+
data: {
|
|
262
|
+
direction: 'split',
|
|
263
|
+
label: `條件分流 ${index}`,
|
|
264
|
+
triggerMode: 'AND',
|
|
265
|
+
},
|
|
266
|
+
type,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function readNextWorkflowNodeIndex(
|
|
271
|
+
nodes: readonly WorkflowNode[],
|
|
272
|
+
type: NodePaletteType,
|
|
273
|
+
): number {
|
|
274
|
+
const usedIndexes = new Set(
|
|
275
|
+
nodes
|
|
276
|
+
.filter((node) => node.type === type)
|
|
277
|
+
.map((node) => Number(node.id.replace(`${type}_`, '')))
|
|
278
|
+
.filter((index) => Number.isInteger(index) && index > 0),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
Array.from({ length: nodes.length + 1 }, (_, index) => index + 1).find(
|
|
283
|
+
(index) => !usedIndexes.has(index),
|
|
284
|
+
) ?? nodes.length + 1
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function createWorkflowEdge(
|
|
289
|
+
source: string,
|
|
290
|
+
target: string,
|
|
291
|
+
data: WorkflowEdgeData,
|
|
292
|
+
createId: WorkflowEdgeIdFactory = defaultWorkflowEdgeId,
|
|
293
|
+
): WorkflowEdge {
|
|
294
|
+
return {
|
|
295
|
+
data,
|
|
296
|
+
id: createId(source, target),
|
|
297
|
+
source,
|
|
298
|
+
sourceHandle: WORKFLOW_OUTPUT_HANDLE_ID,
|
|
299
|
+
target,
|
|
300
|
+
targetHandle: WORKFLOW_INPUT_HANDLE_ID,
|
|
301
|
+
type: 'smoothstep',
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function readInsertedOutgoingEdgeData(
|
|
306
|
+
node: WorkflowNode,
|
|
307
|
+
): WorkflowEdgeData {
|
|
308
|
+
if (node.type === 'exclusiveGateway') {
|
|
309
|
+
return { isDefault: true, label: '其他情況' };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Node insertion ─────────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
export function insertWorkflowNodeIntoDefinition({
|
|
318
|
+
createId = defaultWorkflowEdgeId,
|
|
319
|
+
definition,
|
|
320
|
+
node,
|
|
321
|
+
selectedEdgeId,
|
|
322
|
+
selectedNodeId,
|
|
323
|
+
}: {
|
|
324
|
+
readonly createId?: WorkflowEdgeIdFactory;
|
|
325
|
+
readonly definition: WorkflowDefinition;
|
|
326
|
+
readonly node: WorkflowNode;
|
|
327
|
+
readonly selectedEdgeId: string | null;
|
|
328
|
+
readonly selectedNodeId: string | null;
|
|
329
|
+
}): WorkflowNodeInsertResult {
|
|
330
|
+
const selectedEdge = selectedEdgeId
|
|
331
|
+
? (definition.edges.find((edge) => edge.id === selectedEdgeId) ?? null)
|
|
332
|
+
: null;
|
|
333
|
+
const selectedNode = selectedNodeId
|
|
334
|
+
? (definition.nodes.find((candidate) => candidate.id === selectedNodeId) ??
|
|
335
|
+
null)
|
|
336
|
+
: null;
|
|
337
|
+
|
|
338
|
+
if (selectedEdge) {
|
|
339
|
+
return insertWorkflowNodeAtEdge(definition, node, selectedEdge, createId);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (selectedNode) {
|
|
343
|
+
return insertWorkflowNodeAfterNode(definition, node, selectedNode, createId);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
definition: { ...definition, nodes: [...definition.nodes, node] },
|
|
348
|
+
editingEdgeId: null,
|
|
349
|
+
selectedEdgeIds: [],
|
|
350
|
+
selectedNodeId: node.id,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function insertWorkflowNodeAtEdge(
|
|
355
|
+
definition: WorkflowDefinition,
|
|
356
|
+
node: WorkflowNode,
|
|
357
|
+
edge: WorkflowEdge,
|
|
358
|
+
createId: WorkflowEdgeIdFactory = defaultWorkflowEdgeId,
|
|
359
|
+
): WorkflowNodeInsertResult {
|
|
360
|
+
if (!isWorkflowNodeInputConnectable(node)) {
|
|
361
|
+
return {
|
|
362
|
+
definition,
|
|
363
|
+
editingEdgeId: null,
|
|
364
|
+
selectedEdgeIds: [edge.id],
|
|
365
|
+
selectedNodeId: null,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!isWorkflowNodeOutputConnectable(node)) {
|
|
370
|
+
const incomingEdge = createWorkflowEdge(edge.source, node.id, {}, createId);
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
definition: {
|
|
374
|
+
...definition,
|
|
375
|
+
edges: [...definition.edges, incomingEdge],
|
|
376
|
+
nodes: [...definition.nodes, node],
|
|
377
|
+
},
|
|
378
|
+
editingEdgeId: null,
|
|
379
|
+
selectedEdgeIds: [],
|
|
380
|
+
selectedNodeId: node.id,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const incomingEdge = createWorkflowEdge(
|
|
385
|
+
edge.source,
|
|
386
|
+
node.id,
|
|
387
|
+
edge.data,
|
|
388
|
+
createId,
|
|
389
|
+
);
|
|
390
|
+
const outgoingEdge = createWorkflowEdge(
|
|
391
|
+
node.id,
|
|
392
|
+
edge.target,
|
|
393
|
+
readInsertedOutgoingEdgeData(node),
|
|
394
|
+
createId,
|
|
395
|
+
);
|
|
396
|
+
const shouldEditOutgoingEdge = isExclusiveGatewaySourceEdge(outgoingEdge, [
|
|
397
|
+
...definition.nodes,
|
|
398
|
+
node,
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
definition: {
|
|
403
|
+
...definition,
|
|
404
|
+
edges: definition.edges.flatMap((currentEdge) =>
|
|
405
|
+
currentEdge.id === edge.id
|
|
406
|
+
? [incomingEdge, outgoingEdge]
|
|
407
|
+
: [currentEdge],
|
|
408
|
+
),
|
|
409
|
+
nodes: [...definition.nodes, node],
|
|
410
|
+
},
|
|
411
|
+
editingEdgeId: shouldEditOutgoingEdge ? outgoingEdge.id : null,
|
|
412
|
+
selectedEdgeIds: shouldEditOutgoingEdge ? [outgoingEdge.id] : [],
|
|
413
|
+
selectedNodeId: shouldEditOutgoingEdge ? null : node.id,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function insertWorkflowNodeAfterNode(
|
|
418
|
+
definition: WorkflowDefinition,
|
|
419
|
+
node: WorkflowNode,
|
|
420
|
+
sourceNode: WorkflowNode,
|
|
421
|
+
createId: WorkflowEdgeIdFactory = defaultWorkflowEdgeId,
|
|
422
|
+
): WorkflowNodeInsertResult {
|
|
423
|
+
if (!isWorkflowNodeOutputConnectable(sourceNode)) {
|
|
424
|
+
return {
|
|
425
|
+
definition: { ...definition, nodes: [...definition.nodes, node] },
|
|
426
|
+
editingEdgeId: null,
|
|
427
|
+
selectedEdgeIds: [],
|
|
428
|
+
selectedNodeId: node.id,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const firstOutgoingEdge =
|
|
433
|
+
definition.edges.find((edge) => edge.source === sourceNode.id) ?? null;
|
|
434
|
+
|
|
435
|
+
if (firstOutgoingEdge && isWorkflowNodeOutputConnectable(node)) {
|
|
436
|
+
return insertWorkflowNodeAtEdge(
|
|
437
|
+
definition,
|
|
438
|
+
node,
|
|
439
|
+
firstOutgoingEdge,
|
|
440
|
+
createId,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const endNode = definition.nodes.find(
|
|
445
|
+
(candidate) => candidate.type === 'endEvent',
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
if (
|
|
449
|
+
endNode &&
|
|
450
|
+
sourceNode.id !== endNode.id &&
|
|
451
|
+
isWorkflowNodeOutputConnectable(node)
|
|
452
|
+
) {
|
|
453
|
+
const incomingEdge = createWorkflowEdge(
|
|
454
|
+
sourceNode.id,
|
|
455
|
+
node.id,
|
|
456
|
+
{},
|
|
457
|
+
createId,
|
|
458
|
+
);
|
|
459
|
+
const outgoingEdge = createWorkflowEdge(
|
|
460
|
+
node.id,
|
|
461
|
+
endNode.id,
|
|
462
|
+
readInsertedOutgoingEdgeData(node),
|
|
463
|
+
createId,
|
|
464
|
+
);
|
|
465
|
+
const shouldEditOutgoingEdge = isExclusiveGatewaySourceEdge(outgoingEdge, [
|
|
466
|
+
...definition.nodes,
|
|
467
|
+
node,
|
|
468
|
+
]);
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
definition: {
|
|
472
|
+
...definition,
|
|
473
|
+
edges: [...definition.edges, incomingEdge, outgoingEdge],
|
|
474
|
+
nodes: [...definition.nodes, node],
|
|
475
|
+
},
|
|
476
|
+
editingEdgeId: shouldEditOutgoingEdge ? outgoingEdge.id : null,
|
|
477
|
+
selectedEdgeIds: shouldEditOutgoingEdge ? [outgoingEdge.id] : [],
|
|
478
|
+
selectedNodeId: shouldEditOutgoingEdge ? null : node.id,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const incomingEdge = createWorkflowEdge(sourceNode.id, node.id, {}, createId);
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
definition: {
|
|
486
|
+
...definition,
|
|
487
|
+
edges: [...definition.edges, incomingEdge],
|
|
488
|
+
nodes: [...definition.nodes, node],
|
|
489
|
+
},
|
|
490
|
+
editingEdgeId: null,
|
|
491
|
+
selectedEdgeIds: [],
|
|
492
|
+
selectedNodeId: node.id,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── Fallback / emptiness ───────────────────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
export function readFallbackWorkflowDefinition(): WorkflowDefinition {
|
|
499
|
+
return {
|
|
500
|
+
edges: [],
|
|
501
|
+
meta: { schemaVersion: 1 },
|
|
502
|
+
nodes: [
|
|
503
|
+
{
|
|
504
|
+
data: { label: '開始' },
|
|
505
|
+
id: 'start',
|
|
506
|
+
position: { x: 80, y: 160 },
|
|
507
|
+
type: 'startEvent',
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
data: { endState: 'APPROVED', label: '完成', triggerMode: 'AND' },
|
|
511
|
+
id: 'end',
|
|
512
|
+
position: { x: 560, y: 160 },
|
|
513
|
+
type: 'endEvent',
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
export function isEmptyDesignerWorkflowDefinition(
|
|
520
|
+
definition: WorkflowDefinition,
|
|
521
|
+
): boolean {
|
|
522
|
+
return (
|
|
523
|
+
definition.edges.length === 0 &&
|
|
524
|
+
definition.nodes.length === 2 &&
|
|
525
|
+
definition.nodes.some((node) => node.type === 'startEvent') &&
|
|
526
|
+
definition.nodes.some((node) => node.type === 'endEvent')
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ── Connection rules ───────────────────────────────────────────────────────
|
|
531
|
+
|
|
532
|
+
export function isWorkflowConnectionValid(
|
|
533
|
+
connection: WorkflowConnectionCandidate,
|
|
534
|
+
nodes: readonly WorkflowNode[],
|
|
535
|
+
): boolean {
|
|
536
|
+
const sourceNode = connection.source
|
|
537
|
+
? (nodes.find((node) => node.id === connection.source) ?? null)
|
|
538
|
+
: null;
|
|
539
|
+
const targetNode = connection.target
|
|
540
|
+
? (nodes.find((node) => node.id === connection.target) ?? null)
|
|
541
|
+
: null;
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
Boolean(sourceNode) &&
|
|
545
|
+
Boolean(targetNode) &&
|
|
546
|
+
connection.source !== connection.target &&
|
|
547
|
+
connection.sourceHandle === WORKFLOW_OUTPUT_HANDLE_ID &&
|
|
548
|
+
connection.targetHandle === WORKFLOW_INPUT_HANDLE_ID &&
|
|
549
|
+
Boolean(sourceNode && isWorkflowNodeOutputConnectable(sourceNode)) &&
|
|
550
|
+
Boolean(targetNode && isWorkflowNodeInputConnectable(targetNode))
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function isWorkflowNodeRemovable(node: WorkflowNode): boolean {
|
|
555
|
+
return node.type !== 'startEvent' && node.type !== 'endEvent';
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export function isWorkflowNodeInputConnectable(node: WorkflowNode): boolean {
|
|
559
|
+
return node.type !== 'startEvent';
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
export function isWorkflowNodeOutputConnectable(node: WorkflowNode): boolean {
|
|
563
|
+
return node.type !== 'endEvent' && !isAsyncNotifyServiceTask(node);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function isAsyncNotifyServiceTask(node: WorkflowNode): boolean {
|
|
567
|
+
return node.type === 'serviceTask' && node.data.action.type === 'NOTIFY';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
export function isExclusiveGatewaySourceEdge(
|
|
571
|
+
edge: WorkflowEdge,
|
|
572
|
+
nodes: readonly WorkflowNode[],
|
|
573
|
+
): boolean {
|
|
574
|
+
return nodes.some(
|
|
575
|
+
(node) => node.id === edge.source && node.type === 'exclusiveGateway',
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function isParallelGatewaySourceEdge(
|
|
580
|
+
edge: WorkflowEdge,
|
|
581
|
+
nodes: readonly WorkflowNode[],
|
|
582
|
+
): boolean {
|
|
583
|
+
return nodes.some(
|
|
584
|
+
(node) => node.id === edge.source && node.type === 'parallelGateway',
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
export function toggleSelectedEdgeId(
|
|
589
|
+
edgeIds: readonly string[],
|
|
590
|
+
edgeId: string,
|
|
591
|
+
): readonly string[] {
|
|
592
|
+
return edgeIds.includes(edgeId)
|
|
593
|
+
? edgeIds.filter((currentEdgeId) => currentEdgeId !== edgeId)
|
|
594
|
+
: [...edgeIds, edgeId];
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ── Validation ─────────────────────────────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
export function readWorkflowDefinitionIssue(
|
|
600
|
+
definition: WorkflowDefinition,
|
|
601
|
+
): string | null {
|
|
602
|
+
const incompleteUserTaskNode = definition.nodes.find(
|
|
603
|
+
(node) =>
|
|
604
|
+
node.type === 'userTask' &&
|
|
605
|
+
Boolean(readApproverResolverIssue(node.data.approverResolver)),
|
|
606
|
+
);
|
|
607
|
+
const incompleteNotifyNode = definition.nodes.find(
|
|
608
|
+
(node) =>
|
|
609
|
+
node.type === 'serviceTask' &&
|
|
610
|
+
node.data.action.type === 'NOTIFY' &&
|
|
611
|
+
readServiceTaskMemberIds(node.data.action).length === 0,
|
|
612
|
+
);
|
|
613
|
+
const incompleteConditionEdge = definition.edges.find(
|
|
614
|
+
(edge) =>
|
|
615
|
+
isExclusiveGatewaySourceEdge(edge, definition.nodes) &&
|
|
616
|
+
!edge.data.isDefault &&
|
|
617
|
+
!edge.data.condition,
|
|
618
|
+
);
|
|
619
|
+
// An exclusive gateway must always keep one "其他情況" (default) outgoing edge,
|
|
620
|
+
// otherwise the flow has no path when none of the conditions match.
|
|
621
|
+
const gatewayMissingDefault = definition.nodes.find((node) => {
|
|
622
|
+
if (node.type !== 'exclusiveGateway') {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const outgoing = definition.edges.filter(
|
|
627
|
+
(edge) => edge.source === node.id,
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
return outgoing.length > 0 && !outgoing.some((edge) => edge.data.isDefault);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
if (incompleteUserTaskNode && incompleteUserTaskNode.type === 'userTask') {
|
|
634
|
+
return readApproverResolverIssue(
|
|
635
|
+
incompleteUserTaskNode.data.approverResolver,
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (incompleteNotifyNode) {
|
|
640
|
+
return '知會節點需要至少一位知會對象。';
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (incompleteConditionEdge) {
|
|
644
|
+
return '條件分流的每條輸出連線都需要先設定條件。';
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (gatewayMissingDefault) {
|
|
648
|
+
return '條件分流需要保留一條「其他情況」預設路徑(用 set_edge_default 將其中一條輸出連線設為預設)。';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export function readApproverResolverIssue(
|
|
655
|
+
resolver: ApproverResolver,
|
|
656
|
+
): string | null {
|
|
657
|
+
if (resolver.type === 'DIRECT' && resolver.memberIds.length === 0) {
|
|
658
|
+
return '簽核節點需要指定簽核會員。';
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (resolver.type === 'ORG_MANAGER' && resolver.levelsUp < 1) {
|
|
662
|
+
return '簽核節點需要指定有效的主管層級。';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (resolver.type === 'ORG_UNIT_MANAGER' && !resolver.orgUnitId.trim()) {
|
|
666
|
+
return '簽核節點需要指定組織。';
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (resolver.type === 'ORG_UNIT_MEMBER' && !resolver.orgUnitId.trim()) {
|
|
670
|
+
return '簽核節點需要指定組織。';
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (
|
|
674
|
+
resolver.type === 'ORG_UNIT_POSITION' &&
|
|
675
|
+
(!resolver.orgUnitId.trim() || !resolver.positionId.trim())
|
|
676
|
+
) {
|
|
677
|
+
return '簽核節點需要指定組織與職位。';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (
|
|
681
|
+
(resolver.type === 'ORG_MANAGER' || resolver.type === 'ORG_UNIT_MANAGER') &&
|
|
682
|
+
resolver.fallback?.type === 'DIRECT' &&
|
|
683
|
+
!resolver.fallback.memberId.trim()
|
|
684
|
+
) {
|
|
685
|
+
return '簽核節點需要指定改派固定人。';
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (resolver.type === 'POSITION' && !resolver.positionId.trim()) {
|
|
689
|
+
return '簽核節點需要指定職位。';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
export function hasConfiguredConditionEdges(
|
|
696
|
+
definition: WorkflowDefinition,
|
|
697
|
+
): boolean {
|
|
698
|
+
return definition.edges.some(
|
|
699
|
+
(edge) =>
|
|
700
|
+
isExclusiveGatewaySourceEdge(edge, definition.nodes) &&
|
|
701
|
+
Boolean(
|
|
702
|
+
edge.data.condition ||
|
|
703
|
+
edge.data.conditionFieldKey ||
|
|
704
|
+
edge.data.conditionOperator ||
|
|
705
|
+
edge.data.conditionValue,
|
|
706
|
+
),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function readServiceTaskMemberIds(
|
|
711
|
+
action: ServiceAction,
|
|
712
|
+
): readonly string[] {
|
|
713
|
+
return action.type === 'NOTIFY' && action.recipients.type === 'DIRECT'
|
|
714
|
+
? action.recipients.memberIds
|
|
715
|
+
: [];
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// ── Condition compilation ──────────────────────────────────────────────────
|
|
719
|
+
|
|
720
|
+
export function readConditionField(
|
|
721
|
+
schema: FormDefinitionSchema | null,
|
|
722
|
+
fieldKey: string | null,
|
|
723
|
+
): FormFieldDefinition | null {
|
|
724
|
+
return fieldKey
|
|
725
|
+
? (schema?.fields.find((field) => field.fieldKey === fieldKey) ?? null)
|
|
726
|
+
: null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function readConditionOperator(
|
|
730
|
+
value: string | null,
|
|
731
|
+
): WorkflowEdgeConditionOperator | null {
|
|
732
|
+
return CONDITION_OPERATOR_OPTIONS.some((option) => option.id === value)
|
|
733
|
+
? (value as WorkflowEdgeConditionOperator)
|
|
734
|
+
: null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export function readConditionOperatorIds(
|
|
738
|
+
field: FormFieldDefinition,
|
|
739
|
+
): readonly WorkflowEdgeConditionOperator[] {
|
|
740
|
+
if (field.type === 'file_upload') {
|
|
741
|
+
return ['IS_FILLED', 'IS_EMPTY'];
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
if (field.type === 'boolean') {
|
|
745
|
+
return ['EQUALS', 'NOT_EQUALS', 'IS_FILLED', 'IS_EMPTY'];
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (
|
|
749
|
+
field.type === 'date' ||
|
|
750
|
+
field.type === 'datetime' ||
|
|
751
|
+
field.type === 'money' ||
|
|
752
|
+
field.type === 'number'
|
|
753
|
+
) {
|
|
754
|
+
return [
|
|
755
|
+
'EQUALS',
|
|
756
|
+
'NOT_EQUALS',
|
|
757
|
+
'GREATER_THAN',
|
|
758
|
+
'GREATER_THAN_OR_EQUALS',
|
|
759
|
+
'LESS_THAN',
|
|
760
|
+
'LESS_THAN_OR_EQUALS',
|
|
761
|
+
'IS_FILLED',
|
|
762
|
+
'IS_EMPTY',
|
|
763
|
+
];
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return ['EQUALS', 'NOT_EQUALS', 'IS_FILLED', 'IS_EMPTY'];
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
export interface ConditionValueOption {
|
|
770
|
+
readonly id: string;
|
|
771
|
+
readonly name: string;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
export function readConditionValueOptions(
|
|
775
|
+
field: FormFieldDefinition | null,
|
|
776
|
+
): readonly ConditionValueOption[] {
|
|
777
|
+
if (!field) {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (field.type === 'boolean') {
|
|
782
|
+
return [
|
|
783
|
+
{ id: 'true', name: '是' },
|
|
784
|
+
{ id: 'false', name: '否' },
|
|
785
|
+
];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
if (
|
|
789
|
+
field.type === 'checkbox' ||
|
|
790
|
+
field.type === 'radio' ||
|
|
791
|
+
field.type === 'select'
|
|
792
|
+
) {
|
|
793
|
+
return field.options.map((option) => ({
|
|
794
|
+
id: option.value,
|
|
795
|
+
name: option.label,
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
export function readNextConditionOperator(
|
|
803
|
+
field: FormFieldDefinition | null,
|
|
804
|
+
operator: WorkflowEdgeConditionOperator | null,
|
|
805
|
+
): WorkflowEdgeConditionOperator | undefined {
|
|
806
|
+
if (!field) {
|
|
807
|
+
return undefined;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const operatorIds = readConditionOperatorIds(field);
|
|
811
|
+
|
|
812
|
+
return operator && operatorIds.includes(operator) ? operator : operatorIds[0];
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
export function readNextConditionValue(
|
|
816
|
+
field: FormFieldDefinition | null,
|
|
817
|
+
operator: WorkflowEdgeConditionOperator | undefined,
|
|
818
|
+
value: string | null,
|
|
819
|
+
): string | undefined {
|
|
820
|
+
if (!field || !operator || !shouldConditionOperatorUseValue(operator)) {
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const valueOptions = readConditionValueOptions(field);
|
|
825
|
+
|
|
826
|
+
if (valueOptions.length === 0) {
|
|
827
|
+
return value ?? undefined;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return valueOptions.some((option) => option.id === value)
|
|
831
|
+
? (value ?? undefined)
|
|
832
|
+
: valueOptions[0]?.id;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export function shouldConditionOperatorUseValue(
|
|
836
|
+
operator: WorkflowEdgeConditionOperator,
|
|
837
|
+
): boolean {
|
|
838
|
+
return CONDITION_OPERATORS_REQUIRING_VALUE.includes(operator);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
export function readConditionLabel(
|
|
842
|
+
field: FormFieldDefinition | null,
|
|
843
|
+
operator: WorkflowEdgeConditionOperator | undefined,
|
|
844
|
+
value: string | undefined,
|
|
845
|
+
): string | undefined {
|
|
846
|
+
if (!field || !operator) {
|
|
847
|
+
return undefined;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const operatorLabel = readConditionOperatorLabel(operator);
|
|
851
|
+
|
|
852
|
+
if (!shouldConditionOperatorUseValue(operator)) {
|
|
853
|
+
return `${field.label} ${operatorLabel}`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
if (!value) {
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
return `${field.label} ${operatorLabel} ${readConditionValueLabel(
|
|
861
|
+
field,
|
|
862
|
+
value,
|
|
863
|
+
)}`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
export function readConditionOperatorLabel(
|
|
867
|
+
operator: WorkflowEdgeConditionOperator,
|
|
868
|
+
): string {
|
|
869
|
+
return (
|
|
870
|
+
CONDITION_OPERATOR_OPTIONS.find((option) => option.id === operator)?.name ??
|
|
871
|
+
operator
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export function readConditionValueLabel(
|
|
876
|
+
field: FormFieldDefinition,
|
|
877
|
+
value: string,
|
|
878
|
+
): string {
|
|
879
|
+
if (field.type === 'boolean') {
|
|
880
|
+
return value === 'true' ? '是' : '否';
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (
|
|
884
|
+
field.type === 'checkbox' ||
|
|
885
|
+
field.type === 'radio' ||
|
|
886
|
+
field.type === 'select'
|
|
887
|
+
) {
|
|
888
|
+
return readFormFieldOption(field.options, value)?.label ?? value;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
return value;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export function readFormFieldOption(
|
|
895
|
+
options: readonly FormFieldOption[],
|
|
896
|
+
value: string,
|
|
897
|
+
): FormFieldOption | null {
|
|
898
|
+
return options.find((option) => option.value === value) ?? null;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function readConditionExpression(
|
|
902
|
+
field: FormFieldDefinition | null,
|
|
903
|
+
operator: WorkflowEdgeConditionOperator | undefined,
|
|
904
|
+
value: string | undefined,
|
|
905
|
+
): string | undefined {
|
|
906
|
+
if (!field || !operator) {
|
|
907
|
+
return undefined;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
const fieldReference = readFormFieldReference(field.fieldKey);
|
|
911
|
+
|
|
912
|
+
if (operator === 'IS_FILLED') {
|
|
913
|
+
return `${fieldReference} != null && ${fieldReference} != ""`;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (operator === 'IS_EMPTY') {
|
|
917
|
+
return `${fieldReference} == null || ${fieldReference} == ""`;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (!value) {
|
|
921
|
+
return undefined;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
return `${fieldReference} ${readConditionExpressionOperator(
|
|
925
|
+
operator,
|
|
926
|
+
)} ${readConditionExpressionValue(field, value)}`;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export function readFormFieldReference(fieldKey: string): string {
|
|
930
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(fieldKey)
|
|
931
|
+
? `form.${fieldKey}`
|
|
932
|
+
: `form[${JSON.stringify(fieldKey)}]`;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export function readConditionExpressionOperator(
|
|
936
|
+
operator: WorkflowEdgeConditionOperator,
|
|
937
|
+
): string {
|
|
938
|
+
if (operator === 'EQUALS') {
|
|
939
|
+
return '==';
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
if (operator === 'NOT_EQUALS') {
|
|
943
|
+
return '!=';
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (operator === 'GREATER_THAN') {
|
|
947
|
+
return '>';
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (operator === 'GREATER_THAN_OR_EQUALS') {
|
|
951
|
+
return '>=';
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (operator === 'LESS_THAN') {
|
|
955
|
+
return '<';
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return '<=';
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export function readConditionExpressionValue(
|
|
962
|
+
field: FormFieldDefinition,
|
|
963
|
+
value: string,
|
|
964
|
+
): string {
|
|
965
|
+
if (field.type === 'boolean') {
|
|
966
|
+
return value === 'true' ? 'true' : 'false';
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
if (field.type === 'money' || field.type === 'number') {
|
|
970
|
+
return Number.isFinite(Number(value)) ? value : JSON.stringify(value);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return JSON.stringify(value);
|
|
974
|
+
}
|