diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 76555254a..79081e5fa 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -69,9 +69,33 @@ function checkLabelCase(label: string, path: string): LintIssue | null { return null; } +function getViewLabel(view: any, viewPath: string): { label?: string; path: string } { + if (view?.list?.label) { + return { label: view.list.label, path: `${viewPath}.list.label` }; + } + + const listViews = view?.listViews && typeof view.listViews === 'object' ? view.listViews : {}; + for (const [key, listView] of Object.entries(listViews)) { + if (listView?.label) { + return { label: listView.label, path: `${viewPath}.listViews.${key}.label` }; + } + } + + if (view?.list) { + return { path: `${viewPath}.list.label` }; + } + + const firstListViewKey = Object.keys(listViews)[0]; + if (firstListViewKey) { + return { path: `${viewPath}.listViews.${firstListViewKey}.label` }; + } + + return { path: `${viewPath}.list.label` }; +} + // ─── Lint Engine ──────────────────────────────────────────────────── -function lintConfig(config: any): LintIssue[] { +export function lintConfig(config: any): LintIssue[] { const issues: LintIssue[] = []; const push = (issue: LintIssue | null) => { @@ -144,9 +168,10 @@ function lintConfig(config: any): LintIssue[] { if (view.name) { push(checkSnakeCase(view.name, `${viewPath}.name`, 'View name')); } - push(checkLabelExists(view, `${viewPath}.label`, 'View')); - if (view.label) { - push(checkLabelCase(view.label, `${viewPath}.label`)); + const viewLabel = getViewLabel(view, viewPath); + push(checkLabelExists({ label: viewLabel.label, name: view.name }, viewLabel.path, 'View')); + if (viewLabel.label) { + push(checkLabelCase(viewLabel.label, viewLabel.path)); } } diff --git a/packages/cli/test/lint-view-label.test.ts b/packages/cli/test/lint-view-label.test.ts new file mode 100644 index 000000000..c8b255790 --- /dev/null +++ b/packages/cli/test/lint-view-label.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'vitest'; +import { lintConfig } from '../src/commands/lint'; + +describe('lint view labels', () => { + it('accepts canonical default list labels', () => { + const issues = lintConfig({ + views: [ + { + list: { + label: 'Accounts', + columns: ['name'], + }, + }, + ], + }); + + expect(issues.filter((issue) => issue.rule === 'required/label')).toEqual([]); + }); + + it('accepts canonical named list view labels', () => { + const issues = lintConfig({ + views: [ + { + listViews: { + all: { + label: 'All Accounts', + columns: ['name'], + }, + }, + }, + ], + }); + + expect(issues.filter((issue) => issue.rule === 'required/label')).toEqual([]); + }); + + it('reports a missing label at the schema-supported list label path', () => { + const issues = lintConfig({ + views: [ + { + list: { + columns: ['name'], + }, + }, + ], + }); + + expect(issues).toContainEqual(expect.objectContaining({ + rule: 'required/label', + path: 'views[0].list.label', + message: 'View "?" is missing a label', + })); + }); +});