Skip to content

Commit 0b7ba4d

Browse files
fix(platform-objects,plugin-approvals): invite_user and approval_approve declare a description; Invite User names the invitee (#22230)
Fixes #22182 Clause-②: no ## What changes - **`invite_user`** is declared three times, on `sys_user`, `sys_invitation` and `sys_member`. Each declaration now has a `description`: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation." The console shows it as the parameter dialog's subtitle. Before, the dialog showed the generic "Please provide the required information to continue." - **`invite_user`'s `successMessage`** is now `Invitation sent to ${result.email}`. Before, it was "Invitation sent". - **`approval_approve`** declares a `description` the way `approval_reject` beside it does: "Approve this request? Your approval is recorded, and the request moves on once this step has the approvals it requires." The wording says "once this step has the approvals it requires" because one approval finalizes a step only under `first_response` or an override. Under `unanimous`, `quorum` and `per_group` it does not. - **Translation bundles.** I regenerated them with `node scripts/check-i18n-bundles.mjs --write`, per package. The extractor seeds each new `description` leaf with the English source in zh-CN, ja-JP and es-ES. It also keeps the old translated success message, because merge mode keeps any non-empty translated value. I translated those leaves by hand and ran `--write` a second time, so the provenance companions (`*.source-hashes.generated.ts`) dropped the copied-from-source entries the first run had recorded. Net: those companions are byte-unchanged against main. Every translated success message keeps the `${result.email}` token. - **Pins:** - `packages/platform-objects/src/identity/invite-user-action-copy.test.ts` (34 cases): the three declarations, their parity, the four bundles, and the served object metadata through `translateMetadataDocument('object', …)` over `SetupAppTranslations`. - `packages/plugins/plugin-approvals/src/translations/approve-decision-question.test.ts` (6 cases): the same shape over `ApprovalsTranslations`. - **Changesets:** one `patch` each for `@objectstack/platform-objects` and `@objectstack/plugin-approvals`. ## The `${result.email}` choice was measured The card did not measure what `invite-member` answers, so I booted the door in-repo and read the live answer. I used `pnpm dev:crm -- --fresh` (before the change) and `pnpm dev -- --fresh` (showcase, after the change), signed in as the seeded admin, and sent the body the console's api handler sends: `POST /api/v1/auth/organization/invite-member` with `email`, `role` and `organizationId`. Both runs answered HTTP 200 with the bare invitation row: {"organizationId":"org_…","email":"grace.hopper@example.com","role":"member","teamId":null,"status":"pending","expiresAt":"…","createdAt":"…","inviterId":"…","businessUnitId":null,"positions":null,"id":"…"} The answer has no `data` key, and its top-level keys are not the legacy action envelope. So, at the objectui pin `a58626c8`: 1. `useConsoleActionRuntime`'s api handler passes the body through as `result.data`. 2. `readActionPayload` returns it unchanged. 3. `composeSuccessMessage` fills `${result.email}` from it. Its scope is `{ result: payload }` only. The runner has no submitted-parameter scope for success copy, so `${result.*}` is the only route. The address comes back lowercased: better-auth stores it that way. ## Served metadata, before and after `GET /api/v1/meta/objects/NAME` with `Accept-Language` is the object-metadata read the console uses. These are the `item.actions[]` entries. | object · action · locale | before (main `7d7943dd`) | after (this branch, dist rebuilt) | | --- | --- | --- | | sys_user · invite_user · en | no description · "Invitation sent" | description "Invite someone by email address. …" · "Invitation sent to ${result.email}" | | sys_user · invite_user · zh-CN | no description · "邀请已发送" | description "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。" · "已向 ${result.email} 发送邀请" | | sys_member / sys_invitation · invite_user · zh-CN | no description · "邀请已发送" | same as sys_user | | sys_approval_request · approval_approve · en | no description | "Approve this request? …" | | sys_approval_request · approval_approve · zh-CN | no description | "通过该请求?你的审批将被记录,此步骤获得所需的审批后,请求即继续流转。" | I ran the console's success-copy composition step for step over the captured answer and the served `successMessage`: - en: "Invitation sent to grace.hopper@example.com" - zh-CN: "已向 grace.hopper@example.com 发送邀请" ja-JP and es-ES are pinned at the bundle level only. Showcase declares `supportedLocales: ['en', 'zh-CN']`, so those two locales serve English on every action there. The control is `ban_user`'s label, which reads "Ban User" under ja-JP. ## Tests and gates (all at HEAD `4ccbef7b22`) - **Package tests:** - `pnpm --filter @objectstack/platform-objects test`: 65 files, 1062 tests, all passed. - `pnpm --filter @objectstack/plugin-approvals test`: 62 files, 905 tests, all passed. - **Typecheck:** `pnpm --filter … typecheck` exited 0 for both packages. Each run includes `check:test-typecheck`, and `--listFiles` shows both new test files are in the test `tsc` program. - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 65 commands, and I ran all 65, plus `check:i18n-coverage` and `check:i18n-walk-parity`. All 67 exited 0. `--ran` printed "65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED". The verdict lines: - `check:i18n`: "OK (9 package(s) — all bundles in sync, no undeclared authoring keys)" - `check:i18n-stale-fill`: "OK (10 bundle set(s) — no new stale fills, 0 baselined)" - `check:i18n-coverage`: "OK (13 config(s), 621 baselined untranslated string(s), none new)" - `check:i18n-walk-parity`: "11 declared group(s), 9 walked, 2 exempted" - `check:nul-bytes`: OK - **Lint, narrowed:** I ran `eslint --no-inline-config --format json` over the 14 touched `.ts` files, with 0 ignored by the config's own matching. Result: 0 errors and 0 warnings. `eslint.config.mjs` never enables type-aware linting, so this diff cannot move the verdict on any untouched file. The full `pnpm lint` is left to CI. ## Ablation (fix committed first; every leg through `scripts/ablation-replace.mjs` WRAP mode) The subjects resolve to `src/` through relative imports, so no rebuild was involved. Each anchor hit as declared, and the blob changed on disk. Each restore was proven against the HEAD blob with `git diff HEAD` empty, and once more for all four paths at the end. | leg | mutation | pin result | | --- | --- | --- | | A | sys_member `invite_user.description` set to `undefined` | 4 failed of 34 (declares a description; mirrors agree; en carries / serves the source for sys_member) | | C | zh-CN `invite_user.successMessage` reverted to the extractor-kept "邀请已发送" (3 hits) | 6 failed of 34 (token kept ×3; zh-CN served ×3) | | D | `approval_approve.description` set to `undefined` | 2 failed of 6 | | E | zh-CN `approval_approve.description` set to the English source the extractor seeds | 2 failed of 6 (translated; served zh-CN) | ## Acceptance notes - `docs/qa/platform-checklist/areas/ux-conventions.json` (lines 27, 39 and 71) and `docs/qa/platform-checklist/FOLLOW-UPS.md` (line 686) quote the old `successMessage 'Invitation sent'`. They describe the toast that objectstack-ai/objectui#11817 measured, and they are now one literal behind. I left them alone (outside this card's file fence). - PR #22166 also edits `platform-objects`' generated bundles, for other objects. Both are generator-owned (`merge=os-regen`). Whichever lands second merges `main` and regenerates. --- _Generated by [Claude Code](https://claude.ai/code/session_01WkL6Eijt432S1Y7ekb6ovQ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 31cd210 commit 0b7ba4d

16 files changed

Lines changed: 290 additions & 15 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
---
4+
5+
fix(plugin-approvals): Approve asks its decision question in the dialog, as Reject does
6+
7+
Clause-②: no
8+
9+
`approval_approve` on `sys_approval_request` declares a `description`: "Approve this request? Your approval is recorded, and the request moves on once this step has the approvals it requires." The console shows it as the parameter dialog's subtitle, where a generic "Please provide the required information to continue." showed before; `approval_reject` beside it already declared its question this way. The zh-CN, ja-JP and es-ES bundles carry a translation of it.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): Invite User explains itself in its dialog and names the invitee in its success message
6+
7+
Clause-②: no
8+
9+
- **The parameter dialog.** `invite_user` on `sys_user`, `sys_invitation` and `sys_member` declares a `description`: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation." The console shows it as the dialog's subtitle, where a generic "Please provide the required information to continue." showed before.
10+
- **The success toast.** `successMessage` is now `Invitation sent to ${result.email}`. `POST /api/v1/auth/organization/invite-member` answers the invitation row, and its top-level `email` is the invitee's address as stored (lowercased), so the toast reads "Invitation sent to ada@example.com" where it read "Invitation sent".
11+
- **Translations.** The zh-CN, ja-JP and es-ES bundles carry the new description and a success message that keeps the `${result.email}` token, so every locale names the invitee.

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
141141
_actions: {
142142
invite_user: {
143143
label: "Invite User",
144-
successMessage: "Invitation sent"
144+
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
145+
successMessage: "Invitation sent to ${result.email}"
145146
},
146147
ban_user: {
147148
label: "Ban User",
@@ -633,7 +634,8 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
633634
_actions: {
634635
invite_user: {
635636
label: "Invite User",
636-
successMessage: "Invitation sent"
637+
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
638+
successMessage: "Invitation sent to ${result.email}"
637639
},
638640
add_member: {
639641
label: "Add Member",
@@ -734,7 +736,8 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
734736
_actions: {
735737
invite_user: {
736738
label: "Invite User",
737-
successMessage: "Invitation sent"
739+
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
740+
successMessage: "Invitation sent to ${result.email}"
738741
},
739742
cancel_invitation: {
740743
label: "Cancel Invitation",

‎packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
141141
_actions: {
142142
invite_user: {
143143
label: "Invitar usuario",
144-
successMessage: "Invitación enviada"
144+
description: "Invita a alguien por su dirección de correo electrónico. Se une a esta organización con el rol elegido cuando acepta la invitación.",
145+
successMessage: "Invitación enviada a ${result.email}"
145146
},
146147
ban_user: {
147148
label: "Bloquear usuario",
@@ -633,7 +634,8 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
633634
_actions: {
634635
invite_user: {
635636
label: "Invitar usuario",
636-
successMessage: "Invitación enviada"
637+
description: "Invita a alguien por su dirección de correo electrónico. Se une a esta organización con el rol elegido cuando acepta la invitación.",
638+
successMessage: "Invitación enviada a ${result.email}"
637639
},
638640
add_member: {
639641
label: "Añadir miembro",
@@ -734,7 +736,8 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
734736
_actions: {
735737
invite_user: {
736738
label: "Invitar usuario",
737-
successMessage: "Invitación enviada"
739+
description: "Invita a alguien por su dirección de correo electrónico. Se une a esta organización con el rol elegido cuando acepta la invitación.",
740+
successMessage: "Invitación enviada a ${result.email}"
738741
},
739742
cancel_invitation: {
740743
label: "Cancelar invitación",

‎packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
141141
_actions: {
142142
invite_user: {
143143
label: "ユーザーを招待",
144-
successMessage: "招待を送信しました"
144+
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
145+
successMessage: "${result.email} に招待を送信しました"
145146
},
146147
ban_user: {
147148
label: "利用停止",
@@ -633,7 +634,8 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
633634
_actions: {
634635
invite_user: {
635636
label: "ユーザーを招待",
636-
successMessage: "招待を送信しました"
637+
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
638+
successMessage: "${result.email} に招待を送信しました"
637639
},
638640
add_member: {
639641
label: "メンバーを追加",
@@ -734,7 +736,8 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
734736
_actions: {
735737
invite_user: {
736738
label: "ユーザーを招待",
737-
successMessage: "招待を送信しました"
739+
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
740+
successMessage: "${result.email} に招待を送信しました"
738741
},
739742
cancel_invitation: {
740743
label: "招待をキャンセル",

‎packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
141141
_actions: {
142142
invite_user: {
143143
label: "邀请用户",
144-
successMessage: "邀请已发送"
144+
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
145+
successMessage: "已向 ${result.email} 发送邀请"
145146
},
146147
ban_user: {
147148
label: "封禁用户",
@@ -633,7 +634,8 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
633634
_actions: {
634635
invite_user: {
635636
label: "邀请用户",
636-
successMessage: "邀请已发送"
637+
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
638+
successMessage: "已向 ${result.email} 发送邀请"
637639
},
638640
add_member: {
639641
label: "添加成员",
@@ -734,7 +736,8 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
734736
_actions: {
735737
invite_user: {
736738
label: "邀请用户",
737-
successMessage: "邀请已发送"
739+
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
740+
successMessage: "已向 ${result.email} 发送邀请"
738741
},
739742
cancel_invitation: {
740743
label: "取消邀请",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// `invite_user`'s user-facing copy — the parameter dialog's subtitle and the
4+
// success toast — on all three declaration sites (sys_user, sys_invitation,
5+
// sys_member).
6+
//
7+
// What the console does with each key, measured on the pinned objectui:
8+
//
9+
// - `description` is the parameter dialog's subtitle. With none declared the
10+
// dialog falls back to a generic "Please provide the required information
11+
// to continue.", which says nothing about what Confirm will do.
12+
// - `successMessage` is the toast after Confirm. On a `type: 'api'` action the
13+
// runner fills `${result.*}` from the answer (ActionSchema.successMessage).
14+
// `/api/v1/auth/organization/invite-member` answers the bare invitation row,
15+
// whose top-level `email` is the invitee's address, so `${result.email}`
16+
// names the person invited. A copy without the token names nobody.
17+
//
18+
// The translated bundles are pinned as well, because the extractor cannot keep
19+
// them honest by itself: `--fill=default` seeds a NEW key (the description)
20+
// with the English source in every locale, and merge mode keeps every existing
21+
// translated value forever, so a revised `successMessage` would go on serving
22+
// the old, invitee-less sentence. Both states are "in sync" to `check:i18n`.
23+
import { describe, expect, it } from 'vitest';
24+
import { translateMetadataDocument } from '@objectstack/spec/system';
25+
import { SetupAppTranslations } from '../apps/translations/setup.translation.js';
26+
import { enObjects } from '../apps/translations/en.objects.generated.js';
27+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
28+
import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js';
29+
import { esESObjects } from '../apps/translations/es-ES.objects.generated.js';
30+
import { SysInvitation } from './sys-invitation.object.js';
31+
import { SysMember } from './sys-member.object.js';
32+
import { SysUser } from './sys-user.object.js';
33+
34+
interface CopyAction {
35+
name?: string;
36+
description?: unknown;
37+
successMessage?: unknown;
38+
}
39+
40+
/** The token the console runner fills from the invite-member answer. */
41+
const INVITEE = '${result.email}';
42+
43+
const MIRRORS: Array<[string, unknown]> = [
44+
['sys_user', SysUser],
45+
['sys_invitation', SysInvitation],
46+
['sys_member', SysMember],
47+
];
48+
49+
const TRANSLATED = [
50+
['zh-CN', zhCNObjects],
51+
['ja-JP', jaJPObjects],
52+
['es-ES', esESObjects],
53+
] as const;
54+
55+
function invite(object: unknown): CopyAction {
56+
const found = ((object as { actions?: CopyAction[] }).actions ?? []).find(
57+
(a) => a.name === 'invite_user',
58+
);
59+
expect(found, 'invite_user is declared').toBeDefined();
60+
return found as CopyAction;
61+
}
62+
63+
const bundleNode = (bundle: unknown, object: string): CopyAction | undefined =>
64+
(bundle as Record<string, { _actions?: Record<string, CopyAction> }>)?.[object]?._actions
65+
?.invite_user;
66+
67+
describe('invite_user — declared copy', () => {
68+
it.each(MIRRORS)('%s declares a description for the parameter dialog', (_name, object) => {
69+
const { description } = invite(object);
70+
expect(typeof description).toBe('string');
71+
expect((description as string).trim()).not.toBe('');
72+
});
73+
74+
it.each(MIRRORS)('%s names the invitee in its success message', (_name, object) => {
75+
expect(invite(object).successMessage).toContain(INVITEE);
76+
});
77+
78+
it('the three mirrors say the same thing', () => {
79+
// One action, three declaration sites: compared to EACH OTHER, so a mirror
80+
// edited alone goes red without any literal copied into this file.
81+
const [first, ...rest] = MIRRORS.map(([, object]) => invite(object));
82+
for (const other of rest) {
83+
expect(other.description).toBe(first.description);
84+
expect(other.successMessage).toBe(first.successMessage);
85+
}
86+
});
87+
});
88+
89+
describe('invite_user — translated bundles follow the source', () => {
90+
it.each(MIRRORS)('en carries the source copy for %s', (name, object) => {
91+
const node = bundleNode(enObjects, name);
92+
expect(node?.description).toBe(invite(object).description);
93+
expect(node?.successMessage).toBe(invite(object).successMessage);
94+
});
95+
96+
for (const [locale, bundle] of TRANSLATED) {
97+
it.each(MIRRORS)(`${locale} translates the description for %s`, (name, object) => {
98+
const translated = bundleNode(bundle, name)?.description;
99+
expect(translated, `${locale} ${name}.invite_user.description missing`).toBeTruthy();
100+
expect(
101+
translated,
102+
`${locale} ${name}.invite_user.description is the English source the extractor seeds a new key with`,
103+
).not.toBe(invite(object).description);
104+
});
105+
106+
it.each(MIRRORS)(`${locale} keeps the invitee token in the success message for %s`, (name) => {
107+
// The old translation ("invitation sent") survives every re-extract in
108+
// merge mode; only the token proves the leaf was revised with the source.
109+
expect(bundleNode(bundle, name)?.successMessage).toContain(INVITEE);
110+
});
111+
}
112+
});
113+
114+
describe('invite_user — the served object metadata', () => {
115+
// The object-metadata read the console uses localizes through
116+
// `translateMetadataDocument('object', …)` over the i18n service's bundle;
117+
// `SetupAppTranslations` is the bundle this package hands that service.
118+
const served = (object: unknown, locale: string): CopyAction => {
119+
const doc = translateMetadataDocument('object', object, SetupAppTranslations, { locale });
120+
return invite(doc);
121+
};
122+
123+
it.each(MIRRORS)('zh-CN serves the translated description for %s', (name, object) => {
124+
const zh = served(object, 'zh-CN');
125+
expect(zh.description).toBe(bundleNode(zhCNObjects, name)?.description);
126+
expect(zh.description).not.toBe(invite(object).description);
127+
expect(zh.successMessage).toContain(INVITEE);
128+
});
129+
130+
it.each(MIRRORS)('en serves the declared copy for %s', (_name, object) => {
131+
const en = served(object, 'en');
132+
expect(en.description).toBe(invite(object).description);
133+
expect(en.successMessage).toBe(invite(object).successMessage);
134+
});
135+
});

‎packages/platform-objects/src/identity/sys-invitation.object.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export const SysInvitation = ObjectSchema.create({
5454
{
5555
name: 'invite_user',
5656
label: 'Invite User',
57+
// Same wording as the sys_user / sys_member mirrors.
58+
description:
59+
'Invite someone by email address. They join this organization with the chosen role when they accept the invitation.',
5760
icon: 'user-plus',
5861
variant: 'primary',
5962
locations: ['list_toolbar'],
@@ -67,7 +70,9 @@ export const SysInvitation = ObjectSchema.create({
6770
// unreachable in single-org anyway (no invitation rows exist).
6871
requiresFeature: 'organization',
6972
requiresMembershipReach: 'invite_member',
70-
successMessage: 'Invitation sent',
73+
// `${result.email}`: the invitee's address from the invitation row
74+
// invite-member answers (see sys_user's mirror).
75+
successMessage: 'Invitation sent to ${result.email}',
7176
refreshAfter: true,
7277
params: [
7378
{ field: 'email', required: true },

‎packages/platform-objects/src/identity/sys-member.object.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ export const SysMember = ObjectSchema.create({
9090
// BUILTIN_MEMBERSHIP_ROLE_OPTIONS constant sys_invitation reads.
9191
name: 'invite_user',
9292
label: 'Invite User',
93+
// Same wording as the sys_user / sys_invitation mirrors.
94+
description:
95+
'Invite someone by email address. They join this organization with the chosen role when they accept the invitation.',
9396
icon: 'user-plus',
9497
variant: 'primary',
9598
locations: ['list_toolbar'],
@@ -102,7 +105,9 @@ export const SysMember = ObjectSchema.create({
102105
// ...and the grades that reach the endpoint (ADR-0108 D1): owner,
103106
// admin and delegated_admin — a plain member is not offered it.
104107
requiresMembershipReach: 'invite_member',
105-
successMessage: 'Invitation sent',
108+
// `${result.email}`: the invitee's address from the invitation row
109+
// invite-member answers (see sys_user's mirror).
110+
successMessage: 'Invitation sent to ${result.email}',
106111
refreshAfter: true,
107112
params: [
108113
{ field: 'email', objectOverride: 'sys_invitation', required: true },

‎packages/platform-objects/src/identity/sys-user.object.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export const SysUser = ObjectSchema.create({
8181
{
8282
name: 'invite_user',
8383
label: 'Invite User',
84+
// The parameter dialog's subtitle. Same wording on all three mirrors
85+
// (sys_member, sys_invitation) so one translation serves each.
86+
description:
87+
'Invite someone by email address. They join this organization with the chosen role when they accept the invitation.',
8488
icon: 'user-plus',
8589
variant: 'primary',
8690
locations: ['list_toolbar'],
@@ -95,7 +99,9 @@ export const SysUser = ObjectSchema.create({
9599
// sys_user CRUD.
96100
requiresFeature: 'organization',
97101
requiresMembershipReach: 'invite_member',
98-
successMessage: 'Invitation sent',
102+
// `${result.email}`: invite-member answers the bare invitation row,
103+
// whose top-level `email` is the invitee's address as stored (lowercased).
104+
successMessage: 'Invitation sent to ${result.email}',
99105
refreshAfter: true,
100106
params: [
101107
{ field: 'email', required: true },

0 commit comments

Comments
 (0)