Skip to content

Commit f7c545a

Browse files
fix(plugin-tree): the object tree reads its loading and empty text from the bundle (#28957)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbNSK5n9VEENdLx47urr6NTn)_ Fixes #28892 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:对象树(`object-tree`,例如 Setup › 业务单元的默认树视图)的加载态和空态不再写死英文,改读语言包的 `common.loading` / `common.noData`。中文界面显示「加载中…」和「暂无数据」;英文空态从 `No records` 变为 `No data`。 - **为什么改**:中文界面打开业务单元时,加载中和无数据都显示英文,和整页中文不一致。repeater / record-picker 已在 objectui#12910 改读同一对键,树渲染器漏了。 - **风险与回滚**:低。只改两行文案和 4 个测试里的英文字面,没有新键、没有元数据变化。回滚即还原本 PR。 - **先看**:`ui/packages/plugin-tree/src/ObjectTree.tsx`(`TREE_DEFAULT_TRANSLATIONS` 和两处占位)、`ui/packages/plugin-tree/src/ObjectTree.placeholderI18n.test.tsx`。 - **两分钟验证**:`pnpm --dir ui exec vitest run packages/plugin-tree/src/ObjectTree.placeholderI18n.test.tsx`,预期 1 个文件、4 个测试全绿。 - **需要你做的**:无,席位落地。 ## What changes - `ObjectTree` draws its loading line with `t('common.loading')` and its empty line with `t('common.noData')`, through the `useTreeTranslation` hook it already calls for the overlay heading. Before, both were the English literals `Loading…` and `No records`. - The two keys join `TREE_DEFAULT_TRANSLATIONS`, so a provider-less host still reads English (`Loading…`, `No data`). - The English empty line changes from `No records` to `No data`, the `en` pack's value for `common.noData`. That is the same move objectui#12910 made for the record picker. - `ui/packages/i18n` is untouched. Both keys already exist in all ten packs. ## Tests - **New: `ObjectTree.placeholderI18n.test.tsx`, 4 cases.** Under a zh provider, a pending read draws 加载中… and an empty result draws 暂无数据, with no English beside them. Under en, the same states draw `Loading…` and `No data`. - **Four existing files read `No data` instead of `No records`:** `ObjectTree.contractEnvelope-6839`, `ObjectTree.dataArmSpecRow-8348`, `ObjectTree.expandedDerived-8666` and `__tests__/objectTreeInputs-11168`. They render without a provider, so they read the defaults table. ## Acceptance notes - **Acceptance 1:** done; see What changes. - **Acceptance 2 (a declared emptyText still wins): holds, because there is no declared key on the tree path.** `ComponentPropsMap['object-tree']` (`ObjectTreePropsSchema`) declares no `emptyText` or `emptyState`, and plugin-tree reads none. This PR takes no authored text away. - **The view-level `emptyState` is not drawn for tree views, before or after this PR.** `ListView` draws `ListEmptyState` from the view's `emptyState` only for the grid and gallery layouts, and hands a tree view's empty set to `ObjectTree`. That is a separate gap in `ListView.tsx`, outside this card's surface. The seat files it. - **Acceptance 3:** done; see Tests. - The error line (`Failed to load tree: …`) is still English. It is outside this card. ## Verification All readings are at head `636a312d` (base `origin/main` `9a9823d9`), under `scripts/pm/os-verify-lock.sh`. <details><summary>plugin-tree · 26 of 26 files · type-check</summary> - `pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-tree/`: Test Files 26 passed (26), Tests 172 passed (172). The lock printed `VERDICT command-exit 0`. - `pnpm --filter @object-ui/plugin-tree run type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`): exit 0. - The prerequisite build was `turbo run build` for the plugin-tree dependency closure plus formula, sdui-parser, lint and client: 48 of 48 tasks. </details> <details><summary>Ablations · both red, both restored</summary> Each leg ran through `scripts/ablation-replace.mjs`, which reported the anchor count 1 to 0 and a blob change, then restored the file to the HEAD blob with an empty `git diff HEAD`. - Loading line put back to the literal `Loading…`: `draws the zh loading line while its read is pending` red, 1 failed and 3 passed. - Empty line put back to the literal `No records`: `draws the zh empty line for an empty result` and `draws the en empty line for an empty result` red, 2 failed and 2 passed. </details> <details><summary>Derived gates · 34 of 34</summary> `node scripts/pm/dispatch-gates.mjs --commands` derived 34 commands from this branch. All 34 exited 0, including `node scripts/check-sdui-manifest.mjs` and `pnpm --dir ui exec vitest run scripts/`. `dispatch-gates --ran` reconciled 34 run, 0 not measured, 0 unrun. On this exact body, `check-changeset-no-major` and `check-adr-0087-registration` exited 0, `check-partof-closing-keyword` exited 0, and `check-closing-keyword-parity --body` lists one closing declaration, for this card alone. </details> Diff: 7 files, 154 changed lines. --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jThkd3ikeV2qYP7jeF)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 135054e commit f7c545a

7 files changed

Lines changed: 137 additions & 17 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@object-ui/plugin-tree': patch
3+
---
4+
5+
fix(plugin-tree): the object tree's loading and empty placeholders speak the page's language
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`object-tree` drew its loading line and its empty line as the English literals `Loading…` and `No records`, so a zh-CN page showed English in the middle of a localized view. Both now read the bundle, `common.loading` and `common.noData`, the same keys the repeater and record-picker elements read for their own placeholders. A zh-CN page now reads 加载中… and 暂无数据, and English reads `Loading…` and `No data` (the empty line used to say `No records`). The `object-tree` contract declares no empty-text key, so there is no authored text to override these.

‎ui/packages/plugin-tree/src/ObjectTree.contractEnvelope-6839.test.tsx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* on PRs that cannot reach `plugin-tree` at all — `expected 1 to be 2`.
3636
*
3737
* That wait was `container.querySelector('[data-testid="object-tree"]') ??
38-
* queryByText('No records')`, and the row count was read the instant it passed.
38+
* queryByText('No data')`, and the row count was read the instant it passed.
3939
* The testid is on the TABLE WRAPPER, so it is a MOUNT signal, not a rows
4040
* signal — and the rows this file counts arrive at a LATER commit than the
4141
* table does.
@@ -73,7 +73,7 @@
7373
* count alone is also satisfied by a tree that flattens everything.
7474
* - the REFUSAL arm cannot wait for an absence, so it takes a SETTLED read
7575
* anchored on something that DOES appear in that scenario: the tree's own
76-
* "No records" panel, which `ObjectTree` renders only once `loading` has
76+
* "No data" panel, which `ObjectTree` renders only once `loading` has
7777
* flipped false. Probed on the `records` fixture the sequence is
7878
* `loading → empty-state` with no table at any point, and the panel is
7979
* absent while loading — so this arm cannot pass by timing out on an
@@ -173,14 +173,14 @@ function drawnRows(container: HTMLElement): DrawnRow[] {
173173
}
174174

175175
/**
176-
* Has the tree drawn its own "No records" panel?
176+
* Has the tree drawn its own "No data" panel?
177177
*
178178
* ⚠️ `queryAllByText`, not `queryByText`: the singular form THROWS on multiple
179179
* matches as well as answering `null` on none, so it cannot express "how many"
180180
* without the throw becoming the result.
181181
*/
182182
function emptyPanelDrawn(container: HTMLElement): boolean {
183-
return within(container).queryAllByText('No records').length > 0;
183+
return within(container).queryAllByText('No data').length > 0;
184184
}
185185

186186
function readSettled(container: HTMLElement): Settled {
@@ -246,14 +246,14 @@ async function settledOn(envelope: Envelope, outcome: Outcome): Promise<Settled>
246246
if ('refuses' in outcome) {
247247
// A refusal has no arrival to wait for, so this is a SETTLED read built
248248
// from the two things that CAN be observed: `find` has answered (above),
249-
// and the tree has drawn the "No records" panel — a node it renders only
249+
// and the tree has drawn the "No data" panel — a node it renders only
250250
// after `loading` flips false, so it is a COMPLETION ANCHOR and not the
251251
// mere passage of time. `act` then flushes what React still had queued; it
252252
// is the opposite of widening a timeout.
253253
await waitFor(() =>
254254
expect(
255255
emptyPanelDrawn(container),
256-
'the tree must have drawn its own "No records" panel before it is read — without that anchor an absence assertion passes by timing out',
256+
'the tree must have drawn its own "No data" panel before it is read — without that anchor an absence assertion passes by timing out',
257257
).toBe(true),
258258
);
259259
await act(async () => {});
@@ -304,12 +304,12 @@ describe('ObjectTree — the find() envelope it reads (objectui#6839)', () => {
304304
it('does NOT read `records` — not a QueryResult member', async () => {
305305
// Before the fix these two nodes drew off a key `QueryResult` does not
306306
// declare, and did so AHEAD of `data`. The tree now settles on its own
307-
// "No records" panel — a DIFFERENT node from the table, which is what makes
307+
// "No data" panel — a DIFFERENT node from the table, which is what makes
308308
// this a reading of the refusal rather than of a mount that never happened.
309309
const settled = await settledOn(asRecords, REFUSES);
310310
expect(
311311
settled.emptyPanelDrawn,
312-
'the refusal must be OBSERVED: the tree drew its "No records" panel',
312+
'the refusal must be OBSERVED: the tree drew its "No data" panel',
313313
).toBe(true);
314314
expect(
315315
settled.rows,

‎ui/packages/plugin-tree/src/ObjectTree.dataArmSpecRow-8348.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
*
4444
* ## The outcome, named — the same one `object-map` and `object-gantt` give
4545
*
46-
* A bare array under `data` draws nothing (`No records`) when the node names
46+
* A bare array under `data` draws nothing (`No data`) when the node names
4747
* no object, and the tree queries its `objectName` instead when it does — the
4848
* ladder falls through to rung 2, then rung 3. In a development build
4949
* `SchemaRenderer` says so once, naming the key and the spelling that works;
@@ -164,7 +164,7 @@ describe('object-tree honours only the `data` arm its published row declares (ob
164164
renderNode({ ...BASE, id: 't2', data: AUTHORED }, ds);
165165

166166
expect(await drawn()).toEqual([]);
167-
expect(screen.getByText('No records')).toBeTruthy();
167+
expect(screen.getByText('No data')).toBeTruthy();
168168
expect(ds.find).not.toHaveBeenCalled();
169169
expect(refusalWarningsFor(warn, 'object-tree')).toHaveLength(1);
170170
});

‎ui/packages/plugin-tree/src/ObjectTree.expandedDerived-8666.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ function snapshot(container: HTMLElement): string {
156156
if (!container.querySelector('[data-testid="object-tree"]')) {
157157
const text = container.textContent ?? '';
158158
if (text.includes('Loading')) return 'loading';
159-
if (text.includes('No records')) return 'empty';
159+
if (text.includes('No data')) return 'empty';
160160
return 'nothing';
161161
}
162162
return `table:${drawnRows(container).length}rows`;
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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+
* `ObjectTree`'s loading and empty placeholders speak the session locale: they
11+
* read the bundle's `common.loading` / `common.noData`, the pair the sibling
12+
* renderers (`element:repeater`, `element:record_picker`) read for theirs.
13+
*
14+
* The zh cases are the ones that were red while the two placeholders were the
15+
* English literals `Loading…` / `No records`. The en cases pin the English the
16+
* `en` pack spells for the same keys.
17+
*
18+
* The provider-less path (the factory's English defaults table) is held by the
19+
* package's other tests, which render without an `I18nProvider` and read the
20+
* same English strings.
21+
*/
22+
23+
import React from 'react';
24+
import { describe, it, expect, vi, afterEach } from 'vitest';
25+
import { render, screen, cleanup, waitFor } from '@testing-library/react';
26+
import '@testing-library/jest-dom';
27+
import { I18nProvider } from '@object-ui/i18n';
28+
import { ObjectTree } from './ObjectTree';
29+
30+
/** A data source whose `find` answers `rows`, or never answers when `rows` is null. */
31+
function makeDataSource(rows: Array<Record<string, unknown>> | null) {
32+
return {
33+
find: vi.fn(
34+
(_object: string, _query?: unknown) =>
35+
rows === null
36+
? new Promise<never>(() => {})
37+
: Promise.resolve({ data: rows, total: rows.length }),
38+
),
39+
findOne: vi.fn(),
40+
create: vi.fn(),
41+
update: vi.fn(),
42+
delete: vi.fn(),
43+
getObjectSchema: vi.fn(async (object: string) => ({
44+
name: object,
45+
fields: { name: { type: 'text' }, parent_id: { type: 'text' } },
46+
})),
47+
};
48+
}
49+
50+
function renderTreeIn(language: string, ds: ReturnType<typeof makeDataSource>) {
51+
return render(
52+
<I18nProvider config={{ defaultLanguage: language, detectBrowserLanguage: false }}>
53+
<ObjectTree
54+
schema={{
55+
type: 'object-tree',
56+
objectName: 'category',
57+
parentField: 'parent_id',
58+
labelField: 'name',
59+
} as never}
60+
dataSource={ds as never}
61+
/>
62+
</I18nProvider>,
63+
);
64+
}
65+
66+
afterEach(() => {
67+
cleanup();
68+
});
69+
70+
describe('ObjectTree placeholders read the bundle', () => {
71+
it('draws the zh loading line while its read is pending', async () => {
72+
const ds = makeDataSource(null);
73+
renderTreeIn('zh', ds);
74+
await waitFor(() => expect(ds.find).toHaveBeenCalled());
75+
76+
expect(screen.getByText('加载中…')).toBeInTheDocument();
77+
expect(screen.queryByText('Loading…')).toBeNull();
78+
});
79+
80+
it('draws the zh empty line for an empty result', async () => {
81+
const ds = makeDataSource([]);
82+
renderTreeIn('zh', ds);
83+
84+
expect(await screen.findByText('暂无数据')).toBeInTheDocument();
85+
expect(screen.queryByText('No records')).toBeNull();
86+
expect(screen.queryByText('No data')).toBeNull();
87+
});
88+
89+
it('draws the en loading line while its read is pending', async () => {
90+
const ds = makeDataSource(null);
91+
renderTreeIn('en', ds);
92+
await waitFor(() => expect(ds.find).toHaveBeenCalled());
93+
94+
expect(screen.getByText('Loading…')).toBeInTheDocument();
95+
});
96+
97+
it('draws the en empty line for an empty result', async () => {
98+
const ds = makeDataSource([]);
99+
renderTreeIn('en', ds);
100+
101+
expect(await screen.findByText('No data')).toBeInTheDocument();
102+
});
103+
});

‎ui/packages/plugin-tree/src/ObjectTree.tsx‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ const TREE_DEFAULT_TRANSLATIONS: Record<string, string> = {
109109
// pack. This control is a tree-GRID — a table of rows re-read in place — so
110110
// it takes the name `ObjectGrid` gives the very same bar.
111111
'grid.refreshing': 'Refreshing…',
112+
// The loading and empty placeholders read the bundle's `common.*` pair, the
113+
// keys the sibling renderers (`element:repeater`, `element:record_picker`)
114+
// read for theirs, so a zh-CN page never shows English here. The object-tree
115+
// contract declares no empty-text key, so nothing authored overrides them.
116+
'common.loading': 'Loading…',
117+
'common.noData': 'No data',
112118
};
113119

114120
const useTreeTranslation = createSafeTranslation(
@@ -1157,8 +1163,9 @@ export const ObjectTree: React.FC<ObjectTreeProps> = ({
11571163
onRowClick,
11581164
});
11591165

1160-
// Heading of the record-detail overlay rendered at the bottom of this file.
1161-
// Must stay above the conditional returns below — rules-of-hooks.
1166+
// The placeholders below and the record-detail overlay heading rendered at
1167+
// the bottom of this file. Must stay above the conditional returns below —
1168+
// rules-of-hooks.
11621169
const { t } = useTreeTranslation();
11631170

11641171
if (error) {
@@ -1181,15 +1188,15 @@ export const ObjectTree: React.FC<ObjectTreeProps> = ({
11811188
if (records === null || (loading && records.length === 0)) {
11821189
return (
11831190
<div className={cn('flex items-center justify-center h-40 text-muted-foreground', className)}>
1184-
<p>Loading…</p>
1191+
<p>{t('common.loading')}</p>
11851192
</div>
11861193
);
11871194
}
11881195

11891196
if (records.length === 0) {
11901197
return (
11911198
<div className={cn('flex items-center justify-center h-40 text-muted-foreground', className)}>
1192-
<p>No records</p>
1199+
<p>{t('common.noData')}</p>
11931200
</div>
11941201
);
11951202
}

‎ui/packages/plugin-tree/src/__tests__/objectTreeInputs-11168.test.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ describe('`object-tree.data` — the members are the `{ provider, … }` configu
317317
expect(specRow.safeParse({ data }).success, 'the spec row accepts the configuration').toBe(true);
318318
const ds = mount({ tree: TREE, data });
319319
expect(await drawn()).toEqual([]);
320-
expect(screen.getByText('No records')).toBeInTheDocument();
320+
expect(screen.getByText('No data')).toBeInTheDocument();
321321
expect(ds.find).not.toHaveBeenCalled();
322322
});
323323

@@ -329,7 +329,7 @@ describe('`object-tree.data` — the members are the `{ provider, … }` configu
329329
expect(specRow.safeParse({ data }).success, 'the spec row refuses the retired provider').toBe(false);
330330
const ds = mount({ tree: TREE, data });
331331
expect(await drawn()).toEqual([]);
332-
expect(screen.getByText('No records')).toBeInTheDocument();
332+
expect(screen.getByText('No data')).toBeInTheDocument();
333333
expect(ds.find).not.toHaveBeenCalled();
334334
});
335335

0 commit comments

Comments
 (0)