Skip to content

Commit 5d2c5a1

Browse files
fix(platform-objects): Create User result dialog reads true for a typed password (#25081)
Fixes #24936 Clause-②: no (text only: the create-user result dialog's sentence and button; no param, response, status, outcome or export moves) ## What changed `sys_user.create_user`'s result dialog said "Copy the temporary password now — it is shown only once and never stored." with an "I have saved this password" button. It said that in both outcomes. The endpoint (`plugin-auth` `admin-user-endpoints.ts`, `...(resolved.generated ? { temporaryPassword } : {})`) returns `temporaryPassword` only when it generated one. The dialog skips a field whose path is absent from the response, so after a typed password it showed the email alone and still asked the admin to save a password. - `packages/platform-objects/src/identity/sys-user.object.ts`: the `create_user` `resultDialog` now reads **"If a temporary password was generated, copy it now — it is shown only once and never stored."** with a **"Done"** button. That is the same sentence and button as `set_user_password` in the same file, whose dialog has the same two outcomes. The title, params, `bodyExtra`, handler, response and dialog fields are unchanged. - `en.objects.generated.ts`: regenerated with `node scripts/check-i18n-bundles.mjs --write` (exit 0), not edited by hand. - `zh-CN` / `ja-JP` / `es-ES` `objects.generated.ts`: the two hand-written leaves now reuse `set_user_password`'s existing translations (如果生成了临时密码… / 完成, 一時パスワードが生成された場合は… / 完了, Si se generó una contraseña temporal… / Hecho). The merge-mode regeneration kept them. None of these leaves has a source-hash entry. - New pin `packages/platform-objects/src/identity/create-user-result-dialog-copy.test.ts`. It checks the source sentence and button (the button must not mention a saved password), the three dialog fields, and, in all four locales, that the `create_user` dialog's description and button equal `set_user_password`'s. - Changeset: `patch` for `@objectstack/platform-objects`. ## Wording choice `ActionSchema.resultDialog` has no per-outcome copy, so one sentence must hold for both outcomes. I reused `set_user_password`'s sentence and button rather than writing new copy: - **Real need:** the typed case shows only the email. "Done" claims nothing about a password, and the conditional sentence is true in both cases. - **Long-term fit:** one hedged sentence for the two dialogs that share this response shape. No spec key for per-outcome copy is added; that is out of scope and `domain:spec`'s. - **Hard for an AI to get wrong:** the pin ties both dialogs together in every locale, so they cannot drift apart without a red test. - **No scope growth:** text only, and the translations already exist in the catalog. ## Pins and fixtures - `packages/cli/test/i18n-extract.test.ts` has its own `set_password` fixture with different text ("…shown only once."). It does not quote this object and does not move. - `packages/spec/src/system/i18n-resolver.test.ts` has its own inline `createUser` object and inline zh-CN bundle. It imports nothing from `platform-objects` and does not move. - No pin in another lane moved. ## Verification (head `4fdd4999b`, base `origin/main` `2b1382a5b`; `origin/main` re-fetched before opening and already merged, no new commits) - `pnpm exec turbo run build --concurrency=2` (full build, under the verify lock): VERDICT command-exit 0. - `pnpm --filter @objectstack/platform-objects typecheck`: exit 0. - `pnpm --filter @objectstack/platform-objects test`: exit 0, Test Files 84 passed (84), Tests 1243 passed (1243). - `pnpm --filter @objectstack/core test:repo`: exit 0, Test Files 6 passed (6), Tests 82 passed (82). - `node scripts/check-i18n-bundles.mjs --write`: exit 0 after building its prerequisite closure. The first run exited 3 (CLI not built). - Ablation 1 (source): `scripts/ablation-replace.mjs` put the old sentence and button back in `sys-user.object.ts` (anchor 1 → 0, blob `bc6b1792` → `3f5199d7`). 2 of the 4 pin tests went red ("the source sentence is conditional…" and "the button claims no saved password…"). Restored: blob == HEAD `bc6b1792`, `git diff HEAD` empty. - Ablation 2 (locale): put the old zh-CN leaves back (blob `c6d8953c` → `5326f33a`). 1 of 4 went red ("every locale says the same thing as set_user_password…"). Restored: blob == HEAD, `git diff HEAD` empty. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 66 families. All 66 were run with each exit code captured before any pipe. 65 exited 0 on the first run. `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET, no `dist/`), was re-run after the full build, and exited 0. `--ran` reconciliation: "66 derived famil(ies) accounted for — 66 run, 0 NOT-MEASURED (a DERIVED zero — all 66 recorded an exit code and none of them is 3)". ## Acceptance notes - I did not check objectui's dialog renderer locally, because objectui is not in this container. That it skips an absent `temporaryPassword` comes from the card's seat review and the existing code comment on the `fields` list. The renderer is unchanged here. --- _Generated by [Claude Code](https://claude.ai/code/session_01MZw2Leqg5M8ZH9pKpMh9jS)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a39389e commit 5d2c5a1

7 files changed

Lines changed: 92 additions & 10 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the Create User result dialog no longer tells an admin who typed the password to copy a temporary one
6+
7+
Clause-②: no (text only: the create-user result dialog's sentence and button; no param, response, status, outcome or export moves)
8+
9+
`sys_user.create_user`'s result dialog said "Copy the temporary password now — it is shown only once and never stored." with an "I have saved this password" button, in both outcomes. The endpoint returns `temporaryPassword` only when it generated one, so after a typed password the dialog showed the email alone and asked the admin to save a password it never showed. The dialog now reads "If a temporary password was generated, copy it now — it is shown only once and never stored." with a "Done" button — the same sentence and button as `set_user_password`, true for a typed and for a generated password. The zh-CN, ja-JP and es-ES bundles carry the same change, reusing `set_user_password`'s existing translations.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
189189
},
190190
resultDialog: {
191191
title: "User Created",
192-
description: "Copy the temporary password now — it is shown only once and never stored.",
193-
acknowledge: "I have saved this password",
192+
description: "If a temporary password was generated, copy it now — it is shown only once and never stored.",
193+
acknowledge: "Done",
194194
fields: {
195195
"user.email": "Email",
196196
"user.phoneNumber": "Phone Number",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
189189
},
190190
resultDialog: {
191191
title: "Usuario creado",
192-
description: "Copie la contraseña temporal ahora: se muestra una sola vez y no se almacena.",
193-
acknowledge: "He guardado esta contraseña",
192+
description: "Si se generó una contraseña temporal, cópiela ahora: se muestra una sola vez y no se almacena.",
193+
acknowledge: "Hecho",
194194
fields: {
195195
"user.email": "Correo electrónico",
196196
"user.phoneNumber": "Número de teléfono",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
189189
},
190190
resultDialog: {
191191
title: "ユーザーを作成しました",
192-
description: "一時パスワードを今すぐコピーしてください。表示は一度きりで、保存されません。",
193-
acknowledge: "パスワードを保存しました",
192+
description: "一時パスワードが生成された場合は今すぐコピーしてください。表示は一度きりで、保存されません。",
193+
acknowledge: "完了",
194194
fields: {
195195
"user.email": "メールアドレス",
196196
"user.phoneNumber": "電話番号",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
189189
},
190190
resultDialog: {
191191
title: "用户已创建",
192-
description: "请立即复制临时密码——它只显示一次,不会被保存。",
193-
acknowledge: "我已保存该密码",
192+
description: "如果生成了临时密码,请立即复制——它只显示一次,不会被保存。",
193+
acknowledge: "完成",
194194
fields: {
195195
"user.email": "邮箱",
196196
"user.phoneNumber": "手机号",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* `sys_user.create_user` — the result dialog reads true whether the admin
5+
* typed the password or left it empty to generate one.
6+
*
7+
* The endpoint (`plugin-auth`, `admin-user-endpoints.ts`) returns
8+
* `temporaryPassword` only when it minted one, and the dialog skips a field
9+
* whose path is absent from the response. So after a typed password the
10+
* dialog shows the email alone; a sentence ordering the admin to "copy the
11+
* temporary password now" beside an "I have saved this password" button then
12+
* describes a password that was never shown. `ActionSchema.resultDialog` has
13+
* no per-outcome copy, so one sentence must hold for both outcomes — the
14+
* hedged shape `set_user_password` in the same file already uses.
15+
*
16+
* Pinned in the source and in all four shipped locales: the translated leaves
17+
* are hand-written and carry no source-hash entry, so a stale translation of
18+
* an edited source is not flagged anywhere else.
19+
*/
20+
21+
import { describe, expect, it } from 'vitest';
22+
import { SysUser } from './index.js';
23+
import { enObjects } from '../apps/translations/en.objects.generated.js';
24+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
25+
import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js';
26+
import { esESObjects } from '../apps/translations/es-ES.objects.generated.js';
27+
28+
const action = (name: string) => (SysUser.actions ?? []).find((a) => a.name === name);
29+
const dialog = (bundle: any, name: string) => bundle?.sys_user?._actions?.[name]?.resultDialog;
30+
31+
const LOCALES: readonly (readonly [string, any])[] = [
32+
['en', enObjects],
33+
['zh-CN', zhCNObjects],
34+
['ja-JP', jaJPObjects],
35+
['es-ES', esESObjects],
36+
];
37+
38+
describe('create_user — the result dialog holds for a typed and a generated password', () => {
39+
it('the source sentence is conditional on a password having been generated', () => {
40+
const rd = action('create_user')?.resultDialog;
41+
expect(rd, 'create_user declares a resultDialog').toBeDefined();
42+
expect(rd?.description).toBe(
43+
'If a temporary password was generated, copy it now — it is shown only once and never stored.',
44+
);
45+
});
46+
47+
it('the button claims no saved password, since a typed one shows none', () => {
48+
const ack = action('create_user')?.resultDialog?.acknowledge ?? '';
49+
expect(ack).toBe('Done');
50+
expect(ack).not.toMatch(/saved|password/i);
51+
});
52+
53+
it('the temporary password stays a conditional field beside the email', () => {
54+
expect(action('create_user')?.resultDialog?.fields?.map((f) => [f.path, f.format])).toEqual([
55+
['user.email', 'text'],
56+
['user.phoneNumber', 'text'],
57+
['temporaryPassword', 'secret'],
58+
]);
59+
});
60+
61+
it('every locale says the same thing as set_user_password, whose dialog has the same two outcomes', () => {
62+
for (const [locale, bundle] of LOCALES) {
63+
const created = dialog(bundle, 'create_user');
64+
const updated = dialog(bundle, 'set_user_password');
65+
expect(created?.description, `${locale} create_user description`).toBeTruthy();
66+
expect(created.description, `${locale} description`).toBe(updated?.description);
67+
expect(created.acknowledge, `${locale} acknowledge`).toBe(updated?.acknowledge);
68+
}
69+
});
70+
});

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,9 +281,12 @@ export const SysUser = ObjectSchema.create({
281281
],
282282
resultDialog: {
283283
title: 'User Created',
284+
// Hedged for both outcomes: a typed password returns no
285+
// `temporaryPassword`, so the dialog must not claim one to copy.
286+
// Same sentence and button as `set_user_password` below.
284287
description:
285-
'Copy the temporary password now — it is shown only once and never stored.',
286-
acknowledge: 'I have saved this password',
288+
'If a temporary password was generated, copy it now — it is shown only once and never stored.',
289+
acknowledge: 'Done',
287290
fields: [
288291
// The dialog skips fields whose path is absent from the response, so
289292
// conditional keys are safe to declare: phoneNumber only exists for

0 commit comments

Comments
 (0)