Uh oh!
There was an error while loading. Please reload this page.
fix(platform-objects,spec): surface the email-invite entry on the org page default Members tab - #11562
Conversation
…ault Members tab The sys_organization record page (ADR-0081) opens on tab-0 Members, whose related-list toolbar carried only `add_member` (attach an already-registered user by id). The email-invite entry lived on tab-1 Invitations, so an admin looking to invite a teammate by email found no invite affordance at all. - sys_member declares its own `invite_user` on `list_toolbar`, ahead of `add_member` (declaration order is render order in the toolbar bridge). - The `email` param names `objectOverride: 'sys_invitation'` — sys_member has no `email` field, and an unresolvable field-backed param degrades silently to an untyped text input (ADR-0078). `role` resolves natively. - `add_member` is differentiated in chrome only: `variant: 'secondary'` + `icon: 'link-2'`. Behaviour, target and label unchanged. - PUBLIC_AUTH_FEATURES.organization.gatedInputs books the new gated action, as the feature-gate completeness guard requires. Fixes#11544 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vite-entry-members-tab
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 748ed671c1db109dea81b59a2fa823124a20621f && git checkout 748ed671c1db109dea81b59a2fa823124a20621f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ba8420b58d36077fe79ca8ce0f201bb31f8be2bc 22ecd75cbd5db06793808b76b3c640bbdb9d024d && git checkout -B drift-repro ba8420b58d36077fe79ca8ce0f201bb31f8be2bc && git merge --no-ff 22ecd75cbd5db06793808b76b3c640bbdb9d024d
node scripts/docs-audit/affected-docs.mjs --json ba8420b58d36077fe79ca8ce0f201bb31f8be2bc
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11544
The
sys_organizationrecord page (ADR-0081) opens on tab-0 Members, whose related-list toolbar carried exactly one action —add_member, which attaches an already-registered user by id. The email-invite entry,invite_user, was declared only onsys_invitationandsys_user, so it appeared only on tab-1 Invitations. An admin looking to "invite a teammate by email" landed on Members, found no invite affordance, and concluded the product had none. The delivery half worked the whole time — only the door was in another room.What changed
sys_memberdeclares its owninvite_useronlist_toolbar, ahead ofadd_member. Same endpoint (/api/v1/auth/organization/invite-member), same email + role inputs, samerequiresFeature: 'organization'capability gate as the other two mirrors.add_memberis differentiated in chrome only —variant: 'secondary'andicon: 'link-2'. Its behaviour, target, params and label are untouched.PUBLIC_AUTH_FEATURES.organization.gatedInputsbooks the new gated action (see the declared deviation below).The PM's three mechanism assumptions, as measured
Read against
../objectuiatbc21c704b(read-only; nothing in that repo was edited).list_toolbaractions — CONFIRMED.RelatedRecordActionsBridge.deriveActionsfilters the child object's actions bylocations.includes('list_toolbar')and assigns the whole array;RelatedListmaps over(toolbarActions ?? []). No cap, no slice. Two buttons render side by side.invite_usercould NOT be mirrored unchanged — REFUTED, and this is the substantive finding.sys_memberhas noemailfield, so thesys_invitationcopy's{ field: 'email' }is unresolvable there.resolveActionParams.tsanswers an unresolvable field-backed param with atype: 'text'fallback labelled by the raw field name — the dialog still renders and still submits, so nothing goes red anywhere (the ADR-0078 valid-but-inert class). The fix stays entirely insideplatform-objectsvia the existing authorableobjectOverridekey, exactly assys_user's own copy already reaches forsys_memberfor itsrolehalf. No spec schema change was needed, so the card's premise held.roleneeds no override:sys_memberdeclares it, from the sameBUILTIN_MEMBERSHIP_ROLE_OPTIONSconstantsys_invitationreads.list_toolbaraction needs no row context — CONFIRMED. The bridge dispatchesrunRowAction(objectName, undefined, action); theundefinedrow is the same path thesys_invitationcopy already executes on.Also measured, because it decides whether the differentiation is real rather than decorative:
RelatedToolbarButtonrendersvariant={action.variant === 'primary' ? 'default' : 'outline'}, so demotingadd_membertosecondarygenuinely changes it from a filled button to an outlined one. The icon is resolved through lucide and rendered. Neither half is inert metadata.Declared deviation — one line in
packages/specThe card was claimed
Clause-②: no, and the dispatch instruction was to stop and escalate ifpackages/spechad to be touched. That instruction anticipated a schema change to make the mirror expressible; assumption 2 above shows none was needed. What the change does require is one line of registry bookkeeping:feature-gate-guard.test.tsenforces, in both directions, that everyfeatures.*reference in any platform object's lowered predicate is booked inPUBLIC_AUTH_FEATURES.<flag>.gatedInputs. DeclaringrequiresFeature: 'organization'on the new action therefore mechanically obliges the ledger entry — the twelve sibling entries beside it, includingsys_invitation.actions.invite_user, are the same obligation discharged. The alternatives were to ship the button ungated (it would render and then 404 wherever the org capability is off — a Prime Directive #10 violation) or to leave the card undelivered.No schema, no export, no authorable key and no accept/reject behaviour changes;
pnpm --filter @objectstack/spec check:generatedreports all 14 generated artifacts up to date with no regeneration, which is the mechanical confirmation that the public surface did not move. Flagging it here rather than deciding it quietly: if the PM reads the ledger append as clause-② territory, the enqueue gate judges the actual diff before this can queue.Tests
New:
packages/platform-objects/src/identity/invite-entry-toolbar.test.ts(21 cases). Beyond pinning the new declaration, its load-bearing case is stated over all three mirrors rather than over the one that was wrong — every field-backed param of everyinvite_usercopy must name a field that really exists on the object it resolves against. That closes the class, so the next copy of any action onto an object lacking one of its param fields lands as a red test instead of as a silently untyped dialog.Extended: the
#2874feature-gate lowering matrix inplatform-objects.test.tsgains theSysMember.invite_userrow.Ablation (predicted direction: red).
sys-member.object.tsreverted toorigin/mainwith everything else intact; the mutation was proven on disk before any result was read — the three markersname: 'invite_user',objectOverride: 'sys_invitation'andicon: 'link-2'each dropped to 0 grep hits and the pre-fixicon: 'user-plus'onadd_membercame back. Mutated: 10 failed | 11 passed (21). Restored: 21 passed (21), withgit status --porcelainempty on the path, proving the restore was byte-exact. The 11 that stay green under mutation are thesys_user/sys_invitationmirror rows, which the mutation did not touch — the right shape for a targeted revert. No rebuild was required for either leg: these suites import the object modules by relative source path, so nodist/sits between the mutation and the assertion.Gates run locally, all on merge commit
22ecd75cb(the head this PR opens with), each exit code captured before any pipe:pnpm --filter @objectstack/platform-objects test→ 29 files / 476 passed ·typecheck(platform-objects + spec) ·check:i18n·--filter @objectstack/spec check:generated(14/14 up to date) ·check:nul-bytes·check:engine-double-contract·check:where-matcher·check:cross-package-test-inputs·check:type-check-coverage·check-adr-0087-registration·check-empty-changeset·check-changeset-no-major— all exit 0.Earlier in the round, on the pre-merge commit, the rest of the derived family also ran green:
check:changeset-gate-self-tests,check:query-options-erasure,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check:spec-parsed-alias,check:merge-driver,check-ci-filter-parity,check-plugin-teardown-shape,check:doc-formula-expressions, and spec'scheck:empty-state/check:liveness/check:strictness-ledger/check:variant-docs.Declared narrowings — host capability, not diff findings
This seat ran on macOS with bash 3.2.57, which has no
mapfilebuiltin, and noflock. Three consequences, all declared rather than worked around:scripts/pm/os-verify-lock.shrefuses on this host —VERDICT lock-unusable (exit 99) · never acquired · refused before waiting · nothing was built or tested. Every build and test above therefore ran unlocked.pnpm check:objectui-changesetreports 7--self-testfailures, all ten diagnostics namingscripts/bump-objectui.sh: line 324: mapfile: command not found. That script is not in this diff, and the self-test operates on temp fixtures rather than on this changeset. Linux CI has the builtin.scripts/pm/os-regen-merge.shcould not run for the same reason, so its documented order was followed by hand. Nomerge=os-regenpath appears on either side of this merge, so its step 2 was a no-op; no deferral marker was recorded and none is outstanding.node scripts/check-dev-prereqs.mjsreports the workspace unbuilt (12 of 67 packages missing adist/entry point —client-react,studio,embedder-openaiand others). That is a consequence of building only the affected closure per resource discipline, not a verdict about this diff; none of the named packages is touched here.Merge note
#11530landed as033a34c7cwhile this branch was in flight and is merged in here. Its overlap with this diff was wider than the claim comment anticipated — beyondidentity/, it also moved the same four translation bundles,platform-objects.test.ts,feature-gate-guard.test.tsandpackages/spec/src/kernel/public-auth-features.ts. All hunks are disjoint and both sides survive: in the spec ledger this branch's entry sits in theorganizationblock while that PR's retirement comment sits in theadminblock. The full re-verification above was run after the merge for exactly this reason.Generated by Claude Code