Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
43 changes: 25 additions & 18 deletions config/visual-editor.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,16 @@
| fully-qualified block names (e.g. `core/paragraph`, `core/query`).
|
| The V1 defaults follow the M5 block-library audit
| (see docs/block-library-audit.md), updated by E4 (#381). The
| allow-list now includes the entity-scoped blocks that B1's expanded
| `core-data` shim plus the C1–C5 REST surface can round-trip:
| `core/template-part`, `core/post-*`, `core/site-*`, and
| `core/navigation`. The deny-list still removes the loop / feed
| widgets (`core/query`, `core/latest-comments`, `core/archives`,
| `core/categories`, `core/tag-cloud`) — they need a real loop runtime
| and term/comment endpoints that the shim does not implement, and
| stay deferred until V2 (`artisanpack-ui/cms-framework`) and V1.1+
| respectively. Keep the JS-side mirror in
| (see docs/block-library-audit.md), updated by E4 (#381) and G4b
| (#401). The allow-list now includes the entity-scoped blocks that
| B1's expanded `core-data` shim plus the C1–C5 REST surface can
| round-trip (`core/template-part`, `core/post-*`, `core/site-*`,
| `core/navigation`) and the taxonomy/feed widgets that G4b wires to
| cms-framework's term + post APIs (`core/categories`,
| `core/tag-cloud`, `core/archives`). The deny-list still removes
| the loop runtime (`core/query`, `core/query-loop` — V1 G4c) and
| the comments widgets (`core/latest-comments` — V1.1+, requires a
| Comments module in cms-framework). Keep the JS-side mirror in
| `resources/js/visual-editor/site-editor/site-editor-app.tsx`
| (`D2_DISABLED_BLOCKS`) in sync with the deny-list — the two lists
| want to agree.
Expand Down Expand Up @@ -133,28 +133,35 @@
'core/site-tagline',
'core/site-logo',
'core/navigation',
// G4b (#401) — taxonomy/feed widgets backed by cms-framework's
// term + post APIs through the dynamic-block registry. Hosts
// without cms-framework leave them registered client-side but
// the server-side renderer falls back to the unknown-block
// shell since no DynamicBlock is registered.
'core/categories',
'core/tag-cloud',
'core/archives',
'artisanpack/callout',
],

'disabled_blocks' => [
// `core/navigation` was enabled by D4 and stays enabled in E4.
// `core/template-part`, `core/post-*`, and `core/site-*` are
// promoted to the allow-list above by E4 (#381). The blocks
// listed here remain deliberately deferred:
// promoted to the allow-list above by E4 (#381). G4b (#401)
// promotes `core/categories`, `core/tag-cloud`, and
// `core/archives`. The blocks listed here remain deliberately
// deferred:
//
// - core/query / core/query-loop need a real loop runtime
// (V2 — `artisanpack-ui/cms-framework`).
// - The taxonomy/feed widgets need term + comment endpoints
// that the shim does not implement (V1.1+).
// (V1 G4c — `cms-framework` `QueryRuntime` service).
// - core/latest-comments needs a Comments module in
// cms-framework that does not exist yet (V1.1+).
//
// The JS-side mirror in site-editor-app.tsx is updated
// alongside this entry.
'core/query',
'core/query-loop',
'core/latest-comments',
'core/archives',
'core/categories',
'core/tag-cloud',
],

/*
Expand Down
41 changes: 38 additions & 3 deletions docs/block-library-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,49 @@ title. Better UX is no block at all until `cms-framework` lands.
| Block | Required data |
| --- | --- |
| `core/latest-comments` | Comments feed. |
| `core/archives` | Archive list query. |
| `core/categories` | Term hierarchy. |
| `core/tag-cloud` | Term weights. |
| `core/rss` | External feed fetch — not blocked by shim but disabled for consistency with the other feed widgets. |
| `core/calendar` | Post date aggregation. |
| `core/page-list` | Hierarchical page query. |
| `core/table-of-contents` | Heading scan against rendered entity. |

### G4b — re-enabled against cms-framework's term + post APIs (#401)

`core/categories`, `core/tag-cloud`, and `core/archives` were promoted out
of the empty-state list in G4b. Each block ships as a `DynamicBlock`
subclass under `src/Blocks/Core/` that reads from cms-framework's
`PostCategory`, `PostTag`, and `Post` models respectively. Registration is
gated on `class_exists(BlogManager::class)` in `VisualEditorServiceProvider`
— host apps without cms-framework still register the block client-side
(via the inserter allow-list) but the preview endpoint resolves to the
unknown-block fallback because no `DynamicBlock` is bound.

Editor preview, Blade renderer, React renderer, and Vue renderer all
resolve through the same `DynamicBlock::render()` path:

- Editor surface: Gutenberg's upstream `Edit` components for these three
blocks already use `ServerSideRender`, which POSTs the block's name
and attributes to `/visual-editor/api/blocks/preview` — that controller
routes through the dynamic-block registry.
- Front-end Blade renderer (`@artisanpack-ui/visual-editor-renderer-blade`)
invokes the registered `DynamicBlock` directly when walking the saved
block tree.
- Front-end React (`...-renderer-react`) and Vue (`...-renderer-vue`)
renderers POST to the same preview endpoint on mount and splice the
returned HTML back into the tree.

Archive URLs default to `/blog/{year}/{month}` (the cms-framework blog
default route shape). Hosts with a different archive route should rebind
the dynamic block via `VisualEditor::registerDynamicBlock(ArchivesBlock::class)`
with a subclass that overrides `decorate()`. Custom-taxonomy support is
out of scope for V1 (deferred to V1.1).

The taxonomy entity registration (`{kind: 'taxonomy', name: 'category', …}`)
called for in #401's implementation notes is intentionally **not** added in
V1 — the upstream `core/categories|tag-cloud|archives` `Edit` components
all render through `ServerSideRender`, so they never query
`useEntityRecords('taxonomy', 'category')`. The shim entities can be
layered in cheaply later if a future block needs them.

## Crash or backend-required — disabled by default

These blocks call selectors the shim returns `null` for but which the block's
Expand Down
6 changes: 6 additions & 0 deletions resources/js/visual-editor/editor/editor-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { registerContrastWarning } from './contrast-warning';
import { ConvertToPatternControl } from './convert-to-pattern-control';
import { discoverAndRegisterCustomBlocks } from './custom-blocks';
import { registerSyncedPatternIndicator } from './synced-pattern-indicator';
import { registerTaxonomyAndArchiveBlockOverrides } from './taxonomy-archive-block-overrides';
import {
DocumentPanels,
type AuthorOption,
Expand Down Expand Up @@ -167,6 +168,11 @@ function registerOnce(): void {
// for the same reason — the wrapper sees `core/block` once the
// block registers and applies the badge from then on.
registerSyncedPatternIndicator();
// G4b — swap the broken upstream Edit components for
// `core/categories`, `core/tag-cloud`, and `core/archives` with our
// ServerSideRender-backed wrappers BEFORE `registerCoreBlocks()` so
// the override applies during initial registration.
registerTaxonomyAndArchiveBlockOverrides();
registerCoreBlocks();
// Discover host-app custom blocks under
// `resources/js/visual-editor/blocks/{block-name}/index.ts` and
Expand Down
241 changes: 241 additions & 0 deletions resources/js/visual-editor/editor/taxonomy-archive-block-overrides.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/**
* Edit-component overrides for the three G4b dynamic blocks.
*
* The upstream `core/categories`, `core/tag-cloud`, and `core/archives`
* Edit components depend on data the shim does not provide:
*
* - `core/categories` reads `getTaxonomy('category').labels.name`.
* - `core/tag-cloud` calls `getTaxonomies({ per_page: -1 })`.
* - `core/archives` renders through Gutenberg's
* `@wordpress/server-side-render`, which POSTs to
* `wp/v2/block-renderer/core/archives`. Visual-editor's preview
* endpoint lives at `/visual-editor/api/blocks/preview` instead.
*
* Rather than backfill `getTaxonomies` / `getTaxonomy` selectors and a
* `wp/v2/block-renderer` route just for these three blocks, this filter
* swaps each block's `edit` with a thin wrapper that uses our
* `ServerSideRender` shim. The block's saved attribute shape, supports,
* and `save()` stay untouched — anything written by the previous
* upstream Edit (or by hosts that bind their own controls) is still
* round-tripped against the same attribute keys.
*
* Idempotent across HMR via a global Symbol guard so the post-editor
* and site-editor entries can both call `registerTaxonomyAndArchiveBlockOverrides()`.
*/

import { InspectorControls } from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
ToggleControl,
RangeControl,
} from '@wordpress/components';
import { useBlockProps } from '@wordpress/block-editor';
import { addFilter } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';

