Skip to content

Commit ee7518f

Browse files
fix(plugin-grid): the built-in bulk delete reads the grid translation key (#29001)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb3UD8sKc65YtUfu8o3rLVg6)_ Fixes #28910 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:列表多选后,批量操作栏里内置的「删除」按钮和多选右键菜单里的同一项,现在按当前语言显示(中文为「删除」),不再固定显示英文 Delete。 - 为什么改:没有声明批量操作的对象会自动得到一个内置的删除操作,它的文字是从英文名格式化出来的,没读语言包里早已存在的翻译。 - 风险与回滚:只改按钮文字;操作集合、按钮位置和颜色都不变,自定义批量操作仍用自己的文字。回滚即还原本 PR。 - 先看:`ui/packages/plugin-grid/src/components/BulkActionBar.tsx` 的 `useBulkActionItems`(文字在这里解析一次),然后 `RowContextMenu.tsx` 的两行改动。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/plugin-grid/src/__tests__/BulkActionBar.deleteLabel.zh-28910.test.tsx`,预期 6 个测试全部通过。 - 需要你做的:无,席位落地。 ## What changes - `useBulkActionItems` (the one resolver the bulk bar and the selection's context menu both read) now resolves each string action's label once. `LegacyBulkActionItem` gains `label`: the string `delete` (case-insensitive) reads `grid.delete` from the active pack, and any other string name is still formatted from itself with `formatActionLabel`. - `BulkActionBar` and `SelectionContextMenuItems` render that `label`, so the two entry points cannot disagree. - The memo keys on the active `language` as well as on `t`, so the label follows a language switch without resting on the identity `t` keeps. - Untouched: the default action set (`ObjectGrid` still hands the bar `delete`), the button's position and colour, and declared bulk defs, which keep `def.label`. `ui/packages/i18n` is read only: `grid.delete` is already present in all ten packs. ## Tests - New `ui/packages/plugin-grid/src/__tests__/BulkActionBar.deleteLabel.zh-28910.test.tsx` (6 tests): under zh the bar's button and the context-menu item read `zh.grid.delete` and not `en.grid.delete`; under en the button reads `en.grid.delete`; another string name (`mark_paid`) still reads "Mark Paid"; a declared def keeps its own label. Strings are read from the packs, never from literals. - Ablation: with the label put back to `formatActionLabel(action)` through `scripts/ablation-replace.mjs`, the two zh pins go red (2 failed, 4 passed) and the restore reads blob equal to HEAD with an empty `git diff HEAD`. ## Acceptance notes - Under zh-CN the button reads 删除; under en it reads Delete. Pinned by the new test. - A declared custom bulk action keeps its own label. Pinned by the new test. ## Verification Readings are in the os-dev report on the card. --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jTzzCxE5e1FmY2cuc2)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jTzzCxE5e1FmY2cuc2 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2bfdc42 commit ee7518f

4 files changed

Lines changed: 115 additions & 5 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
fix(plugin-grid): the built-in bulk Delete reads the grid's translation key
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
An object with no declared bulk actions gets the string bulk action `delete`, and the bulk bar drew it from its English name, so it read "Delete" in every language. `useBulkActionItems` now resolves each string action's label once: `delete` reads `grid.delete` from the active pack (删除 under zh), and any other string name is still formatted from itself. The bulk bar's button and the selection's right-click menu both read that label. A declared bulk action keeps its own label, and the action set, the button's position and its colour are unchanged.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack#28910 — the built-in string bulk action `delete` reads the
11+
* grid's translation key.
12+
*
13+
* An object with no declared bulk actions gets the string action `delete`,
14+
* and the bar drew it with `formatActionLabel`, so under zh it read "Delete".
15+
* `useBulkActionItems` now resolves the label once, so the bar's button and
16+
* the selection's context menu read `grid.delete` from the pack. Any other
17+
* string name is still formatted from itself, and a declared def keeps its
18+
* own label. Strings are read from the packs, never from literals.
19+
*/
20+
import '@testing-library/jest-dom/vitest';
21+
import { describe, it, expect, afterEach } from 'vitest';
22+
import React from 'react';
23+
import { render, screen, cleanup } from '@testing-library/react';
24+
import { I18nProvider } from '@object-ui/react';
25+
import { DropdownMenu, DropdownMenuContent } from '@object-ui/components';
26+
import { en, zh } from '@object-ui/i18n/locales';
27+
import type { BulkActionDef } from '@object-ui/types';
28+
import { BulkActionBar } from '../components/BulkActionBar';
29+
import { SelectionContextMenuItems } from '../components/RowContextMenu';
30+
31+
afterEach(cleanup);
32+
33+
const ROWS = [{ id: '1' }, { id: '2' }];
34+
35+
const inLanguage = (language: string, node: React.ReactNode) => (
36+
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }} persistLanguage={false}>
37+
{node}
38+
</I18nProvider>
39+
);
40+
41+
const renderBar = (language: string, actions: string[], actionDefs?: BulkActionDef[]) =>
42+
render(inLanguage(language, <BulkActionBar selectedRows={ROWS} actions={actions} actionDefs={actionDefs} />));
43+
44+
const renderMenu = (language: string, actions: string[]) =>
45+
render(
46+
inLanguage(
47+
language,
48+
<DropdownMenu open>
49+
<DropdownMenuContent>
50+
<SelectionContextMenuItems selectedRows={ROWS} actions={actions} />
51+
</DropdownMenuContent>
52+
</DropdownMenu>,
53+
),
54+
);
55+
56+
describe('the built-in bulk delete reads grid.delete (objectstack#28910)', () => {
57+
it('the zh pack translates the key, so the zh pins below can tell the two apart', () => {
58+
expect(zh.grid.delete).not.toBe(en.grid.delete);
59+
});
60+
61+
it('under zh, the bar button reads the zh pack, not the English name', async () => {
62+
renderBar('zh', ['delete']);
63+
const button = await screen.findByTestId('bulk-action-delete');
64+
expect(button).toHaveTextContent(zh.grid.delete);
65+
expect(button).not.toHaveTextContent(en.grid.delete);
66+
});
67+
68+
it('under en, the bar button reads the en pack', async () => {
69+
renderBar('en', ['delete']);
70+
expect(await screen.findByTestId('bulk-action-delete')).toHaveTextContent(en.grid.delete);
71+
});
72+
73+
it("under zh, the selection's context menu reads the same zh label", async () => {
74+
renderMenu('zh', ['delete']);
75+
const item = await screen.findByTestId('row-context-bulk-delete');
76+
expect(item).toHaveTextContent(zh.grid.delete);
77+
expect(item).not.toHaveTextContent(en.grid.delete);
78+
});
79+
80+
it('another string name is still formatted from itself', async () => {
81+
renderBar('zh', ['mark_paid']);
82+
expect(await screen.findByTestId('bulk-action-mark_paid')).toHaveTextContent('Mark Paid');
83+
});
84+
85+
it('a declared bulk def keeps its own label', async () => {
86+
const def = { name: 'delete', label: 'Purge selected', operation: 'delete' } as BulkActionDef;
87+
renderBar('zh', [], [def]);
88+
expect(await screen.findByText('Purge selected')).toBeInTheDocument();
89+
expect(screen.queryByText(zh.grid.delete)).toBeNull();
90+
});
91+
});

