From 57575374c68b5b9b19a247a58cdf47b824a835a6 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Fri, 12 Jun 2026 18:24:29 +0500 Subject: [PATCH 1/3] =?UTF-8?q?feat(list):=20ADR-0047=20phase=202=20?= =?UTF-8?q?=E2=80=94=20spec=20tabs=20reach=20the=20renderer,=20whitelist?= =?UTF-8?q?=E2=88=A9capability=20visualization=20switcher?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ObjectView forwards viewDef.tabs (spec ViewTab metadata was stored and served but never rendered) and viewDef.appearance (allowedVisualizations whitelist), and turns on the dormant ViewSwitcher when the author whitelisted more than one type - ListView.handleTabChange accepts the canonical ViewFilterRule[] tab filter shape (was silently ignored — only the legacy FilterGroup worked); mapOperator learns the spec snake_case operators - availableViews: effective options = author whitelist ∩ capability- resolvable types (kanban needs groupBy, calendar a date field, …) instead of trusting the whitelist blindly; chart included - UserFilters tabs element normalizes spec ViewTab presets ({name,filter,isDefault} → {id,filters,default}) Co-Authored-By: Claude Fable 5 --- packages/app-shell/src/views/ObjectView.tsx | 24 +++++-- packages/plugin-list/src/ListView.tsx | 80 ++++++++++++--------- packages/plugin-list/src/UserFilters.tsx | 36 +++++++++- 3 files changed, 103 insertions(+), 37 deletions(-) diff --git a/packages/app-shell/src/views/ObjectView.tsx b/packages/app-shell/src/views/ObjectView.tsx index ae20c57d4..8603b919a 100644 --- a/packages/app-shell/src/views/ObjectView.tsx +++ b/packages/app-shell/src/views/ObjectView.tsx @@ -1122,9 +1122,21 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an persistViewPatch(viewDef.id, viewDef, { columnState: state }); }, inlineEdit: viewDef.inlineEdit ?? viewDef.editRecordsInline ?? listSchema.inlineEdit, - appearance: viewDef.showDescription != null - ? { showDescription: viewDef.showDescription } - : listSchema.appearance, + // ADR-0047 — spec `appearance` (incl. allowedVisualizations, the + // runtime visualization whitelist) flows from the view metadata; + // the legacy bare `showDescription` flag is folded in on top. + appearance: viewDef.appearance + ? (viewDef.showDescription != null + ? { ...viewDef.appearance, showDescription: viewDef.showDescription } + : viewDef.appearance) + : (viewDef.showDescription != null + ? { showDescription: viewDef.showDescription } + : listSchema.appearance), + // Offer the visualization switcher only when the author + // whitelisted more than one type; ListView intersects the + // whitelist with capability-resolvable types. + showViewSwitcher: + ((viewDef.appearance ?? listSchema.appearance)?.allowedVisualizations?.length ?? 0) > 1, // Propagate toolbar/display flags for all view types showSearch: viewDef.showSearch ?? listSchema.showSearch, showSort: viewDef.showSort ?? listSchema.showSort, @@ -1202,7 +1214,11 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: an ?? resolveManagedByEmptyState((objectDef as any)?.managedBy, t), ), aria: viewDef.aria ?? listSchema.aria, - tabs: listSchema.tabs, + // ADR-0047 — per-view filter tabs from metadata (ViewTab presets) + // take precedence over schema-level tabs. Dropping viewDef.tabs + // here was the one-line gap that kept spec tab metadata from + // ever reaching the TabBar. + tabs: viewDef.tabs ?? listSchema.tabs, // Propagate filter/sort as default filters/sort for data flow ...((() => { const combined = [ diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index 7a7280bb2..cd1a4ef3d 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -42,19 +42,21 @@ export interface ListViewProps { [key: string]: any; } -// Helper to convert FilterBuilder group to ObjectStack AST +// Helper to convert FilterBuilder group to ObjectStack AST. +// Accepts both the FilterBuilder vocabulary (camelCase) and the +// @objectstack/spec ViewFilterRule vocabulary (snake_case). function mapOperator(op: string) { switch (op) { case 'equals': case 'eq': return '='; - case 'notEquals': case 'ne': case 'neq': return '!='; + case 'notEquals': case 'not_equals': case 'ne': case 'neq': return '!='; case 'contains': return 'contains'; - case 'notContains': return 'notcontains'; + case 'notContains': case 'notcontains': return 'notcontains'; case 'greaterThan': case 'gt': return '>'; case 'greaterOrEqual': case 'gte': return '>='; case 'lessThan': case 'lt': return '<'; case 'lessOrEqual': case 'lte': return '<='; case 'in': return 'in'; - case 'notIn': return 'not in'; + case 'notIn': case 'not_in': case 'nin': return 'not in'; case 'before': return '<'; case 'after': return '>'; default: return op; @@ -459,12 +461,20 @@ export const ListView = React.forwardRef(({ const handleTabChange = React.useCallback( (tab: ViewTab) => { setActiveTab(tab.name); - // Apply tab filter if defined + // Apply tab filter if defined. Two shapes are accepted: + // - @objectstack/spec ViewTab.filter: ViewFilterRule[] — an array of + // `{ field, operator, value }` rules (the canonical metadata shape) + // - legacy FilterGroup `{ logic, conditions }` if (tab.filter) { + const conditions = Array.isArray(tab.filter) + ? tab.filter + .filter((r: any) => r && typeof r.field === 'string') + .map((r: any) => ({ field: r.field, operator: r.operator ?? 'equals', value: r.value })) + : (tab.filter.conditions || []); const tabFilters: FilterGroup = { id: `tab-filter-${tab.name}`, - logic: tab.filter.logic || 'and', - conditions: tab.filter.conditions || [], + logic: (!Array.isArray(tab.filter) && tab.filter.logic) || 'and', + conditions, }; setCurrentFilters(tabFilters); onFilterChange?.(tabFilters); @@ -1040,52 +1050,58 @@ export const ListView = React.forwardRef(({ // Available view types based on schema configuration const availableViews = React.useMemo(() => { - // If appearance.allowedVisualizations is set, use it as whitelist - if (schema.appearance?.allowedVisualizations && schema.appearance.allowedVisualizations.length > 0) { - return schema.appearance.allowedVisualizations.filter((v: any) => - ['grid', 'kanban', 'gallery', 'calendar', 'timeline', 'gantt', 'map'].includes(v) - ) as ViewType[]; - } + // Capability-resolvable types: a visualization is only offered when its + // required field bindings resolve (ADR-0047) — kanban needs a groupBy, + // calendar a start date, etc. `grid` always renders. + const resolvable: ViewType[] = ['grid']; - const views: ViewType[] = ['grid']; - // Check for Kanban capabilities (spec config takes precedence) - if (schema.kanban?.groupField || schema.options?.kanban?.groupField) { - views.push('kanban'); + if (schema.kanban?.groupByField || schema.kanban?.groupField || schema.options?.kanban?.groupField) { + resolvable.push('kanban'); } // Check for Gallery capabilities (spec config takes precedence) if (schema.gallery?.coverField || schema.gallery?.imageField || schema.options?.gallery?.imageField) { - views.push('gallery'); + resolvable.push('gallery'); } - + // Check for Calendar capabilities (spec config takes precedence) if (schema.calendar?.startDateField || schema.options?.calendar?.startDateField) { - views.push('calendar'); + resolvable.push('calendar'); } - + // Check for Timeline capabilities (spec config takes precedence) if (schema.timeline?.startDateField || schema.options?.timeline?.startDateField || schema.options?.timeline?.dateField || schema.options?.calendar?.startDateField) { - views.push('timeline'); + resolvable.push('timeline'); } - + // Check for Gantt capabilities (spec config takes precedence) if (schema.gantt?.startDateField || schema.options?.gantt?.startDateField) { - views.push('gantt'); + resolvable.push('gantt'); } - + // Check for Map capabilities if (schema.options?.map?.locationField || (schema.options?.map?.latitudeField && schema.options?.map?.longitudeField)) { - views.push('map'); + resolvable.push('map'); } - - // Always allow switching back to the viewType defined in schema if it's one of the supported types - if (schema.viewType && !views.includes(schema.viewType as ViewType) && + + // Always allow switching back to the viewType defined in schema + if (schema.viewType && !resolvable.includes(schema.viewType as ViewType) && ['grid', 'kanban', 'calendar', 'timeline', 'gantt', 'map', 'gallery', 'chart'].includes(schema.viewType)) { - views.push(schema.viewType as ViewType); + resolvable.push(schema.viewType as ViewType); } - - return views; + + // appearance.allowedVisualizations is the author whitelist (ADR-0047): + // effective options = whitelist ∩ resolvable. Types whose bindings don't + // resolve are hidden even when whitelisted — a kanban without a groupBy + // field renders garbage, so it must not be offered. + const whitelist = schema.appearance?.allowedVisualizations; + if (Array.isArray(whitelist) && whitelist.length > 0) { + const filtered = whitelist.filter((v: any) => resolvable.includes(v)) as ViewType[]; + return filtered.length > 0 ? filtered : (['grid'] as ViewType[]); + } + + return resolvable; }, [schema.options, schema.viewType, schema.kanban, schema.calendar, schema.gantt, schema.gallery, schema.timeline, schema.appearance?.allowedVisualizations]); // Sync view from props diff --git a/packages/plugin-list/src/UserFilters.tsx b/packages/plugin-list/src/UserFilters.tsx index 3538ebc1c..642588ef2 100644 --- a/packages/plugin-list/src/UserFilters.tsx +++ b/packages/plugin-list/src/UserFilters.tsx @@ -61,6 +61,40 @@ export interface UserFiltersProps { className?: string; } +/** Map @objectstack/spec ViewFilterRule operators to ObjectQL AST operators. */ +function specOperatorToAst(op: string | undefined): string { + switch (op) { + case undefined: case 'equals': case 'eq': return '='; + case 'not_equals': case 'ne': case 'neq': return '!='; + case 'gte': return '>='; case 'lte': return '<='; + case 'gt': return '>'; case 'lt': return '<'; + case 'not_in': case 'nin': return 'not in'; + default: return op; + } +} + +/** + * Normalize tab presets to the client shape. Accepts both: + * - @objectstack/spec ViewTab: `{ name, label, filter: ViewFilterRule[], isDefault }` + * - legacy client shape: `{ id, label, filters: triplet[], default }` + */ +function normalizeTabPresets(tabs: any[]): Array<{ id: string; label: string; filters: any[]; default?: boolean }> { + return (tabs || []) + .filter((t: any) => t && (t.id || t.name)) + .map((t: any) => ({ + id: t.id ?? t.name, + label: typeof t.label === 'string' ? t.label : (t.label?.toString?.() ?? t.id ?? t.name), + filters: Array.isArray(t.filters) + ? t.filters + : (Array.isArray(t.filter) + ? t.filter + .filter((r: any) => r && typeof r.field === 'string') + .map((r: any) => [r.field, specOperatorToAst(r.operator), r.value]) + : []), + default: t.default ?? t.isDefault, + })); +} + /** * UserFilters — Airtable Interfaces-style filter bar. * @@ -92,7 +126,7 @@ export function UserFilters({ case 'tabs': return ( Date: Fri, 12 Jun 2026 18:42:31 +0500 Subject: [PATCH 2/3] =?UTF-8?q?feat(app-shell):=20ADR-0047=20phase=204=20?= =?UTF-8?q?=E2=80=94=20PageView=20renders=20interfaceConfig=20(interface?= =?UTF-8?q?=20mode)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InterfaceListPage binds interfaceConfig.source/sourceView to the merged object definition, inherits columns/filter/sort from the referenced view (the iron rule), and overlays the page's presentation policy: userFilters, appearance.allowedVisualizations (single entry = no switcher), userActions toggles (closed-by-default toolbar — no advanced filter builder, no view management, no export). PageView routes pages carrying interfaceConfig.source to this surface instead of the region renderer. Co-Authored-By: Claude Fable 5 --- .../app-shell/src/views/InterfaceListPage.tsx | 150 ++++++++++++++++++ packages/app-shell/src/views/PageView.tsx | 23 ++- 2 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 packages/app-shell/src/views/InterfaceListPage.tsx diff --git a/packages/app-shell/src/views/InterfaceListPage.tsx b/packages/app-shell/src/views/InterfaceListPage.tsx new file mode 100644 index 000000000..9a5c614fb --- /dev/null +++ b/packages/app-shell/src/views/InterfaceListPage.tsx @@ -0,0 +1,150 @@ +/** + * Interface List Page — ADR-0047 interface mode. + * + * Renders a page whose `interfaceConfig` binds a single source view into an + * author-curated list surface. Where ObjectView (data mode) shows ALL of an + * object's list views as switcher tabs and lets users create views, this + * surface is deliberately closed: + * + * • the page REFERENCES one view (`interfaceConfig.sourceView`) — columns, + * base filter and sort are inherited, never restated (the iron rule); + * • end users get exactly the `userFilters` the author enabled; + * • the visualization comes from `appearance.allowedVisualizations` + * (a single entry renders no switcher); + * • `userActions` toggles map onto the toolbar — advanced filtering and + * view management are absent by default. + */ + +import * as React from 'react'; +import { ListView } from '@object-ui/plugin-list'; +import { useAdapter } from '@object-ui/react'; +import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components'; +import { Database } from 'lucide-react'; +import { useObjectTranslation } from '@object-ui/i18n'; +import { useMetadata } from '../providers/MetadataProvider'; + +interface InterfaceListPageProps { + page: any; + className?: string; +} + +/** + * Resolve the source list view from the merged object definition. + * Views merged from ADR-0017 ViewItems are keyed `.`; + * the page author writes the bare key (`sourceView: 'default'`). + */ +function resolveSourceView(objectDef: any, sourceView?: string): any | undefined { + const views: Record = objectDef?.listViews || objectDef?.list_views || {}; + if (sourceView) { + return ( + views[`${objectDef.name}.${sourceView}`] + ?? views[sourceView] + ?? (sourceView === 'default' || sourceView === 'list' ? objectDef?.list : undefined) + ); + } + return objectDef?.list ?? Object.values(views)[0]; +} + +export function InterfaceListPage({ page, className }: InterfaceListPageProps) { + const { t } = useObjectTranslation(); + const { objects } = useMetadata(); + const dataSource = useAdapter(); + + const cfg = page?.interfaceConfig || {}; + const objectDef = React.useMemo( + () => (objects || []).find((o: any) => o.name === cfg.source), + [objects, cfg.source], + ); + const viewDef = React.useMemo( + () => resolveSourceView(objectDef, cfg.sourceView), + [objectDef, cfg.sourceView], + ); + + const schema = React.useMemo(() => { + if (!objectDef) return undefined; + const view = viewDef || {}; + const appearance = cfg.appearance ?? view.appearance; + const allowed: string[] = appearance?.allowedVisualizations || []; + const userActions = cfg.userActions || {}; + + // Inherited data semantics (the iron rule: all from the view) + the + // page's own always-on criteria (`filterBy`). + const filters = [ + ...(Array.isArray(view.filter) ? view.filter : []), + ...(Array.isArray(cfg.filterBy) ? cfg.filterBy : []), + ]; + + return { + type: 'list-view' as const, + objectName: objectDef.name, + viewType: (allowed[0] ?? view.type ?? 'grid'), + fields: view.columns, + ...(filters.length ? { filters } : {}), + ...(view.sort?.length ? { sort: view.sort } : {}), + grouping: view.grouping, + rowColor: view.rowColor, + pagination: view.pagination, + searchableFields: view.searchableFields, + emptyState: view.emptyState, + kanban: view.kanban, + calendar: view.calendar, + gallery: view.gallery, + timeline: view.timeline, + gantt: view.gantt, + + // Presentation policy — the page layer (ADR-0047). + userFilters: cfg.userFilters ?? view.userFilters, + appearance, + showViewSwitcher: allowed.length > 1, + showRecordCount: cfg.showRecordCount, + + // userActions toggles → toolbar flags. Interface mode is closed by + // default: the advanced filter builder and view-management tools are + // only present when the author opted in. + showSearch: userActions.search !== false, + showSort: userActions.sort !== false, + showFilters: userActions.filter === true, + showDensity: userActions.rowHeight === true, + showHideFields: false, + showGroup: false, + showColor: false, + allowExport: false, + inlineEdit: false, + }; + }, [objectDef, viewDef, cfg]); + + if (!objectDef || !schema) { + return ( +
+ +
+ +
+ {t('empty.objectNotFound', { defaultValue: 'Source object not found' })} + + {t('empty.interfacePageSourceMissing', { + defaultValue: 'This interface page references "{{name}}", which is not available.', + name: cfg.source || '?', + })} + +
+
+ ); + } + + return ( +
+
+

+ {typeof page.label === 'string' ? page.label : page.name} +

+ {typeof page.description === 'string' && page.description && ( +

{page.description}

+ )} +
+
+ +
+
+ ); +} diff --git a/packages/app-shell/src/views/PageView.tsx b/packages/app-shell/src/views/PageView.tsx index 373b4ee03..fe70722cd 100644 --- a/packages/app-shell/src/views/PageView.tsx +++ b/packages/app-shell/src/views/PageView.tsx @@ -19,6 +19,7 @@ import { useAuth } from '@object-ui/auth'; import { MetadataPanel, useMetadataInspector } from './MetadataInspector'; import { useMetadata } from '../providers/MetadataProvider'; import { ConsoleActionRuntimeProvider } from '../hooks/useConsoleActionRuntime'; +import { InterfaceListPage } from './InterfaceListPage'; export function PageView() { const { t } = useObjectTranslation(); @@ -91,14 +92,20 @@ export function PageView() { {t('common.editInStudio', { defaultValue: 'Edit in studio' })} )} - + {(page as any).interfaceConfig?.source ? ( + // ADR-0047 interface mode: the page binds a source view into a + // curated list surface — rendered directly, not via regions. + + ) : ( + + )} Date: Fri, 12 Jun 2026 20:22:16 +0500 Subject: [PATCH 3/3] =?UTF-8?q?fix(adr-0047):=20verification-round=20fixes?= =?UTF-8?q?=20=E2=80=94=20switcher=20schema=20fallback,=20hollow-view=20hy?= =?UTF-8?q?dration,=20getView=20type=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ListView reads showViewSwitcher from the schema too (ObjectView/ InterfaceListPage stamp it there; the prop-only read left the switcher permanently hidden) - InterfaceListPage: prefer source-view candidates that actually carry columns, and hydrate hollow ADR-0017 expansion items through listViewOverrides()/getView() — the same path ObjectView uses - data-objectstack getView(): pass 'view' as the metadata type instead of the object name (hit /meta// and always 404ed — latent since ADR-0017) All verified live in the browser: data mode (tabs + dropdowns + Grid/ Kanban/Gallery/Calendar switcher, tab filter 10→2 records) and interface mode (inherited columns, page-curated dropdowns only, locked grid, closed toolbar, dropdown filter 10→2 records). Co-Authored-By: Claude Fable 5 --- .../app-shell/src/views/InterfaceListPage.tsx | 56 ++++++++++++++++--- packages/data-objectstack/src/index.ts | 6 +- packages/plugin-list/src/ListView.tsx | 6 +- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/packages/app-shell/src/views/InterfaceListPage.tsx b/packages/app-shell/src/views/InterfaceListPage.tsx index 9a5c614fb..b40ad0c95 100644 --- a/packages/app-shell/src/views/InterfaceListPage.tsx +++ b/packages/app-shell/src/views/InterfaceListPage.tsx @@ -35,14 +35,18 @@ interface InterfaceListPageProps { */ function resolveSourceView(objectDef: any, sourceView?: string): any | undefined { const views: Record = objectDef?.listViews || objectDef?.list_views || {}; - if (sourceView) { - return ( - views[`${objectDef.name}.${sourceView}`] - ?? views[sourceView] - ?? (sourceView === 'default' || sourceView === 'list' ? objectDef?.list : undefined) - ); - } - return objectDef?.list ?? Object.values(views)[0]; + // ADR-0017 expansion can serve a default-view item with an empty config + // while the full body lives on `objectDef.list` — prefer candidates that + // actually carry columns over hollow name matches. + const candidates = sourceView + ? [ + views[`${objectDef?.name}.${sourceView}`], + views[sourceView], + ...(sourceView === 'default' || sourceView === 'list' ? [objectDef?.list] : []), + ] + : [objectDef?.list, ...Object.values(views)]; + const present = candidates.filter(Boolean); + return present.find((v: any) => v?.columns) ?? present[0]; } export function InterfaceListPage({ page, className }: InterfaceListPageProps) { @@ -55,11 +59,45 @@ export function InterfaceListPage({ page, className }: InterfaceListPageProps) { () => (objects || []).find((o: any) => o.name === cfg.source), [objects, cfg.source], ); - const viewDef = React.useMemo( + const resolvedView = React.useMemo( () => resolveSourceView(objectDef, cfg.sourceView), [objectDef, cfg.sourceView], ); + // The view list endpoint can serve hollow expansion items (no columns); + // the full body lives behind the per-view overlay API — the same + // hydration ObjectView performs. Only fetch when the resolution came up + // hollow. + const [hydratedView, setHydratedView] = React.useState(null); + React.useEffect(() => { + let cancelled = false; + setHydratedView(null); + if (!objectDef || !cfg.source || resolvedView?.columns) return; + const viewKey = resolvedView?.name + ?? (cfg.sourceView ? `${cfg.source}.${cfg.sourceView}` : undefined); + if (!viewKey) return; + (async () => { + try { + const ds: any = dataSource; + let full: any = null; + if (typeof ds?.listViewOverrides === 'function') { + const all = await ds.listViewOverrides(cfg.source); + full = all?.[viewKey] ?? null; + } + if (!full?.columns && typeof ds?.getView === 'function') { + full = await ds.getView(cfg.source, viewKey); + } + if (!cancelled && full && typeof full === 'object') setHydratedView(full); + } catch { /* hollow view stays hollow — renderer falls back to defaults */ } + })(); + return () => { cancelled = true; }; + }, [objectDef, cfg.source, cfg.sourceView, resolvedView, dataSource]); + + const viewDef = React.useMemo( + () => (hydratedView ? { ...resolvedView, ...hydratedView } : resolvedView), + [resolvedView, hydratedView], + ); + const schema = React.useMemo(() => { if (!objectDef) return undefined; const view = viewDef || {}; diff --git a/packages/data-objectstack/src/index.ts b/packages/data-objectstack/src/index.ts index 24efcfdc1..2145f29fe 100644 --- a/packages/data-objectstack/src/index.ts +++ b/packages/data-objectstack/src/index.ts @@ -1379,8 +1379,10 @@ export class ObjectStackAdapter implements DataSource { try { const cacheKey = `view:${objectName}:${viewId}`; return await this.metadataCache.get(cacheKey, async () => { - // Try meta.getItem for view metadata - const result: any = await this.client.meta.getItem(objectName, viewId); + // Views are an independent metadata type (ADR-0017) — the first + // getItem argument is the metadata TYPE, not the object name. + // (Passing objectName here hit /meta// and always 404ed.) + const result: any = await this.client.meta.getItem('view', viewId); if (result && result.item) return result.item; return result ?? null; }); diff --git a/packages/plugin-list/src/ListView.tsx b/packages/plugin-list/src/ListView.tsx index cd1a4ef3d..2d403569b 100644 --- a/packages/plugin-list/src/ListView.tsx +++ b/packages/plugin-list/src/ListView.tsx @@ -318,9 +318,13 @@ export const ListView = React.forwardRef(({ onHiddenFieldsChange, onColumnStateChange, onRowClick, - showViewSwitcher = false, + showViewSwitcher: showViewSwitcherProp, ...props }, ref) => { + // The switcher can be enabled either by the host component (prop) or by + // the schema itself (ADR-0047 — ObjectView/InterfaceListPage stamp it on + // the schema when appearance.allowedVisualizations whitelists >1 type). + const showViewSwitcher = showViewSwitcherProp ?? (propSchema as any)?.showViewSwitcher ?? false; // i18n support for record count and other labels const { t } = useListViewTranslation(); const { fieldLabel: resolveFieldLabel, actionLabel: resolveActionLabel } = useListFieldLabel();