Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/27456-api-key-toast-names-key.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -1154,12 +1154,12 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
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"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1154,12 +1154,12 @@ export const esESObjects: NonNullable<TranslationData['objects']> = {
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"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1154,12 +1154,12 @@ export const jaJPObjects: NonNullable<TranslationData['objects']> = {
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 }}」を復元しました"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1154,12 +1154,12 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
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 }}」已恢复"
}
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, { _actions?: Record<string, CopyAction> }>)?.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);
});
});
4 changes: 2 additions & 2 deletions packages/platform-objects/src/identity/sys-api-key.object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
{
Expand All @@ -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,
},
],
Expand Down
Loading