Skip to content

Commit ca64f6f

Browse files
fix(platform-objects): API key Revoke and Restore toasts name the key (#27757)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgDFH4S5pR1C8YvYCWWsZfXx)_ ## 维护者速读 - 改了什么:API 密钥列表的「吊销」「恢复」两个行操作,成功提示从「API key revoked / restored」改为写出密钥名(如 `API key "QA key" revoked`),中文、日文、西班牙文译文同步写出密钥名。 - 为什么改:违反写入反馈约定(#25849 裁决 C:记录级成功提示要点名记录),QA 在 18.3.0 RC 轮发现。 - 风险与回滚:只改两条提示文案的元数据,控制台早已按行把 `{{ record.name }}` 填进提示;回滚即 revert 本 PR,无数据或协议变化。 - 先看:`packages/platform-objects/src/identity/sys-api-key.object.ts`(两行 successMessage)、`packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts`、`zh-CN.objects.generated.ts` 的两条译文。 - 两分钟验证:`pnpm --filter @objectstack/platform-objects exec vitest run src/identity/sys-api-key-action-copy.test.ts`,预期 15 个用例全绿。 - 需要你做的:无,席位落地。 Fixes #27456 Clause-②: no Metadata-protocol: none ## What changes - `sys_api_key`'s `revoke_api_key` and `restore_api_key` row actions declare `successMessage: 'API key "{{ record.name }}" revoked'` / `'... restored'`. Both are `list_item` actions; the console's `ActionRunner` fills a `successMessage` `{{ record.* }}` hole from the row the dispatch carries (`composeSuccessMessage` reads `dispatchRecord(action)`), already pinned in objectui's `ActionRunner.copyTemplate-13367.test.ts`, test "the row a dispatch carries is `record`". `record` is one of `ACTION_SUCCESS_TEMPLATE_ROOTS`, so the action-copy publish judge accepts the hole. `name` is a required field on `sys_api_key`, so the hole always has a value. - The translated bundles (zh-CN, ja-JP, es-ES) carry the same hole, hand-written. Merge mode keeps a translated value forever, so without this a zh-CN viewer would keep the key-less sentence while `check:i18n` reads "in sync". - `en.objects.generated.ts` regenerated from the source with `node scripts/check-i18n-bundles.mjs --write` (exit 0; only those two leaves moved). - Patch changeset for `@objectstack/platform-objects`. ## Tests - New `packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts` (the shape of `invite-user-action-copy.test.ts`): the source copy carries `{{ record.name }}`; the en bundle equals the source; each translated bundle carries the token and is not the English source; the served object metadata (`translateMetadataDocument` over `SetupAppTranslations`) gives zh-CN its translated key-naming copy and en the declared copy. - Ablation (one-shot, through `scripts/ablation-replace.mjs`, restored and hash-verified): the revoke line back to `'API key revoked'` turns 3 of 15 cases red (declared copy, en bundle, en served); restored, 15/15 green. - `pnpm --filter @objectstack/platform-objects test`: 98 files / 1384 tests passed. `pnpm --filter @objectstack/platform-objects typecheck`: exit 0. ## Acceptance notes - The acceptance's rendering layer is the console runner, which this PR does not change; its interpolation of a row's `record` into `successMessage` is the existing objectui test named above, so the pin here is the metadata plus that test. - The surface was widened, with the seat's approval, from the two source lines to the same two values in the four `*.objects.generated.ts` bundles. ## Verification On head `6a8ef24c3c`: the derived list (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, 59 families) plus `node scripts/check-sdui-manifest.mjs`, each exit code recorded before any pipe; `--ran` reconciliation exit 0: 58 run with exit 0, 1 NOT MEASURED (`pnpm check:dual-build-cjs-loads`, exit 3, needs a whole-workspace build; left to CI). `check-plugin-teardown-shape --self-test` first exited 3 on the shallow clone, and 0 after `git fetch --unshallow`. The full readings are in the report comment on #27456. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Utp6CbBKXqV5ioVksATxTi --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CbBKXqV5ioVksATxTi)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 968cacf commit ca64f6f

7 files changed

Lines changed: 117 additions & 10 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the API key Revoke and Restore toasts name the key
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
The `revoke_api_key` and `restore_api_key` row actions on `sys_api_key` now say which key they acted on, for example `API key "QA key" revoked`, instead of the bare `API key revoked` / `API key restored`. The zh-CN, ja-JP and es-ES translations name the key too. Nothing to do.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,12 +1154,12 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
11541154
revoke_api_key: {
11551155
label: "Revoke API Key",
11561156
confirmText: "Revoke this API key? Any clients using it will immediately lose access.",
1157-
successMessage: "API key revoked"
1157+
successMessage: "API key \"{{ record.name }}\" revoked"
11581158
},
11591159
restore_api_key: {
11601160
label: "Restore API Key",
11611161
confirmText: "Restore this revoked API key? Existing clients holding the key will regain access.",
1162-
successMessage: "API key restored"
1162+
successMessage: "API key \"{{ record.name }}\" restored"
11631163
}
11641164
}
11651165
},

