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 CHANGED
@@ -1 +1 @@
1
- 0.10.116
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
- const [signatureNote, setSignatureNote] = useState(null);
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
- setSignatureNote(res.message || say.decisionRefused);
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
- setSignatureNote(null);
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
- setSignatureNote(msg || say.decisionRefused);
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
- setSignatureNote(null);
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
- setSignatureNote(plan.reason === 'no-upload-action' ? say.signatureUnavailable : say.signatureMissing);
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
- setSignatureNote(say.signatureUploading);
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
- setSignatureNote(say.signatureUploadFailed);
155
+ setDecisionNote(say.signatureUploadFailed);
138
156
  return;
139
157
  }
140
158
  sendDecision(item, decision, id);
141
159
  },
142
- onErrorHandle: ()=>setSignatureNote(say.signatureUploadFailed)
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__*/ _jsx("div", {
236
+ store.pendingItems.map((item)=>/*#__PURE__*/ _jsxs("div", {
189
237
  className: theme.pending.card.container,
190
- children: /*#__PURE__*/ _jsxs("div", {
191
- className: "flex items-center justify-between",
192
- children: [
193
- /*#__PURE__*/ _jsxs("div", {
194
- children: [
195
- /*#__PURE__*/ _jsx("p", {
196
- className: theme.pending.card.title,
197
- children: say.pendingCardTitle(item.flow_name, item.step_order)
198
- }),
199
- /*#__PURE__*/ _jsxs("p", {
200
- className: theme.pending.card.subtitle,
201
- children: [
202
- item.entity_name,
203
- "/",
204
- item.entity_id
205
- ]
206
- })
207
- ]
208
- }),
209
- store.decidingId === item.instance_id ? /*#__PURE__*/ _jsxs("div", {
210
- className: theme.pending.card.actions,
211
- children: [
212
- item.require_signature ? /*#__PURE__*/ _jsxs("label", {
213
- className: "flex flex-col gap-1 text-xs",
214
- children: [
215
- /*#__PURE__*/ _jsx("span", {
216
- children: say.signaturePrompt
217
- }),
218
- /*#__PURE__*/ _jsx("input", {
219
- type: "file",
220
- onChange: (e)=>{
221
- setSignatureFile(e.target.files?.[0] ?? null);
222
- setSignatureNote(null);
223
- },
224
- className: theme.pending.input
225
- }),
226
- signatureNote ? /*#__PURE__*/ _jsx("span", {
227
- className: "text-red-600",
228
- children: signatureNote
229
- }) : null
230
- ]
231
- }) : null,
232
- /*#__PURE__*/ _jsx("input", {
233
- type: "text",
234
- placeholder: say.reasonPlaceholder,
235
- value: store.decisionReason,
236
- onChange: (e)=>store.setDecisionReason(e.target.value),
237
- className: theme.pending.input
238
- }),
239
- /*#__PURE__*/ _jsx("button", {
240
- type: "button",
241
- onClick: ()=>handleDecide(item, 'approved'),
242
- className: theme.pending.approveButton,
243
- children: say.approve
244
- }),
245
- /*#__PURE__*/ _jsx("button", {
246
- type: "button",
247
- onClick: ()=>handleDecide(item, 'rejected'),
248
- className: theme.pending.rejectButton,
249
- children: say.reject
250
- }),
251
- /*#__PURE__*/ _jsx("button", {
252
- type: "button",
253
- onClick: ()=>{
254
- store.setDecidingId(null);
255
- store.setDecisionReason('');
256
- setSignatureFile(null);
257
- setSignatureNote(null);
258
- },
259
- className: theme.pending.cancelButton,
260
- children: say.cancel
261
- })
262
- ]
263
- }) : /*#__PURE__*/ _jsx("button", {
264
- type: "button",
265
- onClick: ()=>store.setDecidingId(item.instance_id),
266
- className: theme.pending.reviewButton,
267
- children: say.review
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)=>/*#__PURE__*/ _jsxs("button", {
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
- className: selected.includes(ch) ? theme.optionSelected : theme.option,
97
- onClick: ()=>toggle(ch),
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;