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 CHANGED
@@ -1 +1 @@
1
- 0.10.116
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
- 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,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',