‎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
@@ -1154,12 +1154,12 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
11541154
revoke_api_key: {
11551155
label: "Revocar clave de API",
11561156
confirmText: "¿Revocar esta clave de API? Los clientes que la utilicen perderán el acceso inmediatamente.",
1157-
successMessage: "Clave de API revocada"
1157+
successMessage: "Clave de API \"{{ record.name }}\" revocada"
11581158
},
11591159
restore_api_key: {
11601160
label: "Restaurar clave de API",
11611161
confirmText: "¿Restaurar esta clave de API revocada? Los clientes existentes que la conserven recuperarán el acceso.",
1162-
successMessage: "Clave de API restaurada"
1162+
successMessage: "Clave de API \"{{ record.name }}\" restaurada"
11631163
}
11641164
}
11651165
},

‎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
@@ -1154,12 +1154,12 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
11541154
revoke_api_key: {
11551155
label: "API キーを失効",
11561156
confirmText: "この API キーを失効しますか?使用中のクライアントはすぐにアクセスを失います。",
1157-
successMessage: "API キーを失効しました"
1157+
successMessage: "API キー「{{ record.name }}」を失効しました"
11581158
},
11591159
restore_api_key: {
11601160
label: "API キーを復元",
11611161
confirmText: "この失効済み API キーを復元しますか?キーを保持する既存のクライアントはアクセスを回復します。",
1162-
successMessage: "API キーを復元しました"
1162+
successMessage: "API キー「{{ record.name }}」を復元しました"
11631163
}
11641164
}
11651165
},