‎ui/packages/plugin-grid/src/components/BulkActionBar.tsx‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ function isBulkDefOffered(
4646
/** A legacy string bulk action, with the destructive styling its name implies. */
4747
export interface LegacyBulkActionItem {
4848
name: string;
49+
/**
50+
* What the button and the menu item read: the built-in `delete` reads the
51+
* grid's translation key, any other name is formatted from itself.
52+
*/
53+
label: string;
4954
destructive: boolean;
5055
}
5156

@@ -88,6 +93,9 @@ export function useBulkActionItems(input: {
8893
const { selectedRows, actions, actionDefs, objectFields } = input;
8994
const mayInvoke = useCapabilityGate();
9095
const scope = usePredicateScope();
96+
// `language` keys the memo on the active language itself, so the label
97+
// follows a switch whatever identity `t` keeps across it.
98+
const { t, language } = useObjectTranslation();
9199
return React.useMemo(() => {
92100
const defs = (actionDefs ?? []).filter(
93101
(d) =>
@@ -99,11 +107,12 @@ export function useBulkActionItems(input: {
99107
const actionStr = String(action).toLowerCase();
100108
return {
101109
name: action,
110+
label: actionStr === 'delete' ? t('grid.delete', { defaultValue: 'Delete' }) : formatActionLabel(action),
102111
destructive: actionStr.includes('delete') || actionStr.includes('remove') || actionStr.includes('destroy'),
103112
};
104113
});
105114
return { defs, legacy };
106-
}, [actionDefs, actions, selectedRows, scope, objectFields, mayInvoke]);
115+
}, [actionDefs, actions, selectedRows, scope, objectFields, mayInvoke, t, language]);
107116
}
108117

109118
/** A rich def reads as destructive by its variant or by the delete operation it runs. */
@@ -302,7 +311,7 @@ export const BulkActionBar: React.FC<BulkActionBarProps> = ({
302311
onActionDef={onActionDef}
303312
/>
304313
))}
305-
{items.legacy.map(({ name: action, destructive: isDestructive }) => {
314+
{items.legacy.map(({ name: action, label, destructive: isDestructive }) => {
306315
const Icon = isDestructive ? Trash2 : null;
307316
return (
308317
<Button
@@ -314,7 +323,7 @@ export const BulkActionBar: React.FC<BulkActionBarProps> = ({
314323
data-testid={`bulk-action-${action}`}
315324
>
316325
{Icon && <Icon className="h-3 w-3" />}
317-
{formatActionLabel(action)}
326+
{label}
318327
</Button>
319328
);
320329
})}

‎ui/packages/plugin-grid/src/components/RowContextMenu.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,15 @@ export const SelectionContextMenuItems: React.FC<{
150150
</DropdownMenuItem>
151151
);
152152
})}
153-
{items.legacy.map(({ name, destructive }) => (
153+
{items.legacy.map(({ name, label, destructive }) => (
154154
<DropdownMenuItem
155155
key={name}
156156
onClick={() => onAction?.(name, selectedRows)}
157157
data-testid={`row-context-bulk-${name}`}
158158
className={destructive ? 'text-destructive focus:text-destructive' : undefined}
159159
>
160160
{destructive && <Trash2 className="mr-2 h-4 w-4" />}
161-
{formatActionLabel(name)}
161+
{label}
162162
</DropdownMenuItem>
163163
))}
164164
<DropdownMenuSeparator />

0 commit comments

Comments
 (0)