Skip to content

Commit 235a626

Browse files
authored
Merge branch 'main' into claude/issue-11101-lazy-spec-validation
2 parents 7d24740 + 770cc5b commit 235a626

36 files changed

Lines changed: 875 additions & 220 deletions

‎.changeset/10867-app-wizard-saves-spec-app.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88

99
fix(types,plugin-designer)!: the Studio app wizard saves a document the platform accepts, and an edit keeps the stored `accentColor` (objectui#10867)
1010

11-
⚠️ **BREAKING (authoring)**, marked `minor` under this repository's version-alignment rule (a `major` in the fixed group would move all of it off the `@objectstack` major). Two published `@object-ui/types` members narrow: a navigation separator no longer takes a `label`, and `AppWizardDraft.layout` is removed. A TypeScript literal that writes either no longer compiles. `NavigationItem` is now a union type alias, not an interface. So code that reads `label` off an unnarrowed `NavigationItem` into a `string` slot (its type is now `string | undefined`) also stops compiling, as does code that spreads an entry-only key onto one, and an `interface` that `extends NavigationItem` or augments it (extend `NavigationEntryItem` instead). `objectui validate` now refuses a separator `label` too: the zod mirror's `NavigationItemSchema` refuses every key on a separator that the spec's separator does not declare. The four `appDesigner` layout keys also leave the published `@object-ui/i18n` packs and `DESIGNER_DEFAULT_TRANSLATIONS`, so an application that calls `t()` with one of them now renders the raw key (unless the call passes a `defaultValue`).
11+
⚠️ **BREAKING (authoring)**, marked `minor` under this repository's version-alignment rule (a `major` in the fixed group would move all of it off the `@objectstack` major). Two published `@object-ui/types` members narrow: a navigation separator no longer takes a `label`, and `AppWizardDraft.layout` is removed. A TypeScript literal that writes either no longer compiles. `NavigationItem` is now a union type alias, not an interface. So code that reads `label` off an unnarrowed `NavigationItem` into a `string` slot (its type is now `string | undefined`; objectui#11299, in the same release, widens it to the spec's `I18nLabel` or `undefined`, see `.changeset/11299-types-nav-label-locale-map.md`) also stops compiling, as does code that spreads an entry-only key onto one, and an `interface` that `extends NavigationItem` or augments it (extend `NavigationEntryItem` instead). `objectui validate` now refuses a separator `label` too: the zod mirror's `NavigationItemSchema` refuses every key on a separator that the spec's separator does not declare. The four `appDesigner` layout keys also leave the published `@object-ui/i18n` packs and `DESIGNER_DEFAULT_TRANSLATIONS`, so an application that calls `t()` with one of them now renders the raw key (unless the call passes a `defaultValue`).
1212

1313
**Clause-②: yes (narrowing)** — the separator arm of `NavigationItem` loses `label`, and `AppWizardDraft` loses `layout`.
1414

