From 52928706d90fcad3e4492a48f8a13c7918150722 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 12 Jun 2026 14:17:33 +0100 Subject: [PATCH] Register column picker template governance --- .../subsystems/frontend-primitives.md | 5 ++ .../scripts/shared-template-registry.json | 37 ++++++++++++++ .../SharedPrimitives.guardrails.test.ts | 49 +++++++++++++++++++ 3 files changed, 91 insertions(+) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index f5e6d8f2f0..1971dc0d19 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2421,6 +2421,11 @@ Chart visibility display actions are registry-backed too. pressed-state, title, icon, and toolbar action styling for summary-bearing filter surfaces. Pages must compose that primitive instead of recreating local chart visibility buttons or one-option segmented controls. +Column visibility controls are registry-backed too. `ColumnPicker` owns the +column chooser trigger, panel title, reset action, empty-state copy, hidden +count badge, dropdown width, and outside-click lifecycle through the shared +shell/state/model split. Table surfaces must compose that primitive instead of +recreating local column chooser buttons or panels. Grouped/list table-mode controls are registry-backed as well. `GroupedTableModeSegmentedControl` owns the shared `Group by` label, `Grouped` / `List` labels, tooltip titles, and icons for table mode switching. diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 8f05e1ba26..399effaa3b 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -255,6 +255,24 @@ "scripts/shared-template-audit.mjs" ] }, + { + "id": "column-picker-shell", + "category": "column-picker", + "summary": "Table column visibility controls must compose ColumnPicker instead of recreating column chooser trigger, panel, reset, or hidden-count chrome locally.", + "canonical": { + "path": "src/components/shared/ColumnPicker.tsx", + "export": "ColumnPicker" + }, + "requiredConsumers": [ + { "path": "src/components/Workloads/WorkloadsFilter.tsx" }, + { "path": "src/features/standalone/AgentsMachinesTable.tsx" } + ], + "proof": [ + "src/components/shared/SharedPrimitives.guardrails.test.ts", + "src/components/shared/ColumnPicker.test.tsx", + "scripts/shared-template-audit.mjs" + ] + }, { "id": "grouped-table-mode-segmented-control", "category": "table-mode-control", @@ -387,6 +405,25 @@ "scripts/shared-template-audit.mjs" ] }, + { + "id": "column-picker-local-column-chooser", + "category": "column-picker", + "summary": "Table surfaces must not copy the column chooser title and panel copy owned by ColumnPicker.", + "canonical": { + "path": "src/components/shared/ColumnPicker.tsx", + "export": "ColumnPicker" + }, + "scopes": ["src/components", "src/features", "src/pages"], + "extensions": [".tsx"], + "allPatterns": ["Choose which columns to display", "Show Columns"], + "legacyReason": "Retired migration debt. Table column visibility controls must use ColumnPicker; local column chooser copies are not allowed.", + "allowedPaths": [], + "ignoredPaths": ["src/components/shared/ColumnPicker.test.tsx"], + "proof": [ + "src/components/shared/SharedPrimitives.guardrails.test.ts", + "scripts/shared-template-audit.mjs" + ] + }, { "id": "grouped-table-mode-local-segmented-control", "category": "table-mode-control", diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index f92c337e61..fd0b50483a 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -778,8 +778,48 @@ describe('shared primitive guardrails', () => { }); it('keeps column picker on shell, runtime, and model owners', () => { + const registry = JSON.parse(sharedTemplateRegistrySource) as { + rules?: Array<{ + id: string; + canonical?: { path?: string; export?: string }; + requiredConsumers?: Array<{ path?: string }>; + }>; + patternGuards?: Array<{ + id: string; + canonical?: { path?: string; export?: string }; + allPatterns?: string[]; + scopes?: string[]; + allowedPaths?: string[]; + ignoredPaths?: string[]; + }>; + }; + const registeredRule = registry.rules?.find((rule) => rule.id === 'column-picker-shell'); + const registeredGuard = registry.patternGuards?.find( + (guard) => guard.id === 'column-picker-local-column-chooser', + ); + + expect(registeredRule?.canonical?.path).toBe('src/components/shared/ColumnPicker.tsx'); + expect(registeredRule?.canonical?.export).toBe('ColumnPicker'); + expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([ + 'src/components/Workloads/WorkloadsFilter.tsx', + 'src/features/standalone/AgentsMachinesTable.tsx', + ]); + expect(registeredGuard?.canonical?.path).toBe('src/components/shared/ColumnPicker.tsx'); + expect(registeredGuard?.canonical?.export).toBe('ColumnPicker'); + expect(registeredGuard?.allPatterns).toEqual([ + 'Choose which columns to display', + 'Show Columns', + ]); + expect(registeredGuard?.allowedPaths ?? []).toHaveLength(0); + expect(registeredGuard?.ignoredPaths).toEqual(['src/components/shared/ColumnPicker.test.tsx']); + expect(registeredGuard?.scopes).toEqual( + expect.arrayContaining(['src/components', 'src/features', 'src/pages']), + ); + expect(columnPickerSource).toContain('useColumnPickerState'); expect(columnPickerSource).toContain('COLUMN_PICKER_PANEL_TITLE'); + expect(columnPickerSource).toContain('COLUMN_PICKER_BUTTON_TITLE'); + expect(columnPickerSource).toContain('filterUtilityBadgeClass'); expect(columnPickerSource).not.toContain('createSignal'); expect(columnPickerSource).not.toContain('createEffect'); expect(columnPickerSource).not.toContain('document.addEventListener'); @@ -793,10 +833,19 @@ describe('shared primitive guardrails', () => { expect(columnPickerStateSource).toContain('hiddenCount'); expect(columnPickerModelSource).toContain('COLUMN_PICKER_BUTTON_LABEL'); + expect(columnPickerModelSource).toContain('COLUMN_PICKER_BUTTON_TITLE'); expect(columnPickerModelSource).toContain('COLUMN_PICKER_PANEL_TITLE'); + expect(columnPickerModelSource).toContain('COLUMN_PICKER_RESET_LABEL'); + expect(columnPickerModelSource).toContain('COLUMN_PICKER_EMPTY_LABEL'); expect(columnPickerModelSource).toContain('getHiddenColumnCount'); expect(columnPickerModelSource).toContain('shouldShowColumnPickerReset'); expect(columnPickerModelSource).toContain('getColumnPickerOptionTextClass'); + + for (const source of [workloadsFilterSource, agentsMachinesTableSource]) { + expect(source).toContain('ColumnPicker'); + expect(source).not.toContain('Choose which columns to display'); + expect(source).not.toContain('Show Columns'); + } }); it('keeps tag input on shell, runtime, and model owners', () => {