Register column picker template governance

This commit is contained in:
rcourtman
2026-06-12 14:17:33 +01:00
parent 40af69bdb8
commit 52928706d9
3 changed files with 91 additions and 0 deletions
@@ -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.
@@ -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",
@@ -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', () => {