Skip to content

Commit 39da1bd

Browse files
fix(ui): Setup metadata lists take their title from the sidebar and lead with the label (#28197)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoDnVj7S3St12zLfDBXSN5gF)_ ## 维护者速读 - 改了什么:Setup 里的元数据列表(岗位、权限集等)标题改用侧栏里打开它的那一项的名字,所以显示「Positions / 岗位」这类侧栏原词,而不是类型的单数名「Position」;每一行先显示标签(如 Sales Director),API 名小字等宽放在下面,不再单独占一列「Label」;描述最多两行,悬停看全文,手机上一行不再撑到 180 px 高。 - 为什么改:#27356 第 5 行(hotcrm#2320 Setup S4):HotCRM 浏览器走查指出标题是单数、首列是 API 名、手机上行太高。 - 风险与回滚:只改 Setup 下的列表外观;同一页面在 Setup 之外(元数据目录、Studio 的「在 Setup 中打开」)完全不变;侧栏没有恰好指向本页的入口时标题退回类型名。回滚就是 revert 本 PR。 - 先看:`ResourceListPage.tsx` 里 `setupTitle` / `labelColumn`(`objectstack#27356` 注释处)与新文件 `navEntryTitle.ts`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/ResourceListPage.setupLabelFirst-27356.test.tsx`,应见 5 passed。 - 需要你做的:无,席位落地。 Part of #27356 (row 5, hotcrm#2320 Setup S4: Setup metadata lists read as admin lists) Clause-②: no Metadata-protocol: none ## What changes - **The title is the sidebar's word for the page.** Under Setup, `ResourceListPage`'s header now takes the label of the app's own navigation entry that opens exactly this page, through the new `navEntryTitle` (`metadata-admin/navEntryTitle.ts`). The entry is elected by `@object-ui/layout`'s `resolveActiveNavItem`, the one "which menu am I in" matcher, and counts only when its own address (`resolveHref`) is this page's path; its text goes through `resolveNavItemLabel`, as the sidebar's does. Setup's Access Control entries are `type: 'url'` entries (`nav_positions` labelled "Positions", `nav_permission_sets` "Permission Sets"), served in the reader's language, so the page reads as the row that opened it in every locale the app's bundle carries. With no such entry, the title falls back to the type's label, as before. No plural table is added to the ui registry and nothing changes in `packages/spec`. - **Label first, API name beneath.** Under Setup, when a type's first column is its primary key and it also lists `label`, the two fold into one cell: the label is the row's link, the API name sits under it in small monospace type (`metadata-row-api-name`), and the separate Label column goes. The folded column's header is the Label column's. An item with no label keeps its API name as the monospace link, as before. - **Short rows.** Under Setup a description cell shows two lines at most (`line-clamp-2`) with the whole text in its `title`, so a long description no longer stretches a row at phone width. - **Outside Setup nothing changes:** the directory list and Studio's "Open in Setup" target keep the API name column, the Label column, the type's label as the title and unclamped descriptions. ## Tests - New `ResourceListPage.setupLabelFirst-27356.test.tsx` (5 tests), on the real page with the metadata client doubled as `ResourceListPage.setupPosture-12606` does and the served Setup app's navigation handed in through the expression context: - the header reads "Positions" from the `nav_positions` entry; - with no entry opening exactly this page it reads "Position"; - a row's link is "Sales Director" (not monospace), its API name `sales_director` sits beneath, the header has one "Label" column first and no "Name" column; an unlabelled item keeps its monospace API-name link; - a long description carries `line-clamp-2` and its whole text as `title`; - CONTROL: the same page under `studio` keeps the monospace API-name link, the Name and Label columns and no clamp. - `ResourceListPage.setupPosture-12606`: its Setup case found the row by the link name `everyone`; under Setup that link is now the label, so it finds `Everyone`. Its outside-Setup control is unchanged. <details><summary>Readings (head 464325f, base origin/main 9f13ee8; app-shell closure built with declarations)</summary> - New file: 5/5 passed. - Every ui test file naming `ResourceListPage`, `MetadataResourceListPage` or `navEntryTitle` (24 files, the AppContent route suites included): 24 passed, 294/294 tests. - `pnpm --filter @object-ui/app-shell run type-check` (both programs): lock VERDICT command-exit 0. - Ablation 1, through `scripts/ablation-replace.mjs`: `const labelColumn = inSetup && columns[0]` to `const labelColumn = false && columns[0]`: 4 failed / 1 passed (the control stays green); restored blob = HEAD blob `1abb07375086`, `git diff HEAD` empty. - Ablation 2: `(inSetup && navEntryTitle(` to `(false && navEntryTitle(`: 1 failed / 4 passed (the nav-entry title case); restored blob = HEAD, `git diff HEAD` empty. - vitest resolves `@object-ui/*` to source, so neither ablation needed a rebuild leg. </details> ## Acceptance notes - **Per-row status of #27356's Setup rows** (re-measured on `main` 9f13ee8; the Studio rows 12–22 are settled, handoff 6524323618): | Row | Disposition | |---|---| | 5 | **Taken here.** | | 11 | Taken, next PR from this thread (All reports empty state points to the apps that have reports for the reader). | | 10 | **Will not do on this card.** HotCRM saw Export under Display at 1440 px, not only at phone width: the bundled Display panel with Refresh and Export in its footer is the default at every width by objectui#12423's maintainer-approved design ("Refresh and Export leave the bar (they live in the Display footer)"). Moving Export back to the bar or the ⋯ menu reverses that design and needs a maintainer call. | | 1 | Finding, carrier `domain:server`: Create User's params are server metadata (`create_user` on `sys_user`, platform-objects; `permissionSet` is an inline `type: 'text'` param) and the create-user door accepts no position. | | 4 | Finding, carrier `domain:server`: `POST /automation/:name/trigger` refuses a `runAs: 'system'` schedule flow for anyone but the system principal (`flowStartRefusal`); all of HotCRM's schedule flows run as system. | | 6 | Finding, carrier `domain:server`: the Users row menu is `sys_user`'s server actions; there is no deactivate verb, only Ban. | | 7 | Finding, carrier `domain:server`: "Record ID" is `sys_approval_request`'s own field label (plugin-approvals). | | 8 | Finding, carrier `domain:server`: plugin-approvals knows a request opened on its fallback approvers but persists no marker a reader can show. | The seat files the `domain:server` cards; this PR is `Part of`, and the row 11 PR closes the card once every row is landed or answered. - **The title follows the navigation, not a registry plural.** A list reached by a URL no Setup entry carries (a typed address, another app's link) still reads the type's singular label. That is the honest fallback: the type registry carries no plural label, and adding one is a spec change this card does not take. - **The folded column applies only where the type lists `label`** with the primary key first; a type with its own columns (for example the field list, which leads with name then object) keeps its layout. - Measured in happy-dom, not in a browser at 390 px: the row height on a phone was not re-measured. ## Verification On head 464325f: `dispatch-gates --commands` derived 34 families for this diff; all 34 were run locally with stdin closed, each exit 0, and `--ran` reconciled 34 run, 0 NOT-MEASURED, 0 UNRUN. `check:new-line-citations`: 0 new (report-only gate). `dispatch-gates --tier`: no path-derived mandate; 259 changed lines. CI check-runs were in progress when this was opened. The harness's after-PR assign and review-request step is skipped on the order's word (the seat lands). --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibwpXbWKjEuA5DF45BL)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 16e2122 commit 39da1bd

5 files changed

Lines changed: 247 additions & 12 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/app-shell': patch
3+
---
4+
5+
fix(setup): metadata lists under Setup take their title from the sidebar, lead with the label, and keep rows short
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A metadata list under Setup (Positions, Permission Sets and the other catalog pages) titled itself with the type's singular label ("Position"), led each row with the API name, and let a long description wrap a row to several lines on a phone. Under Setup the page now takes its title from the app's own navigation entry that opens it, so it reads as the sidebar row that opened it ("Positions", in the served language), and falls back to the type's label when no entry opens exactly that page. Each row leads with the item's label, with its API name beneath it in small monospace type, in place of a separate Label column; an item with no label keeps its API name as the link. A description shows two lines at most, with the whole text on hover. The same page outside Setup is unchanged.
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* objectstack#27356 (row 5) — a metadata list under Setup reads as an admin
3+
* list: the header names the page as the app's navigation entry that opens it
4+
* does ("Positions", not the type's singular "Position"), each row leads with
5+
* the item's label with the API name beneath it, and a description takes two
6+
* lines at most, so a row stays one row on a phone.
7+
*
8+
* The same page outside Setup is the control: the API name column, the Label
9+
* column and the type's label as the title, as before.
10+
*/
11+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
12+
import { render, cleanup, screen, within } from '@testing-library/react';
13+
import '@testing-library/jest-dom';
14+
import * as React from 'react';
15+
import { MemoryRouter, Routes, Route } from 'react-router-dom';
16+
// The activation column waits on `audienceAnchorsOf`'s dynamic import of this
17+
// exact specifier; loaded here so its cost lands in the import phase.
18+
import '@objectstack/spec/identity';
19+
20+
const TYPE = 'position';
21+
const LONG = 'Leads the sales organization and gives the final sign-off on the largest deals of the quarter.';
22+
const ITEMS: Record<string, unknown>[] = [
23+
{ name: 'sales_director', label: 'Sales Director', description: LONG, _packageId: 'com.example.crm' },
24+
{ name: 'unlabelled', _packageId: 'com.example.crm' },
25+
];
26+
27+
/** Stable singleton — the page's load effect keys on the client identity. */
28+
const CLIENT = {
29+
list: async (type: string) => (type === TYPE ? ITEMS : []),
30+
};
31+
32+
const ENTRY = { type: TYPE, label: 'Position', domain: 'security', allowEnvironmentOverlay: false, allowRuntimeCreate: true };
33+
34+
/** The served Setup app: its Access Control group names the list "Positions". */
35+
let APP: unknown = null;
36+
37+
vi.mock('./useMetadata', async (importOriginal) => ({
38+
...(await importOriginal<Record<string, unknown>>()),
39+
useMetadataClient: () => CLIENT,
40+
useMetadataTypes: () => ({ loading: false, error: null, entries: [ENTRY] }),
41+
}));
42+
43+
vi.mock('./i18n', async (importOriginal) => ({
44+
...(await importOriginal<Record<string, unknown>>()),
45+
useMetadataLocale: () => 'en-US',
46+
}));
47+
48+
vi.mock('../../hooks/useCanAuthorMetadata', () => ({
49+
useCanAuthorMetadata: () => true,
50+
}));
51+
52+
vi.mock('../../hooks/useTenancyPosture', async (importOriginal) => ({
53+
...(await importOriginal<Record<string, unknown>>()),
54+
useTenancyPosture: () => 'single',
55+
}));
56+
57+
vi.mock('../../providers/ExpressionProvider', async (importOriginal) => {
58+
const actual = await importOriginal<Record<string, any>>();
59+
return { ...actual, useExpressionContext: () => ({ ...actual.useExpressionContext(), app: APP }) };
60+
});
61+
62+
// Imported AFTER the mocks so the page picks them up.
63+
import { MetadataResourceListPage } from './ResourceListPage';
64+
65+
beforeEach(() => {
66+
APP = {
67+
name: 'setup',
68+
navigation: [
69+
{
70+
id: 'grp_access',
71+
type: 'group',
72+
label: 'Access Control',
73+
children: [
74+
{ id: 'nav_positions', type: 'url', label: 'Positions', url: '/apps/setup/metadata/position?scope=environment' },
75+
{ id: 'nav_permission_sets', type: 'url', label: 'Permission Sets', url: '/apps/setup/metadata/permission?scope=environment' },
76+
],
77+
},
78+
],
79+
};
80+
vi.stubGlobal(
81+
'fetch',
82+
vi.fn(async () =>
83+
new Response(JSON.stringify({ records: [], hasMore: false }), {
84+
status: 200,
85+
headers: { 'Content-Type': 'application/json' },
86+
}),
87+
),
88+
);
89+
});
90+
afterEach(() => {
91+
cleanup();
92+
vi.unstubAllGlobals();
93+
});
94+
95+
function mount(appName: string) {
96+
return render(
97+
<MemoryRouter initialEntries={[`/apps/${appName}/metadata/${TYPE}?scope=environment`]}>
98+
<Routes>
99+
<Route path="/apps/:appName/metadata/:type" element={<MetadataResourceListPage />} />
100+
</Routes>
101+
</MemoryRouter>,
102+
);
103+
}
104+
105+
const headers = () => Array.from(document.querySelectorAll('thead th')).map((th) => th.textContent);
106+
107+
describe('a metadata list under Setup leads with labels (objectstack#27356 row 5)', () => {
108+
it('titles the page as the navigation entry that opens it', async () => {
109+
mount('setup');
110+
await screen.findByRole('link', { name: 'Sales Director' });
111+
const header = screen.getByTestId('metadata-list-setup-header');
112+
expect(within(header).getByRole('heading', { level: 1 }).textContent).toBe('Positions');
113+
});
114+
115+
it('falls back to the type label when no entry opens exactly this page', async () => {
116+
APP = { name: 'setup', navigation: [{ id: 'nav_users', type: 'url', label: 'Users', url: '/apps/setup/sys_user' }] };
117+
mount('setup');
118+
await screen.findByRole('link', { name: 'Sales Director' });
119+
const header = screen.getByTestId('metadata-list-setup-header');
120+
expect(within(header).getByRole('heading', { level: 1 }).textContent).toBe('Position');
121+
});
122+
123+
it('leads each row with the label, the API name beneath it, and no separate Label column', async () => {
124+
mount('setup');
125+
const link = await screen.findByRole('link', { name: 'Sales Director' });
126+
const row = screen.getByTestId('metadata-row-sales_director');
127+
expect(within(row).getByTestId('metadata-row-api-name').textContent).toBe('sales_director');
128+
expect(link).not.toHaveClass('font-mono');
129+
expect(headers()[0]).toBe('Label');
130+
expect(headers().filter((h) => h === 'Label')).toHaveLength(1);
131+
expect(headers()).not.toContain('Name');
132+
133+
// An item with no label keeps its API name as the link.
134+
const bare = screen.getByTestId('metadata-row-unlabelled');
135+
expect(within(bare).getByRole('link', { name: 'unlabelled' })).toHaveClass('font-mono');
136+
expect(within(bare).queryByTestId('metadata-row-api-name')).toBeNull();
137+
});
138+
139+
it('clamps a description to two lines and keeps the whole text on hover', async () => {
140+
mount('setup');
141+
await screen.findByRole('link', { name: 'Sales Director' });
142+
const row = screen.getByTestId('metadata-row-sales_director');
143+
const clamp = within(row).getByText(LONG);
144+
expect(clamp).toHaveClass('line-clamp-2');
145+
expect(clamp).toHaveAttribute('title', LONG);
146+
});
147+
148+
it('CONTROL — outside Setup the page keeps the API name column, the Label column and the type title', async () => {
149+
mount('studio');
150+
const link = await screen.findByRole('link', { name: 'sales_director' });
151+
expect(link).toHaveClass('font-mono');
152+
expect(headers().slice(0, 2)).toEqual(['Name', 'Label']);
153+
expect(screen.queryAllByTestId('metadata-row-api-name')).toHaveLength(0);
154+
expect(screen.getByText(LONG)).not.toHaveClass('line-clamp-2');
155+
});
156+
});

‎ui/packages/app-shell/src/views/metadata-admin/ResourceListPage.setupPosture-12606.test.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ const FILTERED_TILE = t('engine.list.filtered', 'en-US');
9393
describe('the metadata-type page under the Setup route (objectui#12606)', () => {
9494
it('renders no breadcrumb and no stat tiles, and says the count in the header’s description', async () => {
9595
const { container } = mount('setup');
96-
await screen.findByRole('link', { name: 'everyone' });
96+
// Under Setup a row's link is its label (objectstack#27356).
97+
await screen.findByRole('link', { name: 'Everyone' });
9798

9899
const header = screen.getByTestId('metadata-list-setup-header');
99100
expect(header.getAttribute('data-posture')).toBe('setup');

‎ui/packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx‎

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*/
1414

1515
import * as React from 'react';
16-
import { Link, Navigate, useNavigate, useParams, useSearchParams } from 'react-router-dom';
16+
import { Link, Navigate, useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
1717
import { Plus, Search, RefreshCw, AlertTriangle, Lock, Copy } from 'lucide-react';
1818
import {
1919
Button,
@@ -51,6 +51,9 @@ import { Empty, EmptyTitle, EmptyDescription, EmptyValue } from '@object-ui/comp
5151
import { PageShell, WritabilityBadge } from './PageShell.js';
5252
import { PageHeader } from '../../layout/PageHeader.js';
5353
import { useInSetupApp } from '../../console/setupApp.js';
54+
import { useExpressionContext } from '../../providers/ExpressionProvider.js';
55+
import { useObjectTranslation } from '@object-ui/i18n';
56+
import { navEntryTitle } from './navEntryTitle.js';
5457
import { getIcon } from '../../utils/getIcon.js';
5558
import { MetadataTypeActions } from './MetadataTypeActions.js';
5659
import { CreatePackageDialog } from './PackagesPage.js';
@@ -187,6 +190,11 @@ function DefaultMetadataList({ type, appName }: { type: string; appName?: string
187190
// the description. Read off the route's `:appName`; the same page anywhere
188191
// else (the directory, Studio's "Open in Setup" target) keeps its shell.
189192
const inSetup = useInSetupApp();
193+
// objectstack#27356 — under Setup the header names the page as the sidebar
194+
// entry that opens it does ("Positions"); the type's label otherwise.
195+
const { app } = useExpressionContext();
196+
const location = useLocation();
197+
const { t: navT, language } = useObjectTranslation();
190198
// objectui#12183 — under a wall the security catalog is the platform
191199
// operator's: the server refuses this caller every write of the type,
192200
// whatever it holds (`catalog-scope.ts`). Not keyed on the scope: the server's
@@ -577,6 +585,13 @@ function DefaultMetadataList({ type, appName }: { type: string; appName?: string
577585
config.bundleMemberField,
578586
);
579587
const typeLabel = translateMetadataType(type, locale, entry?.label ?? type);
588+
const setupTitle =
589+
(inSetup && navEntryTitle(app, `/apps/${appName}`, location.pathname, location.search, navT, language)) || typeLabel;
590+
// objectstack#27356 — under Setup the label leads the identity cell, with the
591+
// API name beneath it, in place of a separate Label column.
592+
const primaryKey = config.primaryKey ?? 'name';
593+
const labelColumn = inSetup && columns[0]?.key === primaryKey ? columns.find((c) => c.key === 'label') : undefined;
594+
const shownColumns = labelColumn ? columns.filter((c) => c !== labelColumn) : columns;
580595
// The type offers SOME runtime write channel (the editors' type tier), and —
581596
// in the environment scope — the caller holds the capability the metadata
582597
// door requires. The server refuses either way; this is the half that says
@@ -826,13 +841,13 @@ function DefaultMetadataList({ type, appName }: { type: string; appName?: string
826841
}
827842
>
828843
<tr className={HEAD_ROW_TINT}>
829-
{columns.map((c) => (
844+
{shownColumns.map((c, ci) => (
830845
<th
831846
key={c.key}
832847
className={cn('px-3 text-left font-medium', inSetup ? 'h-9 py-0' : 'py-2')}
833848
style={c.width ? { width: c.width } : undefined}
834849
>
835-
{localizeColumnLabel(c)}
850+
{localizeColumnLabel(ci === 0 && labelColumn ? labelColumn : c)}
836851
</th>
837852
))}
838853
<th className={cn('px-3 text-right font-medium w-[80px]', inSetup ? 'h-9 py-0' : 'py-2')}>{t('engine.list.col.source', locale)}</th>
@@ -910,9 +925,11 @@ function DefaultMetadataList({ type, appName }: { type: string; appName?: string
910925
HOVER_ROW_TINT,
911926
)}
912927
>
913-
{columns.map((c, ci) => {
928+
{shownColumns.map((c, ci) => {
914929
const value = row.item[c.key];
915930
const cell = c.render ? c.render(value, row.item) : defaultCell(value);
931+
const labelValue = labelColumn ? row.item[labelColumn.key] : undefined;
932+
const hasLabel = labelValue != null && labelValue !== '';
916933
return (
917934
<td key={c.key} className={cellClass}>
918935
{ci === 0 ? (
@@ -935,12 +952,32 @@ function DefaultMetadataList({ type, appName }: { type: string; appName?: string
935952
<AlertTriangle className="w-3.5 h-3.5 text-amber-600 dark:text-amber-400 shrink-0" />
936953
</span>
937954
)}
938-
<Link
939-
to={rowHref}
940-
className="text-primary hover:underline font-mono"
941-
>
942-
{cell}
943-
</Link>
955+
{labelColumn && hasLabel ? (
956+
<span className="min-w-0">
957+
<Link to={rowHref} className="text-primary hover:underline">
958+
{labelColumn.render ? labelColumn.render(labelValue, row.item) : defaultCell(labelValue)}
959+
</Link>
960+
<span
961+
className="block font-mono text-xs text-muted-foreground"
962+
data-testid="metadata-row-api-name"
963+
>
964+
{cell}
965+
</span>
966+
</span>
967+
) : (
968+
<Link
969+
to={rowHref}
970+
className="text-primary hover:underline font-mono"
971+
>
972+
{cell}
973+
</Link>
974+
)}
975+
</span>
976+
) : inSetup && c.key === 'description' && typeof value === 'string' && value !== '' ? (
977+
// objectstack#27356 — two lines at most, so a row
978+
// stays a row on a phone; the whole text on hover.
979+
<span className="line-clamp-2" title={value}>
980+
{cell}
944981
</span>
945982
) : (
946983
cell
@@ -1068,7 +1105,7 @@ function DefaultMetadataList({ type, appName }: { type: string; appName?: string
10681105
return (
10691106
<div className="flex flex-col h-full overflow-hidden">
10701107
<PageHeader
1071-
title={typeLabel}
1108+
title={setupTitle}
10721109
// `createElement`, not a JSX tag: the looked-up icon is a value
10731110
// (the convention ObjectView set).
10741111
icon={React.createElement(getIcon(config.iconName ?? 'layers'), { className: 'h-4 w-4' })}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* The title a Setup list page takes from the app's own navigation
5+
* (objectstack#27356, row 5): the label of the entry that opens exactly this
6+
* page, so the page reads "Positions" as the sidebar row that opened it does,
7+
* in the served language, instead of the type registry's singular label.
8+
*
9+
* The entry is elected by `resolveActiveNavItem`, the one "which menu am I in"
10+
* matcher, and counts only when its own address is this page's path: an entry
11+
* that claims the page by prefix names a different page.
12+
*/
13+
14+
import { resolveActiveNavItem, resolveHref, resolveNavItemLabel } from '@object-ui/layout';
15+
import type { NavigationItem } from '@object-ui/types';
16+
17+
export function navEntryTitle(
18+
app: unknown,
19+
basePath: string,
20+
pathname: string,
21+
search: string,
22+
t: (key: string, options?: any) => string,
23+
locale: string | undefined,
24+
): string | undefined {
25+
const navigation = app && typeof app === 'object' ? (app as { navigation?: unknown }).navigation : undefined;
26+
if (!Array.isArray(navigation)) return undefined;
27+
const item = resolveActiveNavItem(navigation as NavigationItem[], pathname, search, basePath);
28+
if (!item || resolveHref(item, basePath).href.split(/[?#]/)[0] !== pathname) return undefined;
29+
const label = resolveNavItemLabel(item, t, undefined, locale);
30+
return label.trim() !== '' ? label : undefined;
31+
}

0 commit comments

Comments
 (0)