import { TEXT_DOMAIN } from '../vendor/i18n';

import { ServerSideRender } from './server-side-render';

const FILTER_HOOK = 'blocks.registerBlockType';
const FILTER_NAMESPACE = 'artisanpack-ui/visual-editor/taxonomy-archive-overrides';
const TARGET_BLOCKS = new Set( [
'core/categories',
'core/tag-cloud',
'core/archives',
] );

const REGISTERED_KEY = Symbol.for(
'artisanpack-ui.visual-editor.taxonomy-archive-overrides.registered'
);

// Typed as `{ label: string; value: string }[]` so TS does not narrow
// the option values to literal unions — that narrowing collides with
// the broader `string` type the block attributes carry.
const SMALLEST_SIZE_OPTIONS: { label: string; value: string }[] = [
{ label: '8pt', value: '8pt' },
{ label: '10pt', value: '10pt' },
{ label: '12pt', value: '12pt' },
];

const LARGEST_SIZE_OPTIONS: { label: string; value: string }[] = [
{ label: '16pt', value: '16pt' },
{ label: '22pt', value: '22pt' },
{ label: '28pt', value: '28pt' },
];

interface GlobalSentinelHost {
[REGISTERED_KEY]?: boolean;
}

interface BlockSettings {
name?: string;
edit?: unknown;
[key: string]: unknown;
}

