Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/showcase-approver-and-picker-specimens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,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.
143 changes: 143 additions & 0 deletions examples/app-showcase/src/automation/flows/approver-bindings.flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

import { defineFlow } from '@objectstack/spec';

/**
* #3508 dogfood: one node per RECORD-BACKED approver kind.
*
* The designer's Approvers → Value control used to query the metadata registry
* (`GET /api/v1/meta/:type`) for every kind. `user` / `team` / `department` /
* `position` are DATA rows in the platform's directory objects, not registry
* entries, so candidates came back empty and the control degraded to a free-text
* box — the author could only hand-type a userid. `APPROVER_VALUE_BINDINGS`
* now publishes the data contract per kind (`xRef.sources`), and the designer
* renders a real record lookup off it.
*
* This flow is the authoring specimen for that surface: every kind whose control
* changed gets a node, so opening it in the flow designer exercises each one.
* The existing approval flows in `./index.ts` already cover `position` heavily
* (it is the kind showcase actually routes on) and `./dynamic-approval.flow.ts`
* covers `org_membership_level` + `expression`; the kinds below are the ones no
* other flow declares.
*
* ## Why `draft`
*
* Deliberate, not an oversight. The record-backed kinds point at rows a fresh
* boot does not have: `sys_user` holds only the seeded admin, `sys_team` is
* empty (the seeded `Team` rows are the DEMO `showcase_team` object), and both
* unit membership (`sys_business_unit_member`) and position assignment
* (`sys_user_position`) are runtime admin actions by design — see the seed
* notes in `src/data/seed/index.ts`. An `active` flow over them would resolve
* to nobody, which is precisely the 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.
*
* `queue` is absent on purpose: #3536 made it non-authorable
* (`NON_AUTHORABLE_APPROVER_TYPES` → `xEnumDeprecated`) because the engine has
* no queue branch, so the designer no longer offers it.
*/
export const ApproverBindingsFlow = defineFlow({
name: 'showcase_approver_bindings',
label: 'Approver Binding Specimens (#3508)',
description:
'Authoring specimen: one approval node per record-backed approver kind, so the designer '
+ 'renders each Value control. Draft on purpose — a fresh org has no rows for these kinds.',
type: 'autolaunched',
status: 'draft',
nodes: [
{
id: 'start',
type: 'start',
label: 'On Field Zoo Updated',
config: {
objectName: 'showcase_field_zoo',
triggerType: 'record-after-update',
},
},
{
id: 'by_user',
type: 'approval',
label: 'Named user',
config: {
// `sys_user` row id. The most portable-hostile choice there is — an id
// does not survive an environment migration and dies with the person —
// which is why the designer's Type dropdown lists it LAST.
approvers: [{ type: 'user', value: '' }],
behavior: 'first_response',
},
},
{
id: 'by_team',
type: 'approval',
label: 'Team members',
config: {
// `sys_team` row id; the engine expands it through `sys_team_member`.
approvers: [{ type: 'team', value: '' }],
behavior: 'first_response',
},
},
{
id: 'by_department',
type: 'approval',
label: 'Department members',
config: {
// `sys_business_unit` row id — NOT `sys_department`. The seeded org tree
// (bu_acme / bu_field_ops / bu_west_coast / …) is what this picker lists.
approvers: [{ type: 'department', value: '' }],
behavior: 'first_response',
},
},
{
id: 'by_position',
type: 'approval',
label: 'Position holders',
config: {
// Commits the position's machine NAME, not its row id: the engine routes
// on `sys_user_position.position`, and a machine name is what stays
// valid across environments. `manager` is a showcase position.
approvers: [{ type: 'position', value: 'manager' }],
behavior: 'first_response',
},
},
{
id: 'by_manager',
type: 'approval',
label: "Submitter's manager",
config: {
// Resolved at run time from the submitter — there is no value to author,
// so the designer disables the cell instead of offering a free-text box.
approvers: [{ type: 'manager' }],
behavior: 'first_response',
},
},
{
id: 'by_field',
type: 'approval',
label: 'User in a trigger field',
config: {
// Value is a FIELD NAME on the trigger object, not a record reference —
// so this one keeps the object-field selector rather than a record
// lookup. `f_user` is Field Zoo's single-value `sys_user` reference.
approvers: [{ type: 'field', value: 'f_user' }],
behavior: 'first_response',
},
},
{ id: 'approved', type: 'end', label: 'Approved' },
{ id: 'rejected', type: 'end', label: 'Rejected' },
],
edges: [
{ id: 'e1', source: 'start', target: 'by_user' },
{ id: 'e2', source: 'by_user', target: 'by_team', label: 'approve' },
{ id: 'e3', source: 'by_team', target: 'by_department', label: 'approve' },
{ id: 'e4', source: 'by_department', target: 'by_position', label: 'approve' },
{ id: 'e5', source: 'by_position', target: 'by_manager', label: 'approve' },
{ id: 'e6', source: 'by_manager', target: 'by_field', label: 'approve' },
{ id: 'e7', source: 'by_field', target: 'approved', label: 'approve' },
{ id: 'e8', source: 'by_user', target: 'rejected', label: 'reject' },
{ id: 'e9', source: 'by_team', target: 'rejected', label: 'reject' },
{ id: 'e10', source: 'by_department', target: 'rejected', label: 'reject' },
{ id: 'e11', source: 'by_position', target: 'rejected', label: 'reject' },
{ id: 'e12', source: 'by_manager', target: 'rejected', label: 'reject' },
{ id: 'e13', source: 'by_field', target: 'rejected', label: 'reject' },
],
});
5 changes: 5 additions & 0 deletions examples/app-showcase/src/automation/flows/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

