Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/css/manage/_snippets-table.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
@use '../common/theme';

.column-name,
.column-type {
.column-type,
.snippet-location {
.dashicons {
font-size: 16px;
inline-size: 16px;
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -192,6 +192,7 @@ export const ManageSnippetCard: React.FC<ManageSnippetCardProps> = ({
onSelectedChange
}) => {
const { activeByCondition } = useFilteredSnippets()
const location = getSnippetLocation(snippet)

return (
<SnippetCard
Expand All @@ -213,8 +214,10 @@ export const ManageSnippetCard: React.FC<ManageSnippetCardProps> = ({
<div className="card-inner">
<CardHeader snippet={snippet} />

{(0 < snippet.tags.length || !!snippet.modified) && (
{(0 < snippet.tags.length || !!snippet.modified || !!location) && (
<div className={classnames('snippet-card-meta', { 'has-tags': 0 < snippet.tags.length })}>
{location && (
<div className="snippet-card-location"><LocationColumn snippet={snippet} /></div>)}
{0 < snippet.tags.length && (
<span className="snippet-card-tags">
<span className="snippet-card-tags-label">
Expand Down
24 changes: 22 additions & 2 deletions src/js/components/ManageMenu/SnippetsTable/TableColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand Down Expand Up @@ -156,6 +156,20 @@ export const TypeColumn: React.FC<ColumnProps> = ({ snippet }) => {
)
}

export const LocationColumn: React.FC<ColumnProps> = ({ snippet }) => {
const location = getSnippetLocation(snippet)
if (!location) {
return <>&#8212;</>
}

return (
<span className="snippet-location">
<span className={`dashicons dashicons-${SNIPPET_SCOPE_ICONS[snippet.scope as SnippetCodeScope]}`} aria-hidden="true"></span>
{location}
</span>
)
}

export const TagsColumn: React.FC<ColumnProps> = ({ snippet }) =>
snippet.tags.map((tag, index) =>
<Fragment key={tag}>
Expand Down Expand Up @@ -229,6 +243,12 @@ const baseTableColumns: ListTableColumn<Snippet>[] = [
sortedValue: snippet => getSnippetType(snippet),
render: snippet => <TypeColumn snippet={snippet} />
},
{
id: 'location',
title: __('Location', 'code-snippets'),
sortedValue: snippet => getSnippetLocation(snippet) ?? '',
render: snippet => <LocationColumn snippet={snippet} />
},
{
id: 'desc',
title: __('Description', 'code-snippets'),
Expand Down
4 changes: 4 additions & 0 deletions src/js/utils/snippets/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Snippet, 'scope'>): string | undefined =>
isCondition(snippet) ? undefined : SNIPPET_SCOPE_DESCRIPTIONS[<SnippetCodeScope> snippet.scope]

export const isProSnippet = (snippet: Pick<Snippet, 'scope'>): boolean =>
PRO_TYPES.has(getSnippetType(snippet))

Expand Down
1 change: 1 addition & 0 deletions src/php/Admin/Menus/Manage/Manage_Menu_Screen_Options.php
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
Expand Down
116 changes: 116 additions & 0 deletions tests/e2e/code-snippets-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<head> section)' }
]

const snippetNames: Record<string, string> = {}
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
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions tests/e2e/helpers/SnippetsTestHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface CreateSnippetCliOptions {
active: boolean;
conditionId?: number;
desc?: string;
scope?: string;
tags?: readonly string[];
type?: 'php' | 'html' | 'css' | 'js' | 'cond';
}
Expand Down Expand Up @@ -96,6 +97,10 @@ export class SnippetsTestHelper {
break
}

if (options.scope) {
scope = options.scope
}

const code = 'html' === type ? `<p>${options.name}</p>\n` : `// ${options.name}\n`

const php = `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'] );
}
Expand Down