Skip to content
Merged
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
27 changes: 5 additions & 22 deletions src/js/components/ManageMenu/ManageMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@ import { __ } from '@wordpress/i18n'
import { createInterpolateElement } from '@wordpress/element'
import { fetchConstQueryParam, fetchQueryParam, updateQueryParams } from '../../utils/urls'
import { DismissibleNotice, type NoticeType } from '../common/Notice'
import { SUBPAGES, Toolbar } from '../common/Toolbar'
import { AiAgentDemo } from './AiAgentDemo/AiAgentDemo'
import { BlueprintsDemo } from './BlueprintsDemo/BlueprintsDemo'
import { CloudLibraryDemo } from './CloudLibraryDemo/CloudLibraryDemo'
import { CommunityCloud } from './CommunityCloud/CommunityCloud'
import { SnippetsTable } from './SnippetsTable'
import { Toolbar } from '../common/Toolbar'
import { SUBPAGES, SUBPAGE_ENTRIES, type SubpageName } from './subpages'

const repositionTableOptionsSettings = () => {
const screenOptionsForm = document.getElementById('adv-settings')
Expand Down Expand Up @@ -71,26 +67,13 @@ const PageNotices = () => {
}

interface PageContentParams {
subpage: typeof SUBPAGES[number]
subpage: SubpageName
}

const PageContent: React.FC<PageContentParams> = ({ subpage }) => {
switch (subpage) {
case 'snippets':
return <SnippetsTable />
const { Component } = SUBPAGE_ENTRIES[subpage]

case 'cloud-community':
return <CommunityCloud />

case 'ai-agent':
return <AiAgentDemo />

case 'blueprints':
return <BlueprintsDemo />

case 'cloud-library':
return <CloudLibraryDemo />
}
return <Component />
}

export const ManageMenu = () => {
Expand Down
41 changes: 41 additions & 0 deletions src/js/components/ManageMenu/subpages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { AiAgentDemo } from './AiAgentDemo/AiAgentDemo'
import { BlueprintsDemo } from './BlueprintsDemo/BlueprintsDemo'
import { CloudLibraryDemo } from './CloudLibraryDemo/CloudLibraryDemo'
import { CommunityCloud } from './CommunityCloud/CommunityCloud'
import { SnippetsTable } from './SnippetsTable'
import type React from 'react'

export const SUBPAGES = ['snippets', 'blueprints', 'cloud-community', 'cloud-library', 'ai-agent'] as const

export type SubpageName = typeof SUBPAGES[number]

export interface Subpage {
Component: React.FC

/**
* Marks a walkthrough tab. An 'announce' tab advertises itself as new until
* its walkthrough has been watched, which only a tab with a recorded demo
* can do; a 'quiet' tab is only ever labelled as a demo.
*/
demo?: 'announce' | 'quiet'

/**
* Marks a premium tab, which is chipped as such without a licence.
*/
isPro?: boolean
}

/**
* What each subpage of the manage screen renders, and how its toolbar tab
* advertises itself.
*
* The toolbar and the page body read the same entry, so a subpage is described
* once here rather than being kept in step across the two.
*/
export const SUBPAGE_ENTRIES: Record<SubpageName, Subpage> = {
'snippets': { Component: SnippetsTable },
'blueprints': { Component: BlueprintsDemo, demo: 'announce' },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
'cloud-community': { Component: CommunityCloud },
'cloud-library': { Component: CloudLibraryDemo, demo: 'quiet' },
'ai-agent': { Component: AiAgentDemo, demo: 'announce' },
}
40 changes: 18 additions & 22 deletions src/js/components/common/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { __, _x } from '@wordpress/i18n'
import classnames from 'classnames'
import React, { useMemo, useState } from 'react'
import { isLicensed, shouldShowUpsell } from '../../utils/screen'
import { buildUrl } from '../../utils/urls'
import { buildUrl, fetchConstQueryParam } from '../../utils/urls'
import { SUBPAGES, SUBPAGE_ENTRIES, type SubpageName } from '../ManageMenu/subpages'
import { hasSeenDemo } from './demo/useDemoSeen'
import { AiAgentIcon } from './icons/AiAgentIcon'
import { BlueprintIcon } from './icons/BlueprintIcon'
Expand All @@ -11,13 +12,21 @@ import { LibraryIcon } from './icons/LibraryIcon'
import { SettingsIcon } from './icons/SettingsIcon'
import { SnippetsIcon } from './icons/SnippetsIcon'
import { UpsellDialog } from './UpsellDialog'
import type { DemoName } from './demo/useDemoSeen'
import type { SVGProps } from 'react'

export const SUBPAGES = ['snippets', 'blueprints', 'cloud-community', 'cloud-library', 'ai-agent'] as const

const searchParams = new URLSearchParams(window.location.search)

const managePageSlug = window.CODE_SNIPPETS?.urls.manage
? new URL(window.CODE_SNIPPETS.urls.manage).searchParams.get('page')
: null

// The manage screen resolves an absent or unrecognised `subpage` to the first subpage, so
// mirror that fallback here — comparing against the raw param would leave every lower-nav
// tab inactive on the default Snippets view, which is where the page opens.
const activeSubpage = searchParams.get('page') === managePageSlug
? fetchConstQueryParam('subpage', SUBPAGES) ?? SUBPAGES[0]
: null
Comment thread
coderabbitai[bot] marked this conversation as resolved.

interface UpperNavItemProps {
name: string
url: string
Expand Down Expand Up @@ -133,31 +142,21 @@ const UpperNav: React.FC = () => {
)
}

interface SubpageItemBaseProps {
interface SubpageItemProps {
subpage: SubpageName
label: string
Icon: React.FC<SVGProps<SVGSVGElement>>
isPro?: boolean
}

/**
* Marks a walkthrough tab. New features announce themselves until the
* walkthrough has been watched, which only a tab with a recorded demo can do;
* existing ones are only ever labelled as a demo.
*/
type SubpageItemDemoProps =
| { subpage: DemoName, demo: 'announce' }
| { subpage: typeof SUBPAGES[number], demo?: 'quiet' }

export type SubpageItemProps = SubpageItemBaseProps & SubpageItemDemoProps

const SubpageItem: React.FC<SubpageItemProps> = ({ subpage, label, Icon, isPro, demo }) => {
const SubpageItem: React.FC<SubpageItemProps> = ({ subpage, label, Icon }) => {
const { demo, isPro } = SUBPAGE_ENTRIES[subpage]
const isNew = 'announce' === demo && !hasSeenDemo(subpage)

return (
<li>
<a
href={buildUrl(window.CODE_SNIPPETS?.urls.manage, { subpage: subpage })}
className={classnames(`${subpage}-link`, { 'active-link': subpage === searchParams.get('subpage') })}
className={classnames(`${subpage}-link`, { 'active-link': subpage === activeSubpage })}
>
<Icon aria-hidden="true" />
<span className="toolbar-nav-label">{label}</span>
Expand Down Expand Up @@ -212,22 +211,19 @@ const LowerNav = () =>
subpage="cloud-library"
label={__('Cloud Library', 'code-snippets')}
Icon={LibraryIcon}
demo="quiet"
/>

{!isLicensed() && (
<SubpageItem
subpage="blueprints"
label={__('Blueprints', 'code-snippets')}
Icon={BlueprintIcon}
demo="announce"
/>)}

<SubpageItem
subpage="ai-agent"
label={__('AI Agent', 'code-snippets')}
Icon={AiAgentIcon}
demo="announce"
/>

<SettingsItem />
Expand Down
5 changes: 4 additions & 1 deletion src/js/components/common/demo/useDemoSeen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { useCallback, useRef } from 'react'
import { useRestAPI } from '../../../hooks/useRestAPI'
import { handleUnknownError } from '../../../utils/errors'
import { REST_BASES } from '../../../utils/restAPI'
import type { SubpageName } from '../../ManageMenu/subpages'

export type DemoName = 'ai-agent' | 'blueprints'

export const hasSeenDemo = (demo: DemoName): boolean =>
// Widened to any subpage so the toolbar can ask about a tab without first
// narrowing it: a tab with no recorded demo simply never appears in the list.
export const hasSeenDemo = (demo: SubpageName): boolean =>
window.CODE_SNIPPETS?.demosSeen?.includes(demo) ?? false

/**
Expand Down
4 changes: 3 additions & 1 deletion src/php/Admin/Menus/Manage/Manage_Menu_Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ function ( Snippet $snippet ) {
if ( $inline_limit > 0 ) {
$localized['snippetsList'] = array_slice( $snippets, 0, $inline_limit );
}
} elseif ( $this->screen_options->is_ai_agent_view() ) {
}

if ( $this->screen_options->is_ai_agent_view() ) {
// The AI Agent demo writes the site name into the snippet it builds,
// so the walkthrough reads as though it were written for this site.
$localized['aiDemo'] = [
Expand Down
38 changes: 38 additions & 0 deletions tests/e2e/toolbar-active-subpage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { expect, test } from '@playwright/test'
import type { Page } from '@playwright/test'

const MANAGE_URL = '/wp-admin/admin.php?page=snippets'

// The manage screen resolves an absent or unrecognised `subpage` to the first
// subpage, so the toolbar must highlight whichever subpage actually rendered.
// Each case names a landmark of the body it expects, so a highlight that
// disagrees with the rendered page fails rather than passing on the query alone.
const CASES = [
{ name: 'no subpage parameter', query: '', active: 'snippets', body: '.wp-list-table' },
{ name: 'an unrecognised subpage', query: '&subpage=not-a-subpage', active: 'snippets', body: '.wp-list-table' },
{ name: 'a named subpage', query: '&subpage=cloud-community', active: 'cloud-community', body: '.community-cloud-nav' },
]

// The Settings tab is highlighted by its own page slug rather than a subpage,
// so it is excluded here.
const activeSubpageLinks = (page: Page) =>
page.locator('.code-snippets-toolbar-lower a.active-link:not(.settings-link)')

test.describe('Toolbar active subpage', () => {
for (const { name, query, active, body } of CASES) {
test(`the highlighted tab matches the rendered page with ${name}`, async ({ page }) => {
await page.goto(`${MANAGE_URL}${query}`)
await expect(page.locator(body)).toBeVisible()

await expect(activeSubpageLinks(page)).toHaveCount(1)
await expect(activeSubpageLinks(page)).toHaveClass(new RegExp(`(^|\\s)${active}-link(\\s|$)`))
})
}

test('no lower-nav tab is highlighted away from the manage screen', async ({ page }) => {
await page.goto('/wp-admin/admin.php?page=snippets-settings')

await expect(page.locator('.code-snippets-toolbar-lower')).toBeVisible()
await expect(activeSubpageLinks(page)).toHaveCount(0)
})
})
Loading