‎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
@@ -1154,12 +1154,12 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
11541154
revoke_api_key: {
11551155
label: "撤销 API 密钥",
11561156
confirmText: "要撤销该 API 密钥吗?所有使用该密钥的客户端都将立即失去访问权限。",
1157-
successMessage: "API 密钥已撤销"
1157+
successMessage: "API 密钥「{{ record.name }}」已撤销"
11581158
},
11591159
restore_api_key: {
11601160
label: "恢复 API 密钥",
11611161
confirmText: "要恢复已撤销的 API 密钥吗?持有该密钥的现有客户端将重新获得访问权限。",
1162-
successMessage: "API 密钥已恢复"
1162+
successMessage: "API 密钥「{{ record.name }}」已恢复"
11631163
}
11641164
}
11651165
},
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// `sys_api_key`'s Revoke and Restore toasts name the key they acted on: a
4+
// record-level success message names the record (the write-feedback
5+
// convention).
6+
//
7+
// Both are `list_item` row actions. The console's runner fills a
8+
// `successMessage` `{{ record.* }}` hole from the row the dispatch carries
9+
// (ActionRunner `composeSuccessMessage`, pinned in objectui's
10+
// `ActionRunner.copyTemplate-13367.test.ts`, "the row a dispatch carries is
11+
// `record`"), so `{{ record.name }}` prints the key's name. A copy without the
12+
// hole names no key.
13+
//
14+
// The translated bundles are pinned as well: merge mode keeps every existing
15+
// translated value, so a revised source sentence would go on serving the old,
16+
// key-less translation, and both states are "in sync" to `check:i18n`.
17+
import { describe, expect, it } from 'vitest';
18+
import { translateMetadataDocument } from '@objectstack/spec/system';
19+
import { templateSlotSource } from '@objectstack/spec/automation';
20+
import { SetupAppTranslations } from '../apps/translations/setup.translation.js';
21+
import { enObjects } from '../apps/translations/en.objects.generated.js';
22+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
23+
import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js';
24+
import { esESObjects } from '../apps/translations/es-ES.objects.generated.js';
25+
import { SysApiKey } from './sys-api-key.object.js';
26+
27+
interface CopyAction {
28+
name?: string;
29+
successMessage?: unknown;
30+
}
31+
32+
/** The hole the console runner fills from the dispatched row. */
33+
const KEY_NAME = '{{ record.name }}';
34+
35+
const ACTIONS = ['revoke_api_key', 'restore_api_key'] as const;
36+
37+
const TRANSLATED = [
38+
['zh-CN', zhCNObjects],
39+
['ja-JP', jaJPObjects],
40+
['es-ES', esESObjects],
41+
] as const;
42+
43+
// `successMessage` is a `template` slot: the parsed object carries the
44+
// `{ dialect: 'template', source }` envelope, read through `templateSlotSource`.
45+
function action(object: unknown, name: string): CopyAction {
46+
const found = ((object as { actions?: CopyAction[] }).actions ?? []).find((a) => a.name === name);
47+
expect(found, `${name} is declared`).toBeDefined();
48+
return { ...found, successMessage: templateSlotSource(found?.successMessage) };
49+
}
50+
51+
const bundleNode = (bundle: unknown, name: string): CopyAction | undefined =>
52+
(bundle as Record<string, { _actions?: Record<string, CopyAction> }>)?.sys_api_key?._actions?.[name];
53+
54+
describe('sys_api_key Revoke / Restore — declared copy', () => {
55+
it.each(ACTIONS)('%s names the key in its success message', (name) => {
56+
expect(action(SysApiKey, name).successMessage).toContain(KEY_NAME);
57+
});
58+
59+
it('the two toasts are different sentences', () => {
60+
const [revoke, restore] = ACTIONS.map((name) => action(SysApiKey, name).successMessage);
61+
expect(revoke).not.toBe(restore);
62+
});
63+
});
64+
65+
describe('sys_api_key Revoke / Restore — translated bundles follow the source', () => {
66+
it.each(ACTIONS)('en carries the source copy for %s', (name) => {
67+
expect(bundleNode(enObjects, name)?.successMessage).toBe(action(SysApiKey, name).successMessage);
68+
});
69+
70+
for (const [locale, bundle] of TRANSLATED) {
71+
it.each(ACTIONS)(`${locale} keeps the key-name token in the success message for %s`, (name) => {
72+
// The old translation survives every re-extract in merge mode; only the
73+
// token proves the leaf was revised with the source.
74+
const translated = bundleNode(bundle, name)?.successMessage;
75+
expect(translated, `${locale} sys_api_key.${name}.successMessage missing`).toBeTruthy();
76+
expect(translated).toContain(KEY_NAME);
77+
expect(translated).not.toBe(action(SysApiKey, name).successMessage);
78+
});
79+
}
80+
});
81+
82+
describe('sys_api_key Revoke / Restore — the served object metadata', () => {
83+
// The object-metadata read the console uses localizes through
84+
// `translateMetadataDocument('object', …)` over this package's bundle.
85+
const served = (locale: string, name: string): CopyAction =>
86+
action(translateMetadataDocument('object', SysApiKey, SetupAppTranslations, { locale }), name);
87+
88+
it.each(ACTIONS)('zh-CN serves the translated, key-naming copy for %s', (name) => {
89+
const zh = served('zh-CN', name);
90+
expect(zh.successMessage).toBe(bundleNode(zhCNObjects, name)?.successMessage);
91+
expect(zh.successMessage).toContain(KEY_NAME);
92+
});
93+
94+
it.each(ACTIONS)('en serves the declared copy for %s', (name) => {
95+
expect(served('en', name).successMessage).toBe(action(SysApiKey, name).successMessage);
96+
});
97+
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export const SysApiKey = ObjectSchema.create({
158158
// `action-state-visibility.test.ts`.
159159
visible: 'has(record.revoked) && record.revoked != true',
160160
confirmText: 'Revoke this API key? Any clients using it will immediately lose access.',
161-
successMessage: 'API key revoked',
161+
successMessage: 'API key "{{ record.name }}" revoked',
162162
refreshAfter: true,
163163
},
164164
{
@@ -174,7 +174,7 @@ export const SysApiKey = ObjectSchema.create({
174174
bodyExtra: { revoked: false },
175175
visible: 'has(record.revoked) && record.revoked == true',
176176
confirmText: 'Restore this revoked API key? Existing clients holding the key will regain access.',
177-
successMessage: 'API key restored',
177+
successMessage: 'API key "{{ record.name }}" restored',
178178
refreshAfter: true,
179179
},
180180
],

0 commit comments

Comments
 (0)