@burdenoff/microfe-movethewheels 2026.1013.2 → 2026.1016.1
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/README.md +7 -0
- package/dist/components/EntityCombobox.js +80 -74
- package/dist/components/pending-pick/PendingPickHistory.js +47 -38
- package/dist/components/pending-pick/PickReviewPanel.js +317 -0
- package/dist/generated/pending-pick-operations.d.ts +356 -0
- package/dist/movethewheels/components/pending-pick/PickReviewPanel.d.ts +5 -0
- package/dist/movethewheels/utils/pickReviewAcknowledgement.d.ts +4 -0
- package/dist/src/generated/pending-pick-operations.js +75 -6
- package/dist/stories/EntityCombobox.stories.d.ts +7 -0
- package/dist/utils/pickReviewAcknowledgement.js +10 -0
- package/package.json +5 -3
- package/src/generated/pending-pick-operations.ts +239 -1
- package/src/movethewheels/components/EntityCombobox.tsx +8 -2
- package/src/movethewheels/components/pending-pick/PendingPickHistory.tsx +6 -0
- package/src/movethewheels/components/pending-pick/PickReviewPanel.tsx +326 -0
- package/src/movethewheels/utils/pickReviewAcknowledgement.ts +51 -0
- package/src/operations/pending-pick/pick-review.graphql +14 -0
- package/src/stories/EntityCombobox.stories.tsx +58 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
import { PermissionProvider, usePermissions } from '@burdenoff/fe-libs/shared/providers/shell';
|
|
3
|
+
import {
|
|
4
|
+
ApprovePickReviewDocument,
|
|
5
|
+
RejectPickReviewDocument,
|
|
6
|
+
RevokePickReviewDocument,
|
|
7
|
+
PickReviewHistoryDocument,
|
|
8
|
+
type PickReviewHistoryQuery,
|
|
9
|
+
type PendingPickTaskFragment,
|
|
10
|
+
type RecordMtwPickReviewInput,
|
|
11
|
+
type MtwPickReviewAction,
|
|
12
|
+
} from '../../../generated/pending-pick-operations';
|
|
13
|
+
import { confirmPickReviewAcknowledgement } from '../../utils/pickReviewAcknowledgement';
|
|
14
|
+
import { useMoveTheWheels } from '../../providers/MoveTheWheelsProvider';
|
|
15
|
+
import { useConditionApi, useConditionQuery } from '../../hooks/useContainerCondition';
|
|
16
|
+
import { ContainerEvidencePicker, type SelectedEvidence } from '../ContainerEvidencePicker';
|
|
17
|
+
import { ReuseField, ReusePager, ReuseState, reuseCard } from '../ReuseControls';
|
|
18
|
+
import { Button } from '../ui';
|
|
19
|
+
export function PickReviewPanel({ task }: { task: PendingPickTaskFragment }) {
|
|
20
|
+
const { workspaceId, currentUser, apiGatewayUrl } = useMoveTheWheels();
|
|
21
|
+
return (
|
|
22
|
+
<PermissionProvider scopeId={workspaceId ?? null} gateway="workspace">
|
|
23
|
+
<MoveReview
|
|
24
|
+
key={JSON.stringify([
|
|
25
|
+
workspaceId,
|
|
26
|
+
currentUser?.id,
|
|
27
|
+
apiGatewayUrl,
|
|
28
|
+
task.id,
|
|
29
|
+
task.intentHash,
|
|
30
|
+
])}
|
|
31
|
+
task={task}
|
|
32
|
+
/>
|
|
33
|
+
</PermissionProvider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
function MoveReview({ task }: { task: PendingPickTaskFragment }) {
|
|
37
|
+
const permissions = usePermissions(),
|
|
38
|
+
{ currentUser, workspaceId } = useMoveTheWheels(),
|
|
39
|
+
request = useConditionApi('SINGLE_ATTEMPT');
|
|
40
|
+
const storageKey = JSON.stringify([
|
|
41
|
+
'mtw-review-intent',
|
|
42
|
+
workspaceId,
|
|
43
|
+
currentUser?.id,
|
|
44
|
+
task.id,
|
|
45
|
+
task.intentHash,
|
|
46
|
+
]);
|
|
47
|
+
const [attemptKey, setAttemptKey] = useState<string | null>(() => {
|
|
48
|
+
try {
|
|
49
|
+
const value = sessionStorage.getItem(storageKey);
|
|
50
|
+
return value === null ||
|
|
51
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value)
|
|
52
|
+
? value
|
|
53
|
+
: 'UNAVAILABLE';
|
|
54
|
+
} catch {
|
|
55
|
+
return 'UNAVAILABLE';
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const uncertain = attemptKey !== null;
|
|
59
|
+
const [offset, setOffset] = useState(0),
|
|
60
|
+
[revision, setRevision] = useState(0),
|
|
61
|
+
[evidence, setEvidence] = useState<SelectedEvidence[]>([]),
|
|
62
|
+
[busy, setBusy] = useState(false),
|
|
63
|
+
[message, setMessage] = useState<string | null>(null);
|
|
64
|
+
const running = useRef(false);
|
|
65
|
+
const history = useConditionQuery<PickReviewHistoryQuery>(
|
|
66
|
+
PickReviewHistoryDocument,
|
|
67
|
+
permissions.hasPermission('mtw-move-review:list')
|
|
68
|
+
? { taskId: task.id, limit: 10, offset }
|
|
69
|
+
: null,
|
|
70
|
+
revision
|
|
71
|
+
);
|
|
72
|
+
const page = history.data?.mtwPickReviewPage,
|
|
73
|
+
head = page?.latestDecision;
|
|
74
|
+
const independent =
|
|
75
|
+
currentUser?.id && currentUser.id !== task.createdBy && currentUser.id !== task.executorActorId;
|
|
76
|
+
const decision = async (form: HTMLFormElement, kind: 'APPROVE' | 'REJECT' | 'REVOKE') => {
|
|
77
|
+
if (
|
|
78
|
+
running.current ||
|
|
79
|
+
uncertain ||
|
|
80
|
+
!page ||
|
|
81
|
+
!independent ||
|
|
82
|
+
!permissions.hasPermission(`mtw-move-review:${kind.toLowerCase()}`)
|
|
83
|
+
)
|
|
84
|
+
return;
|
|
85
|
+
const data = new FormData(form),
|
|
86
|
+
field = (name: string) => String(data.get(name) ?? '').trim();
|
|
87
|
+
let input: RecordMtwPickReviewInput;
|
|
88
|
+
try {
|
|
89
|
+
if (
|
|
90
|
+
kind === 'APPROVE' &&
|
|
91
|
+
(!/(Z|[+-]\d{2}:\d{2})$/.test(field('validFrom')) ||
|
|
92
|
+
!/(Z|[+-]\d{2}:\d{2})$/.test(field('validUntil')))
|
|
93
|
+
)
|
|
94
|
+
throw new Error('Use explicit ISO timezone offsets for both validity bounds.');
|
|
95
|
+
const validFrom = kind === 'APPROVE' ? new Date(field('validFrom')).toISOString() : null;
|
|
96
|
+
const validUntil = kind === 'APPROVE' ? new Date(field('validUntil')).toISOString() : null;
|
|
97
|
+
const quantity = kind === 'APPROVE' ? Number(field('quantity')) : null;
|
|
98
|
+
const allowed = [
|
|
99
|
+
'RESERVE',
|
|
100
|
+
'STAGE',
|
|
101
|
+
'RELEASE_UNPICKED',
|
|
102
|
+
'RELEASE_STAGED_CUSTODY',
|
|
103
|
+
'SHORT_PICK',
|
|
104
|
+
] as const;
|
|
105
|
+
const actions: MtwPickReviewAction[] =
|
|
106
|
+
kind === 'APPROVE' ? allowed.filter((action) => data.has(action)) : [];
|
|
107
|
+
if (
|
|
108
|
+
!field('note') ||
|
|
109
|
+
(kind === 'APPROVE' &&
|
|
110
|
+
(!actions.length ||
|
|
111
|
+
!Number.isInteger(quantity) ||
|
|
112
|
+
quantity === null ||
|
|
113
|
+
quantity < 1 ||
|
|
114
|
+
quantity > task.requestedQuantity ||
|
|
115
|
+
!evidence.length ||
|
|
116
|
+
!field('handlingReference') ||
|
|
117
|
+
!field('handlingRevision') ||
|
|
118
|
+
!field('sopReference') ||
|
|
119
|
+
!field('sopRevision')))
|
|
120
|
+
)
|
|
121
|
+
throw new Error('Review all required fields and select exact evidence.');
|
|
122
|
+
input = {
|
|
123
|
+
taskId: task.id,
|
|
124
|
+
intentHash: task.intentHash,
|
|
125
|
+
subjectActorId: task.executorActorId,
|
|
126
|
+
expectedPreviousDecisionId: head?.id ?? null,
|
|
127
|
+
expectedVersion: head?.version ?? 0,
|
|
128
|
+
actions,
|
|
129
|
+
quantityCeiling: quantity,
|
|
130
|
+
validFrom,
|
|
131
|
+
validUntil,
|
|
132
|
+
handlingPolicy:
|
|
133
|
+
kind === 'APPROVE'
|
|
134
|
+
? { reference: field('handlingReference'), revision: field('handlingRevision') }
|
|
135
|
+
: null,
|
|
136
|
+
sop:
|
|
137
|
+
kind === 'APPROVE'
|
|
138
|
+
? { reference: field('sopReference'), revision: field('sopRevision') }
|
|
139
|
+
: null,
|
|
140
|
+
evidence:
|
|
141
|
+
kind === 'APPROVE'
|
|
142
|
+
? evidence.map(({ fileId, versionId }) => ({ fileId, versionId }))
|
|
143
|
+
: [],
|
|
144
|
+
note: field('note'),
|
|
145
|
+
idempotencyKey: crypto.randomUUID(),
|
|
146
|
+
};
|
|
147
|
+
} catch (failure) {
|
|
148
|
+
setMessage(failure instanceof Error ? failure.message : 'Check the review fields.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// Store the nonsecret request key before dispatch; refresh must never authorize resubmission.
|
|
152
|
+
try {
|
|
153
|
+
sessionStorage.setItem(storageKey, input.idempotencyKey);
|
|
154
|
+
setAttemptKey(input.idempotencyKey);
|
|
155
|
+
} catch {
|
|
156
|
+
setAttemptKey('UNAVAILABLE');
|
|
157
|
+
setMessage('The intended request key could not be preserved. No decision was submitted.');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
running.current = true;
|
|
161
|
+
setBusy(true);
|
|
162
|
+
setMessage(null);
|
|
163
|
+
try {
|
|
164
|
+
const document =
|
|
165
|
+
kind === 'APPROVE'
|
|
166
|
+
? ApprovePickReviewDocument
|
|
167
|
+
: kind === 'REJECT'
|
|
168
|
+
? RejectPickReviewDocument
|
|
169
|
+
: RevokePickReviewDocument;
|
|
170
|
+
const result = await request<unknown>(document, { input });
|
|
171
|
+
confirmPickReviewAcknowledgement(result, input, kind, currentUser?.id ?? '');
|
|
172
|
+
sessionStorage.removeItem(storageKey);
|
|
173
|
+
setAttemptKey(null);
|
|
174
|
+
setRevision((value) => value + 1);
|
|
175
|
+
setMessage('Review recorded. Stock is unchanged.');
|
|
176
|
+
} catch {
|
|
177
|
+
setMessage(
|
|
178
|
+
'The outcome is not confirmed. Preserve the intended request key and match it against review history; do not resend this decision.'
|
|
179
|
+
);
|
|
180
|
+
} finally {
|
|
181
|
+
running.current = false;
|
|
182
|
+
setBusy(false);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
return (
|
|
186
|
+
<section className={reuseCard} aria-label="Independent move review">
|
|
187
|
+
<h4 className="font-semibold">Independent move review</h4>
|
|
188
|
+
<p>
|
|
189
|
+
Review only. No stock reservation, movement, qualification or physical safety certification.
|
|
190
|
+
Current stock and permissions must be checked again before any future execution.
|
|
191
|
+
</p>
|
|
192
|
+
<p className="break-all">
|
|
193
|
+
Exact intent SHA-256: {task.intentHash}; fixed executor: {task.executorActorId}
|
|
194
|
+
</p>
|
|
195
|
+
{permissions.isLoading && <p role="status">Checking review permissions…</p>}
|
|
196
|
+
{permissions.error && <p role="alert">Review permissions are unavailable.</p>}
|
|
197
|
+
{!permissions.isLoading && !permissions.hasPermission('mtw-move-review:list') && (
|
|
198
|
+
<p>You do not have permission to list move reviews.</p>
|
|
199
|
+
)}
|
|
200
|
+
<ReuseState loading={history.loading} error={history.error} />
|
|
201
|
+
{page && (
|
|
202
|
+
<>
|
|
203
|
+
<p>
|
|
204
|
+
Latest review: {page.reviewState}, evaluated {page.evaluatedAt}. Older approvals never
|
|
205
|
+
override a later rejection or revocation.
|
|
206
|
+
</p>
|
|
207
|
+
{page.items.map((item) => (
|
|
208
|
+
<article key={item.id} className="min-w-0 break-words rounded border p-3">
|
|
209
|
+
<p>
|
|
210
|
+
Version {item.version}: {item.kind} · {item.note}
|
|
211
|
+
</p>
|
|
212
|
+
<p className="break-all">
|
|
213
|
+
Reviewer {item.reviewerActorId}; receipt {item.id}; request key{' '}
|
|
214
|
+
{item.idempotencyKey}; intent {item.intentHash}
|
|
215
|
+
</p>
|
|
216
|
+
<p>
|
|
217
|
+
{item.actions.join(', ') || 'No actions granted'}; ceiling{' '}
|
|
218
|
+
{item.quantityCeiling ?? 'none'}; validity {item.validFrom ?? 'none'} to{' '}
|
|
219
|
+
{item.validUntil ?? 'none'} (exclusive).
|
|
220
|
+
</p>
|
|
221
|
+
<p>
|
|
222
|
+
Handling source{' '}
|
|
223
|
+
{item.handlingPolicy
|
|
224
|
+
? `${item.handlingPolicy.reference} · revision ${item.handlingPolicy.revision}`
|
|
225
|
+
: 'none'}
|
|
226
|
+
; SOP {item.sop ? `${item.sop.reference} · revision ${item.sop.revision}` : 'none'}.
|
|
227
|
+
</p>
|
|
228
|
+
{item.evidence.map((file) => (
|
|
229
|
+
<p key={file.versionId} className="break-all">
|
|
230
|
+
Evidence {file.fileId}, version {file.versionId}, SHA-256 {file.sha256Hash};
|
|
231
|
+
checked {file.checkedAt}.
|
|
232
|
+
</p>
|
|
233
|
+
))}
|
|
234
|
+
</article>
|
|
235
|
+
))}
|
|
236
|
+
<ReusePager
|
|
237
|
+
page={page}
|
|
238
|
+
offset={offset}
|
|
239
|
+
onChange={setOffset}
|
|
240
|
+
busy={busy || history.loading}
|
|
241
|
+
/>
|
|
242
|
+
</>
|
|
243
|
+
)}
|
|
244
|
+
{!independent && <p>The creator and fixed executor cannot review this task.</p>}
|
|
245
|
+
{independent && page && (
|
|
246
|
+
<form onSubmit={(event) => event.preventDefault()}>
|
|
247
|
+
<fieldset disabled={busy || uncertain} className="space-y-3">
|
|
248
|
+
<legend>Record a decision on the exact intent</legend>
|
|
249
|
+
<ReuseField label="Decision note" name="note" maxLength={1000} />
|
|
250
|
+
{permissions.hasPermission('mtw-move-review:approve') && (
|
|
251
|
+
<details>
|
|
252
|
+
<summary>Approval scope and sourced handling review</summary>
|
|
253
|
+
<ReuseField
|
|
254
|
+
label={`Quantity ceiling (1–${task.requestedQuantity})`}
|
|
255
|
+
name="quantity"
|
|
256
|
+
type="number"
|
|
257
|
+
/>
|
|
258
|
+
<ReuseField label="Valid from (ISO with timezone)" name="validFrom" />
|
|
259
|
+
<ReuseField label="Valid until (exclusive, ISO with timezone)" name="validUntil" />
|
|
260
|
+
{(
|
|
261
|
+
[
|
|
262
|
+
'RESERVE',
|
|
263
|
+
'STAGE',
|
|
264
|
+
'RELEASE_UNPICKED',
|
|
265
|
+
'RELEASE_STAGED_CUSTODY',
|
|
266
|
+
'SHORT_PICK',
|
|
267
|
+
] as const
|
|
268
|
+
).map((action) => (
|
|
269
|
+
<label key={action} className="block">
|
|
270
|
+
<input type="checkbox" name={action} /> {action}
|
|
271
|
+
</label>
|
|
272
|
+
))}
|
|
273
|
+
<ReuseField label="Handling policy reference" name="handlingReference" />
|
|
274
|
+
<ReuseField label="Handling policy revision" name="handlingRevision" />
|
|
275
|
+
<ReuseField label="SOP reference" name="sopReference" />
|
|
276
|
+
<ReuseField label="SOP revision" name="sopRevision" />
|
|
277
|
+
<ContainerEvidencePicker
|
|
278
|
+
value={evidence}
|
|
279
|
+
onChange={setEvidence}
|
|
280
|
+
description="Select readable exact Files versions supporting this handling/SOP review. References alone do not certify physical facts."
|
|
281
|
+
/>
|
|
282
|
+
</details>
|
|
283
|
+
)}
|
|
284
|
+
<div className="flex flex-wrap gap-2">
|
|
285
|
+
{(['APPROVE', 'REJECT', 'REVOKE'] as const)
|
|
286
|
+
.filter((kind) =>
|
|
287
|
+
permissions.hasPermission(`mtw-move-review:${kind.toLowerCase()}`)
|
|
288
|
+
)
|
|
289
|
+
.map((kind) => (
|
|
290
|
+
<Button
|
|
291
|
+
type="button"
|
|
292
|
+
key={kind}
|
|
293
|
+
disabled={busy || uncertain || (kind === 'REVOKE' && !head)}
|
|
294
|
+
onClick={(event) => {
|
|
295
|
+
const form = event.currentTarget.form;
|
|
296
|
+
if (form) void decision(form, kind);
|
|
297
|
+
}}
|
|
298
|
+
>
|
|
299
|
+
{kind.toLowerCase()}
|
|
300
|
+
</Button>
|
|
301
|
+
))}
|
|
302
|
+
</div>
|
|
303
|
+
</fieldset>
|
|
304
|
+
</form>
|
|
305
|
+
)}
|
|
306
|
+
<Button
|
|
307
|
+
type="button"
|
|
308
|
+
variant="outline"
|
|
309
|
+
disabled={busy}
|
|
310
|
+
onClick={() => setRevision((value) => value + 1)}
|
|
311
|
+
>
|
|
312
|
+
Refresh review history
|
|
313
|
+
</Button>
|
|
314
|
+
{uncertain && !busy && (
|
|
315
|
+
<p role="alert">
|
|
316
|
+
Submission remains stopped. Intended request key: {attemptKey}. This tab preserves it
|
|
317
|
+
across refresh for this actor, workspace and exact task. Match it to the read-only
|
|
318
|
+
history; an absent page result does not prove no commit. Do not resend or clear browser
|
|
319
|
+
storage to retry. If the key is unavailable, obtain read-only reconciliation before any
|
|
320
|
+
new decision.
|
|
321
|
+
</p>
|
|
322
|
+
)}
|
|
323
|
+
{message && <p role="status">{message}</p>}
|
|
324
|
+
</section>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PickReviewReceiptFragment,
|
|
3
|
+
RecordMtwPickReviewInput,
|
|
4
|
+
} from '../../generated/pending-pick-operations';
|
|
5
|
+
|
|
6
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A truthy GraphQL data object is not an acknowledgment of the intended write. */
|
|
11
|
+
export function confirmPickReviewAcknowledgement(
|
|
12
|
+
result: unknown,
|
|
13
|
+
input: RecordMtwPickReviewInput,
|
|
14
|
+
kind: PickReviewReceiptFragment['kind'],
|
|
15
|
+
reviewerActorId: string
|
|
16
|
+
): void {
|
|
17
|
+
const field =
|
|
18
|
+
kind === 'APPROVE'
|
|
19
|
+
? 'approveMtwPickTask'
|
|
20
|
+
: kind === 'REJECT'
|
|
21
|
+
? 'rejectMtwPickTask'
|
|
22
|
+
: 'revokeMtwPickTaskReview';
|
|
23
|
+
const receipt = record(result) ? result[field] : null;
|
|
24
|
+
if (
|
|
25
|
+
!record(receipt) ||
|
|
26
|
+
typeof receipt.id !== 'string' ||
|
|
27
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(receipt.id) ||
|
|
28
|
+
receipt.kind !== kind ||
|
|
29
|
+
receipt.taskId !== input.taskId ||
|
|
30
|
+
receipt.intentHash !== input.intentHash ||
|
|
31
|
+
receipt.subjectActorId !== input.subjectActorId ||
|
|
32
|
+
receipt.idempotencyKey !== input.idempotencyKey ||
|
|
33
|
+
!reviewerActorId ||
|
|
34
|
+
receipt.reviewerActorId !== reviewerActorId ||
|
|
35
|
+
receipt.version !== input.expectedVersion + 1 ||
|
|
36
|
+
receipt.previousDecisionId !== (input.expectedPreviousDecisionId ?? null) ||
|
|
37
|
+
receipt.classification !== 'REVIEW_ONLY_NO_STOCK_EFFECT' ||
|
|
38
|
+
receipt.note !== input.note ||
|
|
39
|
+
receipt.quantityCeiling !== (input.quantityCeiling ?? null) ||
|
|
40
|
+
receipt.validFrom !== (input.validFrom ?? null) ||
|
|
41
|
+
receipt.validUntil !== (input.validUntil ?? null) ||
|
|
42
|
+
!Array.isArray(receipt.actions) ||
|
|
43
|
+
receipt.actions.length !== input.actions.length ||
|
|
44
|
+
new Set(receipt.actions).size !== receipt.actions.length ||
|
|
45
|
+
!receipt.actions.every(
|
|
46
|
+
(action: unknown) =>
|
|
47
|
+
typeof action === 'string' && input.actions.some((expected) => expected === action)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
throw new Error('The intended move-review receipt is not confirmed.');
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
fragment PickReviewReceipt on MtwPickReview {
|
|
2
|
+
id taskId version previousDecisionId intentHash subjectActorId kind actions quantityCeiling validFrom validUntil
|
|
3
|
+
handlingPolicy { reference revision } sop { reference revision }
|
|
4
|
+
evidence { fileId versionId sha256Hash checkedAt } note idempotencyKey reviewerActorId recordedAt classification
|
|
5
|
+
}
|
|
6
|
+
query PickReviewHistory($taskId: ID!, $limit: Int!, $offset: Int!) {
|
|
7
|
+
mtwPickReviewPage(taskId:$taskId,limit:$limit,offset:$offset) {
|
|
8
|
+
items { ...PickReviewReceipt } total limit offset hasMore nextOffset
|
|
9
|
+
latestDecision { ...PickReviewReceipt } reviewState evaluatedAt
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
mutation ApprovePickReview($input:RecordMtwPickReviewInput!) { approveMtwPickTask(input:$input) { ...PickReviewReceipt } }
|
|
13
|
+
mutation RejectPickReview($input:RecordMtwPickReviewInput!) { rejectMtwPickTask(input:$input) { ...PickReviewReceipt } }
|
|
14
|
+
mutation RevokePickReview($input:RecordMtwPickReviewInput!) { revokeMtwPickTaskReview(input:$input) { ...PickReviewReceipt } }
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { EntityCombobox } from '../movethewheels/components/EntityCombobox';
|
|
4
|
+
|
|
5
|
+
const choices = [
|
|
6
|
+
{ id: 'fictional-alpha', label: 'Fictional Alpha' },
|
|
7
|
+
{ id: 'fictional-beta', label: 'Fictional Beta' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
function PairedPickers() {
|
|
11
|
+
const [external, setExternal] = useState('');
|
|
12
|
+
const [internal, setInternal] = useState('');
|
|
13
|
+
const [search, setSearch] = useState('');
|
|
14
|
+
return (
|
|
15
|
+
<div className="w-80 space-y-6 p-4">
|
|
16
|
+
<p>Synthetic component examples; selections remain local to this story.</p>
|
|
17
|
+
<section>
|
|
18
|
+
<label htmlFor="fictional-external-picker">Fictional external picker</label>
|
|
19
|
+
<EntityCombobox
|
|
20
|
+
id="fictional-external-picker"
|
|
21
|
+
value={external}
|
|
22
|
+
onChange={setExternal}
|
|
23
|
+
options={choices}
|
|
24
|
+
selectedOption={choices.find((item) => item.id === external)}
|
|
25
|
+
getId={(item) => item.id}
|
|
26
|
+
getLabel={(item) => item.label}
|
|
27
|
+
renderOption={(item) => item.label}
|
|
28
|
+
/>
|
|
29
|
+
<output aria-label="External selection">{external || 'None'}</output>
|
|
30
|
+
</section>
|
|
31
|
+
<section>
|
|
32
|
+
<label htmlFor="fictional-internal-picker">Fictional internal picker</label>
|
|
33
|
+
<EntityCombobox
|
|
34
|
+
id="fictional-internal-picker"
|
|
35
|
+
value={internal}
|
|
36
|
+
onChange={setInternal}
|
|
37
|
+
options={choices.filter((item) =>
|
|
38
|
+
item.label.toLowerCase().includes(search.toLowerCase())
|
|
39
|
+
)}
|
|
40
|
+
onSearchChange={setSearch}
|
|
41
|
+
selectedOption={choices.find((item) => item.id === internal)}
|
|
42
|
+
getId={(item) => item.id}
|
|
43
|
+
getLabel={(item) => item.label}
|
|
44
|
+
renderOption={(item) => item.label}
|
|
45
|
+
/>
|
|
46
|
+
<output aria-label="Internal selection">{internal || 'None'}</output>
|
|
47
|
+
</section>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const meta: Meta<typeof PairedPickers> = {
|
|
53
|
+
title: 'MoveTheWheels/EntityCombobox',
|
|
54
|
+
component: PairedPickers,
|
|
55
|
+
};
|
|
56
|
+
export default meta;
|
|
57
|
+
type Story = StoryObj<typeof meta>;
|
|
58
|
+
export const TwoInstances: Story = {};
|