/**
* Task Completed → Notify — an autolaunched, record-triggered flow that fires
Expand Down Expand Up @@ -1635,4 +1636,8 @@ export const allFlows = [
InboundTaskWebhookFlow,
// #3447 P2 dogfood: expression approvers + decision outputs, end to end.
DynamicApprovalFlow,
// #3508 dogfood: one approval node per record-backed approver kind, so the
// designer's Value control has a specimen for each. Draft on purpose — see
// the module docstring.
ApproverBindingsFlow,
];
17 changes: 17 additions & 0 deletions examples/app-showcase/src/system/translations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,23 @@ export const ShowcaseTranslationBundle = {
showcase_field_zoo: {
label: '字段动物园', pluralLabel: '字段动物园',
fields: { f_lookups: { label: '查找 → 客户(多值)' } },
// #3405 — the inline system-object picker specimen. Translated at birth
// because `check-i18n-coverage` ratchets this example at its current
// untranslated count: a newly declared label that skips zh-CN pushes the
// count up and fails the gate. (The gallery's older params sit inside
// that baseline; this one is not allowed to widen it.) `负责人` matches
// what the bundle already renders for `showcase_task.assignee`, rather
// than introducing a second word for the same idea.
_actions: {
showcase_action_param_gallery: {
params: {
p_assignee: {
label: '负责人',
helpText: '指向系统对象的内联查找 —— 可按姓名或邮箱搜索,无需填写 UUID。',
},
},
},
},
},
},
},
Expand Down
9 changes: 9 additions & 0 deletions examples/app-showcase/src/ui/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,15 @@ export const ActionParamGalleryAction = defineAction({
name: 'p_account', type: 'lookup', reference: 'showcase_account', label: 'Related account',
helpText: 'Inline lookup param — searchable record picker, no UUID typing.',
},
// #3405 — the same inline picker aimed at a SYSTEM object. This is the
// shape the bug was reported against (PLAT-DEF-005: "assign an inspector"
// handed the supervisor a box wanting a pasted UUID), so it earns its own
// specimen: `sys_user` is not one of the app's own objects, and a person is
// the reference a human is least able to identify by id.
{
name: 'p_assignee', type: 'lookup', reference: 'sys_user', label: 'Assignee',
helpText: 'Inline lookup at a system object — searchable by name/email, never a UUID.',
},
{ name: 'p_color', type: 'color', label: 'Accent color', defaultValue: '#7C3AED' },
// Spec `autonumber` param → the AutoNumber widget (read-only, auto-assigned).
{ name: 'p_reference', type: 'autonumber', label: 'Reference #' },
Expand Down
Loading