15-
- **A separator carries only `type`, `id` and `order`.** `@objectstack/spec`'s separator branch declares exactly those keys, and its `AppSchema` refuses anything else. `NavigationItem` required a `label` on every item, so the wizard's "Add separator" wrote `{ id, type: 'separator', label: '' }`, and the console's create-app and edit-app saves were refused with `422 INVALID_METADATA` (`unrecognized_keys` `['label']` at `navigation.N`). `NavigationItem` is now a union of two arms, discriminated by `type`. `NavigationEntryItem` holds every other nav type and keeps its required `label`. `NavigationSeparatorItem` admits `type`, `id` and `order`, and every other entry key is `?: never` on it. Both arms are exported. Reading an entry-only key off an unnarrowed item still compiles, but its type now includes `undefined` (a `label` is `string | undefined`), so passing it where a `string` is required does not. Narrow on `item.type === 'separator'` before relying on `label`. `menuItemToNavigationItem` maps a legacy separator to `{ id, type: 'separator' }` and drops its label. `spec-derived-unions.test.ts` no longer pins the separator `label` as a blocker. It asserts, at both spec tiers, that the separator arm admits the spec separator's keys and no others.
15+
- **A separator carries only `type`, `id` and `order`.** `@objectstack/spec`'s separator branch declares exactly those keys, and its `AppSchema` refuses anything else. `NavigationItem` required a `label` on every item, so the wizard's "Add separator" wrote `{ id, type: 'separator', label: '' }`, and the console's create-app and edit-app saves were refused with `422 INVALID_METADATA` (`unrecognized_keys` `['label']` at `navigation.N`). `NavigationItem` is now a union of two arms, discriminated by `type`. `NavigationEntryItem` holds every other nav type and keeps its required `label`. `NavigationSeparatorItem` admits `type`, `id` and `order`, and every other entry key is `?: never` on it. Both arms are exported. Reading an entry-only key off an unnarrowed item still compiles, but its type now includes `undefined` (a `label` is `string | undefined`; objectui#11299, in the same release, makes it the spec's `I18nLabel` or `undefined`, see `.changeset/11299-types-nav-label-locale-map.md`), so passing it where a `string` is required does not. Narrow on `item.type === 'separator'` before relying on `label`. `menuItemToNavigationItem` maps a legacy separator to `{ id, type: 'separator' }` and drops its label. `spec-derived-unions.test.ts` no longer pins the separator `label` as a blocker. It asserts, at both spec tiers, that the separator arm admits the spec separator's keys and no others.
1616
- **`@object-ui/plugin-designer`: the wizard and `NavigationDesigner` write a separator as `{ id, type }`.** `NavigationDesigner` no longer writes a `label` onto a new separator, and its label, icon and visibility patchers skip one.
1717
- **`@object-ui/layout` narrows on the separator arm; nothing it renders changes.** `resolveNavItemLabel` answers `''` for every separator. A stored separator carrying a non-empty `label`, which `menuItemToNavigationItem` produced before this change, used to resolve to that label. No renderer asks it for a separator's label. The mobile bottom nav's leaf list, which already skipped separators, is now typed as entries.
1818
- **`@object-ui/app-shell` narrows the same way; nothing it renders changes.** `useNavPins` registers a favorite only for an entry and leaves a separator as it is, and the Studio sidebar's navigation walk passes a separator through unchanged.

