Skip to content

Commit 78d8ee8

Browse files
os-zhuangclaude
andauthored
test(showcase): specimens for the inline picker and the record-backed approvers (#3405, #3508) (#3970)
* test(showcase): specimens for the inline picker and the record-backed approvers (#3405, #3508) Both issues shipped their platform fix but left their last acceptance item on a real backend, and showcase had no specimen for either shape. **#3405 — `p_assignee`, an inline `lookup` at `sys_user`.** The gallery already covered an inline picker aimed at one of the app's own objects (`p_account` → `showcase_account`). The reported bug (PLAT-DEF-005: "assign an inspector" handed the supervisor a box wanting a pasted UUID) was a picker at a SYSTEM object, and a person is the reference a human is least able to identify by id — `sys_user` ids here are opaque 32-char tokens. That earns its own specimen rather than riding on the account one. **#3508 — `showcase_approver_bindings`, one approval node per record-backed approver kind.** The existing flows cover `position` heavily and `dynamic-approval` covers `org_membership_level` + `expression`; `user`, `team`, `department`, `manager` and `field` had no specimen at all, which is why the designer's Value control could regress unnoticed. `queue` is deliberately absent — #3536 made it non-authorable. The flow is `draft` on purpose, not by omission: the record-backed kinds point at rows a fresh boot does not have (`sys_team` is empty, and unit membership / position assignment are runtime admin actions by design — see the seed notes). An `active` flow over them would resolve to nobody, which is the exact silent dead slot #3508 exists to remove; shipping that as a demo would re-teach the bug. `draft` keeps it out of the runtime while still loading in the designer, which is the surface under test. Verified on a real backend (showcase :5411 + objectui HEAD console :5412, own DB): - the server publishes `reference` for both inline picker params; - the param dialog renders Assignee as a searchable picker — typing "Dev" issues `GET /api/v1/data/sys_user?top=50&search=Dev` and offers "Dev Admin / admin@objectos.ai", with no degrade warning logged; - the designer renders a record picker per kind off the published `xRef.sources`, listing the real seeded org tree for `department` (`sys_business_unit`), `manager` as a disabled "Resolved automatically" cell, `field` as the trigger-object field selector, and no `queue` in the Type dropdown. No `/api/v1/meta/:type` registry calls — the old broken path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkvDs4y4gveyB5NJZKxaZt * chore: add an empty changeset for the showcase specimens (#3405, #3508) The "Check Changeset" gate requires one on every PR, not only on PRs that touch a published package — and it sanctions an empty changeset for a change that releases nothing, which this is: both specimens live in `@objectstack/example-showcase`, which is `private: true`, so changesets would skip it regardless. Matches the existing empty-changeset entries (`adr-0104-design-doc-only.md` and friends). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkvDs4y4gveyB5NJZKxaZt * i18n(showcase): translate the new inline picker param (#3405) `check-i18n-coverage` ratchets each example at its current untranslated count, so a newly declared label that skips a locale the example claims to support pushes the count up and fails the gate — 456 → 460 here (`p_assignee`'s `label` and `helpText`, each counted on two surfaces). Translated rather than ratcheting the baseline up: ratcheting is what the gate exists to prevent, and the script only sanctions `--update` for a count that went DOWN. The gallery's older params sit inside the 456 baseline; this one is not allowed to widen it. `负责人` is what the bundle already renders for `showcase_task.assignee`, so the same idea keeps one word. The flow specimen's own label/description are not flagged — the `i18n/missing-action` rule covers action params, not flow nodes. `check-i18n-coverage` now reports "none new"; validate, typecheck and the 58 example tests all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LkvDs4y4gveyB5NJZKxaZt --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c5c78bb commit 78d8ee8

5 files changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
test(showcase): dogfood specimens for the inline record picker (#3405) and the record-backed approver kinds (#3508) — releases nothing. Both live in `@objectstack/example-showcase`, which is private.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import { defineFlow } from '@objectstack/spec';
4+
5+
/**
6+
* #3508 dogfood: one node per RECORD-BACKED approver kind.
7+
*
8+
* The designer's Approvers → Value control used to query the metadata registry
9+
* (`GET /api/v1/meta/:type`) for every kind. `user` / `team` / `department` /
10+
* `position` are DATA rows in the platform's directory objects, not registry
11+
* entries, so candidates came back empty and the control degraded to a free-text
12+
* box — the author could only hand-type a userid. `APPROVER_VALUE_BINDINGS`
13+
* now publishes the data contract per kind (`xRef.sources`), and the designer
14+
* renders a real record lookup off it.
15+
*
16+
* This flow is the authoring specimen for that surface: every kind whose control
17+
* changed gets a node, so opening it in the flow designer exercises each one.
18+
* The existing approval flows in `./index.ts` already cover `position` heavily
19+
* (it is the kind showcase actually routes on) and `./dynamic-approval.flow.ts`
20+
* covers `org_membership_level` + `expression`; the kinds below are the ones no
21+
* other flow declares.
22+
*
23+
* ## Why `draft`
24+
*
25+
* Deliberate, not an oversight. The record-backed kinds point at rows a fresh
26+
* boot does not have: `sys_user` holds only the seeded admin, `sys_team` is
27+
* empty (the seeded `Team` rows are the DEMO `showcase_team` object), and both
28+
* unit membership (`sys_business_unit_member`) and position assignment
29+
* (`sys_user_position`) are runtime admin actions by design — see the seed
30+
* notes in `src/data/seed/index.ts`. An `active` flow over them would resolve
31+
* to nobody, which is precisely the silent dead slot #3508 exists to remove.
32+
* Shipping that as a demo would re-teach the bug. `draft` keeps it out of the
33+
* runtime while still loading in the designer, which is the surface under test.
34+
*
35+
* `queue` is absent on purpose: #3536 made it non-authorable
36+
* (`NON_AUTHORABLE_APPROVER_TYPES` → `xEnumDeprecated`) because the engine has
37+
* no queue branch, so the designer no longer offers it.
38+
*/
39+
export const ApproverBindingsFlow = defineFlow({
40+
name: 'showcase_approver_bindings',
41+
label: 'Approver Binding Specimens (#3508)',
42+
description:
43+
'Authoring specimen: one approval node per record-backed approver kind, so the designer '
44+
+ 'renders each Value control. Draft on purpose — a fresh org has no rows for these kinds.',
45+
type: 'autolaunched',
46+
status: 'draft',
47+
nodes: [
48+
{
49+
id: 'start',
50+
type: 'start',
51+
label: 'On Field Zoo Updated',
52+
config: {
53+
objectName: 'showcase_field_zoo',
54+
triggerType: 'record-after-update',
55+
},
56+
},
57+
{
58+
id: 'by_user',
59+
type: 'approval',
60+
label: 'Named user',
61+
config: {
62+
// `sys_user` row id. The most portable-hostile choice there is — an id
63+
// does not survive an environment migration and dies with the person —
64+
// which is why the designer's Type dropdown lists it LAST.
65+
approvers: [{ type: 'user', value: '' }],
66+
behavior: 'first_response',
67+
},
68+
},
69+
{
70+
id: 'by_team',
71+
type: 'approval',
72+
label: 'Team members',
73+
config: {
74+
// `sys_team` row id; the engine expands it through `sys_team_member`.
75+
approvers: [{ type: 'team', value: '' }],
76+
behavior: 'first_response',
77+
},
78+
},
79+
{
80+
id: 'by_department',
81+
type: 'approval',
82+
label: 'Department members',
83+
config: {
84+
// `sys_business_unit` row id — NOT `sys_department`. The seeded org tree
85+
// (bu_acme / bu_field_ops / bu_west_coast / …) is what this picker lists.
86+
approvers: [{ type: 'department', value: '' }],
87+
behavior: 'first_response',
88+
},
89+
},
90+
{
91+
id: 'by_position',
92+
type: 'approval',
93+
label: 'Position holders',
94+
config: {
95+
// Commits the position's machine NAME, not its row id: the engine routes
96+
// on `sys_user_position.position`, and a machine name is what stays
97+
// valid across environments. `manager` is a showcase position.
98+
approvers: [{ type: 'position', value: 'manager' }],
99+
behavior: 'first_response',
100+
},
101+
},
102+
{
103+
id: 'by_manager',
104+
type: 'approval',
105+
label: "Submitter's manager",
106+
config: {
107+
// Resolved at run time from the submitter — there is no value to author,
108+
// so the designer disables the cell instead of offering a free-text box.
109+
approvers: [{ type: 'manager' }],
110+
behavior: 'first_response',
111+
},
112+
},
113+
{
114+
id: 'by_field',
115+
type: 'approval',
116+
label: 'User in a trigger field',
117+
config: {
118+
// Value is a FIELD NAME on the trigger object, not a record reference —
119+
// so this one keeps the object-field selector rather than a record
120+
// lookup. `f_user` is Field Zoo's single-value `sys_user` reference.
121+
approvers: [{ type: 'field', value: 'f_user' }],
122+
behavior: 'first_response',
123+
},
124+
},
125+
{ id: 'approved', type: 'end', label: 'Approved' },
126+
{ id: 'rejected', type: 'end', label: 'Rejected' },
127+
],
128+
edges: [
129+
{ id: 'e1', source: 'start', target: 'by_user' },
130+
{ id: 'e2', source: 'by_user', target: 'by_team', label: 'approve' },
131+
{ id: 'e3', source: 'by_team', target: 'by_department', label: 'approve' },
132+
{ id: 'e4', source: 'by_department', target: 'by_position', label: 'approve' },
133+
{ id: 'e5', source: 'by_position', target: 'by_manager', label: 'approve' },
134+
{ id: 'e6', source: 'by_manager', target: 'by_field', label: 'approve' },
135+
{ id: 'e7', source: 'by_field', target: 'approved', label: 'approve' },
136+
{ id: 'e8', source: 'by_user', target: 'rejected', label: 'reject' },
137+
{ id: 'e9', source: 'by_team', target: 'rejected', label: 'reject' },
138+
{ id: 'e10', source: 'by_department', target: 'rejected', label: 'reject' },
139+
{ id: 'e11', source: 'by_position', target: 'rejected', label: 'reject' },
140+
{ id: 'e12', source: 'by_manager', target: 'rejected', label: 'reject' },
141+
{ id: 'e13', source: 'by_field', target: 'rejected', label: 'reject' },
142+
],
143+
});

examples/app-showcase/src/automation/flows/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { defineFlow } from '@objectstack/spec';
44
import { DynamicApprovalFlow } from './dynamic-approval.flow';
5+
import { ApproverBindingsFlow } from './approver-bindings.flow';
56

67
/**
78
* Task Completed → Notify — an autolaunched, record-triggered flow that fires
@@ -1635,4 +1636,8 @@ export const allFlows = [
16351636
InboundTaskWebhookFlow,
16361637
// #3447 P2 dogfood: expression approvers + decision outputs, end to end.
16371638
DynamicApprovalFlow,
1639+
// #3508 dogfood: one approval node per record-backed approver kind, so the
1640+
// designer's Value control has a specimen for each. Draft on purpose — see
1641+
// the module docstring.
1642+
ApproverBindingsFlow,
16381643
];

examples/app-showcase/src/system/translations/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,23 @@ export const ShowcaseTranslationBundle = {
267267
showcase_field_zoo: {
268268
label: '字段动物园', pluralLabel: '字段动物园',
269269
fields: { f_lookups: { label: '查找 → 客户(多值)' } },
270+
// #3405 — the inline system-object picker specimen. Translated at birth
271+
// because `check-i18n-coverage` ratchets this example at its current
272+
// untranslated count: a newly declared label that skips zh-CN pushes the
273+
// count up and fails the gate. (The gallery's older params sit inside
274+
// that baseline; this one is not allowed to widen it.) `负责人` matches
275+
// what the bundle already renders for `showcase_task.assignee`, rather
276+
// than introducing a second word for the same idea.
277+
_actions: {
278+
showcase_action_param_gallery: {
279+
params: {
280+
p_assignee: {
281+
label: '负责人',
282+
helpText: '指向系统对象的内联查找 —— 可按姓名或邮箱搜索,无需填写 UUID。',
283+
},
284+
},
285+
},
286+
},
270287
},
271288
},
272289
},

examples/app-showcase/src/ui/actions/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,15 @@ export const ActionParamGalleryAction = defineAction({
212212
name: 'p_account', type: 'lookup', reference: 'showcase_account', label: 'Related account',
213213
helpText: 'Inline lookup param — searchable record picker, no UUID typing.',
214214
},
215+
// #3405 — the same inline picker aimed at a SYSTEM object. This is the
216+
// shape the bug was reported against (PLAT-DEF-005: "assign an inspector"
217+
// handed the supervisor a box wanting a pasted UUID), so it earns its own
218+
// specimen: `sys_user` is not one of the app's own objects, and a person is
219+
// the reference a human is least able to identify by id.
220+
{
221+
name: 'p_assignee', type: 'lookup', reference: 'sys_user', label: 'Assignee',
222+
helpText: 'Inline lookup at a system object — searchable by name/email, never a UUID.',
223+
},
215224
{ name: 'p_color', type: 'color', label: 'Accent color', defaultValue: '#7C3AED' },
216225
// Spec `autonumber` param → the AutoNumber widget (read-only, auto-assigned).
217226
{ name: 'p_reference', type: 'autonumber', label: 'Reference #' },

0 commit comments

Comments
 (0)