nucleus-core-ts 0.10.116 → 0.10.118
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/VerificationEntityList.d.ts +8 -2
- package/dist/fe/components/VerificationFlowPage/components/VerificationEntityList.js +100 -2
- package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.d.ts +1 -1
- package/dist/fe/components/VerificationFlowPage/components/VerificationFlowPage.js +17 -4
- package/dist/fe/components/VerificationFlowPage/labels.d.ts +20 -0
- package/dist/fe/components/VerificationFlowPage/labels.js +10 -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 +59 -0
- package/dist/fe/components/VerificationPanel/components/PendingVerifications.js +16 -1
- package/dist/index.js +5 -5
- package/dist/server.js +5 -5
- package/dist/src/Nucleus/routes/entity/activeVerification.d.ts +27 -0
- package/dist/src/Server/internalHeaders.d.ts +1 -1
- 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.118
|
|
@@ -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,5 +1,5 @@
|
|
|
1
1
|
import type { VerificationFlowLabels } from '../labels';
|
|
2
|
-
import type { VerificationBulkStatusItem, VerificationEntityStatusesAction } from '../types';
|
|
2
|
+
import type { VerificationBulkStatusItem, VerificationCancelAction, VerificationEntityStatusesAction } from '../types';
|
|
3
3
|
export type VerificationEntityListTheme = {
|
|
4
4
|
container: string;
|
|
5
5
|
header: string;
|
|
@@ -46,5 +46,11 @@ export type VerificationEntityListProps = {
|
|
|
46
46
|
pageSize?: number;
|
|
47
47
|
/** Spread over the English defaults; omit it and the list stays English. */
|
|
48
48
|
labels?: Partial<VerificationFlowLabels>;
|
|
49
|
+
/**
|
|
50
|
+
* Ends a chain that cannot finish. This list is the only screen that knows
|
|
51
|
+
* which chains are still `active`, so it is where the exit belongs — until
|
|
52
|
+
* now the API had one and the product offered none.
|
|
53
|
+
*/
|
|
54
|
+
cancelAction?: VerificationCancelAction;
|
|
49
55
|
};
|
|
50
|
-
export declare function VerificationEntityList({ entityName, entityStatusesAction, theme: themeOverrides, themeMode, onEntityClick, pageSize, labels, }: VerificationEntityListProps): import("react/jsx-runtime").JSX.Element;
|
|
56
|
+
export declare function VerificationEntityList({ entityName, entityStatusesAction, theme: themeOverrides, themeMode, onEntityClick, pageSize, labels, cancelAction, }: VerificationEntityListProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -97,7 +97,7 @@ const lightTheme = {
|
|
|
97
97
|
};
|
|
98
98
|
return map[status] ?? status;
|
|
99
99
|
}
|
|
100
|
-
export function VerificationEntityList({ entityName, entityStatusesAction, theme: themeOverrides, themeMode = 'dark', onEntityClick, pageSize = 20, labels }) {
|
|
100
|
+
export function VerificationEntityList({ entityName, entityStatusesAction, theme: themeOverrides, themeMode = 'dark', onEntityClick, pageSize = 20, labels, cancelAction }) {
|
|
101
101
|
const baseTheme = themeMode === 'light' ? lightTheme : darkTheme;
|
|
102
102
|
const t = {
|
|
103
103
|
...baseTheme,
|
|
@@ -109,6 +109,10 @@ export function VerificationEntityList({ entityName, entityStatusesAction, theme
|
|
|
109
109
|
};
|
|
110
110
|
const [activeTab, setActiveTab] = useState('all');
|
|
111
111
|
const [currentPage, setCurrentPage] = useState(1);
|
|
112
|
+
// Which row is asking for confirmation, and what it is being told.
|
|
113
|
+
const [cancellingId, setCancellingId] = useState(null);
|
|
114
|
+
const [cancelReason, setCancelReason] = useState('');
|
|
115
|
+
const [cancelNote, setCancelNote] = useState(null);
|
|
112
116
|
const loadStatuses = useEffectEvent((status, page)=>{
|
|
113
117
|
entityStatusesAction.start({
|
|
114
118
|
payload: {
|
|
@@ -128,6 +132,39 @@ export function VerificationEntityList({ entityName, entityStatusesAction, theme
|
|
|
128
132
|
setCurrentPage(page);
|
|
129
133
|
loadStatuses(activeTab === 'all' ? undefined : activeTab, page);
|
|
130
134
|
});
|
|
135
|
+
const sendCancel = useEffectEvent((item)=>{
|
|
136
|
+
if (!cancelAction) return;
|
|
137
|
+
setCancelNote(null);
|
|
138
|
+
cancelAction.start({
|
|
139
|
+
payload: {
|
|
140
|
+
entity_name: item.entity_name,
|
|
141
|
+
entity_id: item.entity_id,
|
|
142
|
+
...cancelReason ? {
|
|
143
|
+
reason: cancelReason
|
|
144
|
+
} : {}
|
|
145
|
+
},
|
|
146
|
+
onAfterHandle: (res)=>{
|
|
147
|
+
/*
|
|
148
|
+
* A refusal — "only the person who started this verification, or a
|
|
149
|
+
* godmin, can cancel it" — arrives as a body, not always as a status,
|
|
150
|
+
* so it is read from the body. The row is NOT closed on a refusal:
|
|
151
|
+
* closing it here is the bug that made every other decision screen
|
|
152
|
+
* look like it had worked.
|
|
153
|
+
*/ const response = res;
|
|
154
|
+
if (response?.success === false) {
|
|
155
|
+
setCancelNote(response.message || say.cancelChainFailed);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
setCancellingId(null);
|
|
159
|
+
setCancelReason('');
|
|
160
|
+
loadStatuses(activeTab === 'all' ? undefined : activeTab, currentPage);
|
|
161
|
+
},
|
|
162
|
+
onErrorHandle: (err)=>{
|
|
163
|
+
const msg = err?.message;
|
|
164
|
+
setCancelNote(msg || say.cancelChainFailed);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
131
168
|
useEffect(()=>{
|
|
132
169
|
loadStatuses(undefined, 1);
|
|
133
170
|
}, []);
|
|
@@ -294,7 +331,68 @@ export function VerificationEntityList({ entityName, entityStatusesAction, theme
|
|
|
294
331
|
/*#__PURE__*/ _jsx("span", {
|
|
295
332
|
className: `${t.statusBadge} ${getStatusBadgeClass(item.status)}`,
|
|
296
333
|
children: statusLabel(say, item.status)
|
|
297
|
-
})
|
|
334
|
+
}),
|
|
335
|
+
cancelAction && item.status === 'active' ? cancellingId === item.instance_id ? // No handlers on this wrapper: the row above is a button
|
|
336
|
+
// when `onEntityClick` is set, so each INTERACTIVE child
|
|
337
|
+
// stops the bubble itself rather than a bare div doing it
|
|
338
|
+
// for them.
|
|
339
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
340
|
+
className: "flex flex-col items-stretch gap-2 shrink-0",
|
|
341
|
+
children: [
|
|
342
|
+
/*#__PURE__*/ _jsx("p", {
|
|
343
|
+
className: t.metaLabel,
|
|
344
|
+
children: say.cancelChainConfirm
|
|
345
|
+
}),
|
|
346
|
+
/*#__PURE__*/ _jsx("input", {
|
|
347
|
+
type: "text",
|
|
348
|
+
value: cancelReason,
|
|
349
|
+
placeholder: say.cancelChainReasonPlaceholder,
|
|
350
|
+
onClick: (e)=>e.stopPropagation(),
|
|
351
|
+
onKeyDown: (e)=>e.stopPropagation(),
|
|
352
|
+
onChange: (e)=>setCancelReason(e.target.value),
|
|
353
|
+
className: themeMode === 'light' ? 'rounded-lg border border-gray-200 bg-gray-50 px-2 py-1 text-[11px] text-gray-800 outline-none focus:border-gray-400' : 'rounded-lg border border-white/[0.08] bg-white/[0.04] px-2 py-1 text-[11px] text-white/80 outline-none focus:border-white/20'
|
|
354
|
+
}),
|
|
355
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
356
|
+
className: "flex items-center gap-2",
|
|
357
|
+
children: [
|
|
358
|
+
/*#__PURE__*/ _jsx("button", {
|
|
359
|
+
type: "button",
|
|
360
|
+
onClick: (e)=>{
|
|
361
|
+
e.stopPropagation();
|
|
362
|
+
sendCancel(item);
|
|
363
|
+
},
|
|
364
|
+
className: themeMode === 'light' ? 'rounded-lg border border-red-200 bg-red-50 px-2.5 py-1 text-[11px] font-medium text-red-600 hover:bg-red-100' : 'rounded-lg border border-red-500/30 bg-red-500/20 px-2.5 py-1 text-[11px] font-medium text-red-400 hover:bg-red-500/30',
|
|
365
|
+
children: say.cancelChain
|
|
366
|
+
}),
|
|
367
|
+
/*#__PURE__*/ _jsx("button", {
|
|
368
|
+
type: "button",
|
|
369
|
+
onClick: (e)=>{
|
|
370
|
+
e.stopPropagation();
|
|
371
|
+
setCancellingId(null);
|
|
372
|
+
setCancelReason('');
|
|
373
|
+
setCancelNote(null);
|
|
374
|
+
},
|
|
375
|
+
className: themeMode === 'light' ? 'rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-1 text-[11px] font-medium text-gray-500 hover:bg-gray-100' : 'rounded-lg border border-white/[0.08] bg-white/[0.06] px-2.5 py-1 text-[11px] font-medium text-white/50 hover:bg-white/[0.1]',
|
|
376
|
+
children: say.cancel
|
|
377
|
+
})
|
|
378
|
+
]
|
|
379
|
+
}),
|
|
380
|
+
cancelNote ? /*#__PURE__*/ _jsx("p", {
|
|
381
|
+
role: "alert",
|
|
382
|
+
className: themeMode === 'light' ? 'text-[11px] leading-4 text-amber-900' : 'text-[11px] leading-4 text-amber-200',
|
|
383
|
+
children: cancelNote
|
|
384
|
+
}) : null
|
|
385
|
+
]
|
|
386
|
+
}) : /*#__PURE__*/ _jsx("button", {
|
|
387
|
+
type: "button",
|
|
388
|
+
onClick: (e)=>{
|
|
389
|
+
e.stopPropagation();
|
|
390
|
+
setCancellingId(item.instance_id);
|
|
391
|
+
setCancelNote(null);
|
|
392
|
+
},
|
|
393
|
+
className: themeMode === 'light' ? 'shrink-0 rounded-lg border border-gray-200 bg-white px-2.5 py-1 text-[11px] font-medium text-gray-600 hover:bg-gray-100' : 'shrink-0 rounded-lg border border-white/[0.08] bg-white/[0.04] px-2.5 py-1 text-[11px] font-medium text-white/60 hover:bg-white/[0.1]',
|
|
394
|
+
children: say.cancelChain
|
|
395
|
+
}) : null
|
|
298
396
|
]
|
|
299
397
|
}, item.instance_id);
|
|
300
398
|
})
|
|
@@ -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, cancelAction, }: VerificationFlowPageProps): ReactElement;
|