You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit b0a1df0
Browse filesBrowse the repository at this point in the historyBrowse files
feat(platform-objects): sys_organization declares a record_header invite action; leave_organization folds to the overflow and names what is lost (#24216)
Fixes#24016#24021's copy half rides here (ruling 6228318638 item 2); that card's
record-scope key is not added.
Clause-②: no
## What
`sys_organization` declares the invite action ruling 6228318638 (letter
B′ on #24034, item 1) names, so an organization record page's
`page:header` can list it. A `page:header` resolves action ids against
its own object only.
- **`invite_user` at `record_header`**: `type: 'api'`, `target:
'/api/v1/auth/organization/invite-member'`, `recordIdParam:
'organizationId'`, `requiresFeature: 'organization'`,
`requiresMembershipReach: 'invite_member'`, `variant: 'primary'`,
`order: -1`. Its params are `email` and `role`, both required, with the
role picker on `INVITATION_ROLE_OPTIONS`, as
`sys_invitation.invite_user` declares them. Its description and success
message match the other three mirrors (`sys_user`, `sys_invitation`,
`sys_member`).
- **`leave_organization`** keeps `variant: 'danger'` and gains `order:
1`. Its `confirmText` now names what is lost: "Leave this organization?
You will lose access to its environments and members." The zh-CN, ja-JP
and es-ES bundles carry translated text.
- No new spec key.
## Measurements behind the choices
- **H1, the params.** `email` and `role` are not fields of
`sys_organization`. A field-backed param resolves against the action's
own object unless `objectOverride` names another. So both params carry
`objectOverride: 'sys_invitation'`, the device `sys_member`'s mirror
already uses for `email`. Apart from that the shape is
`sys_invitation`'s (required, same options). The existing "every
field-backed param resolves to a real field" pin now covers all four
mirrors.
- **H2, the gates.** Both keys are declared on `ActionSchema`
(`requiresFeature`, `requiresMembershipReach` in
`packages/spec/src/ui/action.zod.ts`) and are already used by all three
mirrors. The lowered `visible` matches theirs byte for byte; that is
pinned.
- **H3, the order.** I read objectui `9753811`'s `page:header` renderer
(`packages/components/src/renderers/layout/containers.tsx`). It
stable-sorts by `order` (unset = 0) and promotes the first
non-destructive `variant: 'primary'` action. Then the first `maxVisible`
authored actions (default 3) go inline and the rest fold into the `⋯`,
with destructive ones in its last band. With only the invite added, the
row was invite, set_active, **leave**, and `change_slug` folded. So
`leave_organization` stayed inline as the third button. For it to "fall
to the overflow" it needs a declared position: `order: 1` sorts it last,
so the inline row is invite, set_active, change_slug and leave folds.
This reuses the spec's existing `order`, so no new key.
- **H4/H5, the bundles.** `en.objects.generated.ts` is rewritten from
source on every extract. The translated locales keep their values in
merge mode, so the new leaves were seeded in English by `pnpm
i18n:extract`. I set them to the sibling `sys_invitation.invite_user`
translations and translated the new `leave_organization.confirmText`,
then re-ran the extract. It is idempotent and drops the seeded
source-hash entries. `pnpm check:i18n`: all 10 packages in sync.
## Outside the claim's file surface
`packages/spec/src/kernel/public-auth-features.ts` gains one row:
`sys_organization.actions.invite_user` in
`PUBLIC_AUTH_FEATURES.organization.gatedInputs`. This is a ledger entry,
not a key. `platform-objects/src/feature-gate-guard.test.ts` requires
every `features.*` gate in the objects to be booked there, and it went
red without the row. The ruled `requiresFeature: 'organization'` cannot
land without it. `pnpm --filter @objectstack/spec check:generated`
reports all 14 artifacts up to date, and the changeset adds a
`@objectstack/spec` patch.
## Tests
- New `sys-organization-invite-action.test.ts` pins:
- the record-header placement: location, endpoint, `recordIdParam`;
- the invite is the only primary action and sorts first;
- `leave_organization` stays `danger` and sorts past the inline budget.
- `invite-entry-toolbar.test.ts` and `invite-user-action-copy.test.ts`
now include `sys_organization` as a fourth mirror for endpoint, gates,
inputs, param resolvability, copy, translated bundles and served
metadata. The list-toolbar location pin stays on the three toolbar
mirrors.
- Ablation: deleting `leave_organization`'s `order: 1` through
`scripts/ablation-replace.mjs` landed (anchor 1 to 0, blob `919503b9` to
`70b55e52`). The test "sorts last, past the inline budget once the
invite leads" went red (1 failed, 4 passed). The file was restored to
the HEAD blob and `git diff HEAD` was empty.
- `pnpm --filter @objectstack/platform-objects test`: 74 files, 1162
tests passed. `typecheck`: exit 0.
`src/kernel/public-auth-features.test.ts`: 24 passed. All on
`9aa48a7e5`, after merging `origin/main`, which brought #24200 and
#24204; the bundles regenerated byte-identical.
- Local gates on `9aa48a7e5`: `node scripts/pm/dispatch-gates.mjs
--commands` derives 86 commands. All 86 exited 0, and `--ran` reports
"86 run, 0 NOT-MEASURED (a DERIVED zero)".
## Acceptance notes
- `leave_organization` folds only when the header has more than 3
visible authored actions. That is the case for an admin in multi-org:
invite, set_active, change_slug, leave. A plain member does not see the
invite, so leave stays inline as a red outline button. It is never the
primary (objectui#12764). Moving it to the overflow unconditionally
would take `component: 'action:menu'`. I did not do that: the ruling
speaks in terms of order, and cloud#2910 overrides order and variant per
its own posture (ruling item 5).
- The invite is gated on the `organization` capability, as the ruling
says, while its record-header siblings use `multiOrgEnabled`. In
single-org mode the invite is the only header action.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Eey1448sFJ5Z95gbXZh2tG)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`sys_organization` now declares an `invite_user` action at `record_header`, so an organization record page can offer "Invite User" as its primary header button. It calls `/api/v1/auth/organization/invite-member` with the record's id as `organizationId`, asks for `email` and `role` (the role picker offers only the roles the caller may grant), and is gated like the other three invite mirrors: the `organization` capability and the owner / admin / delegated_admin grades. Its `order: -1` puts it first in the header.
7
+
8
+
`leave_organization` keeps `variant: 'danger'` and now declares `order: 1`, so it sorts last among the header actions and folds into the `⋯` overflow menu instead of taking the third inline slot. Its confirmation now names what is lost: the organization's environments and members (en, zh-CN, ja-JP and es-ES).
9
+
10
+
`@objectstack/spec` books the new action in `PUBLIC_AUTH_FEATURES.organization.gatedInputs`, beside the other three invite mirrors.
confirmText: "¿Abandonar esta organización? Perderá el acceso a todos sus recursos.",
592
-
successMessage: "Ha abandonado la organización"
593
-
},
594
604
change_slug: {
595
605
label: "Cambiar slug",
596
606
description: "Renombrar el slug reescribe todos los subdominios de la plataforma para esta organización y reserva el slug anterior durante 90 días. ¿Continuar?",
597
607
successMessage: "Slug de la organización cambiado"
608
+
},
609
+
leave_organization: {
610
+
label: "Abandonar organización",
611
+
confirmText: "¿Abandonar esta organización? Perderá el acceso a sus entornos y miembros.",
0 commit comments