Skip to content

Commit f34b6b4

Browse files
os-zhuangclaude
andcommitted
feat(showcase): stamp real submitters on the seeded approval requests
Every seeded request had a null submitter, because the demo launches flows as SYS and the approval node stamps the requester from the flow context (`submitterId: context?.userId ?? null`, approval-node.ts). Consequences, all invisible until you go looking: * 申请人 rendered as `—` on every row and in the drawer; * the "我发起的" inbox tab was empty for every user, so that whole surface was dead; and * the submitter-only affordances never rendered anywhere — `Send reminder` and `Recall` are gated on `submitter_id == ctx.user.id`, so with no submitter they were unreachable and only 5 of the 7 declared approval actions could ever appear. Pass `userId` on the engine context and route it deliberately: * Invoice Dual Sign-off → submitted by the **admin**, so the logged-in dev admin owns one request and "我发起的" is non-empty with the submitter-only actions visible; * Committee Quorum and Expense Sign-off → submitted by **Mei Phone (demo)**, who holds no approval position and is therefore a clean requester who is never one of her own approvers. Verified in the running console: 申请人 now shows real names, "我发起的" holds the admin's request, and the same viewer sees `Approve / Reject / Reassign / Send back / Request info` on Mei's requests but those **plus** `Send reminder` and `Recall` on his own — i.e. all 7 declared actions render, gated by role. `tsc --noEmit` passes; boot log stays at 0 ERROR. From the #3358 §1 evidence pass; follow-up to #3409. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b132181 commit f34b6b4

1 file changed

Lines changed: 37 additions & 3 deletions

File tree

examples/app-showcase/src/security/seed-approval-demo.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,15 @@ async function launchSignoff(
192192
* start condition silently evaluates false and no request opens.
193193
*/
194194
previousStatus: string,
195+
/**
196+
* Who is asking. The approval node stamps the request from `context.userId`
197+
* (`submitterId: context?.userId ?? null` in approval-node.ts). Without it
198+
* every seeded request has a null submitter, which renders as 申请人 `—`,
199+
* leaves the "我发起的" inbox tab empty for every user, and suppresses the
200+
* submitter-only affordances (recall / remind) — so the submitter half of
201+
* the approval UI is unreachable.
202+
*/
203+
submitterId: string | null,
195204
): Promise<void> {
196205
const recordId = String(record.id ?? '');
197206
if (!recordId) return;
@@ -213,6 +222,7 @@ async function launchSignoff(
213222
previous: { ...record, status: previousStatus },
214223
object: objectName,
215224
organizationId,
225+
...(submitterId ? { userId: submitterId } : {}),
216226
})) as { success?: boolean; error?: string; output?: { skipped?: boolean; reason?: string } };
217227
if (result?.success === false) {
218228
ctx.logger?.warn?.('[showcase] approval-demo flow returned an error', { flow: flowName, error: result.error });
@@ -250,7 +260,9 @@ export function registerShowcaseApprovalDemo(ctx: ApprovalDemoContext): void {
250260
const organizationId = (anyMember?.organization_id as string | undefined) ?? null;
251261

252262
await assignPositions(ctx, adminId, ADMIN_APPROVAL_POSITIONS, organizationId, 'admin');
253-
await ensureDemoUser(ctx, PHONE_DEMO_USER);
263+
// Mei holds no approval position, which makes her a clean *submitter* — a
264+
// requester who is never also one of her own approvers.
265+
const submitterId = (await ensureDemoUser(ctx, PHONE_DEMO_USER)) ?? null;
254266
// The auditor persona backs the `finance` group of the per-group demo. It
255267
// deliberately holds ONLY `auditor`, so the two groups have distinct
256268
// holders and the request stays open until each group has answered.
@@ -272,17 +284,38 @@ export function registerShowcaseApprovalDemo(ctx: ApprovalDemoContext): void {
272284
// is `status == "sent" && previous.status != "sent"`, so it entered from
273285
// `draft`. Both named approvers must answer (not one-per-group — that is
274286
// the expense demo below).
287+
// Submitted by the ADMIN on purpose: it is the one request the logged-in dev
288+
// admin owns, so the "我发起的" tab is non-empty and the submitter-only
289+
// affordances (recall / remind) have somewhere to appear.
275290
const sentInvoice = await findOne(ctx, 'showcase_invoice', { status: 'sent' });
276291
if (sentInvoice) {
277-
await launchSignoff(ctx, engine, 'showcase_invoice_signoff', 'showcase_invoice', sentInvoice, organizationId, 'draft');
292+
await launchSignoff(
293+
ctx,
294+
engine,
295+
'showcase_invoice_signoff',
296+
'showcase_invoice',
297+
sentInvoice,
298+
organizationId,
299+
'draft',
300+
adminId,
301+
);
278302
}
279303

280304
// Quorum (2-of-3): High-Value Committee needs a `submitted` report ≥ $5000;
281305
// the start gate is `status == "submitted" && previous.status != "submitted"
282306
// && total_amount >= 5000`, so it entered from `draft`.
283307
const demoExpense = await findOne(ctx, 'showcase_expense_report', { name: 'EXP-DEMO' });
284308
if (demoExpense) {
285-
await launchSignoff(ctx, engine, 'showcase_committee_quorum', 'showcase_expense_report', demoExpense, organizationId, 'draft');
309+
await launchSignoff(
310+
ctx,
311+
engine,
312+
'showcase_committee_quorum',
313+
'showcase_expense_report',
314+
demoExpense,
315+
organizationId,
316+
'draft',
317+
submitterId,
318+
);
286319
}
287320

288321
// 会签 (per_group): Expense Sign-off needs one approval from EACH of the
@@ -299,6 +332,7 @@ export function registerShowcaseApprovalDemo(ctx: ApprovalDemoContext): void {
299332
perGroupExpense,
300333
organizationId,
301334
'draft',
335+
submitterId,
302336
);
303337
}
304338
};

0 commit comments

Comments
 (0)