interface EditProps {
attributes: Record<string, unknown>;
setAttributes: ( changes: Record<string, unknown> ) => void;
}

function CategoriesEdit( { attributes, setAttributes }: EditProps ): JSX.Element {
const blockProps = useBlockProps();
const showPostCounts = Boolean( attributes.showPostCounts );
const showHierarchy = Boolean( attributes.showHierarchy );
const showOnlyTopLevel = Boolean( attributes.showOnlyTopLevel );
const showEmpty = Boolean( attributes.showEmpty );

return (
<>
<InspectorControls>
<PanelBody title={ __( 'Settings', TEXT_DOMAIN ) }>
<ToggleControl
label={ __( 'Show post counts', TEXT_DOMAIN ) }
checked={ showPostCounts }
onChange={ ( value ) => setAttributes( { showPostCounts: value } ) }
/>
<ToggleControl
label={ __( 'Show hierarchy', TEXT_DOMAIN ) }
checked={ showHierarchy }
onChange={ ( value ) => setAttributes( { showHierarchy: value } ) }
/>
<ToggleControl
label={ __( 'Show only top level', TEXT_DOMAIN ) }
checked={ showOnlyTopLevel }
onChange={ ( value ) => setAttributes( { showOnlyTopLevel: value } ) }
/>
<ToggleControl
label={ __( 'Show empty terms', TEXT_DOMAIN ) }
checked={ showEmpty }
onChange={ ( value ) => setAttributes( { showEmpty: value } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<ServerSideRender block="core/categories" attributes={ attributes } />
</div>
</>
);
}

function TagCloudEdit( { attributes, setAttributes }: EditProps ): JSX.Element {
const blockProps = useBlockProps();
const numberOfTags =
typeof attributes.numberOfTags === 'number' ? attributes.numberOfTags : 45;
const showTagCounts = Boolean( attributes.showTagCounts );
const smallestFontSize =
typeof attributes.smallestFontSize === 'string'
? attributes.smallestFontSize
: '8pt';
const largestFontSize =
typeof attributes.largestFontSize === 'string'
? attributes.largestFontSize
: '22pt';

return (
<>
<InspectorControls>
<PanelBody title={ __( 'Settings', TEXT_DOMAIN ) }>
<RangeControl
label={ __( 'Number of tags', TEXT_DOMAIN ) }
min={ 1 }
max={ 100 }
value={ numberOfTags }
onChange={ ( value ) => setAttributes( { numberOfTags: value ?? 45 } ) }
/>
<ToggleControl
label={ __( 'Show tag counts', TEXT_DOMAIN ) }
checked={ showTagCounts }
onChange={ ( value ) => setAttributes( { showTagCounts: value } ) }
/>
<SelectControl
label={ __( 'Smallest size', TEXT_DOMAIN ) }
value={ smallestFontSize }
options={ SMALLEST_SIZE_OPTIONS }
onChange={ ( value ) => setAttributes( { smallestFontSize: value } ) }
/>
<SelectControl
label={ __( 'Largest size', TEXT_DOMAIN ) }
value={ largestFontSize }
options={ LARGEST_SIZE_OPTIONS }
onChange={ ( value ) => setAttributes( { largestFontSize: value } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<ServerSideRender block="core/tag-cloud" attributes={ attributes } />
</div>
</>
);
}

function ArchivesEdit( { attributes, setAttributes }: EditProps ): JSX.Element {
const blockProps = useBlockProps();
const showPostCounts = Boolean( attributes.showPostCounts );
const displayAsDropdown = Boolean( attributes.displayAsDropdown );
const type = attributes.type === 'yearly' ? 'yearly' : 'monthly';

return (
<>
<InspectorControls>
<PanelBody title={ __( 'Settings', TEXT_DOMAIN ) }>
<ToggleControl
label={ __( 'Display as dropdown', TEXT_DOMAIN ) }
checked={ displayAsDropdown }
onChange={ ( value ) => setAttributes( { displayAsDropdown: value } ) }
/>
<ToggleControl
label={ __( 'Show post counts', TEXT_DOMAIN ) }
checked={ showPostCounts }
onChange={ ( value ) => setAttributes( { showPostCounts: value } ) }
/>
<SelectControl
label={ __( 'Group by', TEXT_DOMAIN ) }
value={ type }
options={ [
{ label: __( 'Monthly', TEXT_DOMAIN ), value: 'monthly' },
{ label: __( 'Yearly', TEXT_DOMAIN ), value: 'yearly' },
] }
onChange={ ( value ) => setAttributes( { type: value } ) }
/>
</PanelBody>
</InspectorControls>
<div { ...blockProps }>
<ServerSideRender block="core/archives" attributes={ attributes } />
</div>
</>
);
}

const EDITS: Record<string, ( props: EditProps ) => JSX.Element> = {
'core/categories': CategoriesEdit,
'core/tag-cloud': TagCloudEdit,
'core/archives': ArchivesEdit,
};

function overrideEdit( settings: BlockSettings, name: string ): BlockSettings {
if ( ! TARGET_BLOCKS.has( name ) ) {
return settings;
}

const edit = EDITS[ name ];

if ( edit === undefined ) {
return settings;
}

return { ...settings, edit };
}

export function registerTaxonomyAndArchiveBlockOverrides(): void {
const host = globalThis as unknown as GlobalSentinelHost;

if ( host[ REGISTERED_KEY ] === true ) {
return;
}

addFilter( FILTER_HOOK, FILTER_NAMESPACE, overrideEdit );
host[ REGISTERED_KEY ] = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ vi.mock('../../editor/synced-pattern-indicator', () => ({
registerSyncedPatternIndicator: () => undefined,
}));

// G4b — stub the taxonomy/archive Edit override registration to avoid
// pulling `@wordpress/block-editor` + `@wordpress/components` into the
// shell test's import graph.
vi.mock('../../editor/taxonomy-archive-block-overrides', () => ({
registerTaxonomyAndArchiveBlockOverrides: () => undefined,
}));

import { SiteEditorApp } from '../site-editor-app';

const ROUTE_BASE = '/visual-editor/site';
Expand Down
Loading