Skip to content

Commit f552fa8

Browse files
feat(plugin-auth): an invite answers its mail-delivery outcome, and sys_invitation gains copy_invitation_link (#27902)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DFLBEddJnGLwHChNQUrFM1f)_ domain:server#3 · os-elon-musk · session_016WV4d1FQ4kRKSX6tuBop8i Fixes #27843 Clause-②: yes (widening) Metadata-protocol: none ## 维护者速读 - 改了什么:邀请接口在成功应答里多带一个 `outcome`(`sent` 或 `link_only`),四处“邀请用户”和“重新发送邀请”按它显示不同提示;邀请列表新增“复制邀请链接”动作,返回邀请邮件里的同一条接受链接。 - 为什么改:没配置邮件的部署上,邀请后提示“已发送”,其实什么都没发出去,也没有地方拿到链接,被邀请人永远收不到(裁决 E,#27797)。 - 风险与回滚:链接是持有即可用的凭证,服务端只给邀请所在组织的 owner/admin/delegated_admin、只给待处理邀请、云托管环境一律拒绝,且从不写日志;回滚即回退本 PR,应答多出的字段对旧前端无影响。 - 先看:`packages/plugins/plugin-auth/src/invitation-link-action.ts`(五道门的顺序)、`invitation-delivery-outcome.ts`(判定规则)、`packages/platform-objects/src/identity/sys-invitation.object.ts`(新动作声明)。 - 两分钟验证:`pnpm --filter @objectstack/plugin-auth exec vitest run src/invitation-delivery-outcome.test.ts`,预期 27 项全过,其中 (a)–(d) 四组各自成组。 - 需要你做的:无,席位落地。 ## What changes - **Delivery outcome on the invite answer.** A successful `POST /api/v1/auth/organization/invite-member`, invite and resend alike, now carries a top-level `outcome: 'sent' | 'link_only'` beside the invitation row (`invitation-delivery-outcome.ts`, called from the global after hook). It is decided at call time by `AuthManager.isEmailDeliveryLive()`, the rule sign-up uses: the email service's own `isConfigured()` answer, so a wired `log` transport is `link_only`, and so are no service and a service without the member. A refusal is left untouched. The row's own columns do not move, so `{{ result.email }}` keeps reading. The private reader reaches the after hook through the two host literals (`AfterHookHost`, `AuthInstanceHost`). - **`sys_invitation.copy_invitation_link`** (`invitation-link-action.ts`), registered by `AuthPlugin.start()` through `engine.registerAction('sys_invitation', 'copy_invitation_link', …, 'com.objectstack.plugin-auth')`. It answers `{ acceptUrl }` from `AuthManager.getConsoleUrl(invitationAcceptPath(id))`. `invitationAcceptPath` is now the one spelling of `/accept-invitation/INVITATION_ID`, and `sendInvitationEmail` composes the mail's link from it too, so no second link builder exists. The handler body runs system-elevated at the action door, so it enforces the gates itself, in this order: 1. cloud-managed → `409 RESOURCE_CONFLICT`, with a new `CLOUD_MANAGED_REFUSAL_MESSAGES.invitationLink` sentence; 2. no record id → `400 MISSING_REQUIRED_FIELD`; 3. no session user → `401 UNAUTHENTICATED`; 4. the caller's `sys_member` row in the invitation's organization must carry a grade of `MEMBERSHIP_REACH.invite_member` (owner, admin, delegated_admin), else `403 PERMISSION_DENIED`. This comes before the status gate, so a caller outside the reach learns nothing about the row; 5. status other than `pending` → `409 RESOURCE_CONFLICT`. The invitation and the membership are re-read under the system context. Nothing logs the link. - **Metadata.** The four `invite_user` mirrors declare `outcomeMessages` from one constant (`invite-outcome-messages.ts`), and so does `resend_invitation`. The `link_only` copy says the invitation was created, no email went out, and to use Copy Invite Link. `successMessage` stays as the fallback. The new action is `type: 'script'`, `target: 'copy_invitation_link'`, `locations: ['list_item', 'record_header']`, with `requiresFeature: 'organization'` and `requiresMembershipReach: 'invite_member'`. Its `visible` is `features.cloudManaged != true && has(record.status) && record.status == 'pending'`. Its `resultDialog` is `format: 'text'` with the one field `acceptUrl`. - **Translations.** The generated bundles were regenerated with `node scripts/check-i18n-bundles.mjs --write`. Every new zh-CN / ja-JP / es-ES leaf is hand-translated, so the source-hash table carries no entry for them. - **Path outside the dispatched surface (seat-amended, option A):** `packages/spec/src/kernel/public-auth-features.ts`. It holds two `gatedInputs` booking lines for the new action, under `organization` and `cloudManaged`. platform-objects' feature-gate completeness guard (#2874) reds without them. No schema or key changes. `pnpm --filter @objectstack/spec check:generated`: all 14 generated artifacts up to date. ## Tests <details><summary>New pins</summary> `packages/plugins/plugin-auth/src/invitation-delivery-outcome.test.ts` uses a real `AuthManager` on the installed better-auth. Every invitation is issued, resent, canceled and accepted through the vendor's routes. The canonical origin is `https://crm.acme.test` and `uiBasePath` is `/console`. - (a) No email service, `isConfigured() === false`, and no `isConfigured` member each answer `link_only`. A configured transport answers `sent`, and the mail goes out. A resend follows the transport at call time (`link_only` → `sent` → `link_only` on one row). The unknown-role refusal carries no `outcome` and keeps `ROLE_NOT_FOUND`. - (b) `acceptUrl` equals `sendTemplate`'s `data.acceptUrl` for the same invitation, which is `https://crm.acme.test/console/accept-invitation/INVITATION_ID`. No console call carries it. - (c) The owner and a delegated admin are admitted. A plain member is refused 403. The owner of another organization is refused 403. No session is refused 401. Canceled and accepted rows are refused 409. A plain member on a canceled row gets 403 (reach before status). An unknown id gets 404. - (d) `OS_CLOUD_MANAGED=1` gets 409 with the cloud-managed sentence, even for the owner of a pending row. - `AuthPlugin.start()` registers the handler once, under `com.objectstack.plugin-auth`. `packages/platform-objects/src/identity/invitation-delivery-copy.test.ts` covers the metadata: - The mirrors and resend declare the outcome copy. `link_only` matches no `/\b(re)?sent\b/i`, and both outcomes carry `${result.email}`. - The action declaration is pinned. - The parsed `visible` is evaluated through `@objectstack/formula`. The action is offered to owner, admin and delegated_admin on a pending row. It is hidden for a plain member, for each non-pending status, for a row projected without `status`, under `cloudManaged`, and with `organization` off. - Every new leaf is translated in each locale and is not the English source. </details> <details><summary>Ablations (each restored: blob == HEAD, `git diff HEAD` empty)</summary> - `invitationDeliveryOutcome` forced to `'sent'`: 4 (a) cases red. - The reach check forced open: 3 (c) cases red (plain member, other-org owner, reach-before-status). </details> <details><summary>Local runs at e4a4c0a</summary> - `@objectstack/platform-objects` `test`: 100 files, 1432/1432 passed. `typecheck`: exit 0. - `@objectstack/plugin-auth` `test`: 204 files, 3885 passed and 10 skipped. `typecheck`: exit 0. - `@objectstack/spec`: `src/kernel/public-auth-features.test.ts` and `src/ui/action.test.ts`, 177/177 passed. `check:generated`: 14/14 up to date. </details> ## Acceptance notes - `sent` describes the transport, not the one send. With a live transport whose send then fails, the vendor's invitation callback swallows the failure (the row is already written), so the answer still says `sent`. That is the ruling's rule (`isEmailDeliveryLive()` at call time). The admin resends. - The handler enforces the invite reach, the pending status and cloud-managed. `requiresFeature: 'organization'` stays a UI gate. The organization plugin is always mounted, and invitation rows exist only under it. - A row whose `status` is still `pending` after its `expires_at` passed is answered a link, and the accept page refuses it as expired. The ruling's gate is `status == 'pending'`, and better-auth does not flip the status on expiry. - A composition that mounts the identity objects without `AuthPlugin` (`createIdentityObjectsPlugin`) declares the action with no handler. ADR-0110's boot inventory warns once there that `sys_invitation:copy_invitation_link` is unbound, and the action door answers 404. - The label is spelled `Copy Invite Link`, in the Title Case of its siblings (`Resend Invitation`, `Cancel Invitation`). The card wrote it as "Copy invite link". - The removed comment in `sendInvitationEmail` said the console's workspace Invitations page builds its own copy of this link (`${origin}${BASE_URL}accept-invitation/INVITATION_ID`). That client-side builder is the ui half's to retire (#27797). ## Verification - `dispatch-gates --commands`: 87 families derived. 86 exited 0. `pnpm check:dual-build-cjs-loads` exited 3, so it is NOT MEASURED: it needs a whole-tree build, and CI runs it. - `dispatch-gates --ran`: 87 accounted for (86 run, 1 NOT MEASURED, 0 unrun). - `pnpm lint`: exit 0. - `check-commit-card-trailers --range origin/main..HEAD`: exit 0, 4 commits. - `dispatch-gates --tier`: no path-derived mandate. The diff includes the Clause-② suspect path `packages/spec/src/**`, through the two booking lines. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (only two registry rows) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: `check:generated` reports every artifact up to date (nothing to regenerate). - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (except the 维护者速读 block, which the dispatch contract writes in Chinese) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_016WV4d1FQ4kRKSX6tuBop8i)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016WV4d1FQ4kRKSX6tuBop8i --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent fdf844c commit f552fa8

21 files changed

Lines changed: 1135 additions & 10 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-auth': patch
3+
'@objectstack/platform-objects': patch
4+
'@objectstack/spec': patch
5+
---
6+
7+
feat(plugin-auth): an invite says whether its mail was delivered, and a pending invitation's link can be copied from Setup → Invitations
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
On a deployment that does not deliver mail (the stock `log` transport, or no email service at all), inviting someone used to toast "Invitation sent to …" while nothing left the server, and nothing offered the accept link, so the invitee never arrived.
13+
14+
- `POST /api/v1/auth/organization/invite-member` (invite and resend) now answers a top-level `outcome` beside the invitation row: `sent` when the deployment delivers mail, `link_only` when it does not. It is decided at call time by the same rule sign-up uses, the email service's own `isConfigured()` answer, so a wired `log` transport is not delivery. The row's columns are unchanged.
15+
- The four `invite_user` actions (Users, Members, Invitations, the organization page) and `resend_invitation` declare `outcomeMessages`: the `link_only` message says the invitation was created and no email went out, and points to Copy Invite Link. `successMessage` stays the fallback.
16+
- New action `sys_invitation.copy_invitation_link` (Copy Invite Link, on the invitation row and record header): answers `{ acceptUrl }`, the same link the invitation mail carries, in a copyable result dialog. The link admits whoever opens it, so the server answers it only to an owner, admin or delegated admin of the invitation's organization, only for a pending invitation, and never while the environment is cloud-managed (`409 RESOURCE_CONFLICT`); it is never logged.
17+
- `PUBLIC_AUTH_FEATURES` books the new action under the `organization` and `cloudManaged` gates.

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
140140
label: "Invite User",
141141
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
142142
successMessage: "Invitation sent to {{ result.email }}",
143+
outcomeMessages: {
144+
sent: "Invitation sent to ${result.email}",
145+
link_only: "Invitation created for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
146+
},
143147
params: {
144148
role: {
145149
options: {
@@ -577,6 +581,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
577581
label: "Invite User",
578582
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
579583
successMessage: "Invitation sent to {{ result.email }}",
584+
outcomeMessages: {
585+
sent: "Invitation sent to ${result.email}",
586+
link_only: "Invitation created for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
587+
},
580588
params: {
581589
role: {
582590
options: {
@@ -664,6 +672,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
664672
label: "Invite User",
665673
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
666674
successMessage: "Invitation sent to {{ result.email }}",
675+
outcomeMessages: {
676+
sent: "Invitation sent to ${result.email}",
677+
link_only: "Invitation created for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
678+
},
667679
params: {
668680
role: {
669681
options: {
@@ -794,6 +806,10 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
794806
label: "Invite User",
795807
description: "Invite someone by email address. They join this organization with the chosen role when they accept the invitation.",
796808
successMessage: "Invitation sent to {{ result.email }}",
809+
outcomeMessages: {
810+
sent: "Invitation sent to ${result.email}",
811+
link_only: "Invitation created for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
812+
},
797813
params: {
798814
role: {
799815
options: {
@@ -813,7 +829,22 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
813829
},
814830
resend_invitation: {
815831
label: "Resend Invitation",
816-
successMessage: "Invitation resent"
832+
successMessage: "Invitation resent",
833+
outcomeMessages: {
834+
sent: "Invitation resent to ${result.email}",
835+
link_only: "Invitation renewed for ${result.email}. Mail is not configured on this server, so no email went out. Use Copy Invite Link on the invitation to share it."
836+
}
837+
},
838+
copy_invitation_link: {
839+
label: "Copy Invite Link",
840+
resultDialog: {
841+
title: "Invite link",
842+
description: "Anyone who opens this link can accept the invitation, so share it only with the person you invited. It stops working once the invitation is accepted, declined, canceled or expires.",
843+
acknowledge: "Done",
844+
fields: {
845+
acceptUrl: "Invite link"
846+
}
847+
}
817848
},
818849
accept_invitation: {
819850
label: "Accept Invitation",

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
140140
label: "Invitar usuario",
141141
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.",
142142
successMessage: "Invitación enviada a {{ result.email }}",
143+
outcomeMessages: {
144+
sent: "Invitación enviada a ${result.email}",
145+
link_only: "Invitación creada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
146+
},
143147
params: {
144148
role: {
145149
options: {
@@ -577,6 +581,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
577581
label: "Invitar usuario",
578582
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.",
579583
successMessage: "Invitación enviada a {{ result.email }}",
584+
outcomeMessages: {
585+
sent: "Invitación enviada a ${result.email}",
586+
link_only: "Invitación creada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
587+
},
580588
params: {
581589
role: {
582590
options: {
@@ -664,6 +672,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
664672
label: "Invitar usuario",
665673
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.",
666674
successMessage: "Invitación enviada a {{ result.email }}",
675+
outcomeMessages: {
676+
sent: "Invitación enviada a ${result.email}",
677+
link_only: "Invitación creada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
678+
},
667679
params: {
668680
role: {
669681
options: {
@@ -794,6 +806,10 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
794806
label: "Invitar usuario",
795807
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.",
796808
successMessage: "Invitación enviada a {{ result.email }}",
809+
outcomeMessages: {
810+
sent: "Invitación enviada a ${result.email}",
811+
link_only: "Invitación creada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
812+
},
797813
params: {
798814
role: {
799815
options: {
@@ -813,7 +829,22 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
813829
},
814830
resend_invitation: {
815831
label: "Reenviar invitación",
816-
successMessage: "Invitación reenviada"
832+
successMessage: "Invitación reenviada",
833+
outcomeMessages: {
834+
sent: "Invitación reenviada a ${result.email}",
835+
link_only: "Invitación renovada para ${result.email}. El correo no está configurado en este servidor, así que no ha salido ningún mensaje. Usa Copiar enlace de invitación en la invitación para compartirla."
836+
}
837+
},
838+
copy_invitation_link: {
839+
label: "Copiar enlace de invitación",
840+
resultDialog: {
841+
title: "Enlace de invitación",
842+
description: "Cualquiera que abra este enlace puede aceptar la invitación, así que compártelo solo con la persona que invitaste. Deja de funcionar cuando la invitación se acepta, se rechaza, se cancela o caduca.",
843+
acknowledge: "Hecho",
844+
fields: {
845+
acceptUrl: "Enlace de invitación"
846+
}
847+
}
817848
},
818849
accept_invitation: {
819850
label: "Aceptar invitación",

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
140140
label: "ユーザーを招待",
141141
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
142142
successMessage: "{{ result.email }} に招待を送信しました",
143+
outcomeMessages: {
144+
sent: "${result.email} に招待を送信しました",
145+
link_only: "${result.email} の招待を作成しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
146+
},
143147
params: {
144148
role: {
145149
options: {
@@ -577,6 +581,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
577581
label: "ユーザーを招待",
578582
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
579583
successMessage: "{{ result.email }} に招待を送信しました",
584+
outcomeMessages: {
585+
sent: "${result.email} に招待を送信しました",
586+
link_only: "${result.email} の招待を作成しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
587+
},
580588
params: {
581589
role: {
582590
options: {
@@ -664,6 +672,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
664672
label: "ユーザーを招待",
665673
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
666674
successMessage: "{{ result.email }} に招待を送信しました",
675+
outcomeMessages: {
676+
sent: "${result.email} に招待を送信しました",
677+
link_only: "${result.email} の招待を作成しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
678+
},
667679
params: {
668680
role: {
669681
options: {
@@ -794,6 +806,10 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
794806
label: "ユーザーを招待",
795807
description: "メールアドレスを指定して招待します。招待が承諾されると、選択したロールでこの組織に参加します。",
796808
successMessage: "{{ result.email }} に招待を送信しました",
809+
outcomeMessages: {
810+
sent: "${result.email} に招待を送信しました",
811+
link_only: "${result.email} の招待を作成しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
812+
},
797813
params: {
798814
role: {
799815
options: {
@@ -813,7 +829,22 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
813829
},
814830
resend_invitation: {
815831
label: "招待を再送",
816-
successMessage: "招待を再送しました"
832+
successMessage: "招待を再送しました",
833+
outcomeMessages: {
834+
sent: "${result.email} に招待を再送しました",
835+
link_only: "${result.email} の招待を更新しました。このサーバーではメールが設定されていないため、メールは配信されていません。招待の「招待リンクをコピー」で共有してください。"
836+
}
837+
},
838+
copy_invitation_link: {
839+
label: "招待リンクをコピー",
840+
resultDialog: {
841+
title: "招待リンク",
842+
description: "このリンクを開いた人は誰でも招待を承諾できるため、招待した本人にのみ共有してください。招待が承諾、辞退、キャンセルされるか、期限が切れると、リンクは使えなくなります。",
843+
acknowledge: "完了",
844+
fields: {
845+
acceptUrl: "招待リンク"
846+
}
847+
}
817848
},
818849
accept_invitation: {
819850
label: "招待を受諾",

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
140140
label: "邀请用户",
141141
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
142142
successMessage: "已向 {{ result.email }} 发送邀请",
143+
outcomeMessages: {
144+
sent: "已向 ${result.email} 发送邀请",
145+
link_only: "已为 ${result.email} 创建邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
146+
},
143147
params: {
144148
role: {
145149
options: {
@@ -577,6 +581,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
577581
label: "邀请用户",
578582
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
579583
successMessage: "已向 {{ result.email }} 发送邀请",
584+
outcomeMessages: {
585+
sent: "已向 ${result.email} 发送邀请",
586+
link_only: "已为 ${result.email} 创建邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
587+
},
580588
params: {
581589
role: {
582590
options: {
@@ -664,6 +672,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
664672
label: "邀请用户",
665673
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
666674
successMessage: "已向 {{ result.email }} 发送邀请",
675+
outcomeMessages: {
676+
sent: "已向 ${result.email} 发送邀请",
677+
link_only: "已为 ${result.email} 创建邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
678+
},
667679
params: {
668680
role: {
669681
options: {
@@ -794,6 +806,10 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
794806
label: "邀请用户",
795807
description: "按电子邮件地址邀请他人。对方接受邀请后,即以所选角色加入此组织。",
796808
successMessage: "已向 {{ result.email }} 发送邀请",
809+
outcomeMessages: {
810+
sent: "已向 ${result.email} 发送邀请",
811+
link_only: "已为 ${result.email} 创建邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
812+
},
797813
params: {
798814
role: {
799815
options: {
@@ -813,7 +829,22 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
813829
},
814830
resend_invitation: {
815831
label: "重新发送邀请",
816-
successMessage: "邀请已重新发送"
832+
successMessage: "邀请已重新发送",
833+
outcomeMessages: {
834+
sent: "已向 ${result.email} 重新发送邀请",
835+
link_only: "已为 ${result.email} 续期邀请。此服务器未配置邮件,因此没有发出任何邮件。请在该邀请上使用“复制邀请链接”分享给对方。"
836+
}
837+
},
838+
copy_invitation_link: {
839+
label: "复制邀请链接",
840+
resultDialog: {
841+
title: "邀请链接",
842+
description: "任何打开此链接的人都可以接受该邀请,请只分享给您邀请的人。邀请被接受、拒绝、取消或过期后,链接即失效。",
843+
acknowledge: "完成",
844+
fields: {
845+
acceptUrl: "邀请链接"
846+
}
847+
}
817848
},
818849
accept_invitation: {
819850
label: "接受邀请",

0 commit comments

Comments
 (0)