From bcb59a552a49268aad2a454e4452c44ec24c1fde Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 21:49:35 +0000 Subject: [PATCH 1/2] fix(platform-objects): API key Revoke and Restore toasts name the key The two row actions' successMessage now carry a `{{ record.name }}` hole, which the console runner fills from the dispatched row, in the source and in the zh-CN / ja-JP / es-ES bundles (merge mode keeps a translated value, so the old object-only sentence would otherwise keep serving). Claude-Session: https://claude.ai/code/session_01Utp6CbBKXqV5ioVksATxTi Co-authored-by: Claude --- .../src/apps/translations/es-ES.objects.generated.ts | 4 ++-- .../src/apps/translations/ja-JP.objects.generated.ts | 4 ++-- .../src/apps/translations/zh-CN.objects.generated.ts | 4 ++-- packages/platform-objects/src/identity/sys-api-key.object.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts index 9a732d8e98a..2e3a5f1b5b6 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.objects.generated.ts @@ -1154,12 +1154,12 @@ export const esESObjects: NonNullable = { revoke_api_key: { label: "Revocar clave de API", confirmText: "¿Revocar esta clave de API? Los clientes que la utilicen perderán el acceso inmediatamente.", - successMessage: "Clave de API revocada" + successMessage: "Clave de API \"{{ record.name }}\" revocada" }, restore_api_key: { label: "Restaurar clave de API", confirmText: "¿Restaurar esta clave de API revocada? Los clientes existentes que la conserven recuperarán el acceso.", - successMessage: "Clave de API restaurada" + successMessage: "Clave de API \"{{ record.name }}\" restaurada" } } }, diff --git a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts index 3d835cd861e..9c9395558d6 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.objects.generated.ts @@ -1154,12 +1154,12 @@ export const jaJPObjects: NonNullable = { revoke_api_key: { label: "API キーを失効", confirmText: "この API キーを失効しますか?使用中のクライアントはすぐにアクセスを失います。", - successMessage: "API キーを失効しました" + successMessage: "API キー「{{ record.name }}」を失効しました" }, restore_api_key: { label: "API キーを復元", confirmText: "この失効済み API キーを復元しますか?キーを保持する既存のクライアントはアクセスを回復します。", - successMessage: "API キーを復元しました" + successMessage: "API キー「{{ record.name }}」を復元しました" } } }, diff --git a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts index 879f0bd3e15..f03e7c8db63 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts @@ -1154,12 +1154,12 @@ export const zhCNObjects: NonNullable = { revoke_api_key: { label: "撤销 API 密钥", confirmText: "要撤销该 API 密钥吗?所有使用该密钥的客户端都将立即失去访问权限。", - successMessage: "API 密钥已撤销" + successMessage: "API 密钥「{{ record.name }}」已撤销" }, restore_api_key: { label: "恢复 API 密钥", confirmText: "要恢复已撤销的 API 密钥吗?持有该密钥的现有客户端将重新获得访问权限。", - successMessage: "API 密钥已恢复" + successMessage: "API 密钥「{{ record.name }}」已恢复" } } }, diff --git a/packages/platform-objects/src/identity/sys-api-key.object.ts b/packages/platform-objects/src/identity/sys-api-key.object.ts index 9a69e053bc5..cdc3ea64dc7 100644 --- a/packages/platform-objects/src/identity/sys-api-key.object.ts +++ b/packages/platform-objects/src/identity/sys-api-key.object.ts @@ -158,7 +158,7 @@ export const SysApiKey = ObjectSchema.create({ // `action-state-visibility.test.ts`. visible: 'has(record.revoked) && record.revoked != true', confirmText: 'Revoke this API key? Any clients using it will immediately lose access.', - successMessage: 'API key revoked', + successMessage: 'API key "{{ record.name }}" revoked', refreshAfter: true, }, { @@ -174,7 +174,7 @@ export const SysApiKey = ObjectSchema.create({ bodyExtra: { revoked: false }, visible: 'has(record.revoked) && record.revoked == true', confirmText: 'Restore this revoked API key? Existing clients holding the key will regain access.', - successMessage: 'API key restored', + successMessage: 'API key "{{ record.name }}" restored', refreshAfter: true, }, ], From 6a8ef24c3c01135e0028d6a30464c034cc406973 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 29 Oct 2026 21:57:14 +0000 Subject: [PATCH 2/2] test(platform-objects): pin the key-naming API key toasts and their bundles Regenerates the en bundle from the source (check-i18n-bundles --write) and adds a patch changeset for @objectstack/platform-objects. Claude-Session: https://claude.ai/code/session_01Utp6CbBKXqV5ioVksATxTi Co-authored-by: Claude --- .changeset/27456-api-key-toast-names-key.md | 10 ++ .../apps/translations/en.objects.generated.ts | 4 +- .../identity/sys-api-key-action-copy.test.ts | 97 +++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 .changeset/27456-api-key-toast-names-key.md create mode 100644 packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts diff --git a/.changeset/27456-api-key-toast-names-key.md b/.changeset/27456-api-key-toast-names-key.md new file mode 100644 index 00000000000..c6b3a72f5af --- /dev/null +++ b/.changeset/27456-api-key-toast-names-key.md @@ -0,0 +1,10 @@ +--- +'@objectstack/platform-objects': patch +--- + +fix(platform-objects): the API key Revoke and Restore toasts name the key + +Clause-②: no +Metadata-protocol: none + +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. diff --git a/packages/platform-objects/src/apps/translations/en.objects.generated.ts b/packages/platform-objects/src/apps/translations/en.objects.generated.ts index 3c10dcf93a5..ee6c6c365e6 100644 --- a/packages/platform-objects/src/apps/translations/en.objects.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.objects.generated.ts @@ -1154,12 +1154,12 @@ export const enObjects: NonNullable = { revoke_api_key: { label: "Revoke API Key", confirmText: "Revoke this API key? Any clients using it will immediately lose access.", - successMessage: "API key revoked" + successMessage: "API key \"{{ record.name }}\" revoked" }, restore_api_key: { label: "Restore API Key", confirmText: "Restore this revoked API key? Existing clients holding the key will regain access.", - successMessage: "API key restored" + successMessage: "API key \"{{ record.name }}\" restored" } } }, diff --git a/packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts b/packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts new file mode 100644 index 00000000000..e64efbec1de --- /dev/null +++ b/packages/platform-objects/src/identity/sys-api-key-action-copy.test.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// `sys_api_key`'s Revoke and Restore toasts name the key they acted on: a +// record-level success message names the record (the write-feedback +// convention). +// +// Both are `list_item` row actions. The console's runner fills a +// `successMessage` `{{ record.* }}` hole from the row the dispatch carries +// (ActionRunner `composeSuccessMessage`, pinned in objectui's +// `ActionRunner.copyTemplate-13367.test.ts`, "the row a dispatch carries is +// `record`"), so `{{ record.name }}` prints the key's name. A copy without the +// hole names no key. +// +// The translated bundles are pinned as well: merge mode keeps every existing +// translated value, so a revised source sentence would go on serving the old, +// key-less translation, and both states are "in sync" to `check:i18n`. +import { describe, expect, it } from 'vitest'; +import { translateMetadataDocument } from '@objectstack/spec/system'; +import { templateSlotSource } from '@objectstack/spec/automation'; +import { SetupAppTranslations } from '../apps/translations/setup.translation.js'; +import { enObjects } from '../apps/translations/en.objects.generated.js'; +import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js'; +import { jaJPObjects } from '../apps/translations/ja-JP.objects.generated.js'; +import { esESObjects } from '../apps/translations/es-ES.objects.generated.js'; +import { SysApiKey } from './sys-api-key.object.js'; + +interface CopyAction { + name?: string; + successMessage?: unknown; +} + +/** The hole the console runner fills from the dispatched row. */ +const KEY_NAME = '{{ record.name }}'; + +const ACTIONS = ['revoke_api_key', 'restore_api_key'] as const; + +const TRANSLATED = [ + ['zh-CN', zhCNObjects], + ['ja-JP', jaJPObjects], + ['es-ES', esESObjects], +] as const; + +// `successMessage` is a `template` slot: the parsed object carries the +// `{ dialect: 'template', source }` envelope, read through `templateSlotSource`. +function action(object: unknown, name: string): CopyAction { + const found = ((object as { actions?: CopyAction[] }).actions ?? []).find((a) => a.name === name); + expect(found, `${name} is declared`).toBeDefined(); + return { ...found, successMessage: templateSlotSource(found?.successMessage) }; +} + +const bundleNode = (bundle: unknown, name: string): CopyAction | undefined => + (bundle as Record }>)?.sys_api_key?._actions?.[name]; + +describe('sys_api_key Revoke / Restore — declared copy', () => { + it.each(ACTIONS)('%s names the key in its success message', (name) => { + expect(action(SysApiKey, name).successMessage).toContain(KEY_NAME); + }); + + it('the two toasts are different sentences', () => { + const [revoke, restore] = ACTIONS.map((name) => action(SysApiKey, name).successMessage); + expect(revoke).not.toBe(restore); + }); +}); + +describe('sys_api_key Revoke / Restore — translated bundles follow the source', () => { + it.each(ACTIONS)('en carries the source copy for %s', (name) => { + expect(bundleNode(enObjects, name)?.successMessage).toBe(action(SysApiKey, name).successMessage); + }); + + for (const [locale, bundle] of TRANSLATED) { + it.each(ACTIONS)(`${locale} keeps the key-name token in the success message for %s`, (name) => { + // The old translation survives every re-extract in merge mode; only the + // token proves the leaf was revised with the source. + const translated = bundleNode(bundle, name)?.successMessage; + expect(translated, `${locale} sys_api_key.${name}.successMessage missing`).toBeTruthy(); + expect(translated).toContain(KEY_NAME); + expect(translated).not.toBe(action(SysApiKey, name).successMessage); + }); + } +}); + +describe('sys_api_key Revoke / Restore — the served object metadata', () => { + // The object-metadata read the console uses localizes through + // `translateMetadataDocument('object', …)` over this package's bundle. + const served = (locale: string, name: string): CopyAction => + action(translateMetadataDocument('object', SysApiKey, SetupAppTranslations, { locale }), name); + + it.each(ACTIONS)('zh-CN serves the translated, key-naming copy for %s', (name) => { + const zh = served('zh-CN', name); + expect(zh.successMessage).toBe(bundleNode(zhCNObjects, name)?.successMessage); + expect(zh.successMessage).toContain(KEY_NAME); + }); + + it.each(ACTIONS)('en serves the declared copy for %s', (name) => { + expect(served('en', name).successMessage).toBe(action(SysApiKey, name).successMessage); + }); +});