nucleus-core-ts 0.10.116 → 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 +22 -7
- package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.d.ts +1 -1
- package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.js +15 -3
- package/dist/fe/components/VerificationFlowPage/labels.d.ts +15 -0
- package/dist/fe/components/VerificationFlowPage/labels.js +6 -0
- package/dist/fe/components/VerificationFlowPage/theme/index.d.ts +18 -0
- package/dist/fe/components/VerificationFlowPage/theme/index.js +26 -4
- 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 +3 -3
- package/dist/server.js +3 -3
- package/dist/src/Nucleus/routes/entity/activeVerification.d.ts +27 -0
- package/dist/src/Services/Verification/index.d.ts +17 -0
- package/package.json +1 -1
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,
|
|
@@ -698,6 +712,7 @@ export function PropertiesPanel({ nodeId, nodeData, onUpdateNode, onDeleteNode,
|
|
|
698
712
|
onChange: (channels)=>update({
|
|
699
713
|
channels
|
|
700
714
|
}),
|
|
715
|
+
available: availableChannels,
|
|
701
716
|
msTheme: theme.multiSelect,
|
|
702
717
|
say: say
|
|
703
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;
|
|
@@ -297,7 +297,7 @@ function buildSavePayload(flowId, entityName, flowName, flowDescription, trigger
|
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
// ─── Main Component ───────────────────────────────────────────────
|
|
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 }) {
|
|
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 }) {
|
|
301
301
|
const theme = getVerificationFlowTheme(themeMode);
|
|
302
302
|
setActiveTheme(theme);
|
|
303
303
|
const say = {
|
|
@@ -643,6 +643,15 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
|
|
|
643
643
|
useEffect(()=>{
|
|
644
644
|
loadFlows();
|
|
645
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
|
+
]);
|
|
646
655
|
useEffect(()=>{
|
|
647
656
|
if (store.selectedFlowId) {
|
|
648
657
|
loadFlowDetail(store.selectedFlowId);
|
|
@@ -1289,7 +1298,8 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
|
|
|
1289
1298
|
listUsersAction: listUsersAction,
|
|
1290
1299
|
listRolesAction: listRolesAction,
|
|
1291
1300
|
panelTheme: theme.propertiesPanel,
|
|
1292
|
-
labels: say
|
|
1301
|
+
labels: say,
|
|
1302
|
+
availableChannels: availableChannels
|
|
1293
1303
|
})
|
|
1294
1304
|
})
|
|
1295
1305
|
]
|
|
@@ -1301,7 +1311,9 @@ export function VerificationFlowPage({ entityName, title, subtitle, flowListActi
|
|
|
1301
1311
|
decideAction: decideAction,
|
|
1302
1312
|
uploadSignatureAction: uploadSignatureAction,
|
|
1303
1313
|
onDecisionMade: onDecisionMade,
|
|
1304
|
-
labels: say
|
|
1314
|
+
labels: say,
|
|
1315
|
+
describeEntity: describeEntity,
|
|
1316
|
+
entityHref: entityHref
|
|
1305
1317
|
})
|
|
1306
1318
|
}),
|
|
1307
1319
|
store.activeTab === 'statuses' && showEntityStatuses && entityStatusesAction && /*#__PURE__*/ _jsx("div", {
|
|
@@ -177,6 +177,21 @@ export type VerificationFlowLabels = {
|
|
|
177
177
|
approve: string;
|
|
178
178
|
reject: string;
|
|
179
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;
|
|
180
195
|
verificationStatusTitle: string;
|
|
181
196
|
/** "12 records for orders" — count and entity, both inside the label. */
|
|
182
197
|
recordCount: (count: number, entityName: string) => string;
|
|
@@ -146,6 +146,12 @@
|
|
|
146
146
|
approve: 'Approve',
|
|
147
147
|
reject: 'Reject',
|
|
148
148
|
review: 'Review',
|
|
149
|
+
pendingLoadFailedTitle: 'The approval queue could not be loaded',
|
|
150
|
+
pendingLoadFailedBody: 'This is not the same as having nothing to approve. Try again, and tell an administrator if it keeps failing.',
|
|
151
|
+
retry: 'Try again',
|
|
152
|
+
pendingRecordLabel: 'Record',
|
|
153
|
+
openRecord: 'Open the record',
|
|
154
|
+
channelUnavailable: 'not available on this installation',
|
|
149
155
|
verificationStatusTitle: 'Verification Status',
|
|
150
156
|
recordCount: (count, entityName)=>`${count} record${count !== 1 ? 's' : ''} for ${entityName}`,
|
|
151
157
|
noRecordsIconTitle: 'No records',
|
|
@@ -160,12 +160,30 @@ export type VerificationFlowPageTheme = {
|
|
|
160
160
|
title: string;
|
|
161
161
|
subtitle: string;
|
|
162
162
|
actions: string;
|
|
163
|
+
/** The record a decision is about, set as a fact and not as a caption. */
|
|
164
|
+
recordRow: string;
|
|
165
|
+
recordLabel: string;
|
|
166
|
+
recordValue: string;
|
|
167
|
+
recordLink: string;
|
|
163
168
|
};
|
|
164
169
|
input: string;
|
|
165
170
|
approveButton: string;
|
|
166
171
|
rejectButton: string;
|
|
167
172
|
cancelButton: string;
|
|
168
173
|
reviewButton: string;
|
|
174
|
+
/**
|
|
175
|
+
* The engine's own refusal sentence. It used to render only inside the
|
|
176
|
+
* signature branch, so every refusal on a step that needs no signature
|
|
177
|
+
* was shown to nobody.
|
|
178
|
+
*/
|
|
179
|
+
notice: string;
|
|
180
|
+
/** A queue that could not be READ, which is not an empty queue. */
|
|
181
|
+
failure: {
|
|
182
|
+
container: string;
|
|
183
|
+
title: string;
|
|
184
|
+
text: string;
|
|
185
|
+
button: string;
|
|
186
|
+
};
|
|
169
187
|
};
|
|
170
188
|
flowList: {
|
|
171
189
|
container: string;
|
|
@@ -156,13 +156,24 @@ export const verificationFlowPageTheme = {
|
|
|
156
156
|
container: 'rounded-xl border border-white/[0.06] bg-white/[0.02] p-4 shadow-sm hover:bg-white/[0.04] transition-colors',
|
|
157
157
|
title: 'text-sm font-medium text-white/90',
|
|
158
158
|
subtitle: 'text-xs text-white/40 mt-0.5',
|
|
159
|
-
actions: 'flex items-center gap-2'
|
|
159
|
+
actions: 'flex flex-wrap items-center gap-2',
|
|
160
|
+
recordRow: 'mt-3 flex flex-wrap items-baseline gap-x-2 gap-y-1',
|
|
161
|
+
recordLabel: 'text-[10px] uppercase tracking-wide text-white/30',
|
|
162
|
+
recordValue: 'text-xs text-white/70',
|
|
163
|
+
recordLink: 'text-xs font-medium text-blue-400 underline underline-offset-2'
|
|
160
164
|
},
|
|
161
165
|
input: 'rounded-lg border border-white/[0.08] bg-white/[0.04] px-3 py-1.5 text-[11px] text-white/80 w-40 outline-none focus:border-white/20',
|
|
162
166
|
approveButton: 'rounded-lg bg-emerald-500/20 border border-emerald-500/30 px-3 py-1.5 text-[11px] font-medium text-emerald-400 hover:bg-emerald-500/30 transition-all',
|
|
163
167
|
rejectButton: 'rounded-lg bg-red-500/20 border border-red-500/30 px-3 py-1.5 text-[11px] font-medium text-red-400 hover:bg-red-500/30 transition-all',
|
|
164
168
|
cancelButton: 'rounded-lg bg-white/[0.06] border border-white/[0.08] px-3 py-1.5 text-[11px] font-medium text-white/50 hover:bg-white/[0.1] transition-all',
|
|
165
|
-
reviewButton: 'rounded-lg bg-blue-500/20 border border-blue-500/30 px-3 py-1.5 text-[11px] font-medium text-blue-400 hover:bg-blue-500/30 transition-all'
|
|
169
|
+
reviewButton: 'rounded-lg bg-blue-500/20 border border-blue-500/30 px-3 py-1.5 text-[11px] font-medium text-blue-400 hover:bg-blue-500/30 transition-all',
|
|
170
|
+
notice: 'mt-3 rounded-lg border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[12px] leading-5 text-amber-200',
|
|
171
|
+
failure: {
|
|
172
|
+
container: 'max-w-xl mx-auto mt-16 rounded-xl border border-red-500/30 bg-red-500/[0.07] p-6 text-center',
|
|
173
|
+
title: 'text-sm font-semibold text-red-300',
|
|
174
|
+
text: 'mt-2 text-[12px] leading-5 text-white/50',
|
|
175
|
+
button: 'mt-4 rounded-lg bg-white/[0.08] border border-white/[0.12] px-4 py-2 text-[12px] font-medium text-white/80 hover:bg-white/[0.14] transition-all'
|
|
176
|
+
}
|
|
166
177
|
},
|
|
167
178
|
flowList: {
|
|
168
179
|
container: 'flex-1 overflow-auto p-6',
|
|
@@ -364,13 +375,24 @@ export const verificationFlowPageLightTheme = {
|
|
|
364
375
|
container: 'rounded-xl border border-gray-200 bg-white p-4 shadow-sm hover:bg-gray-50 transition-colors',
|
|
365
376
|
title: 'text-sm font-medium text-gray-800',
|
|
366
377
|
subtitle: 'text-xs text-gray-400 mt-0.5',
|
|
367
|
-
actions: 'flex items-center gap-2'
|
|
378
|
+
actions: 'flex flex-wrap items-center gap-2',
|
|
379
|
+
recordRow: 'mt-3 flex flex-wrap items-baseline gap-x-2 gap-y-1',
|
|
380
|
+
recordLabel: 'text-[10px] uppercase tracking-wide text-gray-400',
|
|
381
|
+
recordValue: 'text-xs text-gray-700',
|
|
382
|
+
recordLink: 'text-xs font-medium text-blue-600 underline underline-offset-2'
|
|
368
383
|
},
|
|
369
384
|
input: 'rounded-lg border border-gray-200 bg-gray-50 px-3 py-1.5 text-[11px] text-gray-800 w-40 outline-none focus:border-gray-400',
|
|
370
385
|
approveButton: 'rounded-lg bg-emerald-50 border border-emerald-200 px-3 py-1.5 text-[11px] font-medium text-emerald-700 hover:bg-emerald-100 transition-all',
|
|
371
386
|
rejectButton: 'rounded-lg bg-red-50 border border-red-200 px-3 py-1.5 text-[11px] font-medium text-red-600 hover:bg-red-100 transition-all',
|
|
372
387
|
cancelButton: 'rounded-lg bg-gray-50 border border-gray-200 px-3 py-1.5 text-[11px] font-medium text-gray-500 hover:bg-gray-100 transition-all',
|
|
373
|
-
reviewButton: 'rounded-lg bg-blue-50 border border-blue-200 px-3 py-1.5 text-[11px] font-medium text-blue-600 hover:bg-blue-100 transition-all'
|
|
388
|
+
reviewButton: 'rounded-lg bg-blue-50 border border-blue-200 px-3 py-1.5 text-[11px] font-medium text-blue-600 hover:bg-blue-100 transition-all',
|
|
389
|
+
notice: 'mt-3 rounded-lg border border-amber-300 bg-amber-50 px-3 py-2 text-[12px] leading-5 text-amber-900',
|
|
390
|
+
failure: {
|
|
391
|
+
container: 'max-w-xl mx-auto mt-16 rounded-xl border border-red-200 bg-red-50 p-6 text-center',
|
|
392
|
+
title: 'text-sm font-semibold text-red-700',
|
|
393
|
+
text: 'mt-2 text-[12px] leading-5 text-gray-600',
|
|
394
|
+
button: 'mt-4 rounded-lg border border-gray-300 bg-white px-4 py-2 text-[12px] font-medium text-gray-700 hover:bg-gray-100 transition-all'
|
|
395
|
+
}
|
|
374
396
|
},
|
|
375
397
|
flowList: {
|
|
376
398
|
container: 'flex-1 overflow-auto p-6',
|