nucleus-core-ts 0.10.115 → 0.10.117
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/dist/.build-ok +1 -1
- package/dist/fe/components/VerificationFlowPage/components/PendingTab.d.ts +4 -1
- package/dist/fe/components/VerificationFlowPage/components/PendingTab.js +160 -92
- package/dist/fe/components/VerificationFlowPage/components/PropertiesPanel.d.ts +3 -1
- package/dist/fe/components/VerificationFlowPage/components/PropertiesPanel.js +43 -7
- package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.d.ts +1 -1
- package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.js +55 -13
- package/dist/fe/components/VerificationFlowPage/labels.d.ts +31 -0
- package/dist/fe/components/VerificationFlowPage/labels.js +13 -0
- package/dist/fe/components/VerificationFlowPage/theme/index.d.ts +27 -0
- package/dist/fe/components/VerificationFlowPage/theme/index.js +66 -11
- package/dist/fe/components/VerificationFlowPage/types/index.d.ts +37 -0
- package/dist/fe/components/VerificationPanel/components/PendingVerifications.js +16 -1
- package/dist/index.js +13 -13
- package/dist/server.js +13 -13
- package/dist/src/Nucleus/routes/entity/activeVerification.d.ts +27 -0
- package/dist/src/Nucleus/routes/entity/types.d.ts +0 -2
- package/dist/src/Services/Verification/index.d.ts +17 -0
- package/package.json +1 -1
- package/scripts/generate-schema.test.ts +48 -0
- package/scripts/generate-schema.ts +12 -1
- package/dist/src/Nucleus/routes/entity/sortCollation.d.ts +0 -16
package/dist/.build-ok
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.10.
|
|
1
|
+
0.10.117
|
|
@@ -9,6 +9,9 @@ interface PendingTabProps {
|
|
|
9
9
|
onDecisionMade?: (entityName: string, entityId: string) => void;
|
|
10
10
|
/** Spread over the English defaults; omit it and the tab stays English. */
|
|
11
11
|
labels?: Partial<VerificationFlowLabels>;
|
|
12
|
+
/** Names the record a decision is about. See `VerificationFlowPageProps`. */
|
|
13
|
+
describeEntity?: (entityName: string, entityId: string) => string | undefined;
|
|
14
|
+
entityHref?: (entityName: string, entityId: string) => string | undefined;
|
|
12
15
|
}
|
|
13
|
-
export declare function PendingTab({ pendingAction, decideAction, uploadSignatureAction, onDecisionMade, labels, }: PendingTabProps): ReactElement;
|
|
16
|
+
export declare function PendingTab({ pendingAction, decideAction, uploadSignatureAction, onDecisionMade, labels, describeEntity, entityHref, }: PendingTabProps): ReactElement;
|
|
14
17
|
export {};
|
|
@@ -5,11 +5,26 @@ import { DEFAULT_VERIFICATION_FLOW_LABELS } from '../labels';
|
|
|
5
5
|
import { useVerificationFlowStore } from '../store';
|
|
6
6
|
import { getActiveTheme } from '../theme/store';
|
|
7
7
|
import { planDecision, signatureFileType } from './decisionPlan';
|
|
8
|
-
export function PendingTab({ pendingAction, decideAction, uploadSignatureAction, onDecisionMade, labels }) {
|
|
8
|
+
export function PendingTab({ pendingAction, decideAction, uploadSignatureAction, onDecisionMade, labels, describeEntity, entityHref }) {
|
|
9
9
|
// Local, not in the store: only one card is in deciding mode at a time, and
|
|
10
10
|
// a chosen file must not survive cancelling out of that card.
|
|
11
11
|
const [signatureFile, setSignatureFile] = useState(null);
|
|
12
|
-
|
|
12
|
+
/*
|
|
13
|
+
* Renamed from `signatureNote`, and that is the whole bug.
|
|
14
|
+
*
|
|
15
|
+
* It carried every engine refusal — wrong role, already signed an earlier
|
|
16
|
+
* step, account switched off — but its ONE render site sat inside the
|
|
17
|
+
* `require_signature` branch. On a step that needs no signature (the
|
|
18
|
+
* ordinary case) pressing Approve wrote the refusal into state that nothing
|
|
19
|
+
* rendered: the card stayed open, nothing was recorded, and the reviewer was
|
|
20
|
+
* told nothing at all. It is now its own always-rendered block.
|
|
21
|
+
*/ const [decisionNote, setDecisionNote] = useState(null);
|
|
22
|
+
/*
|
|
23
|
+
* A queue that could not be READ is not an empty queue. Both failure paths
|
|
24
|
+
* used to call `setPendingItems([])`, so a 500, an expired session and a
|
|
25
|
+
* genuinely clear queue all rendered the same reassuring tick and
|
|
26
|
+
* "No pending verifications".
|
|
27
|
+
*/ const [loadError, setLoadError] = useState(null);
|
|
13
28
|
const theme = getActiveTheme();
|
|
14
29
|
const say = {
|
|
15
30
|
...DEFAULT_VERIFICATION_FLOW_LABELS,
|
|
@@ -18,6 +33,7 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
18
33
|
const store = useVerificationFlowStore();
|
|
19
34
|
const loadPending = useEffectEvent(()=>{
|
|
20
35
|
store.setLoadingPending(true);
|
|
36
|
+
setLoadError(null);
|
|
21
37
|
pendingAction.start({
|
|
22
38
|
payload: undefined,
|
|
23
39
|
onAfterHandle: (res)=>{
|
|
@@ -25,6 +41,7 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
25
41
|
if (response.success === false) {
|
|
26
42
|
console.error('[PendingTab] API returned success=false:', res);
|
|
27
43
|
store.setPendingItems([]);
|
|
44
|
+
setLoadError(response.message || say.pendingLoadFailedBody);
|
|
28
45
|
store.setLoadingPending(false);
|
|
29
46
|
return;
|
|
30
47
|
}
|
|
@@ -36,6 +53,7 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
36
53
|
const msg = errObj?.message || errObj?.error || JSON.stringify(err);
|
|
37
54
|
console.error(`[PendingTab] Pending error (HTTP ${code ?? '?'}):`, msg);
|
|
38
55
|
store.setPendingItems([]);
|
|
56
|
+
setLoadError(typeof msg === 'string' ? msg : say.pendingLoadFailedBody);
|
|
39
57
|
store.setLoadingPending(false);
|
|
40
58
|
}
|
|
41
59
|
});
|
|
@@ -72,19 +90,19 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
72
90
|
* already says exactly what is wrong, and a reviewer can act on
|
|
73
91
|
* "cannot decide it" in a way they cannot act on "something failed".
|
|
74
92
|
*/ if (res && res.success === false) {
|
|
75
|
-
|
|
93
|
+
setDecisionNote(res.message || say.decisionRefused);
|
|
76
94
|
return;
|
|
77
95
|
}
|
|
78
96
|
store.setDecidingId(null);
|
|
79
97
|
store.setDecisionReason('');
|
|
80
98
|
setSignatureFile(null);
|
|
81
|
-
|
|
99
|
+
setDecisionNote(null);
|
|
82
100
|
onDecisionMade?.(item.entity_name, item.entity_id);
|
|
83
101
|
loadPending();
|
|
84
102
|
},
|
|
85
103
|
onErrorHandle: (err)=>{
|
|
86
104
|
const msg = err?.message;
|
|
87
|
-
|
|
105
|
+
setDecisionNote(msg || say.decisionRefused);
|
|
88
106
|
}
|
|
89
107
|
});
|
|
90
108
|
});
|
|
@@ -99,7 +117,7 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
99
117
|
* uploaded as the signed-in user and only its id is sent. A rejection needs
|
|
100
118
|
* no signature: you do not sign what you are refusing.
|
|
101
119
|
*/ const handleDecide = useEffectEvent((item, decision)=>{
|
|
102
|
-
|
|
120
|
+
setDecisionNote(null);
|
|
103
121
|
const plan = planDecision({
|
|
104
122
|
decision,
|
|
105
123
|
requireSignature: item.require_signature,
|
|
@@ -111,7 +129,7 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
111
129
|
return;
|
|
112
130
|
}
|
|
113
131
|
if (plan.kind === 'blocked') {
|
|
114
|
-
|
|
132
|
+
setDecisionNote(plan.reason === 'no-upload-action' ? say.signatureUnavailable : say.signatureMissing);
|
|
115
133
|
return;
|
|
116
134
|
}
|
|
117
135
|
if (!uploadSignatureAction || !signatureFile) return;
|
|
@@ -126,7 +144,7 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
126
144
|
extension: signatureFile.name.split('.').pop() || '',
|
|
127
145
|
type: signatureFileType(signatureFile.type)
|
|
128
146
|
}));
|
|
129
|
-
|
|
147
|
+
setDecisionNote(say.signatureUploading);
|
|
130
148
|
uploadSignatureAction.start({
|
|
131
149
|
payload: form,
|
|
132
150
|
onAfterHandle: (res)=>{
|
|
@@ -134,12 +152,12 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
134
152
|
if (!id) {
|
|
135
153
|
// Reported, never swallowed: a decision that silently did not happen
|
|
136
154
|
// is worse than one that failed loudly.
|
|
137
|
-
|
|
155
|
+
setDecisionNote(say.signatureUploadFailed);
|
|
138
156
|
return;
|
|
139
157
|
}
|
|
140
158
|
sendDecision(item, decision, id);
|
|
141
159
|
},
|
|
142
|
-
onErrorHandle: ()=>
|
|
160
|
+
onErrorHandle: ()=>setDecisionNote(say.signatureUploadFailed)
|
|
143
161
|
});
|
|
144
162
|
});
|
|
145
163
|
if (store.isLoadingPending) {
|
|
@@ -150,6 +168,36 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
150
168
|
})
|
|
151
169
|
});
|
|
152
170
|
}
|
|
171
|
+
/*
|
|
172
|
+
* The failure state comes FIRST, and it is a different screen from the
|
|
173
|
+
* empty one. "You have nothing to approve" is a fact a reviewer acts on by
|
|
174
|
+
* going away; "we could not ask" is one they act on by trying again.
|
|
175
|
+
*/ if (loadError) {
|
|
176
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
177
|
+
className: theme.pending.failure.container,
|
|
178
|
+
role: "alert",
|
|
179
|
+
children: [
|
|
180
|
+
/*#__PURE__*/ _jsx("p", {
|
|
181
|
+
className: theme.pending.failure.title,
|
|
182
|
+
children: say.pendingLoadFailedTitle
|
|
183
|
+
}),
|
|
184
|
+
/*#__PURE__*/ _jsx("p", {
|
|
185
|
+
className: theme.pending.failure.text,
|
|
186
|
+
children: say.pendingLoadFailedBody
|
|
187
|
+
}),
|
|
188
|
+
/*#__PURE__*/ _jsx("p", {
|
|
189
|
+
className: theme.pending.failure.text,
|
|
190
|
+
children: loadError
|
|
191
|
+
}),
|
|
192
|
+
/*#__PURE__*/ _jsx("button", {
|
|
193
|
+
type: "button",
|
|
194
|
+
onClick: ()=>loadPending(),
|
|
195
|
+
className: theme.pending.failure.button,
|
|
196
|
+
children: say.retry
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
153
201
|
if (store.pendingItems.length === 0) {
|
|
154
202
|
return /*#__PURE__*/ _jsxs("div", {
|
|
155
203
|
className: theme.pending.empty.container,
|
|
@@ -185,89 +233,109 @@ export function PendingTab({ pendingAction, decideAction, uploadSignatureAction,
|
|
|
185
233
|
className: theme.pending.title,
|
|
186
234
|
children: say.pendingApprovals(store.pendingItems.length)
|
|
187
235
|
}),
|
|
188
|
-
store.pendingItems.map((item)=>/*#__PURE__*/
|
|
236
|
+
store.pendingItems.map((item)=>/*#__PURE__*/ _jsxs("div", {
|
|
189
237
|
className: theme.pending.card.container,
|
|
190
|
-
children:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
238
|
+
children: [
|
|
239
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
240
|
+
className: "flex flex-wrap items-start justify-between gap-3",
|
|
241
|
+
children: [
|
|
242
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
243
|
+
className: "min-w-0",
|
|
244
|
+
children: [
|
|
245
|
+
/*#__PURE__*/ _jsx("p", {
|
|
246
|
+
className: theme.pending.card.title,
|
|
247
|
+
children: say.pendingCardTitle(item.flow_name, item.step_order)
|
|
248
|
+
}),
|
|
249
|
+
item.step_name ? /*#__PURE__*/ _jsx("p", {
|
|
250
|
+
className: theme.pending.card.subtitle,
|
|
251
|
+
children: item.step_name
|
|
252
|
+
}) : null,
|
|
253
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
254
|
+
className: theme.pending.card.recordRow,
|
|
255
|
+
children: [
|
|
256
|
+
/*#__PURE__*/ _jsx("span", {
|
|
257
|
+
className: theme.pending.card.recordLabel,
|
|
258
|
+
children: say.pendingRecordLabel
|
|
259
|
+
}),
|
|
260
|
+
/*#__PURE__*/ _jsx("span", {
|
|
261
|
+
className: theme.pending.card.recordValue,
|
|
262
|
+
children: describeEntity?.(item.entity_name, item.entity_id) ?? `${item.entity_name}/${item.entity_id}`
|
|
263
|
+
}),
|
|
264
|
+
entityHref?.(item.entity_name, item.entity_id) ? /*#__PURE__*/ _jsx("a", {
|
|
265
|
+
href: entityHref(item.entity_name, item.entity_id),
|
|
266
|
+
target: "_blank",
|
|
267
|
+
rel: "noopener noreferrer",
|
|
268
|
+
className: theme.pending.card.recordLink,
|
|
269
|
+
children: say.openRecord
|
|
270
|
+
}) : null
|
|
271
|
+
]
|
|
272
|
+
})
|
|
273
|
+
]
|
|
274
|
+
}),
|
|
275
|
+
store.decidingId === item.instance_id ? /*#__PURE__*/ _jsxs("div", {
|
|
276
|
+
className: theme.pending.card.actions,
|
|
277
|
+
children: [
|
|
278
|
+
item.require_signature ? /*#__PURE__*/ _jsxs("label", {
|
|
279
|
+
className: "flex flex-col gap-1 text-xs",
|
|
280
|
+
children: [
|
|
281
|
+
/*#__PURE__*/ _jsx("span", {
|
|
282
|
+
children: say.signaturePrompt
|
|
283
|
+
}),
|
|
284
|
+
/*#__PURE__*/ _jsx("input", {
|
|
285
|
+
type: "file",
|
|
286
|
+
onChange: (e)=>{
|
|
287
|
+
setSignatureFile(e.target.files?.[0] ?? null);
|
|
288
|
+
setDecisionNote(null);
|
|
289
|
+
},
|
|
290
|
+
className: theme.pending.input
|
|
291
|
+
})
|
|
292
|
+
]
|
|
293
|
+
}) : null,
|
|
294
|
+
/*#__PURE__*/ _jsx("input", {
|
|
295
|
+
type: "text",
|
|
296
|
+
placeholder: say.reasonPlaceholder,
|
|
297
|
+
value: store.decisionReason,
|
|
298
|
+
onChange: (e)=>store.setDecisionReason(e.target.value),
|
|
299
|
+
className: theme.pending.input
|
|
300
|
+
}),
|
|
301
|
+
/*#__PURE__*/ _jsx("button", {
|
|
302
|
+
type: "button",
|
|
303
|
+
onClick: ()=>handleDecide(item, 'approved'),
|
|
304
|
+
className: theme.pending.approveButton,
|
|
305
|
+
children: say.approve
|
|
306
|
+
}),
|
|
307
|
+
/*#__PURE__*/ _jsx("button", {
|
|
308
|
+
type: "button",
|
|
309
|
+
onClick: ()=>handleDecide(item, 'rejected'),
|
|
310
|
+
className: theme.pending.rejectButton,
|
|
311
|
+
children: say.reject
|
|
312
|
+
}),
|
|
313
|
+
/*#__PURE__*/ _jsx("button", {
|
|
314
|
+
type: "button",
|
|
315
|
+
onClick: ()=>{
|
|
316
|
+
store.setDecidingId(null);
|
|
317
|
+
store.setDecisionReason('');
|
|
318
|
+
setSignatureFile(null);
|
|
319
|
+
setDecisionNote(null);
|
|
320
|
+
},
|
|
321
|
+
className: theme.pending.cancelButton,
|
|
322
|
+
children: say.cancel
|
|
323
|
+
})
|
|
324
|
+
]
|
|
325
|
+
}) : /*#__PURE__*/ _jsx("button", {
|
|
326
|
+
type: "button",
|
|
327
|
+
onClick: ()=>store.setDecidingId(item.instance_id),
|
|
328
|
+
className: theme.pending.reviewButton,
|
|
329
|
+
children: say.review
|
|
330
|
+
})
|
|
331
|
+
]
|
|
332
|
+
}),
|
|
333
|
+
store.decidingId === item.instance_id && decisionNote ? /*#__PURE__*/ _jsx("p", {
|
|
334
|
+
className: theme.pending.notice,
|
|
335
|
+
role: "alert",
|
|
336
|
+
children: decisionNote
|
|
337
|
+
}) : null
|
|
338
|
+
]
|
|
271
339
|
}, `${item.instance_id}-${item.step_order}`))
|
|
272
340
|
]
|
|
273
341
|
});
|
|
@@ -34,6 +34,8 @@ interface PropertiesPanelProps {
|
|
|
34
34
|
panelTheme: VerificationFlowPageTheme['propertiesPanel'];
|
|
35
35
|
/** Spread over the English defaults; omit it and the panel stays English. */
|
|
36
36
|
labels?: Partial<VerificationFlowLabels>;
|
|
37
|
+
/** The channels this install can deliver on. Omit = all five, as before. */
|
|
38
|
+
availableChannels?: NotificationChannel[];
|
|
37
39
|
}
|
|
38
|
-
export declare function PropertiesPanel({ nodeId, nodeData, onUpdateNode, onDeleteNode, onClose, listUsersAction, listRolesAction, panelTheme, labels, }: PropertiesPanelProps): ReactElement;
|
|
40
|
+
export declare function PropertiesPanel({ nodeId, nodeData, onUpdateNode, onDeleteNode, onClose, listUsersAction, listRolesAction, panelTheme, labels, availableChannels, }: PropertiesPanelProps): ReactElement;
|
|
39
41
|
export {};
|
|
@@ -28,10 +28,11 @@ const ALL_RECIPIENT_TYPES = [
|
|
|
28
28
|
'user'
|
|
29
29
|
];
|
|
30
30
|
// ─── Channel Multi-Select ─────────────────────────────────────────
|
|
31
|
-
function ChannelMultiSelect({ selected, onChange, msTheme, say }) {
|
|
31
|
+
function ChannelMultiSelect({ selected, onChange, msTheme, say, available }) {
|
|
32
32
|
const theme = msTheme;
|
|
33
33
|
const channelLabels = notificationChannelLabels(say);
|
|
34
34
|
const [isOpen, setIsOpen] = useState(false);
|
|
35
|
+
const canDeliver = (ch)=>!available || available.includes(ch);
|
|
35
36
|
const toggle = (channel)=>{
|
|
36
37
|
if (selected.includes(channel)) {
|
|
37
38
|
onChange(selected.filter((c)=>c !== channel));
|
|
@@ -91,15 +92,28 @@ function ChannelMultiSelect({ selected, onChange, msTheme, say }) {
|
|
|
91
92
|
}),
|
|
92
93
|
isOpen && /*#__PURE__*/ _jsx("div", {
|
|
93
94
|
className: theme.dropdown,
|
|
94
|
-
children: ALL_CHANNELS.map((ch)
|
|
95
|
+
children: ALL_CHANNELS.map((ch)=>{
|
|
96
|
+
/*
|
|
97
|
+
* A channel this install cannot deliver on is shown and refused,
|
|
98
|
+
* not hidden: the author should see that SMS exists as a product
|
|
99
|
+
* feature and that THIS deployment does not have it, which is a
|
|
100
|
+
* different thing from it not existing at all.
|
|
101
|
+
*/ const deliverable = canDeliver(ch);
|
|
102
|
+
return /*#__PURE__*/ _jsxs("button", {
|
|
95
103
|
type: "button",
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
disabled: !deliverable,
|
|
105
|
+
title: deliverable ? undefined : say.channelUnavailable,
|
|
106
|
+
className: `${selected.includes(ch) ? theme.optionSelected : theme.option}${deliverable ? '' : ' opacity-40 cursor-not-allowed'}`,
|
|
107
|
+
onClick: ()=>{
|
|
108
|
+
if (deliverable) toggle(ch);
|
|
109
|
+
},
|
|
98
110
|
children: [
|
|
99
111
|
selected.includes(ch) ? '✓ ' : '',
|
|
100
|
-
channelLabels[ch] ?? ch
|
|
112
|
+
channelLabels[ch] ?? ch,
|
|
113
|
+
deliverable ? '' : ` — ${say.channelUnavailable}`
|
|
101
114
|
]
|
|
102
|
-
}, ch)
|
|
115
|
+
}, ch);
|
|
116
|
+
})
|
|
103
117
|
})
|
|
104
118
|
]
|
|
105
119
|
});
|
|
@@ -420,7 +434,7 @@ function RoleSearchList({ listRolesAction, selectedRole, onSelect, ursTheme, say
|
|
|
420
434
|
});
|
|
421
435
|
}
|
|
422
436
|
// ─── Main Properties Panel ────────────────────────────────────────
|
|
423
|
-
export function PropertiesPanel({ nodeId, nodeData, onUpdateNode, onDeleteNode, onClose, listUsersAction, listRolesAction, panelTheme, labels }) {
|
|
437
|
+
export function PropertiesPanel({ nodeId, nodeData, onUpdateNode, onDeleteNode, onClose, listUsersAction, listRolesAction, panelTheme, labels, availableChannels }) {
|
|
424
438
|
const theme = panelTheme;
|
|
425
439
|
const say = {
|
|
426
440
|
...DEFAULT_VERIFICATION_FLOW_LABELS,
|
|
@@ -477,6 +491,27 @@ export function PropertiesPanel({ nodeId, nodeData, onUpdateNode, onDeleteNode,
|
|
|
477
491
|
/*#__PURE__*/ _jsxs("div", {
|
|
478
492
|
className: theme.body,
|
|
479
493
|
children: [
|
|
494
|
+
/*#__PURE__*/ _jsxs("p", {
|
|
495
|
+
className: theme.help,
|
|
496
|
+
children: [
|
|
497
|
+
nodeType === 'step' && say.stepHelp,
|
|
498
|
+
nodeType === 'verifier' && say.verifierHelp,
|
|
499
|
+
nodeType === 'notification' && say.notificationHelp
|
|
500
|
+
]
|
|
501
|
+
}),
|
|
502
|
+
nodeType === 'step' && /* The one fact a step carries at runtime, and the operator cannot
|
|
503
|
+
* type it: it is derived from the arrows when the flow is saved. */ /*#__PURE__*/ _jsxs("p", {
|
|
504
|
+
className: theme.readOnlyRow,
|
|
505
|
+
children: [
|
|
506
|
+
/*#__PURE__*/ _jsx("span", {
|
|
507
|
+
children: say.stepOrderLabel
|
|
508
|
+
}),
|
|
509
|
+
/*#__PURE__*/ _jsx("span", {
|
|
510
|
+
className: theme.readOnlyValue,
|
|
511
|
+
children: Number(data.stepOrder) > 0 ? data.stepOrder : say.stepOrderUnset
|
|
512
|
+
})
|
|
513
|
+
]
|
|
514
|
+
}),
|
|
480
515
|
/*#__PURE__*/ _jsxs("div", {
|
|
481
516
|
className: theme.section,
|
|
482
517
|
children: [
|
|
@@ -677,6 +712,7 @@ export function PropertiesPanel({ nodeId, nodeData, onUpdateNode, onDeleteNode,
|
|
|
677
712
|
onChange: (channels)=>update({
|
|
678
713
|
channels
|
|
679
714
|
}),
|
|
715
|
+
available: availableChannels,
|
|
680
716
|
msTheme: theme.multiSelect,
|
|
681
717
|
say: say
|
|
682
718
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import '@xyflow/react/dist/style.css';
|
|
2
2
|
import type { ReactElement } from 'react';
|
|
3
3
|
import type { VerificationFlowPageProps } from '../types';
|
|
4
|
-
export declare function VerificationFlowPage({ entityName, title, subtitle, flowListAction, flowGetAction, flowSaveAction, flowPublishAction, flowUnpublishAction, flowDeleteAction, pendingAction, decideAction, uploadSignatureAction, listUsersAction, listRolesAction, entityStatusesAction, onFlowSelected, onDecisionMade, onEntityClick, onBack, showPending, showEntityStatuses, className, themeMode, labels, }: VerificationFlowPageProps): ReactElement;
|
|
4
|
+
export declare function VerificationFlowPage({ entityName, title, subtitle, flowListAction, flowGetAction, flowSaveAction, flowPublishAction, flowUnpublishAction, flowDeleteAction, pendingAction, decideAction, uploadSignatureAction, listUsersAction, listRolesAction, entityStatusesAction, onFlowSelected, onDecisionMade, onEntityClick, onBack, showPending, showEntityStatuses, className, themeMode, labels, describeEntity, entityHref, availableChannels, initialTab, }: VerificationFlowPageProps): ReactElement;
|
|
@@ -149,7 +149,19 @@ function createNewNode(nodeType, position, say) {
|
|
|
149
149
|
base.data = {
|
|
150
150
|
...base.data,
|
|
151
151
|
trigger: 'on_step_reached',
|
|
152
|
-
channels: [
|
|
152
|
+
channels: [
|
|
153
|
+
'portal'
|
|
154
|
+
],
|
|
155
|
+
/*
|
|
156
|
+
* The recipient the panel ALREADY shows.
|
|
157
|
+
*
|
|
158
|
+
* The properties panel renders `data.recipientType || 'all_verifiers'`,
|
|
159
|
+
* so a node nobody opened read "All Verifiers" while the field was
|
|
160
|
+
* undefined — and `buildSavePayload` only writes a recipient row when
|
|
161
|
+
* the field is set. The result was a notification node that drew
|
|
162
|
+
* correctly, saved, published green and reached nobody, with no error
|
|
163
|
+
* anywhere. Seeding it makes the screen's claim true.
|
|
164
|
+
*/ recipientType: 'all_verifiers'
|
|
153
165
|
};
|
|
154
166
|
}
|
|
155
167
|
return base;
|
|
@@ -252,14 +264,19 @@ function buildSavePayload(flowId, entityName, flowName, flowDescription, trigger
|
|
|
252
264
|
channel: ch
|
|
253
265
|
});
|
|
254
266
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
267
|
+
/*
|
|
268
|
+
* ALWAYS a recipient row, defaulting to what the panel displays.
|
|
269
|
+
*
|
|
270
|
+
* `if (d.recipientType)` skipped the row for every node the operator
|
|
271
|
+
* never opened, which is every node they dropped and wired without
|
|
272
|
+
* thinking about it — the commonest way to build a flow. A rule with no
|
|
273
|
+
* recipient resolves to an empty list and the send loop never runs.
|
|
274
|
+
*/ notification_recipients.push({
|
|
275
|
+
rule_id: n.id,
|
|
276
|
+
recipient_type: d.recipientType || 'all_verifiers',
|
|
277
|
+
recipient_user_id: d.recipientUserId,
|
|
278
|
+
recipient_role: d.recipientRole
|
|
279
|
+
});
|
|
263
280
|
}
|
|
264
281
|
return {
|
|
265
282
|
flow_id: flowId,
|
|
@@ -280,7 +297,7 @@ function buildSavePayload(flowId, entityName, flowName, flowDescription, trigger
|
|
|
280
297
|
};
|
|
281
298
|
}
|
|
282
299
|
// ─── Main Component ───────────────────────────────────────────────
|
|
283
|
-
export function VerificationFlowPage({ entityName, title, subtitle, flowListAction, flowGetAction, flowSaveAction, flowPublishAction, flowUnpublishAction, flowDeleteAction, pendingAction, decideAction, uploadSignatureAction, listUsersAction, listRolesAction, entityStatusesAction, onFlowSelected, onDecisionMade, onEntityClick, onBack, showPending = true, showEntityStatuses = false, className, themeMode = 'dark', labels }) {
|
|
300
|
+
export function VerificationFlowPage({ entityName, title, subtitle, flowListAction, flowGetAction, flowSaveAction, flowPublishAction, flowUnpublishAction, flowDeleteAction, pendingAction, decideAction, uploadSignatureAction, listUsersAction, listRolesAction, entityStatusesAction, onFlowSelected, onDecisionMade, onEntityClick, onBack, showPending = true, showEntityStatuses = false, className, themeMode = 'dark', labels, describeEntity, entityHref, availableChannels, initialTab }) {
|
|
284
301
|
const theme = getVerificationFlowTheme(themeMode);
|
|
285
302
|
setActiveTheme(theme);
|
|
286
303
|
const say = {
|
|
@@ -626,6 +643,15 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
|
|
|
626
643
|
useEffect(()=>{
|
|
627
644
|
loadFlows();
|
|
628
645
|
}, []);
|
|
646
|
+
/*
|
|
647
|
+
* Open where the host said, once, on mount. The store is module-level and
|
|
648
|
+
* survives navigation, so this is also what stops a reviewer who came from
|
|
649
|
+
* a notification landing on whatever tab they happened to leave open.
|
|
650
|
+
*/ useEffect(()=>{
|
|
651
|
+
if (initialTab) store.setActiveTab(initialTab);
|
|
652
|
+
}, [
|
|
653
|
+
initialTab
|
|
654
|
+
]);
|
|
629
655
|
useEffect(()=>{
|
|
630
656
|
if (store.selectedFlowId) {
|
|
631
657
|
loadFlowDetail(store.selectedFlowId);
|
|
@@ -887,7 +913,7 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
|
|
|
887
913
|
children: viewMode === 'designer' && store.activeTab === 'flow' && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
888
914
|
children: [
|
|
889
915
|
/*#__PURE__*/ _jsx("span", {
|
|
890
|
-
className:
|
|
916
|
+
className: cn('text-[11px] font-medium', themeMode === 'light' ? 'text-gray-600' : 'text-white/50'),
|
|
891
917
|
children: flowName || say.untitledFlow
|
|
892
918
|
}),
|
|
893
919
|
selectedFlow?.is_draft !== undefined && /*#__PURE__*/ _jsx("span", {
|
|
@@ -1127,6 +1153,19 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
|
|
|
1127
1153
|
isDraggingOver && /*#__PURE__*/ _jsx("div", {
|
|
1128
1154
|
className: theme.canvas.dropIndicator
|
|
1129
1155
|
}),
|
|
1156
|
+
!store.isLoadingGraph && nodes.length === 0 && /*#__PURE__*/ _jsxs("div", {
|
|
1157
|
+
className: theme.canvas.emptyHint,
|
|
1158
|
+
children: [
|
|
1159
|
+
/*#__PURE__*/ _jsx("p", {
|
|
1160
|
+
className: theme.canvas.emptyHintTitle,
|
|
1161
|
+
children: say.canvasEmptyTitle
|
|
1162
|
+
}),
|
|
1163
|
+
/*#__PURE__*/ _jsx("p", {
|
|
1164
|
+
className: theme.canvas.emptyHintBody,
|
|
1165
|
+
children: say.canvasEmptyBody
|
|
1166
|
+
})
|
|
1167
|
+
]
|
|
1168
|
+
}),
|
|
1130
1169
|
/*#__PURE__*/ _jsxs(ReactFlow, {
|
|
1131
1170
|
nodes: nodes,
|
|
1132
1171
|
edges: edges,
|
|
@@ -1259,7 +1298,8 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
|
|
|
1259
1298
|
listUsersAction: listUsersAction,
|
|
1260
1299
|
listRolesAction: listRolesAction,
|
|
1261
1300
|
panelTheme: theme.propertiesPanel,
|
|
1262
|
-
labels: say
|
|
1301
|
+
labels: say,
|
|
1302
|
+
availableChannels: availableChannels
|
|
1263
1303
|
})
|
|
1264
1304
|
})
|
|
1265
1305
|
]
|
|
@@ -1271,7 +1311,9 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
|
|
|
1271
1311
|
decideAction: decideAction,
|
|
1272
1312
|
uploadSignatureAction: uploadSignatureAction,
|
|
1273
1313
|
onDecisionMade: onDecisionMade,
|
|
1274
|
-
labels: say
|
|
1314
|
+
labels: say,
|
|
1315
|
+
describeEntity: describeEntity,
|
|
1316
|
+
entityHref: entityHref
|
|
1275
1317
|
})
|
|
1276
1318
|
}),
|
|
1277
1319
|
store.activeTab === 'statuses' && showEntityStatuses && entityStatusesAction && /*#__PURE__*/ _jsx("div", {
|
|
@@ -122,6 +122,22 @@ export type VerificationFlowLabels = {
|
|
|
122
122
|
stepProperties: string;
|
|
123
123
|
verifierProperties: string;
|
|
124
124
|
notificationProperties: string;
|
|
125
|
+
/**
|
|
126
|
+
* What each kind of node IS, said where the operator is standing.
|
|
127
|
+
*
|
|
128
|
+
* The owner of this product asked, about his own screen, "what does the
|
|
129
|
+
* step node even do?" — and nothing on it could answer him. One paragraph
|
|
130
|
+
* at the top of each properties panel, plus the derived order a step
|
|
131
|
+
* cannot be given by hand.
|
|
132
|
+
*/
|
|
133
|
+
stepHelp: string;
|
|
134
|
+
verifierHelp: string;
|
|
135
|
+
notificationHelp: string;
|
|
136
|
+
stepOrderLabel: string;
|
|
137
|
+
stepOrderUnset: string;
|
|
138
|
+
/** The blank canvas, which said nothing at all. */
|
|
139
|
+
canvasEmptyTitle: string;
|
|
140
|
+
canvasEmptyBody: string;
|
|
125
141
|
sectionGeneral: string;
|
|
126
142
|
sectionAssignment: string;
|
|
127
143
|
sectionOptions: string;
|
|
@@ -161,6 +177,21 @@ export type VerificationFlowLabels = {
|
|
|
161
177
|
approve: string;
|
|
162
178
|
reject: string;
|
|
163
179
|
review: string;
|
|
180
|
+
/**
|
|
181
|
+
* The queue could not be READ. Until this existed the tab rendered the
|
|
182
|
+
* empty state for a failed request, so "you have nothing to approve" and
|
|
183
|
+
* "we could not ask" looked identical — and the second one is the state a
|
|
184
|
+
* reviewer must act on.
|
|
185
|
+
*/
|
|
186
|
+
pendingLoadFailedTitle: string;
|
|
187
|
+
pendingLoadFailedBody: string;
|
|
188
|
+
retry: string;
|
|
189
|
+
/** Heading above the record a decision is about. */
|
|
190
|
+
pendingRecordLabel: string;
|
|
191
|
+
/** Opens the record itself, so a reviewer can read what they are signing. */
|
|
192
|
+
openRecord: string;
|
|
193
|
+
/** Shown beside a channel this deployment cannot deliver on. */
|
|
194
|
+
channelUnavailable: string;
|
|
164
195
|
verificationStatusTitle: string;
|
|
165
196
|
/** "12 records for orders" — count and entity, both inside the label. */
|
|
166
197
|
recordCount: (count: number, entityName: string) => string;
|