diff --git a/ROADMAP.md b/ROADMAP.md index 2d2c24af4..52142eb7d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -404,6 +404,11 @@ ObjectUI is a universal Server-Driven UI (SDUI) engine built on React + Tailwind - [x] 18 new ViewConfigPanel interaction tests: collapseAllByDefault, showDescription, clickIntoRecordDetails, addDeleteRecordsInline toggles; sharing visibility conditional hide; navigation width/openNewTab conditional rendering; all 5 rowHeight button clicks; boundary tests (empty actions, long labels, special chars); pageSizeOptions input; densityMode/ARIA live enums; addRecord conditional sub-editor; sharing visibility select - [x] 8 new schema-driven spec tests: accessibility field ordering, emptyState compound field, switch field defaults, comprehensive visibleWhen predicates (sharing, navigation width, navigation openNewTab) - [x] All spec fields verified: Appearance/UserActions/Sharing/Accessibility sections 100% covered with UI controls, defaults, ordering, and conditional visibility +- [x] Add `description` field to `NamedListView` protocol interface (spec alignment) +- [x] Add `disabledWhen` predicate to `ConfigField` type — grid-only fields (striped/bordered/wrapHeaders/resizable) disabled for non-grid views +- [x] Add `expandedSections` prop to `ConfigPanelRenderer` for external section collapse control (auto-focus/highlight) +- [x] Add `helpText` to navigation-dependent fields (width/openNewTab) with i18n hints (all 11 locales) +- [x] 24 new tests: expandedSections override (3), disabledWhen evaluation (2), grid-only disabledWhen predicates (16), helpText validation (2), description spec alignment (1) **Code Reduction:** ~1655 lines imperative → ~170 lines declarative wrapper + ~1100 lines schema factory + ~180 lines shared utils = **>50% net reduction in component code** with significantly improved maintainability diff --git a/apps/console/src/__tests__/view-config-schema.test.tsx b/apps/console/src/__tests__/view-config-schema.test.tsx index 38acf2680..f5bb11dfe 100644 --- a/apps/console/src/__tests__/view-config-schema.test.tsx +++ b/apps/console/src/__tests__/view-config-schema.test.tsx @@ -701,11 +701,16 @@ describe('spec alignment', () => { it('documents UI extension fields not in NamedListView spec', () => { const keys = allFieldKeys(); // These fields are UI extensions — documented as protocol suggestions - const uiExtensions = ['description', '_source', '_groupBy', '_typeOptions']; + const uiExtensions = ['_source', '_groupBy', '_typeOptions']; for (const ext of uiExtensions) { expect(keys).toContain(ext); } }); + + it('description is now a spec-aligned field in NamedListView', () => { + const keys = allFieldKeys(); + expect(keys).toContain('description'); + }); }); // ── Accessibility section field ordering ───────────────────────────── @@ -800,4 +805,80 @@ describe('spec alignment', () => { expect(field.visibleWhen!({})).toBe(true); }); }); + + // ── disabledWhen predicates for grid-only fields ───────────────────── + describe('disabledWhen predicates for grid-only fields', () => { + function buildSchema() { + return buildViewConfigSchema({ + t: mockT, + fieldOptions: mockFieldOptions, + objectDef: mockObjectDef, + updateField: mockUpdateField, + filterGroupValue: mockFilterGroup, + sortItemsValue: mockSortItems, + }); + } + + const gridOnlyFields = ['striped', 'bordered', 'wrapHeaders', 'resizable']; + + for (const fieldKey of gridOnlyFields) { + it(`${fieldKey} should have disabledWhen predicate`, () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === fieldKey)!; + expect(field.disabledWhen).toBeDefined(); + }); + + it(`${fieldKey} should be disabled when view type is kanban`, () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === fieldKey)!; + expect(field.disabledWhen!({ type: 'kanban' })).toBe(true); + }); + + it(`${fieldKey} should not be disabled when view type is grid`, () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === fieldKey)!; + expect(field.disabledWhen!({ type: 'grid' })).toBe(false); + }); + + it(`${fieldKey} should not be disabled when view type is undefined (default grid)`, () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'appearance')!; + const field = section.fields.find(f => f.key === fieldKey)!; + expect(field.disabledWhen!({})).toBe(false); + }); + } + }); + + // ── helpText on navigation-dependent fields ────────────────────────── + describe('helpText on navigation-dependent fields', () => { + function buildSchema() { + return buildViewConfigSchema({ + t: mockT, + fieldOptions: mockFieldOptions, + objectDef: mockObjectDef, + updateField: mockUpdateField, + filterGroupValue: mockFilterGroup, + sortItemsValue: mockSortItems, + }); + } + + it('navigation width field has helpText', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'pageConfig')!; + const field = section.fields.find(f => f.key === '_navigationWidth')!; + expect(field.helpText).toBeDefined(); + expect(typeof field.helpText).toBe('string'); + }); + + it('navigation openNewTab field has helpText', () => { + const schema = buildSchema(); + const section = schema.sections.find(s => s.key === 'pageConfig')!; + const field = section.fields.find(f => f.key === '_navigationOpenNewTab')!; + expect(field.helpText).toBeDefined(); + expect(typeof field.helpText).toBe('string'); + }); + }); }); diff --git a/apps/console/src/utils/view-config-schema.tsx b/apps/console/src/utils/view-config-schema.tsx index b59087894..e22acd423 100644 --- a/apps/console/src/utils/view-config-schema.tsx +++ b/apps/console/src/utils/view-config-schema.tsx @@ -123,8 +123,7 @@ function buildPageConfigSection( ), }, - // UI extension: description — not in NamedListView spec. - // Protocol suggestion: add optional 'description' to NamedListView. + // spec: NamedListView.description — optional view description { key: 'description', label: t('console.objectView.description'), @@ -284,6 +283,7 @@ function buildPageConfigSection( key: '_navigationWidth', label: t('console.objectView.navigationWidth'), type: 'custom', + helpText: t('console.objectView.navigationWidthHint'), visibleWhen: (draft) => ['drawer', 'modal', 'split'].includes(draft.navigation?.mode || 'page'), render: (_value, _onChange, draft) => { const navMode = draft.navigation?.mode || 'page'; @@ -307,6 +307,7 @@ function buildPageConfigSection( key: '_navigationOpenNewTab', label: t('console.objectView.openNewTab'), type: 'custom', + helpText: t('console.objectView.openNewTabHint'), visibleWhen: (draft) => ['page', 'new_window'].includes(draft.navigation?.mode || 'page'), render: (_value, _onChange, draft) => { const navMode = draft.navigation?.mode || 'page'; @@ -972,10 +973,12 @@ function buildAppearanceSection( title: t('console.objectView.appearance'), collapsible: true, fields: [ - // spec: NamedListView.striped - buildSwitchField('striped', t('console.objectView.striped'), 'toggle-striped', false, true), - // spec: NamedListView.bordered - buildSwitchField('bordered', t('console.objectView.bordered'), 'toggle-bordered', false, true), + // spec: NamedListView.striped (grid-only) + buildSwitchField('striped', t('console.objectView.striped'), 'toggle-striped', false, true, + (draft) => draft.type != null && draft.type !== 'grid'), + // spec: NamedListView.bordered (grid-only) + buildSwitchField('bordered', t('console.objectView.bordered'), 'toggle-bordered', false, true, + (draft) => draft.type != null && draft.type !== 'grid'), // spec: NamedListView.color — field for row/card coloring { key: 'color', @@ -997,8 +1000,9 @@ function buildAppearanceSection( ), }, - // spec: NamedListView.wrapHeaders - buildSwitchField('wrapHeaders', t('console.objectView.wrapHeaders'), 'toggle-wrapHeaders', false, true), + // spec: NamedListView.wrapHeaders (grid-only) + buildSwitchField('wrapHeaders', t('console.objectView.wrapHeaders'), 'toggle-wrapHeaders', false, true, + (draft) => draft.type != null && draft.type !== 'grid'), // spec: NamedListView.collapseAllByDefault buildSwitchField('collapseAllByDefault', t('console.objectView.collapseAllByDefault'), 'toggle-collapseAllByDefault', false, true), // spec: NamedListView.fieldTextColor @@ -1024,8 +1028,9 @@ function buildAppearanceSection( }, // spec: NamedListView.showDescription buildSwitchField('showDescription', t('console.objectView.showFieldDescriptions'), 'toggle-showDescription', true), - // spec: NamedListView.resizable - buildSwitchField('resizable', t('console.objectView.resizableColumns'), 'toggle-resizable', false, true), + // spec: NamedListView.resizable (grid-only) + buildSwitchField('resizable', t('console.objectView.resizableColumns'), 'toggle-resizable', false, true, + (draft) => draft.type != null && draft.type !== 'grid'), // spec: NamedListView.densityMode — compact/comfortable/spacious { key: 'densityMode', @@ -1440,17 +1445,27 @@ function buildAccessibilitySection( * Build a standard Switch toggle field with custom testId. * @param defaultOn - if true, treat undefined/absent as enabled (checked = value !== false) * @param explicitTrue - if true, only check when value === true + * @param disabledWhen - optional predicate to disable the switch based on draft state */ -function buildSwitchField(key: string, label: string, testId: string, defaultOn = false, explicitTrue = false): ConfigField { +function buildSwitchField( + key: string, + label: string, + testId: string, + defaultOn = false, + explicitTrue = false, + disabledWhen?: (draft: Record) => boolean, +): ConfigField { return { key, label, type: 'custom', - render: (value, onChange) => ( + disabledWhen, + render: (value, onChange, draft) => ( onChange(checked)} className="scale-75" /> diff --git a/packages/components/src/__tests__/config-field-renderer.test.tsx b/packages/components/src/__tests__/config-field-renderer.test.tsx index 9d826b441..b811b0361 100644 --- a/packages/components/src/__tests__/config-field-renderer.test.tsx +++ b/packages/components/src/__tests__/config-field-renderer.test.tsx @@ -271,4 +271,37 @@ describe('ConfigFieldRenderer', () => { expect(screen.getByText('Add sort')).toBeDefined(); }); }); + + describe('helpText rendering', () => { + it('should render helpText below field when provided', () => { + const field: ConfigField = { + key: 'width', + label: 'Width', + type: 'input', + helpText: 'Available for drawer, modal, and split modes', + }; + render(); + expect(screen.getByText('Available for drawer, modal, and split modes')).toBeDefined(); + }); + + it('should not render helpText paragraph when not provided', () => { + const field: ConfigField = { key: 'name', label: 'Name', type: 'input' }; + const { container } = render(); + expect(container.querySelectorAll('p').length).toBe(0); + }); + + it('should render helpText for custom field type', () => { + const React = require('react'); + const field: ConfigField = { + key: 'custom', + label: 'Custom', + type: 'custom', + helpText: 'Custom help text', + render: (value, onChange) => React.createElement('div', { 'data-testid': 'custom-content' }, 'Custom'), + }; + render(); + expect(screen.getByText('Custom help text')).toBeDefined(); + expect(screen.getByTestId('custom-content')).toBeDefined(); + }); + }); }); diff --git a/packages/components/src/__tests__/config-panel-renderer.test.tsx b/packages/components/src/__tests__/config-panel-renderer.test.tsx index 3346b648c..5558e51de 100644 --- a/packages/components/src/__tests__/config-panel-renderer.test.tsx +++ b/packages/components/src/__tests__/config-panel-renderer.test.tsx @@ -317,4 +317,110 @@ describe('ConfigPanelRenderer', () => { expect(onFieldChange).toHaveBeenCalledWith('name', 'NewName'); }); }); + + describe('expandedSections prop', () => { + it('should override defaultCollapsed when section is in expandedSections', () => { + render( + , + ); + // Appearance is defaultCollapsed=true but expandedSections overrides it + expect(screen.getByText('Theme')).toBeDefined(); + }); + + it('should not affect sections not in expandedSections', () => { + render( + , + ); + // Data section is not in expandedSections, should remain expanded (its default) + expect(screen.getByText('Source')).toBeDefined(); + }); + + it('should allow local toggle to still work alongside expandedSections', () => { + render( + , + ); + // Appearance is forced expanded by expandedSections + expect(screen.getByText('Theme')).toBeDefined(); + + // Data section can still be toggled locally + expect(screen.getByText('Source')).toBeDefined(); + fireEvent.click(screen.getByTestId('section-header-data')); + expect(screen.queryByText('Source')).toBeNull(); + }); + }); + + describe('disabledWhen on fields', () => { + it('should disable input field when disabledWhen returns true', () => { + const schemaWithDisabledWhen: ConfigPanelSchema = { + breadcrumb: ['Test'], + sections: [ + { + key: 'sec', + title: 'Section', + fields: [ + { + key: 'name', + label: 'Name', + type: 'input', + disabledWhen: (draft) => draft.locked === true, + }, + ], + }, + ], + }; + render( + , + ); + const input = screen.getByTestId('config-field-name'); + expect((input as HTMLInputElement).disabled).toBe(true); + }); + + it('should enable input field when disabledWhen returns false', () => { + const schemaWithDisabledWhen: ConfigPanelSchema = { + breadcrumb: ['Test'], + sections: [ + { + key: 'sec', + title: 'Section', + fields: [ + { + key: 'name', + label: 'Name', + type: 'input', + disabledWhen: (draft) => draft.locked === true, + }, + ], + }, + ], + }; + render( + , + ); + const input = screen.getByTestId('config-field-name'); + expect((input as HTMLInputElement).disabled).toBe(false); + }); + }); }); diff --git a/packages/components/src/custom/config-field-renderer.tsx b/packages/components/src/custom/config-field-renderer.tsx index 9a9e4aa82..29c0e459f 100644 --- a/packages/components/src/custom/config-field-renderer.tsx +++ b/packages/components/src/custom/config-field-renderer.tsx @@ -57,55 +57,61 @@ export function ConfigFieldRenderer({ return null; } + const effectiveDisabled = field.disabled || (field.disabledWhen ? field.disabledWhen(draft) : false); const effectiveValue = value ?? field.defaultValue; + let content: React.ReactNode = null; + switch (field.type) { case 'input': - return ( + content = ( onChange(e.target.value)} /> ); + break; case 'switch': - return ( + content = ( onChange(checked)} className="scale-75" /> ); + break; case 'checkbox': - return ( + content = ( onChange(checked)} /> ); + break; case 'select': - return ( + content = ( onChange(e.target.value)} /> ); + break; case 'icon-group': - return ( + content = (
{(field.options ?? []).map((opt) => ( @@ -170,7 +179,7 @@ export function ConfigFieldRenderer({ size="sm" variant={effectiveValue === opt.value ? 'default' : 'ghost'} className={cn('h-7 w-7 p-0', effectiveValue === opt.value && 'ring-1 ring-primary')} - disabled={field.disabled} + disabled={effectiveDisabled} onClick={() => onChange(opt.value)} title={opt.label} > @@ -180,20 +189,22 @@ export function ConfigFieldRenderer({
); + break; case 'field-picker': - return ( + content = ( { + onClick={effectiveDisabled ? undefined : () => { /* open field picker - consumers should use type='custom' for full integration */ }} /> ); + break; case 'filter': - return ( + content = (
); + break; case 'sort': - return ( + content = (
); + break; case 'custom': if (field.render) { - return <>{field.render(effectiveValue, onChange, draft)}; + content = <>{field.render(effectiveValue, onChange, draft)}; } - return null; + break; default: - return null; + break; } + + if (!content) return null; + + // Wrap with helpText when provided + if (field.helpText) { + return ( +
+ {content} +

{field.helpText}

+
+ ); + } + + return <>{content}; } diff --git a/packages/components/src/custom/config-panel-renderer.tsx b/packages/components/src/custom/config-panel-renderer.tsx index 6e0aeaafc..be2284b4b 100644 --- a/packages/components/src/custom/config-panel-renderer.tsx +++ b/packages/components/src/custom/config-panel-renderer.tsx @@ -61,6 +61,8 @@ export interface ConfigPanelRendererProps { saveTestId?: string; /** Override data-testid for the discard button (default: "config-panel-discard") */ discardTestId?: string; + /** Externally-controlled set of section keys that should be expanded (overrides local collapse state) */ + expandedSections?: string[]; } /** @@ -97,6 +99,7 @@ export function ConfigPanelRenderer({ footerTestId, saveTestId, discardTestId, + expandedSections, }: ConfigPanelRendererProps) { const [collapsed, setCollapsed] = useState>({}); @@ -109,6 +112,14 @@ export function ConfigPanelRenderer({ })); }; + // Resolve effective collapsed state: expandedSections prop overrides local state + const isCollapsed = (sectionKey: string, defaultCollapsed?: boolean): boolean => { + if (expandedSections && expandedSections.includes(sectionKey)) { + return false; + } + return collapsed[sectionKey] ?? defaultCollapsed ?? false; + }; + return (
{ if (section.visibleWhen && !section.visibleWhen(draft)) return null; - const isCollapsed = - collapsed[section.key] ?? section.defaultCollapsed ?? false; + const sectionCollapsed = isCollapsed(section.key, section.defaultCollapsed); return (
toggleCollapse(section.key, section.defaultCollapsed)} testId={`section-header-${section.key}`} /> @@ -176,7 +186,7 @@ export function ConfigPanelRenderer({ {section.hint}

)} - {!isCollapsed && ( + {!sectionCollapsed && (
{section.fields.map((field) => ( ; /** Visibility predicate evaluated against the current draft */ visibleWhen?: (draft: Record) => boolean; + /** Disabled predicate evaluated against the current draft */ + disabledWhen?: (draft: Record) => boolean; /** Custom render function for type='custom' */ render?: ( value: any, diff --git a/packages/i18n/src/locales/ar.ts b/packages/i18n/src/locales/ar.ts index 44ea0667c..311e5b068 100644 --- a/packages/i18n/src/locales/ar.ts +++ b/packages/i18n/src/locales/ar.ts @@ -398,6 +398,11 @@ const ar = { inlineEdit: 'تحرير السجلات مباشرة', addDeleteRecordsInline: 'إضافة/حذف السجلات مباشرة', clickIntoRecordDetails: 'انقر لعرض تفاصيل السجل', + navigationMode: 'وضع التنقل', + navigationWidth: 'عرض التنقل', + navigationWidthHint: 'متاح لأوضاع التنقل: الدرج والنافذة المنبثقة والعرض المقسم', + openNewTab: 'فتح في علامة تبويب جديدة', + openNewTabHint: 'متاح لأوضاع التنقل: الصفحة والنافذة الجديدة', }, localeSwitcher: { label: 'اللغة', diff --git a/packages/i18n/src/locales/de.ts b/packages/i18n/src/locales/de.ts index dfd515352..6b3a5ddaa 100644 --- a/packages/i18n/src/locales/de.ts +++ b/packages/i18n/src/locales/de.ts @@ -402,6 +402,11 @@ const de = { inlineEdit: 'Datensätze inline bearbeiten', addDeleteRecordsInline: 'Datensätze inline hinzufügen/löschen', clickIntoRecordDetails: 'Klicken für Datensatzdetails', + navigationMode: 'Navigationsmodus', + navigationWidth: 'Navigationsbreite', + navigationWidthHint: 'Verfügbar für Schublade-, Modal- und Split-Navigationsmodi', + openNewTab: 'In neuem Tab öffnen', + openNewTabHint: 'Verfügbar für Seiten- und Neues-Fenster-Navigationsmodi', }, localeSwitcher: { label: 'Sprache', diff --git a/packages/i18n/src/locales/en.ts b/packages/i18n/src/locales/en.ts index 378bf6d1b..5baca830b 100644 --- a/packages/i18n/src/locales/en.ts +++ b/packages/i18n/src/locales/en.ts @@ -404,7 +404,9 @@ const en = { clickIntoRecordDetails: 'Click into record details', navigationMode: 'Navigation mode', navigationWidth: 'Navigation width', + navigationWidthHint: 'Available for drawer, modal, and split navigation modes', openNewTab: 'Open in new tab', + openNewTabHint: 'Available for page and new window navigation modes', selectionMode: 'Selection mode', selectionNone: 'None', selectionSingle: 'Single', diff --git a/packages/i18n/src/locales/es.ts b/packages/i18n/src/locales/es.ts index 2efdeb91d..4921bba24 100644 --- a/packages/i18n/src/locales/es.ts +++ b/packages/i18n/src/locales/es.ts @@ -397,6 +397,11 @@ const es = { inlineEdit: 'Editar registros en línea', addDeleteRecordsInline: 'Agregar/eliminar registros en línea', clickIntoRecordDetails: 'Clic para detalles del registro', + navigationMode: 'Modo de navegación', + navigationWidth: 'Ancho de navegación', + navigationWidthHint: 'Disponible para los modos de navegación cajón, modal y dividido', + openNewTab: 'Abrir en nueva pestaña', + openNewTabHint: 'Disponible para los modos de navegación página y nueva ventana', }, localeSwitcher: { label: 'Idioma', diff --git a/packages/i18n/src/locales/fr.ts b/packages/i18n/src/locales/fr.ts index 777939ff6..80af93389 100644 --- a/packages/i18n/src/locales/fr.ts +++ b/packages/i18n/src/locales/fr.ts @@ -402,6 +402,11 @@ const fr = { inlineEdit: 'Modifier les enregistrements en ligne', addDeleteRecordsInline: 'Ajouter/supprimer des enregistrements en ligne', clickIntoRecordDetails: "Cliquer pour les détails de l'enregistrement", + navigationMode: 'Mode de navigation', + navigationWidth: 'Largeur de navigation', + navigationWidthHint: 'Disponible pour les modes de navigation tiroir, modale et partagée', + openNewTab: 'Ouvrir dans un nouvel onglet', + openNewTabHint: 'Disponible pour les modes de navigation page et nouvelle fenêtre', }, localeSwitcher: { label: 'Langue', diff --git a/packages/i18n/src/locales/ja.ts b/packages/i18n/src/locales/ja.ts index d95a71ee8..aeabe6233 100644 --- a/packages/i18n/src/locales/ja.ts +++ b/packages/i18n/src/locales/ja.ts @@ -397,6 +397,11 @@ const ja = { inlineEdit: 'インラインでレコードを編集', addDeleteRecordsInline: 'インラインでレコードを追加/削除', clickIntoRecordDetails: 'レコード詳細をクリックで表示', + navigationMode: 'ナビゲーションモード', + navigationWidth: 'ナビゲーション幅', + navigationWidthHint: 'ドロワー、モーダル、スプリットナビゲーションモードで使用可能', + openNewTab: '新しいタブで開く', + openNewTabHint: 'ページおよび新しいウィンドウナビゲーションモードで使用可能', }, localeSwitcher: { label: '言語', diff --git a/packages/i18n/src/locales/ko.ts b/packages/i18n/src/locales/ko.ts index fcbec2c9d..2c7fcc068 100644 --- a/packages/i18n/src/locales/ko.ts +++ b/packages/i18n/src/locales/ko.ts @@ -397,6 +397,11 @@ const ko = { inlineEdit: '인라인으로 레코드 편집', addDeleteRecordsInline: '인라인으로 레코드 추가/삭제', clickIntoRecordDetails: '레코드 상세 보기 클릭', + navigationMode: '탐색 모드', + navigationWidth: '탐색 너비', + navigationWidthHint: '서랍, 모달, 분할 탐색 모드에서 사용 가능', + openNewTab: '새 탭에서 열기', + openNewTabHint: '페이지 및 새 창 탐색 모드에서 사용 가능', }, localeSwitcher: { label: '언어', diff --git a/packages/i18n/src/locales/pt.ts b/packages/i18n/src/locales/pt.ts index 576f8fefc..a7886bd1c 100644 --- a/packages/i18n/src/locales/pt.ts +++ b/packages/i18n/src/locales/pt.ts @@ -397,6 +397,11 @@ const pt = { inlineEdit: 'Editar registros em linha', addDeleteRecordsInline: 'Adicionar/excluir registros em linha', clickIntoRecordDetails: 'Clique para detalhes do registro', + navigationMode: 'Modo de navegação', + navigationWidth: 'Largura de navegação', + navigationWidthHint: 'Disponível para os modos de navegação gaveta, modal e dividido', + openNewTab: 'Abrir em nova aba', + openNewTabHint: 'Disponível para os modos de navegação página e nova janela', }, localeSwitcher: { label: 'Idioma', diff --git a/packages/i18n/src/locales/ru.ts b/packages/i18n/src/locales/ru.ts index bc7211021..45221dbd9 100644 --- a/packages/i18n/src/locales/ru.ts +++ b/packages/i18n/src/locales/ru.ts @@ -397,6 +397,11 @@ const ru = { inlineEdit: 'Редактировать записи встроенно', addDeleteRecordsInline: 'Добавлять/удалять записи встроенно', clickIntoRecordDetails: 'Нажмите для просмотра деталей записи', + navigationMode: 'Режим навигации', + navigationWidth: 'Ширина навигации', + navigationWidthHint: 'Доступно для режимов навигации: выдвижная панель, модальное окно и разделённый вид', + openNewTab: 'Открыть в новой вкладке', + openNewTabHint: 'Доступно для режимов навигации: страница и новое окно', }, localeSwitcher: { label: 'Язык', diff --git a/packages/i18n/src/locales/zh.ts b/packages/i18n/src/locales/zh.ts index 0c700a1b5..16e39ec36 100644 --- a/packages/i18n/src/locales/zh.ts +++ b/packages/i18n/src/locales/zh.ts @@ -404,7 +404,9 @@ const zh = { clickIntoRecordDetails: '点击进入记录详情', navigationMode: '导航模式', navigationWidth: '导航宽度', + navigationWidthHint: '适用于抽屉、弹窗和分屏导航模式', openNewTab: '在新标签页打开', + openNewTabHint: '适用于页面和新窗口导航模式', selectionMode: '选择模式', selectionNone: '无', selectionSingle: '单选', diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 43f05ac21..122fb99cd 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -1129,6 +1129,9 @@ export interface NamedListView { /** Prefix field displayed before the main title */ prefixField?: string; + /** View description */ + description?: string; + /** Show field descriptions below headers @default false */ showDescription?: boolean;