‎.changeset/11201-nav-label-verbatim.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ shows the current, localized label of what the entry opens.
1313
**To fix such an entry:** clear its label so it inherits the target's localized label, or set the
1414
text you want shown. Stored navigation is not converted, and no notice is sent.
1515
- An entry label written as an inline locale map (`{ en: 'Accounts', 'zh-CN': '客户' }`) used to
16-
render as empty text. It now renders the map's text. The sidebar is not told the viewer's locale,
17-
so it reads the `en` entry, then `default`, then any entry, as it already does for an area label.
16+
render as empty text. It now renders the map's text. objectui#11201 did not tell the sidebar the
17+
viewer's locale, so it read the `en` entry, then `default`, then any entry; objectui#11299, in the
18+
same release, gives the sidebar the viewer's locale (`.changeset/11299-layout-nav-label-locale.md`).
1819
- `NavigationRenderer`'s `resolveObjectLabel`, `resolveDashboardLabel` and `resolveViewLabel` props,
19-
and the matching arguments of `resolveNavItemLabel`, are no longer read. They are still accepted,
20-
so no caller breaks. An unlabelled entry's localized text comes from `resolveTargetLabel`.
20+
and the matching arguments of `resolveNavItemLabel`, are no longer read. objectui#11201 still
21+
accepted them, so no caller broke; objectui#11299, in the same release, removes them
22+
(`.changeset/11299-layout-nav-label-locale.md`). An unlabelled entry's localized text comes from
23+
`resolveTargetLabel`.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): a navigation label written as an inline locale map shows in the viewer's language across the console (objectui#11299)
6+
7+
An app navigation entry whose `label` is an inline locale map
8+
(`{ en: 'Accounts', 'zh-CN': '客户' }`) showed its `en` text to every viewer. The
9+
console now passes its active UI language — the one the sidebar already shows area
10+
labels in — to every surface that names a navigation entry: the sidebar (app and home
11+
navigation), the `nav:menu` page block, the command palette and the search results
12+
page. A viewer reading Chinese sees `客户`, and the palette and search match it by that
13+
text. Plain-string labels and unlabelled entries are unchanged.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@object-ui/layout': minor
3+
---
4+
5+
feat(layout): a navigation label written as an inline locale map renders in the viewer's locale; three inert resolver props retire (objectui#11299)
6+
7+
**Breaking for a consumer that passes the retired props or calls `resolveNavItemLabel` with six arguments** — a compile error, never a silent change of what renders.
8+
9+
- **New: `NavigationRenderer`'s optional `locale` prop**, and a trailing `locale`
10+
argument on `resolveNavItemLabel`. A present entry label written as an inline locale
11+
map (`{ en: 'Accounts', 'zh-CN': '客户' }`) renders the entry for that locale, through
12+
the spec's own `resolveI18nLabel`; when the map has no entry for it, that resolver's
13+
fallback order applies. objectui#11201 rendered such a map's `en` entry for every
14+
viewer; pass the viewer's language as `locale` to show theirs. Without `locale` the
15+
map still reads its `en` entry. A plain-string label and an absent label are
16+
unaffected.
17+
- **Retired: `NavigationRenderer`'s `resolveObjectLabel`, `resolveDashboardLabel` and
18+
`resolveViewLabel` props.** objectui#11201 stopped reading them and kept them as
19+
no-ops; they are removed now. Delete them from your `NavigationRenderer` call. An
20+
unlabelled entry's localized text comes from `resolveTargetLabel`, as before.
21+
- **Changed signature: `resolveNavItemLabel(item, t?, targetLabel?, locale?)`.** The
22+
2nd, 4th and 5th arguments (the same three resolvers) are gone. A call written
23+
`resolveNavItemLabel(item, undefined, t, undefined, undefined, targetLabel)` becomes
24+
`resolveNavItemLabel(item, t, targetLabel, locale)`.
25+
26+
Marked `minor`, not `major`: this repository's fixed release group follows the
27+
`@objectstack` major, so a breaking change of its own is declared `minor` with the
28+
break spelled out here.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/plugin-designer': patch
3+
---
4+
5+
chore(plugin-designer): follow `@object-ui/layout`'s and `@object-ui/types`' navigation label changes (objectui#11299)
6+
7+
- `@object-ui/layout` removed the three unused resolver arguments of
8+
`resolveNavItemLabel`, so the app creation wizard's preview of an unlabelled
9+
navigation entry now calls it as `resolveNavItemLabel(item, undefined, targetLabel)`.
10+
- `@object-ui/types` now types a navigation entry's `label` as the spec's `I18nLabel`
11+
(a string or an inline locale map), so the navigation designer's inline rename reads
12+
the label through a helper that takes both: a string, and objectui's keyed
13+
reference, start the draft as before, and an inline locale map starts it empty, as it
14+
already did.
15+
16+
What either component shows is unchanged.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@object-ui/types': minor
3+
---
4+
5+
fix(types): a navigation entry's `label` accepts an inline locale map, as the spec does (objectui#11299)
6+
7+
`@objectstack/spec` declares a navigation entry's `label` as `I18nLabel`: a plain
8+
string, or an inline locale map such as `{ en: 'Accounts', 'zh-CN': '客户' }`. The
9+
platform's save door accepts the map, but `objectui validate` refused it with
10+
"expected string, received object", because this package declared the label as a
11+
string.
12+
13+
- `NavigationItemSchema` now takes the spec's `I18nLabelSchema` by reference for an
14+
entry's `label`, so `objectui validate` and the platform judge a nav label alike:
15+
a map the spec accepts passes, and a map the spec refuses (a keyed
16+
`{ key, defaultValue }` reference, a non-string entry, a key that is not a locale
17+
tag) is refused here too.
18+
- `NavigationEntryItem.label` is typed as the spec's `I18nLabel`. Code that reads
19+
`item.label` directly now sees `string | InlineLocaleMap`; resolve it through
20+
`resolveNavItemLabel` from `@object-ui/layout` (which also handles an absent
21+
label) rather than treating it as a string.
22+
- An empty string label is still refused, as before: omit the key to inherit the
23+
target's label.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@object-ui/runner': patch
3+
---
4+
5+
fix(runner): the sidebar no longer reads the app's retired `version` key
6+
7+
`version` is a retired key on `@objectstack/spec`'s app schema: the spec
8+
refuses any value there at parse, so no valid app document carries one. The
9+
runner's sidebar still drew a "vVERSION" footer from it, a read that could only
10+
ever render nothing on valid metadata. It is removed.
11+
12+
The read also stopped compiling against objectstack `main`, where a retired
13+
key's TypeScript type became a branded `[REMOVED]` mark instead of `undefined`,
14+
and a mark is not something React can render. Removing it is part of what turns
15+
the `Spec Main Shape Gate` green again, without moving the `@objectstack/spec`
16+
pin (objectui#11330).

‎packages/app-shell/src/chrome/CommandPalette.navLabelInherits-9868.test.tsx‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,19 @@
2727
* - a label-less dashboard and a label-less view entry show their targets'
2828
* CURRENT metadata labels, and a rename shows on the next render;
2929
* - CONTROL: an authored label renders verbatim.
30+
*
31+
* objectui#11299: an entry label written as an inline locale map is shown — and
32+
* searched — in the viewer's language, the `useObjectTranslation().language`
33+
* the palette hands `resolveNavItemLabel` as its `locale`, under a real
34+
* `I18nProvider` in that language.
3035
*/
3136

3237
import * as React from 'react';
3338
import { describe, it, expect, vi, afterEach } from 'vitest';
3439
import { render, cleanup } from '@testing-library/react';
3540
import { MemoryRouter } from 'react-router-dom';
3641
import { MetadataCtx } from '@object-ui/react';
42+
import { createI18n, I18nProvider } from '@object-ui/i18n';
3743

3844
vi.mock('@object-ui/auth', async (importOriginal) => ({
3945
...(await importOriginal<Record<string, unknown>>()),
@@ -53,6 +59,8 @@ const APP = {
5359
{ id: 'nav_board', type: 'object', objectName: 'customer', viewName: 'board' },
5460
// Control: an authored label.
5561
{ id: 'nav_home', type: 'page', pageName: 'home', label: 'Team Home' },
62+
// objectui#11299: a present label written as an inline locale map.
63+
{ id: 'nav_contacts', type: 'object', objectName: 'contact', label: { en: 'Contacts', 'zh-CN': '联系人' } },
5664
],
5765
};
5866

@@ -135,3 +143,26 @@ describe('objectui#9868 — the ⌘K palette names a label-less nav entry like t
135143
expect(home?.value.toLowerCase()).toContain('team home');
136144
});
137145
});
146+
147+
describe('objectui#11299 — the ⌘K palette names a map-labelled entry in the viewer’s language', () => {
148+
const inLanguage = (language: string) => (
149+
<I18nProvider instance={createI18n({ defaultLanguage: language, detectBrowserLanguage: false })} persistLanguage={false}>
150+
{tree(metadata('Board', 'Sales Overview'))}
151+
</I18nProvider>
152+
);
153+
154+
it('under zh-CN the row shows, and is searched by, its zh-CN text', () => {
155+
render(inLanguage('zh-CN'));
156+
if (!document.querySelector('[cmdk-input]')) throw new Error('the palette did not open');
157+
const contacts = rows('object').find((r) => r.value.includes('contact'));
158+
expect(contacts?.text).toBe('联系人');
159+
expect(contacts?.value).toContain('联系人');
160+
expect(contacts?.value).not.toContain('Contacts');
161+
});
162+
163+
it('under en the same row shows its en text', () => {
164+
render(inLanguage('en'));
165+
if (!document.querySelector('[cmdk-input]')) throw new Error('the palette did not open');
166+
expect(rows('object').find((r) => r.value.includes('contact'))?.text).toBe('Contacts');
167+
});
168+
});

‎packages/app-shell/src/chrome/CommandPalette.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,15 @@ export function CommandPalette({ apps, activeApp, objects, onAppChange, dataSour
6060
const { appName } = useParams();
6161
const { setTheme } = useTheme();
6262
const { evaluator } = useExpressionContext();
63-
const { t } = useObjectTranslation();
63+
const { t, language } = useObjectTranslation();
6464
// A nav entry's text, the way the sidebar and `nav:menu` name it: an entry
6565
// with NO `label` shows its target's current label, else its target's machine
6666
// name (objectui#9868 — `NavigationSyncEffect` writes such entries). A present
67-
// label resolves exactly as `resolveKeyedI18nLabel(item.label, t)` did here.
67+
// label renders as authored — an inline locale map in the viewer's `language`,
68+
// the locale the sidebar passes (objectui#11299).
6869
const targetLabel = useNavTargetLabel();
6970
const navLabel = (item: NavigationItem) =>
70-
resolveNavItemLabel(item, undefined, t, undefined, undefined, targetLabel);
71+
resolveNavItemLabel(item, t, targetLabel, language);
7172

7273
// The ⌘K / Ctrl+K accelerator and the open-state source of truth now live in
7374
// CommandPaletteProvider so the keyboard shortcut, the header button, and the

‎packages/app-shell/src/hooks/__tests__/useNavigationSync.standardEntryLocalizes-11201.test.tsx‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
* 1. the writer — `NavigationSyncEffect`, the console's one caller of the
2121
* sync — stores the new dashboard's entry with NO `label` key;
2222
* 2. that STORED entry, handed to the sidebar's `NavigationRenderer` wired as
23-
* `UnifiedSidebar` wires it (the convention resolvers from
24-
* `useObjectLabel` plus `useNavTargetLabel`), under a real `I18nProvider`
23+
* `UnifiedSidebar` wires it (`useNavTargetLabel` plus the active
24+
* `language` as `locale`, objectui#11299), under a real `I18nProvider`
2525
* carrying the app's zh-CN translation bundle the way the console loads
2626
* it, shows the dashboard's label in each language.
2727
*
@@ -47,7 +47,7 @@ import {
4747
I18nProvider,
4848
isSpecTranslationData,
4949
transformSpecTranslations,
50-
useObjectLabel,
50+
useObjectTranslation,
5151
} from '@object-ui/i18n';
5252
import { NavigationSyncEffect } from '../useNavigationSync';
5353
import { useNavTargetLabel } from '../useNavTargetLabel';
@@ -126,18 +126,16 @@ function i18nIn(language: string) {
126126

127127
/** The sidebar surface, wired the way `UnifiedSidebar` wires `NavigationRenderer`. */
128128
function Sidebar({ items }: { items: NavigationItem[] }) {
129-
const { objectLabel, dashboardLabel, viewLabel } = useObjectLabel();
129+
const { language } = useObjectTranslation();
130130
const resolveTargetLabel = useNavTargetLabel();
131131
return (
132132
<SidebarProvider defaultOpen>
133133
<nav aria-label="Sidebar">
134134
<NavigationRenderer
135135
items={items}
136136
basePath="/apps/crm"
137-
resolveObjectLabel={(objectName, fallback) => objectLabel({ name: objectName, label: fallback })}
138-
resolveDashboardLabel={(dashboardName, fallback) => dashboardLabel({ name: dashboardName, label: fallback })}
139-
resolveViewLabel={(objectName, viewName, fallback) => viewLabel(objectName, viewName, fallback)}
140137
resolveTargetLabel={resolveTargetLabel}
138+
locale={language}
141139
/>
142140
</nav>
143141
</SidebarProvider>

0 commit comments

Comments
 (0)