Skip to content

Commit 5da3a8c

Browse files
fix(plugin-list): a filter row with no value is not a filter, a date sort reads Oldest → Newest, one sort row per field, and an empty list says how to start (#28423)
<!-- claude-projects-attribution: {"github_login":"os-sales"} --> _Requested by **Sales** · [project thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuYMKU2Un7YLKcuYSwjb25e1)_ Part of #27520 (rows 7 and 17 built; row 21 measured and not built because it no longer reproduces; the card's other open rows stay on it) Clause-②: yes (widening: `@object-ui/i18n` gains `list.clearSearch`, `sortBuilder.oldestFirst` and `sortBuilder.newestFirst` in all ten locales, and `SortBuilder`'s `fields` entries take an optional `type`; no accept set moves) Metadata-protocol: none ## 维护者速读 - **改了什么**:列表工具栏四处小改。筛选面板里还没填值的行不再算进筛选徽标、不再让列表显示「无匹配记录」、也不再被存进用户的列表状态;日期/日期时间字段排序显示「最早 → 最新」而不是「A → Z」;同一字段不能在排序里加两次;搜索导致的「无匹配记录」空状态多一个「清除搜索」按钮;空列表在用户有新建权限时给出「新建」按钮,不再要求视图写 `addRecord.enabled`。 - **为什么改**:18.3.0 RC 走查(#27520 第 7、17 行)在 Tasks 列表上看到:空筛选行算进徽标并在刷新后回来;日期排序写着 A → Z;同字段可重复排序;搜索跨导航保留,回来只看到「无匹配记录」却没有出口;新对象的空列表没有新建按钮。 - **风险与回滚**:只动前端列表组件和三个新翻译键,不改服务端、不改 spec。筛选只是少存未完成的行,面板里照样显示,用户可以继续填;空状态的新建仍受新建权限(`listNew`)约束,作者显式写 `addRecord.enabled: false` 仍然隐藏。回滚即 revert 本 PR。 - **先看**:`ui/packages/plugin-list/src/ListView.tsx`(`appliedFilterGroup`、`emptyStateOffersCreate`、`onClearSearch`);`ui/packages/components/src/custom/sort-builder.tsx`(`takenElsewhere`、`directionLabels`);`ListView.toolbarRows-27520.test.tsx`。 - **两分钟验证**:`pnpm --dir ui exec vitest run packages/plugin-list/src/__tests__/ListView.toolbarRows-27520.test.tsx packages/components/src/__tests__/sort-builder-date-direction-duplicates-27520.test.tsx` → 2 个文件 19 个用例全绿。 - **需要你做的**:无,席位落地(新增 i18n 键属 Clause-② widening,席位先做 TIER_DEFAULT 复核)。 ## What changes Measured on `main` `af6ca001` before any code, by reading the code and then pinning each item red through the real `ListView` / `SortBuilder`: | Item | On `main` | This PR | |:--|:--|:--| | Row 7: filter row with no value | counted on the badge (`shownFilterCount` counted every row), made the list "No matching records", and was handed whole to `onFilterChange`, which the console stores as the user's list state (`listPrefs.patchView`) | not counted, not a user query, not handed to the host; the panel still shows it so it can be finished | | Row 7: date sort direction | `SortBuilder` had no field type, so every row read "A → Z" | a `date` / `datetime` row reads "Oldest → Newest" / "Newest → Oldest"; `ListView` hands each sort field's type | | Row 7: duplicate sort field | every row offered every field; "Add sort" seeded the first field even when a row held it | a field another row holds is unavailable in this row; "Add sort" seeds the first free field and is disabled once none is left | | Row 7: "No matching records" after a search | no way out in the empty state | "Clear search", which clears the search and tells the host (`onSearchChange('')`) | | Row 17: empty list, no New | the empty-state Create was drawn only when the view declared `addRecord.enabled: true`, which few views do, although the console hands `onAddRecord` exactly when its header New is offered | drawn whenever the host hands a create path and `mayCreate` allows it; an explicit `addRecord: { enabled: false }` or `userActions.addRecordForm: false` still withholds it; a view that declares the inline block keeps it as before | | Row 21: "In a queue" tooltip | **does not reproduce**: the toggle carries `title={t('list.sittingInQueueHint')}` in all ten packs and is drawn only on an object the server serves with its injected `queue` column (`objectHasOwnershipQueue`), both since #27264 (`2302b92b`, an ancestor of the walkthrough's `c4c515ad`) | not built (reported) | Not built here, reported: Studio's list-view sort (`ui/packages/app-shell/src/views/studio-design/ObjectListViewInspector.tsx`, held by #28148) hands `SortBuilder` fields without `type`, so its date label stays "A → Z" until that file passes `type: f.type`. The duplicate refusal lives in `SortBuilder` itself, so Studio's sort gets it with no edit there. The search box's own clear (×) button is still unnamed; it is frozen in `iconOnlyButtonCensus-12143`'s backlog and is left to that card. ## Tests - New: `ui/packages/components/src/__tests__/sort-builder-date-direction-duplicates-27520.test.tsx` (8), `ui/packages/plugin-list/src/__tests__/ListView.toolbarRows-27520.test.tsx` (11). Each negative sits beside a control on the same rig. - Red first on unfixed source (tracked changes stashed, new tests kept): 11 failed / 8 passed, each on its target assertion (no option disabled, badge still counts the unfinished row, `onFilterChange` receives the unfinished row, no "Clear search", no `empty-state-add-record`). - Adapted, not deleted: `ListView.sortRemovableOnly-11943.test.tsx`, two controls that read the old duplicate behaviour ("Add sort" seeded the field the first row held; a field held by the other row was read from the first row's dropdown). - Suite: `vitest run packages/plugin-list/ packages/components/ packages/plugin-view/ packages/i18n/` under the verify lock: 816 files, 6 reds in 4 files, all from this change's first cut (the two adapted controls; two `emptyStateLink-12270` and one `ObjectGallery.emptyState-12217` case that read a declared `addRecord.enabled` with no host handler, answered by keeping that leg; one `iconOnlyButtonCensus-12143` count from an aria-label I then took back out). Those 7 files rerun green: 50/50. - Ablations through `scripts/ablation-replace.mjs`, each restored to the HEAD blob (`git diff HEAD` empty after every leg): taken-elsewhere returns false → 1 red; date label test → false → 2 red; "Add sort" ignores held fields → 2 red; emit the whole group → 1 red; badge from the whole group → 3 red; sort fields untyped → 1 red; host create leg off → 1 red; clear search off → 1 red; user query counts unfinished rows → 1 red. ## Acceptance notes - `@object-ui/plugin-view` `ObjectView.tsx` was added to the claim's surface (amendment 6543827414) for one line, and is left unchanged: its `renderListView` branch is entered only when a host passes that prop, and Studio's design surface folds "+ New" into the list through it with `showCreate: false`, so withholding `onAddRecord` there would break Studio's New. - No live browser run; every case mounts the real `ListView` with the real `FilterBuilder` / `SortBuilder` and Radix `Select`. ## Verification All local readings at head `81af3395` (the merge of `origin/main` `380a1078` is in it). <details><summary>Derived gate families: 34 derived, 34 run, 0 NOT-MEASURED</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 34 commands; each was run with stdin from `/dev/null` and its exit code captured before any pipe. All 34 exited 0, among them `node scripts/check-sdui-manifest.mjs`, `pnpm check:nul-bytes`, `pnpm check:type-check-debt` and `pnpm --dir ui exec vitest run scripts/` (169 files passed, 2 skipped; 4901 tests). `--ran` on the recorded list: "34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN … a DERIVED zero — all 34 recorded an exit code and none of them is 3". </details> <details><summary>Lint, type-check, merge-tree</summary> - `eslint` on the changed sources, the two new tests, the adapted test and `ui/packages/i18n/src/locales/`: 0 errors; 154 warnings, none on a changed line. - `pnpm --filter @object-ui/components --filter @object-ui/plugin-list --filter @object-ui/i18n run type-check` (each `tsc --noEmit && tsc -p tsconfig.test.json`): exit 0 after typing the new fixtures as `FilterBuilderRow`. - `git merge-tree --write-tree` against the open PRs that share a file (#28422 and #28394, the ten locale packs) and the other open ui PRs (#28322, #28321, #28305, #28294): exit 0, no conflict. </details> CI check-runs: not started at the time of writing. --- _Generated by [Claude Code](https://claude.ai/code/session_01L6bhgVW2q9rsHS1vCrQ8Q9)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01L6bhgVW2q9rsHS1vCrQ8Q9 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bf74607 commit 5da3a8c

17 files changed

Lines changed: 546 additions & 23 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/plugin-list': patch
3+
'@object-ui/components': patch
4+
'@object-ui/i18n': patch
5+
---
6+
7+
fix(plugin-list): a filter row with no value is not a filter, a date sort reads Oldest → Newest, a sort keeps one row per field, and an empty list says how to start
8+
9+
Clause-②: yes (widening: `@object-ui/i18n` gains `list.clearSearch`, `sortBuilder.oldestFirst` and `sortBuilder.newestFirst` in all ten locales, and `SortBuilder`'s `fields` entries take an optional `type`; no accept set moves)
10+
Metadata-protocol: none
11+
12+
- **Unfinished filter rows.** A filter-panel row with no value yet is no longer counted on the Filter badge, no longer turns the list into "No matching records", and is no longer handed to the host's `onFilterChange`, so the console does not store it as the user's list state and it does not come back after a reload. The panel still shows the row so it can be finished.
13+
- **Date sort direction.** The sort picker names a `date` or `datetime` field's direction "Oldest → Newest" / "Newest → Oldest" instead of "A → Z" / "Z → A". The list hands the picker each field's type.
14+
- **One sort row per field.** A field another sort row already orders by is unavailable in the other rows, "Add sort" starts on the first field no row holds, and it is disabled once every field has a row.
15+
- **Clear search.** The "No matching records" a search produced offers "Clear search".
16+
- **Create on an empty list.** An empty list offers Create whenever the host gives a create path and the user may create, without the view having to declare an inline `addRecord` block. An author's `addRecord: { enabled: false }` or `userActions.addRecordForm: false` still withholds it, and a view that declares the inline block keeps its Create as before.
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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#27520 row 7, the sort half: `SortBuilder` names a date's
11+
* direction as time, and keeps one row per field.
12+
*
13+
* Observed in the RC walkthrough on the Tasks list toolbar: the direction of a
14+
* datetime sort read "A → Z", and the same field could be added to the sort
15+
* twice. The second key on a field the first already orders by changes
16+
* nothing, so a duplicate row is a control that does nothing.
17+
*
18+
* Every case reads the real `SortBuilder` and its real Radix `Select`, and
19+
* keeps its control beside it: a text field still reads "A → Z", and a field
20+
* no other row holds is still an ordinary option.
21+
*/
22+
import { describe, it, expect, vi, afterEach } from 'vitest';
23+
import React from 'react';
24+
import { cleanup, render, screen, fireEvent, within } from '@testing-library/react';
25+
import '@testing-library/jest-dom';
26+
import { SortBuilder, type SortBuilderProps, type SortItem } from '../custom/sort-builder';
27+
28+
type Fields = NonNullable<SortBuilderProps['fields']>;
29+
type Row = Pick<SortItem, 'field' | 'order'>;
30+
31+
const FIELDS: Fields = [
32+
{ value: 'title', label: 'Title', type: 'text' },
33+
{ value: 'due_date', label: 'Due Date', type: 'date' },
34+
{ value: 'created_at', label: 'Created At', type: 'datetime' },
35+
];
36+
37+
/** Holds the value the way a host does, so a change is rendered back. */
38+
function Harness({ fields, initial, onChange }: { fields: Fields; initial: Row[]; onChange: (rows: Row[]) => void }) {
39+
const [value, setValue] = React.useState<SortItem[]>(() =>
40+
initial.map((row, i) => ({ id: `r${i}`, ...row })),
41+
);
42+
return (
43+
<SortBuilder
44+
fields={fields}
45+
value={value}
46+
onChange={(next) => {
47+
onChange(next.map(({ field, order }) => ({ field, order })));
48+
setValue(next);
49+
}}
50+
/>
51+
);
52+
}
53+
54+
function mount(initial: Row[], fields: Fields = FIELDS) {
55+
const onChange = vi.fn();
56+
render(<Harness fields={fields} initial={initial} onChange={onChange} />);
57+
return onChange;
58+
}
59+
60+
/** Each row is `[field select, direction select]`. */
61+
const fieldTriggers = () => screen.getAllByRole('combobox').filter((_, i) => i % 2 === 0);
62+
const directionTriggers = () => screen.getAllByRole('combobox').filter((_, i) => i % 2 === 1);
63+
64+
/** Open a dropdown and return what it offers, closing it again. */
65+
function optionsOf(trigger: HTMLElement): Array<{ label: string; disabled: boolean }> {
66+
fireEvent.click(trigger);
67+
const listbox = screen.getByRole('listbox');
68+
const options = within(listbox).getAllByRole('option').map((o) => ({
69+
label: o.textContent?.trim() ?? '',
70+
disabled: o.getAttribute('aria-disabled') === 'true',
71+
}));
72+
fireEvent.keyDown(listbox, { key: 'Escape' });
73+
return options;
74+
}
75+
76+
const asc = (field: string): Row => ({ field, order: 'asc' });
77+
78+
afterEach(() => {
79+
cleanup();
80+
});
81+
82+
describe('SortBuilder names a date sort as time (objectstack#27520 row 7)', () => {
83+
it('a date row reads "Oldest → Newest" / "Newest → Oldest"; a datetime row too', () => {
84+
mount([asc('due_date'), { field: 'created_at', order: 'desc' }]);
85+
const [dateDir, datetimeDir] = directionTriggers();
86+
expect(dateDir).toHaveTextContent('Oldest → Newest');
87+
expect(datetimeDir).toHaveTextContent('Newest → Oldest');
88+
expect(optionsOf(dateDir).map((o) => o.label)).toEqual(['Oldest → Newest', 'Newest → Oldest']);
89+
});
90+
91+
it('CONTROL: a text row keeps "A → Z" / "Z → A"', () => {
92+
mount([asc('title')]);
93+
expect(directionTriggers()[0]).toHaveTextContent('A → Z');
94+
expect(optionsOf(directionTriggers()[0]).map((o) => o.label)).toEqual(['A → Z', 'Z → A']);
95+
});
96+
97+
it('the label follows the row when its field changes from text to date', () => {
98+
mount([asc('title')]);
99+
fireEvent.click(fieldTriggers()[0]);
100+
fireEvent.click(within(screen.getByRole('listbox')).getByRole('option', { name: 'Due Date' }));
101+
expect(directionTriggers()[0]).toHaveTextContent('Oldest → Newest');
102+
});
103+
104+
it('CONTROL: a field entry with no type keeps "A → Z"', () => {
105+
mount([asc('due_date')], FIELDS.map(({ value, label }) => ({ value, label })));
106+
expect(directionTriggers()[0]).toHaveTextContent('A → Z');
107+
});
108+
});
109+
110+
describe('SortBuilder keeps one row per field (objectstack#27520 row 7)', () => {
111+
it('a field another row sorts by is unavailable in this row; the row\'s own field and the free ones are not', () => {
112+
mount([asc('title'), asc('due_date')]);
113+
expect(optionsOf(fieldTriggers()[1])).toEqual([
114+
{ label: 'Title', disabled: true },
115+
{ label: 'Due Date', disabled: false },
116+
{ label: 'Created At', disabled: false },
117+
]);
118+
// CONTROL: the first row sees the second row's field the same way.
119+
expect(optionsOf(fieldTriggers()[0])).toEqual([
120+
{ label: 'Title', disabled: false },
121+
{ label: 'Due Date', disabled: true },
122+
{ label: 'Created At', disabled: false },
123+
]);
124+
});
125+
126+
it('"Add sort" seeds the first field no row holds, not the first field', () => {
127+
const onChange = mount([asc('title')]);
128+
fireEvent.click(screen.getByRole('button', { name: /add sort/i }));
129+
expect(onChange).toHaveBeenLastCalledWith([asc('title'), asc('due_date')]);
130+
});
131+
132+
it('"Add sort" is disabled once every field has a row', () => {
133+
mount([asc('title'), asc('due_date'), asc('created_at')]);
134+
expect(screen.getByRole('button', { name: /add sort/i })).toBeDisabled();
135+
});
136+
137+
it('CONTROL: with a field still free, "Add sort" stays enabled', () => {
138+
mount([asc('title'), asc('due_date')]);
139+
expect(screen.getByRole('button', { name: /add sort/i })).toBeEnabled();
140+
});
141+
});

‎ui/packages/components/src/custom/sort-builder.tsx‎

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,17 @@ const useSafeSortTranslation = createSafeTranslation(
2222
'sortBuilder.selectField': 'Select field',
2323
'sortBuilder.ascending': 'A → Z',
2424
'sortBuilder.descending': 'Z → A',
25+
'sortBuilder.oldestFirst': 'Oldest → Newest',
26+
'sortBuilder.newestFirst': 'Newest → Oldest',
2527
'sortBuilder.addSort': 'Add sort',
2628
'sortBuilder.removeSort': 'Remove sort',
2729
},
2830
'sortBuilder.sortBy',
2931
)
3032

33+
/** Field types whose order is time: their direction reads "Oldest → Newest". */
34+
const DATE_SORT_TYPES = new Set(['date', 'datetime'])
35+
3136
/**
3237
* One row of the SortBuilder UI: the spec's `SortItem` (`{ field, order }`)
3338
* plus the React key this list needs.
@@ -56,6 +61,12 @@ export interface SortBuilderProps {
5661
* disabled, and is itself disabled when every entry is.
5762
*/
5863
disabled?: boolean
64+
/**
65+
* The field's type (objectstack#27520). A `date` or `datetime` row names
66+
* its direction as time ("Oldest → Newest") instead of "A → Z". Absent,
67+
* the row reads "A → Z" as before.
68+
*/
69+
type?: string
5970
}>;
6071
value?: SortItem[];
6172
onChange?: (value: SortItem[]) => void;
@@ -97,9 +108,27 @@ export function SortBuilder({
97108
onChange?.(newItems);
98109
};
99110

111+
// One row per field (objectstack#27520): a second key on a field the sort
112+
// already orders by changes nothing, so a field another row holds is not
113+
// offered to this one. A row's own field stays choosable in its dropdown.
114+
const heldBy = new Map<string, string>();
115+
for (const item of items) if (item.field && !heldBy.has(item.field)) heldBy.set(item.field, item.id);
116+
const takenElsewhere = (field: string, rowId: string) => {
117+
const holder = heldBy.get(field);
118+
return holder !== undefined && holder !== rowId;
119+
};
120+
100121
// A new row starts on the first field the user may choose: a disabled
101-
// entry is only there for the row that already names it.
102-
const firstChoosable = fields.find((f) => !f.disabled);
122+
// entry is only there for the row that already names it, and a field a row
123+
// already sorts by is not offered again.
124+
const firstChoosable = fields.find((f) => !f.disabled && !heldBy.has(f.value));
125+
126+
// A date's direction is time, not the alphabet (objectstack#27520).
127+
const typeOf = new Map(fields.map((f) => [f.value, f.type]));
128+
const directionLabels = (field: string) =>
129+
DATE_SORT_TYPES.has(typeOf.get(field) ?? '')
130+
? { asc: t('sortBuilder.oldestFirst'), desc: t('sortBuilder.newestFirst') }
131+
: { asc: t('sortBuilder.ascending'), desc: t('sortBuilder.descending') };
103132

104133
const addItem = () => {
105134
const newItem: SortItem = {
@@ -121,7 +150,9 @@ export function SortBuilder({
121150
return (
122151
<div className={cn("space-y-3", className)}>
123152
<div className="space-y-2">
124-
{items.map((item, index) => (
153+
{items.map((item, index) => {
154+
const direction = directionLabels(item.field);
155+
return (
125156
<div key={item.id} className="flex items-center gap-2">
126157
<span className="text-sm font-medium w-16 text-muted-foreground">
127158
{index === 0 ? t('sortBuilder.sortBy') : t('sortBuilder.thenBy')}
@@ -136,12 +167,18 @@ export function SortBuilder({
136167
</SelectTrigger>
137168
<SelectContent>
138169
{fields.map(f => (
139-
<SelectItem key={f.value} value={f.value} disabled={f.disabled}>{f.label}</SelectItem>
170+
<SelectItem
171+
key={f.value}
172+
value={f.value}
173+
disabled={f.disabled || takenElsewhere(f.value, item.id)}
174+
>
175+
{f.label}
176+
</SelectItem>
140177
))}
141178
</SelectContent>
142179
</Select>
143180
</div>
144-
<div className="w-28">
181+
<div className="w-36">
145182
<Select
146183
value={item.order}
147184
onValueChange={(val) => updateItem(item.id, { order: val as 'asc' | 'desc' })}
@@ -150,8 +187,8 @@ export function SortBuilder({
150187
<SelectValue />
151188
</SelectTrigger>
152189
<SelectContent>
153-
<SelectItem value="asc">{t('sortBuilder.ascending')}</SelectItem>
154-
<SelectItem value="desc">{t('sortBuilder.descending')}</SelectItem>
190+
<SelectItem value="asc">{direction.asc}</SelectItem>
191+
<SelectItem value="desc">{direction.desc}</SelectItem>
155192
</SelectContent>
156193
</Select>
157194
</div>
@@ -165,7 +202,8 @@ export function SortBuilder({
165202
<X className="h-4 w-4" />
166203
</Button>
167204
</div>
168-
))}
205+
);
206+
})}
169207
</div>
170208
<Button
171209
variant="outline"

‎ui/packages/i18n/src/locales/ar.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,7 @@ const ar = {
849849
firstRunMessage: "أنشئ سجلك الأول للبدء.",
850850
noMatches: "لا توجد سجلات مطابقة",
851851
noMatchesMessage: "لا توجد سجلات تطابق عوامل التصفية أو البحث الحالية. حاول تعديلها أو مسحها.",
852+
clearSearch: "مسح البحث",
852853
viewFilterNoMatchesMessage: "لا توجد سجلات تطابق عامل التصفية لطريقة العرض هذه.",
853854
ownedNoneTitle: "لا يوجد شيء يخصك بعد",
854855
ownedNoneMessage: "تعرض طريقة العرض هذه سجلاتك فقط، وليس لديك أي سجل بعد.",
@@ -5051,6 +5052,8 @@ const ar = {
50515052
selectField: "اختر الحقل",
50525053
ascending: "A → Z",
50535054
descending: "Z → A",
5055+
oldestFirst: "الأقدم → الأحدث",
5056+
newestFirst: "الأحدث → الأقدم",
50545057
addSort: "إضافة ترتيب",
50555058
removeSort: "إزالة الترتيب",
50565059
},

‎ui/packages/i18n/src/locales/de.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ const de = {
759759
firstRunMessage: "Legen Sie Ihren ersten Datensatz an, um loszulegen.",
760760
noMatches: "Keine passenden Datensätze",
761761
noMatchesMessage: "Keine Datensätze entsprechen Ihren aktuellen Filtern oder Ihrer Suche. Passen Sie sie an oder setzen Sie sie zurück.",
762+
clearSearch: "Suche löschen",
762763
viewFilterNoMatchesMessage: "Keine Datensätze entsprechen dem Filter dieser Ansicht.",
763764
ownedNoneTitle: "Noch nichts von Ihnen",
764765
ownedNoneMessage: "Diese Ansicht listet nur Ihre eigenen Datensätze auf, und Sie haben noch keine.",
@@ -4594,6 +4595,8 @@ const de = {
45944595
selectField: "Feld auswählen",
45954596
ascending: "A → Z",
45964597
descending: "Z → A",
4598+
oldestFirst: "Älteste → Neueste",
4599+
newestFirst: "Neueste → Älteste",
45974600
addSort: "Sortierung hinzufügen",
45984601
removeSort: "Sortierung entfernen",
45994602
},

‎ui/packages/i18n/src/locales/en.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1062,6 +1062,7 @@ const en = {
10621062
firstRunMessage: 'Create your first record to get started.',
10631063
noMatches: 'No matching records',
10641064
noMatchesMessage: 'No records match your current filters or search. Try adjusting or clearing them.',
1065+
clearSearch: 'Clear search',
10651066
viewFilterNoMatchesMessage: 'No records match this view’s filter.',
10661067
ownedNoneTitle: 'Nothing of yours yet',
10671068
ownedNoneMessage: 'This view lists only records that are yours, and you don’t have any yet.',
@@ -5255,6 +5256,8 @@ const en = {
52555256
selectField: 'Select field',
52565257
ascending: 'A → Z',
52575258
descending: 'Z → A',
5259+
oldestFirst: 'Oldest → Newest',
5260+
newestFirst: 'Newest → Oldest',
52585261
addSort: 'Add sort',
52595262
removeSort: 'Remove sort',
52605263
},

‎ui/packages/i18n/src/locales/es.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,7 @@ const es = {
785785
firstRunMessage: "Cree su primer registro para empezar.",
786786
noMatches: "No hay registros coincidentes",
787787
noMatchesMessage: "Ningún registro coincide con sus filtros o su búsqueda actuales. Pruebe a ajustarlos o borrarlos.",
788+
clearSearch: "Borrar búsqueda",
788789
viewFilterNoMatchesMessage: "Ningún registro coincide con el filtro de esta vista.",
789790
ownedNoneTitle: "Aún no tiene nada propio",
790791
ownedNoneMessage: "Esta vista solo muestra sus propios registros, y todavía no tiene ninguno.",
@@ -4709,6 +4710,8 @@ const es = {
47094710
selectField: "Seleccionar campo",
47104711
ascending: "A → Z",
47114712
descending: "Z → A",
4713+
oldestFirst: "Más antiguo → Más reciente",
4714+
newestFirst: "Más reciente → Más antiguo",
47124715
addSort: "Agregar orden",
47134716
removeSort: "Quitar orden",
47144717
},

‎ui/packages/i18n/src/locales/fr.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,7 @@ const fr = {
780780
firstRunMessage: "Créez votre premier enregistrement pour commencer.",
781781
noMatches: "Aucun enregistrement correspondant",
782782
noMatchesMessage: "Aucun enregistrement ne correspond à vos filtres ou à votre recherche. Essayez de les ajuster ou de les effacer.",
783+
clearSearch: "Effacer la recherche",
783784
viewFilterNoMatchesMessage: "Aucun enregistrement ne correspond au filtre de cette vue.",
784785
ownedNoneTitle: "Rien à vous pour l'instant",
785786
ownedNoneMessage: "Cette vue ne liste que vos propres enregistrements, et vous n'en avez encore aucun.",
@@ -4705,6 +4706,8 @@ const fr = {
47054706
selectField: "Sélectionner un champ",
47064707
ascending: "A → Z",
47074708
descending: "Z → A",
4709+
oldestFirst: "Plus ancien → Plus récent",
4710+
newestFirst: "Plus récent → Plus ancien",
47084711
addSort: "Ajouter un tri",
47094712
removeSort: "Supprimer le tri",
47104713
},

‎ui/packages/i18n/src/locales/ja.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ const ja = {
759759
firstRunMessage: "最初のレコードを作成して始めましょう。",
760760
noMatches: "該当するレコードがありません",
761761
noMatchesMessage: "現在のフィルターや検索条件に一致するレコードはありません。条件を調整するか解除してください。",
762+
clearSearch: "検索をクリア",
762763
viewFilterNoMatchesMessage: "このビューのフィルターに一致するレコードはありません。",
763764
ownedNoneTitle: "あなたのレコードはまだありません",
764765
ownedNoneMessage: "このビューにはあなたのレコードだけが表示されますが、まだ1件もありません。",
@@ -4596,6 +4597,8 @@ const ja = {
45964597
selectField: "フィールドを選択",
45974598
ascending: "A → Z",
45984599
descending: "Z → A",
4600+
oldestFirst: "古い順",
4601+
newestFirst: "新しい順",
45994602
addSort: "並べ替えを追加",
46004603
removeSort: "並べ替えを削除",
46014604
},

‎ui/packages/i18n/src/locales/ko.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,7 @@ const ko = {
759759
firstRunMessage: "첫 레코드를 만들어 시작하세요.",
760760
noMatches: "일치하는 레코드가 없습니다",
761761
noMatchesMessage: "현재 필터나 검색어와 일치하는 레코드가 없습니다. 조건을 조정하거나 지워 보세요.",
762+
clearSearch: "검색 지우기",
762763
viewFilterNoMatchesMessage: "이 뷰의 필터와 일치하는 레코드가 없습니다.",
763764
ownedNoneTitle: "아직 내 레코드가 없습니다",
764765
ownedNoneMessage: "이 뷰에는 내 레코드만 표시되며, 아직 하나도 없습니다.",
@@ -4593,6 +4594,8 @@ const ko = {
45934594
selectField: "필드 선택",
45944595
ascending: "A → Z",
45954596
descending: "Z → A",
4597+
oldestFirst: "오래된 순",
4598+
newestFirst: "최신 순",
45964599
addSort: "정렬 추가",
45974600
removeSort: "정렬 삭제",
45984601
},

0 commit comments

Comments
 (0)