From 1a8b401673992b393cd8953c456c6e704dd04b2d Mon Sep 17 00:00:00 2001 From: Faisal Ahammad Date: Tue, 13 Oct 2026 02:55:25 +0600 Subject: [PATCH] feat: show snippet run location in list and card views --- src/css/manage/_snippets-table.scss | 15 ++- .../SnippetsTable/ManageSnippetCard.tsx | 9 +- .../ManageMenu/SnippetsTable/TableColumns.tsx | 24 +++- src/js/utils/snippets/snippets.ts | 4 + .../Manage/Manage_Menu_Screen_Options.php | 1 + tests/e2e/code-snippets-list.spec.ts | 116 ++++++++++++++++++ tests/e2e/helpers/SnippetsTestHelper.ts | 5 + .../Manage_Menu_Screen_Options_Test.php | 1 + 8 files changed, 169 insertions(+), 6 deletions(-) diff --git a/src/css/manage/_snippets-table.scss b/src/css/manage/_snippets-table.scss index 6a39d5901..00bdede61 100644 --- a/src/css/manage/_snippets-table.scss +++ b/src/css/manage/_snippets-table.scss @@ -3,7 +3,8 @@ @use '../common/theme'; .column-name, -.column-type { +.column-type, +.snippet-location { .dashicons { font-size: 16px; inline-size: 16px; @@ -16,6 +17,14 @@ } } +.snippet-location { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--cs-color-text-muted); + white-space: nowrap; +} + // `.wp-list-table` prefix keeps this above WP 7.0's 40px control height so the boxed // priority field stays 30px tall (below the 38px control token, which would stretch the // table rows) and vertically aligned with the rest of the row. @@ -261,6 +270,10 @@ color: var(--cs-color-text-muted); } + .snippet-card-location { + inline-size: 100%; + } + .snippet-card-tags a { color: var(--cs-color-accent); text-decoration: none; diff --git a/src/js/components/ManageMenu/SnippetsTable/ManageSnippetCard.tsx b/src/js/components/ManageMenu/SnippetsTable/ManageSnippetCard.tsx index 3648a397e..1768c0d91 100644 --- a/src/js/components/ManageMenu/SnippetsTable/ManageSnippetCard.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/ManageSnippetCard.tsx @@ -8,13 +8,13 @@ import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI' import { useSnippetsList } from '../../../hooks/useSnippetsList' import { handleUnknownError } from '../../../utils/errors' import { downloadSnippetExportFile } from '../../../utils/files' -import { canModifySnippet, cloneSnippetObject, getSnippetDisplayName, getSnippetEditUrl, getSnippetType, isNetworkOnlySnippet, isSnippetActive } from '../../../utils/snippets/snippets' +import { canModifySnippet, cloneSnippetObject, getSnippetDisplayName, getSnippetEditUrl, getSnippetLocation, getSnippetType, isNetworkOnlySnippet, isSnippetActive } from '../../../utils/snippets/snippets' import { Button } from '../../common/Button' import { KebabMenu, KebabMenuDivider, KebabMenuItem, KebabMenuRow } from '../../common/KebabMenu' import { SnippetCard } from '../../common/snippets/SnippetCard' import { SnippetPreviewModal } from '../../common/snippets/SnippetPreviewModal' import { useFilteredSnippets } from './WithFilteredSnippetsContext' -import { ActivateColumn, PriorityColumn, SnippetExtraIcons, SnippetName, TagsColumn, TypeColumn } from './TableColumns' +import { ActivateColumn, LocationColumn, PriorityColumn, SnippetExtraIcons, SnippetName, TagsColumn, TypeColumn } from './TableColumns' import type { Snippet } from '../../../types/Snippet' interface SnippetCardActionsProps { @@ -192,6 +192,7 @@ export const ManageSnippetCard: React.FC = ({ onSelectedChange }) => { const { activeByCondition } = useFilteredSnippets() + const location = getSnippetLocation(snippet) return ( = ({
- {(0 < snippet.tags.length || !!snippet.modified) && ( + {(0 < snippet.tags.length || !!snippet.modified || !!location) && (
+ {location && ( +
)} {0 < snippet.tags.length && ( diff --git a/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx b/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx index 6755e12da..39cbf5868 100644 --- a/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx +++ b/src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx @@ -6,7 +6,7 @@ import { useSnippetsAPI } from '../../../hooks/useSnippetsAPI' import { useSnippetsList } from '../../../hooks/useSnippetsList' import { handleUnknownError } from '../../../utils/errors' import { isNetworkAdmin } from '../../../utils/screen' -import { getSnippetDisplayName, getSnippetEditUrl, getSnippetType } from '../../../utils/snippets/snippets' +import { SNIPPET_SCOPE_ICONS, getSnippetDisplayName, getSnippetEditUrl, getSnippetLocation, getSnippetType } from '../../../utils/snippets/snippets' import { getRunOnceNonce } from '../../../utils/restAPI' import { buildUrl } from '../../../utils/urls' import { Badge } from '../../common/Badge' @@ -16,7 +16,7 @@ import { RowActions } from './RowActions' import { useFilteredSnippets } from './WithFilteredSnippetsContext' import { useSnippetsFilters } from './WithSnippetsTableFilters' import type { Key } from 'react' -import type { Snippet } from '../../../types/Snippet' +import type { Snippet, SnippetCodeScope } from '../../../types/Snippet' import type { ListTableColumn } from '../../common/ListTable' interface ColumnProps { @@ -156,6 +156,20 @@ export const TypeColumn: React.FC = ({ snippet }) => { ) } +export const LocationColumn: React.FC = ({ snippet }) => { + const location = getSnippetLocation(snippet) + if (!location) { + return <>— + } + + return ( + + + {location} + + ) +} + export const TagsColumn: React.FC = ({ snippet }) => snippet.tags.map((tag, index) => @@ -229,6 +243,12 @@ const baseTableColumns: ListTableColumn[] = [ sortedValue: snippet => getSnippetType(snippet), render: snippet => }, + { + id: 'location', + title: __('Location', 'code-snippets'), + sortedValue: snippet => getSnippetLocation(snippet) ?? '', + render: snippet => + }, { id: 'desc', title: __('Description', 'code-snippets'), diff --git a/src/js/utils/snippets/snippets.ts b/src/js/utils/snippets/snippets.ts index 86e352bdf..e8a513b54 100644 --- a/src/js/utils/snippets/snippets.ts +++ b/src/js/utils/snippets/snippets.ts @@ -115,6 +115,10 @@ export const validateSnippet = (snippet: Snippet): undefined | string => { export const isCondition = (snippet: { scope?: SnippetScope }): boolean => 'condition' === snippet.scope +/** The visible location label of a snippet, or undefined for conditional snippets which have no single run location. */ +export const getSnippetLocation = (snippet: Pick): string | undefined => + isCondition(snippet) ? undefined : SNIPPET_SCOPE_DESCRIPTIONS[ snippet.scope] + export const isProSnippet = (snippet: Pick): boolean => PRO_TYPES.has(getSnippetType(snippet)) diff --git a/src/php/Admin/Menus/Manage/Manage_Menu_Screen_Options.php b/src/php/Admin/Menus/Manage/Manage_Menu_Screen_Options.php index ad036e4bc..6138e0123 100644 --- a/src/php/Admin/Menus/Manage/Manage_Menu_Screen_Options.php +++ b/src/php/Admin/Menus/Manage/Manage_Menu_Screen_Options.php @@ -65,6 +65,7 @@ public function get_columns( array $columns = [] ): array { 'id' => __( 'ID', 'code-snippets' ), 'name' => __( 'Name', 'code-snippets' ), 'type' => __( 'Type', 'code-snippets' ), + 'location' => __( 'Location', 'code-snippets' ), 'desc' => __( 'Description', 'code-snippets' ), 'tags' => __( 'Tags', 'code-snippets' ), 'date' => __( 'Modified', 'code-snippets' ), diff --git a/tests/e2e/code-snippets-list.spec.ts b/tests/e2e/code-snippets-list.spec.ts index 9d70ae0a4..75957c9ff 100644 --- a/tests/e2e/code-snippets-list.spec.ts +++ b/tests/e2e/code-snippets-list.spec.ts @@ -505,6 +505,103 @@ test.describe('Code Snippets List Page Actions', () => { }) }) +test.describe('Snippet run location', () => { + const PREFIX = 'E2E Run Location' + + interface LocatedSnippet { + type: 'php' | 'html' | 'css' | 'js' + scope: string + label: string + } + + const locatedSnippets: LocatedSnippet[] = [ + { type: 'php', scope: 'global', label: 'Run everywhere' }, + { type: 'php', scope: 'admin', label: 'Only run in administration area' }, + { type: 'php', scope: 'front-end', label: 'Only run on site front-end' }, + { type: 'php', scope: 'single-use', label: 'Only run once' }, + { type: 'html', scope: 'content', label: 'Where inserted in editor' }, + { type: 'css', scope: 'site-css', label: 'Site front-end' }, + { type: 'js', scope: 'site-head-js', label: 'In site header ( section)' } + ] + + const snippetNames: Record = {} + let conditionSnippetName = '' + let helper: SnippetsTestHelper + + test.beforeAll(async () => { + await SnippetsTestHelper.cleanupSnippetsByPrefix(PREFIX) + + for (const { type, scope } of locatedSnippets) { + snippetNames[scope] = SnippetsTestHelper.makeUniqueSnippetName(`${PREFIX} ${scope}`) + await SnippetsTestHelper.createSnippetViaCli({ + name: snippetNames[scope], + active: false, + type, + scope + }) + } + + conditionSnippetName = SnippetsTestHelper.makeUniqueSnippetName(`${PREFIX} condition`) + await SnippetsTestHelper.createSnippetViaCli({ + name: conditionSnippetName, + active: false, + type: 'cond' + }) + }) + + test.afterAll(async () => { + await SnippetsTestHelper.cleanupSnippetsByPrefix(PREFIX) + }) + + test.beforeEach(async ({ page }) => { + helper = new SnippetsTestHelper(page) + await helper.navigateToSnippetsAdmin() + }) + + test('Table rows show the same location label the editor offers', async ({ page }) => { + for (const { scope, label } of locatedSnippets) { + const cell = snippetRowByName(page, snippetNames[scope]).locator('.column-location') + await expect(cell).toHaveText(label) + + // The icon hides from assistive tech, so the label carries the meaning. + const icon = cell.locator('.dashicons') + await expect(icon).toHaveCount(1) + await expect(icon).toHaveAttribute('aria-hidden', 'true') + } + }) + + test('Table rows show no location for conditional snippets', async ({ page }) => { + const cell = snippetRowByName(page, conditionSnippetName).locator('.column-location') + await expect(cell).toHaveText('—') + }) + + test('Cards show the same location label as the table', async ({ page }) => { + await switchSnippetView(page, 'Card view') + + try { + for (const { scope, label } of locatedSnippets) { + const card = page.locator('.snippets-card-grid .code-snippets-card').filter({ hasText: snippetNames[scope] }) + await expect(card).toBeVisible() + await expect(card.locator('.snippet-card-location .snippet-location')).toHaveText(label) + } + } finally { + await switchSnippetView(page, 'Table view').catch(() => undefined) + } + }) + + test('Cards show no location for conditional snippets', async ({ page }) => { + await switchSnippetView(page, 'Card view') + + try { + const card = page.locator('.snippets-card-grid .code-snippets-card').filter({ hasText: conditionSnippetName }) + await expect(card).toBeVisible() + await expect(card.locator('.snippet-card-location')).toHaveCount(0) + } finally { + await switchSnippetView(page, 'Table view').catch(() => undefined) + } + }) +}) + test.describe('Manage table Screen Options', () => { let helper: SnippetsTestHelper let snippetName: string @@ -601,6 +698,25 @@ test.describe('Manage table Screen Options', () => { await page.waitForLoadState('networkidle') }) + test('Location column visibility toggle hides and shows columns in real time', async ({ page }) => { + await openScreenOptions(page) + + const locationToggle = page.locator('#adv-settings input.hide-column-tog[value="location"]') + await expect(locationToggle).toBeVisible() + + // Ensure Location column is initially visible. + await locationToggle.check() + await expect(page.locator('.wp-list-table th.column-location').first()).not.toHaveClass(/\bhidden\b/) + + // Uncheck — column should disappear in real time. + await locationToggle.uncheck() + await expect(page.locator('.wp-list-table th.column-location').first()).toHaveClass(/\bhidden\b/) + + // Re-check — column should reappear in real time. + await locationToggle.check() + await expect(page.locator('.wp-list-table th.column-location').first()).not.toHaveClass(/\bhidden\b/) + }) + test('Truncation toggle applies and removes the truncation class in real time', async ({ page }) => { await openScreenOptions(page) diff --git a/tests/e2e/helpers/SnippetsTestHelper.ts b/tests/e2e/helpers/SnippetsTestHelper.ts index c279bea47..e18578cef 100644 --- a/tests/e2e/helpers/SnippetsTestHelper.ts +++ b/tests/e2e/helpers/SnippetsTestHelper.ts @@ -32,6 +32,7 @@ export interface CreateSnippetCliOptions { active: boolean; conditionId?: number; desc?: string; + scope?: string; tags?: readonly string[]; type?: 'php' | 'html' | 'css' | 'js' | 'cond'; } @@ -96,6 +97,10 @@ export class SnippetsTestHelper { break } + if (options.scope) { + scope = options.scope + } + const code = 'html' === type ? `

${options.name}

\n` : `// ${options.name}\n` const php = ` diff --git a/tests/unit/Admin/Menus/Manage/Manage_Menu_Screen_Options_Test.php b/tests/unit/Admin/Menus/Manage/Manage_Menu_Screen_Options_Test.php index 919e63ebf..68d7a2e9d 100644 --- a/tests/unit/Admin/Menus/Manage/Manage_Menu_Screen_Options_Test.php +++ b/tests/unit/Admin/Menus/Manage/Manage_Menu_Screen_Options_Test.php @@ -42,6 +42,7 @@ public function test_get_columns_adds_snippet_columns(): void { $this->assertSame( 'Existing', $columns['existing'] ); $this->assertSame( 'Columns', $columns['_title'] ); + $this->assertSame( 'Location', $columns['location'] ); $this->assertSame( 'Description', $columns['desc'] ); $this->assertSame( 'Modified', $columns['date'] ); }