From d72fec79bc4b76f462c7229533afa5885049725e Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Fri, 17 Apr 2026 22:04:54 -0400 Subject: [PATCH 01/11] add new review and integration upgrade plan --- CLAUDE.md | 157 ++++++++ docs/gutenberg-alignment/README.md | 30 ++ docs/gutenberg-alignment/consolidated-plan.md | 289 +++++++++++++ docs/gutenberg-alignment/core-pr-migration.md | 341 ++++++++++++++++ docs/gutenberg-alignment/pass-a.md | 380 ++++++++++++++++++ docs/gutenberg-alignment/pass-b.md | 314 +++++++++++++++ docs/gutenberg-alignment/pass-c.md | 368 +++++++++++++++++ 7 files changed, 1879 insertions(+) create mode 100644 CLAUDE.md create mode 100644 docs/gutenberg-alignment/README.md create mode 100644 docs/gutenberg-alignment/consolidated-plan.md create mode 100644 docs/gutenberg-alignment/core-pr-migration.md create mode 100644 docs/gutenberg-alignment/pass-a.md create mode 100644 docs/gutenberg-alignment/pass-b.md create mode 100644 docs/gutenberg-alignment/pass-c.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0c23304 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,157 @@ +# Validation API Plugin + +A declarative validation framework for the WordPress block editor. Provides infrastructure for registering, executing, and displaying content validation checks — no built-in checks or settings UI. + +## Architecture + +Three validation scopes, each with a PHP registry and JS filter: + +| Scope | PHP Registry | Registration Function | JS Filter | +|---|---|---|---| +| Block attributes | `ValidationAPI\Block\Registry` | `wp_register_block_validation_check()` | `editor.validateBlock` | +| Post meta fields | `ValidationAPI\Meta\Registry` | `wp_register_meta_validation_check()` | `editor.validateMeta` | +| Editor/document | `ValidationAPI\Editor\Registry` | `wp_register_editor_validation_check()` | `editor.validateEditor` | + +All registration functions require `namespace`, `name`, and `error_msg` in the `$args` array. Meta checks also require `meta_key`. + +### Severity + +Three levels: `error` (blocks save), `warning` (shows feedback), `none` (disabled). Filterable at runtime via `wp_validation_check_level`. + +### Data Flow + +``` +PHP Registries + → block_editor_settings_all filter (Assets.php) + → select('core/editor').getEditorSettings().validationApi + → getValidationConfig.js utility functions + → validateBlock / validateMeta / validateEditor + → core/validation store (actions/selectors) + → ValidationAPI component (lockPostSaving, CSS classes) + → ValidationSidebar component (issue display) +``` + +### Key PHP Hooks + +- `wp_validation_check_level` — Override check severity at runtime +- `wp_validation_check_args` — Modify check config before registration +- `wp_validation_should_register_check` — Prevent specific checks from registering +- `wp_validation_initialized`, `wp_validation_ready`, `wp_validation_editor_checks_ready` — Lifecycle + +### JS Store + +Store name: `core/validation` + +**Selectors:** `getInvalidBlocks()`, `getInvalidMeta()`, `getInvalidEditorChecks()`, `getBlockValidation(clientId)`, `hasErrors()`, `hasWarnings()` + +**Actions:** `setInvalidBlocks()`, `setInvalidMeta()`, `setInvalidEditorChecks()`, `setBlockValidation()`, `clearBlockValidation()` + +### REST API + +`GET /wp/v2/validation-checks` — Returns all registered checks grouped by scope (block, meta, editor). Requires `manage_options`. + +## Project Structure + +``` +includes/ + Block/Registry.php # Block check registration + Editor/Registry.php # Editor check registration + Meta/Registry.php # Meta check registration + Meta/Validator.php # Server-side meta validation helper + Core/Plugin.php # Plugin initialization + Core/Assets.php # Script enqueuing + editor settings injection + Core/I18n.php # Script translations + Core/Traits/EditorDetection.php # Post editor context detection + Rest/ChecksController.php # REST endpoint + Contracts/CheckProvider.php # Optional interface for class-based registration + +src/ + script.js # Entry point + editor/ + register.js # registerPlugin('core-validation', ...) + store/ # core/validation Redux store + components/ + ValidationProvider.js # Single computation point (renderless) + ValidationSidebar.js # Issue display panel + ValidationToolbarButton.js + validation/ + ValidationAPI.js # Side effects: lockPostSaving, CSS classes (renderless) + blocks/validateBlock.js + meta/validateMeta.js + editor/validateEditor.js + meta/hooks/useMetaField.js + meta/hooks/useMetaValidation.js + hoc/ + withErrorHandling.js # editor.BlockEdit filter + withBlockValidationClasses.js # editor.BlockListBlock filter + shared/ + utils/validation/ + issueHelpers.js # createIssue, createValidationResult, hasErrors, hasWarnings + getValidationConfig.js # Reads from editor settings (replaces window.ValidationAPI) + getInvalidBlocks.js # React hook + getInvalidMeta.js # React hook + getInvalidEditorChecks.js # React hook + hooks/ + useDebouncedValidation.js +``` + +## Build + +```bash +pnpm build # wp-scripts build → build/validation-api.js +pnpm start # wp-scripts start (watch mode) +pnpm lint # JS + PHP + CSS linting +``` + +Webpack aliases: `@` → `src/`, `@editor` → `src/editor/`, `@shared` → `src/shared/` + +## Companion Plugins (same local wp-content/plugins/) + +- **validation-api-integration-example** — Demo plugin with block, meta, and editor checks. Must be rebuilt separately (`npm run build` in its directory) after any JS filter name changes. +- **validation-api-settings** — Admin settings page using WordPress DataForm. Reads checks from REST endpoint, lets admins override severity via `wp_validation_check_level` filter. Must be rebuilt separately. + +## Conventions + +- PHP registration args use snake_case (`error_msg`, `warning_msg`). JS issue objects use camelCase (`errorMsg`, `warningMsg`). Transformation happens in `createIssue()`. +- Plugin registers as `registerPlugin('core-validation', ...)` in JS. +- Editor context scoping: validation loads in post editor only (not site editor). Detection via `EditorDetection` trait. +- `PluginContext` was removed. Plugin attribution uses a `namespace` field in registration args, stored as `_namespace` internally. +- PHPCS config (`phpcs.xml.dist`) allows `wp_register` and `wp_validation` as global prefixes. + +## Integration Pattern + +External plugins register checks like this: + +```php +add_action( 'init', function() { + if ( ! function_exists( 'wp_register_block_validation_check' ) ) { + return; + } + + wp_register_block_validation_check( 'core/image', [ + 'namespace' => 'my-plugin', + 'name' => 'alt_text', + 'level' => 'error', + 'error_msg' => __( 'Images must have alt text.', 'my-plugin' ), + ] ); +} ); +``` + +```javascript +import { addFilter } from '@wordpress/hooks'; + +addFilter( 'editor.validateBlock', 'my-plugin/image-alt-text', + ( isValid, blockType, attributes, checkName ) => { + if ( blockType !== 'core/image' || checkName !== 'alt_text' ) return isValid; + return !! attributes.alt?.trim(); + } +); +``` + +## Key Docs + +- `docs/PROPOSAL.md` — Core merge proposal +- `docs/INTEGRATION.md` — Gutenberg integration strategy +- `docs/TODO.md` — Remaining work (testing, TypeScript, performance, future features) +- `docs/guide/` — Developer integration guides +- `docs/technical/` — Architecture, API reference, hooks reference diff --git a/docs/gutenberg-alignment/README.md b/docs/gutenberg-alignment/README.md new file mode 100644 index 0000000..6be3b99 --- /dev/null +++ b/docs/gutenberg-alignment/README.md @@ -0,0 +1,30 @@ +# Gutenberg Alignment + +Planning docs for aligning the Validation API plugin with current Gutenberg conventions, in preparation for a potential core-merge proposal. These are planning artifacts — not active implementation yet. + +## Read in this order + +1. **[consolidated-plan.md](consolidated-plan.md)** — Start here. The authoritative execution plan: five batches, order, acceptance criteria, verification steps. +2. **[pass-a.md](pass-a.md)** — Convention & alignment findings. Contains the full checklist for every batch. +3. **[pass-b.md](pass-b.md)** — Architectural review. Explains why `ValidationProvider` + `ValidationAPI` convert to hooks and why `editor.preSavePost` gets added. +4. **[pass-c.md](pass-c.md)** — Leanness review. Explains the ~375 LOC of deletable PHP. +5. **[core-pr-migration.md](core-pr-migration.md)** — Deferred items. Activate only when all NOW-batches ship and a core PR is being cut. + +## Scope + +| Document | Covers | Status | +|---|---|---| +| `pass-a.md` | Naming, file layout, style, REST namespace, package-ready src/ layout | Complete | +| `pass-b.md` | Renderless components vs hooks, SlotFills, `registerPlugin`, save-locking pattern, REST permissions | Complete | +| `pass-c.md` | Dead code, duplication, abstraction cost/benefit | Complete | +| `consolidated-plan.md` | Batch sequencing, acceptance criteria, manual verification | Complete | +| `core-pr-migration.md` | PSR-4 → WP-core style, text domain, `@since`, sidebar mount, CSS prefix | Complete (dormant until PR) | + +## NOW vs. deferred + +- **NOW batches** (in `consolidated-plan.md` + `pass-a.md`) preserve standalone-plugin viability. No public API breaking changes except the REST namespace (coordinated with the only consumer, the settings addon). +- **Deferred migrations** (in `core-pr-migration.md`) would break standalone functionality and only make sense when translating the plugin into core-style PHP/JS. Activate when cutting the actual core PR. + +## Ownership context + +User owns all three related plugins (`validation-api`, `validation-api-settings`, `validation-api-integration-example`). No backward-compat concerns apply to internal consumers; coordinated changes across all three are acceptable. diff --git a/docs/gutenberg-alignment/consolidated-plan.md b/docs/gutenberg-alignment/consolidated-plan.md new file mode 100644 index 0000000..cb1e911 --- /dev/null +++ b/docs/gutenberg-alignment/consolidated-plan.md @@ -0,0 +1,289 @@ +# Consolidated Action Plan — Gutenberg Alignment + +Authoritative execution plan for aligning the Validation API plugin with Gutenberg conventions while preserving standalone-plugin viability. Synthesizes findings from Pass A (conventions), Pass B (architecture), and Pass C (leanness). + +For rationale and evidence behind each change, see: +- [pass-a.md](pass-a.md) — conventions & alignment +- [pass-b.md](pass-b.md) — architecture +- [pass-c.md](pass-c.md) — leanness +- [core-pr-migration.md](core-pr-migration.md) — deferred core-merge-only changes + +## Status + +- [x] Pass A complete +- [x] Pass B complete +- [x] Pass C complete +- [ ] Consolidated plan approved +- [ ] Execution + +## Scope + +**Five batches.** Estimated impact: +- Reduced PHP LOC: ~375 (Batches 3, 4, 5) +- JS restructure: ~40 files moved/renamed, ~22 LOC saved (Batch 1) +- REST namespace cleanup (Batch 2) + +All batches preserve the plugin's public API (global PHP registration functions, JS filter names, store name, kept hooks). The REST endpoint path is the only externally visible breaking change, and the only consumer (settings addon) is updated in the same change. + +| Batch | Summary | Risk | From passes | +|---|---|---|---| +| 1 | JS source reshape: flat package layout, hook-based lifecycle, drop aliases, absorb `useValidationIssues` + `useMetaField` consolidation, add `pre-save-validation`, rename `getInvalid*` → `useInvalid*` | Low-Medium | A + B + C | +| 2 | REST namespace move: `wp/v2/validation-checks` → `wp-validation/v1/checks`; settings addon updated in same change | Low | A | +| 3 | Delete `Core/I18n.php`; inline `wp_set_script_translations()` in `Core/Assets.php` | None | A (confirmed C) | +| 4 | PHP dead-code deletions (~260 LOC): `Meta\Validator`, `Contracts/CheckProvider`, dead registry methods, orphan hooks, unreachable `EditorDetection` branch | Very low | C | +| 5 | Extract `AbstractValidationRegistry` base class (~115 LOC saved via deduplication) | Medium | C | + +## Execution order + +**Recommended order:** cleanup before restructure. Quick wins first, biggest change last. + +``` +1. Batch 4 (PHP deletions) — fast win, reduces surface for Batch 5 +2. Batch 3 (I18n inline) — trivial, clears PHP clutter +3. Batch 5 (Registry extraction) — PHP refactor, benefits from 4 being done +4. Batch 2 (REST namespace) — coordinated with settings addon +5. Batch 1 (JS restructure) — largest single change; do when PHP is settled +``` + +**Rationale:** +- Batches 4 + 3 are near-zero risk. Ship them first, enjoy the cleaner baseline. +- Batch 5 is easier after 4 (fewer dead methods to reason about when extracting the base class). +- Batch 2 is self-contained and coordinates with the settings addon — do it before any JS work that might surface REST dependencies. +- Batch 1 is the largest scope. Doing it last means the PHP side is stable and we're not juggling two moving targets. + +**Dependencies (strict):** + +- Batch 5 depends on Batch 4 (don't refactor code that's about to be deleted). +- Batch 2 core-plugin change and settings addon change must ship together (either atomic commit or immediate back-to-back). +- No other hard dependencies. Batches can be parallelized if preferred. + +**Alternative orderings** (acceptable): +- Strict "smallest first": 3 → 4 → 2 → 5 → 1 +- "PHP then JS, both internal": 4 → 3 → 5 → 1 → 2 + +## Per-batch detail + +Each batch's full checklist lives in [pass-a.md](pass-a.md). This section provides acceptance criteria and verification steps only. + +### Batch 1 — JS source reshape + +**Full checklist:** See [pass-a.md](pass-a.md) Batch 1. + +**Summary of touched files:** +- All files under `src/editor/` and `src/shared/` move to a flat `src/` layout +- `src/script.js` renamed to `src/index.js` +- `src/editor/hoc/with*.js` files convert from HOC-exports to module-scope side effects in `src/hooks/` +- `ValidationProvider.js` + `ValidationAPI.js` convert from renderless components to hooks (`useValidationSync`, `useValidationLifecycle`) +- New file `src/hooks/pre-save-validation.js` (adds `editor.preSavePost` filter) +- New file `src/utils/use-validation-issues.js` (consolidates duplicate `useSelect`) +- `src/utils/use-meta-field.js` consolidates its dual `useSelect` +- `getInvalid*.js` hook wrappers rename to `useInvalid*.js` +- Webpack aliases dropped; imports become relative +- `package.json` gains `sideEffects` field + +**Acceptance criteria:** +- [ ] `pnpm lint` passes +- [ ] `pnpm build` succeeds; `build/validation-api.js` still produced at expected path +- [ ] No file under `src/editor/` or `src/shared/` remains +- [ ] No `@`, `@editor`, `@shared` alias imports remain +- [ ] Grep for `ValidationProvider\|ValidationAPI` in JS finds no renderless-component definitions (only `useValidationSync` / `useValidationLifecycle`) + +**Manual verification (in WP):** +- [ ] Open a post — editor loads without console errors +- [ ] Integration example plugin's validation checks fire (block check, meta check, editor check all surface in sidebar) +- [ ] Sidebar opens under the validation icon; shows grouped issues +- [ ] Toolbar button appears on blocks with validation errors; clicking shows modal with messages +- [ ] Add/resolve an error; verify `lockPostSaving` toggles (Publish/Update button enables/disables) +- [ ] Attempt to save while in error state; `editor.preSavePost` throws; UI shows save failed +- [ ] Body classes `has-validation-errors` / `has-validation-warnings` apply correctly +- [ ] Click a block error in the sidebar; editor scrolls to and selects the block + +**Post-batch rebuild:** +- [ ] Rebuild `validation-api-integration-example` (`npm run build` in its directory) + +**Rollback:** Single large commit; revert if issues. Or: keep the old `src/editor/` + `src/shared/` tree in a branch until Batch 1 is verified. + +--- + +### Batch 2 — REST namespace move + +**Full checklist:** See [pass-a.md](pass-a.md) Batch 2. + +**Touched files:** +- `includes/Rest/ChecksController.php` — namespace + rest_base +- `validation-api-settings/src/settings/App.js` — fetch path +- Documentation: `docs/PROPOSAL.md`, `docs/technical/*`, `CLAUDE.md`, settings addon README + +**Acceptance criteria:** +- [ ] `curl -u admin:pass http://site/wp-json/wp-validation/v1/checks` returns the grouped-by-scope response +- [ ] `curl http://site/wp-json/wp/v2/validation-checks` returns 404 +- [ ] Grep across all plugins: no references to old path remain + +**Manual verification:** +- [ ] Settings addon page loads; table populates with all registered checks +- [ ] Change a check's level, save; reload; change persists +- [ ] `wp_validation_check_level` filter still applies the override in the editor (confirm by registering a check and setting it to "none" — check should disappear from validation surface) + +**Rollback:** Revert the two file changes and rebuild settings addon. + +--- + +### Batch 3 — I18n class simplification + +**Full checklist:** See [pass-a.md](pass-a.md) Batch 3. + +**Touched files:** +- `includes/Core/I18n.php` (deleted) +- `includes/Core/Assets.php` (inlined `wp_set_script_translations`) +- `includes/Core/Plugin.php` (I18n instantiation removed) + +**Acceptance criteria:** +- [ ] PHP loads without fatal errors +- [ ] `pnpm build` still succeeds +- [ ] `wp_set_script_translations` is called exactly once for the editor script handle (grep to confirm) + +**Manual verification:** +- [ ] If a translation `.json` exists in `languages/`, editor strings render translated +- [ ] No I18n-related PHP warnings in debug log + +**Rollback:** Revert the file changes; `I18n.php` is a pure deletion. + +--- + +### Batch 4 — PHP dead-code deletions + +**Full checklist:** See [pass-a.md](pass-a.md) Batch 4. + +**Touched files:** +- `includes/Meta/Validator.php` (deleted) +- `includes/Contracts/CheckProvider.php` (deleted) +- `includes/Contracts/` (directory removed if empty) +- `includes/Block/Registry.php` (two methods removed, two actions removed) +- `includes/Editor/Registry.php` (one method removed) +- `includes/Core/Traits/EditorDetection.php` (fallback branch removed) +- `docs/guide/check-providers.md` (removed or updated) +- `docs/technical/hooks.md` (two hooks removed from documentation, if listed) + +**Acceptance criteria:** +- [ ] PHP linting passes +- [ ] Plugin activates without fatal errors +- [ ] `includes/Contracts/` directory removed or empty +- [ ] No PHP warnings for missing classes/methods +- [ ] Grep confirms zero remaining references to: `Meta\Validator`, `CheckProvider`, `unregister_check`, `set_check_enabled`, `register_editor_check_for_post_types`, `wp_validation_check_unregistered`, `wp_validation_check_toggled` + +**Manual verification:** +- [ ] Settings addon still reads all checks via REST endpoint +- [ ] `wp_validation_check_level` filter still fires +- [ ] Integration example plugin registers all its checks (visible in settings addon table) +- [ ] Editor post/page editor still enqueues validation scripts (post editor context detection still works after removing the fallback branch) + +**Rollback:** Deletions are recoverable via git. Each deletion is independent — can restore one without affecting others. + +--- + +### Batch 5 — Registry abstract base class extraction + +**Full checklist:** See [pass-a.md](pass-a.md) Batch 5. + +**Touched files:** +- `includes/AbstractRegistry.php` (new file) +- `includes/Block/Registry.php` (extends new abstract class) +- `includes/Meta/Registry.php` (extends new abstract class) +- `includes/Editor/Registry.php` (extends new abstract class) + +**Acceptance criteria:** +- [ ] `pnpm lint` (PHP) passes +- [ ] Each registry's public method signatures unchanged +- [ ] `BlockRegistry::get_instance()`, `MetaRegistry::get_instance()`, `EditorRegistry::get_instance()` still return singletons +- [ ] Total PHP LOC reduced by ~115 compared to pre-Batch-5 +- [ ] REST endpoint response structure unchanged + +**Manual verification:** +- [ ] Register a block check via `wp_register_block_validation_check()` — appears in REST response +- [ ] Register a meta check — appears; the 3-level `[post_type][meta_key][check_name]` structure intact +- [ ] Register an editor check — appears +- [ ] Settings addon lists all checks across all three scopes +- [ ] Changing a check's level via settings still filters through `wp_validation_check_level` +- [ ] Check with duplicate `namespace`+`name` logs expected error (confirm `log_error` path still wired via `Logger` trait) +- [ ] Invalid level parameter (e.g., `'critical'`) logs error and defaults to `'error'` + +**Rollback:** This is the most involved batch. Revert the four file changes. Inspect each registry's `register_check()` method to confirm parity with pre-Batch-5 behavior. + +--- + +## Cross-batch verification (post-all-batches) + +After all five batches ship, run through these end-to-end checks: + +- [ ] Fresh WP install; activate core plugin only — no errors +- [ ] Activate integration example — all checks register, validation surfaces in the editor +- [ ] Activate settings addon — table populates, level overrides save and apply +- [ ] Disable core plugin — integration example's `function_exists` guards kick in, no errors +- [ ] Re-enable core plugin — everything reconnects +- [ ] Create a new post of each registered post type — checks fire appropriately per post type +- [ ] Publish a post with errors resolved — save succeeds +- [ ] Attempt to publish with errors — `lockPostSaving` prevents save; `editor.preSavePost` throws if somehow bypassed +- [ ] Change a check's level via settings addon to `'warning'` — UI updates to warning styling; save allowed +- [ ] Change a check's level to `'none'` — check disappears entirely +- [ ] PHP debug log shows no warnings/notices under WP_DEBUG +- [ ] JS console shows no errors or warnings + +## Post-batch polish (not required, nice to have) + +Tracked but not part of the five batches: + +- [ ] Add `@example` JSDoc blocks to public-facing hooks and utils (`useMetaField`, `useMetaValidation`, `useInvalidBlocks`, etc.) — matches Gutenberg package style +- [ ] Start TypeScript migration with `src/store/constants.ts` — matches `packages/editor/src/store/` +- [ ] Add unit tests for store reducer, selectors, actions (from [docs/TODO.md](../TODO.md)) +- [ ] Add unit tests for `validateBlock`, `validateMeta`, `validateEditor` utility functions +- [ ] Performance benchmarks with 200+, 500+, 1000+ block posts (from [docs/TODO.md](../TODO.md)) +- [ ] Add integration tests using `@wordpress/env` + `@wordpress/e2e-test-utils` + +These polishes are orthogonal to Gutenberg alignment. Do them on your own schedule. + +## Deferred — not in these batches + +Changes that only make sense when cutting the actual core PR are captured in [core-pr-migration.md](core-pr-migration.md). These include: +- PSR-4 namespaced classes → `WP_*` flat files in `lib/validation/` +- Text domain `validation-api` → `default` (or `gutenberg` while in plugin) +- `@since 1.0.0` → target WP version +- REST namespace from `wp-validation/v1` → whatever core accepts +- CSS class prefix rename +- JS `__()` text-domain argument removal +- Sidebar mount `registerPlugin` → direct `ComplementaryArea` +- `@package ValidationAPI` → `@package gutenberg` / `@package WordPress` +- Composer PSR-4 autoload → `require_once` chain + +## Sign-off checklist + +Before execution begins: + +- [ ] Plan reviewed and approved +- [ ] Batch order confirmed (recommended vs. alternative) +- [ ] Acceptance criteria understood for each batch +- [ ] Rollback strategy acceptable (per-batch git revert) +- [ ] Manual verification steps understood (no automated tests available) +- [ ] All three plugins accessible for testing (core, settings, integration example) +- [ ] Test WordPress site available + +During execution (per batch): + +- [ ] Pre-batch git status clean +- [ ] Batch checklist in [pass-a.md](pass-a.md) followed item-by-item +- [ ] Acceptance criteria verified +- [ ] Manual verification complete +- [ ] Commit with batch number in message +- [ ] Move to next batch + +After all batches: + +- [ ] Cross-batch verification passed +- [ ] Post-batch polish items scheduled +- [ ] [core-pr-migration.md](core-pr-migration.md) reviewed for completeness + +## Notes + +- This plan assumes no automated test suite. Manual verification is the gate. +- The user owns all consumers of the plugins (core, settings addon, integration example), so no backward-compat concerns apply. +- Estimated total effort: Batch 4 ~1 hour, Batch 3 ~15 min, Batch 5 ~2-3 hours, Batch 2 ~30 min, Batch 1 ~4-6 hours. Total ~8-11 hours of focused work. +- The JS restructure (Batch 1) is the single biggest time investment; most of it is mechanical file moves + import rewriting. diff --git a/docs/gutenberg-alignment/core-pr-migration.md b/docs/gutenberg-alignment/core-pr-migration.md new file mode 100644 index 0000000..61f75f7 --- /dev/null +++ b/docs/gutenberg-alignment/core-pr-migration.md @@ -0,0 +1,341 @@ +# Core-PR Migration Guide + +Changes that only make sense when cutting the actual Gutenberg core pull request — deferred from the standalone-plugin alignment work. This document is the authoritative checklist for producing a core-ready codebase. + +**This document is not active until all five NOW-batches from [consolidated-plan.md](consolidated-plan.md) have shipped.** + +For the proposal that sets up this migration, see [docs/PROPOSAL.md](../PROPOSAL.md). + +## When to activate this doc + +Activate when all of the following are true: +- [ ] Batches 1–5 (consolidated plan) are complete and verified +- [ ] Plugin has been released and is stable in standalone form +- [ ] Gutenberg team has responded favorably to the proposal (or you've decided to submit regardless) +- [ ] Target WordPress version for the merge is known (e.g., WP 6.9) +- [ ] You have a local `gutenberg/` trunk checkout on a fresh PR branch + +## Why these are deferred + +Each deferred item either: +1. Would break the plugin's standalone functionality (e.g., text domain change breaks localization) +2. Is meaningless until the target WP version is known (e.g., `@since` tags) +3. Requires Gutenberg-core-team input (e.g., final REST namespace, CSS class prefix) +4. Makes the code harder to maintain as a standalone plugin (e.g., PSR-4 → flat WP-core style) + +## Migration overview + +Five major migrations: + +| # | Migration | Blocker until | Est. effort | +|---|---|---|---| +| 1 | PHP architecture: PSR-4 namespaced classes → WP-core flat `class-wp-*.php` | PR branch cut | Large (rewrite PHP tree) | +| 2 | JS package mount: standalone plugin → Gutenberg package inside `packages/validation/` | PR branch cut | Medium | +| 3 | i18n: `'validation-api'` text domain → `'gutenberg'` (plugin) or `'default'` (core) | PR branch cut | Small (find-replace) | +| 4 | Versioning: `@since 1.0.0` → target WP version | WP version known | Small | +| 5 | Naming: CSS classes, namespace keys, tag prefixes | Core review | Small-medium | + +## Target PHP architecture + +### File tree after migration + +``` +gutenberg/lib/validation/ +├── load.php # require_once chain, hook registrations +├── class-wp-validation-registry.php # Abstract base (renamed from AbstractRegistry) +├── class-wp-validation-block-registry.php # from Block\Registry +├── class-wp-validation-meta-registry.php # from Meta\Registry +├── class-wp-validation-editor-registry.php # from Editor\Registry +└── class-wp-rest-validation-checks-controller.php # from Rest\ChecksController +``` + +### Class renaming map + +| Current (standalone) | Target (core) | +|---|---| +| `ValidationAPI\AbstractRegistry` | `WP_Validation_Registry` | +| `ValidationAPI\Block\Registry` | `WP_Validation_Block_Registry` | +| `ValidationAPI\Meta\Registry` | `WP_Validation_Meta_Registry` | +| `ValidationAPI\Editor\Registry` | `WP_Validation_Editor_Registry` | +| `ValidationAPI\Rest\ChecksController` | `WP_REST_Validation_Checks_Controller` | +| `ValidationAPI\Core\Plugin` | (removed; replaced by functions in `load.php`) | +| `ValidationAPI\Core\Assets` | (removed; functions in `load.php` or `client-assets.php`) | +| `ValidationAPI\Core\Traits\Logger` | Inlined (WP core doesn't use traits) | +| `ValidationAPI\Core\Traits\EditorDetection` | Inlined or moved to a helper function | + +### Global function signatures (unchanged) + +These stay identical — they're already core-style: +- `wp_register_block_validation_check( $block_type, $args )` +- `wp_register_meta_validation_check( $post_type, $args )` +- `wp_register_editor_validation_check( $post_type, $args )` + +### Autoload change + +- **Remove** Composer PSR-4 autoload (`composer.json` goes away or gets stripped) +- **Add** `require_once` chain in `lib/validation/load.php`: + ```php + require_once __DIR__ . '/class-wp-validation-registry.php'; + require_once __DIR__ . '/class-wp-validation-block-registry.php'; + require_once __DIR__ . '/class-wp-validation-meta-registry.php'; + require_once __DIR__ . '/class-wp-validation-editor-registry.php'; + require_once __DIR__ . '/class-wp-rest-validation-checks-controller.php'; + ``` +- **Include** `lib/validation/load.php` from Gutenberg's main `lib/load.php` or equivalent loader + +### Trait migration + +- `Logger` trait's `log_error()` and `log_debug()` methods: inline as two standalone functions (likely `_wp_validation_log_error()`, `_wp_validation_log_debug()`) or use the `wp_trigger_error()` / `error_log()` conventions preferred by core. Confirm the current WP core convention at migration time. +- `EditorDetection` trait: collapse the `get_editor_context()` logic into a helper function in `lib/validation/load.php` (e.g., `_wp_validation_get_editor_context()`). + +### Singleton preservation + +WP core-style singletons still work — the existing pattern (`private __construct` + `static $instance` + `get_instance()`) is fine. `WP_Connector_Registry` in `gutenberg/lib/compat/wordpress-7.0/class-wp-connector-registry.php` is a direct analog and can be referenced. + +## Target JS package + +### Target location + +``` +gutenberg/packages/validation/ +├── package.json +├── README.md +├── CHANGELOG.md +├── src/ +│ ├── index.js +│ ├── store/ +│ ├── hooks/ +│ ├── components/ +│ ├── utils/ +│ └── style.scss +└── build/, build-module/, build-types/ # generated +``` + +### Migration steps + +1. `git mv wp-content/plugins/validation-api/src gutenberg/packages/validation/src` +2. Copy `package.json` pattern from an existing Gutenberg package (e.g., `packages/notices/package.json`) and adapt: + - `name: "@wordpress/validation"` + - `version: "0.1.0"` or per Gutenberg's versioning policy + - `sideEffects` declaration as in current standalone `package.json` + - `wpScript: true` + - `react-native`, `exports`, `main`, `module`, `types` fields per Gutenberg convention +3. Delete `wp-content/plugins/validation-api/` from working copy (or keep as a cross-check for the migration) +4. Update `lerna.json` and `package.json` in Gutenberg root to include the new package in the monorepo + +### Sidebar mount migration + +**Current (standalone):** +```js +// src/hooks/register-sidebar.js +registerPlugin( 'core-validation', { render: ValidationPlugin } ); +``` + +**Target (core):** +The sidebar becomes a direct child of the editor layout, mounted via `` from `@wordpress/interface`. No `registerPlugin` call. + +Approach: pattern match on how Gutenberg's own Document and Block sidebars mount — they're rendered directly inside `packages/editor/src/components/sidebar/` or equivalent. The validation sidebar would sit alongside them. + +Reference file to diff against at migration time: `gutenberg/packages/editor/src/components/sidebar/index.js` (or the current-trunk equivalent). + +### Side-effect hooks + +The `src/hooks/` pattern works identically in core. `src/hooks/index.js` continues to import side-effect files on package load. One change: the `ValidationPlugin` root component (which currently calls `useValidationSync` + `useValidationLifecycle`) no longer needs to exist as a `registerPlugin` payload — those hooks would be called from wherever the core sidebar is mounted, or from a dedicated `` mount inside the editor provider tree. + +### Webpack / build + +Gutenberg's monorepo build handles packages automatically via the root config. The standalone plugin's `webpack.config.js` goes away. + +## Target i18n + +### PHP side + +**Mechanical find-and-replace in the migrated `lib/validation/` files:** + +- `__( 'text', 'validation-api' )` → `__( 'text' )` (in WP core, default text domain is used by not specifying) +- OR `__( 'text', 'gutenberg' )` if the code still lives in the Gutenberg plugin + +Confirm target text domain from Gutenberg's `phpcs.xml.dist` at migration time. Gutenberg currently accepts both `'gutenberg'` and `'default'` per: + +```xml + + + + + + +``` + +### JS side + +- Remove text-domain argument from all `__()`, `_x()`, `_n()`, `sprintf( __( ... ) )` calls: + ```js + __( 'Validation', 'validation-api' ) → __( 'Validation' ) + ``` +- JS translations are loaded by `wp_set_script_translations()` in PHP (configured at enqueue time). No domain needed at the call site. + +### `wp_set_script_translations` target + +Currently called inline in `Core/Assets.php` (post-Batch-3). In core, the script registration happens in `gutenberg/lib/client-assets.php` or a similar loader. Follow the pattern used for other Gutenberg packages — they typically get auto-registered by the Gutenberg build pipeline. + +## Target versioning + +### `@since` tags + +Find-and-replace all `@since 1.0.0` with the target WP version: +- `@since 1.0.0` → `@since 6.x.y` (where 6.x.y is the WP version the feature will ship in) + +**Do not do this until the target version is confirmed.** Gutenberg PR reviews sometimes reassign version targets during review. + +### `@package` tags + +- PHP: `@package ValidationAPI` → `@package gutenberg` (while in plugin) or `@package WordPress` (once merged to core) +- JS: usually no `@package` tags in JSDoc + +## Target naming (CSS classes, internal keys) + +### CSS class prefix + +Currently `validation-api-*`. Target prefix options (pick one after core review): +- `wp-validation-*` — neutral, matches other core UI patterns +- `editor-validation-*` — if scoped to post editor +- `validation-*` — minimalist but may collide + +**Mechanical migration steps** once prefix is locked: + +1. Find-and-replace in SCSS: `validation-api-` → `NEW_PREFIX-` +2. Find-and-replace in JS `className={ ... }` and template literals: same +3. Find-and-replace in `withBlockValidationClasses.js` constants +4. Search for any DOM queries using the old classes and update + +### Editor settings key + +Currently `validationApi` in `block_editor_settings_all` injection. Target: +- `validation` (if namespace is clean enough) +- `__experimentalValidation` (if initial merge is experimental) + +Pick based on Gutenberg's conventions at migration time. Recent examples: `__experimentalFeatures`, `__experimentalGlobalStylesUserEntityId`. + +### Store name + +Currently `core/validation`. **Keep this.** Already core-style. Confirm no collision at migration time. + +### Global namespace registration key + +Internal `_namespace` field stamped from `namespace` arg. Keep; it's internal implementation. + +### PHP hook names + +All `wp_validation_*` prefixed hooks stay — already core-style. No rename. + +### JS filter names + +`editor.validateBlock`, `editor.validateMeta`, `editor.validateEditor` — already core-style (`editor.*` namespace). No rename. + +## Target REST namespace + +Currently `wp-validation/v1/checks`. Core review will decide: + +- **Option A:** `wp/v2/validation-checks` — if validation becomes a stable core feature +- **Option B:** `wp-block-editor/v1/validation-checks` — if it's editor-specific +- **Option C:** Keep `wp-validation/v1/checks` — if the feature is considered its own namespace + +Mechanical migration: +1. Update `$this->namespace` and `$this->rest_base` in the REST controller +2. Update the settings addon's `apiFetch` path — but the settings addon probably doesn't come to core; it stays a plugin +3. Update documentation + +## PR branch cut — procedure + +Suggested procedure for creating the actual PR: + +1. Clone Gutenberg trunk locally if not already present +2. Create a feature branch: `feat/validation-api` +3. Copy PHP tree to `lib/validation/` with rename (see PHP architecture section) +4. Copy JS tree to `packages/validation/src/` (see JS package section) +5. Add `packages/validation/package.json` based on a similar package +6. Run mechanical find-and-replace migrations (text domain, `@since`, CSS prefix, `@package`) +7. Inline trait methods (Logger, EditorDetection) +8. Remove `Core/Plugin.php` initialization pattern; replace with functional `lib/validation/load.php` +9. Wire the new `lib/validation/` loader into Gutenberg's main loader +10. Register the new `packages/validation/` in `lerna.json` and root `package.json` +11. Adapt sidebar mount to use `ComplementaryArea` directly instead of `registerPlugin` +12. Run Gutenberg's `npm run build` and `phpcs` +13. Fix any core-convention violations caught by lint +14. Smoke test: activate Gutenberg in a local WP, register a test check, verify it surfaces in the editor + +## PR description template + +Starter content for the PR description — flesh out per Gutenberg's contribution guidelines at submission time: + +``` +## What? + +Introduces a Validation API for the block editor — a declarative framework for +registering, executing, and displaying content validation checks in real time. + +## Why? + +[Reference docs/PROPOSAL.md — summarize the problem and existing primitives.] + +## How? + +- Three validation scopes (block, meta, editor) with corresponding PHP registries + and JS filter hooks +- Dedicated @wordpress/data store (core/validation) +- Severity model (error/warning/none) with runtime override filter +- Editor UI: sidebar (issue list), per-block toolbar button, body CSS classes +- Post-save gate via editor.preSavePost + lockPostSaving +- No new low-level primitives — built on existing @wordpress/hooks, @wordpress/data, + lockPostSaving, @wordpress/interface, block editor filter hooks + +## Testing Instructions + +[Provide register-a-check example + verification flow] + +## Screenshots/Videos + +[Sidebar showing issues, toolbar button, save-locked state, save attempt with error] + +## References + +- Proposal: [link to publicly-hosted PROPOSAL.md] +- Reference implementation as plugin: [github.com/troychaplin/validation-api] +- Integration example: [github.com/troychaplin/validation-api-integration-example] +- Companion settings plugin (not proposed for core): [github.com/troychaplin/validation-api-settings] +``` + +## Open questions for core review + +Track these separately when submitting — the PR discussion will resolve them: + +1. **REST namespace** — core team's preference (see Target REST Namespace) +2. **CSS class prefix** — core team's preference (see Target Naming) +3. **Editor settings key** — `validation` vs `__experimentalValidation` (see Target Naming) +4. **Whether API ships as a package** (`@wordpress/validation`) or lives inside `@wordpress/editor` +5. **Site editor support** — currently excluded; core team may want template validation considered +6. **Async validation via `applyFiltersAsync` on `editor.validateBlock`** — current filters are sync; core team may want async design +7. **Block.json validation declaration** — is there appetite for declarative simple checks in block.json (e.g., `"attributes": { "alt": { "validation": { "required": true } } }`)? +8. **TypeScript conversion timeline** — full TS now, partial, or defer? + +## Items NOT requiring migration + +These stay as-is through core merge: + +| Item | Reason | +|---|---| +| Global PHP function names (`wp_register_*_check`) | Already core-style | +| JS filter names (`editor.validate*`) | Already core-style | +| Store name (`core/validation`) | Already core-style | +| PHP hook prefix (`wp_validation_*`) | Already core-style | +| Severity model (`error`/`warning`/`none`) | Stable | +| `block_editor_settings_all` injection | Canonical mechanism | +| Registry singleton pattern | Matches `WP_Connector_Registry` | +| HOC-via-filter approach for block integrations | Matches recent Gutenberg patterns | +| `lockPostSaving` + `editor.preSavePost` save-locking | Canonical pair | + +## Notes + +- The companion settings plugin (`validation-api-settings`) does NOT migrate to core. It stays a standalone plugin that consumes the core API via REST + filter. Its ongoing maintenance is separate. +- The integration example plugin is a documentation artifact; it doesn't migrate either, but its patterns should be preserved as live documentation. +- If the PR is rejected or stalls, the standalone plugin can continue to ship independently. No work in this migration doc is irreversible; it's a translation of the standalone form into core-style. diff --git a/docs/gutenberg-alignment/pass-a.md b/docs/gutenberg-alignment/pass-a.md new file mode 100644 index 0000000..f63ff17 --- /dev/null +++ b/docs/gutenberg-alignment/pass-a.md @@ -0,0 +1,380 @@ +# Pass A — Convention & Alignment + +Review of the Validation API plugin against current Gutenberg conventions. This pass covers naming, file organization, export patterns, and style. It does **not** cover architectural decisions (Pass B) or leanness / duplication (Pass C). + +## Status + +- [x] Review complete +- [x] Decisions locked (see below) +- [ ] Consolidated plan reviewed (awaits Pass B + Pass C) +- [ ] Execution + +## Decisions locked during Pass A + +| Decision | Value | +|---|---| +| REST namespace (standalone phase) | `wp-validation/v1/checks` | +| Batch 1 execution style | Single coherent restructure (owner controls all consumers; no need to split) | +| Backwards compatibility | Not required — owner controls all consuming plugins | + +## Reference sources + +- Gutenberg trunk: `wp-content/plugins/gutenberg/` — sampled `packages/editor`, `packages/block-editor`, `packages/core-data`, `packages/notices`, `packages/plugins`, `packages/interface`, and `lib/`, `lib/experimental/`, `lib/compat/` +- See `docs/PROPOSAL.md` for the core-merge pitch this alignment supports + +## Findings summary + +### What already aligns with Gutenberg (do not change) + +| Area | Current state | +|---|---| +| Store name | `core/validation` — matches `core/*` convention | +| JS filter namespace | `editor.validateBlock` / `editor.validateMeta` / `editor.validateEditor` — matches `editor.*` behavioral-filter pattern | +| Global PHP function names | `wp_register_block_validation_check()` etc. — core-style | +| PHP hook prefix | `wp_validation_*` — positioned for core | +| Registry singleton pattern | Static `get_instance()` — matches `WP_Connector_Registry` | +| Editor settings injection | `block_editor_settings_all` filter — canonical | +| Selector naming | `getInvalidBlocks`, `hasErrors`, `hasWarnings` — matches `get*`/`has*`/`is*` convention | +| Action naming | `setInvalidBlocks` (present tense) — matches `editPost`/`savePost` style | +| Filter name strings | Inline (not constants) — matches Gutenberg | +| CSS class prefix `validation-api-*` | Correct for standalone; renamed at core-PR time | + +### Tier 1 — blockers for core merge (deferred; standalone-safe as-is) + +These are addressed in `core-pr-migration.md` (drafted after all three passes complete). + +- PSR-4 namespaced classes → `WP_*` flat classes under `lib/validation/` +- Text domain `validation-api` → `gutenberg` → `default` +- `@since 1.0.0` → target WP version +- CSS class prefix rename +- Drop text-domain argument from JS `__()` calls + +### Tier 2 — significant alignment (addressed below or deferred to Pass B/C) + +- [Batch 1] JS entry point and package shape: `src/script.js` + `registerPlugin` + `src/editor/` + `src/shared/` split + webpack aliases +- [Batch 2] REST namespace `wp/v2/validation-checks` → `wp-validation/v1/checks` +- [Pass B] REST permission callback `manage_options` — audit consumers +- [Pass B] `registerPlugin('core-validation', ...)` — keep for standalone sidebar mount, but verify against architectural review +- [Pass C] Three duplicate `Registry` classes — collapse candidate +- [Pass C] Six lifecycle hooks — audit consumers, prune unused + +### Tier 3 — polish (addressed below) + +- [Batch 1] `package.json` `sideEffects` declaration +- [Batch 1] `getInvalid*.js` hook wrappers renamed to `useInvalid*.js` +- [Batch 3] `Core/I18n.php` collapsed to an enqueue line +- [Future] JSDoc `@example` blocks on public APIs (optional polish, separate task) +- [Future] TypeScript for `store/constants.ts` (optional, defer until after Pass C) + +--- + +## Action plan + +Three independent batches. Order of execution is decided in the consolidated plan after Pass B and Pass C. + +--- + +### Batch 1 — JS source reshape + +**Goal:** Restructure `src/` to match Gutenberg package layout. Pure reorganization. No public API changes. + +**Risk:** Low. Internal file paths change; public API (store name, filter names, global PHP functions) unchanged. Integration example requires rebuild but no code change. + +#### Checklist + +**Directory flattening — file moves:** + +- [ ] `src/editor/store/` → `src/store/` +- [ ] `src/editor/store/constants.js` → `src/store/constants.js` +- [ ] `src/editor/store/actions.js` → `src/store/actions.js` +- [ ] `src/editor/store/selectors.js` → `src/store/selectors.js` +- [ ] `src/editor/store/reducer.js` → `src/store/reducer.js` +- [ ] `src/editor/store/index.js` → `src/store/index.js` +- [ ] `src/editor/components/ValidationSidebar.js` → `src/components/validation-sidebar/index.js` +- [ ] `src/editor/components/ValidationToolbarButton.js` → `src/components/validation-toolbar-button/index.js` +- [ ] `src/editor/components/ValidationIcon.js` → `src/components/validation-icon/index.js` +- [ ] `src/editor/components/ValidationProvider.js` → `src/hooks/use-validation-sync.js` — **convert from renderless component to hook per Pass B finding B-1**. Export `useValidationSync()`; it runs the three `GetInvalid*` hooks and dispatches to the store via `useEffect`. +- [ ] `src/editor/validation/ValidationAPI.js` → `src/hooks/use-validation-lifecycle.js` — **convert from renderless component to hook per Pass B finding B-1**. Export `useValidationLifecycle()`; it `useSelect`s from the store and runs the two `useEffect`s (save-locking + body CSS classes). +- [ ] `src/editor/validation/blocks/validateBlock.js` → `src/utils/validate-block.js` +- [ ] `src/editor/validation/meta/validateMeta.js` → `src/utils/validate-meta.js` +- [ ] `src/editor/validation/editor/validateEditor.js` → `src/utils/validate-editor.js` +- [ ] `src/editor/validation/meta/hooks/useMetaField.js` → `src/utils/use-meta-field.js` +- [ ] `src/editor/validation/meta/hooks/useMetaValidation.js` → `src/utils/use-meta-validation.js` +- [ ] `src/shared/utils/validation/issueHelpers.js` → `src/utils/issue-helpers.js` +- [ ] `src/shared/utils/validation/getValidationConfig.js` → `src/utils/get-validation-config.js` +- [ ] `src/shared/hooks/useDebouncedValidation.js` → `src/utils/use-debounced-validation.js` +- [ ] `src/editor/register.js` → `src/hooks/register-sidebar.js` — calls `registerPlugin( 'core-validation', { render: ValidationPlugin, icon } )`. The `ValidationPlugin` root component (defined in this file or co-located) calls `useValidationSync()` and `useValidationLifecycle()` inside its body, then returns ``. Keeps hooks running even when the sidebar conditionally returns `null`. (Pass B finding B-1.) +- [ ] **New file `src/hooks/pre-save-validation.js`** — Pass B finding B-2. Registers `addFilter( 'editor.preSavePost', 'validation-api/pre-save-gate', async ( edits ) => { if ( select( validationStore ).hasErrors() ) throw new Error( '...' ); return edits; } )` as a save-time safety net layered on top of `lockPostSaving`. +- [ ] **New file `src/utils/use-validation-issues.js`** — Pass C finding C-9. Extracts the duplicated 7-line `useSelect` block (currently in `ValidationSidebar` and post-Pass-B `useValidationLifecycle`) into a single shared hook returning `{ invalidBlocks, invalidMeta, invalidEditorChecks }`. Update both consumers. +- [ ] **Consolidate dual `useSelect` in `use-meta-field.js`** — Pass C finding C-10. Currently calls `useMetaValidation()` (which has its own `useSelect`) plus a second `useSelect` for the meta value. Merge into one `useSelect` that reads both. +- [ ] Remove empty `src/editor/`, `src/shared/`, index barrel files that no longer serve + +**HOC → side-effect hook files:** + +- [ ] `src/editor/hoc/withErrorHandling.js` → `src/hooks/validate-block.js` + - Convert from `export default createHigherOrderComponent(...)` to a module-scope `addFilter('editor.BlockEdit', 'validation-api/error-handling', withErrorHandling)` side effect +- [ ] `src/editor/hoc/withBlockValidationClasses.js` → `src/hooks/block-validation-classes.js` + - Same conversion: HOC defined locally, `addFilter('editor.BlockListBlock', 'validation-api/block-classes', ...)` at module scope + +**New file — `src/hooks/index.js`:** + +```js +// Side-effect imports — each module registers its filter/plugin on import +import './register-sidebar'; +import './validate-block'; +import './block-validation-classes'; +import './pre-save-validation'; +``` + +*Note: `use-validation-sync.js` and `use-validation-lifecycle.js` are custom hooks (not side-effect modules), so they are NOT imported here. They are imported by `register-sidebar.js`'s `ValidationPlugin` component and invoked inside its render.* + +**Getter-style hooks renamed to `use*`:** + +- [ ] `src/shared/utils/validation/getInvalidBlocks.js` → `src/utils/use-invalid-blocks.js` (rename export `getInvalidBlocks` → `useInvalidBlocks`) +- [ ] `src/shared/utils/validation/getInvalidMeta.js` → `src/utils/use-invalid-meta.js` (rename export `getInvalidMeta` → `useInvalidMeta`) +- [ ] `src/shared/utils/validation/getInvalidEditorChecks.js` → `src/utils/use-invalid-editor-checks.js` (rename export `getInvalidEditorChecks` → `useInvalidEditorChecks`) +- [ ] Update all call sites (likely in `ValidationProvider.js`) +- [ ] **Note:** Store selectors keep `getInvalid*` names — only the React-hook wrapper files rename + +**Entry point:** + +- [ ] `src/script.js` → `src/index.js` +- [ ] `src/index.js` body: + +```js +import './store'; // registers core/validation store (side effect) +import './hooks'; // registers filters + sidebar (side effects) +import './styles.scss'; + +// Public exports (for any future consumer importing from build/) +export * from './store'; +export * from './utils'; +export { default as ValidationSidebar } from './components/validation-sidebar'; +export { default as ValidationToolbarButton } from './components/validation-toolbar-button'; +export { default as ValidationIcon } from './components/validation-icon'; +``` + +- [ ] Update `webpack.config.js` entry: `index: path.resolve(__dirname, 'src/index.js')` — keep output filename `validation-api.js` so `Core/Assets.php` enqueue path doesn't change +- [ ] Verify `Core/Assets.php` still finds the built file + +**Webpack aliases removed:** + +- [ ] Delete `resolve.alias` entries for `@`, `@editor`, `@shared` from `webpack.config.js` +- [ ] Convert every `@/...`, `@editor/...`, `@shared/...` import in JS files to relative path +- [ ] Verify no alias references remain (grep `@editor/`, `@shared/`, `from '@/`) + +**Styles:** + +- [ ] `src/styles.scss` and `src/styles/` tree — keep at `src/styles.scss` + `src/styles/` (Gutenberg packages typically co-locate style per component, but plugin-scoped global stylesheet is acceptable) +- [ ] Verify component folder style co-location works: `src/components/validation-sidebar/style.scss` for component-scoped styles, if any exist + +**`package.json`:** + +- [ ] Add `sideEffects` field: + +```json +"sideEffects": [ + "src/index.js", + "src/hooks/**", + "src/store/index.js", + "src/**/*.scss" +] +``` + +- [ ] Update `main` field if present (probably `build/validation-api.js`) — no change needed unless entry filename changes + +**Integration example plugin rebuild:** + +- [ ] After Batch 1 ships, rebuild `validation-api-integration-example` (`npm run build` in its directory) +- [ ] Verify its validation hooks still fire — the public filter names and PHP functions didn't change, so this should just work + +**Settings addon:** + +- [ ] No changes required from Batch 1 alone (settings addon doesn't import from core plugin JS) + +#### Acceptance criteria + +- [ ] `pnpm build` completes cleanly +- [ ] Plugin loads in `wp-admin` with no console errors +- [ ] Opening a post shows the validation sidebar under its usual icon +- [ ] Integration example plugin's checks still fire (block, meta, editor scopes) +- [ ] Settings addon settings page still loads (REST endpoint unchanged in Batch 1) +- [ ] No file under `src/editor/` or `src/shared/` remains +- [ ] No webpack alias imports remain +- [ ] `pnpm lint` passes + +--- + +### Batch 2 — REST namespace move + +**Goal:** Move REST route off `wp/v2` (reserved core namespace) to plugin-owned `wp-validation/v1`. Settings addon updated in the same change. + +**Risk:** Low for owner (all consumers controlled). Any third-party consumer of old endpoint breaks — acceptable. + +**Decision locked:** `wp-validation/v1/checks` + +#### Checklist + +**Core plugin:** + +- [ ] `includes/Rest/ChecksController.php`: change `$this->namespace = 'wp/v2'` → `$this->namespace = 'wp-validation/v1'` +- [ ] `includes/Rest/ChecksController.php`: change `$this->rest_base = 'validation-checks'` → `$this->rest_base = 'checks'` +- [ ] Verify no other file hard-codes the old path +- [ ] Grep for `wp/v2/validation-checks` across codebase — replace with `wp-validation/v1/checks` + +**Settings addon:** + +- [ ] `validation-api-settings/src/settings/App.js`: update the `apiFetch({ path: '/wp/v2/validation-checks' })` call to `/wp-validation/v1/checks` +- [ ] Grep settings addon for old path — replace +- [ ] Rebuild settings addon (`npm run build` in its directory) + +**Documentation:** + +- [ ] Update `docs/PROPOSAL.md` — the "REST API" paragraph currently cites `GET /wp/v2/validation-checks`. Replace with `GET /wp-validation/v1/checks` and add a note: "final namespace in core TBD during PR — candidates: `wp/v2/validation-checks`, `wp-block-editor/v1/validation-checks`" +- [ ] Update `docs/technical/` REST reference if present +- [ ] Update `CLAUDE.md` REST API section in core plugin (`/wp/v2/validation-checks` → `/wp-validation/v1/checks`) +- [ ] Update settings addon README if it documents the REST integration + +#### Acceptance criteria + +- [ ] `curl -u admin:pass http://site/wp-json/wp-validation/v1/checks` returns the expected grouped-by-scope response +- [ ] `curl http://site/wp-json/wp/v2/validation-checks` returns 404 +- [ ] Settings addon page loads, table populates with checks +- [ ] Settings addon save round-trip works (POST → `wp_options` → reload → table reflects saved levels) + +--- + +### Batch 3 — I18n class simplification + +**Goal:** Match Gutenberg's functional style for script translation loading. + +**Risk:** None. Same behavior, one less class. + +#### Checklist + +- [ ] Identify the `wp_set_script_translations()` call inside `includes/Core/I18n.php` +- [ ] Move the call inline into `includes/Core/Assets.php` wherever `wp_register_script()`/`wp_enqueue_script()` is called for the main validation-api editor script +- [ ] Remove `$this->i18n = new I18n(...)` and associated init from `includes/Core/Plugin.php` +- [ ] Delete `includes/Core/I18n.php` +- [ ] Verify Composer autoload works without the file (PSR-4 discovers by convention, no registry update needed) + +#### Acceptance criteria + +- [ ] `pnpm build` and PHP load still succeed +- [ ] Existing `.json` translation file (if any under `languages/`) still loads for the editor script +- [ ] `wp_set_script_translations` is called exactly once for the editor script handle + +--- + +## Pending Pass B (architectural) — RESOLVED + +Pass B completed. Results folded into Batch 1 above, with full rationale in `pass-b.md`. Summary: + +- [x] **REST permission callback `manage_options`** — **KEEP**. Only consumer is the settings admin page; editor JS uses `block_editor_settings_all` injection, not the REST endpoint. `manage_options` is correct for an admin-only config endpoint. +- [x] **`registerPlugin('core-validation', ...)` for sidebar** — **KEEP** during standalone phase. Canonical for third-party plugins; Gutenberg reserves `ComplementaryArea` direct mount for built-in sidebars. Swap deferred to core-PR. +- [x] **`ValidationProvider` / `ValidationAPI` renderless components** — **CONVERT TO HOOKS** (Batch 1 updated above). `useValidationSync` + `useValidationLifecycle` invoked from a single `ValidationPlugin` root component. +- [x] **`EditorDetection` trait** — **KEEP**. No Gutenberg PHP helper exists to replace it. Gutenberg features detect context per-feature (same pattern). +- [x] **`editor.preSavePost` usage** — **ADD** as belt-and-suspenders safety net (Batch 1 now includes new file `src/hooks/pre-save-validation.js`). + +## Pending Pass C (leanness) — RESOLVED + +Pass C completed. New batches 4 and 5 added below. Results summary: + +- [x] **Collapse three `Registry` classes** — Refined: extract `AbstractRegistry` base class (keep scope-specific subclasses due to state-shape differences). See Batch 5 below. ~115 LOC saved. +- [x] **Prune lifecycle actions** — Delete 2 (undocumented, coupled to dead methods): `wp_validation_check_unregistered`, `wp_validation_check_toggled`. Keep 4 + 3 filters (documented public API). +- [x] **`Contracts/CheckProvider` interface** — DELETE (no implementations found workspace-wide). See Batch 4. +- [x] **`Core/Traits/Logger` trait** — KEEP. 28 active call sites; debugging consistency value. +- [x] **`getValidationConfig.js` utility layer** — KEEP. The indirection earns its file. +- [x] **`EditorDetection` trait internals** — Trim 8 LOC dead `get_current_screen()` fallback branch. See Batch 4. +- [x] **Store-subscription consolidation** (Pass B forward) — Add `useValidationIssues()` hook; absorbed into Batch 1. +- [x] **Global editor-settings injection vs per-context** (Pass B forward) — Skip. No perf concern at current scale. + +--- + +## Batch 4 — PHP dead-code deletions (Pass C) + +**Goal:** Delete public API surfaces that have no consumers and no documented contract. + +**Risk:** Very low. All deletions are of unused code; public filter/action names that remain are those with documentation or active consumers. + +#### Checklist + +- [ ] **Delete `includes/Meta/Validator.php`** (109 LOC). No callers in workspace; server-side meta validation is available via `register_post_meta( ..., 'validate_callback' => ... )` directly. + - [ ] Remove any `use` statements or references to `ValidationAPI\Meta\Validator` elsewhere + - [ ] Remove mention from `docs/guide/` and `docs/technical/` if present +- [ ] **Delete `includes/Contracts/CheckProvider.php`** (47 LOC). No implementations in workspace. + - [ ] Remove the `includes/Contracts/` directory if empty afterward + - [ ] Remove mention from `docs/guide/check-providers.md` (or delete that guide entirely if it was the only content) +- [ ] **Delete `Block\Registry::unregister_check()`** (17 LOC) + the `wp_validation_check_unregistered` action it fires +- [ ] **Delete `Block\Registry::set_check_enabled()`** (12 LOC) + the `wp_validation_check_toggled` action it fires +- [ ] **Delete `Editor\Registry::register_editor_check_for_post_types()`** (9 LOC). Bulk convenience helper, never called. +- [ ] **Delete `EditorDetection::get_current_screen()` fallback branch** (lines where it falls through after the `$pagenow` + post-type checks; ~8 LOC). Unreachable in modern WP admin where `$pagenow` is always set. +- [ ] **Documentation cleanup:** + - [ ] Remove `wp_validation_check_unregistered` and `wp_validation_check_toggled` from `docs/technical/hooks.md` (if present — Pass C found them undocumented, but verify) + - [ ] Remove any `docs/guide/` examples using the deleted methods + +#### Acceptance criteria + +- [ ] `pnpm lint` (PHP portion) passes — no unresolved references to deleted classes/methods +- [ ] Plugin activates without fatal errors +- [ ] Settings addon's `wp_validation_check_level` filter still fires (confirm by loading settings page, toggling a level, saving, confirming the level applies) +- [ ] Integration example plugin still registers all its checks +- [ ] REST endpoint `GET /wp-validation/v1/checks` still returns expected structure + +--- + +## Batch 5 — Registry duplication reduction (Pass C) + +**Goal:** Extract shared registration/validation logic from three `Registry` classes into an abstract base class. Internal refactor; no public API change. + +**Risk:** Medium. Touches all three registries. Must verify behavior parity. + +#### Checklist + +- [ ] **Create `includes/AbstractRegistry.php`** with: + - `normalize_args( array $args, string $scope ): array` — merges defaults, validates level, sets `warning_msg` fallback, stamps `_namespace` from `namespace` arg + - `validate_required_args( array $args, array $required ): bool` — required-field check + log_error on failure + - `sort_by_priority( array &$checks ): void` — extracts the `uasort` pattern + - `use` the `Logger` trait +- [ ] **Refactor `Block\Registry` to extend `AbstractRegistry`** + - Replace duplicated defaults/validation block with `parent::normalize_args()` + `parent::validate_required_args()` + - Keep scope-specific methods (`register_check`, `get_registered_block_types`, etc.) + - Confirm hook firing (`wp_validation_check_registered`) still fires with same args +- [ ] **Refactor `Meta\Registry` to extend `AbstractRegistry`** + - Same approach; scope-specific 3-level storage stays + - Hook firing stays scope-specific (`wp_validation_meta_check_registered` if distinct, or same hook — confirm current behavior) +- [ ] **Refactor `Editor\Registry` to extend `AbstractRegistry`** + - Same approach +- [ ] **Confirm `BlockRegistry::get_instance()`, `MetaRegistry::get_instance()`, `EditorRegistry::get_instance()` all still return the same singleton instances** (important — settings addon expects these) + +#### Acceptance criteria + +- [ ] All existing tests pass (once tests exist — currently there are none; at minimum, manual regression) +- [ ] Integration example plugin registers all its checks without error +- [ ] Settings addon lists all registered checks in the table +- [ ] Total PHP LOC reduced by ~115 compared to pre-Batch-5 +- [ ] No change in public PHP API surface (same global function signatures, same hooks fired with same args) +- [ ] No change in REST endpoint response shape + +## Deferred to core-PR (future `core-pr-migration.md`) + +These are documented here as the authoritative list; full migration steps go in a dedicated doc after all three passes complete. + +| Item | Current | Core-PR target | Trigger | +|---|---|---|---| +| PHP class style | `ValidationAPI\Block\Registry` (PSR-4 namespaced) | `WP_Validation_Block_Registry` in `lib/validation/class-wp-validation-block-registry.php` | PR branch cut against `gutenberg/` | +| Class autoload | Composer PSR-4 | WP-style `require_once` chain in `lib/validation/load.php` | PR branch cut | +| Traits | `Core/Traits/Logger`, `Core/Traits/EditorDetection` | Inline or convert to abstract class (core rarely uses traits) | PR branch cut | +| Text domain | `validation-api` | `gutenberg` (while in plugin) → `default` (in core) | PR branch cut | +| JS `__()` text domain arg | `__( 'text', 'validation-api' )` | `__( 'text' )` (no domain) | PR branch cut | +| `@since` tag | `@since 1.0.0` | `@since 6.x.y` (target WP version) | PR branch cut; WP version confirmed | +| CSS class prefix | `validation-api-*` | `wp-validation-*` or as core review decides | PR branch cut | +| REST namespace | `wp-validation/v1/checks` | `wp/v2/validation-checks` OR `wp-block-editor/v1/validation-checks` | PR review feedback | +| Sidebar mount | `registerPlugin('core-validation', ...)` | Direct `ComplementaryArea` / slot registration from within package | PR branch cut | +| `@package` tag in PHP | `@package ValidationAPI` | `@package gutenberg` → `@package WordPress` | PR branch cut | + +## Notes / open questions + +- Recent-PR sampling for tone/commit style was skipped during Pass A. Trunk code is the style reference. If the eventual PR needs tone examples, do a 1-hour sampling pass just before drafting. +- TypeScript migration is optional polish; defer until after Pass C (no point typing code that may get consolidated or deleted). +- Integration example plugin's build needs to be re-run after Batch 1 ships. diff --git a/docs/gutenberg-alignment/pass-b.md b/docs/gutenberg-alignment/pass-b.md new file mode 100644 index 0000000..89ef08d --- /dev/null +++ b/docs/gutenberg-alignment/pass-b.md @@ -0,0 +1,314 @@ +# Pass B — Architectural Review + +Review of the Validation API plugin's architectural choices against modern Gutenberg patterns. This pass examines whether the plugin re-implements primitives Gutenberg already provides, and whether the patterns it chose are still the current best practice. + +## Status + +- [x] Review complete +- [x] Action items folded into Pass A doc (`pass-a.md`) Batch 1 +- [ ] Execution (awaits consolidated plan) + +## Scope of Pass B + +| Area examined | Verdict | +|---|---| +| Renderless components (`ValidationProvider`, `ValidationAPI`) | **Convert to hooks** (B-1) | +| `registerPlugin` for sidebar mount | Keep (standalone); swap at core-PR | +| `PluginSidebar` from `@wordpress/editor` | Keep | +| HOCs via `editor.BlockEdit` + `editor.BlockListBlock` | Keep | +| `lockPostSaving` / autosave / publish-sidebar locking | Keep | +| `editor.preSavePost` as save-time gate | **Add** (B-2) | +| `EditorDetection` trait (PHP) | Keep | +| `block_editor_settings_all` global vs per-context injection | Keep global for now | +| REST permission callback `manage_options` | Keep | +| Store subscription surface | Keep; possible Pass C consolidation | + +## Reference sources + +- Gutenberg packages sampled: `packages/editor`, `packages/block-editor`, `packages/core-data`, `packages/notices`, `packages/plugins`, `packages/interface`, `packages/components`, `packages/dataviews` +- Gutenberg PHP sampled: `lib/`, `lib/experimental/`, `lib/compat/` +- Key reference files (quoted in findings): + - `packages/plugins/src/api/index.ts` — `registerPlugin` implementation + - `packages/editor/src/components/plugin-sidebar/index.js` — `PluginSidebar` as wrapper + - `packages/interface/src/components/complementary-area/index.js` — `ComplementaryArea` + - `packages/editor/src/components/provider/use-upload-save-lock.js` — canonical hook-based save lock pattern + - `packages/editor/src/store/actions.js` — `editor.preSavePost` application, `lockPostSaving` action creators + - `packages/edit-widgets/src/filters/move-to-widget-area.js` — HOC + BlockControls pattern (modern) + - `packages/editor/src/hooks/pattern-overrides.js` — HOC with `createHigherOrderComponent` + +## Findings + +### B-1: Convert renderless components to hooks + +**Current state:** + +- `src/editor/components/ValidationProvider.js` — renderless component. Calls `GetInvalidBlocks()`, `GetInvalidMeta()`, `GetInvalidEditorChecks()`, then dispatches to `core/validation` store via `useEffect`. Returns `null`. +- `src/editor/validation/ValidationAPI.js` — renderless component. `useSelect`s from `core/validation` store, runs two `useEffect`s (save-locking + body CSS classes). Returns `null`. +- Both mounted as siblings in `registerPlugin`'s render prop alongside `ValidationSidebar`. + +**Gutenberg reference:** + +`packages/editor/src/components/provider/use-upload-save-lock.js`: + +```js +export function useUploadSaveLock() { + const isUploading = useSelect( /* ... */, [] ); + const { lockPostSaving, unlockPostSaving, lockPostAutosaving, unlockPostAutosaving } + = useDispatch( editorStore ); + + useEffect( () => { + if ( isUploading ) { + lockPostSaving( LOCK_NAME ); + lockPostAutosaving( LOCK_NAME ); + } else { + unlockPostSaving( LOCK_NAME ); + unlockPostAutosaving( LOCK_NAME ); + } + }, [ isUploading ] ); +} +``` + +This is the direct pattern match for what `ValidationAPI.js` does today. Gutenberg uses a hook, not a renderless component. + +Renderless components still exist in `packages/editor` (`global-keyboard-shortcuts`, `unsaved-changes-warning`, `theme-support-check`) but newer side-effect code uses hooks. + +**Proposed structure:** + +```js +// src/hooks/use-validation-sync.js +export function useValidationSync() { + const invalidBlocks = useInvalidBlocks(); + const invalidMeta = useInvalidMeta(); + const invalidEditorChecks = useInvalidEditorChecks(); + const { setInvalidBlocks, setInvalidMeta, setInvalidEditorChecks } + = useDispatch( validationStore ); + + useEffect( () => { setInvalidBlocks( invalidBlocks ); }, [ invalidBlocks, setInvalidBlocks ] ); + useEffect( () => { setInvalidMeta( invalidMeta ); }, [ invalidMeta, setInvalidMeta ] ); + useEffect( () => { setInvalidEditorChecks( invalidEditorChecks ); }, [ invalidEditorChecks, setInvalidEditorChecks ] ); +} +``` + +```js +// src/hooks/use-validation-lifecycle.js +export function useValidationLifecycle() { + const editorContext = getEditorContext(); + const isValidContext = editorContext === 'post-editor' || editorContext === 'post-editor-template'; + const { invalidBlocks, invalidMeta, invalidEditorChecks } = useSelect( /* ... */ ); + const { lockPostSaving, unlockPostSaving, /* ... */ } = useDispatch( editorStore ); + + useEffect( () => { /* save-locking logic */ }, [ /* deps */ ] ); + useEffect( () => { /* body CSS class logic */ }, [ /* deps */ ] ); +} +``` + +```js +// src/hooks/register-sidebar.js +import { registerPlugin } from '@wordpress/plugins'; +import { useValidationSync } from './use-validation-sync'; +import { useValidationLifecycle } from './use-validation-lifecycle'; +import ValidationSidebar from '../components/validation-sidebar'; + +function ValidationPlugin() { + useValidationSync(); + useValidationLifecycle(); + return ; +} + +registerPlugin( 'core-validation', { render: ValidationPlugin } ); +``` + +**Why this works:** + +- `ValidationSidebar` already returns `null` when no issues exist. Moving the hooks to `ValidationPlugin` (always rendered) keeps them running regardless of sidebar visibility. +- One root mount point instead of three siblings. +- Each hook is independently testable (no wrapper component needed in tests). +- Matches Gutenberg's `use-upload-save-lock.js` pattern exactly. + +**Why not:** + +- Behavior is identical either way; this is stylistic alignment, not a bug fix. + +**Integration with Batch 1:** Already folded into the Batch 1 checklist in `pass-a.md`. File moves updated to `src/hooks/use-validation-sync.js` and `src/hooks/use-validation-lifecycle.js`. + +--- + +### B-2: Add `editor.preSavePost` as save-time gate + +**Current state:** Plugin does not use `editor.preSavePost`. Save-blocking is entirely `lockPostSaving`-based in `ValidationAPI.js`. + +**Gutenberg reference:** `packages/editor/src/store/actions.js` applies the filter during save: + +```js +try { + edits = await applyFiltersAsync( + 'editor.preSavePost', + edits, + options + ); +} catch ( err ) { + error = err; +} +``` + +The filter is stable since WP 6.7 and is async. Throwing aborts the save. + +**How `lockPostSaving` and `editor.preSavePost` relate:** + +- `lockPostSaving` — guards the `savePost()` action via the `isPostSavingLocked()` selector check. Prevents save attempts from proceeding. +- `editor.preSavePost` — runs inside the save pipeline as an async filter. Can modify `edits` or throw to abort. + +They are complementary: +- Lock is the primary mechanism for a reactive, always-on save-gate. +- `preSavePost` is a per-save interception point, useful for: + - Race-condition safety (lock not yet applied when save fires) + - Async final validation (e.g., server-side check) + - Edge paths that don't go through `savePost`-action-as-normal + +**Proposed addition:** + +New file `src/hooks/pre-save-validation.js`: + +```js +import { addFilter } from '@wordpress/hooks'; +import { select } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { store as validationStore } from '../store'; + +addFilter( + 'editor.preSavePost', + 'validation-api/pre-save-gate', + async ( edits ) => { + if ( select( validationStore ).hasErrors() ) { + throw new Error( + __( 'Validation errors must be resolved before saving.', 'validation-api' ) + ); + } + return edits; + } +); +``` + +**Why:** + +- Cheap safety net. If the reactive lock is correct, this filter never fires against real errors. +- Matches the designed-in use of `editor.preSavePost`. +- Forward-compatible: if Gutenberg adds non-savePost save paths or plugins dispatch save directly, this catches them. + +**Why not:** + +- Redundant in the happy path. Skippable if minimal-surface-area is preferred. + +**Verdict:** Recommend adding. Low cost, low risk, semantically correct. + +**Integration with Batch 1:** Added as a new line item in `pass-a.md` Batch 1 checklist. File path: `src/hooks/pre-save-validation.js`. Imported from `src/hooks/index.js`. + +--- + +## Confirmed aligned — do not change + +### `registerPlugin('core-validation', ...)` for sidebar mount + +`packages/plugins/src/api/index.ts` confirms `registerPlugin` is designed for third-party plugin authors. Gutenberg's own built-in sidebars (Document, Block) mount `ComplementaryArea` directly in the editor layout — they do NOT use `registerPlugin`. For a third-party plugin, `registerPlugin` is correct. + +**Core-PR migration:** At merge time, the sidebar becomes a direct `` inside the editor's render tree, as Gutenberg does internally. Documented in `core-pr-migration.md` (to be written). + +### HOC pattern via `editor.BlockEdit` and `editor.BlockListBlock` + +Gutenberg's own code actively uses `createHigherOrderComponent` for these filters. Examples: + +- `packages/edit-widgets/src/filters/move-to-widget-area.js` — HOC on `editor.BlockEdit` adding `` fill +- `packages/editor/src/hooks/pattern-overrides.js` — HOC on `editor.BlockEdit` adding conditional controls +- `packages/editor/src/hooks/custom-sources-backwards-compatibility.js` — HOC on `editor.BlockEdit` for attribute shimming + +Plugin's `withErrorHandling` already matches this pattern exactly (HOC + `` fill). `withBlockValidationClasses` matches the `wrapperProps.className` injection pattern Gutenberg uses for `editor.BlockListBlock`. No change. + +### `lockPostSaving` / `lockPostAutosaving` / `disablePublishSidebar` + +Plugin uses all three with a single named lock (`'core/validation'`), reactive via `useEffect`. This matches `packages/editor/src/components/provider/use-upload-save-lock.js` precisely. No change. + +### `EditorDetection` trait (PHP) + +Gutenberg does **not** expose an `is_post_editor()` / `is_site_editor()` PHP helper. Features detect context per-feature using: + +- `$pagenow` +- `get_current_screen()` +- Request parameters (`post`, `post_type`, `postType`) + +The trait's approach is the same. No core helper to adopt. Keep as-is. + +**Note:** Pass C may find opportunities to simplify the trait's internals, but its *role* is correct. + +### `block_editor_settings_all` global injection + +Plugin injects all registered checks (across all post types) into editor settings under `validationApi.*` keys. JS filters to current post type client-side. + +**Gutenberg supports per-context injection** via `WP_Block_Editor_Context` and the REST endpoint context parameter. But for a plugin with a small number of checks per post type, global injection is fine. Revisit only if editor settings payload becomes large (e.g., hundreds of checks). + +Not changing now. Flagged for Pass C performance consideration. + +### REST permission callback `manage_options` + +Endpoint: `GET /wp-validation/v1/checks` (post Batch 2). + +**Consumer audit result:** The only consumer is `validation-api-settings/src/settings/App.js`. Editor JS receives config via `block_editor_settings_all` injection, not REST. Integration example plugin does not fetch the endpoint. + +**Gutenberg pattern:** +- `edit_posts` — editor-facing read config (most of block editor settings controller) +- `manage_options` — admin settings (guidelines, etc.) + +Since the sole consumer is the admin settings page (which already requires `manage_options`), the existing capability is correct. If a future feature needs editor-JS consumption, revisit. + +### Store subscription pattern + +`useSelect` with `[]` deps in `ValidationAPI.js` and `ValidationSidebar.js` is **correct usage** — the deps gate the `mapSelect` identity, not the selectors themselves. Store reactivity is independent. Not a bug. + +--- + +## Action items (added to Pass A Batch 1) + +The Pass B changes are *not* separate batches. They fold into Pass A's Batch 1 because they touch the same files Batch 1 is already moving/renaming. Doing them together avoids touching the same files twice. + +### Checklist additions to `pass-a.md` Batch 1: + +- [x] Rename `ValidationProvider.js` target from `src/validation-provider.js` (renderless) → `src/hooks/use-validation-sync.js` (hook) +- [x] Rename `ValidationAPI.js` target from `src/hooks/validation-lifecycle.js` (renderless) → `src/hooks/use-validation-lifecycle.js` (hook) +- [x] Update `register-sidebar.js` to define `ValidationPlugin` root component that calls both hooks, pass it as `registerPlugin`'s `render` +- [x] Add new file line: `src/hooks/pre-save-validation.js` with `addFilter('editor.preSavePost', ...)` side effect +- [x] Update `src/hooks/index.js` to import `pre-save-validation`; document that `use-validation-*` hooks are NOT imported here + +All edits already applied to `pass-a.md`. + +--- + +## Items forwarded to Pass C + +### Potential store-subscription consolidation + +`ValidationSidebar.js` and `useValidationLifecycle` both `useSelect` the same three selectors (`getInvalidBlocks`, `getInvalidMeta`, `getInvalidEditorChecks`). Each needs the values independently, so this is not a bug. But a shared `useValidationIssues()` hook could consolidate them. + +**Action:** Pass C evaluates whether consolidation is worthwhile or premature abstraction. + +### `EditorDetection` trait internals + +The trait uses `$pagenow`, site-editor post-type checks, and `get_current_screen()` fallbacks. Several branches may be dead code or redundant. Pass C leanness review. + +### `getValidationConfig` utility layer + +Wraps access to `select('core/editor').getEditorSettings().validationApi`. Whether this indirection earns its file is a Pass C call. + +### Global editor settings injection + +All checks for all post types always injected. If settings payload becomes a perf concern, use `WP_Block_Editor_Context` to filter per-request. Not a current issue; flagged for Pass C perf review. + +--- + +## Nothing new deferred to core-PR from Pass B + +All deferred items (registerPlugin → ComplementaryArea direct mount, etc.) were already captured in Pass A's deferred table. No additions from this pass. + +## Notes + +- The plugin's architecture is in better shape than a typical 4,000-LOC codebase would suggest. Most concerns raised during planning (renderless vs. hooks being the only notable one) turn out to be trend shifts rather than mistakes. +- `editor.preSavePost` is the one genuinely *missing* integration. Adding it is cheap and aligned. +- The integration example plugin's validation logic is not architecturally concerning — it uses the documented filter names and PHP registration functions. No changes needed there as a result of Pass B. diff --git a/docs/gutenberg-alignment/pass-c.md b/docs/gutenberg-alignment/pass-c.md new file mode 100644 index 0000000..eb16098 --- /dev/null +++ b/docs/gutenberg-alignment/pass-c.md @@ -0,0 +1,368 @@ +# Pass C — Leanness Review + +Review of the Validation API plugin for deletion and collapse candidates. Pass A (conventions) and Pass B (architecture) are done. Pass C asks: what can go away without changing behavior, and what can collapse with equivalent behavior? + +## Status + +- [x] Review complete +- [x] Action items folded into `pass-a.md` (Batches 1, 4, 5) +- [ ] Execution (awaits consolidated plan) + +## Scope of Pass C + +| Area examined | Verdict | +|---|---| +| `Meta\Validator` class | **DELETE** (C-1) | +| `Contracts/CheckProvider` interface | **DELETE** (C-2) | +| Dead `Block\Registry` methods (`unregister_check`, `set_check_enabled`) | **DELETE** (C-3) | +| Undocumented lifecycle hooks tied to deleted methods | **DELETE** (C-4) | +| `Editor\Registry::register_editor_check_for_post_types()` | **DELETE** (C-5) | +| `EditorDetection::get_current_screen()` fallback | **DELETE** (C-6) | +| `Core/I18n.php` | **DELETE + inline** (C-7, already in Pass A Batch 3) | +| Registry duplication | **Extract abstract base class** (C-8, new Batch 5) | +| `useValidationIssues()` hook extraction | **ADD** (C-9, absorbed into Batch 1) | +| `useMetaField` dual `useSelect` consolidation | **CONSOLIDATE** (C-10, absorbed into Batch 1) | +| `filterIssuesByType` helper | KEEP (C-11, too marginal to inline) | +| `getValidationConfig` wrapper layer | KEEP (C-12, earns its file) | +| `Logger` trait | KEEP (28 active call sites) | +| `useDebouncedValidation` hook | KEEP (custom "immediate + debounce" not in `@wordpress/compose`) | +| Plugin init chain | KEEP (no no-op steps) | +| Styles | KEEP (no orphaned stylesheets) | + +## Reference sources + +- Full code audit of `/Users/troychaplin/Develop/wp-projects/validation-api/wp-content/plugins/validation-api/includes/` and `src/` +- Workspace-wide grep for consumers of each candidate (all three plugins: core, settings addon, integration example) +- Documentation audit of `docs/guide/`, `docs/technical/` for public-API contracts + +--- + +## Findings + +### C-1: Delete `Meta\Validator` class (109 LOC) + +**File:** `includes/Meta/Validator.php` + +**What it provides:** One static method `Validator::required()` returning a closure usable as `register_post_meta( ..., 'validate_callback' => Validator::required(...) )`. + +**Consumer audit:** + +``` +grep -r "Meta\\Validator\|Meta::Validator" wp-content/plugins/ → 0 external hits +grep -r "Validator::required" wp-content/plugins/ → 0 hits outside Validator.php +``` + +- Not used by core plugin +- Not used by integration example +- Not used by settings addon +- Not referenced in `docs/guide/` + +**Why to delete:** WordPress's native `register_post_meta( ..., 'validate_callback' )` pattern does the same thing with no helper needed. The 109 LOC solves a non-existent problem. + +**Action:** Delete the file. Remove any stale doc references. + +**Risk:** Very low. No consumers. + +--- + +### C-2: Delete `Contracts/CheckProvider` interface (47 LOC) + +**File:** `includes/Contracts/CheckProvider.php` + +``` +grep -r "implements CheckProvider" → 0 hits +grep -r "Contracts\\CheckProvider" → only in docs/guide/check-providers.md +``` + +**Why to delete:** Added speculatively for class-based registration. No one adopted it. Can be reintroduced in v1.1 if demand appears. + +**Action:** Delete the file; delete (or update) `docs/guide/check-providers.md`; remove the `includes/Contracts/` directory if it becomes empty. + +**Risk:** Very low. + +--- + +### C-3 & C-4: Delete dead `Block\Registry` methods and their orphaned hooks + +**Methods to delete:** +- `Block\Registry::unregister_check()` — 17 LOC, never called +- `Block\Registry::set_check_enabled()` — 12 LOC, never called + +**Actions fired only by these methods:** +- `wp_validation_check_unregistered` — no doc entry, no consumer +- `wp_validation_check_toggled` — no doc entry, no consumer + +**Consumer audit:** + +``` +grep -r "unregister_check\|set_check_enabled" → 0 hits outside Block/Registry.php +grep -r "wp_validation_check_unregistered\|wp_validation_check_toggled" → only in Block/Registry.php definition +``` + +**Lifecycle actions that stay** (documented in `docs/technical/hooks.md`, part of declared public API): +- `wp_validation_initialized` +- `wp_validation_ready` +- `wp_validation_editor_checks_ready` +- `wp_validation_check_registered` + +**Filter hooks that stay:** +- `wp_validation_check_args` (documented) +- `wp_validation_should_register_check` (documented) +- `wp_validation_check_level` (actively consumed by settings addon) + +**Action:** Delete both methods + both actions. Keep all other lifecycle hooks. + +**Risk:** Very low. + +--- + +### C-5: Delete `Editor\Registry::register_editor_check_for_post_types()` (9 LOC) + +**Consumer audit:** + +``` +grep -r "register_editor_check_for_post_types" → 0 hits outside Editor/Registry.php +``` + +Bulk-convenience helper that loops `register_editor_check()` over an array of post types. Never called. Users who need the pattern can write a `foreach` in three lines. + +**Action:** Delete the method. + +**Risk:** Very low. + +--- + +### C-6: Delete `EditorDetection::get_current_screen()` fallback (8 LOC) + +**File:** `includes/Core/Traits/EditorDetection.php` + +Inside `get_editor_context()`, after the `$pagenow === 'post.php' || 'post-new.php'` branch and its post-type resolution, there's a fallback: + +```php +if ( function_exists( 'get_current_screen' ) ) { + $current_screen = \get_current_screen(); + if ( $current_screen && isset( $current_screen->post_type ) ) { + if ( ! in_array( $current_screen->post_type, $this->get_site_editor_post_types(), true ) ) { + return 'post-editor'; + } + } +} +``` + +**Why it's unreachable:** `$GLOBALS['pagenow']` is set by WP for every admin page. If `$pagenow` is neither `post.php` nor `post-new.php`, we're not in the post editor. The `get_current_screen()` fallback adds nothing. + +**Pass B context:** Pass B confirmed the trait's overall role is correct (no Gutenberg helper replaces it). Pass C trims the internals. + +**Action:** Delete the fallback block. + +**Risk:** Very low. Even in the edge case where `$pagenow` isn't set (e.g., custom CLI contexts), the function returning `'none'` is the safe default. + +--- + +### C-7: `Core/I18n.php` — delete and inline (58 LOC) + +**Already planned:** Pass A Batch 3 calls for this. Pass C confirms it's correct. + +**File summary:** 58 LOC class with a constructor storing two values and one method calling `wp_set_script_translations()`. + +**Action:** Delete class. Inline the one `wp_set_script_translations()` call in `Core/Assets.php` at the enqueue site. + +**Risk:** None. + +--- + +### C-8: Extract `AbstractValidationRegistry` base class (~115 LOC saved) + +**Pass A context:** Pass A recommended "collapse three registries into one parameterized class." Pass C refines this based on concrete audit of the three files. + +**Current LOC:** +- `Block\Registry` — 300 LOC +- `Meta\Registry` — 240 LOC +- `Editor\Registry` — 244 LOC +- **Total: 784 LOC** + +**Duplicated code across all three (identical or near-identical):** + +1. Defaults + `wp_parse_args` (~10 LOC × 3): + ```php + $defaults = array( + 'error_msg' => '', + 'warning_msg' => '', + 'level' => 'error', + 'priority' => 10, + 'enabled' => true, + 'description' => '', + 'configurable' => true, + ); + $check_args = \wp_parse_args( $check_args, $defaults ); + ``` + +2. Required-field validation (~8 LOC × 3) + +3. Level validation (~8 LOC × 3): + ```php + $valid_levels = array( 'error', 'warning', 'none' ); + if ( ! in_array( $check_args['level'], $valid_levels, true ) ) { ... } + ``` + +4. `warning_msg` fallback to `error_msg` (~3 LOC × 3) + +5. Namespace stamping (`_namespace` internal key) (~5 LOC × 3) + +6. Priority sort via `uasort` (~3 LOC × 3) + +**Why full collapse is wrong:** + +- `Meta\Registry` has 3-level storage (`[post_type][meta_key][check_name]`) vs 2-level for Block/Editor +- Scope-specific methods (`get_registered_block_types` on Block only) +- Different hook-name suffixes per scope + +**Right approach — abstract base class:** + +``` +ValidationAPI\AbstractRegistry (new, ~100 LOC) +├── normalize_args( $args ): array +├── validate_required_args( $args, $required ): bool +├── stamp_namespace( $args ): array +├── sort_by_priority( &$checks ): void +└── uses Logger trait + +Block\Registry extends AbstractRegistry (~200 LOC) +Meta\Registry extends AbstractRegistry (~180 LOC) +Editor\Registry extends AbstractRegistry (~190 LOC) +``` + +**After extraction:** +- Total LOC: ~670 (from 784) +- Saved: ~115 LOC +- Public API unchanged +- Singleton pattern preserved + +**Action:** New Batch 5. Checklist in `pass-a.md`. + +**Risk:** Medium. Requires careful behavior parity verification across all three scopes. + +--- + +### C-9: Extract `useValidationIssues()` hook (saves ~10 LOC) + +**Duplicated block** in two files (14 LOC total): + +```js +// ValidationAPI.js and ValidationSidebar.js, identical: +const { invalidBlocks, invalidMeta, invalidEditorChecks } = useSelect( ( select ) => { + const store = select( STORE_NAME ); + return { + invalidBlocks: store.getInvalidBlocks(), + invalidMeta: store.getInvalidMeta(), + invalidEditorChecks: store.getInvalidEditorChecks(), + }; +}, [] ); +``` + +**Consolidation:** + +```js +// src/utils/use-validation-issues.js +export function useValidationIssues() { + return useSelect( ( select ) => { + const store = select( validationStore ); + return { + invalidBlocks: store.getInvalidBlocks(), + invalidMeta: store.getInvalidMeta(), + invalidEditorChecks: store.getInvalidEditorChecks(), + }; + }, [] ); +} +``` + +Both call sites become: + +```js +const { invalidBlocks, invalidMeta, invalidEditorChecks } = useValidationIssues(); +``` + +**Action:** Fold into Batch 1 (both consumers are already moving). + +**Risk:** Low. + +--- + +### C-10: Consolidate dual `useSelect` in `useMetaField` (saves ~12 LOC) + +**Current:** `useMetaField` calls `useMetaValidation()` (which has its own `useSelect`) AND does a separate `useSelect` for the meta value. Same component fetches from the editor store twice. + +**Fix:** Single `useSelect` reads both meta value and validation-derived state in one pass. + +**Action:** Fold into Batch 1 (file is already moving to `src/utils/use-meta-field.js`). + +**Risk:** Low. + +--- + +### C-11: `filterIssuesByType` — skip + +**Helper:** `issues => issues.filter( i => i.type === type )` — 3 LOC, called 4 times. + +**Inlining savings:** 3 LOC total. Not worth disrupting the helper's existence. + +**Action:** None. + +--- + +### C-12: `getValidationConfig` wrapper layer — skip + +**Analysis:** Five named exports (`getValidationRules`, `getMetaValidationRules`, `getEditorValidationRules`, `getEditorContext`, `getRegisteredBlockTypes`) each doing one line of editor-settings access. Collapsing saves ~30 LOC but forces call sites to nested property access. + +**Verdict:** Named exports are self-documenting; the 30 LOC is earning its keep. + +**Action:** None. + +--- + +## Items KEPT after Pass C — do not change + +| Item | Reason | +|---|---| +| `Core/Traits/Logger` | 28 active call sites; debug consistency valuable | +| `getValidationConfig.js` | Named exports earn their file | +| `useDebouncedValidation` | Custom "immediate + debounce" behavior not in `@wordpress/compose` | +| HOC files (`withErrorHandling`, `withBlockValidationClasses`) | Already minimal | +| Store selectors/actions | All 11 exports used | +| Reducer | All action types have corresponding creators | +| Plugin initialization chain | No no-op steps | +| Styles | All stylesheets correspond to live components | +| `Meta\hooks\useMetaField` / `useMetaValidation` | Actively consumed by integration example | +| Lifecycle hooks (the 4 kept + 3 filters) | Documented public API | + +--- + +## Summary table + +| Item | Current LOC | Action | LOC saved | Risk | Batch | +|---|---|---|---|---|---| +| `Meta\Validator` class | 109 | DELETE | 109 | Very low | 4 | +| `Core/I18n.php` | 58 | DELETE + inline | 58 | Very low | 3 (Pass A) | +| `Contracts/CheckProvider` interface | 47 | DELETE | 47 | Very low | 4 | +| `Block\Registry::unregister_check()` | 17 | DELETE | 17 | Very low | 4 | +| `Block\Registry::set_check_enabled()` | 12 | DELETE | 12 | Very low | 4 | +| `Editor\Registry::register_editor_check_for_post_types()` | 9 | DELETE | 9 | Very low | 4 | +| `EditorDetection` `get_current_screen()` fallback | 8 | DELETE | 8 | Very low | 4 | +| `wp_validation_check_unregistered` action | coupled | DELETE | 0 extra | Very low | 4 | +| `wp_validation_check_toggled` action | coupled | DELETE | 0 extra | Very low | 4 | +| Registry shared logic (AbstractRegistry extraction) | ~60 duplicated × 3 | EXTRACT base class | ~115 | Medium | 5 | +| `useValidationIssues()` extraction | 14 duplicated | EXTRACT hook | ~10 | Low | 1 | +| `useMetaField` dual `useSelect` | 12 | CONSOLIDATE | ~12 | Low | 1 | + +**Totals:** +- PHP deletions (Batch 4): ~260 LOC +- PHP collapse (Batch 5): ~115 LOC +- JS absorbed into Batch 1: ~22 LOC +- I18n (Batch 3): 58 LOC +- **Grand total: ~455 LOC** (~11% of ~4,000 LOC codebase) + +## Notes + +- No dead code found in JS. No TODO/FIXME/XXX comments, no commented-out blocks, no console.log leftovers. JS codebase is already tight. +- No dead code found in PHP either. Deletions are of unused-but-well-written public API surfaces, not neglected code. +- Both the deletions and the registry extraction are reversible — if a consumer later emerges, reintroduction is straightforward. From 7ab948d4b1f81f8428a861c23cc19bc38609b454 Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Fri, 17 Apr 2026 22:42:58 -0400 Subject: [PATCH 02/11] batch 4: delete PHP dead code per Gutenberg alignment plan Remove public API surfaces that had no consumers in the workspace and no contracts beyond historical intent. ~260 LOC removed; no active consumer breaks. - Delete Meta\Validator class (server-side validation helper) - Delete Contracts\CheckProvider interface (no implementations) - Remove Block\Registry::unregister_check() + wp_validation_check_unregistered - Remove Block\Registry::set_check_enabled() + wp_validation_check_toggled - Remove Editor\Registry::register_editor_check_for_post_types() bulk helper - Remove EditorDetection get_current_screen() fallback (unreachable) - Remove orphaned wp_validation_validate_meta filter - Update technical/hooks.md, technical/api.md, technical/decisions.md, guide/README.md, guide/meta-checks.md, guide/examples.md, docs/README.md, CLAUDE.md, INTEGRATION.md, PROPOSAL.md - Delete guide/check-providers.md Lifecycle hooks kept (documented public API, consumed by settings addon or reserved): wp_validation_initialized, wp_validation_ready, wp_validation_editor_checks_ready, wp_validation_check_registered, wp_validation_editor_check_registered, wp_validation_meta_check_registered, wp_validation_check_args, wp_validation_editor_check_args, wp_validation_meta_check_args, wp_validation_should_register_check, wp_validation_should_register_editor_check, wp_validation_should_register_meta_check, wp_validation_check_level. See docs/gutenberg-alignment/pass-c.md for rationale. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 1 - docs/INTEGRATION.md | 1 - docs/PROPOSAL.md | 19 ++- docs/README.md | 1 - docs/guide/README.md | 1 - docs/guide/check-providers.md | 205 ----------------------- docs/guide/examples.md | 63 +++---- docs/guide/meta-checks.md | 54 ++---- docs/technical/api.md | 44 ----- docs/technical/decisions.md | 4 +- docs/technical/hooks.md | 46 ----- includes/Block/Registry.php | 45 ----- includes/Contracts/CheckProvider.php | 47 ------ includes/Core/Traits/EditorDetection.php | 10 -- includes/Editor/Registry.php | 18 -- includes/Meta/Validator.php | 109 ------------ 16 files changed, 46 insertions(+), 622 deletions(-) delete mode 100644 docs/guide/check-providers.md delete mode 100644 includes/Contracts/CheckProvider.php delete mode 100644 includes/Meta/Validator.php diff --git a/CLAUDE.md b/CLAUDE.md index 0c23304..2e3a7ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,6 @@ includes/ Core/I18n.php # Script translations Core/Traits/EditorDetection.php # Post editor context detection Rest/ChecksController.php # REST endpoint - Contracts/CheckProvider.php # Optional interface for class-based registration src/ script.js # Entry point diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index b4989c9..7d682cc 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -78,7 +78,6 @@ These items were adapted from plugin conventions to core conventions and are now | `ValidationToolbarButton` | Per-block validation toolbar UI | | Block/Meta/Editor registries | Declarative check registration | | `wp_validation_check_level` filter | Runtime severity override | -| `Validator::required()` helper | Bridge between client and server meta validation | | REST `wp/v2/validation-checks` | Check introspection for admin tooling | ## Packages Affected diff --git a/docs/PROPOSAL.md b/docs/PROPOSAL.md index e21f43a..9b5e7a7 100644 --- a/docs/PROPOSAL.md +++ b/docs/PROPOSAL.md @@ -156,20 +156,24 @@ addFilter( ); ``` -For server-side enforcement on save, the API provides a `Validator` helper that registers both the client-side check and a `validate_callback` for `register_post_meta()`: +For server-side enforcement on save, use WordPress's native `validate_callback` parameter on `register_post_meta()`. The Validation API handles the client-side UX; server-side enforcement is a separate, complementary concern: ```php -use ValidationAPI\Meta\Validator; - register_post_meta( 'post', 'seo_description', [ 'single' => true, 'type' => 'string', 'show_in_rest' => true, 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => Validator::required( 'post', 'seo_description', [ - 'error_msg' => __( 'SEO description is required.', 'my-plugin' ), - 'level' => 'error', - ] ), + 'validate_callback' => static function ( $value ) { + if ( empty( trim( (string) $value ) ) ) { + return new WP_Error( + 'seo_description_required', + __( 'SEO description is required.', 'my-plugin' ), + [ 'status' => 400 ] + ); + } + return true; + }, ] ); ``` @@ -324,7 +328,6 @@ The proposal is specifically for the **Validation API framework** -- the infrast - PHP action hooks for lifecycle events (`wp_validation_initialized`, `wp_validation_ready`, `wp_validation_editor_checks_ready`) - PHP filter hooks for check modification (`wp_validation_check_args`, `wp_validation_should_register_check`, `wp_validation_check_level`) - REST API endpoint (`GET /wp/v2/validation-checks`) for admin tooling -- Meta validation helper (`Validator::required()`) for server-side enforcement via `register_post_meta()` **Not included (remains in plugin territory):** diff --git a/docs/README.md b/docs/README.md index ba16b03..83a3b54 100644 --- a/docs/README.md +++ b/docs/README.md @@ -9,7 +9,6 @@ For plugin authors integrating with the Validation API. - **[Meta Checks](guide/meta-checks.md)** — Validate post meta fields with server-side enforcement - **[Editor Checks](guide/editor-checks.md)** — Validate document-level concerns (heading hierarchy, content structure) - **[Severity Model](guide/severity.md)** — Error vs. warning vs. none, and how to override levels at runtime -- **[CheckProvider Pattern](guide/check-providers.md)** — Class-based registration for enterprise-scale plugins - **[Examples](guide/examples.md)** — Complete integration examples and common recipes ## Technical Reference diff --git a/docs/guide/README.md b/docs/guide/README.md index 4244563..b0a65e4 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -128,5 +128,4 @@ You only write the registration and the validation logic. Everything else is han - [Block Checks](block-checks.md) — Most common starting point - [Severity Model](severity.md) — Understand the filter system -- [CheckProvider Pattern](check-providers.md) — Scale beyond a few checks - [Examples](examples.md) — Complete plugin integration examples diff --git a/docs/guide/check-providers.md b/docs/guide/check-providers.md deleted file mode 100644 index 408214b..0000000 --- a/docs/guide/check-providers.md +++ /dev/null @@ -1,205 +0,0 @@ -# CheckProvider Pattern - -For plugins with more than a handful of checks, the `CheckProvider` interface lets you organize registrations across multiple classes. Each class handles one concern — image checks, heading checks, meta checks — and they're all wired together through a shared `namespace`. - -## The Interface - -```php -namespace ValidationAPI\Contracts; - -interface CheckProvider { - public function register(): void; -} -``` - -One method. No constructor requirements. No abstract base class. Your class implements `register()` and calls the global registration functions inside it. - -## Basic Usage - -```php -use ValidationAPI\Contracts\CheckProvider; - -class ImageChecks implements CheckProvider { - public function register(): void { - wp_register_block_validation_check( 'core/image', [ - 'namespace' => 'enterprise-content-rules', - 'name' => 'alt_text', - 'level' => 'error', - 'description' => 'Images must have alt text', - 'error_msg' => 'This image is missing alt text.', - 'warning_msg' => 'Consider adding alt text to this image.', - ] ); - - wp_register_block_validation_check( 'core/image', [ - 'namespace' => 'enterprise-content-rules', - 'name' => 'file_size', - 'level' => 'warning', - 'description' => 'Images should be optimized', - 'error_msg' => 'This image exceeds the recommended file size.', - 'warning_msg' => 'This image is larger than recommended.', - ] ); - } -} -``` - -## Registering Providers - -Instantiate each provider and call `register()`: - -```php -add_action( 'init', function() { - if ( ! function_exists( 'wp_register_block_validation_check' ) ) { - return; - } - - $providers = [ - new ImageChecks(), - new HeadingChecks(), - new ButtonChecks(), - new MetaChecks(), - new EditorChecks(), - ]; - - foreach ( $providers as $provider ) { - $provider->register(); - } -} ); -``` - -All checks use the same `namespace` value (e.g., `'enterprise-content-rules'`) to group them together in the REST API and companion settings. - -## Mixing Scopes - -A single provider can register checks across all three scopes: - -```php -class AccessibilityChecks implements CheckProvider { - public function register(): void { - // Block check - wp_register_block_validation_check( 'core/image', [ - 'namespace' => 'accessibility', - 'name' => 'alt_text', - 'level' => 'error', - 'error_msg' => 'Images must have alt text.', - ] ); - - // Editor check - wp_register_editor_validation_check( 'post', [ - 'namespace' => 'accessibility', - 'name' => 'heading_hierarchy', - 'level' => 'warning', - 'error_msg' => 'Heading hierarchy is broken.', - ] ); - - // Meta check - wp_register_meta_validation_check( 'post', [ - 'namespace' => 'accessibility', - 'name' => 'required', - 'meta_key' => 'seo_description', - 'level' => 'error', - 'error_msg' => 'SEO description is required.', - ] ); - } -} -``` - -However, for larger plugins, separating by concern (one provider per block type or per validation domain) is more maintainable. - -## Organizing a Large Plugin - -A typical enterprise structure: - -``` -my-validation-plugin/ -├── my-validation-plugin.php ← Bootstrap: guard + register providers -├── src/ -│ ├── Checks/ -│ │ ├── ImageChecks.php ← CheckProvider for core/image -│ │ ├── ButtonChecks.php ← CheckProvider for core/button -│ │ ├── HeadingChecks.php ← CheckProvider for editor-level heading rules -│ │ └── SeoMetaChecks.php ← CheckProvider for SEO meta fields -│ └── ... -├── build/ -│ └── validation.js ← JS validation logic for all checks -└── ... -``` - -**Bootstrap file:** - -```php -register(); - } -} ); - -add_action( 'enqueue_block_editor_assets', function() { - wp_enqueue_script( - 'my-validation-rules', - plugins_url( 'build/validation.js', __FILE__ ), - [ 'wp-hooks' ], - '1.0.0', - true - ); -} ); -``` - -## CheckProvider vs. Inline Registration - -Both approaches are supported. Use whichever fits your plugin: - -**Inline** — best for small integrations with a few checks: - -```php -wp_register_block_validation_check( 'core/image', [ - 'namespace' => 'simple-rules', - 'name' => 'alt_text', - 'error_msg' => 'Alt text required.', -] ); - -wp_register_block_validation_check( 'core/button', [ - 'namespace' => 'simple-rules', - 'name' => 'has_link', - 'error_msg' => 'Button needs a link.', -] ); -``` - -**CheckProvider** — best for larger integrations where you want separation of concerns, testability, and organized file structure: - -```php -$providers = [ - new ImageChecks(), - new ButtonChecks(), - new HeadingChecks(), -]; - -foreach ( $providers as $provider ) { - $provider->register(); -} -``` - -## Error Handling - -The framework validates providers at registration time: - -- If a class doesn't exist, `_doing_it_wrong()` is called and that provider is skipped -- If a class doesn't implement `CheckProvider`, `_doing_it_wrong()` is called and that provider is skipped -- Other providers in the array continue to register normally - -This means a typo in one class name won't break your entire plugin's validation — the other providers still register. diff --git a/docs/guide/examples.md b/docs/guide/examples.md index 2b550a5..1082c1e 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -208,27 +208,38 @@ addFilter( ## Recipe: Server-Side Meta Validation -Use `Validator::required()` for simultaneous client-side and server-side enforcement: +The Validation API covers client-side validation. For server-side enforcement (REST writes, non-editor save paths), use WordPress's native `validate_callback` parameter on `register_post_meta()` alongside the client-side check: ```php -use ValidationAPI\Meta\Validator; - add_action( 'init', function() { if ( ! function_exists( 'wp_register_meta_validation_check' ) ) { return; } - // Register the meta field with server-side validation + // Client-side validation (editor UX) + wp_register_meta_validation_check( 'event', [ + 'namespace' => 'my-events-plugin', + 'name' => 'event_date_required', + 'meta_key' => 'event_date', + 'level' => 'error', + 'error_msg' => 'Events must have a date.', + ] ); + + // Server-side validation (REST + save paths) register_post_meta( 'event', 'event_date', [ 'show_in_rest' => true, 'single' => true, 'type' => 'string', - 'validate_callback' => Validator::required( 'event', 'event_date', [ - 'error_msg' => 'Events must have a date.', - 'warning_msg' => 'Consider setting an event date.', - 'level' => 'error', - 'description' => 'Event date is required', - ] ), + 'validate_callback' => static function ( $value ) { + if ( empty( trim( (string) $value ) ) ) { + return new WP_Error( + 'event_date_required', + 'Events must have a date.', + [ 'status' => 400 ] + ); + } + return true; + }, ] ); } ); ``` @@ -253,38 +264,6 @@ add_filter( 'wp_validation_check_level', function( $level, $context ) { }, 10, 2 ); ``` -## Recipe: CheckProvider with Shared Configuration - -```php -use ValidationAPI\Contracts\CheckProvider; - -class ImageChecks implements CheckProvider { - private const BLOCK_TYPE = 'core/image'; - private const NAMESPACE = 'my-validation-plugin'; - - public function register(): void { - $this->register_check( 'alt_text', 'error', [ - 'description' => 'Images must have alt text', - 'error_msg' => 'This image is missing alt text.', - 'warning_msg' => 'Consider adding alt text.', - ] ); - - $this->register_check( 'dimensions', 'warning', [ - 'description' => 'Images should have explicit dimensions', - 'error_msg' => 'This image has no dimensions set.', - 'warning_msg' => 'Consider setting explicit image dimensions.', - ] ); - } - - private function register_check( string $name, string $level, array $args ): void { - wp_register_block_validation_check( self::BLOCK_TYPE, array_merge( - $args, - [ 'namespace' => self::NAMESPACE, 'name' => $name, 'level' => $level ] - ) ); - } -} -``` - ## Recipe: Preventing Check Registration Use the `wp_validation_should_register_check` filter to conditionally prevent checks from registering: diff --git a/docs/guide/meta-checks.md b/docs/guide/meta-checks.md index 154b2ef..6846ac8 100644 --- a/docs/guide/meta-checks.md +++ b/docs/guide/meta-checks.md @@ -87,57 +87,29 @@ addFilter( Return `true` if the field passes validation, `false` if it fails. -## Server-Side Validation with Meta\Validator +## Server-Side Validation -The `ValidationAPI\Meta\Validator` helper class bridges meta checks with WordPress's `register_post_meta()`. It registers the check with the Validation API and returns a `validate_callback` for server-side enforcement: +For server-side enforcement, use WordPress's native `validate_callback` parameter on `register_post_meta()`. The Validation API handles client-side validation; server-side is up to you and happens at `register_post_meta()` time: ```php -use ValidationAPI\Meta\Validator; - register_post_meta( 'post', 'seo_description', [ 'show_in_rest' => true, 'single' => true, 'type' => 'string', - 'validate_callback' => Validator::required( 'post', 'seo_description', [ - 'error_msg' => 'SEO description is required.', - 'warning_msg' => 'Consider adding an SEO description.', - 'level' => 'error', - ] ), + 'validate_callback' => static function ( $value ) { + if ( empty( trim( (string) $value ) ) ) { + return new WP_Error( + 'seo_description_required', + 'SEO description is required.', + [ 'status' => 400 ] + ); + } + return true; + }, ] ); ``` -### What Validator::required() Does - -1. Registers the check with the Meta Registry (so the client-side validation is aware of it) -2. Returns a `validate_callback` function for `register_post_meta()` -3. The callback runs on save and returns a `WP_Error` if validation fails at `error` level -4. At `warning` level, the callback allows the save (client-side shows the warning) -5. At `none` level, the callback is a no-op - -This means a single call handles both client-side and server-side validation for required fields. - -### Validator Parameters - -```php -Validator::required( string $post_type, string $meta_key, array $args = [] ): callable -``` - -| Arg Key | Type | Default | Description | -|---|---|---|---| -| `error_msg` | `string` | `'This field is required.'` | Error message | -| `warning_msg` | `string` | `'This field is recommended.'` | Warning message | -| `level` | `string` | `'error'` | Severity level | -| `check_name` | `string` | `'required'` | Check identifier | -| `description` | `string` | `''` | Human-readable description | - -### When to Use Validator vs. Manual Registration - -Use `Validator::required()` when you're already calling `register_post_meta()` and want both client and server validation in one step. - -Use `wp_register_meta_validation_check()` directly when: -- You don't need server-side enforcement -- You need a custom validation rule beyond "required" -- You're validating meta that's registered elsewhere +Client-side validation (via `wp_register_meta_validation_check()` + the `editor.validateMeta` JS filter) covers the editor experience. Server-side `validate_callback` covers REST writes and other non-editor save paths. They are independent; register both if you need both. ## Meta Validation Data Flow diff --git a/docs/technical/api.md b/docs/technical/api.md index 76e7a24..6916b1e 100644 --- a/docs/technical/api.md +++ b/docs/technical/api.md @@ -72,26 +72,6 @@ All registration functions accept an `$args` array with the following keys: |---|---|---|---| | `meta_key` | `string` | Yes | The post meta key to validate | -## Contracts - -### CheckProvider - -Interface for class-based check registration. - -```php -namespace ValidationAPI\Contracts; - -interface CheckProvider { - /** - * Register validation checks. - * Called within a scoped plugin context. - */ - public function register(): void; -} -``` - -Implementations call global registration functions (`wp_register_block_validation_check()`, etc.) inside `register()`. All checks use the same `namespace` value to group them together. - ## Registry Classes These are the internal registry singletons. Most integrations should use the global functions above. Registry methods are documented here for contributors and advanced use cases. @@ -102,8 +82,6 @@ Singleton. Access via `BlockRegistry::get_instance()`. ```php register_check( string $block_type, string $check_name, array $check_args ): bool -unregister_check( string $block_type, string $check_name ): bool -set_check_enabled( string $block_type, string $check_name, bool $enabled ): bool get_checks( string $block_type ): array get_all_checks(): array is_check_registered( string $block_type, string $check_name ): bool @@ -118,7 +96,6 @@ Singleton. Access via `EditorRegistry::get_instance()`. ```php register_editor_check( string $post_type, string $check_name, array $check_args ): bool -register_editor_check_for_post_types( array $post_types, string $check_name, array $check_args ): array get_editor_checks( string $post_type ): array get_all_editor_checks(): array get_editor_check_config( string $post_type, string $check_name ): ?array @@ -137,27 +114,6 @@ get_meta_check_config( string $post_type, string $meta_key, string $check_name ) get_effective_meta_check_level( string $post_type, string $meta_key, string $check_name ): string ``` -### ValidationAPI\Meta\Validator - -Static helper for server-side meta validation integrated with `register_post_meta()`. - -```php -Validator::required( string $post_type, string $meta_key, array $args = [] ): callable -``` - -| Arg Key | Type | Default | Description | -|---|---|---|---| -| `error_msg` | `string` | `'This field is required.'` | Error message | -| `warning_msg` | `string` | `'This field is recommended.'` | Warning message | -| `level` | `string` | `'error'` | Severity level | -| `check_name` | `string` | `'required'` | Check identifier | -| `description` | `string` | `''` | Human-readable description | - -Returns a `callable` for use as the `validate_callback` parameter in `register_post_meta()`. The callback: -- Returns `true` if the check passes or is disabled (`none`) -- Returns `WP_Error` if the check fails at `error` level -- Returns `true` for `warning` level failures (allows save; client-side shows the warning) - ## REST API ### GET /wp/v2/validation-checks diff --git a/docs/technical/decisions.md b/docs/technical/decisions.md index 596773e..dfe383e 100644 --- a/docs/technical/decisions.md +++ b/docs/technical/decisions.md @@ -20,8 +20,7 @@ Namespace structure: - `ValidationAPI\Core\*` — Plugin bootstrap, assets, traits - `ValidationAPI\Block\*` — Block registry - `ValidationAPI\Editor\*` — Editor registry -- `ValidationAPI\Meta\*` — Meta registry and validator -- `ValidationAPI\Contracts\*` — Interfaces (CheckProvider) +- `ValidationAPI\Meta\*` — Meta registry - `ValidationAPI\Rest\*` — REST API controllers ## #3 — Data Export Mechanism @@ -106,7 +105,6 @@ Benefits: - Plugin identity is declared per-check via `namespace`, not inferred - No need to know about registry instances - The `function_exists` guard is clean and obvious -- CheckProvider classes can call the same global functions ## #9 — Category Parameter diff --git a/docs/technical/hooks.md b/docs/technical/hooks.md index 2eb18dd..4c11ae1 100644 --- a/docs/technical/hooks.md +++ b/docs/technical/hooks.md @@ -56,33 +56,6 @@ do_action( 'wp_validation_check_registered', string $block_type, string $check_n | `$check_name` | `string` | Check identifier | | `$check_args` | `array` | The final check configuration | -### wp_validation_check_unregistered - -Fires when a block check is unregistered. - -```php -do_action( 'wp_validation_check_unregistered', string $block_type, string $check_name ); -``` - -| Parameter | Type | Description | -|---|---|---| -| `$block_type` | `string` | Block type name | -| `$check_name` | `string` | Check identifier | - -### wp_validation_check_toggled - -Fires when a block check is enabled or disabled. - -```php -do_action( 'wp_validation_check_toggled', string $block_type, string $check_name, bool $enabled ); -``` - -| Parameter | Type | Description | -|---|---|---| -| `$block_type` | `string` | Block type name | -| `$check_name` | `string` | Check identifier | -| `$enabled` | `bool` | Whether the check is now enabled | - ### wp_validation_editor_check_registered Fires when an editor check is successfully registered. @@ -245,25 +218,6 @@ $level = apply_filters( 'wp_validation_check_level', string $level, array $conte [ 'scope' => 'editor', 'post_type' => 'post', 'check_name' => 'heading_hierarchy' ] ``` -### wp_validation_validate_meta - -Server-side meta validation filter. Fires during `register_post_meta()` validate_callback when using `Validator::required()`. - -```php -$is_valid = apply_filters( 'wp_validation_validate_meta', bool $is_valid, mixed $value, string $post_type, string $meta_key, string $check_name, array $config ); -``` - -| Parameter | Type | Description | -|---|---|---| -| `$is_valid` | `bool` | Current validation state (default: `true`) | -| `$value` | `mixed` | The meta value being validated | -| `$post_type` | `string` | Post type | -| `$meta_key` | `string` | The meta key | -| `$check_name` | `string` | Check identifier | -| `$config` | `array` | The check configuration | - -**Return:** `bool` — `true` if valid, `false` if invalid. - ## JavaScript Filters All JS filters use `@wordpress/hooks` (`wp.hooks`). Register with `addFilter()`, imported from `@wordpress/hooks`. diff --git a/includes/Block/Registry.php b/includes/Block/Registry.php index e8a84bf..6000fb5 100644 --- a/includes/Block/Registry.php +++ b/includes/Block/Registry.php @@ -152,51 +152,6 @@ public function register_check( string $block_type, string $check_name, array $c } } - /** - * Unregister a validation check - * - * @param string $block_type Block type. - * @param string $check_name Check name. - * @return bool True on success, false if check not found. - */ - public function unregister_check( string $block_type, string $check_name ): bool { - if ( ! isset( $this->checks[ $block_type ][ $check_name ] ) ) { - return false; - } - - unset( $this->checks[ $block_type ][ $check_name ] ); - - if ( empty( $this->checks[ $block_type ] ) ) { - unset( $this->checks[ $block_type ] ); - } - - // Action hook for developers to know when a validation check is unregistered. - \do_action( 'wp_validation_check_unregistered', $block_type, $check_name ); - - return true; - } - - /** - * Enable or disable a specific validation check - * - * @param string $block_type Block type. - * @param string $check_name Check name. - * @param bool $enabled Whether to enable or disable the check. - * @return bool True on success, false if check not found. - */ - public function set_check_enabled( string $block_type, string $check_name, bool $enabled ): bool { - if ( ! isset( $this->checks[ $block_type ][ $check_name ] ) ) { - return false; - } - - $this->checks[ $block_type ][ $check_name ]['enabled'] = (bool) $enabled; - - // Action hook for developers to know when a validation check is enabled/disabled. - \do_action( 'wp_validation_check_toggled', $block_type, $check_name, $enabled ); - - return true; - } - /** * Get checks for a specific block type * diff --git a/includes/Contracts/CheckProvider.php b/includes/Contracts/CheckProvider.php deleted file mode 100644 index d67a99a..0000000 --- a/includes/Contracts/CheckProvider.php +++ /dev/null @@ -1,47 +0,0 @@ - 'alt_text', - * 'level' => 'error', - * 'error_msg' => 'This image is missing alt text.', - * ] ); - * } - * } - */ -interface CheckProvider { - - /** - * Register validation checks. - * - * Called within a scoped plugin context when used with - * validation_api_register_plugin(). All checks registered here - * are automatically attributed to the parent plugin. - * - * @return void - */ - public function register(): void; -} diff --git a/includes/Core/Traits/EditorDetection.php b/includes/Core/Traits/EditorDetection.php index e509916..e815d51 100644 --- a/includes/Core/Traits/EditorDetection.php +++ b/includes/Core/Traits/EditorDetection.php @@ -75,16 +75,6 @@ private function get_editor_context(): string { // phpcs:enable WordPress.Security.NonceVerification.Recommended } - // Fallback - check screen post type if available. - if ( function_exists( 'get_current_screen' ) ) { - $current_screen = \get_current_screen(); - if ( $current_screen && isset( $current_screen->post_type ) ) { - if ( ! in_array( $current_screen->post_type, $this->get_site_editor_post_types(), true ) ) { - return 'post-editor'; - } - } - } - return 'none'; } diff --git a/includes/Editor/Registry.php b/includes/Editor/Registry.php index c5bd8f5..08faf69 100644 --- a/includes/Editor/Registry.php +++ b/includes/Editor/Registry.php @@ -154,24 +154,6 @@ public function register_editor_check( string $post_type, string $check_name, ar } } - /** - * Register an editor check for multiple post types - * - * @param array $post_types Array of post types. - * @param string $check_name Unique check name. - * @param array $check_args Check configuration. - * @return array Array of results keyed by post type. - */ - public function register_editor_check_for_post_types( array $post_types, string $check_name, array $check_args ): array { - $results = array(); - - foreach ( $post_types as $post_type ) { - $results[ $post_type ] = $this->register_editor_check( $post_type, $check_name, $check_args ); - } - - return $results; - } - /** * Get editor checks for a specific post type * diff --git a/includes/Meta/Validator.php b/includes/Meta/Validator.php deleted file mode 100644 index 8bb0510..0000000 --- a/includes/Meta/Validator.php +++ /dev/null @@ -1,109 +0,0 @@ - Validator::required( 'band', 'band_origin', [ - * 'error_msg' => 'Field is required', - * 'level' => 'error', - * ] ), - * ] ); - * - * @param string $post_type Post type (e.g., 'band', 'post'). - * @param string $meta_key Meta key being validated. - * @param array $args Configuration arguments. - * @return callable The validation callback. - */ - public static function required( string $post_type, string $meta_key, array $args = array() ): callable { - $defaults = array( - 'error_msg' => 'This field is required.', - 'warning_msg' => 'This field is recommended.', - 'level' => 'error', - 'check_name' => 'required', - 'description' => '', - ); - - $config = \wp_parse_args( $args, $defaults ); - - // Register the check immediately so client-side validation is aware on initial load. - $registry = Registry::get_instance(); - $registry->register_meta_check( - $post_type, - $meta_key, - $config['check_name'], - $config - ); - - // Return the validation callback. - return function ( $value ) use ( $post_type, $meta_key, $config ) { - $registry = Registry::get_instance(); - $level = $registry->get_effective_meta_check_level( - $post_type, - $meta_key, - $config['check_name'] - ); - - // Check disabled — allow save. - if ( 'none' === $level ) { - return true; - } - - // Run validation through filter system. - $is_valid = \apply_filters( - 'wp_validation_validate_meta', - true, - $value, - $post_type, - $meta_key, - $config['check_name'], - $config - ); - - // Default validation logic for 'required' check. - if ( $is_valid && 'required' === $config['check_name'] ) { - $is_valid = ! empty( $value ) && trim( (string) $value ) !== ''; - } - - // Return error only if validation fails and level is 'error'. - if ( ! $is_valid && 'error' === $level ) { - return new \WP_Error( - 'wp_validation_failed', - $config['error_msg'], - array( 'status' => 400 ) - ); - } - - // For warnings, allow save (client-side will show warning). - return true; - }; - } -} From 3d353520f8d4d27efceac81c24d785415ce30050 Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Fri, 17 Apr 2026 22:58:45 -0400 Subject: [PATCH 03/11] batch 3: inline wp_set_script_translations, delete Core/I18n class The 58-LOC I18n class was a wrapper around a single wp_set_script_translations() call. Per Gutenberg-style functional preference, collapse into Assets.php at the enqueue site. - Delete includes/Core/I18n.php - Assets::__construct() now accepts (string $plugin_file, string $text_domain) - Assets::enqueue_block_assets() calls wp_set_script_translations() inline - Plugin::init() drops init_translations() step; passes text_domain directly to the Assets constructor - Update CLAUDE.md project structure diagram No public API changes. See docs/gutenberg-alignment/pass-a.md Batch 3 / pass-c.md C-7 for rationale. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 3 +-- includes/Core/Assets.php | 20 ++++++++------ includes/Core/I18n.php | 58 ---------------------------------------- includes/Core/Plugin.php | 20 +------------- 4 files changed, 14 insertions(+), 87 deletions(-) delete mode 100644 includes/Core/I18n.php diff --git a/CLAUDE.md b/CLAUDE.md index 2e3a7ca..b321309 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,8 +59,7 @@ includes/ Meta/Registry.php # Meta check registration Meta/Validator.php # Server-side meta validation helper Core/Plugin.php # Plugin initialization - Core/Assets.php # Script enqueuing + editor settings injection - Core/I18n.php # Script translations + Core/Assets.php # Script enqueuing + editor settings injection + script translations Core/Traits/EditorDetection.php # Post editor context detection Rest/ChecksController.php # REST endpoint diff --git a/includes/Core/Assets.php b/includes/Core/Assets.php index 124866d..0865119 100644 --- a/includes/Core/Assets.php +++ b/includes/Core/Assets.php @@ -59,21 +59,21 @@ class Assets { private $plugin_file; /** - * The translations object. + * The plugin text domain. * - * @var I18n + * @var string */ - private $translations; + private $text_domain; /** * Constructs a new instance of the Assets class. * * @param string $plugin_file The path to the plugin file. - * @param I18n $translations The translations object. + * @param string $text_domain The plugin text domain. */ - public function __construct( string $plugin_file, I18n $translations ) { - $this->plugin_file = $plugin_file; - $this->translations = $translations; + public function __construct( string $plugin_file, string $text_domain ) { + $this->plugin_file = $plugin_file; + $this->text_domain = $text_domain; // Inject validation config into editor settings instead of using wp_localize_script. add_filter( 'block_editor_settings_all', array( $this, 'inject_editor_settings' ), 10, 2 ); @@ -92,7 +92,11 @@ public function enqueue_block_assets() { return; } - $this->translations->setup_script_translations( self::VALIDATION_SCRIPT_HANDLE ); + \wp_set_script_translations( + self::VALIDATION_SCRIPT_HANDLE, + $this->text_domain, + \plugin_dir_path( $this->plugin_file ) . 'languages' + ); $this->enqueue_block_scripts(); $this->enqueue_block_styles(); diff --git a/includes/Core/I18n.php b/includes/Core/I18n.php deleted file mode 100644 index e69312f..0000000 --- a/includes/Core/I18n.php +++ /dev/null @@ -1,58 +0,0 @@ -plugin_file = $plugin_file; - $this->text_domain = $text_domain; - } - - /** - * Sets up translations for a script. - * - * @param string $script_handle The handle of the script to set up translations for. - * @return void - */ - public function setup_script_translations( string $script_handle ): void { - \wp_set_script_translations( - $script_handle, - $this->text_domain, - \plugin_dir_path( $this->plugin_file ) . 'languages' - ); - } -} diff --git a/includes/Core/Plugin.php b/includes/Core/Plugin.php index 5a87e31..4bc959d 100644 --- a/includes/Core/Plugin.php +++ b/includes/Core/Plugin.php @@ -73,7 +73,6 @@ public function __construct( string $plugin_file, string $text_domain ) { public function init(): void { try { // Initialize services in the correct order. - $this->init_translations(); $this->init_scripts_styles(); $this->init_block_checks_registry(); $this->init_editor_checks_registry(); @@ -99,22 +98,6 @@ public function init(): void { } } - /** - * Initialize translations - * - * @return void - * @throws \Exception If translations service initialization fails. - */ - private function init_translations(): void { - try { - $translations = new I18n( $this->plugin_file, $this->text_domain ); - $this->services['translations'] = $translations; - } catch ( \Exception $e ) { - $this->log_error( 'Failed to initialize translations: ' . $e->getMessage() ); - throw $e; - } - } - /** * Initialize scripts and styles * @@ -123,8 +106,7 @@ private function init_translations(): void { */ private function init_scripts_styles(): void { try { - $translations = $this->get_service( 'translations' ); - $scripts_styles = new Assets( $this->plugin_file, $translations ); + $scripts_styles = new Assets( $this->plugin_file, $this->text_domain ); $this->services['scripts_styles'] = $scripts_styles; } catch ( \Exception $e ) { $this->log_error( 'Failed to initialize scripts and styles: ' . $e->getMessage() ); From c44e389dcd907f9231b19fe8c0362b26c851463f Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Fri, 17 Apr 2026 23:57:49 -0400 Subject: [PATCH 04/11] batch 5: extract AbstractRegistry from Block/Meta/Editor registries Pull duplicated registration logic (defaults merge, required-field check, level validation, warning_msg fallback, namespace stamping, priority sort, wp_validation_check_level filter application) into a shared abstract base class. Concrete subclasses keep their own singleton, storage shape, scope-specific register method, and scope-specific hook names. - Add includes/AbstractRegistry.php with shared helpers: normalize_args(), stamp_namespace(), sort_by_priority(), apply_level_filter(), plus DEFAULTS + VALID_LEVELS constants - Block, Meta, Editor registries now extend AbstractRegistry - Logger trait methods changed from private to protected so subclasses and the abstract can share inherited access Behavior change (improvement): priority validation now applies to all three scopes. Previously only Block validated priority; Meta and Editor silently accepted garbage values. Non-numeric priorities now coerce to 10 uniformly. Public API unchanged: global function signatures, filter names, action hook names, REST response shape all identical. Constants declared without visibility modifiers for PHP 7.0 compatibility (visibility on class constants requires PHP 7.1+; plugin header declares 7.0 minimum). See docs/gutenberg-alignment/pass-c.md C-8 for rationale. Co-Authored-By: Claude Opus 4.7 (1M context) --- includes/AbstractRegistry.php | 146 ++++++++++++++++++++++++++++++++ includes/Block/Registry.php | 79 +++-------------- includes/Core/Traits/Logger.php | 4 +- includes/Editor/Registry.php | 71 +++------------- includes/Meta/Registry.php | 71 +++------------- 5 files changed, 185 insertions(+), 186 deletions(-) create mode 100644 includes/AbstractRegistry.php diff --git a/includes/AbstractRegistry.php b/includes/AbstractRegistry.php new file mode 100644 index 0000000..95703f3 --- /dev/null +++ b/includes/AbstractRegistry.php @@ -0,0 +1,146 @@ + '', + 'warning_msg' => '', + 'level' => 'error', + 'priority' => 10, + 'enabled' => true, + 'description' => '', + 'configurable' => true, + ); + + /** + * Severity levels accepted by the framework. + * + * @var array + */ + const VALID_LEVELS = array( 'error', 'warning', 'none' ); + + /** + * Normalize and validate check arguments. + * + * Applies defaults, enforces required fields (error_msg), falls back + * warning_msg to error_msg, validates level, and coerces non-numeric + * priority to the default. + * + * @param array $check_args Raw arguments passed by the caller. + * @param string $context_label Human-readable identifier for log messages (e.g. "core/image/alt_text"). + * @return array|false Normalized args on success, false if required fields are missing. + */ + protected function normalize_args( array $check_args, string $context_label ) { + $check_args = \wp_parse_args( $check_args, self::DEFAULTS ); + + // error_msg is required. + if ( empty( $check_args['error_msg'] ) ) { + $this->log_error( "error_msg is required for {$context_label}" ); + return false; + } + + // Fall back warning_msg to error_msg. + if ( empty( $check_args['warning_msg'] ) ) { + $check_args['warning_msg'] = $check_args['error_msg']; + } + + // Validate level. + if ( ! in_array( $check_args['level'], self::VALID_LEVELS, true ) ) { + $this->log_error( "Invalid level '{$check_args['level']}' for {$context_label}. Using 'error'." ); + $check_args['level'] = 'error'; + } + + // Validate priority. + if ( ! is_numeric( $check_args['priority'] ) ) { + $this->log_error( "Invalid priority '{$check_args['priority']}' for {$context_label}. Using 10." ); + $check_args['priority'] = 10; + } + + return $check_args; + } + + /** + * Stamp the public `namespace` arg into the internal `_namespace` key. + * + * The public arg is removed so downstream consumers don't see duplicate keys. + * + * @param array $check_args The check args. + * @return array Modified check args. + */ + protected function stamp_namespace( array $check_args ): array { + if ( ! empty( $check_args['namespace'] ) ) { + $check_args['_namespace'] = $check_args['namespace']; + unset( $check_args['namespace'] ); + } + return $check_args; + } + + /** + * Sort a flat check-list array in place by priority ascending. + * + * @param array $checks Reference to the associative array of checks to sort. + * @return void + */ + protected function sort_by_priority( array &$checks ): void { + \uasort( + $checks, + static function ( $a, $b ) { + return $a['priority'] - $b['priority']; + } + ); + } + + /** + * Apply the wp_validation_check_level filter with a 'none' short-circuit. + * + * @param string $registered_level The level as registered. + * @param array $context Scope-specific context passed to consumers of the filter. + * @return string The effective level ('error', 'warning', 'none'). + */ + protected function apply_level_filter( string $registered_level, array $context ): string { + if ( 'none' === $registered_level ) { + return 'none'; + } + + return \apply_filters( 'wp_validation_check_level', $registered_level, $context ); + } +} diff --git a/includes/Block/Registry.php b/includes/Block/Registry.php index 6000fb5..50bd076 100644 --- a/includes/Block/Registry.php +++ b/includes/Block/Registry.php @@ -10,16 +10,14 @@ namespace ValidationAPI\Block; -use ValidationAPI\Core\Traits\Logger; +use ValidationAPI\AbstractRegistry; /** * Validation Checks Registry Class * * Manages registration and retrieval of validation checks for block types. */ -class Registry { - - use Logger; +class Registry extends AbstractRegistry { /** * Registered checks @@ -74,40 +72,11 @@ public function register_check( string $block_type, string $check_name, array $c return false; } - $defaults = array( - 'error_msg' => '', - 'warning_msg' => '', - 'level' => 'error', - 'priority' => 10, - 'enabled' => true, - 'description' => '', - 'configurable' => true, - ); - - $check_args = \wp_parse_args( $check_args, $defaults ); - - // Validate required parameters. - if ( empty( $check_args['error_msg'] ) ) { - $this->log_error( "error_msg is required for {$block_type}/{$check_name}" ); - return false; - } - - // Fallback for warning_msg to error_msg. - if ( empty( $check_args['warning_msg'] ) ) { - $check_args['warning_msg'] = $check_args['error_msg']; - } + $context_label = "{$block_type}/{$check_name}"; + $check_args = $this->normalize_args( $check_args, $context_label ); - // Validate level parameter (optional, defaults to 'error'). - $valid_levels = array( 'error', 'warning', 'none' ); - if ( ! in_array( $check_args['level'], $valid_levels, true ) ) { - $this->log_error( "Invalid level '{$check_args['level']}' for {$block_type}/{$check_name}. Using 'error'." ); - $check_args['level'] = 'error'; - } - - // Validate priority parameter. - if ( ! is_numeric( $check_args['priority'] ) ) { - $this->log_error( "Invalid priority '{$check_args['priority']}' for {$block_type}/{$check_name}. Using 10." ); - $check_args['priority'] = 10; + if ( false === $check_args ) { + return false; } // Allow developers to filter check arguments before registration. @@ -115,13 +84,13 @@ public function register_check( string $block_type, string $check_name, array $c // Allow developers to prevent specific checks from being registered. if ( ! \apply_filters( 'wp_validation_should_register_check', true, $block_type, $check_name, $check_args ) ) { - $this->log_debug( "Check registration prevented by filter: {$block_type}/{$check_name}" ); + $this->log_debug( "Check registration prevented by filter: {$context_label}" ); return false; } // Check if check already exists. if ( isset( $this->checks[ $block_type ][ $check_name ] ) ) { - $this->log_debug( "Overriding existing check: {$block_type}/{$check_name}" ); + $this->log_debug( "Overriding existing check: {$context_label}" ); } // Initialize block type array if needed. @@ -129,17 +98,12 @@ public function register_check( string $block_type, string $check_name, array $c $this->checks[ $block_type ] = array(); } - // Stamp namespace attribution from registration args. - if ( ! empty( $check_args['namespace'] ) ) { - $check_args['_namespace'] = $check_args['namespace']; - unset( $check_args['namespace'] ); - } + $check_args = $this->stamp_namespace( $check_args ); // Store the check. $this->checks[ $block_type ][ $check_name ] = $check_args; - // Sort checks by priority. - \uasort( $this->checks[ $block_type ], array( $this, 'sort_checks_by_priority' ) ); + $this->sort_by_priority( $this->checks[ $block_type ] ); // Action hook for developers to know when a validation check is registered. \do_action( 'wp_validation_check_registered', $block_type, $check_name, $check_args ); @@ -206,17 +170,6 @@ public function get_registered_block_types(): array { return \array_keys( $this->checks ); } - /** - * Sort checks by priority - * - * @param array $a First check. - * @param array $b Second check. - * @return int Comparison result. - */ - private function sort_checks_by_priority( $a, $b ) { - return $a['priority'] - $b['priority']; - } - /** * Get the effective check level for a specific check * @@ -235,16 +188,10 @@ public function get_effective_check_level( string $block_type, string $check_nam return 'none'; } - $check_type = $checks[ $check_name ]['level'] ?? 'error'; - - // 'none' short-circuits — filter does not fire. - if ( 'none' === $check_type ) { - return 'none'; - } + $registered_level = $checks[ $check_name ]['level'] ?? 'error'; - return \apply_filters( - 'wp_validation_check_level', - $check_type, + return $this->apply_level_filter( + $registered_level, array( 'scope' => 'block', 'block_type' => $block_type, diff --git a/includes/Core/Traits/Logger.php b/includes/Core/Traits/Logger.php index 69e165e..e80246b 100644 --- a/includes/Core/Traits/Logger.php +++ b/includes/Core/Traits/Logger.php @@ -25,7 +25,7 @@ trait Logger { * @param string $message Error message to log. * @return void */ - private function log_error( string $message ): void { + protected function log_error( string $message ): void { if ( defined( 'WP_DEBUG' ) && constant( 'WP_DEBUG' ) ) { $class_name = basename( str_replace( '\\', '/', static::class ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log @@ -39,7 +39,7 @@ private function log_error( string $message ): void { * @param string $message Debug message to log. * @return void */ - private function log_debug( string $message ): void { + protected function log_debug( string $message ): void { if ( defined( 'WP_DEBUG' ) && constant( 'WP_DEBUG' ) && defined( 'WP_DEBUG_LOG' ) && constant( 'WP_DEBUG_LOG' ) ) { $class_name = basename( str_replace( '\\', '/', static::class ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log diff --git a/includes/Editor/Registry.php b/includes/Editor/Registry.php index 08faf69..96babe1 100644 --- a/includes/Editor/Registry.php +++ b/includes/Editor/Registry.php @@ -10,16 +10,14 @@ namespace ValidationAPI\Editor; -use ValidationAPI\Core\Traits\Logger; +use ValidationAPI\AbstractRegistry; /** * Editor Checks Registry Class * * Manages registration and execution of validation checks for the general editor state. */ -class Registry { - - use Logger; +class Registry extends AbstractRegistry { /** * Registered editor checks @@ -55,17 +53,6 @@ private function __construct() { // Private constructor for singleton pattern. } - /** - * Sort checks by priority - * - * @param array $a First check. - * @param array $b Second check. - * @return int Comparison result. - */ - private function sort_checks_by_priority( $a, $b ) { - return $a['priority'] - $b['priority']; - } - /** * Register an editor check * @@ -87,34 +74,11 @@ public function register_editor_check( string $post_type, string $check_name, ar return false; } - $defaults = array( - 'error_msg' => '', - 'warning_msg' => '', - 'level' => 'error', - 'priority' => 10, - 'enabled' => true, - 'description' => '', - 'configurable' => true, - ); - - $check_args = \wp_parse_args( $check_args, $defaults ); - - // Validate required parameters. - if ( empty( $check_args['error_msg'] ) ) { - $this->log_error( "error_msg is required for {$post_type}/{$check_name}" ); - return false; - } + $context_label = "{$post_type}/{$check_name}"; + $check_args = $this->normalize_args( $check_args, $context_label ); - // Fallback for warning_msg to error_msg. - if ( empty( $check_args['warning_msg'] ) ) { - $check_args['warning_msg'] = $check_args['error_msg']; - } - - // Validate level parameter. - $valid_levels = array( 'error', 'warning', 'none' ); - if ( ! in_array( $check_args['level'], $valid_levels, true ) ) { - $this->log_error( "Invalid level '{$check_args['level']}' for {$post_type}/{$check_name}. Using 'error'." ); - $check_args['level'] = 'error'; + if ( false === $check_args ) { + return false; } // Allow developers to filter check arguments before registration. @@ -122,7 +86,7 @@ public function register_editor_check( string $post_type, string $check_name, ar // Allow developers to prevent specific checks from being registered. if ( ! \apply_filters( 'wp_validation_should_register_editor_check', true, $post_type, $check_name, $check_args ) ) { - $this->log_debug( "Editor check registration prevented by filter: {$post_type}/{$check_name}" ); + $this->log_debug( "Editor check registration prevented by filter: {$context_label}" ); return false; } @@ -131,17 +95,12 @@ public function register_editor_check( string $post_type, string $check_name, ar $this->editor_checks[ $post_type ] = array(); } - // Stamp namespace attribution from registration args. - if ( ! empty( $check_args['namespace'] ) ) { - $check_args['_namespace'] = $check_args['namespace']; - unset( $check_args['namespace'] ); - } + $check_args = $this->stamp_namespace( $check_args ); // Store the check. $this->editor_checks[ $post_type ][ $check_name ] = $check_args; - // Sort checks by priority. - \uasort( $this->editor_checks[ $post_type ], array( $this, 'sort_checks_by_priority' ) ); + $this->sort_by_priority( $this->editor_checks[ $post_type ] ); // Action hook for developers to know when a check is registered. \do_action( 'wp_validation_editor_check_registered', $post_type, $check_name, $check_args ); @@ -206,16 +165,10 @@ public function get_effective_editor_check_level( string $post_type, string $che return 'none'; } - $check_type = $editor_checks[ $check_name ]['level'] ?? 'error'; - - // 'none' short-circuits — filter does not fire. - if ( 'none' === $check_type ) { - return 'none'; - } + $registered_level = $editor_checks[ $check_name ]['level'] ?? 'error'; - return \apply_filters( - 'wp_validation_check_level', - $check_type, + return $this->apply_level_filter( + $registered_level, array( 'scope' => 'editor', 'post_type' => $post_type, diff --git a/includes/Meta/Registry.php b/includes/Meta/Registry.php index 8f2dfb7..8e6d1cd 100644 --- a/includes/Meta/Registry.php +++ b/includes/Meta/Registry.php @@ -10,16 +10,14 @@ namespace ValidationAPI\Meta; -use ValidationAPI\Core\Traits\Logger; +use ValidationAPI\AbstractRegistry; /** * Meta Checks Registry Class * * Manages registration and execution of validation checks for post meta fields. */ -class Registry { - - use Logger; +class Registry extends AbstractRegistry { /** * Registered meta checks @@ -55,17 +53,6 @@ private function __construct() { // Private constructor for singleton pattern. } - /** - * Sort checks by priority - * - * @param array $a First check. - * @param array $b Second check. - * @return int Comparison result. - */ - private function sort_checks_by_priority( $a, $b ) { - return $a['priority'] - $b['priority']; - } - /** * Register a meta check * @@ -93,34 +80,11 @@ public function register_meta_check( string $post_type, string $meta_key, string return false; } - $defaults = array( - 'error_msg' => '', - 'warning_msg' => '', - 'level' => 'error', - 'priority' => 10, - 'enabled' => true, - 'description' => '', - 'configurable' => true, - ); - - $check_args = \wp_parse_args( $check_args, $defaults ); - - // Validate required parameters. - if ( empty( $check_args['error_msg'] ) ) { - $this->log_error( "error_msg is required for {$post_type}/{$meta_key}/{$check_name}" ); - return false; - } + $context_label = "{$post_type}/{$meta_key}/{$check_name}"; + $check_args = $this->normalize_args( $check_args, $context_label ); - // Fallback for warning_msg to error_msg. - if ( empty( $check_args['warning_msg'] ) ) { - $check_args['warning_msg'] = $check_args['error_msg']; - } - - // Validate level parameter. - $valid_levels = array( 'error', 'warning', 'none' ); - if ( ! in_array( $check_args['level'], $valid_levels, true ) ) { - $this->log_error( "Invalid level '{$check_args['level']}' for {$post_type}/{$meta_key}/{$check_name}. Using 'error'." ); - $check_args['level'] = 'error'; + if ( false === $check_args ) { + return false; } // Allow developers to filter check arguments before registration. @@ -128,7 +92,7 @@ public function register_meta_check( string $post_type, string $meta_key, string // Allow developers to prevent specific checks from being registered. if ( ! \apply_filters( 'wp_validation_should_register_meta_check', true, $post_type, $meta_key, $check_name, $check_args ) ) { - $this->log_debug( "Meta check registration prevented by filter: {$post_type}/{$meta_key}/{$check_name}" ); + $this->log_debug( "Meta check registration prevented by filter: {$context_label}" ); return false; } @@ -142,17 +106,12 @@ public function register_meta_check( string $post_type, string $meta_key, string $this->meta_checks[ $post_type ][ $meta_key ] = array(); } - // Stamp namespace attribution from registration args. - if ( ! empty( $check_args['namespace'] ) ) { - $check_args['_namespace'] = $check_args['namespace']; - unset( $check_args['namespace'] ); - } + $check_args = $this->stamp_namespace( $check_args ); // Store the check. $this->meta_checks[ $post_type ][ $meta_key ][ $check_name ] = $check_args; - // Sort checks by priority. - \uasort( $this->meta_checks[ $post_type ][ $meta_key ], array( $this, 'sort_checks_by_priority' ) ); + $this->sort_by_priority( $this->meta_checks[ $post_type ][ $meta_key ] ); // Action hook for developers to know when a check is registered. \do_action( 'wp_validation_meta_check_registered', $post_type, $meta_key, $check_name, $check_args ); @@ -219,16 +178,10 @@ public function get_effective_meta_check_level( string $post_type, string $meta_ return 'none'; } - $check_type = $meta_checks[ $meta_key ][ $check_name ]['level'] ?? 'error'; - - // 'none' short-circuits — filter does not fire. - if ( 'none' === $check_type ) { - return 'none'; - } + $registered_level = $meta_checks[ $meta_key ][ $check_name ]['level'] ?? 'error'; - return \apply_filters( - 'wp_validation_check_level', - $check_type, + return $this->apply_level_filter( + $registered_level, array( 'scope' => 'meta', 'post_type' => $post_type, From c927184f084c4b351291686c42b35d92bb4e0356 Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Sat, 18 Apr 2026 09:22:27 -0400 Subject: [PATCH 05/11] batch 2: move REST namespace from wp/v2 to wp-validation/v1 Stop squatting on the core-reserved wp/v2 namespace. Use a plugin-owned namespace now; the core-PR can negotiate a final location (wp/v2 or wp-block-editor/v1) during review. - ChecksController::$namespace: wp/v2 -> wp-validation/v1 - ChecksController::$rest_base: validation-checks -> checks - Final URL: /wp-json/wp-validation/v1/checks - Update docs: CLAUDE.md, README.md, readme.txt, docs/technical/README.md, docs/technical/api.md, docs/technical/companion-package.md, docs/INTEGRATION.md, docs/PROPOSAL.md, docs/guide/README.md, docs/guide/examples.md - PROPOSAL.md calls out that final core namespace is TBD during PR Settings addon updated in a separate commit in its own repo. See docs/gutenberg-alignment/pass-a.md Batch 2 for rationale. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 2 +- README.md | 2 +- docs/INTEGRATION.md | 6 +++--- docs/PROPOSAL.md | 4 ++-- docs/guide/README.md | 2 +- docs/guide/examples.md | 2 +- docs/technical/README.md | 2 +- docs/technical/api.md | 2 +- docs/technical/companion-package.md | 2 +- includes/Rest/ChecksController.php | 4 ++-- readme.txt | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b321309..c73b7fe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ Store name: `core/validation` ### REST API -`GET /wp/v2/validation-checks` — Returns all registered checks grouped by scope (block, meta, editor). Requires `manage_options`. +`GET /wp-validation/v1/checks` — Returns all registered checks grouped by scope (block, meta, editor). Requires `manage_options`. ## Project Structure diff --git a/README.md b/README.md index 8caf09b..d134a72 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Designed for Gutenberg core merge. External plugins provide the rules. - **Flat Registration API:** Register checks with `wp_register_block_validation_check()` and related functions — a `namespace` field attributes each check to the registering plugin - **Filterable Severity:** Every check passes through the `wp_validation_check_level` filter — any plugin can override severity at runtime - **Centralized Data Store:** A dedicated `core/validation` store via `@wordpress/data` manages all validation state with reactive selectors -- **REST API:** Registered checks are exposed via `GET /wp/v2/validation-checks` for admin tooling and companion packages +- **REST API:** Registered checks are exposed via `GET /wp-validation/v1/checks` for admin tooling and companion packages - **Editor Settings Integration:** Validation config flows from PHP to JS via the `block_editor_settings_all` filter, following Gutenberg's standard data passing pattern - **Extensible:** 20+ PHP actions/filters and 3 JS filters for complete customization diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index 7d682cc..29c4ee7 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -65,7 +65,7 @@ These items were adapted from plugin conventions to core conventions and are now | Function names `wp_register_*_validation_check()` | Done | Renamed from `validation_api_*` prefix | | JS filter names `editor.validate*` | Done | Renamed from `validation_api_validate_*` | | camelCase-only issue model in JS | Done | Removed dual camelCase/snake_case compatibility layer | -| REST endpoint `wp/v2/validation-checks` | Done | Renamed from `validation-api/v1/checks` | +| REST endpoint `wp-validation/v1/checks` | Done | Plugin-owned namespace; core-PR will negotiate final location (`wp/v2/validation-checks` or `wp-block-editor/v1/*`) | ### New to Gutenberg (No Equivalent) @@ -78,7 +78,7 @@ These items were adapted from plugin conventions to core conventions and are now | `ValidationToolbarButton` | Per-block validation toolbar UI | | Block/Meta/Editor registries | Declarative check registration | | `wp_validation_check_level` filter | Runtime severity override | -| REST `wp/v2/validation-checks` | Check introspection for admin tooling | +| REST `wp-validation/v1/checks` | Check introspection for admin tooling | ## Packages Affected @@ -227,7 +227,7 @@ The following naming changes have been applied throughout the codebase. These re | Old (Plugin) | Current | |---|---| -| `validation-api/v1/checks` | `wp/v2/validation-checks` | +| `validation-api/v1/checks` | `wp-validation/v1/checks` | ### Structural diff --git a/docs/PROPOSAL.md b/docs/PROPOSAL.md index 9b5e7a7..f182bf0 100644 --- a/docs/PROPOSAL.md +++ b/docs/PROPOSAL.md @@ -296,7 +296,7 @@ The [Validation API](https://github.com/troychaplin/validation-api) plugin demon ### REST API -A read-only endpoint (`GET /wp/v2/validation-checks`) exposes all registered checks grouped by scope (block, meta, editor). This enables admin tooling and companion packages to read the validation configuration without parsing PHP internals. +A read-only endpoint exposes all registered checks grouped by scope (block, meta, editor). This enables admin tooling and companion packages to read the validation configuration without parsing PHP internals. The reference plugin currently exposes this at `GET /wp-validation/v1/checks`; the final namespace in core is TBD during PR review (candidates: `wp/v2/validation-checks`, `wp-block-editor/v1/validation-checks`). ### External Plugin Integration @@ -327,7 +327,7 @@ The proposal is specifically for the **Validation API framework** -- the infrast - Editor context scoping (post editor only, content blocks within templates) - PHP action hooks for lifecycle events (`wp_validation_initialized`, `wp_validation_ready`, `wp_validation_editor_checks_ready`) - PHP filter hooks for check modification (`wp_validation_check_args`, `wp_validation_should_register_check`, `wp_validation_check_level`) -- REST API endpoint (`GET /wp/v2/validation-checks`) for admin tooling +- REST API endpoint for admin tooling (plugin exposes at `GET /wp-validation/v1/checks`; final namespace in core TBD) **Not included (remains in plugin territory):** diff --git a/docs/guide/README.md b/docs/guide/README.md index b0a65e4..d273a55 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -119,7 +119,7 @@ When you register checks, the Validation API automatically handles: - **Block indicators** — Red (error) and yellow (warning) borders on blocks with issues - **Validation sidebar** — All issues grouped by severity, with click-to-navigate - **Publish locking** — Error-level checks prevent publishing via `lockPostSaving`/`unlockPostSaving` -- **REST API** — Registered checks exposed at `GET /wp/v2/validation-checks` +- **REST API** — Registered checks exposed at `GET /wp-validation/v1/checks` - **Multi-context** — Works in both the post editor and the site editor You only write the registration and the validation logic. Everything else is handled. diff --git a/docs/guide/examples.md b/docs/guide/examples.md index 1082c1e..31f5f92 100644 --- a/docs/guide/examples.md +++ b/docs/guide/examples.md @@ -284,7 +284,7 @@ Use the REST API to see what's registered: ```javascript // In the browser console -wp.apiFetch( { path: '/wp/v2/validation-checks' } ).then( console.log ); +wp.apiFetch( { path: '/wp-validation/v1/checks' } ).then( console.log ); ``` This returns all registered checks grouped by scope, including `_namespace` attribution. Requires `manage_options` capability. diff --git a/docs/technical/README.md b/docs/technical/README.md index b7d5532..599b90c 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -84,7 +84,7 @@ The `ValidationAPI\Core\Assets` class handles: ### REST API -The `ValidationAPI\Rest\ChecksController` registers `GET /wp/v2/validation-checks`. It requires `manage_options` capability and returns all registered checks across all three scopes, including `_namespace` attribution. +The `ValidationAPI\Rest\ChecksController` registers `GET /wp-validation/v1/checks`. It requires `manage_options` capability and returns all registered checks across all three scopes, including `_namespace` attribution. ### Traits diff --git a/docs/technical/api.md b/docs/technical/api.md index 6916b1e..1fc07be 100644 --- a/docs/technical/api.md +++ b/docs/technical/api.md @@ -116,7 +116,7 @@ get_effective_meta_check_level( string $post_type, string $meta_key, string $che ## REST API -### GET /wp/v2/validation-checks +### GET /wp-validation/v1/checks Returns all registered checks across all three scopes. diff --git a/docs/technical/companion-package.md b/docs/technical/companion-package.md index c233bd0..a8397fe 100644 --- a/docs/technical/companion-package.md +++ b/docs/technical/companion-package.md @@ -28,7 +28,7 @@ The core plugin has no knowledge of the companion. It fires the filter; the comp The companion reads all registered checks via the REST API: ``` -GET /wp/v2/validation-checks +GET /wp-validation/v1/checks ``` This returns every check across all scopes, including `_namespace` attribution. The companion uses this to build its settings form dynamically — no hardcoded check list. diff --git a/includes/Rest/ChecksController.php b/includes/Rest/ChecksController.php index 51b7f60..64aa957 100644 --- a/includes/Rest/ChecksController.php +++ b/includes/Rest/ChecksController.php @@ -31,14 +31,14 @@ class ChecksController extends WP_REST_Controller { * * @var string */ - protected $namespace = 'wp/v2'; + protected $namespace = 'wp-validation/v1'; /** * The base for this controller's routes. * * @var string */ - protected $rest_base = 'validation-checks'; + protected $rest_base = 'checks'; /** * Register the routes for this controller. diff --git a/readme.txt b/readme.txt index 81b3e2d..91bf2d4 100644 --- a/readme.txt +++ b/readme.txt @@ -40,7 +40,7 @@ All severity levels are filterable at runtime via `wp_validation_check_level`, e * Full PHP Registry API with singleton access for advanced use cases * JavaScript validation runs client-side via `@wordpress/hooks` filters (`editor.validateBlock`, `editor.validateMeta`, `editor.validateEditor`) for real-time feedback * Centralized `core/validation` data store via `@wordpress/data` for reactive state management -* REST endpoint at `/wp/v2/validation-checks` returns all registered checks for companion tooling +* REST endpoint at `/wp-validation/v1/checks` returns all registered checks for companion tooling **Integration Example:** From 16b1dceba5e4171f36d6096f2b667c239ee8e922 Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Sat, 18 Apr 2026 10:17:42 -0400 Subject: [PATCH 06/11] batch 1: reshape src/ to package layout, convert to hooks Flatten src/editor/* and src/shared/* into a Gutenberg-package-style tree so the eventual core-PR lands as a git mv into packages/validation/src/. All public API surfaces (store name, filter names, global PHP functions, REST endpoint) are unchanged; this is a pure reorganization plus a few architectural polish items. Structure: src/index.js Entry point (replaces src/script.js) src/store/ Data store (moved verbatim from src/editor/store/) src/utils/ Flat utilities (validate-*, use-invalid-*, use-meta-*, issue-helpers, get-validation-config, use-debounced-validation, use-validation-issues, index.js barrel) src/components/ One folder per component src/hooks/ Side-effect modules + two React hooks (use-validation-sync, use-validation-lifecycle) Architectural changes (Pass B): - ValidationProvider renderless component becomes useValidationSync hook - ValidationAPI renderless component becomes useValidationLifecycle hook - Both hooks invoked by renderless sibling wrappers (, ) inside the ValidationPlugin root to avoid the infinite render loop that arose from subscribing and dispatching to the same store within one parent component - New src/hooks/pre-save-validation.js installs the editor.preSavePost async filter as a belt-and-suspenders save gate on top of lockPostSaving Consolidations (Pass C): - Extracted useValidationIssues() hook (new src/utils/) to deduplicate the 3-selector useSelect block from ValidationSidebar and the lifecycle hook - use-meta-field.js collapses its dual useSelect into one Getter-style hook files renamed: GetInvalid* -> useInvalid* both in file names and exported function names, reflecting their nature as React hooks rather than plain getters. Build/tooling: - webpack.config.js: entry points to src/index.js; path aliases (@, @editor, @shared) removed in favour of relative imports - package.json: sideEffects declared for src/index.js, src/hooks/**, src/store/index.js, src/**/*.scss; main updated to build/validation-api.js HOC behavior unchanged: - editor.BlockEdit filter still registers withErrorHandling (now lives in src/hooks/validate-block.js as a side-effect module) - editor.BlockListBlock filter still registers withBlockValidationClasses (now in src/hooks/block-validation-classes.js) Completes the five-batch Gutenberg alignment plan. See docs/gutenberg-alignment/consolidated-plan.md and pass-a.md for the full rationale; pass-b.md for the hook conversion and preSavePost decisions; pass-c.md for the consolidation rationale. Co-Authored-By: Claude Opus 4.7 (1M context) --- build/validation-api.asset.php | 2 +- build/validation-api.js | 1504 ++++++++++------- package.json | 8 +- .../validation-icon/index.js} | 2 + .../validation-sidebar/index.js} | 20 +- .../validation-toolbar-button/index.js} | 6 +- src/editor/components/ValidationProvider.js | 45 - src/editor/components/index.js | 9 - src/editor/register.js | 29 - src/editor/validation/ValidationAPI.js | 189 --- src/editor/validation/blocks/index.js | 7 - src/editor/validation/editor/index.js | 7 - src/editor/validation/meta/hooks/index.js | 8 - .../block-validation-classes.js} | 9 +- src/hooks/index.js | 13 + src/hooks/pre-save-validation.js | 32 + src/hooks/register-sidebar.js | 53 + src/hooks/use-validation-lifecycle.js | 133 ++ src/hooks/use-validation-sync.js | 44 + .../validate-block.js} | 28 +- src/index.js | 40 + src/script.js | 13 - src/shared/hooks/index.js | 7 - src/shared/utils/validation/index.js | 17 - src/{editor => }/store/actions.js | 0 src/{editor => }/store/constants.js | 0 src/{editor => }/store/index.js | 0 src/{editor => }/store/reducer.js | 0 src/{editor => }/store/selectors.js | 0 .../get-validation-config.js} | 0 src/utils/index.js | 28 + .../issue-helpers.js} | 0 .../use-debounced-validation.js} | 0 .../use-invalid-blocks.js} | 6 +- .../use-invalid-editor-checks.js} | 4 +- .../use-invalid-meta.js} | 6 +- .../use-meta-field.js} | 69 +- .../use-meta-validation.js} | 2 +- src/utils/use-validation-issues.js | 29 + .../validate-block.js} | 4 +- .../validate-editor.js} | 8 +- .../validate-meta.js} | 8 +- webpack.config.js | 11 +- 43 files changed, 1328 insertions(+), 1072 deletions(-) rename src/{editor/components/ValidationIcon.js => components/validation-icon/index.js} (97%) rename src/{editor/components/ValidationSidebar.js => components/validation-sidebar/index.js} (96%) rename src/{editor/components/ValidationToolbarButton.js => components/validation-toolbar-button/index.js} (93%) delete mode 100644 src/editor/components/ValidationProvider.js delete mode 100644 src/editor/components/index.js delete mode 100644 src/editor/register.js delete mode 100644 src/editor/validation/ValidationAPI.js delete mode 100644 src/editor/validation/blocks/index.js delete mode 100644 src/editor/validation/editor/index.js delete mode 100644 src/editor/validation/meta/hooks/index.js rename src/{editor/hoc/withBlockValidationClasses.js => hooks/block-validation-classes.js} (85%) create mode 100644 src/hooks/index.js create mode 100644 src/hooks/pre-save-validation.js create mode 100644 src/hooks/register-sidebar.js create mode 100644 src/hooks/use-validation-lifecycle.js create mode 100644 src/hooks/use-validation-sync.js rename src/{editor/hoc/withErrorHandling.js => hooks/validate-block.js} (69%) create mode 100644 src/index.js delete mode 100644 src/script.js delete mode 100644 src/shared/hooks/index.js delete mode 100644 src/shared/utils/validation/index.js rename src/{editor => }/store/actions.js (100%) rename src/{editor => }/store/constants.js (100%) rename src/{editor => }/store/index.js (100%) rename src/{editor => }/store/reducer.js (100%) rename src/{editor => }/store/selectors.js (100%) rename src/{shared/utils/validation/getValidationConfig.js => utils/get-validation-config.js} (100%) create mode 100644 src/utils/index.js rename src/{shared/utils/validation/issueHelpers.js => utils/issue-helpers.js} (100%) rename src/{shared/hooks/useDebouncedValidation.js => utils/use-debounced-validation.js} (100%) rename src/{shared/utils/validation/getInvalidBlocks.js => utils/use-invalid-blocks.js} (96%) rename src/{shared/utils/validation/getInvalidEditorChecks.js => utils/use-invalid-editor-checks.js} (92%) rename src/{shared/utils/validation/getInvalidMeta.js => utils/use-invalid-meta.js} (89%) rename src/{editor/validation/meta/hooks/useMetaField.js => utils/use-meta-field.js} (58%) rename src/{editor/validation/meta/hooks/useMetaValidation.js => utils/use-meta-validation.js} (97%) create mode 100644 src/utils/use-validation-issues.js rename src/{editor/validation/blocks/validateBlock.js => utils/validate-block.js} (96%) rename src/{editor/validation/editor/validateEditor.js => utils/validate-editor.js} (92%) rename src/{editor/validation/meta/validateMeta.js => utils/validate-meta.js} (95%) diff --git a/build/validation-api.asset.php b/build/validation-api.asset.php index f5b945c..0d9d030 100644 --- a/build/validation-api.asset.php +++ b/build/validation-api.asset.php @@ -1 +1 @@ - array('wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins'), 'version' => '8dd2b2d70b25cb6f7678'); + array('wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins'), 'version' => '349b73cbd185e076f865'); diff --git a/build/validation-api.js b/build/validation-api.js index d61743b..1eb9ee0 100644 --- a/build/validation-api.js +++ b/build/validation-api.js @@ -18,28 +18,34 @@ t = {}; (e.r(t), e.d(t, { - getBlockValidation: () => te, - getInvalidBlocks: () => X, - getInvalidEditorChecks: () => ee, - getInvalidMeta: () => Y, - hasErrors: () => re, - hasWarnings: () => ne, + getBlockValidation: () => w, + getInvalidBlocks: () => b, + getInvalidEditorChecks: () => h, + getInvalidMeta: () => g, + hasErrors: () => O, + hasWarnings: () => E, })); var r = {}; (e.r(r), e.d(r, { - clearBlockValidation: () => ce, - setBlockValidation: () => le, - setInvalidBlocks: () => oe, - setInvalidEditorChecks: () => ae, - setInvalidMeta: () => ie, + clearBlockValidation: () => R, + setBlockValidation: () => P, + setInvalidBlocks: () => j, + setInvalidEditorChecks: () => k, + setInvalidMeta: () => S, })); - const n = window.wp.plugins, - o = window.wp.data, - i = window.wp.element; - function a(e) { + const n = window.wp.data; + var o = 'core/validation', + i = 'SET_INVALID_BLOCKS', + a = 'SET_INVALID_META', + c = 'SET_INVALID_EDITOR_CHECKS', + l = 'SET_BLOCK_VALIDATION', + u = 'CLEAR_BLOCK_VALIDATION', + s = { blocks: [], meta: [], editor: [], blockValidation: {} }, + f = Object.freeze({ mode: 'none', issues: [] }); + function d(e) { return ( - (a = + (d = 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator ? function (e) { return typeof e; @@ -52,10 +58,10 @@ ? 'symbol' : typeof e; }), - a(e) + d(e) ); } - function l(e, t) { + function p(e, t) { var r = Object.keys(e); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); @@ -67,35 +73,222 @@ } return r; } - function c(e) { + function m(e) { for (var t = 1; t < arguments.length; t++) { var r = null != arguments[t] ? arguments[t] : {}; t % 2 - ? l(Object(r), !0).forEach(function (t) { - u(e, t, r[t]); + ? p(Object(r), !0).forEach(function (t) { + v(e, t, r[t]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : l(Object(r)).forEach(function (t) { + : p(Object(r)).forEach(function (t) { Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); }); } return e; } - function u(e, t, r) { + function v(e, t, r) { + return ( + (t = y(t)) in e + ? Object.defineProperty(e, t, { + value: r, + enumerable: !0, + configurable: !0, + writable: !0, + }) + : (e[t] = r), + e + ); + } + function y(e) { + var t = (function (e) { + if ('object' != d(e) || !e) return e; + var t = e[Symbol.toPrimitive]; + if (void 0 !== t) { + var r = t.call(e, 'string'); + if ('object' != d(r)) return r; + throw new TypeError('@@toPrimitive must return a primitive value.'); + } + return String(e); + })(e); + return 'symbol' == d(t) ? t : t + ''; + } + function b(e) { + return e.blocks; + } + function g(e) { + return e.meta; + } + function h(e) { + return e.editor; + } + function w(e, t) { + return e.blockValidation[t] || f; + } + function O(e) { + var t = e.blocks.some(function (e) { + return 'error' === e.mode; + }), + r = e.meta.some(function (e) { + return e.hasErrors; + }), + n = e.editor.some(function (e) { + return 'error' === e.type; + }); + return t || r || n; + } + function E(e) { + if (O(e)) return !1; + var t = e.blocks.some(function (e) { + return 'warning' === e.mode; + }), + r = e.meta.some(function (e) { + return e.hasWarnings && !e.hasErrors; + }), + n = e.editor.some(function (e) { + return 'warning' === e.type; + }); + return t || r || n; + } + function j(e) { + return { type: i, results: e }; + } + function S(e) { + return { type: a, results: e }; + } + function k(e) { + return { type: c, issues: e }; + } + function P(e, t) { + return { type: l, clientId: e, result: t }; + } + function R(e) { + return { type: u, clientId: e }; + } + var I = (0, n.createReduxStore)(o, { + reducer: function () { + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : s, + t = arguments.length > 1 ? arguments[1] : void 0; + switch (t.type) { + case i: + return m(m({}, e), {}, { blocks: t.results }); + case a: + return m(m({}, e), {}, { meta: t.results }); + case c: + return m(m({}, e), {}, { editor: t.issues }); + case l: + return m( + m({}, e), + {}, + { + blockValidation: m( + m({}, e.blockValidation), + {}, + v({}, t.clientId, t.result) + ), + } + ); + case u: + var r = e.blockValidation, + n = t.clientId, + o = + (r[n], + (function (e, t) { + if (null == e) return {}; + var r, + n, + o = (function (e, t) { + if (null == e) return {}; + var r = {}; + for (var n in e) + if ({}.hasOwnProperty.call(e, n)) { + if (-1 !== t.indexOf(n)) continue; + r[n] = e[n]; + } + return r; + })(e, t); + if (Object.getOwnPropertySymbols) { + var i = Object.getOwnPropertySymbols(e); + for (n = 0; n < i.length; n++) + ((r = i[n]), + -1 === t.indexOf(r) && + {}.propertyIsEnumerable.call(e, r) && + (o[r] = e[r])); + } + return o; + })(r, [n].map(y))); + return m(m({}, e), {}, { blockValidation: o }); + default: + return e; + } + }, + selectors: t, + actions: r, + }); + (0, n.register)(I); + const A = window.wp.plugins, + _ = window.wp.element, + B = window.wp.hooks; + function N(e) { + return ( + (N = + 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator + ? function (e) { + return typeof e; + } + : function (e) { + return e && + 'function' == typeof Symbol && + e.constructor === Symbol && + e !== Symbol.prototype + ? 'symbol' + : typeof e; + }), + N(e) + ); + } + function T(e, t) { + var r = Object.keys(e); + if (Object.getOwnPropertySymbols) { + var n = Object.getOwnPropertySymbols(e); + (t && + (n = n.filter(function (t) { + return Object.getOwnPropertyDescriptor(e, t).enumerable; + })), + r.push.apply(r, n)); + } + return r; + } + function C(e) { + for (var t = 1; t < arguments.length; t++) { + var r = null != arguments[t] ? arguments[t] : {}; + t % 2 + ? T(Object(r), !0).forEach(function (t) { + L(e, t, r[t]); + }) + : Object.getOwnPropertyDescriptors + ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) + : T(Object(r)).forEach(function (t) { + Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); + }); + } + return e; + } + function L(e, t, r) { return ( (t = (function (e) { var t = (function (e) { - if ('object' != a(e) || !e) return e; + if ('object' != N(e) || !e) return e; var t = e[Symbol.toPrimitive]; if (void 0 !== t) { var r = t.call(e, 'string'); - if ('object' != a(r)) return r; + if ('object' != N(r)) return r; throw new TypeError('@@toPrimitive must return a primitive value.'); } return String(e); })(e); - return 'symbol' == a(t) ? t : t + ''; + return 'symbol' == N(t) ? t : t + ''; })(t)) in e ? Object.defineProperty(e, t, { value: r, @@ -107,37 +300,37 @@ e ); } - var s = function (e, t) { + var M = function (e, t) { return e.filter(function (e) { return e.type === t; }); }, - f = function (e) { - return s(e, 'error'); + V = function (e) { + return M(e, 'error'); }, - d = function (e) { - return s(e, 'warning'); + D = function (e) { + return M(e, 'warning'); }, - m = function (e) { + x = function (e) { return e.some(function (e) { return 'error' === e.type; }); }, - p = function (e) { + F = function (e) { return e.some(function (e) { return 'warning' === e.type; }); }, - v = function (e) { + K = function (e) { return null != e && !1 !== e.enabled; }, - y = function (e, t) { + U = function (e, t) { var r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}, n = e.message || '', o = e.error_msg || n, i = e.warning_msg || e.error_msg || n, a = e.level || 'error'; - return c( + return C( { check: t, checkName: t, @@ -150,27 +343,40 @@ r ); }, - b = function (e) { + G = function (e) { var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}; - return c({ isValid: 0 === e.length, issues: e, hasErrors: m(e), hasWarnings: p(e) }, t); + return C({ isValid: 0 === e.length, issues: e, hasErrors: x(e), hasWarnings: F(e) }, t); }; - const g = window.wp.hooks; - function h(e, t) { + function W() { + try { + var e = (0, n.select)('core/editor').getEditorSettings(); + return (null == e ? void 0 : e.validationApi) || {}; + } catch (e) { + return {}; + } + } + function $() { + return W().metaValidationRules || {}; + } + function q() { + return W().editorContext || 'none'; + } + function H(e, t) { (null == t || t > e.length) && (t = e.length); for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; return n; } - var w = function (e) { + var Z = function (e) { var t = e.name, r = e.attributes, n = [], - o = (O().validationRules || {})[t] || {}; + o = (W().validationRules || {})[t] || {}; if (0 === Object.keys(o).length) return { isValid: !0, issues: [], mode: 'none', clientId: e.clientId, name: t }; Object.entries(o).forEach(function (o) { var i, a, - l = + c = ((a = 2), (function (e) { if (Array.isArray(e)) return e; @@ -186,26 +392,26 @@ o, i, a, - l = [], - c = !0, + c = [], + l = !0, u = !1; try { if (((i = (r = r.call(e)).next), 0 === t)) { if (Object(r) !== r) return; - c = !1; + l = !1; } else for ( ; - !(c = (n = i.call(r)).done) && - (l.push(n.value), l.length !== t); - c = !0 + !(l = (n = i.call(r)).done) && + (c.push(n.value), c.length !== t); + l = !0 ); } catch (e) { ((u = !0), (o = e)); } finally { try { if ( - !c && + !l && null != r.return && ((a = r.return()), Object(a) !== a) ) @@ -214,12 +420,12 @@ if (u) throw o; } } - return l; + return c; } })(i, a) || (function (e, t) { if (e) { - if ('string' == typeof e) return h(e, t); + if ('string' == typeof e) return H(e, t); var r = {}.toString.call(e).slice(8, -1); return ( 'Object' === r && e.constructor && (r = e.constructor.name), @@ -227,7 +433,7 @@ ? Array.from(e) : 'Arguments' === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? h(e, t) + ? H(e, t) : void 0 ); } @@ -237,58 +443,44 @@ 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' ); })()), - c = l[0], - u = l[1]; - if (v(u)) { + l = c[0], + u = c[1]; + if (K(u)) { var s = !0; ('function' == typeof u.validator && (s = u.validator(r, e)), - (s = (0, g.applyFilters)('editor.validateBlock', s, t, r, c, e)) || - n.push(y(u, c))); + (s = (0, B.applyFilters)('editor.validateBlock', s, t, r, l, e)) || + n.push(U(u, l))); } }); var i = 'none'; return ( - m(n) ? (i = 'error') : p(n) && (i = 'warning'), - b(n, { mode: i, clientId: e.clientId, name: t }) + x(n) ? (i = 'error') : F(n) && (i = 'warning'), + G(n, { mode: i, clientId: e.clientId, name: t }) ); }; - function O() { - try { - var e = (0, o.select)('core/editor').getEditorSettings(); - return (null == e ? void 0 : e.validationApi) || {}; - } catch (e) { - return {}; - } - } - function E() { - return O().metaValidationRules || {}; - } - function k() { - return O().editorContext || 'none'; - } - function S(e, t) { + function z(e, t) { if (e) { - if ('string' == typeof e) return j(e, t); + if ('string' == typeof e) return J(e, t); var r = {}.toString.call(e).slice(8, -1); return ( 'Object' === r && e.constructor && (r = e.constructor.name), 'Map' === r || 'Set' === r ? Array.from(e) : 'Arguments' === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? j(e, t) + ? J(e, t) : void 0 ); } } - function j(e, t) { + function J(e, t) { (null == t || t > e.length) && (t = e.length); for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; return n; } - function P(e) { + function Q(e) { return e.flatMap(function (e) { var t, - r = w(e), + r = Z(e), n = []; return ( r.isValid || n.push(r), @@ -296,8 +488,8 @@ ? [].concat( n, (function (e) { - if (Array.isArray(e)) return j(e); - })((t = P(e.innerBlocks))) || + if (Array.isArray(e)) return J(e); + })((t = Q(e.innerBlocks))) || (function (e) { if ( ('undefined' != typeof Symbol && @@ -306,7 +498,7 @@ ) return Array.from(e); })(t) || - S(t) || + z(t) || (function () { throw new TypeError( 'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' @@ -317,12 +509,12 @@ ); }); } - function R(e) { + function X(e) { var t, r = (function (e) { var t = ('undefined' != typeof Symbol && e[Symbol.iterator]) || e['@@iterator']; if (!t) { - if (Array.isArray(e) || (t = S(e))) { + if (Array.isArray(e) || (t = z(e))) { t && (e = t); var _n = 0, r = function () {}; @@ -369,7 +561,7 @@ var n = t.value; if ('core/post-content' === n.name) return n; if (n.innerBlocks && n.innerBlocks.length > 0) { - var o = R(n.innerBlocks); + var o = X(n.innerBlocks); if (o) return o; } } @@ -380,16 +572,16 @@ } return null; } - function I() { - var e = k(), + function Y() { + var e = q(), t = 'post-editor' === e || 'post-editor-template' === e; - return P( - (0, o.useSelect)( + return Q( + (0, n.useSelect)( function (e) { var r = e('core/block-editor'), n = r.getBlocks(); if (t) { - var o = R(n); + var o = X(n); if (o) { var i = r.getBlock(o.clientId), a = r @@ -409,7 +601,7 @@ ) ); } - function A(e, t) { + function ee(e, t) { return ( (function (e) { if (Array.isArray(e)) return e; @@ -424,35 +616,35 @@ o, i, a, - l = [], - c = !0, + c = [], + l = !0, u = !1; try { if (((i = (r = r.call(e)).next), 0 === t)) { if (Object(r) !== r) return; - c = !1; + l = !1; } else for ( ; - !(c = (n = i.call(r)).done) && (l.push(n.value), l.length !== t); - c = !0 + !(l = (n = i.call(r)).done) && (c.push(n.value), c.length !== t); + l = !0 ); } catch (e) { ((u = !0), (o = e)); } finally { try { - if (!c && null != r.return && ((a = r.return()), Object(a) !== a)) + if (!l && null != r.return && ((a = r.return()), Object(a) !== a)) return; } finally { if (u) throw o; } } - return l; + return c; } })(e, t) || (function (e, t) { if (e) { - if ('string' == typeof e) return B(e, t); + if ('string' == typeof e) return te(e, t); var r = {}.toString.call(e).slice(8, -1); return ( 'Object' === r && e.constructor && (r = e.constructor.name), @@ -460,7 +652,7 @@ ? Array.from(e) : 'Arguments' === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? B(e, t) + ? te(e, t) : void 0 ); } @@ -472,43 +664,43 @@ })() ); } - function B(e, t) { + function te(e, t) { (null == t || t > e.length) && (t = e.length); for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; return n; } - function N(e, t, r, n) { + function re(e, t, r, n) { var o, i = - null === (o = E()[e]) || void 0 === o || null === (o = o[t]) || void 0 === o + null === (o = $()[e]) || void 0 === o || null === (o = o[t]) || void 0 === o ? void 0 : o[n]; - if (!v(i)) return !0; + if (!K(i)) return !0; var a = !0; return ( 'required' === n && (a = '' !== r && null != r), - (0, g.applyFilters)('editor.validateMeta', a, r, e, t, n) + (0, B.applyFilters)('editor.validateMeta', a, r, e, t, n) ); } - function C(e, t, r) { + function ne(e, t, r) { for ( - var n = (E()[e] || {})[t] || {}, o = [], i = 0, a = Object.entries(n); + var n = ($()[e] || {})[t] || {}, o = [], i = 0, a = Object.entries(n); i < a.length; i++ ) { - var l = A(a[i], 2), - c = l[0], - u = l[1]; - if (v(u) && !N(e, t, r, c)) { - var s = y(u, c, { metaKey: t }); + var c = ee(a[i], 2), + l = c[0], + u = c[1]; + if (K(u) && !re(e, t, r, l)) { + var s = U(u, l, { metaKey: t }); o.push(s); } } - return b(o); + return G(o); } - function _(e) { + function oe(e) { return ( - (_ = + (oe = 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator ? function (e) { return typeof e; @@ -521,10 +713,10 @@ ? 'symbol' : typeof e; }), - _(e) + oe(e) ); } - function L(e, t) { + function ie(e, t) { var r = Object.keys(e); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); @@ -536,35 +728,35 @@ } return r; } - function M(e) { + function ae(e) { for (var t = 1; t < arguments.length; t++) { var r = null != arguments[t] ? arguments[t] : {}; t % 2 - ? L(Object(r), !0).forEach(function (t) { - T(e, t, r[t]); + ? ie(Object(r), !0).forEach(function (t) { + ce(e, t, r[t]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : L(Object(r)).forEach(function (t) { + : ie(Object(r)).forEach(function (t) { Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); }); } return e; } - function T(e, t, r) { + function ce(e, t, r) { return ( (t = (function (e) { var t = (function (e) { - if ('object' != _(e) || !e) return e; + if ('object' != oe(e) || !e) return e; var t = e[Symbol.toPrimitive]; if (void 0 !== t) { var r = t.call(e, 'string'); - if ('object' != _(r)) return r; + if ('object' != oe(r)) return r; throw new TypeError('@@toPrimitive must return a primitive value.'); } return String(e); })(e); - return 'symbol' == _(t) ? t : t + ''; + return 'symbol' == oe(t) ? t : t + ''; })(t)) in e ? Object.defineProperty(e, t, { value: r, @@ -576,7 +768,7 @@ e ); } - function V(e, t) { + function le(e, t) { return ( (function (e) { if (Array.isArray(e)) return e; @@ -591,35 +783,35 @@ o, i, a, - l = [], - c = !0, + c = [], + l = !0, u = !1; try { if (((i = (r = r.call(e)).next), 0 === t)) { if (Object(r) !== r) return; - c = !1; + l = !1; } else for ( ; - !(c = (n = i.call(r)).done) && (l.push(n.value), l.length !== t); - c = !0 + !(l = (n = i.call(r)).done) && (c.push(n.value), c.length !== t); + l = !0 ); } catch (e) { ((u = !0), (o = e)); } finally { try { - if (!c && null != r.return && ((a = r.return()), Object(a) !== a)) + if (!l && null != r.return && ((a = r.return()), Object(a) !== a)) return; } finally { if (u) throw o; } } - return l; + return c; } })(e, t) || (function (e, t) { if (e) { - if ('string' == typeof e) return D(e, t); + if ('string' == typeof e) return ue(e, t); var r = {}.toString.call(e).slice(8, -1); return ( 'Object' === r && e.constructor && (r = e.constructor.name), @@ -627,7 +819,7 @@ ? Array.from(e) : 'Arguments' === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? D(e, t) + ? ue(e, t) : void 0 ); } @@ -639,210 +831,19 @@ })() ); } - function D(e, t) { + function ue(e, t) { (null == t || t > e.length) && (t = e.length); for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; return n; } - var x = 'core/validation', - F = 'SET_INVALID_BLOCKS', - K = 'SET_INVALID_META', - U = 'SET_INVALID_EDITOR_CHECKS', - W = 'SET_BLOCK_VALIDATION', - $ = 'CLEAR_BLOCK_VALIDATION', - q = { blocks: [], meta: [], editor: [], blockValidation: {} }, - H = Object.freeze({ mode: 'none', issues: [] }); - function Z(e) { - return ( - (Z = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (e) { - return typeof e; - } - : function (e) { - return e && - 'function' == typeof Symbol && - e.constructor === Symbol && - e !== Symbol.prototype - ? 'symbol' - : typeof e; - }), - Z(e) - ); - } - function z(e, t) { - var r = Object.keys(e); - if (Object.getOwnPropertySymbols) { - var n = Object.getOwnPropertySymbols(e); - (t && - (n = n.filter(function (t) { - return Object.getOwnPropertyDescriptor(e, t).enumerable; - })), - r.push.apply(r, n)); - } - return r; - } - function G(e) { - for (var t = 1; t < arguments.length; t++) { - var r = null != arguments[t] ? arguments[t] : {}; - t % 2 - ? z(Object(r), !0).forEach(function (t) { - J(e, t, r[t]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : z(Object(r)).forEach(function (t) { - Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); - }); - } - return e; - } - function J(e, t, r) { - return ( - (t = Q(t)) in e - ? Object.defineProperty(e, t, { - value: r, - enumerable: !0, - configurable: !0, - writable: !0, - }) - : (e[t] = r), - e - ); - } - function Q(e) { - var t = (function (e) { - if ('object' != Z(e) || !e) return e; - var t = e[Symbol.toPrimitive]; - if (void 0 !== t) { - var r = t.call(e, 'string'); - if ('object' != Z(r)) return r; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return String(e); - })(e); - return 'symbol' == Z(t) ? t : t + ''; - } - function X(e) { - return e.blocks; - } - function Y(e) { - return e.meta; - } - function ee(e) { - return e.editor; - } - function te(e, t) { - return e.blockValidation[t] || H; - } - function re(e) { - var t = e.blocks.some(function (e) { - return 'error' === e.mode; - }), - r = e.meta.some(function (e) { - return e.hasErrors; - }), - n = e.editor.some(function (e) { - return 'error' === e.type; - }); - return t || r || n; - } - function ne(e) { - if (re(e)) return !1; - var t = e.blocks.some(function (e) { - return 'warning' === e.mode; - }), - r = e.meta.some(function (e) { - return e.hasWarnings && !e.hasErrors; - }), - n = e.editor.some(function (e) { - return 'warning' === e.type; - }); - return t || r || n; - } - function oe(e) { - return { type: F, results: e }; - } - function ie(e) { - return { type: K, results: e }; - } - function ae(e) { - return { type: U, issues: e }; - } - function le(e, t) { - return { type: W, clientId: e, result: t }; - } - function ce(e) { - return { type: $, clientId: e }; - } - var ue = (0, o.createReduxStore)(x, { - reducer: function () { - var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : q, - t = arguments.length > 1 ? arguments[1] : void 0; - switch (t.type) { - case F: - return G(G({}, e), {}, { blocks: t.results }); - case K: - return G(G({}, e), {}, { meta: t.results }); - case U: - return G(G({}, e), {}, { editor: t.issues }); - case W: - return G( - G({}, e), - {}, - { - blockValidation: G( - G({}, e.blockValidation), - {}, - J({}, t.clientId, t.result) - ), - } - ); - case $: - var r = e.blockValidation, - n = t.clientId, - o = - (r[n], - (function (e, t) { - if (null == e) return {}; - var r, - n, - o = (function (e, t) { - if (null == e) return {}; - var r = {}; - for (var n in e) - if ({}.hasOwnProperty.call(e, n)) { - if (-1 !== t.indexOf(n)) continue; - r[n] = e[n]; - } - return r; - })(e, t); - if (Object.getOwnPropertySymbols) { - var i = Object.getOwnPropertySymbols(e); - for (n = 0; n < i.length; n++) - ((r = i[n]), - -1 === t.indexOf(r) && - {}.propertyIsEnumerable.call(e, r) && - (o[r] = e[r])); - } - return o; - })(r, [n].map(Q))); - return G(G({}, e), {}, { blockValidation: o }); - default: - return e; - } - }, - selectors: t, - actions: r, - }); function se() { var e, t, r, - n = I(), + i = Y(), a = (function () { for ( - var e = (0, o.useSelect)(function (e) { + var e = (0, n.useSelect)(function (e) { var t = e('core/editor'); return { postType: t.getCurrentPostType(), @@ -851,21 +852,21 @@ }, []), t = e.postType, r = e.meta, - n = E()[t] || {}, + o = $()[t] || {}, i = [], a = 0, - l = Object.keys(n); - a < l.length; + c = Object.keys(o); + a < c.length; a++ ) { - var c = l[a], - u = C(t, c, null == r ? void 0 : r[c]); - u.isValid || i.push(M(M({}, u), {}, { metaKey: c })); + var l = c[a], + u = ne(t, l, null == r ? void 0 : r[l]); + u.isValid || i.push(ae(ae({}, u), {}, { metaKey: l })); } return i; })(), - l = - ((t = (e = (0, o.useSelect)(function (e) { + c = + ((t = (e = (0, n.useSelect)(function (e) { var t = e('core/editor'), r = e('core/block-editor'); return { @@ -877,21 +878,21 @@ (r = e.postType) && t ? (function (e, t) { for ( - var r = (O().editorValidationRules || {})[e] || {}, + var r = (W().editorValidationRules || {})[e] || {}, n = [], o = 0, i = Object.entries(r); o < i.length; o++ ) { - var a = V(i[o], 2), - l = a[0], - c = a[1]; + var a = le(i[o], 2), + c = a[0], + l = a[1]; if ( - v(c) && - !(0, g.applyFilters)('editor.validateEditor', !0, t, e, l, c) + K(l) && + !(0, B.applyFilters)('editor.validateEditor', !0, t, e, c, l) ) { - var u = y(c, l); + var u = U(l, c); n.push(u); } } @@ -899,129 +900,49 @@ n.sort(function (e, t) { return e.priority - t.priority; }), - b(n) + G(n) ); })(r, t).issues : []), - c = (0, o.useDispatch)(x), - u = c.setInvalidBlocks, - s = c.setInvalidMeta, - f = c.setInvalidEditorChecks; - return ( - (0, i.useEffect)( - function () { - u(n); - }, - [n, u] - ), - (0, i.useEffect)( + l = (0, n.useDispatch)(o), + u = l.setInvalidBlocks, + s = l.setInvalidMeta, + f = l.setInvalidEditorChecks; + ((0, _.useEffect)( + function () { + u(i); + }, + [i, u] + ), + (0, _.useEffect)( function () { s(a); }, [a, s] ), - (0, i.useEffect)( + (0, _.useEffect)( function () { - f(l); + f(c); }, - [l, f] - ), - null - ); + [c, f] + )); } function fe() { - var e = k(), - t = 'post-editor' === e || 'post-editor-template' === e, - r = 'core/editor', - n = (0, o.useDispatch)(r), - a = wp.data && wp.data.select && wp.data.select(r), - l = (0, o.useSelect)(function (e) { - var t = e(x); - return { - invalidBlocks: t.getInvalidBlocks(), - invalidMeta: t.getInvalidMeta(), - invalidEditorChecks: t.getInvalidEditorChecks(), - }; - }, []), - c = l.invalidBlocks, - u = l.invalidMeta, - s = l.invalidEditorChecks, - f = n || {}, - d = f.lockPostSaving, - v = f.unlockPostSaving, - y = f.lockPostAutosaving, - b = f.unlockPostAutosaving, - g = f.disablePublishSidebar, - h = f.enablePublishSidebar; - return ( - (0, i.useEffect)( - function () { - if (t && 'none' !== e && a && d && v) { - var r = c.some(function (e) { - return 'error' === e.mode; - }), - n = u.some(function (e) { - return e.hasErrors; - }), - o = m(s); - r || n || o - ? (d('core/validation'), y && y('core/validation'), g && g()) - : (v('core/validation'), b && b('core/validation'), h && h()); - } - }, - [c, u, s, d, v, y, b, g, h, t, e, a] - ), - (0, i.useEffect)( - function () { - if (t && 'none' !== e && document.body) { - var r = c.some(function (e) { - return 'error' === e.mode; - }), - n = c.some(function (e) { - return 'warning' === e.mode; - }), - o = u.some(function (e) { - return e.hasErrors; - }), - i = u.some(function (e) { - return e.hasWarnings && !e.hasErrors; - }), - a = m(s), - l = p(s), - f = r || o || a, - d = !f && (n || i || l); - return ( - f - ? (document.body.classList.add('has-validation-errors'), - document.body.classList.remove('has-validation-warnings')) - : d - ? (document.body.classList.add('has-validation-warnings'), - document.body.classList.remove('has-validation-errors')) - : document.body.classList.remove( - 'has-validation-errors', - 'has-validation-warnings' - ), - function () { - document.body && - document.body.classList.remove( - 'has-validation-errors', - 'has-validation-warnings' - ); - } - ); - } - }, - [c, u, s, t, e] - ), - null - ); + return (0, n.useSelect)(function (e) { + var t = e(o); + return { + invalidBlocks: t.getInvalidBlocks(), + invalidMeta: t.getInvalidMeta(), + invalidEditorChecks: t.getInvalidEditorChecks(), + }; + }, []); } - (0, o.register)(ue); - const de = window.wp.editor, + var de = 'core/validation'; + const pe = window.wp.editor, me = window.wp.components, - pe = window.wp.i18n, - ve = window.wp.blocks; - function ye(e) { + ve = window.wp.i18n, + ye = window.wp.blocks; + function be(e) { var t = e.fill, r = void 0 === t ? 'currentColor' : t; return React.createElement( @@ -1041,20 +962,20 @@ }) ); } - function be(e, t) { + function ge(e, t) { var r = new Map(); return ( e.forEach(function (e) { - ('error' === t ? f(e.issues || []) : d(e.issues || [])).forEach(function (n) { + ('error' === t ? V(e.issues || []) : D(e.issues || [])).forEach(function (n) { var o, i, a = 'error' === t ? n.errorMsg : n.warningMsg || n.errorMsg, - l = ''.concat(e.name, '|').concat(a); - (r.has(l) || - r.set(l, { + c = ''.concat(e.name, '|').concat(a); + (r.has(c) || + r.set(c, { blockName: ((o = e.name), - (i = (0, ve.getBlockType)(o)), + (i = (0, ye.getBlockType)(o)), i && i.title ? i.title : (o.split('/')[1] || o) @@ -1068,18 +989,18 @@ clientIds: [], }), e.clientId && - !r.get(l).clientIds.includes(e.clientId) && - r.get(l).clientIds.push(e.clientId)); + !r.get(c).clientIds.includes(e.clientId) && + r.get(c).clientIds.push(e.clientId)); }); }), Array.from(r.values()) ); } - function ge(e, t) { + function he(e, t) { var r = new Map(); return ( e.forEach(function (e) { - ('error' === t ? f(e.issues || []) : d(e.issues || [])).forEach(function (n) { + ('error' === t ? V(e.issues || []) : D(e.issues || [])).forEach(function (n) { var o = 'error' === t ? n.errorMsg : n.warningMsg || n.errorMsg, i = ''.concat(e.metaKey, '|').concat(o); r.has(i) || r.set(i, { metaKey: e.metaKey, message: o }); @@ -1088,7 +1009,7 @@ Array.from(r.values()) ); } - function he(e, t) { + function we(e, t) { var r = new Map(); return ( e.forEach(function (e) { @@ -1099,38 +1020,31 @@ Array.from(r.values()) ); } - function we() { - var e = (0, o.useSelect)(function (e) { - var t = e(x); - return { - invalidBlocks: t.getInvalidBlocks(), - invalidMeta: t.getInvalidMeta(), - invalidEditorChecks: t.getInvalidEditorChecks(), - }; - }, []), + function Oe() { + var e = fe(), t = e.invalidBlocks, r = e.invalidMeta, - n = e.invalidEditorChecks, - a = (0, o.useDispatch)('core/block-editor').selectBlock, - l = (0, i.useRef)(null), - c = s(n, 'error'), - u = s(n, 'warning'), - f = be(t, 'error'), - d = be(t, 'warning'), - m = ge(r, 'error'), - p = ge(r, 'warning'), - v = he(c, 'error'), - y = he(u, 'warning'), - b = f.length + m.length + v.length, - g = d.length + p.length + y.length, - h = 'currentColor'; - b > 0 ? (h = '#d82000') : g > 0 && (h = '#dbc900'); - var w = React.createElement(ye, { fill: h }), - O = function (e) { + o = e.invalidEditorChecks, + i = (0, n.useDispatch)('core/block-editor').selectBlock, + a = (0, _.useRef)(null), + c = M(o, 'error'), + l = M(o, 'warning'), + u = ge(t, 'error'), + s = ge(t, 'warning'), + f = he(r, 'error'), + d = he(r, 'warning'), + p = we(c, 'error'), + m = we(l, 'warning'), + v = u.length + f.length + p.length, + y = s.length + d.length + m.length, + b = 'currentColor'; + v > 0 ? (b = '#d82000') : y > 0 && (b = '#dbc900'); + var g = React.createElement(be, { fill: b }), + h = function (e) { e && - (a(e), - l.current && clearTimeout(l.current), - (l.current = setTimeout(function () { + (i(e), + a.current && clearTimeout(a.current), + (a.current = setTimeout(function () { var t = document.querySelector('[data-block="'.concat(e, '"]')); (t || (t = document.querySelector( @@ -1144,34 +1058,34 @@ }, 100))); }; return ( - (0, i.useEffect)(function () { + (0, _.useEffect)(function () { return function () { - l.current && clearTimeout(l.current); + a.current && clearTimeout(a.current); }; }, []), - 0 === b && 0 === g + 0 === v && 0 === y ? null : React.createElement( - de.PluginSidebar, + pe.PluginSidebar, { name: 'validation-sidebar', - title: (0, pe.__)('Validation', 'validation-api'), - icon: w, + title: (0, ve.__)('Validation', 'validation-api'), + icon: g, className: 'validation-api-validation-sidebar', }, - b > 0 && + v > 0 && React.createElement( me.PanelBody, { - title: (0, pe.sprintf)( + title: (0, ve.sprintf)( /* translators: %d: number of errors */ /* translators: %d: number of errors */ - (0, pe.__)('Errors (%d)', 'validation-api'), - b + (0, ve.__)('Errors (%d)', 'validation-api'), + v ), initialOpen: !0, className: 'validation-api-errors-panel', }, - f.length > 0 && + u.length > 0 && React.createElement( me.PanelRow, null, @@ -1181,12 +1095,12 @@ React.createElement( 'p', { className: 'validation-api-error-subheading' }, - (0, pe.__)('Block Issues', 'validation-api') + (0, ve.__)('Block Issues', 'validation-api') ), React.createElement( 'ul', { className: 'validation-api-error-list' }, - f.map(function (e, t) { + u.map(function (e, t) { var r = e.clientIds.length, n = r > 1 ? ' (x'.concat(r, ')') : ''; return React.createElement( @@ -1199,7 +1113,7 @@ className: 'validation-api-issue-link', onClick: function () { - return O(e.clientIds[0]); + return h(e.clientIds[0]); }, }, e.blockName @@ -1212,7 +1126,7 @@ ) ) ), - m.length > 0 && + f.length > 0 && React.createElement( me.PanelRow, null, @@ -1222,12 +1136,12 @@ React.createElement( 'p', { className: 'validation-api-error-subheading' }, - (0, pe.__)('Field Issues', 'validation-api') + (0, ve.__)('Field Issues', 'validation-api') ), React.createElement( 'ul', { className: 'validation-api-error-list' }, - m.map(function (e, t) { + f.map(function (e, t) { return React.createElement( 'li', { key: 'meta-error-'.concat(t) }, @@ -1237,7 +1151,7 @@ ) ) ), - v.length > 0 && + p.length > 0 && React.createElement( me.PanelRow, null, @@ -1247,12 +1161,12 @@ React.createElement( 'p', { className: 'validation-api-error-subheading' }, - (0, pe.__)('Editor Issues', 'validation-api') + (0, ve.__)('Editor Issues', 'validation-api') ), React.createElement( 'ul', { className: 'validation-api-error-list' }, - v.map(function (e, t) { + p.map(function (e, t) { return React.createElement( 'li', { key: 'editor-error-'.concat(t) }, @@ -1263,19 +1177,19 @@ ) ) ), - g > 0 && + y > 0 && React.createElement( me.PanelBody, { - title: (0, pe.sprintf)( + title: (0, ve.sprintf)( /* translators: %d: number of warnings */ /* translators: %d: number of warnings */ - (0, pe.__)('Warnings (%d)', 'validation-api'), - g + (0, ve.__)('Warnings (%d)', 'validation-api'), + y ), initialOpen: !0, className: 'validation-api-warnings-panel', }, - d.length > 0 && + s.length > 0 && React.createElement( me.PanelRow, null, @@ -1285,12 +1199,12 @@ React.createElement( 'p', { className: 'validation-api-warning-subheading' }, - (0, pe.__)('Block Issues', 'validation-api') + (0, ve.__)('Block Issues', 'validation-api') ), React.createElement( 'ul', { className: 'validation-api-warning-list' }, - d.map(function (e, t) { + s.map(function (e, t) { var r = e.clientIds.length, n = r > 1 ? ' (x'.concat(r, ')') : ''; return React.createElement( @@ -1303,7 +1217,7 @@ className: 'validation-api-issue-link', onClick: function () { - return O(e.clientIds[0]); + return h(e.clientIds[0]); }, }, e.blockName @@ -1316,7 +1230,7 @@ ) ) ), - p.length > 0 && + d.length > 0 && React.createElement( me.PanelRow, null, @@ -1326,12 +1240,12 @@ React.createElement( 'p', { className: 'validation-api-warning-subheading' }, - (0, pe.__)('Field Issues', 'validation-api') + (0, ve.__)('Field Issues', 'validation-api') ), React.createElement( 'ul', { className: 'validation-api-warning-list' }, - p.map(function (e, t) { + d.map(function (e, t) { return React.createElement( 'li', { key: 'meta-warning-'.concat(t) }, @@ -1341,7 +1255,7 @@ ) ) ), - y.length > 0 && + m.length > 0 && React.createElement( me.PanelRow, null, @@ -1351,12 +1265,12 @@ React.createElement( 'p', { className: 'validation-api-warning-subheading' }, - (0, pe.__)('Editor Issues', 'validation-api') + (0, ve.__)('Editor Issues', 'validation-api') ), React.createElement( 'ul', { className: 'validation-api-warning-list' }, - y.map(function (e, t) { + m.map(function (e, t) { return React.createElement( 'li', { key: 'editor-warning-'.concat(t) }, @@ -1370,30 +1284,114 @@ ) ); } - (0, n.registerPlugin)('core-validation', { + function Ee() { + return (se(), null); + } + function je() { + var e, t, r, o, i, a, c, l, u, s, f, d, p; + return ( + (e = q()), + (t = 'post-editor' === e || 'post-editor-template' === e), + (r = (0, n.useDispatch)('core/editor')), + (o = r.lockPostSaving), + (i = r.unlockPostSaving), + (a = r.lockPostAutosaving), + (c = r.unlockPostAutosaving), + (l = r.disablePublishSidebar), + (u = r.enablePublishSidebar), + (s = fe()), + (f = s.invalidBlocks), + (d = s.invalidMeta), + (p = s.invalidEditorChecks), + (0, _.useEffect)( + function () { + if (t && o && i) { + var e = f.some(function (e) { + return 'error' === e.mode; + }), + r = d.some(function (e) { + return e.hasErrors; + }), + n = x(p); + e || r || n ? (o(de), a && a(de), l && l()) : (i(de), c && c(de), u && u()); + } + }, + [f, d, p, o, i, a, c, l, u, t] + ), + (0, _.useEffect)( + function () { + if (t && document.body) { + var e = f.some(function (e) { + return 'error' === e.mode; + }), + r = f.some(function (e) { + return 'warning' === e.mode; + }), + n = d.some(function (e) { + return e.hasErrors; + }), + o = d.some(function (e) { + return e.hasWarnings && !e.hasErrors; + }), + i = x(p), + a = F(p), + c = e || n || i, + l = !c && (r || o || a); + return ( + c + ? (document.body.classList.add('has-validation-errors'), + document.body.classList.remove('has-validation-warnings')) + : l + ? (document.body.classList.add('has-validation-warnings'), + document.body.classList.remove('has-validation-errors')) + : document.body.classList.remove( + 'has-validation-errors', + 'has-validation-warnings' + ), + function () { + document.body && + document.body.classList.remove( + 'has-validation-errors', + 'has-validation-warnings' + ); + } + ); + } + }, + [f, d, p, t] + ), + null + ); + } + (0, A.registerPlugin)('core-validation', { render: function () { return React.createElement( React.Fragment, null, - React.createElement(se, null), - React.createElement(fe, null), - React.createElement(we, null) + React.createElement(Ee, null), + React.createElement(je, null), + React.createElement(Oe, null) ); }, }); - const Oe = window.wp.compose, - Ee = window.wp.blockEditor; - function ke(e, t) { + const Se = window.wp.compose, + ke = window.wp.blockEditor; + function Pe(e, t) { (null == t || t > e.length) && (t = e.length); for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; return n; } - function Se(e) { + function Re(e, t) { + (null == t || t > e.length) && (t = e.length); + for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; + return n; + } + function Ie(e) { var t, r, n = e.issues, o = - ((t = (0, i.useState)(!1)), + ((t = (0, _.useState)(!1)), (r = 2), (function (e) { if (Array.isArray(e)) return e; @@ -1409,26 +1407,26 @@ o, i, a, - l = [], - c = !0, + c = [], + l = !0, u = !1; try { if (((i = (r = r.call(e)).next), 0 === t)) { if (Object(r) !== r) return; - c = !1; + l = !1; } else for ( ; - !(c = (n = i.call(r)).done) && - (l.push(n.value), l.length !== t); - c = !0 + !(l = (n = i.call(r)).done) && + (c.push(n.value), c.length !== t); + l = !0 ); } catch (e) { ((u = !0), (o = e)); } finally { try { if ( - !c && + !l && null != r.return && ((a = r.return()), Object(a) !== a) ) @@ -1437,12 +1435,12 @@ if (u) throw o; } } - return l; + return c; } })(t, r) || (function (e, t) { if (e) { - if ('string' == typeof e) return ke(e, t); + if ('string' == typeof e) return Re(e, t); var r = {}.toString.call(e).slice(8, -1); return ( 'Object' === r && e.constructor && (r = e.constructor.name), @@ -1450,7 +1448,7 @@ ? Array.from(e) : 'Arguments' === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? ke(e, t) + ? Re(e, t) : void 0 ); } @@ -1460,41 +1458,41 @@ 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' ); })()), - a = o[0], - l = o[1]; + i = o[0], + a = o[1]; if (!n || 0 === n.length) return null; - var c = m(n), - u = f(n), - s = d(n), - p = c - ? React.createElement(ye, { fill: '#d82000' }) - : React.createElement(ye, { fill: '#dbc900' }); + var c = x(n), + l = V(n), + u = D(n), + s = c + ? React.createElement(be, { fill: '#d82000' }) + : React.createElement(be, { fill: '#dbc900' }); return React.createElement( React.Fragment, null, React.createElement(me.ToolbarButton, { - icon: p, + icon: s, onClick: function () { - return l(!0); + return a(!0); }, - label: (0, pe.__)('View block issues or concerns', 'validation-api'), + label: (0, ve.__)('View block issues or concerns', 'validation-api'), className: 'validation-api-toolbar-button', isCompact: !0, }), - a && + i && React.createElement( me.Modal, { - title: (0, pe.__)('Issues or Concerns', 'validation-api'), + title: (0, ve.__)('Issues or Concerns', 'validation-api'), onRequestClose: function () { - return l(!1); + return a(!1); }, className: 'validation-api-block-indicator-modal', }, React.createElement( 'div', { className: 'validation-api-indicator-modal-content' }, - u.length > 0 && + l.length > 0 && React.createElement( 'div', { @@ -1507,12 +1505,12 @@ React.createElement('span', { className: 'validation-api-indicator-section-title-circle', }), - (0, pe.__)('Errors', 'validation-api') + (0, ve.__)('Errors', 'validation-api') ), React.createElement( 'ul', null, - u.map(function (e, t) { + l.map(function (e, t) { return React.createElement( 'li', { key: 'error-'.concat(t) }, @@ -1521,7 +1519,7 @@ }) ) ), - s.length > 0 && + u.length > 0 && React.createElement( 'div', { @@ -1534,12 +1532,12 @@ React.createElement('span', { className: 'validation-api-indicator-section-title-circle', }), - (0, pe.__)('Warnings', 'validation-api') + (0, ve.__)('Warnings', 'validation-api') ), React.createElement( 'ul', null, - s.map(function (e, t) { + u.map(function (e, t) { return React.createElement( 'li', { key: 'warning-'.concat(t) }, @@ -1552,14 +1550,9 @@ ) ); } - function je(e, t) { - (null == t || t > e.length) && (t = e.length); - for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; - return n; - } - function Pe(e) { + function Ae(e) { return ( - (Pe = + (Ae = 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator ? function (e) { return typeof e; @@ -1572,10 +1565,10 @@ ? 'symbol' : typeof e; }), - Pe(e) + Ae(e) ); } - function Re(e, t) { + function _e(e, t) { var r = Object.keys(e); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); @@ -1587,35 +1580,35 @@ } return r; } - function Ie(e) { + function Be(e) { for (var t = 1; t < arguments.length; t++) { var r = null != arguments[t] ? arguments[t] : {}; t % 2 - ? Re(Object(r), !0).forEach(function (t) { - Ae(e, t, r[t]); + ? _e(Object(r), !0).forEach(function (t) { + Ne(e, t, r[t]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : Re(Object(r)).forEach(function (t) { + : _e(Object(r)).forEach(function (t) { Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); }); } return e; } - function Ae(e, t, r) { + function Ne(e, t, r) { return ( (t = (function (e) { var t = (function (e) { - if ('object' != Pe(e) || !e) return e; + if ('object' != Ae(e) || !e) return e; var t = e[Symbol.toPrimitive]; if (void 0 !== t) { var r = t.call(e, 'string'); - if ('object' != Pe(r)) return r; + if ('object' != Ae(r)) return r; throw new TypeError('@@toPrimitive must return a primitive value.'); } return String(e); })(e); - return 'symbol' == Pe(t) ? t : t + ''; + return 'symbol' == Ae(t) ? t : t + ''; })(t)) in e ? Object.defineProperty(e, t, { value: r, @@ -1627,27 +1620,27 @@ e ); } - var Be = (0, Oe.createHigherOrderComponent)(function (e) { + var Te = (0, Se.createHigherOrderComponent)(function (e) { return function (t) { var r = t.clientId, - n = t.attributes, - a = (0, o.useSelect)( + i = t.attributes, + a = (0, n.useSelect)( function (e) { return e('core/block-editor').getBlock(r); }, [r] ), - l = (0, o.useDispatch)(x), - c = l.setBlockValidation, - u = l.clearBlockValidation, + c = (0, n.useDispatch)(o), + l = c.setBlockValidation, + u = c.clearBlockValidation, s = (function (e, t) { var r, n, o = (arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}) .delay, - a = void 0 === o ? 300 : o, - l = - ((r = (0, i.useState)(function () { + i = void 0 === o ? 300 : o, + a = + ((r = (0, _.useState)(function () { return e(); })), (n = 2), @@ -1666,26 +1659,26 @@ o, i, a, - l = [], - c = !0, + c = [], + l = !0, u = !1; try { if (((i = (r = r.call(e)).next), 0 === t)) { if (Object(r) !== r) return; - c = !1; + l = !1; } else for ( ; - !(c = (n = i.call(r)).done) && - (l.push(n.value), l.length !== t); - c = !0 + !(l = (n = i.call(r)).done) && + (c.push(n.value), c.length !== t); + l = !0 ); } catch (e) { ((u = !0), (o = e)); } finally { try { if ( - !c && + !l && null != r.return && ((a = r.return()), Object(a) !== a) ) @@ -1694,12 +1687,12 @@ if (u) throw o; } } - return l; + return c; } })(r, n) || (function (e, t) { if (e) { - if ('string' == typeof e) return je(e, t); + if ('string' == typeof e) return Pe(e, t); var r = {}.toString.call(e).slice(8, -1); return ( 'Object' === r && @@ -1711,7 +1704,7 @@ /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test( r ) - ? je(e, t) + ? Pe(e, t) : void 0 ); } @@ -1721,20 +1714,20 @@ 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' ); })()), - c = l[0], - u = l[1], - s = (0, i.useRef)(null), - f = (0, i.useRef)(!0); + c = a[0], + l = a[1], + u = (0, _.useRef)(null), + s = (0, _.useRef)(!0); return ( - (0, i.useEffect)(function () { - return f.current - ? ((f.current = !1), void u(e())) - : (s.current && clearTimeout(s.current), - (s.current = setTimeout(function () { - u(e()); - }, a)), + (0, _.useEffect)(function () { + return s.current + ? ((s.current = !1), void l(e())) + : (u.current && clearTimeout(u.current), + (u.current = setTimeout(function () { + l(e()); + }, i)), function () { - s.current && clearTimeout(s.current); + u.current && clearTimeout(u.current); }); }, t), c @@ -1742,23 +1735,23 @@ })( function () { if (!a) return { isValid: !0, issues: [], mode: 'none' }; - var e = Ie(Ie({}, a), {}, { attributes: n || a.attributes }); - return w(e); + var e = Be(Be({}, a), {}, { attributes: i || a.attributes }); + return Z(e); }, - [a, n], + [a, i], { delay: 300 } ); return ( - (0, i.useEffect)( + (0, _.useEffect)( function () { return ( - c(r, s), + l(r, s), function () { return u(r); } ); }, - [r, s, c, u] + [r, s, l, u] ), React.createElement( React.Fragment, @@ -1766,17 +1759,17 @@ React.createElement(e, t), !s.isValid && React.createElement( - Ee.BlockControls, + ke.BlockControls, { group: 'block' }, - React.createElement(Se, { issues: s.issues }) + React.createElement(Ie, { issues: s.issues }) ) ) ); }; }, 'withErrorHandling'); - function Ne(e) { + function Ce(e) { return ( - (Ne = + (Ce = 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator ? function (e) { return typeof e; @@ -1789,12 +1782,12 @@ ? 'symbol' : typeof e; }), - Ne(e) + Ce(e) ); } - function Ce() { + function Le() { return ( - (Ce = Object.assign + (Le = Object.assign ? Object.assign.bind() : function (e) { for (var t = 1; t < arguments.length; t++) { @@ -1803,10 +1796,10 @@ } return e; }), - Ce.apply(null, arguments) + Le.apply(null, arguments) ); } - function _e(e, t) { + function Me(e, t) { var r = Object.keys(e); if (Object.getOwnPropertySymbols) { var n = Object.getOwnPropertySymbols(e); @@ -1818,35 +1811,35 @@ } return r; } - function Le(e) { + function Ve(e) { for (var t = 1; t < arguments.length; t++) { var r = null != arguments[t] ? arguments[t] : {}; t % 2 - ? _e(Object(r), !0).forEach(function (t) { - Me(e, t, r[t]); + ? Me(Object(r), !0).forEach(function (t) { + De(e, t, r[t]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : _e(Object(r)).forEach(function (t) { + : Me(Object(r)).forEach(function (t) { Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); }); } return e; } - function Me(e, t, r) { + function De(e, t, r) { return ( (t = (function (e) { var t = (function (e) { - if ('object' != Ne(e) || !e) return e; + if ('object' != Ce(e) || !e) return e; var t = e[Symbol.toPrimitive]; if (void 0 !== t) { var r = t.call(e, 'string'); - if ('object' != Ne(r)) return r; + if ('object' != Ce(r)) return r; throw new TypeError('@@toPrimitive must return a primitive value.'); } return String(e); })(e); - return 'symbol' == Ne(t) ? t : t + ''; + return 'symbol' == Ce(t) ? t : t + ''; })(t)) in e ? Object.defineProperty(e, t, { value: r, @@ -1858,31 +1851,246 @@ e ); } - (wp.hooks.addFilter('editor.BlockEdit', 'validation-api/with-error-handling', Be), - (0, g.addFilter)( + function xe() { + var e, + t, + r = 'function' == typeof Symbol ? Symbol : {}, + n = r.iterator || '@@iterator', + o = r.toStringTag || '@@toStringTag'; + function i(r, n, o, i) { + var l = n && n.prototype instanceof c ? n : c, + u = Object.create(l.prototype); + return ( + Fe( + u, + '_invoke', + (function (r, n, o) { + var i, + c, + l, + u = 0, + s = o || [], + f = !1, + d = { + p: 0, + n: 0, + v: e, + a: p, + f: p.bind(e, 4), + d: function (t, r) { + return ((i = t), (c = 0), (l = e), (d.n = r), a); + }, + }; + function p(r, n) { + for (c = r, l = n, t = 0; !f && u && !o && t < s.length; t++) { + var o, + i = s[t], + p = d.p, + m = i[2]; + r > 3 + ? (o = m === n) && + ((l = i[(c = i[4]) ? 5 : ((c = 3), 3)]), (i[4] = i[5] = e)) + : i[0] <= p && + ((o = r < 2 && p < i[1]) + ? ((c = 0), (d.v = n), (d.n = i[1])) + : p < m && + (o = r < 3 || i[0] > n || n > m) && + ((i[4] = r), (i[5] = n), (d.n = m), (c = 0))); + } + if (o || r > 1) return a; + throw ((f = !0), n); + } + return function (o, s, m) { + if (u > 1) throw TypeError('Generator is already running'); + for ( + f && 1 === s && p(s, m), c = s, l = m; + (t = c < 2 ? e : l) || !f; + ) { + i || + (c + ? c < 3 + ? (c > 1 && (d.n = -1), p(c, l)) + : (d.n = l) + : (d.v = l)); + try { + if (((u = 2), i)) { + if ((c || (o = 'next'), (t = i[o]))) { + if (!(t = t.call(i, l))) + throw TypeError('iterator result is not an object'); + if (!t.done) return t; + ((l = t.value), c < 2 && (c = 0)); + } else + (1 === c && (t = i.return) && t.call(i), + c < 2 && + ((l = TypeError( + "The iterator does not provide a '" + + o + + "' method" + )), + (c = 1))); + i = e; + } else if ((t = (f = d.n < 0) ? l : r.call(n, d)) !== a) break; + } catch (t) { + ((i = e), (c = 1), (l = t)); + } finally { + u = 1; + } + } + return { value: t, done: f }; + }; + })(r, o, i), + !0 + ), + u + ); + } + var a = {}; + function c() {} + function l() {} + function u() {} + t = Object.getPrototypeOf; + var s = [][n] + ? t(t([][n]())) + : (Fe((t = {}), n, function () { + return this; + }), + t), + f = (u.prototype = c.prototype = Object.create(s)); + function d(e) { + return ( + Object.setPrototypeOf + ? Object.setPrototypeOf(e, u) + : ((e.__proto__ = u), Fe(e, o, 'GeneratorFunction')), + (e.prototype = Object.create(f)), + e + ); + } + return ( + (l.prototype = u), + Fe(f, 'constructor', u), + Fe(u, 'constructor', l), + (l.displayName = 'GeneratorFunction'), + Fe(u, o, 'GeneratorFunction'), + Fe(f), + Fe(f, o, 'Generator'), + Fe(f, n, function () { + return this; + }), + Fe(f, 'toString', function () { + return '[object Generator]'; + }), + (xe = function () { + return { w: i, m: d }; + })() + ); + } + function Fe(e, t, r, n) { + var o = Object.defineProperty; + try { + o({}, '', {}); + } catch (e) { + o = 0; + } + ((Fe = function (e, t, r, n) { + function i(t, r) { + Fe(e, t, function (e) { + return this._invoke(t, r, e); + }); + } + t + ? o + ? o(e, t, { value: r, enumerable: !n, configurable: !n, writable: !n }) + : (e[t] = r) + : (i('next', 0), i('throw', 1), i('return', 2)); + }), + Fe(e, t, r, n)); + } + function Ke(e, t, r, n, o, i, a) { + try { + var c = e[i](a), + l = c.value; + } catch (e) { + return void r(e); + } + c.done ? t(l) : Promise.resolve(l).then(n, o); + } + ((0, B.addFilter)('editor.BlockEdit', 'validation-api/with-error-handling', Te), + (0, B.addFilter)( 'editor.BlockListBlock', 'validation-api/with-block-validation-classes', function (e) { return function (t) { - var r = (0, o.useSelect)( + var r = (0, n.useSelect)( function (e) { - return e(x).getBlockValidation(t.clientId); + return e(o).getBlockValidation(t.clientId); }, [t.clientId] ); if ('none' === r.mode) return React.createElement(e, t); - var n = + var i = 'error' === r.mode ? 'validation-api-block-error' : 'validation-api-block-warning', - i = t.wrapperProps || {}, - a = Le( - Le({}, i), + a = t.wrapperProps || {}, + c = Ve( + Ve({}, a), {}, - { className: [i.className, n].filter(Boolean).join(' ') } + { className: [a.className, i].filter(Boolean).join(' ') } ); - return React.createElement(e, Ce({}, t, { wrapperProps: a })); + return React.createElement(e, Le({}, t, { wrapperProps: c })); }; } + ), + (0, B.addFilter)( + 'editor.preSavePost', + 'validation-api/pre-save-gate', + (function () { + var e, + t = + ((e = xe().m(function e(t) { + var r; + return xe().w(function (e) { + for (;;) + switch (e.n) { + case 0: + if ( + !( + (r = (0, n.select)(o)) && + r.hasErrors && + r.hasErrors() + ) + ) { + e.n = 1; + break; + } + throw new Error( + (0, ve.__)( + 'Validation errors must be resolved before saving.', + 'validation-api' + ) + ); + case 1: + return e.a(2, t); + } + }, e); + })), + function () { + var t = this, + r = arguments; + return new Promise(function (n, o) { + var i = e.apply(t, r); + function a(e) { + Ke(i, n, o, a, c, 'next', e); + } + function c(e) { + Ke(i, n, o, a, c, 'throw', e); + } + a(void 0); + }); + }); + return function (_x) { + return t.apply(this, arguments); + }; + })() )); })(); diff --git a/package.json b/package.json index 7385fe9..22f7dc7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,13 @@ "description": "A framework for registering block, meta, and editor validation checks in the WordPress block editor", "author": "Troy Chaplin", "license": "GPL-2.0-or-later", - "main": "build/index.js", + "main": "build/validation-api.js", + "sideEffects": [ + "src/index.js", + "src/hooks/**", + "src/store/index.js", + "src/**/*.scss" + ], "packageManager": "pnpm@10.33.0", "lint-staged": { "*.{css,scss}": [ diff --git a/src/editor/components/ValidationIcon.js b/src/components/validation-icon/index.js similarity index 97% rename from src/editor/components/ValidationIcon.js rename to src/components/validation-icon/index.js index ee345f3..caee645 100644 --- a/src/editor/components/ValidationIcon.js +++ b/src/components/validation-icon/index.js @@ -25,3 +25,5 @@ export function ValidationIcon({ fill = 'currentColor' }) { ); } + +export default ValidationIcon; diff --git a/src/editor/components/ValidationSidebar.js b/src/components/validation-sidebar/index.js similarity index 96% rename from src/editor/components/ValidationSidebar.js rename to src/components/validation-sidebar/index.js index d023d32..8c9de98 100644 --- a/src/editor/components/ValidationSidebar.js +++ b/src/components/validation-sidebar/index.js @@ -4,16 +4,16 @@ import { PluginSidebar } from '@wordpress/editor'; import { PanelBody, PanelRow } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useDispatch } from '@wordpress/data'; import { useEffect, useRef } from '@wordpress/element'; import { getBlockType } from '@wordpress/blocks'; /** * Internal dependencies */ -import { ValidationIcon } from './ValidationIcon'; -import { STORE_NAME } from '../store'; -import { filterIssuesByType, getErrors, getWarnings } from '../../shared/utils/validation'; +import { ValidationIcon } from '../validation-icon'; +import { filterIssuesByType, getErrors, getWarnings } from '../../utils/issue-helpers'; +import { useValidationIssues } from '../../utils/use-validation-issues'; /** * Get display name for a block type @@ -154,15 +154,7 @@ function deduplicateEditorIssues(issues, severity) { * The icon color reflects the highest severity issue present (red for errors, yellow for warnings). */ export function ValidationSidebar() { - // Read validation results from the centralized store - const { invalidBlocks, invalidMeta, invalidEditorChecks } = useSelect(select => { - const store = select(STORE_NAME); - return { - invalidBlocks: store.getInvalidBlocks(), - invalidMeta: store.getInvalidMeta(), - invalidEditorChecks: store.getInvalidEditorChecks(), - }; - }, []); + const { invalidBlocks, invalidMeta, invalidEditorChecks } = useValidationIssues(); // Get dispatch function to select blocks when user clicks on issues const { selectBlock } = useDispatch('core/block-editor'); @@ -437,3 +429,5 @@ export function ValidationSidebar() { ); } + +export default ValidationSidebar; diff --git a/src/editor/components/ValidationToolbarButton.js b/src/components/validation-toolbar-button/index.js similarity index 93% rename from src/editor/components/ValidationToolbarButton.js rename to src/components/validation-toolbar-button/index.js index 28fcc6b..81fe305 100644 --- a/src/editor/components/ValidationToolbarButton.js +++ b/src/components/validation-toolbar-button/index.js @@ -8,8 +8,8 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { ValidationIcon } from './ValidationIcon'; -import { hasErrors, getErrors, getWarnings } from '../../shared/utils/validation'; +import { ValidationIcon } from '../validation-icon'; +import { hasErrors, getErrors, getWarnings } from '../../utils/issue-helpers'; /** * Validation Toolbar Button @@ -92,3 +92,5 @@ export function ValidationToolbarButton({ issues }) { ); } + +export default ValidationToolbarButton; diff --git a/src/editor/components/ValidationProvider.js b/src/editor/components/ValidationProvider.js deleted file mode 100644 index c560b7d..0000000 --- a/src/editor/components/ValidationProvider.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * WordPress dependencies - */ -import { useDispatch } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { - GetInvalidBlocks, - GetInvalidMeta, - GetInvalidEditorChecks, -} from '../../shared/utils/validation'; -import { STORE_NAME } from '../store'; - -/** - * Validation Provider Component - * - * Renderless component that computes validation state from the three core hooks - * and dispatches the results into the validation-api data store. This is the - * single place where validation is computed — all other consumers read from - * the store via selectors. - */ -export function ValidationProvider() { - const invalidBlocks = GetInvalidBlocks(); - const invalidMeta = GetInvalidMeta(); - const invalidEditorChecks = GetInvalidEditorChecks(); - - const { setInvalidBlocks, setInvalidMeta, setInvalidEditorChecks } = useDispatch(STORE_NAME); - - useEffect(() => { - setInvalidBlocks(invalidBlocks); - }, [invalidBlocks, setInvalidBlocks]); - - useEffect(() => { - setInvalidMeta(invalidMeta); - }, [invalidMeta, setInvalidMeta]); - - useEffect(() => { - setInvalidEditorChecks(invalidEditorChecks); - }, [invalidEditorChecks, setInvalidEditorChecks]); - - return null; -} diff --git a/src/editor/components/index.js b/src/editor/components/index.js deleted file mode 100644 index 324ae23..0000000 --- a/src/editor/components/index.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Editor Components - * - * Barrel export for editor UI components. - */ - -export { ValidationIcon } from './ValidationIcon'; -export { ValidationSidebar } from './ValidationSidebar'; -export { ValidationToolbarButton } from './ValidationToolbarButton'; diff --git a/src/editor/register.js b/src/editor/register.js deleted file mode 100644 index 6aa494a..0000000 --- a/src/editor/register.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * WordPress dependencies - */ -import { registerPlugin } from '@wordpress/plugins'; - -/** - * Internal dependencies - */ -import { ValidationProvider } from './components/ValidationProvider'; -import { ValidationAPI } from './validation/ValidationAPI'; -import { ValidationSidebar } from './components/ValidationSidebar'; - -/** - * Register the validation plugin with WordPress - * - * This plugin registration activates the validation system in the block editor, - * rendering both the ValidationAPI (which handles validation logic and state) - * and the ValidationSidebar (which displays validation results to users). - * Both components are rendered together to provide a complete validation experience. - */ -registerPlugin('core-validation', { - render: () => ( - <> - - - - - ), -}); diff --git a/src/editor/validation/ValidationAPI.js b/src/editor/validation/ValidationAPI.js deleted file mode 100644 index e41a019..0000000 --- a/src/editor/validation/ValidationAPI.js +++ /dev/null @@ -1,189 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import { STORE_NAME } from '../store'; -import { - hasErrors as issueHasErrors, - hasWarnings as issueHasWarnings, - getEditorContext, -} from '../../shared/utils/validation'; - -/** - * Validation API Component - * - * Central component that orchestrates validation across blocks, meta fields, and editor checks. - * Manages post/template saving restrictions and body classes based on validation results. When - * errors are detected, prevents saving/autosaving and disables the publish sidebar to ensure - * content meets accessibility requirements before publication. - * - * Supports post editor contexts only: - * - post-editor: Default post/page editing - * - post-editor-template: Post/page editing with template visible - * - * This component doesn't render any UI but manages validation state and editor behavior. - */ -export function ValidationAPI() { - // Get the editor context from editor settings - const editorContext = getEditorContext(); - - // Check if we're in a supported editor context (post editor only) - const isValidContext = - editorContext === 'post-editor' || editorContext === 'post-editor-template'; - - // IMPORTANT: lockPostSaving/unlockPostSaving are ONLY in 'core/editor' - const editorStore = 'core/editor'; - - // Call useDispatch unconditionally (React Hook rules - must be before any early returns) - const dispatch = useDispatch(editorStore); - - // Verify the store exists before using it - const storeExists = wp.data && wp.data.select && wp.data.select(editorStore); - - // Read validation results from the centralized store - const { invalidBlocks, invalidMeta, invalidEditorChecks } = useSelect(select => { - const store = select(STORE_NAME); - return { - invalidBlocks: store.getInvalidBlocks(), - invalidMeta: store.getInvalidMeta(), - invalidEditorChecks: store.getInvalidEditorChecks(), - }; - }, []); - - // Destructure functions - these exist in core/editor for both contexts - const { - lockPostSaving, - unlockPostSaving, - lockPostAutosaving, - unlockPostAutosaving, - disablePublishSidebar, - enablePublishSidebar, - } = dispatch || {}; - - /** - * Manage post/template saving restrictions based on validation errors - * - * Monitors validation results from blocks, meta fields, and editor checks. - * When any errors are detected, locks both manual and automatic saving - * and disables the publish sidebar. This prevents publishing content with - * accessibility issues. When all errors are resolved, re-enables saving. - * - * Works in post editor contexts only. - */ - useEffect(() => { - // Exit early if not in a supported context or store doesn't exist - if (!isValidContext || editorContext === 'none' || !storeExists) { - return; - } - - // Verify we have the necessary dispatch functions - if (!lockPostSaving || !unlockPostSaving) { - return; - } - - // Check for errors across all validation types - const hasBlockErrors = invalidBlocks.some(block => block.mode === 'error'); - const hasMetaErrors = invalidMeta.some(meta => meta.hasErrors); - const hasEditorErrors = issueHasErrors(invalidEditorChecks); - - // Lock saving if any validation errors exist - if (hasBlockErrors || hasMetaErrors || hasEditorErrors) { - lockPostSaving('core/validation'); - if (lockPostAutosaving) { - lockPostAutosaving('core/validation'); - } - if (disablePublishSidebar) { - disablePublishSidebar(); - } - } else { - // Re-enable saving when all errors are resolved - unlockPostSaving('core/validation'); - if (unlockPostAutosaving) { - unlockPostAutosaving('core/validation'); - } - if (enablePublishSidebar) { - enablePublishSidebar(); - } - } - }, [ - invalidBlocks, - invalidMeta, - invalidEditorChecks, - lockPostSaving, - unlockPostSaving, - lockPostAutosaving, - unlockPostAutosaving, - disablePublishSidebar, - enablePublishSidebar, - isValidContext, - editorContext, - storeExists, - ]); - - /** - * Manage body classes for validation state styling - * - * Adds CSS classes to the document body based on validation results from blocks, - * meta fields, and editor checks. These classes enable theme/plugin developers to - * style the editor interface based on validation state (e.g., highlighting - * areas with issues). Classes are removed when validation passes or component unmounts. - * - * Works in post editor contexts only. - */ - useEffect(() => { - // Exit early if not in a supported context - if (!isValidContext || editorContext === 'none') { - return; - } - - // Ensure document.body is available before manipulating classes - if (!document.body) { - return; - } - - // Check for errors and warnings across all validation types - const hasBlockErrors = invalidBlocks.some(block => block.mode === 'error'); - const hasBlockWarnings = invalidBlocks.some(block => block.mode === 'warning'); - const hasMetaErrors = invalidMeta.some(meta => meta.hasErrors); - const hasMetaWarnings = invalidMeta.some(meta => meta.hasWarnings && !meta.hasErrors); - const hasEditorErrors = issueHasErrors(invalidEditorChecks); - const hasEditorWarnings = issueHasWarnings(invalidEditorChecks); - - // Check for overall errors first (blocks, meta, or editor) - const hasAnyErrors = hasBlockErrors || hasMetaErrors || hasEditorErrors; - - // Check for overall warnings only if no errors exist - const hasAnyWarnings = - !hasAnyErrors && (hasBlockWarnings || hasMetaWarnings || hasEditorWarnings); - - // Apply error class if errors exist - if (hasAnyErrors) { - document.body.classList.add('has-validation-errors'); - document.body.classList.remove('has-validation-warnings'); - } - // Apply warning class only if no errors but warnings exist - else if (hasAnyWarnings) { - document.body.classList.add('has-validation-warnings'); - document.body.classList.remove('has-validation-errors'); - } - // Remove both classes if no issues - else { - document.body.classList.remove('has-validation-errors', 'has-validation-warnings'); - } - - // Cleanup: Remove classes when component unmounts - return () => { - if (document.body) { - document.body.classList.remove('has-validation-errors', 'has-validation-warnings'); - } - }; - }, [invalidBlocks, invalidMeta, invalidEditorChecks, isValidContext, editorContext]); - - // This component manages side effects only, no UI rendering - return null; -} diff --git a/src/editor/validation/blocks/index.js b/src/editor/validation/blocks/index.js deleted file mode 100644 index 5139293..0000000 --- a/src/editor/validation/blocks/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Block Validation - * - * Barrel export for block validation code. - */ - -export { validateBlock } from './validateBlock'; diff --git a/src/editor/validation/editor/index.js b/src/editor/validation/editor/index.js deleted file mode 100644 index 7b55596..0000000 --- a/src/editor/validation/editor/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Editor Validation - * - * Barrel export for editor-level validation code. - */ - -export { validateEditor } from './validateEditor'; diff --git a/src/editor/validation/meta/hooks/index.js b/src/editor/validation/meta/hooks/index.js deleted file mode 100644 index e8b3d3d..0000000 --- a/src/editor/validation/meta/hooks/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Meta Validation Hooks - * - * Barrel export for meta validation hooks. - */ - -export { useMetaField } from './useMetaField'; -export { useMetaValidation } from './useMetaValidation'; diff --git a/src/editor/hoc/withBlockValidationClasses.js b/src/hooks/block-validation-classes.js similarity index 85% rename from src/editor/hoc/withBlockValidationClasses.js rename to src/hooks/block-validation-classes.js index 7366492..92b2b48 100644 --- a/src/editor/hoc/withBlockValidationClasses.js +++ b/src/hooks/block-validation-classes.js @@ -1,3 +1,10 @@ +/** + * Side-effect module. Adds the `editor.BlockListBlock` filter that injects + * CSS classes onto each block's wrapper based on its validation state. + * + * Imported for side effects from src/hooks/index.js. + */ + /** * WordPress dependencies */ @@ -7,7 +14,7 @@ import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { STORE_NAME } from '../store'; +import { STORE_NAME } from '../store/constants'; /** * Adds validation CSS classes to the block's own wrapper element. diff --git a/src/hooks/index.js b/src/hooks/index.js new file mode 100644 index 0000000..7babd40 --- /dev/null +++ b/src/hooks/index.js @@ -0,0 +1,13 @@ +/** + * Side-effect imports. Each module registers a filter, slot, or plugin at + * import time; nothing here is re-exported. + * + * Note: `use-validation-sync` and `use-validation-lifecycle` are React + * hooks (not side-effect modules), so they are NOT imported here — they + * are invoked by the `ValidationPlugin` component in `register-sidebar.js`. + */ + +import './register-sidebar'; +import './validate-block'; +import './block-validation-classes'; +import './pre-save-validation'; diff --git a/src/hooks/pre-save-validation.js b/src/hooks/pre-save-validation.js new file mode 100644 index 0000000..37befa3 --- /dev/null +++ b/src/hooks/pre-save-validation.js @@ -0,0 +1,32 @@ +/** + * Side-effect module. Adds the `editor.preSavePost` async filter as a + * save-time safety net layered on top of `lockPostSaving`. + * + * If any error-level validation failures are present in the store at save + * time, the save is aborted by throwing from the filter callback. In the + * happy path this never fires — `useValidationLifecycle` locks saving + * reactively. This hook catches edge cases where the lock may not have + * propagated (race conditions, direct dispatches, etc.). + * + * Imported for side effects from src/hooks/index.js. + */ + +/** + * WordPress dependencies + */ +import { addFilter } from '@wordpress/hooks'; +import { select } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from '../store/constants'; + +addFilter('editor.preSavePost', 'validation-api/pre-save-gate', async edits => { + const validationStore = select(STORE_NAME); + if (validationStore && validationStore.hasErrors && validationStore.hasErrors()) { + throw new Error(__('Validation errors must be resolved before saving.', 'validation-api')); + } + return edits; +}); diff --git a/src/hooks/register-sidebar.js b/src/hooks/register-sidebar.js new file mode 100644 index 0000000..9b25163 --- /dev/null +++ b/src/hooks/register-sidebar.js @@ -0,0 +1,53 @@ +/** + * Side-effect module. Registers the validation plugin with the block editor. + * + * The registered plugin mounts three siblings: + * - : invokes useValidationSync, returns null + * - : invokes useValidationLifecycle, returns null + * - : renders the sidebar (null when no issues) + * + * Siblings (rather than two hooks inside a single parent component) avoid an + * infinite-render loop: useValidationSync dispatches to the core/validation + * store; useValidationLifecycle subscribes to it. Putting both hooks in the + * same component causes the dispatch to re-render that component, which + * re-runs the sync hook with fresh array references → another dispatch → loop. + * Keeping them as siblings isolates their render cycles. + * + * Imported for side effects from src/hooks/index.js. + */ + +/** + * WordPress dependencies + */ +import { registerPlugin } from '@wordpress/plugins'; + +/** + * Internal dependencies + */ +import { useValidationSync } from './use-validation-sync'; +import { useValidationLifecycle } from './use-validation-lifecycle'; +import { ValidationSidebar } from '../components/validation-sidebar'; + +function ValidationSync() { + useValidationSync(); + return null; +} + +function ValidationLifecycle() { + useValidationLifecycle(); + return null; +} + +function ValidationPlugin() { + return ( + <> + + + + + ); +} + +registerPlugin('core-validation', { + render: ValidationPlugin, +}); diff --git a/src/hooks/use-validation-lifecycle.js b/src/hooks/use-validation-lifecycle.js new file mode 100644 index 0000000..636966d --- /dev/null +++ b/src/hooks/use-validation-lifecycle.js @@ -0,0 +1,133 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { useValidationIssues } from '../utils/use-validation-issues'; +import { + hasErrors as issueHasErrors, + hasWarnings as issueHasWarnings, +} from '../utils/issue-helpers'; +import { getEditorContext } from '../utils/get-validation-config'; + +const LOCK_NAME = 'core/validation'; + +/** + * Side-effect hook that reads aggregate validation state from the store and + * orchestrates editor-wide consequences: + * + * - Locks/unlocks post saving, autosaving, and the publish sidebar based on + * whether any error-level validation failures exist. + * - Applies body classes (`has-validation-errors`, `has-validation-warnings`) + * for plugins/themes that want to style the editor based on state. + * + * Replaces the former renderless component. Call this hook + * once from a top-level component (see src/hooks/register-sidebar.js). + * + * Only active in post editor contexts (default and template views). The site + * editor is intentionally excluded. + */ +export function useValidationLifecycle() { + const editorContext = getEditorContext(); + const isValidContext = + editorContext === 'post-editor' || editorContext === 'post-editor-template'; + + const { + lockPostSaving, + unlockPostSaving, + lockPostAutosaving, + unlockPostAutosaving, + disablePublishSidebar, + enablePublishSidebar, + } = useDispatch('core/editor'); + + const { invalidBlocks, invalidMeta, invalidEditorChecks } = useValidationIssues(); + + /** + * Manage post saving restrictions based on validation errors. + */ + useEffect(() => { + if (!isValidContext) { + return; + } + if (!lockPostSaving || !unlockPostSaving) { + return; + } + + const hasBlockErrors = invalidBlocks.some(block => block.mode === 'error'); + const hasMetaErrors = invalidMeta.some(meta => meta.hasErrors); + const hasEditorErrors = issueHasErrors(invalidEditorChecks); + + if (hasBlockErrors || hasMetaErrors || hasEditorErrors) { + lockPostSaving(LOCK_NAME); + if (lockPostAutosaving) { + lockPostAutosaving(LOCK_NAME); + } + if (disablePublishSidebar) { + disablePublishSidebar(); + } + } else { + unlockPostSaving(LOCK_NAME); + if (unlockPostAutosaving) { + unlockPostAutosaving(LOCK_NAME); + } + if (enablePublishSidebar) { + enablePublishSidebar(); + } + } + }, [ + invalidBlocks, + invalidMeta, + invalidEditorChecks, + lockPostSaving, + unlockPostSaving, + lockPostAutosaving, + unlockPostAutosaving, + disablePublishSidebar, + enablePublishSidebar, + isValidContext, + ]); + + /** + * Manage body classes for validation state styling. + */ + useEffect(() => { + if (!isValidContext) { + return; + } + if (!document.body) { + return; + } + + const hasBlockErrors = invalidBlocks.some(block => block.mode === 'error'); + const hasBlockWarnings = invalidBlocks.some(block => block.mode === 'warning'); + const hasMetaErrors = invalidMeta.some(meta => meta.hasErrors); + const hasMetaWarnings = invalidMeta.some(meta => meta.hasWarnings && !meta.hasErrors); + const hasEditorErrors = issueHasErrors(invalidEditorChecks); + const hasEditorWarnings = issueHasWarnings(invalidEditorChecks); + + const hasAnyErrors = hasBlockErrors || hasMetaErrors || hasEditorErrors; + const hasAnyWarnings = + !hasAnyErrors && (hasBlockWarnings || hasMetaWarnings || hasEditorWarnings); + + if (hasAnyErrors) { + document.body.classList.add('has-validation-errors'); + document.body.classList.remove('has-validation-warnings'); + } else if (hasAnyWarnings) { + document.body.classList.add('has-validation-warnings'); + document.body.classList.remove('has-validation-errors'); + } else { + document.body.classList.remove('has-validation-errors', 'has-validation-warnings'); + } + + return () => { + if (document.body) { + document.body.classList.remove('has-validation-errors', 'has-validation-warnings'); + } + }; + }, [invalidBlocks, invalidMeta, invalidEditorChecks, isValidContext]); +} diff --git a/src/hooks/use-validation-sync.js b/src/hooks/use-validation-sync.js new file mode 100644 index 0000000..c9de5ac --- /dev/null +++ b/src/hooks/use-validation-sync.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from '../store/constants'; +import { useInvalidBlocks } from '../utils/use-invalid-blocks'; +import { useInvalidMeta } from '../utils/use-invalid-meta'; +import { useInvalidEditorChecks } from '../utils/use-invalid-editor-checks'; + +/** + * Computes validation state across blocks, meta, and editor checks and syncs + * the results into the `core/validation` data store. + * + * This is the single computation point; all other consumers read from the + * store via selectors rather than running the three hooks independently. + * + * Replaces the former renderless component. Call this + * hook once from a top-level component that lives as long as the editor is + * mounted (see src/hooks/register-sidebar.js). + */ +export function useValidationSync() { + const invalidBlocks = useInvalidBlocks(); + const invalidMeta = useInvalidMeta(); + const invalidEditorChecks = useInvalidEditorChecks(); + + const { setInvalidBlocks, setInvalidMeta, setInvalidEditorChecks } = useDispatch(STORE_NAME); + + useEffect(() => { + setInvalidBlocks(invalidBlocks); + }, [invalidBlocks, setInvalidBlocks]); + + useEffect(() => { + setInvalidMeta(invalidMeta); + }, [invalidMeta, setInvalidMeta]); + + useEffect(() => { + setInvalidEditorChecks(invalidEditorChecks); + }, [invalidEditorChecks, setInvalidEditorChecks]); +} diff --git a/src/editor/hoc/withErrorHandling.js b/src/hooks/validate-block.js similarity index 69% rename from src/editor/hoc/withErrorHandling.js rename to src/hooks/validate-block.js index 209dc19..6920efc 100644 --- a/src/editor/hoc/withErrorHandling.js +++ b/src/hooks/validate-block.js @@ -1,6 +1,15 @@ +/** + * Side-effect module. Adds the `editor.BlockEdit` filter that runs per-block + * validation, syncs the result to the `core/validation` store, and renders + * a toolbar button (via BlockControls) when issues exist. + * + * Imported for side effects from src/hooks/index.js. + */ + /** * WordPress dependencies */ +import { addFilter } from '@wordpress/hooks'; import { createHigherOrderComponent } from '@wordpress/compose'; import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; @@ -9,20 +18,11 @@ import { BlockControls } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { validateBlock } from '../validation/blocks'; -import { ValidationToolbarButton } from '../components/ValidationToolbarButton'; -import { useDebouncedValidation } from '../../shared/hooks'; -import { STORE_NAME } from '../store'; +import { STORE_NAME } from '../store/constants'; +import { validateBlock } from '../utils/validate-block'; +import { useDebouncedValidation } from '../utils/use-debounced-validation'; +import { ValidationToolbarButton } from '../components/validation-toolbar-button'; -/** - * Higher-order component that adds validation indicators to blocks. - * - * Runs debounced validation on each block and: - * - Syncs the result to the shared validation store so the - * editor.BlockListBlock filter can apply CSS classes. - * - Renders a toolbar button (via BlockControls) when issues exist, - * allowing users to view the full issue list in a modal. - */ const withErrorHandling = createHigherOrderComponent(BlockEdit => { return props => { const { clientId, attributes } = props; @@ -71,4 +71,4 @@ const withErrorHandling = createHigherOrderComponent(BlockEdit => { }; }, 'withErrorHandling'); -wp.hooks.addFilter('editor.BlockEdit', 'validation-api/with-error-handling', withErrorHandling); +addFilter('editor.BlockEdit', 'validation-api/with-error-handling', withErrorHandling); diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..8e959ed --- /dev/null +++ b/src/index.js @@ -0,0 +1,40 @@ +/** + * Validation API package entry. + * + * Side-effect imports register the store, filters, and sidebar plugin on + * module load. Named exports are provided so a consumer that imports from + * `build/validation-api.js` can access the public API (store selectors/ + * actions, utilities, components) directly — though the typical consumer + * will interact with the package via the WP filter hooks and the + * `core/validation` @wordpress/data store. + */ + +import './store'; +import './hooks'; +import './styles.scss'; + +export { STORE_NAME } from './store/constants'; +export { + setInvalidBlocks, + setInvalidMeta, + setInvalidEditorChecks, + setBlockValidation, + clearBlockValidation, +} from './store/actions'; +export { + getInvalidBlocks, + getInvalidMeta, + getInvalidEditorChecks, + getBlockValidation, + hasErrors, + hasWarnings, +} from './store/selectors'; + +export * from './utils'; + +export { ValidationIcon } from './components/validation-icon'; +export { ValidationSidebar } from './components/validation-sidebar'; +export { ValidationToolbarButton } from './components/validation-toolbar-button'; + +export { useValidationSync } from './hooks/use-validation-sync'; +export { useValidationLifecycle } from './hooks/use-validation-lifecycle'; diff --git a/src/script.js b/src/script.js deleted file mode 100644 index 513b6a6..0000000 --- a/src/script.js +++ /dev/null @@ -1,13 +0,0 @@ -// Register the plugin -import './editor/register'; - -// Validate blocks -import './editor/validation/blocks/validateBlock'; -import './editor/hoc/withErrorHandling'; -import './editor/hoc/withBlockValidationClasses'; - -// Editor Validation -import './editor/validation/editor/validateEditor'; - -// Styles -import './styles.scss'; diff --git a/src/shared/hooks/index.js b/src/shared/hooks/index.js deleted file mode 100644 index d56e9ff..0000000 --- a/src/shared/hooks/index.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Shared Hooks - * - * Barrel export for shared custom React hooks. - */ - -export { useDebouncedValidation } from './useDebouncedValidation'; diff --git a/src/shared/utils/validation/index.js b/src/shared/utils/validation/index.js deleted file mode 100644 index 03b4860..0000000 --- a/src/shared/utils/validation/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Validation Utilities - * - * Barrel export for validation-related utility functions. - */ - -export * from './issueHelpers'; -export { GetInvalidBlocks } from './getInvalidBlocks'; -export { GetInvalidMeta } from './getInvalidMeta'; -export { GetInvalidEditorChecks } from './getInvalidEditorChecks'; -export { - getValidationRules, - getMetaValidationRules, - getEditorValidationRules, - getEditorContext, - getRegisteredBlockTypes, -} from './getValidationConfig'; diff --git a/src/editor/store/actions.js b/src/store/actions.js similarity index 100% rename from src/editor/store/actions.js rename to src/store/actions.js diff --git a/src/editor/store/constants.js b/src/store/constants.js similarity index 100% rename from src/editor/store/constants.js rename to src/store/constants.js diff --git a/src/editor/store/index.js b/src/store/index.js similarity index 100% rename from src/editor/store/index.js rename to src/store/index.js diff --git a/src/editor/store/reducer.js b/src/store/reducer.js similarity index 100% rename from src/editor/store/reducer.js rename to src/store/reducer.js diff --git a/src/editor/store/selectors.js b/src/store/selectors.js similarity index 100% rename from src/editor/store/selectors.js rename to src/store/selectors.js diff --git a/src/shared/utils/validation/getValidationConfig.js b/src/utils/get-validation-config.js similarity index 100% rename from src/shared/utils/validation/getValidationConfig.js rename to src/utils/get-validation-config.js diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..304680d --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,28 @@ +/** + * Validation API utilities barrel export. + * + * Consumers can import individual symbols from this entry point, or from the + * package root. + */ + +export * from './issue-helpers'; +export { + getValidationRules, + getMetaValidationRules, + getEditorValidationRules, + getEditorContext, + getRegisteredBlockTypes, +} from './get-validation-config'; + +export { validateBlock } from './validate-block'; +export { validateMetaField, validateAllMetaChecks } from './validate-meta'; +export { validateEditor } from './validate-editor'; + +export { useInvalidBlocks } from './use-invalid-blocks'; +export { useInvalidMeta } from './use-invalid-meta'; +export { useInvalidEditorChecks } from './use-invalid-editor-checks'; +export { useValidationIssues } from './use-validation-issues'; + +export { useMetaField } from './use-meta-field'; +export { useMetaValidation } from './use-meta-validation'; +export { useDebouncedValidation } from './use-debounced-validation'; diff --git a/src/shared/utils/validation/issueHelpers.js b/src/utils/issue-helpers.js similarity index 100% rename from src/shared/utils/validation/issueHelpers.js rename to src/utils/issue-helpers.js diff --git a/src/shared/hooks/useDebouncedValidation.js b/src/utils/use-debounced-validation.js similarity index 100% rename from src/shared/hooks/useDebouncedValidation.js rename to src/utils/use-debounced-validation.js diff --git a/src/shared/utils/validation/getInvalidBlocks.js b/src/utils/use-invalid-blocks.js similarity index 96% rename from src/shared/utils/validation/getInvalidBlocks.js rename to src/utils/use-invalid-blocks.js index c7a3509..f3653c6 100644 --- a/src/shared/utils/validation/getInvalidBlocks.js +++ b/src/utils/use-invalid-blocks.js @@ -6,8 +6,8 @@ import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { validateBlock } from '../../../editor/validation/blocks'; -import { getEditorContext } from './getValidationConfig'; +import { validateBlock } from './validate-block'; +import { getEditorContext } from './get-validation-config'; /** * Recursively retrieves invalid blocks from a block tree. @@ -79,7 +79,7 @@ function findPostContentBlock(blocks) { * * @return {Array} Array of validation results for all invalid blocks in the editor. */ -export function GetInvalidBlocks() { +export function useInvalidBlocks() { // Get editor context to determine filtering strategy const editorContext = getEditorContext(); const isPostEditor = diff --git a/src/shared/utils/validation/getInvalidEditorChecks.js b/src/utils/use-invalid-editor-checks.js similarity index 92% rename from src/shared/utils/validation/getInvalidEditorChecks.js rename to src/utils/use-invalid-editor-checks.js index 05b4253..bf3b9d9 100644 --- a/src/shared/utils/validation/getInvalidEditorChecks.js +++ b/src/utils/use-invalid-editor-checks.js @@ -6,7 +6,7 @@ import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { validateEditor } from '../../../editor/validation/editor'; +import { validateEditor } from './validate-editor'; /** * React hook that retrieves all invalid editor-level validation checks. @@ -18,7 +18,7 @@ import { validateEditor } from '../../../editor/validation/editor'; * * @return {Array} Array of validation issues for editor-level checks that failed. */ -export function GetInvalidEditorChecks() { +export function useInvalidEditorChecks() { // Retrieve current post type, blocks, and title from the editor store // Including title ensures validation updates in real-time as user types const { blocks, postType } = useSelect(select => { diff --git a/src/shared/utils/validation/getInvalidMeta.js b/src/utils/use-invalid-meta.js similarity index 89% rename from src/shared/utils/validation/getInvalidMeta.js rename to src/utils/use-invalid-meta.js index f7b839f..70ac53b 100644 --- a/src/shared/utils/validation/getInvalidMeta.js +++ b/src/utils/use-invalid-meta.js @@ -6,8 +6,8 @@ import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { validateAllMetaChecks } from '../../../editor/validation/meta/validateMeta'; -import { getMetaValidationRules } from './getValidationConfig'; +import { validateAllMetaChecks } from './validate-meta'; +import { getMetaValidationRules } from './get-validation-config'; /** * React hook that retrieves all invalid meta field validations for the current post. @@ -19,7 +19,7 @@ import { getMetaValidationRules } from './getValidationConfig'; * * @return {Array} Array of validation results for meta fields that failed validation. */ -export function GetInvalidMeta() { +export function useInvalidMeta() { // Retrieve current post type and meta fields from the editor store const { postType, meta } = useSelect(select => { const editor = select('core/editor'); diff --git a/src/editor/validation/meta/hooks/useMetaField.js b/src/utils/use-meta-field.js similarity index 58% rename from src/editor/validation/meta/hooks/useMetaField.js rename to src/utils/use-meta-field.js index 4421040..45f7fae 100644 --- a/src/editor/validation/meta/hooks/useMetaField.js +++ b/src/utils/use-meta-field.js @@ -6,7 +6,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { useMetaValidation } from './useMetaValidation'; +import { validateAllMetaChecks } from './validate-meta'; /** * Custom React hook to manage meta field state, validation, and UI integration. @@ -17,56 +17,75 @@ import { useMetaValidation } from './useMetaValidation'; * - Displaying validation errors/warnings in the help text * - Applying validation-specific CSS classes for styling * - * This hook integrates validation results into the field's help text, appending - * error or warning messages to provide immediate feedback to users. - * * @param {string} metaKey - The meta key to manage (e.g., '_wp_page_template'). * @param {string} originalHelp - Optional original help text to display alongside validation messages. * @return {Object} Object containing value, onChange handler, help text, and className for the control. */ export function useMetaField(metaKey, originalHelp = '') { - // Get validation state for this meta field - const validation = useMetaValidation(metaKey); - - // Retrieve current meta field value from the editor store - const { value } = useSelect( + // Single useSelect reads post type + meta value + runs validation. + const { value, validation } = useSelect( select => { const editor = select('core/editor'); - // Guard against editor not being available if (!editor) { - return { value: '' }; + return { + value: '', + validation: { + isValid: true, + hasErrors: false, + hasWarnings: false, + issues: [], + wrapperClassName: '', + }, + }; } - // Get the meta object and extract the value for this specific meta key + const postType = editor.getCurrentPostType(); const meta = editor.getEditedPostAttribute('meta'); + const currentValue = meta ? meta[metaKey] : ''; + + if (!postType || !metaKey) { + return { + value: currentValue, + validation: { + isValid: true, + hasErrors: false, + hasWarnings: false, + issues: [], + wrapperClassName: '', + }, + }; + } + + const result = validateAllMetaChecks(postType, metaKey, currentValue); + + let wrapperClassName = ''; + if (result.hasErrors) { + wrapperClassName = 'validation-api-meta-error'; + } else if (result.hasWarnings) { + wrapperClassName = 'validation-api-meta-warning'; + } + return { - value: meta ? meta[metaKey] : '', + value: currentValue, + validation: { ...result, wrapperClassName }, }; }, [metaKey] ); - // Get dispatch function to update meta field value const { editPost } = useDispatch('core/editor'); - // Start with the original help text (if provided) + // Enhance help text with validation messages if issues exist. let helpText = originalHelp; - - // Enhance help text with validation messages if issues exist if (validation && (validation.hasErrors || validation.hasWarnings)) { - // Extract all validation messages from issues const messages = validation.issues .map(issue => issue.message || issue.errorMsg || issue.warningMsg) .join('. '); - - // Determine CSS class based on severity (errors take precedence) const messageClass = validation.hasErrors ? 'validation-api-error-text' : 'validation-api-warning-text'; - // Append validation messages to existing help text, or create new help text if (helpText) { - // Combine original help with validation messages helpText = ( <> {helpText} @@ -74,24 +93,18 @@ export function useMetaField(metaKey, originalHelp = '') { ); } else { - // Use only validation messages if no original help text exists helpText = * {messages}; } } - // Return props object to be spread onto the meta field control return { - // Current meta field value (default to empty string) value: value || '', - // Handler to update meta field when user changes input onChange: newValue => { if (editPost) { editPost({ meta: { [metaKey]: newValue } }); } }, - // Help text with validation messages appended if issues exist help: helpText, - // CSS classes for styling validation state (error/warning indicators) className: validation?.wrapperClassName ? `validation-api-field ${validation.wrapperClassName}` : '', diff --git a/src/editor/validation/meta/hooks/useMetaValidation.js b/src/utils/use-meta-validation.js similarity index 97% rename from src/editor/validation/meta/hooks/useMetaValidation.js rename to src/utils/use-meta-validation.js index 4e8c6be..1867ec1 100644 --- a/src/editor/validation/meta/hooks/useMetaValidation.js +++ b/src/utils/use-meta-validation.js @@ -6,7 +6,7 @@ import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { validateAllMetaChecks } from '../validateMeta'; +import { validateAllMetaChecks } from './validate-meta'; /** * React hook to retrieve meta field validation status. diff --git a/src/utils/use-validation-issues.js b/src/utils/use-validation-issues.js new file mode 100644 index 0000000..01f7833 --- /dev/null +++ b/src/utils/use-validation-issues.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { STORE_NAME } from '../store/constants'; + +/** + * React hook that reads the three aggregate validation arrays from the store. + * + * Consolidates the 3-selector `useSelect` block that was previously duplicated + * between `useValidationLifecycle` and `ValidationSidebar`. Components that + * need any or all of these arrays call this hook to read them in one pass. + * + * @return {Object} { invalidBlocks, invalidMeta, invalidEditorChecks } + */ +export function useValidationIssues() { + return useSelect(select => { + const store = select(STORE_NAME); + return { + invalidBlocks: store.getInvalidBlocks(), + invalidMeta: store.getInvalidMeta(), + invalidEditorChecks: store.getInvalidEditorChecks(), + }; + }, []); +} diff --git a/src/editor/validation/blocks/validateBlock.js b/src/utils/validate-block.js similarity index 96% rename from src/editor/validation/blocks/validateBlock.js rename to src/utils/validate-block.js index 61b7490..abb6d8b 100644 --- a/src/editor/validation/blocks/validateBlock.js +++ b/src/utils/validate-block.js @@ -12,8 +12,8 @@ import { hasErrors, hasWarnings, createValidationResult, - getValidationRules, -} from '../../../shared/utils/validation'; +} from './issue-helpers'; +import { getValidationRules } from './get-validation-config'; /** * Validates a block against all PHP-registered checks. diff --git a/src/editor/validation/editor/validateEditor.js b/src/utils/validate-editor.js similarity index 92% rename from src/editor/validation/editor/validateEditor.js rename to src/utils/validate-editor.js index 79ced96..a8d5149 100644 --- a/src/editor/validation/editor/validateEditor.js +++ b/src/utils/validate-editor.js @@ -6,12 +6,8 @@ import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import { - isCheckEnabled, - createIssue, - createValidationResult, - getEditorValidationRules, -} from '../../../shared/utils/validation'; +import { isCheckEnabled, createIssue, createValidationResult } from './issue-helpers'; +import { getEditorValidationRules } from './get-validation-config'; /** * Validates entire editor content against editor-level validation rules. diff --git a/src/editor/validation/meta/validateMeta.js b/src/utils/validate-meta.js similarity index 95% rename from src/editor/validation/meta/validateMeta.js rename to src/utils/validate-meta.js index 882e950..846252c 100644 --- a/src/editor/validation/meta/validateMeta.js +++ b/src/utils/validate-meta.js @@ -6,12 +6,8 @@ import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import { - isCheckEnabled, - createIssue, - createValidationResult, - getMetaValidationRules, -} from '../../../shared/utils/validation'; +import { isCheckEnabled, createIssue, createValidationResult } from './issue-helpers'; +import { getMetaValidationRules } from './get-validation-config'; /** * Validates a single meta field against a specific validation check. diff --git a/webpack.config.js b/webpack.config.js index f1caa04..7e8e367 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,20 +5,11 @@ module.exports = { ...defaultConfig, entry: { ...defaultConfig.entry, - 'validation-api': [path.resolve(__dirname, 'src/script.js')], + 'validation-api': [path.resolve(__dirname, 'src/index.js')], }, output: { ...defaultConfig.output, path: path.resolve(__dirname, 'build'), filename: '[name].js', }, - resolve: { - ...defaultConfig.resolve, - alias: { - ...defaultConfig.resolve.alias, - '@': path.resolve(__dirname, 'src/'), - '@editor': path.resolve(__dirname, 'src/editor/'), - '@shared': path.resolve(__dirname, 'src/shared/'), - }, - }, }; From 561e32acd7c7ca0b236fd3ef605031940635452d Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Sat, 18 Apr 2026 10:25:46 -0400 Subject: [PATCH 07/11] polish: add @example JSDoc blocks to public API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match Gutenberg package convention of showing realistic usage snippets on public selectors/actions and user-facing hooks. Improves IDE hover documentation for external plugin authors. - store/selectors.js: 6 selectors (getInvalidBlocks, getInvalidMeta, getInvalidEditorChecks, getBlockValidation, hasErrors, hasWarnings) each showing a useSelect pattern - store/actions.js: 5 actions (setInvalidBlocks, setInvalidMeta, setInvalidEditorChecks, setBlockValidation, clearBlockValidation) each showing a useDispatch pattern - utils/use-meta-field.js: TextControl spread example - utils/use-meta-validation.js: custom render + usage-vs-useMetaField note Internal helpers (issue-helpers, validate-*) keep their one-line @param/@return style — consumed by the framework itself, not by external plugins. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/store/actions.js | 67 +++++++++++++++++++- src/store/selectors.js | 102 ++++++++++++++++++++++++++++++- src/utils/use-meta-field.js | 21 ++++++- src/utils/use-meta-validation.js | 25 +++++++- 4 files changed, 205 insertions(+), 10 deletions(-) diff --git a/src/store/actions.js b/src/store/actions.js index 9496cb1..2c60420 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -12,7 +12,21 @@ import { /** * Set the array of invalid block validation results. * - * @param {Array} results Invalid block results from GetInvalidBlocks. + * Typically dispatched by the validation lifecycle hook on every editor + * change. External plugins rarely need to dispatch this directly. + * + * @example + * + * ```js + * import { useDispatch } from '@wordpress/data'; + * + * const { setInvalidBlocks } = useDispatch( 'core/validation' ); + * setInvalidBlocks( [ + * { clientId: 'abc', name: 'core/image', mode: 'error', issues: [ ... ] }, + * ] ); + * ``` + * + * @param {Array} results Invalid block results from useInvalidBlocks. * @return {Object} Action object. */ export function setInvalidBlocks(results) { @@ -22,7 +36,18 @@ export function setInvalidBlocks(results) { /** * Set the array of invalid meta validation results. * - * @param {Array} results Invalid meta results from GetInvalidMeta. + * @example + * + * ```js + * import { useDispatch } from '@wordpress/data'; + * + * const { setInvalidMeta } = useDispatch( 'core/validation' ); + * setInvalidMeta( [ + * { metaKey: 'seo_description', hasErrors: true, issues: [ ... ] }, + * ] ); + * ``` + * + * @param {Array} results Invalid meta results from useInvalidMeta. * @return {Object} Action object. */ export function setInvalidMeta(results) { @@ -32,7 +57,18 @@ export function setInvalidMeta(results) { /** * Set the array of editor-level validation issues. * - * @param {Array} issues Editor check issues from GetInvalidEditorChecks. + * @example + * + * ```js + * import { useDispatch } from '@wordpress/data'; + * + * const { setInvalidEditorChecks } = useDispatch( 'core/validation' ); + * setInvalidEditorChecks( [ + * { type: 'error', errorMsg: 'Posts must start with a heading.' }, + * ] ); + * ``` + * + * @param {Array} issues Editor check issues from useInvalidEditorChecks. * @return {Object} Action object. */ export function setInvalidEditorChecks(issues) { @@ -42,6 +78,18 @@ export function setInvalidEditorChecks(issues) { /** * Store a single block's validation result. * + * The per-block validation map is keyed by clientId and read by the + * `editor.BlockListBlock` filter to apply error/warning CSS classes. + * + * @example + * + * ```js + * import { useDispatch } from '@wordpress/data'; + * + * const { setBlockValidation } = useDispatch( 'core/validation' ); + * setBlockValidation( clientId, { mode: 'warning', issues: [ ... ] } ); + * ``` + * * @param {string} clientId Block client ID. * @param {Object} result Validation result ({ mode, issues }). * @return {Object} Action object. @@ -53,6 +101,19 @@ export function setBlockValidation(clientId, result) { /** * Remove a single block's validation result. * + * Typically dispatched when a block unmounts so its entry doesn't linger + * in the per-block validation map. + * + * @example + * + * ```js + * import { useDispatch } from '@wordpress/data'; + * import { useEffect } from '@wordpress/element'; + * + * const { clearBlockValidation } = useDispatch( 'core/validation' ); + * useEffect( () => () => clearBlockValidation( clientId ), [ clientId ] ); + * ``` + * * @param {string} clientId Block client ID. * @return {Object} Action object. */ diff --git a/src/store/selectors.js b/src/store/selectors.js index 2587e8c..d02e1c2 100644 --- a/src/store/selectors.js +++ b/src/store/selectors.js @@ -4,7 +4,20 @@ import { DEFAULT_BLOCK_RESULT } from './constants'; /** - * Get all invalid block validation results. + * Get all invalid block validation results for the current post. + * + * @example + * + * ```js + * import { useSelect } from '@wordpress/data'; + * + * const InvalidBlocksCount = () => { + * const invalidBlocks = useSelect( ( select ) => + * select( 'core/validation' ).getInvalidBlocks() + * ); + * return { invalidBlocks.length } block issues; + * }; + * ``` * * @param {Object} state Store state. * @return {Array} Array of invalid block results. @@ -14,7 +27,20 @@ export function getInvalidBlocks(state) { } /** - * Get all invalid meta validation results. + * Get all invalid meta validation results for the current post. + * + * @example + * + * ```js + * import { useSelect } from '@wordpress/data'; + * + * const InvalidMetaCount = () => { + * const invalidMeta = useSelect( ( select ) => + * select( 'core/validation' ).getInvalidMeta() + * ); + * return { invalidMeta.length } meta field issues; + * }; + * ``` * * @param {Object} state Store state. * @return {Array} Array of invalid meta results. @@ -24,7 +50,20 @@ export function getInvalidMeta(state) { } /** - * Get all editor-level validation issues. + * Get all editor-level validation issues for the current post. + * + * @example + * + * ```js + * import { useSelect } from '@wordpress/data'; + * + * const EditorChecks = () => { + * const editorIssues = useSelect( ( select ) => + * select( 'core/validation' ).getInvalidEditorChecks() + * ); + * return { editorIssues.length } document-level issues; + * }; + * ``` * * @param {Object} state Store state. * @return {Array} Array of editor check issues. @@ -36,6 +75,25 @@ export function getInvalidEditorChecks(state) { /** * Get a single block's validation result. * + * Returns `{ mode: 'none', issues: [] }` when no result has been stored for + * the given clientId. The `mode` property is one of `'error'`, `'warning'`, + * or `'none'`. + * + * @example + * + * ```js + * import { useSelect } from '@wordpress/data'; + * + * const BlockStatus = ( { clientId } ) => { + * const { mode, issues } = useSelect( + * ( select ) => select( 'core/validation' ).getBlockValidation( clientId ), + * [ clientId ] + * ); + * if ( mode === 'none' ) return null; + * return { issues.length }; + * }; + * ``` + * * @param {Object} state Store state. * @param {string} clientId Block client ID. * @return {Object} Validation result ({ mode, issues }). @@ -47,6 +105,21 @@ export function getBlockValidation(state, clientId) { /** * Check if any validation errors exist across blocks, meta, and editor checks. * + * Commonly used to gate publish/save UI or drive a global warning banner. + * + * @example + * + * ```js + * import { useSelect } from '@wordpress/data'; + * + * const PublishButton = () => { + * const hasErrors = useSelect( ( select ) => + * select( 'core/validation' ).hasErrors() + * ); + * return ; + * }; + * ``` + * * @param {Object} state Store state. * @return {boolean} True if any errors exist. */ @@ -60,6 +133,29 @@ export function hasErrors(state) { /** * Check if any validation warnings exist (only when no errors are present). * + * Errors take precedence — if any error exists this returns `false` even + * when warnings are also present. Use in combination with `hasErrors()` for + * a tri-state UI. + * + * @example + * + * ```js + * import { useSelect } from '@wordpress/data'; + * + * const StatusBadge = () => { + * const { hasErrors, hasWarnings } = useSelect( ( select ) => { + * const store = select( 'core/validation' ); + * return { + * hasErrors: store.hasErrors(), + * hasWarnings: store.hasWarnings(), + * }; + * } ); + * if ( hasErrors ) return ; + * if ( hasWarnings ) return ; + * return ; + * }; + * ``` + * * @param {Object} state Store state. * @return {boolean} True if warnings exist and no errors exist. */ diff --git a/src/utils/use-meta-field.js b/src/utils/use-meta-field.js index 45f7fae..9335d60 100644 --- a/src/utils/use-meta-field.js +++ b/src/utils/use-meta-field.js @@ -17,9 +17,24 @@ import { validateAllMetaChecks } from './validate-meta'; * - Displaying validation errors/warnings in the help text * - Applying validation-specific CSS classes for styling * - * @param {string} metaKey - The meta key to manage (e.g., '_wp_page_template'). - * @param {string} originalHelp - Optional original help text to display alongside validation messages. - * @return {Object} Object containing value, onChange handler, help text, and className for the control. + * Spread the returned object onto a `TextControl` (or compatible component) to + * wire value, change handler, help text, and validation styling in one line. + * + * @example + * + * ```js + * import { TextControl } from '@wordpress/components'; + * import { useMetaField } from '@wordpress/validation'; + * + * const BandOriginField = () => { + * const props = useMetaField( 'band_origin', 'Where the band formed.' ); + * return ; + * }; + * ``` + * + * @param {string} metaKey - The meta key to manage (e.g., 'seo_description'). + * @param {string} originalHelp - Optional help text to display alongside validation messages. + * @return {Object} Props for TextControl: { value, onChange, help, className }. */ export function useMetaField(metaKey, originalHelp = '') { // Single useSelect reads post type + meta value + runs validation. diff --git a/src/utils/use-meta-validation.js b/src/utils/use-meta-validation.js index 1867ec1..42965e2 100644 --- a/src/utils/use-meta-validation.js +++ b/src/utils/use-meta-validation.js @@ -16,9 +16,32 @@ import { validateAllMetaChecks } from './validate-meta'; * validation results including error/warning flags, issues array, and CSS * class name for styling the field based on validation state. * + * Use this when you want full control over how a validation result is + * rendered. For the common "spread onto TextControl" case, use + * `useMetaField` instead, which wraps this hook with change-handler and + * help-text integration. + * * Updates automatically when the meta field value changes due to useSelect reactivity. * - * @param {string} metaKey - The meta key to validate (e.g., '_wp_page_template'). + * @example + * + * ```js + * import { useMetaValidation } from '@wordpress/validation'; + * + * const SeoDescriptionStatus = () => { + * const { hasErrors, hasWarnings, issues } = useMetaValidation( 'seo_description' ); + * if ( ! hasErrors && ! hasWarnings ) return null; + * return ( + *
    + * { issues.map( ( issue ) => ( + *
  • { issue.errorMsg }
  • + * ) ) } + *
+ * ); + * }; + * ``` + * + * @param {string} metaKey - The meta key to validate (e.g., 'seo_description'). * @return {Object} Validation result object containing: * - isValid: Boolean indicating if validation passed * - hasErrors: Boolean indicating if any errors exist From a228e5d60b66b40c2564b6b6bbffee89de0b78e1 Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Sat, 18 Apr 2026 10:43:54 -0400 Subject: [PATCH 08/11] polish: start TypeScript migration with store/constants.ts Rename src/store/constants.js to constants.ts and add type definitions for the store state, validation issues, and block/meta validation results. Action type constants use `as const` for narrower literal types, enabling exhaustive switch checks in the reducer. Other modules continue running as JavaScript via @babel/preset-typescript; they gain IDE type inference on imports from constants.ts without requiring .ts migration themselves. Delete stale babel.config.json (pre-TS two-preset config from March) that was shadowing @wordpress/babel-preset-default and blocking TS syntax handling. wp-scripts' default preset now applies as designed and includes @babel/preset-typescript out of the box. Incidental bundle-size drop from ~94KB to ~70KB (minified) from the more optimized preset configuration. New exported types: - ValidationMode, IssueType - ValidationIssue - BlockValidationResult - MetaValidationResult - State No runtime behavior change. Matches Gutenberg package convention of a typed store/constants file. Co-Authored-By: Claude Opus 4.7 (1M context) --- babel.config.json | 3 - build/validation-api.asset.php | 2 +- build/validation-api.js | 2654 +++++++++----------------------- src/store/constants.js | 20 - src/store/constants.ts | 95 ++ 5 files changed, 825 insertions(+), 1949 deletions(-) delete mode 100644 babel.config.json delete mode 100644 src/store/constants.js create mode 100644 src/store/constants.ts diff --git a/babel.config.json b/babel.config.json deleted file mode 100644 index e07bf46..0000000 --- a/babel.config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["@babel/preset-env", "@babel/preset-react"] -} diff --git a/build/validation-api.asset.php b/build/validation-api.asset.php index 0d9d030..9116511 100644 --- a/build/validation-api.asset.php +++ b/build/validation-api.asset.php @@ -1 +1 @@ - array('wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins'), 'version' => '349b73cbd185e076f865'); + array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-editor', 'wp-element', 'wp-hooks', 'wp-i18n', 'wp-plugins'), 'version' => '7d727c41a8042b07ec2d'); diff --git a/build/validation-api.js b/build/validation-api.js index 1eb9ee0..19064d2 100644 --- a/build/validation-api.js +++ b/build/validation-api.js @@ -1,13 +1,13 @@ (() => { 'use strict'; var e = { - d: (t, r) => { - for (var n in r) - e.o(r, n) && - !e.o(t, n) && - Object.defineProperty(t, n, { enumerable: !0, get: r[n] }); + d: (i, t) => { + for (var n in t) + e.o(t, n) && + !e.o(i, n) && + Object.defineProperty(i, n, { enumerable: !0, get: t[n] }); }, - o: (e, t) => Object.prototype.hasOwnProperty.call(e, t), + o: (e, i) => Object.prototype.hasOwnProperty.call(e, i), r: e => { ('undefined' != typeof Symbol && Symbol.toStringTag && @@ -15,1330 +15,695 @@ Object.defineProperty(e, '__esModule', { value: !0 })); }, }, - t = {}; + i = {}; + (e.r(i), + e.d(i, { + getBlockValidation: () => h, + getInvalidBlocks: () => p, + getInvalidEditorChecks: () => g, + getInvalidMeta: () => m, + hasErrors: () => v, + hasWarnings: () => f, + })); + var t = {}; (e.r(t), e.d(t, { - getBlockValidation: () => w, - getInvalidBlocks: () => b, - getInvalidEditorChecks: () => h, - getInvalidMeta: () => g, - hasErrors: () => O, - hasWarnings: () => E, - })); - var r = {}; - (e.r(r), - e.d(r, { - clearBlockValidation: () => R, - setBlockValidation: () => P, - setInvalidBlocks: () => j, + clearBlockValidation: () => x, + setBlockValidation: () => j, + setInvalidBlocks: () => w, setInvalidEditorChecks: () => k, - setInvalidMeta: () => S, + setInvalidMeta: () => b, })); - const n = window.wp.data; - var o = 'core/validation', - i = 'SET_INVALID_BLOCKS', - a = 'SET_INVALID_META', - c = 'SET_INVALID_EDITOR_CHECKS', + const n = window.wp.data, + r = 'core/validation', + s = 'SET_INVALID_BLOCKS', + o = 'SET_INVALID_META', + a = 'SET_INVALID_EDITOR_CHECKS', l = 'SET_BLOCK_VALIDATION', - u = 'CLEAR_BLOCK_VALIDATION', - s = { blocks: [], meta: [], editor: [], blockValidation: {} }, - f = Object.freeze({ mode: 'none', issues: [] }); - function d(e) { - return ( - (d = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (e) { - return typeof e; - } - : function (e) { - return e && - 'function' == typeof Symbol && - e.constructor === Symbol && - e !== Symbol.prototype - ? 'symbol' - : typeof e; - }), - d(e) - ); - } - function p(e, t) { - var r = Object.keys(e); - if (Object.getOwnPropertySymbols) { - var n = Object.getOwnPropertySymbols(e); - (t && - (n = n.filter(function (t) { - return Object.getOwnPropertyDescriptor(e, t).enumerable; - })), - r.push.apply(r, n)); - } - return r; - } - function m(e) { - for (var t = 1; t < arguments.length; t++) { - var r = null != arguments[t] ? arguments[t] : {}; - t % 2 - ? p(Object(r), !0).forEach(function (t) { - v(e, t, r[t]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : p(Object(r)).forEach(function (t) { - Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); - }); - } - return e; - } - function v(e, t, r) { - return ( - (t = y(t)) in e - ? Object.defineProperty(e, t, { - value: r, - enumerable: !0, - configurable: !0, - writable: !0, - }) - : (e[t] = r), - e - ); - } - function y(e) { - var t = (function (e) { - if ('object' != d(e) || !e) return e; - var t = e[Symbol.toPrimitive]; - if (void 0 !== t) { - var r = t.call(e, 'string'); - if ('object' != d(r)) return r; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return String(e); - })(e); - return 'symbol' == d(t) ? t : t + ''; - } - function b(e) { + c = 'CLEAR_BLOCK_VALIDATION', + d = { blocks: [], meta: [], editor: [], blockValidation: {} }, + u = Object.freeze({ mode: 'none', issues: [] }); + function p(e) { return e.blocks; } - function g(e) { + function m(e) { return e.meta; } - function h(e) { + function g(e) { return e.editor; } - function w(e, t) { - return e.blockValidation[t] || f; + function h(e, i) { + return e.blockValidation[i] || u; } - function O(e) { - var t = e.blocks.some(function (e) { - return 'error' === e.mode; - }), - r = e.meta.some(function (e) { - return e.hasErrors; - }), - n = e.editor.some(function (e) { - return 'error' === e.type; - }); - return t || r || n; + function v(e) { + const i = e.blocks.some(e => 'error' === e.mode), + t = e.meta.some(e => e.hasErrors), + n = e.editor.some(e => 'error' === e.type); + return i || t || n; } - function E(e) { - if (O(e)) return !1; - var t = e.blocks.some(function (e) { - return 'warning' === e.mode; - }), - r = e.meta.some(function (e) { - return e.hasWarnings && !e.hasErrors; - }), - n = e.editor.some(function (e) { - return 'warning' === e.type; - }); - return t || r || n; + function f(e) { + if (v(e)) return !1; + const i = e.blocks.some(e => 'warning' === e.mode), + t = e.meta.some(e => e.hasWarnings && !e.hasErrors), + n = e.editor.some(e => 'warning' === e.type); + return i || t || n; } - function j(e) { - return { type: i, results: e }; + function w(e) { + return { type: s, results: e }; } - function S(e) { - return { type: a, results: e }; + function b(e) { + return { type: o, results: e }; } function k(e) { - return { type: c, issues: e }; + return { type: a, issues: e }; } - function P(e, t) { - return { type: l, clientId: e, result: t }; + function j(e, i) { + return { type: l, clientId: e, result: i }; } - function R(e) { - return { type: u, clientId: e }; + function x(e) { + return { type: c, clientId: e }; } - var I = (0, n.createReduxStore)(o, { - reducer: function () { - var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : s, - t = arguments.length > 1 ? arguments[1] : void 0; - switch (t.type) { - case i: - return m(m({}, e), {}, { blocks: t.results }); + const y = (0, n.createReduxStore)(r, { + reducer: function (e = d, i) { + switch (i.type) { + case s: + return { ...e, blocks: i.results }; + case o: + return { ...e, meta: i.results }; case a: - return m(m({}, e), {}, { meta: t.results }); - case c: - return m(m({}, e), {}, { editor: t.issues }); + return { ...e, editor: i.issues }; case l: - return m( - m({}, e), - {}, - { - blockValidation: m( - m({}, e.blockValidation), - {}, - v({}, t.clientId, t.result) - ), - } - ); - case u: - var r = e.blockValidation, - n = t.clientId, - o = - (r[n], - (function (e, t) { - if (null == e) return {}; - var r, - n, - o = (function (e, t) { - if (null == e) return {}; - var r = {}; - for (var n in e) - if ({}.hasOwnProperty.call(e, n)) { - if (-1 !== t.indexOf(n)) continue; - r[n] = e[n]; - } - return r; - })(e, t); - if (Object.getOwnPropertySymbols) { - var i = Object.getOwnPropertySymbols(e); - for (n = 0; n < i.length; n++) - ((r = i[n]), - -1 === t.indexOf(r) && - {}.propertyIsEnumerable.call(e, r) && - (o[r] = e[r])); - } - return o; - })(r, [n].map(y))); - return m(m({}, e), {}, { blockValidation: o }); + return { + ...e, + blockValidation: { ...e.blockValidation, [i.clientId]: i.result }, + }; + case c: { + const { [i.clientId]: t, ...n } = e.blockValidation; + return { ...e, blockValidation: n }; + } default: return e; } }, - selectors: t, - actions: r, + selectors: i, + actions: t, }); - (0, n.register)(I); - const A = window.wp.plugins, - _ = window.wp.element, - B = window.wp.hooks; - function N(e) { - return ( - (N = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (e) { - return typeof e; - } - : function (e) { - return e && - 'function' == typeof Symbol && - e.constructor === Symbol && - e !== Symbol.prototype - ? 'symbol' - : typeof e; - }), - N(e) - ); - } - function T(e, t) { - var r = Object.keys(e); - if (Object.getOwnPropertySymbols) { - var n = Object.getOwnPropertySymbols(e); - (t && - (n = n.filter(function (t) { - return Object.getOwnPropertyDescriptor(e, t).enumerable; - })), - r.push.apply(r, n)); - } - return r; - } - function C(e) { - for (var t = 1; t < arguments.length; t++) { - var r = null != arguments[t] ? arguments[t] : {}; - t % 2 - ? T(Object(r), !0).forEach(function (t) { - L(e, t, r[t]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : T(Object(r)).forEach(function (t) { - Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); - }); - } - return e; - } - function L(e, t, r) { - return ( - (t = (function (e) { - var t = (function (e) { - if ('object' != N(e) || !e) return e; - var t = e[Symbol.toPrimitive]; - if (void 0 !== t) { - var r = t.call(e, 'string'); - if ('object' != N(r)) return r; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return String(e); - })(e); - return 'symbol' == N(t) ? t : t + ''; - })(t)) in e - ? Object.defineProperty(e, t, { - value: r, - enumerable: !0, - configurable: !0, - writable: !0, - }) - : (e[t] = r), - e - ); - } - var M = function (e, t) { - return e.filter(function (e) { - return e.type === t; - }); - }, - V = function (e) { - return M(e, 'error'); - }, - D = function (e) { - return M(e, 'warning'); - }, - x = function (e) { - return e.some(function (e) { - return 'error' === e.type; - }); - }, - F = function (e) { - return e.some(function (e) { - return 'warning' === e.type; - }); - }, - K = function (e) { - return null != e && !1 !== e.enabled; - }, - U = function (e, t) { - var r = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}, - n = e.message || '', - o = e.error_msg || n, - i = e.warning_msg || e.error_msg || n, - a = e.level || 'error'; - return C( + (0, n.register)(y); + const I = window.wp.plugins, + E = window.wp.element, + B = window.wp.hooks, + _ = (e, i) => e.filter(e => e.type === i), + N = e => _(e, 'error'), + L = e => _(e, 'warning'), + C = e => e.some(e => 'error' === e.type), + V = e => e.some(e => 'warning' === e.type), + M = e => null != e && !1 !== e.enabled, + S = (e, i, t = {}) => { + const n = e.message || '', + r = e.error_msg || n, + s = e.warning_msg || e.error_msg || n, + o = e.level || 'error'; + let a; + return ( + (a = 'error' === o ? 1 : 'warning' === o ? 2 : 3), { - check: t, - checkName: t, - type: a, - priority: 'error' === a ? 1 : 'warning' === a ? 2 : 3, + check: i, + checkName: i, + type: o, + priority: a, message: n, - errorMsg: o, - warningMsg: i, - }, - r + errorMsg: r, + warningMsg: s, + ...t, + } ); }, - G = function (e) { - var t = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : {}; - return C({ isValid: 0 === e.length, issues: e, hasErrors: x(e), hasWarnings: F(e) }, t); - }; - function W() { + P = (e, i = {}) => ({ + isValid: 0 === e.length, + issues: e, + hasErrors: C(e), + hasWarnings: V(e), + ...i, + }); + function T() { try { - var e = (0, n.select)('core/editor').getEditorSettings(); - return (null == e ? void 0 : e.validationApi) || {}; - } catch (e) { + const e = (0, n.select)('core/editor').getEditorSettings(); + return e?.validationApi || {}; + } catch { return {}; } } - function $() { - return W().metaValidationRules || {}; - } - function q() { - return W().editorContext || 'none'; + function O() { + return T().metaValidationRules || {}; } - function H(e, t) { - (null == t || t > e.length) && (t = e.length); - for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; - return n; + function A() { + return T().editorContext || 'none'; } - var Z = function (e) { - var t = e.name, - r = e.attributes, + const R = e => { + const i = e.name, + t = e.attributes, n = [], - o = (W().validationRules || {})[t] || {}; - if (0 === Object.keys(o).length) - return { isValid: !0, issues: [], mode: 'none', clientId: e.clientId, name: t }; - Object.entries(o).forEach(function (o) { - var i, - a, - c = - ((a = 2), - (function (e) { - if (Array.isArray(e)) return e; - })((i = o)) || - (function (e, t) { - var r = - null == e - ? null - : ('undefined' != typeof Symbol && e[Symbol.iterator]) || - e['@@iterator']; - if (null != r) { - var n, - o, - i, - a, - c = [], - l = !0, - u = !1; - try { - if (((i = (r = r.call(e)).next), 0 === t)) { - if (Object(r) !== r) return; - l = !1; - } else - for ( - ; - !(l = (n = i.call(r)).done) && - (c.push(n.value), c.length !== t); - l = !0 - ); - } catch (e) { - ((u = !0), (o = e)); - } finally { - try { - if ( - !l && - null != r.return && - ((a = r.return()), Object(a) !== a) - ) - return; - } finally { - if (u) throw o; - } - } - return c; - } - })(i, a) || - (function (e, t) { - if (e) { - if ('string' == typeof e) return H(e, t); - var r = {}.toString.call(e).slice(8, -1); - return ( - 'Object' === r && e.constructor && (r = e.constructor.name), - 'Map' === r || 'Set' === r - ? Array.from(e) - : 'Arguments' === r || - /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? H(e, t) - : void 0 - ); - } - })(i, a) || - (function () { - throw new TypeError( - 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })()), - l = c[0], - u = c[1]; - if (K(u)) { - var s = !0; - ('function' == typeof u.validator && (s = u.validator(r, e)), - (s = (0, B.applyFilters)('editor.validateBlock', s, t, r, l, e)) || - n.push(U(u, l))); - } + r = (T().validationRules || {})[i] || {}; + if (0 === Object.keys(r).length) + return { isValid: !0, issues: [], mode: 'none', clientId: e.clientId, name: i }; + Object.entries(r).forEach(([r, s]) => { + if (!M(s)) return; + let o = !0; + ('function' == typeof s.validator && (o = s.validator(t, e)), + (o = (0, B.applyFilters)('editor.validateBlock', o, i, t, r, e)), + o || n.push(S(s, r))); }); - var i = 'none'; + let s = 'none'; return ( - x(n) ? (i = 'error') : F(n) && (i = 'warning'), - G(n, { mode: i, clientId: e.clientId, name: t }) + C(n) ? (s = 'error') : V(n) && (s = 'warning'), + P(n, { mode: s, clientId: e.clientId, name: i }) ); }; - function z(e, t) { - if (e) { - if ('string' == typeof e) return J(e, t); - var r = {}.toString.call(e).slice(8, -1); + function $(e) { + return e.flatMap(e => { + const i = R(e), + t = []; return ( - 'Object' === r && e.constructor && (r = e.constructor.name), - 'Map' === r || 'Set' === r - ? Array.from(e) - : 'Arguments' === r || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? J(e, t) - : void 0 - ); - } - } - function J(e, t) { - (null == t || t > e.length) && (t = e.length); - for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; - return n; - } - function Q(e) { - return e.flatMap(function (e) { - var t, - r = Z(e), - n = []; - return ( - r.isValid || n.push(r), - e.innerBlocks && e.innerBlocks.length > 0 - ? [].concat( - n, - (function (e) { - if (Array.isArray(e)) return J(e); - })((t = Q(e.innerBlocks))) || - (function (e) { - if ( - ('undefined' != typeof Symbol && - null != e[Symbol.iterator]) || - null != e['@@iterator'] - ) - return Array.from(e); - })(t) || - z(t) || - (function () { - throw new TypeError( - 'Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })() - ) - : n + i.isValid || t.push(i), + e.innerBlocks && e.innerBlocks.length > 0 ? [...t, ...$(e.innerBlocks)] : t ); }); } - function X(e) { - var t, - r = (function (e) { - var t = ('undefined' != typeof Symbol && e[Symbol.iterator]) || e['@@iterator']; - if (!t) { - if (Array.isArray(e) || (t = z(e))) { - t && (e = t); - var _n = 0, - r = function () {}; - return { - s: r, - n: function () { - return _n >= e.length ? { done: !0 } : { done: !1, value: e[_n++] }; - }, - e: function (e) { - throw e; - }, - f: r, - }; - } - throw new TypeError( - 'Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - } - var n, - o = !0, - i = !1; - return { - s: function () { - t = t.call(e); - }, - n: function () { - var e = t.next(); - return ((o = e.done), e); - }, - e: function (e) { - ((i = !0), (n = e)); - }, - f: function () { - try { - o || null == t.return || t.return(); - } finally { - if (i) throw n; - } - }, - }; - })(e); - try { - for (r.s(); !(t = r.n()).done; ) { - var n = t.value; - if ('core/post-content' === n.name) return n; - if (n.innerBlocks && n.innerBlocks.length > 0) { - var o = X(n.innerBlocks); - if (o) return o; - } + function F(e) { + for (const i of e) { + if ('core/post-content' === i.name) return i; + if (i.innerBlocks && i.innerBlocks.length > 0) { + const e = F(i.innerBlocks); + if (e) return e; } - } catch (e) { - r.e(e); - } finally { - r.f(); } return null; } - function Y() { - var e = q(), - t = 'post-editor' === e || 'post-editor-template' === e; - return Q( + function D() { + const e = A(), + i = 'post-editor' === e || 'post-editor-template' === e; + return $( (0, n.useSelect)( - function (e) { - var r = e('core/block-editor'), - n = r.getBlocks(); - if (t) { - var o = X(n); - if (o) { - var i = r.getBlock(o.clientId), - a = r - .getBlockOrder(o.clientId) - .map(function (e) { - var t = r.getBlock(e); - return (r.getBlockOrder(e), t); + e => { + const t = e('core/block-editor'), + n = t.getBlocks(); + if (i) { + const e = F(n); + if (e) { + const i = t.getBlock(e.clientId), + n = t + .getBlockOrder(e.clientId) + .map(e => { + const i = t.getBlock(e); + return (t.getBlockOrder(e), i); }) .filter(Boolean); - return a.length > 0 ? a : (null == i ? void 0 : i.innerBlocks) || []; + return n.length > 0 ? n : i?.innerBlocks || []; } return n; } return n; }, - [t] + [i] ) ); } - function ee(e, t) { + function K(e, i, t, n) { + const r = O()[e]?.[i]?.[n]; + if (!M(r)) return !0; + let s = !0; return ( - (function (e) { - if (Array.isArray(e)) return e; - })(e) || - (function (e, t) { - var r = - null == e - ? null - : ('undefined' != typeof Symbol && e[Symbol.iterator]) || e['@@iterator']; - if (null != r) { - var n, - o, - i, - a, - c = [], - l = !0, - u = !1; - try { - if (((i = (r = r.call(e)).next), 0 === t)) { - if (Object(r) !== r) return; - l = !1; - } else - for ( - ; - !(l = (n = i.call(r)).done) && (c.push(n.value), c.length !== t); - l = !0 - ); - } catch (e) { - ((u = !0), (o = e)); - } finally { - try { - if (!l && null != r.return && ((a = r.return()), Object(a) !== a)) - return; - } finally { - if (u) throw o; - } - } - return c; - } - })(e, t) || - (function (e, t) { - if (e) { - if ('string' == typeof e) return te(e, t); - var r = {}.toString.call(e).slice(8, -1); - return ( - 'Object' === r && e.constructor && (r = e.constructor.name), - 'Map' === r || 'Set' === r - ? Array.from(e) - : 'Arguments' === r || - /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? te(e, t) - : void 0 - ); - } - })(e, t) || - (function () { - throw new TypeError( - 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })() - ); - } - function te(e, t) { - (null == t || t > e.length) && (t = e.length); - for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; - return n; - } - function re(e, t, r, n) { - var o, - i = - null === (o = $()[e]) || void 0 === o || null === (o = o[t]) || void 0 === o - ? void 0 - : o[n]; - if (!K(i)) return !0; - var a = !0; - return ( - 'required' === n && (a = '' !== r && null != r), - (0, B.applyFilters)('editor.validateMeta', a, r, e, t, n) + 'required' === n && (s = '' !== t && null != t), + (s = (0, B.applyFilters)('editor.validateMeta', s, t, e, i, n)), + s ); } - function ne(e, t, r) { - for ( - var n = ($()[e] || {})[t] || {}, o = [], i = 0, a = Object.entries(n); - i < a.length; - i++ - ) { - var c = ee(a[i], 2), - l = c[0], - u = c[1]; - if (K(u) && !re(e, t, r, l)) { - var s = U(u, l, { metaKey: t }); - o.push(s); + function W(e, i, t) { + const n = (O()[e] || {})[i] || {}, + r = []; + for (const [s, o] of Object.entries(n)) + if (M(o) && !K(e, i, t, s)) { + const e = S(o, s, { metaKey: i }); + r.push(e); } - } - return G(o); - } - function oe(e) { - return ( - (oe = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (e) { - return typeof e; - } - : function (e) { - return e && - 'function' == typeof Symbol && - e.constructor === Symbol && - e !== Symbol.prototype - ? 'symbol' - : typeof e; - }), - oe(e) - ); - } - function ie(e, t) { - var r = Object.keys(e); - if (Object.getOwnPropertySymbols) { - var n = Object.getOwnPropertySymbols(e); - (t && - (n = n.filter(function (t) { - return Object.getOwnPropertyDescriptor(e, t).enumerable; - })), - r.push.apply(r, n)); - } - return r; - } - function ae(e) { - for (var t = 1; t < arguments.length; t++) { - var r = null != arguments[t] ? arguments[t] : {}; - t % 2 - ? ie(Object(r), !0).forEach(function (t) { - ce(e, t, r[t]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : ie(Object(r)).forEach(function (t) { - Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); - }); - } - return e; - } - function ce(e, t, r) { - return ( - (t = (function (e) { - var t = (function (e) { - if ('object' != oe(e) || !e) return e; - var t = e[Symbol.toPrimitive]; - if (void 0 !== t) { - var r = t.call(e, 'string'); - if ('object' != oe(r)) return r; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return String(e); - })(e); - return 'symbol' == oe(t) ? t : t + ''; - })(t)) in e - ? Object.defineProperty(e, t, { - value: r, - enumerable: !0, - configurable: !0, - writable: !0, - }) - : (e[t] = r), - e - ); + return P(r); } - function le(e, t) { - return ( - (function (e) { - if (Array.isArray(e)) return e; - })(e) || - (function (e, t) { - var r = - null == e - ? null - : ('undefined' != typeof Symbol && e[Symbol.iterator]) || e['@@iterator']; - if (null != r) { - var n, - o, - i, - a, - c = [], - l = !0, - u = !1; - try { - if (((i = (r = r.call(e)).next), 0 === t)) { - if (Object(r) !== r) return; - l = !1; - } else - for ( - ; - !(l = (n = i.call(r)).done) && (c.push(n.value), c.length !== t); - l = !0 - ); - } catch (e) { - ((u = !0), (o = e)); - } finally { - try { - if (!l && null != r.return && ((a = r.return()), Object(a) !== a)) - return; - } finally { - if (u) throw o; - } - } - return c; - } - })(e, t) || - (function (e, t) { - if (e) { - if ('string' == typeof e) return ue(e, t); - var r = {}.toString.call(e).slice(8, -1); - return ( - 'Object' === r && e.constructor && (r = e.constructor.name), - 'Map' === r || 'Set' === r - ? Array.from(e) - : 'Arguments' === r || - /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? ue(e, t) - : void 0 - ); - } - })(e, t) || - (function () { - throw new TypeError( - 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })() - ); - } - function ue(e, t) { - (null == t || t > e.length) && (t = e.length); - for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; - return n; - } - function se() { - var e, - t, - r, - i = Y(), - a = (function () { - for ( - var e = (0, n.useSelect)(function (e) { - var t = e('core/editor'); - return { - postType: t.getCurrentPostType(), - meta: t.getEditedPostAttribute('meta'), - }; - }, []), - t = e.postType, - r = e.meta, - o = $()[t] || {}, - i = [], - a = 0, - c = Object.keys(o); - a < c.length; - a++ - ) { - var l = c[a], - u = ne(t, l, null == r ? void 0 : r[l]); - u.isValid || i.push(ae(ae({}, u), {}, { metaKey: l })); + function q() { + const e = D(), + i = (function () { + const { postType: e, meta: i } = (0, n.useSelect)(e => { + const i = e('core/editor'); + return { + postType: i.getCurrentPostType(), + meta: i.getEditedPostAttribute('meta'), + }; + }, []), + t = O()[e] || {}, + r = []; + for (const n of Object.keys(t)) { + const t = i?.[n], + s = W(e, n, t); + s.isValid || r.push({ ...s, metaKey: n }); } - return i; + return r; })(), - c = - ((t = (e = (0, n.useSelect)(function (e) { - var t = e('core/editor'), - r = e('core/block-editor'); + t = (function () { + const { blocks: e, postType: i } = (0, n.useSelect)(e => { + const i = e('core/editor'), + t = e('core/block-editor'); return { - postType: t.getCurrentPostType(), - blocks: r.getBlocks(), - title: t.getEditedPostAttribute('title'), + postType: i.getCurrentPostType(), + blocks: t.getBlocks(), + title: i.getEditedPostAttribute('title'), }; - }, [])).blocks), - (r = e.postType) && t - ? (function (e, t) { - for ( - var r = (W().editorValidationRules || {})[e] || {}, - n = [], - o = 0, - i = Object.entries(r); - o < i.length; - o++ - ) { - var a = le(i[o], 2), - c = a[0], - l = a[1]; + }, []); + return i && e + ? (function (e, i) { + const t = (T().editorValidationRules || {})[e] || {}, + n = []; + for (const [r, s] of Object.entries(t)) if ( - K(l) && - !(0, B.applyFilters)('editor.validateEditor', !0, t, e, c, l) + M(s) && + !(0, B.applyFilters)('editor.validateEditor', !0, i, e, r, s) ) { - var u = U(l, c); - n.push(u); + const e = S(s, r); + n.push(e); } - } - return ( - n.sort(function (e, t) { - return e.priority - t.priority; - }), - G(n) - ); - })(r, t).issues - : []), - l = (0, n.useDispatch)(o), - u = l.setInvalidBlocks, - s = l.setInvalidMeta, - f = l.setInvalidEditorChecks; - ((0, _.useEffect)( - function () { - u(i); - }, - [i, u] - ), - (0, _.useEffect)( - function () { - s(a); - }, - [a, s] - ), - (0, _.useEffect)( - function () { - f(c); - }, - [c, f] - )); - } - function fe() { - return (0, n.useSelect)(function (e) { - var t = e(o); + return (n.sort((e, i) => e.priority - i.priority), P(n)); + })(i, e).issues + : []; + })(), + { + setInvalidBlocks: s, + setInvalidMeta: o, + setInvalidEditorChecks: a, + } = (0, n.useDispatch)(r); + ((0, E.useEffect)(() => { + s(e); + }, [e, s]), + (0, E.useEffect)(() => { + o(i); + }, [i, o]), + (0, E.useEffect)(() => { + a(t); + }, [t, a])); + } + function H() { + return (0, n.useSelect)(e => { + const i = e(r); return { - invalidBlocks: t.getInvalidBlocks(), - invalidMeta: t.getInvalidMeta(), - invalidEditorChecks: t.getInvalidEditorChecks(), + invalidBlocks: i.getInvalidBlocks(), + invalidMeta: i.getInvalidMeta(), + invalidEditorChecks: i.getInvalidEditorChecks(), }; }, []); } - var de = 'core/validation'; - const pe = window.wp.editor, - me = window.wp.components, - ve = window.wp.i18n, - ye = window.wp.blocks; - function be(e) { - var t = e.fill, - r = void 0 === t ? 'currentColor' : t; - return React.createElement( - 'svg', - { - viewBox: '-0.81 -0.81 25.62 25.62', - xmlns: 'http://www.w3.org/2000/svg', - className: 'validation-api-sidebar-icon', - }, - React.createElement('path', { - fill: r, - d: 'M21.77205 2.96949V9.22968L24 11.49539L21.41025 14.12373C20.18445 17.59455 17.82645 19.4559 16.5927 20.1609C15.7053 20.66805 13.45103 22.0566 12.42537 22.69365L12 22.9578L11.57463 22.69365C10.54898 22.0566 8.2947 20.66805 7.4073 20.1609C6.17361 19.4559 3.81545 17.59455 2.58966 14.12373L0 11.49539L2.22791 9.22968V2.96949L10.16957 0L10.73433 1.51038L3.84047 4.08809V9.88976L2.26275 11.49425L3.99707 13.25445L4.05633 13.4307C5.10714 16.5531 7.20452 18.1878 8.2074 18.7608C9.01367 19.2216 10.87026 20.36115 12 21.06C13.12974 20.36115 14.98634 19.2216 15.7926 18.7608C16.7955 18.1878 18.8928 16.5531 19.9437 13.4307L20.00295 13.25445L21.73725 11.49425L20.15955 9.88976V4.08809L13.26567 1.51038L13.83044 0L21.77205 2.96949Z', - }), - React.createElement('path', { - fill: r, - d: 'M16.95615 8.74307L10.64529 15.05385L7.23707 11.64567L8.37732 10.50542L10.64529 12.77339L15.81585 7.60281L16.95615 8.74307Z', - }) - ); + const Z = 'core/validation', + z = window.wp.editor, + J = window.wp.components, + U = window.wp.i18n, + X = window.wp.blocks, + G = window.ReactJSXRuntime; + function Q({ fill: e = 'currentColor' }) { + return (0, G.jsxs)('svg', { + viewBox: '-0.81 -0.81 25.62 25.62', + xmlns: 'http://www.w3.org/2000/svg', + className: 'validation-api-sidebar-icon', + children: [ + (0, G.jsx)('path', { + fill: e, + d: 'M21.77205 2.96949V9.22968L24 11.49539L21.41025 14.12373C20.18445 17.59455 17.82645 19.4559 16.5927 20.1609C15.7053 20.66805 13.45103 22.0566 12.42537 22.69365L12 22.9578L11.57463 22.69365C10.54898 22.0566 8.2947 20.66805 7.4073 20.1609C6.17361 19.4559 3.81545 17.59455 2.58966 14.12373L0 11.49539L2.22791 9.22968V2.96949L10.16957 0L10.73433 1.51038L3.84047 4.08809V9.88976L2.26275 11.49425L3.99707 13.25445L4.05633 13.4307C5.10714 16.5531 7.20452 18.1878 8.2074 18.7608C9.01367 19.2216 10.87026 20.36115 12 21.06C13.12974 20.36115 14.98634 19.2216 15.7926 18.7608C16.7955 18.1878 18.8928 16.5531 19.9437 13.4307L20.00295 13.25445L21.73725 11.49425L20.15955 9.88976V4.08809L13.26567 1.51038L13.83044 0L21.77205 2.96949Z', + }), + (0, G.jsx)('path', { + fill: e, + d: 'M16.95615 8.74307L10.64529 15.05385L7.23707 11.64567L8.37732 10.50542L10.64529 12.77339L15.81585 7.60281L16.95615 8.74307Z', + }), + ], + }); } - function ge(e, t) { - var r = new Map(); + function Y(e) { + const i = (0, X.getBlockType)(e); + return i && i.title + ? i.title + : (e.split('/')[1] || e) + .split(/[-_]/) + .map(e => e.charAt(0).toUpperCase() + e.slice(1)) + .join(' '); + } + function ee(e, i) { + const t = new Map(); return ( - e.forEach(function (e) { - ('error' === t ? V(e.issues || []) : D(e.issues || [])).forEach(function (n) { - var o, - i, - a = 'error' === t ? n.errorMsg : n.warningMsg || n.errorMsg, - c = ''.concat(e.name, '|').concat(a); - (r.has(c) || - r.set(c, { - blockName: - ((o = e.name), - (i = (0, ye.getBlockType)(o)), - i && i.title - ? i.title - : (o.split('/')[1] || o) - .split(/[-_]/) - .map(function (e) { - return e.charAt(0).toUpperCase() + e.slice(1); - }) - .join(' ')), + e.forEach(e => { + ('error' === i ? N(e.issues || []) : L(e.issues || [])).forEach(n => { + const r = 'error' === i ? n.errorMsg : n.warningMsg || n.errorMsg, + s = `${e.name}|${r}`; + (t.has(s) || + t.set(s, { + blockName: Y(e.name), blockType: e.name, - message: a, + message: r, clientIds: [], }), e.clientId && - !r.get(c).clientIds.includes(e.clientId) && - r.get(c).clientIds.push(e.clientId)); + !t.get(s).clientIds.includes(e.clientId) && + t.get(s).clientIds.push(e.clientId)); }); }), - Array.from(r.values()) + Array.from(t.values()) ); } - function he(e, t) { - var r = new Map(); + function ie(e, i) { + const t = new Map(); return ( - e.forEach(function (e) { - ('error' === t ? V(e.issues || []) : D(e.issues || [])).forEach(function (n) { - var o = 'error' === t ? n.errorMsg : n.warningMsg || n.errorMsg, - i = ''.concat(e.metaKey, '|').concat(o); - r.has(i) || r.set(i, { metaKey: e.metaKey, message: o }); + e.forEach(e => { + ('error' === i ? N(e.issues || []) : L(e.issues || [])).forEach(n => { + const r = 'error' === i ? n.errorMsg : n.warningMsg || n.errorMsg, + s = `${e.metaKey}|${r}`; + t.has(s) || t.set(s, { metaKey: e.metaKey, message: r }); }); }), - Array.from(r.values()) + Array.from(t.values()) ); } - function we(e, t) { - var r = new Map(); + function te(e, i) { + const t = new Map(); return ( - e.forEach(function (e) { - var n = 'error' === t ? e.errorMsg : e.warningMsg || e.errorMsg, - o = n; - r.has(o) || r.set(o, { message: n, description: e.description }); + e.forEach(e => { + const n = 'error' === i ? e.errorMsg : e.warningMsg || e.errorMsg, + r = n; + t.has(r) || t.set(r, { message: n, description: e.description }); }), - Array.from(r.values()) + Array.from(t.values()) ); } - function Oe() { - var e = fe(), - t = e.invalidBlocks, - r = e.invalidMeta, - o = e.invalidEditorChecks, - i = (0, n.useDispatch)('core/block-editor').selectBlock, - a = (0, _.useRef)(null), - c = M(o, 'error'), - l = M(o, 'warning'), - u = ge(t, 'error'), - s = ge(t, 'warning'), - f = he(r, 'error'), - d = he(r, 'warning'), - p = we(c, 'error'), - m = we(l, 'warning'), - v = u.length + f.length + p.length, - y = s.length + d.length + m.length, - b = 'currentColor'; - v > 0 ? (b = '#d82000') : y > 0 && (b = '#dbc900'); - var g = React.createElement(be, { fill: b }), - h = function (e) { + function ne() { + const { invalidBlocks: e, invalidMeta: i, invalidEditorChecks: t } = H(), + { selectBlock: r } = (0, n.useDispatch)('core/block-editor'), + s = (0, E.useRef)(null), + o = _(t, 'error'), + a = _(t, 'warning'), + l = ee(e, 'error'), + c = ee(e, 'warning'), + d = ie(i, 'error'), + u = ie(i, 'warning'), + p = te(o, 'error'), + m = te(a, 'warning'), + g = l.length + d.length + p.length, + h = c.length + u.length + m.length; + let v = 'currentColor'; + g > 0 ? (v = '#d82000') : h > 0 && (v = '#dbc900'); + const f = (0, G.jsx)(Q, { fill: v }), + w = e => { e && - (i(e), - a.current && clearTimeout(a.current), - (a.current = setTimeout(function () { - var t = document.querySelector('[data-block="'.concat(e, '"]')); - (t || - (t = document.querySelector( - '[data-type][data-block="'.concat(e, '"]') - )), - t || - (t = document.querySelector( - '.wp-block[data-block="'.concat(e, '"]') - )), - t && t.scrollIntoView({ behavior: 'smooth', block: 'center' })); + (r(e), + s.current && clearTimeout(s.current), + (s.current = setTimeout(() => { + let i = document.querySelector(`[data-block="${e}"]`); + (i || (i = document.querySelector(`[data-type][data-block="${e}"]`)), + i || (i = document.querySelector(`.wp-block[data-block="${e}"]`)), + i && i.scrollIntoView({ behavior: 'smooth', block: 'center' })); }, 100))); }; return ( - (0, _.useEffect)(function () { - return function () { - a.current && clearTimeout(a.current); - }; - }, []), - 0 === v && 0 === y + (0, E.useEffect)( + () => () => { + s.current && clearTimeout(s.current); + }, + [] + ), + 0 === g && 0 === h ? null - : React.createElement( - pe.PluginSidebar, - { - name: 'validation-sidebar', - title: (0, ve.__)('Validation', 'validation-api'), - icon: g, - className: 'validation-api-validation-sidebar', - }, - v > 0 && - React.createElement( - me.PanelBody, - { - title: (0, ve.sprintf)( + : (0, G.jsxs)(z.PluginSidebar, { + name: 'validation-sidebar', + title: (0, U.__)('Validation', 'validation-api'), + icon: f, + className: 'validation-api-validation-sidebar', + children: [ + g > 0 && + (0, G.jsxs)(J.PanelBody, { + title: (0, U.sprintf)( /* translators: %d: number of errors */ /* translators: %d: number of errors */ - (0, ve.__)('Errors (%d)', 'validation-api'), - v + (0, U.__)('Errors (%d)', 'validation-api'), + g ), initialOpen: !0, className: 'validation-api-errors-panel', - }, - u.length > 0 && - React.createElement( - me.PanelRow, - null, - React.createElement( - 'div', - { className: 'validation-api-error-group' }, - React.createElement( - 'p', - { className: 'validation-api-error-subheading' }, - (0, ve.__)('Block Issues', 'validation-api') - ), - React.createElement( - 'ul', - { className: 'validation-api-error-list' }, - u.map(function (e, t) { - var r = e.clientIds.length, - n = r > 1 ? ' (x'.concat(r, ')') : ''; - return React.createElement( - 'li', - { key: 'block-error-'.concat(t) }, - React.createElement( - 'button', - { - type: 'button', - className: - 'validation-api-issue-link', - onClick: function () { - return h(e.clientIds[0]); - }, - }, - e.blockName - ), - ': ', - e.message, - n - ); - }) - ) - ) - ), - f.length > 0 && - React.createElement( - me.PanelRow, - null, - React.createElement( - 'div', - { className: 'validation-api-error-group' }, - React.createElement( - 'p', - { className: 'validation-api-error-subheading' }, - (0, ve.__)('Field Issues', 'validation-api') - ), - React.createElement( - 'ul', - { className: 'validation-api-error-list' }, - f.map(function (e, t) { - return React.createElement( - 'li', - { key: 'meta-error-'.concat(t) }, - e.message - ); - }) - ) - ) - ), - p.length > 0 && - React.createElement( - me.PanelRow, - null, - React.createElement( - 'div', - { className: 'validation-api-error-group' }, - React.createElement( - 'p', - { className: 'validation-api-error-subheading' }, - (0, ve.__)('Editor Issues', 'validation-api') - ), - React.createElement( - 'ul', - { className: 'validation-api-error-list' }, - p.map(function (e, t) { - return React.createElement( - 'li', - { key: 'editor-error-'.concat(t) }, - e.message - ); - }) - ) - ) - ) - ), - y > 0 && - React.createElement( - me.PanelBody, - { - title: (0, ve.sprintf)( + children: [ + l.length > 0 && + (0, G.jsx)(J.PanelRow, { + children: (0, G.jsxs)('div', { + className: 'validation-api-error-group', + children: [ + (0, G.jsx)('p', { + className: + 'validation-api-error-subheading', + children: (0, U.__)( + 'Block Issues', + 'validation-api' + ), + }), + (0, G.jsx)('ul', { + className: 'validation-api-error-list', + children: l.map((e, i) => { + const t = e.clientIds.length, + n = t > 1 ? ` (x${t})` : ''; + return (0, G.jsxs)( + 'li', + { + children: [ + (0, G.jsx)('button', { + type: 'button', + className: + 'validation-api-issue-link', + onClick: () => + w( + e + .clientIds[0] + ), + children: + e.blockName, + }), + ': ', + e.message, + n, + ], + }, + `block-error-${i}` + ); + }), + }), + ], + }), + }), + d.length > 0 && + (0, G.jsx)(J.PanelRow, { + children: (0, G.jsxs)('div', { + className: 'validation-api-error-group', + children: [ + (0, G.jsx)('p', { + className: + 'validation-api-error-subheading', + children: (0, U.__)( + 'Field Issues', + 'validation-api' + ), + }), + (0, G.jsx)('ul', { + className: 'validation-api-error-list', + children: d.map((e, i) => + (0, G.jsx)( + 'li', + { children: e.message }, + `meta-error-${i}` + ) + ), + }), + ], + }), + }), + p.length > 0 && + (0, G.jsx)(J.PanelRow, { + children: (0, G.jsxs)('div', { + className: 'validation-api-error-group', + children: [ + (0, G.jsx)('p', { + className: + 'validation-api-error-subheading', + children: (0, U.__)( + 'Editor Issues', + 'validation-api' + ), + }), + (0, G.jsx)('ul', { + className: 'validation-api-error-list', + children: p.map((e, i) => + (0, G.jsx)( + 'li', + { children: e.message }, + `editor-error-${i}` + ) + ), + }), + ], + }), + }), + ], + }), + h > 0 && + (0, G.jsxs)(J.PanelBody, { + title: (0, U.sprintf)( /* translators: %d: number of warnings */ /* translators: %d: number of warnings */ - (0, ve.__)('Warnings (%d)', 'validation-api'), - y + (0, U.__)('Warnings (%d)', 'validation-api'), + h ), initialOpen: !0, className: 'validation-api-warnings-panel', - }, - s.length > 0 && - React.createElement( - me.PanelRow, - null, - React.createElement( - 'div', - { className: 'validation-api-warning-group' }, - React.createElement( - 'p', - { className: 'validation-api-warning-subheading' }, - (0, ve.__)('Block Issues', 'validation-api') - ), - React.createElement( - 'ul', - { className: 'validation-api-warning-list' }, - s.map(function (e, t) { - var r = e.clientIds.length, - n = r > 1 ? ' (x'.concat(r, ')') : ''; - return React.createElement( - 'li', - { key: 'block-warning-'.concat(t) }, - React.createElement( - 'button', - { - type: 'button', - className: - 'validation-api-issue-link', - onClick: function () { - return h(e.clientIds[0]); - }, - }, - e.blockName - ), - ': ', - e.message, - n - ); - }) - ) - ) - ), - d.length > 0 && - React.createElement( - me.PanelRow, - null, - React.createElement( - 'div', - { className: 'validation-api-warning-group' }, - React.createElement( - 'p', - { className: 'validation-api-warning-subheading' }, - (0, ve.__)('Field Issues', 'validation-api') - ), - React.createElement( - 'ul', - { className: 'validation-api-warning-list' }, - d.map(function (e, t) { - return React.createElement( - 'li', - { key: 'meta-warning-'.concat(t) }, - e.message - ); - }) - ) - ) - ), - m.length > 0 && - React.createElement( - me.PanelRow, - null, - React.createElement( - 'div', - { className: 'validation-api-warning-group' }, - React.createElement( - 'p', - { className: 'validation-api-warning-subheading' }, - (0, ve.__)('Editor Issues', 'validation-api') - ), - React.createElement( - 'ul', - { className: 'validation-api-warning-list' }, - m.map(function (e, t) { - return React.createElement( - 'li', - { key: 'editor-warning-'.concat(t) }, - e.message - ); - }) - ) - ) - ) - ) - ) + children: [ + c.length > 0 && + (0, G.jsx)(J.PanelRow, { + children: (0, G.jsxs)('div', { + className: 'validation-api-warning-group', + children: [ + (0, G.jsx)('p', { + className: + 'validation-api-warning-subheading', + children: (0, U.__)( + 'Block Issues', + 'validation-api' + ), + }), + (0, G.jsx)('ul', { + className: + 'validation-api-warning-list', + children: c.map((e, i) => { + const t = e.clientIds.length, + n = t > 1 ? ` (x${t})` : ''; + return (0, G.jsxs)( + 'li', + { + children: [ + (0, G.jsx)('button', { + type: 'button', + className: + 'validation-api-issue-link', + onClick: () => + w( + e + .clientIds[0] + ), + children: + e.blockName, + }), + ': ', + e.message, + n, + ], + }, + `block-warning-${i}` + ); + }), + }), + ], + }), + }), + u.length > 0 && + (0, G.jsx)(J.PanelRow, { + children: (0, G.jsxs)('div', { + className: 'validation-api-warning-group', + children: [ + (0, G.jsx)('p', { + className: + 'validation-api-warning-subheading', + children: (0, U.__)( + 'Field Issues', + 'validation-api' + ), + }), + (0, G.jsx)('ul', { + className: + 'validation-api-warning-list', + children: u.map((e, i) => + (0, G.jsx)( + 'li', + { children: e.message }, + `meta-warning-${i}` + ) + ), + }), + ], + }), + }), + m.length > 0 && + (0, G.jsx)(J.PanelRow, { + children: (0, G.jsxs)('div', { + className: 'validation-api-warning-group', + children: [ + (0, G.jsx)('p', { + className: + 'validation-api-warning-subheading', + children: (0, U.__)( + 'Editor Issues', + 'validation-api' + ), + }), + (0, G.jsx)('ul', { + className: + 'validation-api-warning-list', + children: m.map((e, i) => + (0, G.jsx)( + 'li', + { children: e.message }, + `editor-warning-${i}` + ) + ), + }), + ], + }), + }), + ], + }), + ], + }) ); } - function Ee() { - return (se(), null); + function re() { + return (q(), null); } - function je() { - var e, t, r, o, i, a, c, l, u, s, f, d, p; + function se() { return ( - (e = q()), - (t = 'post-editor' === e || 'post-editor-template' === e), - (r = (0, n.useDispatch)('core/editor')), - (o = r.lockPostSaving), - (i = r.unlockPostSaving), - (a = r.lockPostAutosaving), - (c = r.unlockPostAutosaving), - (l = r.disablePublishSidebar), - (u = r.enablePublishSidebar), - (s = fe()), - (f = s.invalidBlocks), - (d = s.invalidMeta), - (p = s.invalidEditorChecks), - (0, _.useEffect)( - function () { - if (t && o && i) { - var e = f.some(function (e) { - return 'error' === e.mode; - }), - r = d.some(function (e) { - return e.hasErrors; - }), - n = x(p); - e || r || n ? (o(de), a && a(de), l && l()) : (i(de), c && c(de), u && u()); - } - }, - [f, d, p, o, i, a, c, l, u, t] - ), - (0, _.useEffect)( - function () { - if (t && document.body) { - var e = f.some(function (e) { - return 'error' === e.mode; - }), - r = f.some(function (e) { - return 'warning' === e.mode; - }), - n = d.some(function (e) { - return e.hasErrors; - }), - o = d.some(function (e) { - return e.hasWarnings && !e.hasErrors; - }), - i = x(p), - a = F(p), - c = e || n || i, - l = !c && (r || o || a); + (function () { + const e = A(), + i = 'post-editor' === e || 'post-editor-template' === e, + { + lockPostSaving: t, + unlockPostSaving: r, + lockPostAutosaving: s, + unlockPostAutosaving: o, + disablePublishSidebar: a, + enablePublishSidebar: l, + } = (0, n.useDispatch)('core/editor'), + { invalidBlocks: c, invalidMeta: d, invalidEditorChecks: u } = H(); + ((0, E.useEffect)(() => { + if (!i) return; + if (!t || !r) return; + const e = c.some(e => 'error' === e.mode), + n = d.some(e => e.hasErrors), + p = C(u); + e || n || p ? (t(Z), s && s(Z), a && a()) : (r(Z), o && o(Z), l && l()); + }, [c, d, u, t, r, s, o, a, l, i]), + (0, E.useEffect)(() => { + if (!i) return; + if (!document.body) return; + const e = c.some(e => 'error' === e.mode), + t = c.some(e => 'warning' === e.mode), + n = d.some(e => e.hasErrors), + r = d.some(e => e.hasWarnings && !e.hasErrors), + s = C(u), + o = V(u), + a = e || n || s, + l = !a && (t || r || o); return ( - c + a ? (document.body.classList.add('has-validation-errors'), document.body.classList.remove('has-validation-warnings')) : l @@ -1348,7 +713,7 @@ 'has-validation-errors', 'has-validation-warnings' ), - function () { + () => { document.body && document.body.classList.remove( 'has-validation-errors', @@ -1356,741 +721,180 @@ ); } ); - } - }, - [f, d, p, t] - ), + }, [c, d, u, i])); + })(), null ); } - (0, A.registerPlugin)('core-validation', { + (0, I.registerPlugin)('core-validation', { render: function () { - return React.createElement( - React.Fragment, - null, - React.createElement(Ee, null), - React.createElement(je, null), - React.createElement(Oe, null) - ); + return (0, G.jsxs)(G.Fragment, { + children: [(0, G.jsx)(re, {}), (0, G.jsx)(se, {}), (0, G.jsx)(ne, {})], + }); }, }); - const Se = window.wp.compose, - ke = window.wp.blockEditor; - function Pe(e, t) { - (null == t || t > e.length) && (t = e.length); - for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; - return n; - } - function Re(e, t) { - (null == t || t > e.length) && (t = e.length); - for (var r = 0, n = Array(t); r < t; r++) n[r] = e[r]; - return n; - } - function Ie(e) { - var t, - r, - n = e.issues, - o = - ((t = (0, _.useState)(!1)), - (r = 2), - (function (e) { - if (Array.isArray(e)) return e; - })(t) || - (function (e, t) { - var r = - null == e - ? null - : ('undefined' != typeof Symbol && e[Symbol.iterator]) || - e['@@iterator']; - if (null != r) { - var n, - o, - i, - a, - c = [], - l = !0, - u = !1; - try { - if (((i = (r = r.call(e)).next), 0 === t)) { - if (Object(r) !== r) return; - l = !1; - } else - for ( - ; - !(l = (n = i.call(r)).done) && - (c.push(n.value), c.length !== t); - l = !0 - ); - } catch (e) { - ((u = !0), (o = e)); - } finally { - try { - if ( - !l && - null != r.return && - ((a = r.return()), Object(a) !== a) - ) - return; - } finally { - if (u) throw o; - } - } - return c; - } - })(t, r) || - (function (e, t) { - if (e) { - if ('string' == typeof e) return Re(e, t); - var r = {}.toString.call(e).slice(8, -1); - return ( - 'Object' === r && e.constructor && (r = e.constructor.name), - 'Map' === r || 'Set' === r - ? Array.from(e) - : 'Arguments' === r || - /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r) - ? Re(e, t) - : void 0 - ); - } - })(t, r) || - (function () { - throw new TypeError( - 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })()), - i = o[0], - a = o[1]; - if (!n || 0 === n.length) return null; - var c = x(n), - l = V(n), - u = D(n), - s = c - ? React.createElement(be, { fill: '#d82000' }) - : React.createElement(be, { fill: '#dbc900' }); - return React.createElement( - React.Fragment, - null, - React.createElement(me.ToolbarButton, { - icon: s, - onClick: function () { - return a(!0); - }, - label: (0, ve.__)('View block issues or concerns', 'validation-api'), - className: 'validation-api-toolbar-button', - isCompact: !0, - }), - i && - React.createElement( - me.Modal, - { - title: (0, ve.__)('Issues or Concerns', 'validation-api'), - onRequestClose: function () { - return a(!1); - }, + const oe = window.wp.compose, + ae = window.wp.blockEditor; + function le({ issues: e }) { + const [i, t] = (0, E.useState)(!1); + if (!e || 0 === e.length) return null; + const n = C(e), + r = N(e), + s = L(e), + o = n ? (0, G.jsx)(Q, { fill: '#d82000' }) : (0, G.jsx)(Q, { fill: '#dbc900' }); + return (0, G.jsxs)(G.Fragment, { + children: [ + (0, G.jsx)(J.ToolbarButton, { + icon: o, + onClick: () => t(!0), + label: (0, U.__)('View block issues or concerns', 'validation-api'), + className: 'validation-api-toolbar-button', + isCompact: !0, + }), + i && + (0, G.jsx)(J.Modal, { + title: (0, U.__)('Issues or Concerns', 'validation-api'), + onRequestClose: () => t(!1), className: 'validation-api-block-indicator-modal', - }, - React.createElement( - 'div', - { className: 'validation-api-indicator-modal-content' }, - l.length > 0 && - React.createElement( - 'div', - { - className: - 'validation-api-indicator-section validation-api-indicator-errors', - }, - React.createElement( - 'h2', - { className: 'validation-api-indicator-section-title' }, - React.createElement('span', { - className: 'validation-api-indicator-section-title-circle', + children: (0, G.jsxs)('div', { + className: 'validation-api-indicator-modal-content', + children: [ + r.length > 0 && + (0, G.jsxs)('div', { + className: + 'validation-api-indicator-section validation-api-indicator-errors', + children: [ + (0, G.jsxs)('h2', { + className: 'validation-api-indicator-section-title', + children: [ + (0, G.jsx)('span', { + className: + 'validation-api-indicator-section-title-circle', + }), + (0, U.__)('Errors', 'validation-api'), + ], + }), + (0, G.jsx)('ul', { + children: r.map((e, i) => + (0, G.jsx)( + 'li', + { children: e.errorMsg }, + `error-${i}` + ) + ), + }), + ], }), - (0, ve.__)('Errors', 'validation-api') - ), - React.createElement( - 'ul', - null, - l.map(function (e, t) { - return React.createElement( - 'li', - { key: 'error-'.concat(t) }, - e.errorMsg - ); - }) - ) - ), - u.length > 0 && - React.createElement( - 'div', - { - className: - 'validation-api-indicator-section validation-api-indicator-warnings', - }, - React.createElement( - 'h2', - { className: 'validation-api-indicator-section-title' }, - React.createElement('span', { - className: 'validation-api-indicator-section-title-circle', + s.length > 0 && + (0, G.jsxs)('div', { + className: + 'validation-api-indicator-section validation-api-indicator-warnings', + children: [ + (0, G.jsxs)('h2', { + className: 'validation-api-indicator-section-title', + children: [ + (0, G.jsx)('span', { + className: + 'validation-api-indicator-section-title-circle', + }), + (0, U.__)('Warnings', 'validation-api'), + ], + }), + (0, G.jsx)('ul', { + children: s.map((e, i) => + (0, G.jsx)( + 'li', + { children: e.warningMsg || e.errorMsg }, + `warning-${i}` + ) + ), + }), + ], }), - (0, ve.__)('Warnings', 'validation-api') - ), - React.createElement( - 'ul', - null, - u.map(function (e, t) { - return React.createElement( - 'li', - { key: 'warning-'.concat(t) }, - e.warningMsg || e.errorMsg - ); - }) - ) - ) - ) - ) - ); - } - function Ae(e) { - return ( - (Ae = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (e) { - return typeof e; - } - : function (e) { - return e && - 'function' == typeof Symbol && - e.constructor === Symbol && - e !== Symbol.prototype - ? 'symbol' - : typeof e; + ], }), - Ae(e) - ); - } - function _e(e, t) { - var r = Object.keys(e); - if (Object.getOwnPropertySymbols) { - var n = Object.getOwnPropertySymbols(e); - (t && - (n = n.filter(function (t) { - return Object.getOwnPropertyDescriptor(e, t).enumerable; - })), - r.push.apply(r, n)); - } - return r; - } - function Be(e) { - for (var t = 1; t < arguments.length; t++) { - var r = null != arguments[t] ? arguments[t] : {}; - t % 2 - ? _e(Object(r), !0).forEach(function (t) { - Ne(e, t, r[t]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : _e(Object(r)).forEach(function (t) { - Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); - }); - } - return e; - } - function Ne(e, t, r) { - return ( - (t = (function (e) { - var t = (function (e) { - if ('object' != Ae(e) || !e) return e; - var t = e[Symbol.toPrimitive]; - if (void 0 !== t) { - var r = t.call(e, 'string'); - if ('object' != Ae(r)) return r; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return String(e); - })(e); - return 'symbol' == Ae(t) ? t : t + ''; - })(t)) in e - ? Object.defineProperty(e, t, { - value: r, - enumerable: !0, - configurable: !0, - writable: !0, - }) - : (e[t] = r), - e - ); + }), + ], + }); } - var Te = (0, Se.createHigherOrderComponent)(function (e) { - return function (t) { - var r = t.clientId, - i = t.attributes, - a = (0, n.useSelect)( - function (e) { - return e('core/block-editor').getBlock(r); - }, - [r] - ), - c = (0, n.useDispatch)(o), - l = c.setBlockValidation, - u = c.clearBlockValidation, - s = (function (e, t) { - var r, - n, - o = (arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}) - .delay, - i = void 0 === o ? 300 : o, - a = - ((r = (0, _.useState)(function () { - return e(); - })), - (n = 2), - (function (e) { - if (Array.isArray(e)) return e; - })(r) || - (function (e, t) { - var r = - null == e - ? null - : ('undefined' != typeof Symbol && - e[Symbol.iterator]) || - e['@@iterator']; - if (null != r) { - var n, - o, - i, - a, - c = [], - l = !0, - u = !1; - try { - if (((i = (r = r.call(e)).next), 0 === t)) { - if (Object(r) !== r) return; - l = !1; - } else - for ( - ; - !(l = (n = i.call(r)).done) && - (c.push(n.value), c.length !== t); - l = !0 - ); - } catch (e) { - ((u = !0), (o = e)); - } finally { - try { - if ( - !l && - null != r.return && - ((a = r.return()), Object(a) !== a) - ) - return; - } finally { - if (u) throw o; - } - } - return c; - } - })(r, n) || - (function (e, t) { - if (e) { - if ('string' == typeof e) return Pe(e, t); - var r = {}.toString.call(e).slice(8, -1); - return ( - 'Object' === r && - e.constructor && - (r = e.constructor.name), - 'Map' === r || 'Set' === r - ? Array.from(e) - : 'Arguments' === r || - /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test( - r - ) - ? Pe(e, t) - : void 0 - ); - } - })(r, n) || - (function () { - throw new TypeError( - 'Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.' - ); - })()), - c = a[0], - l = a[1], - u = (0, _.useRef)(null), - s = (0, _.useRef)(!0); + const ce = (0, oe.createHigherOrderComponent)( + e => i => { + const { clientId: t, attributes: s } = i, + o = (0, n.useSelect)(e => e('core/block-editor').getBlock(t), [t]), + { setBlockValidation: a, clearBlockValidation: l } = (0, n.useDispatch)(r), + c = (function (e, i, t = {}) { + const { delay: n = 300 } = t, + [r, s] = (0, E.useState)(() => e()), + o = (0, E.useRef)(null), + a = (0, E.useRef)(!0); return ( - (0, _.useEffect)(function () { - return s.current - ? ((s.current = !1), void l(e())) - : (u.current && clearTimeout(u.current), - (u.current = setTimeout(function () { - l(e()); - }, i)), - function () { - u.current && clearTimeout(u.current); - }); - }, t), - c + (0, E.useEffect)( + () => + a.current + ? ((a.current = !1), void s(e())) + : (o.current && clearTimeout(o.current), + (o.current = setTimeout(() => { + s(e()); + }, n)), + () => { + o.current && clearTimeout(o.current); + }), + i + ), + r ); })( - function () { - if (!a) return { isValid: !0, issues: [], mode: 'none' }; - var e = Be(Be({}, a), {}, { attributes: i || a.attributes }); - return Z(e); + () => { + if (!o) return { isValid: !0, issues: [], mode: 'none' }; + const e = { ...o, attributes: s || o.attributes }; + return R(e); }, - [a, i], + [o, s], { delay: 300 } ); return ( - (0, _.useEffect)( - function () { - return ( - l(r, s), - function () { - return u(r); - } - ); - }, - [r, s, l, u] - ), - React.createElement( - React.Fragment, - null, - React.createElement(e, t), - !s.isValid && - React.createElement( - ke.BlockControls, - { group: 'block' }, - React.createElement(Ie, { issues: s.issues }) - ) - ) - ); - }; - }, 'withErrorHandling'); - function Ce(e) { - return ( - (Ce = - 'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator - ? function (e) { - return typeof e; - } - : function (e) { - return e && - 'function' == typeof Symbol && - e.constructor === Symbol && - e !== Symbol.prototype - ? 'symbol' - : typeof e; - }), - Ce(e) - ); - } - function Le() { - return ( - (Le = Object.assign - ? Object.assign.bind() - : function (e) { - for (var t = 1; t < arguments.length; t++) { - var r = arguments[t]; - for (var n in r) ({}).hasOwnProperty.call(r, n) && (e[n] = r[n]); - } - return e; - }), - Le.apply(null, arguments) - ); - } - function Me(e, t) { - var r = Object.keys(e); - if (Object.getOwnPropertySymbols) { - var n = Object.getOwnPropertySymbols(e); - (t && - (n = n.filter(function (t) { - return Object.getOwnPropertyDescriptor(e, t).enumerable; - })), - r.push.apply(r, n)); - } - return r; - } - function Ve(e) { - for (var t = 1; t < arguments.length; t++) { - var r = null != arguments[t] ? arguments[t] : {}; - t % 2 - ? Me(Object(r), !0).forEach(function (t) { - De(e, t, r[t]); - }) - : Object.getOwnPropertyDescriptors - ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(r)) - : Me(Object(r)).forEach(function (t) { - Object.defineProperty(e, t, Object.getOwnPropertyDescriptor(r, t)); - }); - } - return e; - } - function De(e, t, r) { - return ( - (t = (function (e) { - var t = (function (e) { - if ('object' != Ce(e) || !e) return e; - var t = e[Symbol.toPrimitive]; - if (void 0 !== t) { - var r = t.call(e, 'string'); - if ('object' != Ce(r)) return r; - throw new TypeError('@@toPrimitive must return a primitive value.'); - } - return String(e); - })(e); - return 'symbol' == Ce(t) ? t : t + ''; - })(t)) in e - ? Object.defineProperty(e, t, { - value: r, - enumerable: !0, - configurable: !0, - writable: !0, - }) - : (e[t] = r), - e - ); - } - function xe() { - var e, - t, - r = 'function' == typeof Symbol ? Symbol : {}, - n = r.iterator || '@@iterator', - o = r.toStringTag || '@@toStringTag'; - function i(r, n, o, i) { - var l = n && n.prototype instanceof c ? n : c, - u = Object.create(l.prototype); - return ( - Fe( - u, - '_invoke', - (function (r, n, o) { - var i, - c, - l, - u = 0, - s = o || [], - f = !1, - d = { - p: 0, - n: 0, - v: e, - a: p, - f: p.bind(e, 4), - d: function (t, r) { - return ((i = t), (c = 0), (l = e), (d.n = r), a); - }, - }; - function p(r, n) { - for (c = r, l = n, t = 0; !f && u && !o && t < s.length; t++) { - var o, - i = s[t], - p = d.p, - m = i[2]; - r > 3 - ? (o = m === n) && - ((l = i[(c = i[4]) ? 5 : ((c = 3), 3)]), (i[4] = i[5] = e)) - : i[0] <= p && - ((o = r < 2 && p < i[1]) - ? ((c = 0), (d.v = n), (d.n = i[1])) - : p < m && - (o = r < 3 || i[0] > n || n > m) && - ((i[4] = r), (i[5] = n), (d.n = m), (c = 0))); - } - if (o || r > 1) return a; - throw ((f = !0), n); - } - return function (o, s, m) { - if (u > 1) throw TypeError('Generator is already running'); - for ( - f && 1 === s && p(s, m), c = s, l = m; - (t = c < 2 ? e : l) || !f; - ) { - i || - (c - ? c < 3 - ? (c > 1 && (d.n = -1), p(c, l)) - : (d.n = l) - : (d.v = l)); - try { - if (((u = 2), i)) { - if ((c || (o = 'next'), (t = i[o]))) { - if (!(t = t.call(i, l))) - throw TypeError('iterator result is not an object'); - if (!t.done) return t; - ((l = t.value), c < 2 && (c = 0)); - } else - (1 === c && (t = i.return) && t.call(i), - c < 2 && - ((l = TypeError( - "The iterator does not provide a '" + - o + - "' method" - )), - (c = 1))); - i = e; - } else if ((t = (f = d.n < 0) ? l : r.call(n, d)) !== a) break; - } catch (t) { - ((i = e), (c = 1), (l = t)); - } finally { - u = 1; - } - } - return { value: t, done: f }; - }; - })(r, o, i), - !0 - ), - u - ); - } - var a = {}; - function c() {} - function l() {} - function u() {} - t = Object.getPrototypeOf; - var s = [][n] - ? t(t([][n]())) - : (Fe((t = {}), n, function () { - return this; - }), - t), - f = (u.prototype = c.prototype = Object.create(s)); - function d(e) { - return ( - Object.setPrototypeOf - ? Object.setPrototypeOf(e, u) - : ((e.__proto__ = u), Fe(e, o, 'GeneratorFunction')), - (e.prototype = Object.create(f)), - e + (0, E.useEffect)(() => (a(t, c), () => l(t)), [t, c, a, l]), + (0, G.jsxs)(G.Fragment, { + children: [ + (0, G.jsx)(e, { ...i }), + !c.isValid && + (0, G.jsx)(ae.BlockControls, { + group: 'block', + children: (0, G.jsx)(le, { issues: c.issues }), + }), + ], + }) ); - } - return ( - (l.prototype = u), - Fe(f, 'constructor', u), - Fe(u, 'constructor', l), - (l.displayName = 'GeneratorFunction'), - Fe(u, o, 'GeneratorFunction'), - Fe(f), - Fe(f, o, 'Generator'), - Fe(f, n, function () { - return this; - }), - Fe(f, 'toString', function () { - return '[object Generator]'; - }), - (xe = function () { - return { w: i, m: d }; - })() - ); - } - function Fe(e, t, r, n) { - var o = Object.defineProperty; - try { - o({}, '', {}); - } catch (e) { - o = 0; - } - ((Fe = function (e, t, r, n) { - function i(t, r) { - Fe(e, t, function (e) { - return this._invoke(t, r, e); - }); - } - t - ? o - ? o(e, t, { value: r, enumerable: !n, configurable: !n, writable: !n }) - : (e[t] = r) - : (i('next', 0), i('throw', 1), i('return', 2)); - }), - Fe(e, t, r, n)); - } - function Ke(e, t, r, n, o, i, a) { - try { - var c = e[i](a), - l = c.value; - } catch (e) { - return void r(e); - } - c.done ? t(l) : Promise.resolve(l).then(n, o); - } - ((0, B.addFilter)('editor.BlockEdit', 'validation-api/with-error-handling', Te), + }, + 'withErrorHandling' + ); + ((0, B.addFilter)('editor.BlockEdit', 'validation-api/with-error-handling', ce), (0, B.addFilter)( 'editor.BlockListBlock', 'validation-api/with-block-validation-classes', function (e) { - return function (t) { - var r = (0, n.useSelect)( - function (e) { - return e(o).getBlockValidation(t.clientId); - }, - [t.clientId] + return i => { + const t = (0, n.useSelect)( + e => e(r).getBlockValidation(i.clientId), + [i.clientId] ); - if ('none' === r.mode) return React.createElement(e, t); - var i = - 'error' === r.mode + if ('none' === t.mode) return (0, G.jsx)(e, { ...i }); + const s = + 'error' === t.mode ? 'validation-api-block-error' : 'validation-api-block-warning', - a = t.wrapperProps || {}, - c = Ve( - Ve({}, a), - {}, - { className: [a.className, i].filter(Boolean).join(' ') } - ); - return React.createElement(e, Le({}, t, { wrapperProps: c })); + o = i.wrapperProps || {}, + a = { ...o, className: [o.className, s].filter(Boolean).join(' ') }; + return (0, G.jsx)(e, { ...i, wrapperProps: a }); }; } ), - (0, B.addFilter)( - 'editor.preSavePost', - 'validation-api/pre-save-gate', - (function () { - var e, - t = - ((e = xe().m(function e(t) { - var r; - return xe().w(function (e) { - for (;;) - switch (e.n) { - case 0: - if ( - !( - (r = (0, n.select)(o)) && - r.hasErrors && - r.hasErrors() - ) - ) { - e.n = 1; - break; - } - throw new Error( - (0, ve.__)( - 'Validation errors must be resolved before saving.', - 'validation-api' - ) - ); - case 1: - return e.a(2, t); - } - }, e); - })), - function () { - var t = this, - r = arguments; - return new Promise(function (n, o) { - var i = e.apply(t, r); - function a(e) { - Ke(i, n, o, a, c, 'next', e); - } - function c(e) { - Ke(i, n, o, a, c, 'throw', e); - } - a(void 0); - }); - }); - return function (_x) { - return t.apply(this, arguments); - }; - })() - )); + (0, B.addFilter)('editor.preSavePost', 'validation-api/pre-save-gate', async e => { + const i = (0, n.select)(r); + if (i && i.hasErrors && i.hasErrors()) + throw new Error( + (0, U.__)('Validation errors must be resolved before saving.', 'validation-api') + ); + return e; + })); })(); diff --git a/src/store/constants.js b/src/store/constants.js deleted file mode 100644 index f8c3c2c..0000000 --- a/src/store/constants.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Store constants for the core/validation data store. - */ - -export const STORE_NAME = 'core/validation'; - -export const SET_INVALID_BLOCKS = 'SET_INVALID_BLOCKS'; -export const SET_INVALID_META = 'SET_INVALID_META'; -export const SET_INVALID_EDITOR_CHECKS = 'SET_INVALID_EDITOR_CHECKS'; -export const SET_BLOCK_VALIDATION = 'SET_BLOCK_VALIDATION'; -export const CLEAR_BLOCK_VALIDATION = 'CLEAR_BLOCK_VALIDATION'; - -export const DEFAULT_STATE = { - blocks: [], - meta: [], - editor: [], - blockValidation: {}, -}; - -export const DEFAULT_BLOCK_RESULT = Object.freeze({ mode: 'none', issues: [] }); diff --git a/src/store/constants.ts b/src/store/constants.ts new file mode 100644 index 0000000..6327bc2 --- /dev/null +++ b/src/store/constants.ts @@ -0,0 +1,95 @@ +/** + * Store constants and types for the core/validation data store. + * + * This file is the first TypeScript module in the package. Type definitions + * exported here describe the shapes consumed by selectors, actions, and + * reducer. Other modules continue to run as JavaScript via babel-preset- + * typescript; they benefit from the types when edited in a TS-aware editor + * without requiring .ts migration themselves. + */ + +export const STORE_NAME = 'core/validation'; + +export const SET_INVALID_BLOCKS = 'SET_INVALID_BLOCKS' as const; +export const SET_INVALID_META = 'SET_INVALID_META' as const; +export const SET_INVALID_EDITOR_CHECKS = 'SET_INVALID_EDITOR_CHECKS' as const; +export const SET_BLOCK_VALIDATION = 'SET_BLOCK_VALIDATION' as const; +export const CLEAR_BLOCK_VALIDATION = 'CLEAR_BLOCK_VALIDATION' as const; + +/** + * Severity mode for a block-level validation result. + */ +export type ValidationMode = 'error' | 'warning' | 'none'; + +/** + * Severity type for an individual validation issue. + */ +export type IssueType = 'error' | 'warning'; + +/** + * A single validation issue produced by `createIssue` in issue-helpers. + * + * `metaKey` is populated only for meta-scope issues. + */ +export interface ValidationIssue { + check: string; + checkName: string; + type: IssueType; + priority: number; + message: string; + errorMsg: string; + warningMsg: string; + metaKey?: string; +} + +/** + * Validation result for a single block. Produced by `validateBlock` and + * stored per-clientId in `state.blockValidation`. + */ +export interface BlockValidationResult { + mode: ValidationMode; + issues: ValidationIssue[]; + isValid?: boolean; + hasErrors?: boolean; + hasWarnings?: boolean; + clientId?: string; + name?: string; +} + +/** + * Validation result for a single meta field. Produced by + * `validateAllMetaChecks` and stored in `state.meta`. + */ +export interface MetaValidationResult { + metaKey: string; + isValid: boolean; + hasErrors: boolean; + hasWarnings: boolean; + issues: ValidationIssue[]; +} + +/** + * Top-level state shape for the `core/validation` store. + */ +export interface State { + blocks: BlockValidationResult[]; + meta: MetaValidationResult[]; + editor: ValidationIssue[]; + blockValidation: Record; +} + +export const DEFAULT_STATE: State = { + blocks: [], + meta: [], + editor: [], + blockValidation: {}, +}; + +/** + * Returned by `getBlockValidation` when no result has been dispatched for + * a given clientId. Frozen so consumers can compare by reference. + */ +export const DEFAULT_BLOCK_RESULT: Readonly = Object.freeze({ + mode: 'none' as ValidationMode, + issues: [], +}); From f78624e6fdb323b625ad5ae65acdda95fecbed6d Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Sat, 18 Apr 2026 10:54:46 -0400 Subject: [PATCH 09/11] polish: add Jest unit tests for store + issue-helpers (56 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover the pure logic layer — store reducer, action creators, selectors, and the issue-helper factories — with Jest. No mocks required; these are all pure functions consuming plain inputs. Test infrastructure is zero-config via @wordpress/scripts (test-unit-js uses its bundled Jest preset). Added `test` and `test:watch` npm scripts and an .eslintrc override so Jest globals (describe/it/expect) don't trip no-undef in __tests__ directories. Coverage: - src/store/__tests__/reducer.test.js — default state, all 5 action types, unknown-action pass-through, CLEAR_BLOCK_VALIDATION no-op edge case - src/store/__tests__/actions.test.js — each action creator's shape - src/store/__tests__/selectors.test.js — all 6 selectors including error-precedence logic in hasErrors/hasWarnings and DEFAULT_BLOCK_RESULT fallback - src/utils/__tests__/issue-helpers.test.js — all 8 exports, including the PHP-style snake_case to camelCase transform in createIssue Deferred to a future polish pass (need filter/store mocking): - validate-block, validate-meta, validate-editor - Custom hooks (useMetaField, useMetaValidation, useInvalid* hooks) - Integration / e2e tests via @wordpress/env Run with `pnpm test` (~1s). 56/56 pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .eslintrc.json | 10 +- package.json | 2 + src/store/__tests__/actions.test.js | 59 ++++++++ src/store/__tests__/reducer.test.js | 125 +++++++++++++++++ src/store/__tests__/selectors.test.js | 131 +++++++++++++++++ src/utils/__tests__/issue-helpers.test.js | 163 ++++++++++++++++++++++ 6 files changed, 489 insertions(+), 1 deletion(-) create mode 100644 src/store/__tests__/actions.test.js create mode 100644 src/store/__tests__/reducer.test.js create mode 100644 src/store/__tests__/selectors.test.js create mode 100644 src/utils/__tests__/issue-helpers.test.js diff --git a/.eslintrc.json b/.eslintrc.json index a3530fe..d3d2c14 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,5 +13,13 @@ "rules": { "@wordpress/no-global-active-element": "warn", "@wordpress/no-unsafe-wp-apis": "off" - } + }, + "overrides": [ + { + "files": ["**/__tests__/**/*.js", "**/*.test.js"], + "env": { + "jest": true + } + } + ] } diff --git a/package.json b/package.json index 22f7dc7..e3638a2 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "scripts": { "build": "wp-scripts build", "start": "wp-scripts start", + "test": "wp-scripts test-unit-js", + "test:watch": "wp-scripts test-unit-js --watch", "format": "pnpm format:js && pnpm format:php && pnpm format:css", "format:js": "wp-scripts lint-js --fix", "format:php": "composer run format", diff --git a/src/store/__tests__/actions.test.js b/src/store/__tests__/actions.test.js new file mode 100644 index 0000000..d783bc1 --- /dev/null +++ b/src/store/__tests__/actions.test.js @@ -0,0 +1,59 @@ +/** + * Internal dependencies + */ +import { + setInvalidBlocks, + setInvalidMeta, + setInvalidEditorChecks, + setBlockValidation, + clearBlockValidation, +} from '../actions'; +import { + SET_INVALID_BLOCKS, + SET_INVALID_META, + SET_INVALID_EDITOR_CHECKS, + SET_BLOCK_VALIDATION, + CLEAR_BLOCK_VALIDATION, +} from '../constants'; + +describe('core/validation action creators', () => { + it('setInvalidBlocks returns SET_INVALID_BLOCKS with results', () => { + const results = [{ clientId: 'a', mode: 'error', issues: [] }]; + expect(setInvalidBlocks(results)).toEqual({ + type: SET_INVALID_BLOCKS, + results, + }); + }); + + it('setInvalidMeta returns SET_INVALID_META with results', () => { + const results = [{ metaKey: 'seo', hasErrors: true, issues: [] }]; + expect(setInvalidMeta(results)).toEqual({ + type: SET_INVALID_META, + results, + }); + }); + + it('setInvalidEditorChecks returns SET_INVALID_EDITOR_CHECKS with issues', () => { + const issues = [{ type: 'error', errorMsg: 'Missing heading' }]; + expect(setInvalidEditorChecks(issues)).toEqual({ + type: SET_INVALID_EDITOR_CHECKS, + issues, + }); + }); + + it('setBlockValidation returns SET_BLOCK_VALIDATION with clientId and result', () => { + const result = { mode: 'warning', issues: [] }; + expect(setBlockValidation('abc', result)).toEqual({ + type: SET_BLOCK_VALIDATION, + clientId: 'abc', + result, + }); + }); + + it('clearBlockValidation returns CLEAR_BLOCK_VALIDATION with clientId', () => { + expect(clearBlockValidation('abc')).toEqual({ + type: CLEAR_BLOCK_VALIDATION, + clientId: 'abc', + }); + }); +}); diff --git a/src/store/__tests__/reducer.test.js b/src/store/__tests__/reducer.test.js new file mode 100644 index 0000000..3c78b6c --- /dev/null +++ b/src/store/__tests__/reducer.test.js @@ -0,0 +1,125 @@ +/** + * Internal dependencies + */ +import { reducer } from '../reducer'; +import { + DEFAULT_STATE, + SET_INVALID_BLOCKS, + SET_INVALID_META, + SET_INVALID_EDITOR_CHECKS, + SET_BLOCK_VALIDATION, + CLEAR_BLOCK_VALIDATION, +} from '../constants'; + +describe('core/validation reducer', () => { + it('returns the default state when called with undefined', () => { + const next = reducer(undefined, { type: '@@INIT' }); + expect(next).toEqual(DEFAULT_STATE); + }); + + it('returns unchanged state for unknown action types', () => { + const state = { + blocks: [{ clientId: 'a', mode: 'error', issues: [] }], + meta: [], + editor: [], + blockValidation: {}, + }; + const next = reducer(state, { type: 'UNRELATED_ACTION' }); + expect(next).toBe(state); + }); + + describe('SET_INVALID_BLOCKS', () => { + it('replaces the blocks array with the provided results', () => { + const results = [ + { clientId: 'abc', mode: 'error', issues: [] }, + { clientId: 'def', mode: 'warning', issues: [] }, + ]; + const next = reducer(DEFAULT_STATE, { + type: SET_INVALID_BLOCKS, + results, + }); + expect(next.blocks).toBe(results); + expect(next.meta).toBe(DEFAULT_STATE.meta); + expect(next.editor).toBe(DEFAULT_STATE.editor); + }); + }); + + describe('SET_INVALID_META', () => { + it('replaces the meta array with the provided results', () => { + const results = [{ metaKey: 'seo', hasErrors: true, issues: [] }]; + const next = reducer(DEFAULT_STATE, { + type: SET_INVALID_META, + results, + }); + expect(next.meta).toBe(results); + }); + }); + + describe('SET_INVALID_EDITOR_CHECKS', () => { + it('replaces the editor array with the provided issues', () => { + const issues = [{ type: 'error', errorMsg: 'Required heading' }]; + const next = reducer(DEFAULT_STATE, { + type: SET_INVALID_EDITOR_CHECKS, + issues, + }); + expect(next.editor).toBe(issues); + }); + }); + + describe('SET_BLOCK_VALIDATION', () => { + it('stores a per-block result keyed by clientId', () => { + const result = { mode: 'error', issues: [{ type: 'error' }] }; + const next = reducer(DEFAULT_STATE, { + type: SET_BLOCK_VALIDATION, + clientId: 'abc', + result, + }); + expect(next.blockValidation.abc).toBe(result); + }); + + it('preserves other per-block results', () => { + const existing = { mode: 'warning', issues: [] }; + const state = { + ...DEFAULT_STATE, + blockValidation: { existing }, + }; + const next = reducer(state, { + type: SET_BLOCK_VALIDATION, + clientId: 'abc', + result: { mode: 'error', issues: [] }, + }); + expect(next.blockValidation.existing).toBe(existing); + expect(next.blockValidation.abc.mode).toBe('error'); + }); + }); + + describe('CLEAR_BLOCK_VALIDATION', () => { + it('removes the entry for the given clientId', () => { + const state = { + ...DEFAULT_STATE, + blockValidation: { + abc: { mode: 'error', issues: [] }, + def: { mode: 'warning', issues: [] }, + }, + }; + const next = reducer(state, { + type: CLEAR_BLOCK_VALIDATION, + clientId: 'abc', + }); + expect(next.blockValidation).not.toHaveProperty('abc'); + expect(next.blockValidation.def).toBeDefined(); + }); + + it('is a no-op when the clientId has no entry', () => { + const state = { + ...DEFAULT_STATE, + blockValidation: { def: { mode: 'warning', issues: [] } }, + }; + const next = reducer(state, { + type: CLEAR_BLOCK_VALIDATION, + clientId: 'abc', + }); + expect(next.blockValidation).toEqual(state.blockValidation); + }); + }); +}); diff --git a/src/store/__tests__/selectors.test.js b/src/store/__tests__/selectors.test.js new file mode 100644 index 0000000..eb011b2 --- /dev/null +++ b/src/store/__tests__/selectors.test.js @@ -0,0 +1,131 @@ +/** + * Internal dependencies + */ +import { + getInvalidBlocks, + getInvalidMeta, + getInvalidEditorChecks, + getBlockValidation, + hasErrors, + hasWarnings, +} from '../selectors'; +import { DEFAULT_STATE, DEFAULT_BLOCK_RESULT } from '../constants'; + +function buildState(overrides = {}) { + return { ...DEFAULT_STATE, ...overrides }; +} + +describe('core/validation selectors', () => { + describe('getInvalidBlocks / getInvalidMeta / getInvalidEditorChecks', () => { + it('returns the raw arrays from state', () => { + const blocks = [{ clientId: 'a', mode: 'error', issues: [] }]; + const meta = [{ metaKey: 'seo', hasErrors: true, issues: [] }]; + const editor = [{ type: 'warning' }]; + const state = buildState({ blocks, meta, editor }); + + expect(getInvalidBlocks(state)).toBe(blocks); + expect(getInvalidMeta(state)).toBe(meta); + expect(getInvalidEditorChecks(state)).toBe(editor); + }); + + it('returns empty arrays by default', () => { + const state = buildState(); + expect(getInvalidBlocks(state)).toEqual([]); + expect(getInvalidMeta(state)).toEqual([]); + expect(getInvalidEditorChecks(state)).toEqual([]); + }); + }); + + describe('getBlockValidation', () => { + it('returns the stored result for the given clientId', () => { + const result = { mode: 'error', issues: [] }; + const state = buildState({ blockValidation: { abc: result } }); + expect(getBlockValidation(state, 'abc')).toBe(result); + }); + + it('returns DEFAULT_BLOCK_RESULT when no entry exists', () => { + const state = buildState(); + expect(getBlockValidation(state, 'missing')).toBe(DEFAULT_BLOCK_RESULT); + }); + }); + + describe('hasErrors', () => { + it('returns false when all scopes are empty', () => { + expect(hasErrors(buildState())).toBe(false); + }); + + it('returns true when a block has mode error', () => { + const state = buildState({ + blocks: [{ clientId: 'a', mode: 'error', issues: [] }], + }); + expect(hasErrors(state)).toBe(true); + }); + + it('returns false when blocks only have warnings', () => { + const state = buildState({ + blocks: [{ clientId: 'a', mode: 'warning', issues: [] }], + }); + expect(hasErrors(state)).toBe(false); + }); + + it('returns true when a meta entry has hasErrors true', () => { + const state = buildState({ + meta: [{ metaKey: 'seo', hasErrors: true, hasWarnings: false, issues: [] }], + }); + expect(hasErrors(state)).toBe(true); + }); + + it('returns true when an editor issue has type error', () => { + const state = buildState({ editor: [{ type: 'error' }] }); + expect(hasErrors(state)).toBe(true); + }); + + it('returns false when editor has only warning-type issues', () => { + const state = buildState({ editor: [{ type: 'warning' }] }); + expect(hasErrors(state)).toBe(false); + }); + }); + + describe('hasWarnings', () => { + it('returns false when all scopes are empty', () => { + expect(hasWarnings(buildState())).toBe(false); + }); + + it('returns true when a block has mode warning and no errors exist', () => { + const state = buildState({ + blocks: [{ clientId: 'a', mode: 'warning', issues: [] }], + }); + expect(hasWarnings(state)).toBe(true); + }); + + it('returns false when an error also exists (errors take precedence)', () => { + const state = buildState({ + blocks: [ + { clientId: 'a', mode: 'warning', issues: [] }, + { clientId: 'b', mode: 'error', issues: [] }, + ], + }); + expect(hasWarnings(state)).toBe(false); + }); + + it('returns true when a meta entry has only warnings', () => { + const state = buildState({ + meta: [{ metaKey: 'seo', hasErrors: false, hasWarnings: true, issues: [] }], + }); + expect(hasWarnings(state)).toBe(true); + }); + + it('ignores meta entries where hasErrors is also true', () => { + const state = buildState({ + meta: [{ metaKey: 'seo', hasErrors: true, hasWarnings: true, issues: [] }], + }); + // hasErrors short-circuits hasWarnings to false. + expect(hasWarnings(state)).toBe(false); + }); + + it('returns true when an editor issue has type warning and no errors', () => { + const state = buildState({ editor: [{ type: 'warning' }] }); + expect(hasWarnings(state)).toBe(true); + }); + }); +}); diff --git a/src/utils/__tests__/issue-helpers.test.js b/src/utils/__tests__/issue-helpers.test.js new file mode 100644 index 0000000..5761b9c --- /dev/null +++ b/src/utils/__tests__/issue-helpers.test.js @@ -0,0 +1,163 @@ +/** + * Internal dependencies + */ +import { + filterIssuesByType, + getErrors, + getWarnings, + hasErrors, + hasWarnings, + isCheckEnabled, + createIssue, + createValidationResult, +} from '../issue-helpers'; + +const errorIssue = { type: 'error', message: 'required' }; +const warningIssue = { type: 'warning', message: 'suggested' }; + +describe('filterIssuesByType', () => { + it('returns only issues matching the given type', () => { + const result = filterIssuesByType([errorIssue, warningIssue, errorIssue], 'error'); + expect(result).toEqual([errorIssue, errorIssue]); + }); + + it('returns an empty array when no matches exist', () => { + expect(filterIssuesByType([warningIssue], 'error')).toEqual([]); + }); + + it('returns an empty array for an empty input', () => { + expect(filterIssuesByType([], 'error')).toEqual([]); + }); +}); + +describe('getErrors / getWarnings', () => { + it('getErrors returns issues with type error', () => { + expect(getErrors([errorIssue, warningIssue])).toEqual([errorIssue]); + }); + + it('getWarnings returns issues with type warning', () => { + expect(getWarnings([errorIssue, warningIssue])).toEqual([warningIssue]); + }); +}); + +describe('hasErrors / hasWarnings (array versions)', () => { + it('hasErrors is true when at least one error exists', () => { + expect(hasErrors([warningIssue, errorIssue])).toBe(true); + }); + + it('hasErrors is false when no errors exist', () => { + expect(hasErrors([warningIssue])).toBe(false); + }); + + it('hasErrors is false for empty arrays', () => { + expect(hasErrors([])).toBe(false); + }); + + it('hasWarnings is true when at least one warning exists', () => { + expect(hasWarnings([warningIssue])).toBe(true); + }); + + it('hasWarnings is false when no warnings exist', () => { + expect(hasWarnings([errorIssue])).toBe(false); + }); +}); + +describe('isCheckEnabled', () => { + it('returns false for null', () => { + expect(isCheckEnabled(null)).toBe(false); + }); + + it('returns false for undefined', () => { + expect(isCheckEnabled(undefined)).toBe(false); + }); + + it('returns false when enabled is explicitly false', () => { + expect(isCheckEnabled({ enabled: false })).toBe(false); + }); + + it('returns true when enabled is explicitly true', () => { + expect(isCheckEnabled({ enabled: true })).toBe(true); + }); + + it('returns true when enabled is absent (default)', () => { + expect(isCheckEnabled({ level: 'error' })).toBe(true); + }); +}); + +describe('createIssue', () => { + it('creates an issue with defaults when config is minimal', () => { + const issue = createIssue({}, 'my_check'); + expect(issue).toMatchObject({ + check: 'my_check', + checkName: 'my_check', + type: 'error', + priority: 1, + message: '', + errorMsg: '', + warningMsg: '', + }); + }); + + it('pulls error_msg from PHP-style config', () => { + const issue = createIssue({ error_msg: 'Alt text required' }, 'alt_text'); + expect(issue.errorMsg).toBe('Alt text required'); + expect(issue.warningMsg).toBe('Alt text required'); // warning falls back to error + }); + + it('pulls both error_msg and warning_msg when provided', () => { + const issue = createIssue({ error_msg: 'Required', warning_msg: 'Recommended' }, 'check'); + expect(issue.errorMsg).toBe('Required'); + expect(issue.warningMsg).toBe('Recommended'); + }); + + it('sets type from level and assigns matching priority (error=1)', () => { + const issue = createIssue({ level: 'error' }, 'c'); + expect(issue.type).toBe('error'); + expect(issue.priority).toBe(1); + }); + + it('sets type from level and assigns matching priority (warning=2)', () => { + const issue = createIssue({ level: 'warning' }, 'c'); + expect(issue.type).toBe('warning'); + expect(issue.priority).toBe(2); + }); + + it('assigns priority 3 for levels other than error/warning', () => { + const issue = createIssue({ level: 'none' }, 'c'); + expect(issue.priority).toBe(3); + }); + + it('merges additional fields into the issue', () => { + const issue = createIssue({}, 'c', { metaKey: 'seo' }); + expect(issue.metaKey).toBe('seo'); + }); +}); + +describe('createValidationResult', () => { + it('isValid is true for an empty issue list', () => { + const result = createValidationResult([]); + expect(result.isValid).toBe(true); + expect(result.hasErrors).toBe(false); + expect(result.hasWarnings).toBe(false); + expect(result.issues).toEqual([]); + }); + + it('isValid is false when issues exist', () => { + const result = createValidationResult([errorIssue]); + expect(result.isValid).toBe(false); + expect(result.hasErrors).toBe(true); + expect(result.hasWarnings).toBe(false); + }); + + it('derives hasErrors and hasWarnings from the issues array', () => { + const result = createValidationResult([errorIssue, warningIssue]); + expect(result.hasErrors).toBe(true); + expect(result.hasWarnings).toBe(true); + }); + + it('merges additional fields into the result', () => { + const result = createValidationResult([], { clientId: 'abc', name: 'core/image' }); + expect(result.clientId).toBe('abc'); + expect(result.name).toBe('core/image'); + }); +}); From 5d956d40d98f624887867f5f96d977b8b0705a3e Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Sat, 18 Apr 2026 11:52:39 -0400 Subject: [PATCH 10/11] docs: refresh TODO and consolidated plan after polish pass Mark Polish 1-4 done with commit hashes in the consolidated plan. Add deferred-polish list (5, 5b, 6, 7) with explicit scope, prerequisites, and target file paths so the next iteration can pick them up cold. Rewrite TODO.md to reflect the current tree: - "Completed" now covers naming alignment + five-batch plan + post-batch polish 1-4 (each with commit hashes) - "Remaining" restructured into Testing / Performance / TypeScript / Future considerations, with file paths updated to post-Batch-1 locations (src/hooks/use-validation-*.js, src/store/__tests__/, etc.) - Cross-links to the consolidated plan for authoritative polish status Plugin is now in the state described for picking up integration-plugin work before cycling back to draft the Gutenberg PR. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/TODO.md | 110 +++++++++++------- docs/gutenberg-alignment/consolidated-plan.md | 31 +++-- 2 files changed, 88 insertions(+), 53 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index baf63d4..6bbb77f 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,52 +1,73 @@ # Integration TODO -Remaining work items to prepare the Validation API plugin for a Gutenberg core proposal. The naming alignment and architectural changes are complete -- these items cover testing, performance, and future enhancements. +Remaining work items to prepare the Validation API plugin for a Gutenberg core proposal. The five-batch alignment plan is complete; items below cover remaining testing, performance, and future enhancements. -## Completed +**See also:** [docs/gutenberg-alignment/consolidated-plan.md](gutenberg-alignment/consolidated-plan.md) for the authoritative status of alignment batches and post-batch polish items. This file and the consolidated plan intentionally overlap for the polish/future sections so either doc can orient a reader. -The following work has been done to align with Gutenberg conventions: +## Completed — alignment + polish -- **Store renamed** to `core/validation` (from `validation-api`) +### Naming / structural alignment + +- **Store renamed** to `core/validation` - **JS filter hooks renamed** to `editor.validateBlock`, `editor.validateMeta`, `editor.validateEditor` - **`window.ValidationAPI` replaced** with `block_editor_settings_all` filter. Config available via `select('core/editor').getEditorSettings().validationApi` - **`PluginContext` dropped**. Replaced with required `namespace` field in check registration args - **PHP functions renamed** to `wp_register_block_validation_check()`, `wp_register_meta_validation_check()`, `wp_register_editor_validation_check()` - **PHP hooks renamed** from `validation_api_*` to `wp_validation_*` -- **REST endpoint moved** to `wp/v2/validation-checks` -- **Issue model standardized** to camelCase only in JS (`errorMsg`, `warningMsg`). PHP still uses `error_msg`, `warning_msg` -- transformation happens at the boundary in `createIssue()` -- **`window.ValidationAPI.useMetaField` export dropped**. External plugins import directly +- **REST endpoint** moved to `wp-validation/v1/checks` (plugin-owned namespace; final core namespace TBD during PR) +- **Issue model standardized** to camelCase only in JS. PHP still uses snake_case; transformation happens at the boundary in `createIssue()` +- **`window.ValidationAPI.useMetaField` export dropped**. External plugins import directly or consume via the store - **Documentation updated** across all guide, technical, and root docs - **Integration example plugin updated** to use new API names -## Testing +### Five-batch alignment plan + +- **Batch 1** — `src/` restructured to Gutenberg-package layout; renderless components converted to hooks (`useValidationSync`, `useValidationLifecycle`); `editor.preSavePost` save gate added; `useValidationIssues` consolidated hook; `useMetaField` dual `useSelect` collapsed; `getInvalid*` → `useInvalid*`; webpack aliases dropped; `package.json` `sideEffects` declared. +- **Batch 2** — REST namespace moved to `wp-validation/v1/checks`; settings addon updated in tandem. +- **Batch 3** — `Core/I18n.php` class deleted; `wp_set_script_translations()` inlined in `Core/Assets.php`. +- **Batch 4** — PHP dead-code deletions (~260 LOC): `Meta\Validator`, `Contracts/CheckProvider`, dead `Block\Registry` methods, orphan hooks, unreachable `EditorDetection` branch. +- **Batch 5** — `AbstractRegistry` base class extracted from Block/Meta/Editor registries; shared defaults, level validation, namespace stamping, priority sort, and filter application now live in one place. + +### Post-batch polish + +- **Polish 1** — `@example` JSDoc blocks on the public API (store selectors + actions, `useMetaField`, `useMetaValidation`). +- **Polish 2** — `src/store/constants.ts` (TypeScript start); stale `babel.config.json` deleted. +- **Polish 3 + 4** — Jest unit-test infrastructure + 56 tests covering store (reducer, actions, selectors) and `issue-helpers`. Run with `pnpm test`. + +## Remaining + +### Testing (the big gaps) + +#### Add unit tests for validation functions + +**Scope:** `validateBlock()`, `validateMetaField()` / `validateAllMetaChecks()`, `validateEditor()` -### Add unit tests for store +**Why deferred:** Each calls `applyFilters('editor.validate*', ...)` and reads editor settings via `getValidationConfig`. Testing well requires: +- Mocking `@wordpress/hooks` to control filter return values +- Mocking `select('core/editor').getEditorSettings()` to supply test rule payloads +- Thoughtful test scenarios (disabled checks, missing rules, chained filter callbacks) -**Files**: New test files in `src/editor/store/__tests__/` +**Files** (target): `src/utils/__tests__/validate-block.test.js`, `validate-meta.test.js`, `validate-editor.test.js` -Test reducer, actions, and selectors. The store is the foundation -- it should have comprehensive test coverage before proposing upstream. +#### Add unit tests for custom hooks -### Add unit tests for validation functions +**Scope:** `useMetaField`, `useMetaValidation`, `useInvalidBlocks`, `useInvalidMeta`, `useInvalidEditorChecks`, `useValidationIssues`, `useDebouncedValidation`, `useValidationSync`, `useValidationLifecycle` -**Files**: New test files alongside validation modules +**Why deferred:** Need `@testing-library/react` for hook rendering + `@wordpress/data` store test harness. Store mocking is straightforward; block-editor mocking is the harder bit for `useInvalidBlocks`. -- `validateBlock()` with various block types and check configs -- `validateMeta()` with required, custom, and multi-check scenarios -- `validateEditor()` with various block arrangements -- `issueHelpers` utility functions -- `getValidationConfig` utility functions +#### Add integration tests for the full validation flow -### Add integration tests for the full validation flow +Full end-to-end: PHP registration → editor settings injection → JS validation → store dispatch → save-lock → `editor.preSavePost` gate. Uses `@wordpress/env` + `@wordpress/e2e-test-utils-playwright`. -Test the end-to-end flow: PHP registration -> editor settings injection -> JS validation -> store dispatch -> lock/unlock. This could use `@wordpress/env` and `@wordpress/e2e-test-utils`. +**Why deferred:** Docker setup + WP test environment + Playwright infrastructure. Worth investing before the PR so reviewers can replicate. -## Performance +### Performance -### Benchmark with large posts +#### Benchmark with large posts -Test validation performance with posts containing 200+, 500+, and 1000+ blocks. The current approach validates all blocks when any block changes. Measure: -- Time for `GetInvalidBlocks()` to complete -- Re-render count for `ValidationProvider` +Test validation performance with posts containing 200+, 500+, and 1000+ blocks. Current approach validates all blocks when any block changes. Measure: +- Time for `useInvalidBlocks()` to complete +- Re-render count for `ValidationSync` (the renderless sibling that calls `useValidationSync`) - Memory overhead of `blockValidation` store slice with many entries If performance is an issue, consider: @@ -54,31 +75,32 @@ If performance is an issue, consider: - Lazy validation for off-screen blocks - Batch dispatching instead of per-block dispatches -### Audit `useEffect` dependency arrays +#### Audit `useEffect` dependency arrays -**Files**: `src/editor/components/ValidationProvider.js`, `src/editor/validation/ValidationAPI.js` +**Files:** `src/hooks/use-validation-sync.js`, `src/hooks/use-validation-lifecycle.js` -Ensure validation re-computation is triggered only when relevant data changes, not on every render. The `ValidationProvider` dispatches to the store on every effect run -- verify that React's dependency array prevents unnecessary cycles. +Verify re-computation is triggered only when relevant data changes. Batch 1 fixed a render-loop by making the sync/lifecycle hooks siblings (not hooks in the same parent); a deeper audit may find further opportunities. -## TypeScript +### TypeScript -### Add type definitions +#### Expand beyond `constants.ts` -**Files**: New `.d.ts` files or convert `.js` to `.ts` +Constants are already typed. Next candidates, in order of type payoff: -Gutenberg packages include TypeScript definitions. At minimum, add type definitions for: -- Store state shape, actions, and selectors -- Check registration args (PHP side documented, JS side needs types) -- Validation result and issue objects -- Public hooks (`useMetaField`, `useMetaValidation`) +- `src/store/reducer.js` → `.ts` — exhaustive switch on typed `Action` union +- `src/store/actions.js` → `.ts` — typed action creators +- `src/store/selectors.js` → `.ts` — typed selector returns +- `src/utils/issue-helpers.js` → `.ts` — typed helper signatures (already has JSDoc types) +- Public hooks (`useMetaField`, `useMetaValidation`) — signatures benefit consumers most +- Add JSDoc `.d.ts` for check registration args (the `$args` shape callers pass to `wp_register_*_validation_check()`) -## Future Considerations +### Future considerations (design discussions) -These items are not blockers but are worth tracking for the core proposal discussion. +These are not blockers but are worth tracking for the core proposal discussion. -### Block.json validation support +#### Block.json validation support -Explore declaring simple validation rules in `block.json`: +Declare simple validation rules in `block.json`: ```json { @@ -94,12 +116,12 @@ Explore declaring simple validation rules in `block.json`: } ``` -This would reduce JS boilerplate for common checks. Complex validation would still use JS filters. +Reduces JS boilerplate for common checks. Complex validation still uses JS filters. -### Async validation support +#### Async validation support -The current filter hooks are synchronous (`applyFilters`). Some validation needs are inherently async (link checking, server-side content analysis). Explore using `applyFiltersAsync` for validation hooks, with loading states in the UI. +Current filter hooks are synchronous (`applyFilters`). Some validation needs are inherently async (link checking, server-side content analysis). Explore using `applyFiltersAsync` for validation hooks, with loading states in the UI. -### Site editor support +#### Site editor support The plugin currently excludes the site editor. Template validation (required blocks in templates, valid template structure) is a related but distinct problem that would need its own design discussion. diff --git a/docs/gutenberg-alignment/consolidated-plan.md b/docs/gutenberg-alignment/consolidated-plan.md index cb1e911..9d4b929 100644 --- a/docs/gutenberg-alignment/consolidated-plan.md +++ b/docs/gutenberg-alignment/consolidated-plan.md @@ -228,18 +228,31 @@ After all five batches ship, run through these end-to-end checks: - [ ] PHP debug log shows no warnings/notices under WP_DEBUG - [ ] JS console shows no errors or warnings -## Post-batch polish (not required, nice to have) +## Post-batch polish -Tracked but not part of the five batches: +Orthogonal to Gutenberg alignment; tracked separately. -- [ ] Add `@example` JSDoc blocks to public-facing hooks and utils (`useMetaField`, `useMetaValidation`, `useInvalidBlocks`, etc.) — matches Gutenberg package style -- [ ] Start TypeScript migration with `src/store/constants.ts` — matches `packages/editor/src/store/` -- [ ] Add unit tests for store reducer, selectors, actions (from [docs/TODO.md](../TODO.md)) -- [ ] Add unit tests for `validateBlock`, `validateMeta`, `validateEditor` utility functions -- [ ] Performance benchmarks with 200+, 500+, 1000+ block posts (from [docs/TODO.md](../TODO.md)) -- [ ] Add integration tests using `@wordpress/env` + `@wordpress/e2e-test-utils` +### Completed -These polishes are orthogonal to Gutenberg alignment. Do them on your own schedule. +- [x] **Polish 1** — `@example` JSDoc blocks added to the public API surface (6 store selectors, 5 store actions, `useMetaField`, `useMetaValidation`). Commit `561e32a`. +- [x] **Polish 2** — TypeScript migration started with `src/store/constants.ts`. Exports `State`, `ValidationIssue`, `BlockValidationResult`, `MetaValidationResult`, `ValidationMode`, `IssueType`, and the action-type constants as narrow literal types. Stale `babel.config.json` deleted in the same change so `@wordpress/scripts`' default preset (which includes `@babel/preset-typescript`) takes effect. Commit `a228e5d`. +- [x] **Polish 3+4** — Unit tests via `@wordpress/scripts test-unit-js`. Test infrastructure added (`test` / `test:watch` npm scripts; Jest env override in `.eslintrc.json`). 56 tests across 4 suites covering store reducer, actions, selectors, and `issue-helpers`. All pure-function coverage in ~1s. Commit `f78624e`. + +### Deferred — pick up before the core PR + +- [ ] **Polish 5** — Unit tests for the validation-dispatch functions. Requires `@wordpress/hooks` filter mocking and editor-settings mocking. Targets: + - `validateBlock()` — block-type rule lookup, `editor.validateBlock` filter application, mode derivation + - `validateMetaField()` / `validateAllMetaChecks()` — per-key rule lookup, `editor.validateMeta` filter, required-field fallback + - `validateEditor()` — per-post-type rules, `editor.validateEditor` filter, priority sort +- [ ] **Polish 5b** — Unit tests for custom hooks. Requires `@testing-library/react` plus `@wordpress/data` store mocking. Targets: `useMetaField`, `useMetaValidation`, `useInvalidBlocks`, `useInvalidMeta`, `useInvalidEditorChecks`, `useValidationIssues`, `useDebouncedValidation`, `useValidationSync`, `useValidationLifecycle`. +- [ ] **Polish 6** — Performance benchmarks with 200+/500+/1000+ block posts. Measures `useInvalidBlocks` re-computation time, dispatch churn, and memory of the `blockValidation` store slice. Outputs inform whether to add per-block diffing or lazy validation before the PR. +- [ ] **Polish 7** — Integration / e2e tests via `@wordpress/env` + `@wordpress/e2e-test-utils-playwright`. Full-stack coverage: PHP check registration → editor settings injection → JS validation → store dispatch → save-lock → `editor.preSavePost` gate. + +### Also worth doing before core PR (from [docs/TODO.md](../TODO.md)) + +- [ ] Further TypeScript migration: `src/store/actions.js`, `src/store/selectors.js`, `src/store/reducer.js`, `src/store/index.js`. Constants are typed already; making the consumers typed closes the loop. +- [ ] Add `.d.ts` or inline JSDoc types for check registration args (the object shape accepted by `wp_register_block_validation_check()` on the JS-filter side). +- [ ] **Future considerations** (design discussions, not straight tasks): `block.json` declarative validation, async validation via `applyFiltersAsync`, site-editor support. See [docs/TODO.md](../TODO.md) "Future Considerations" for rationale. ## Deferred — not in these batches From 5fdd9abe7d4f799c90a26e0ed55f3d1fb962c2c9 Mon Sep 17 00:00:00 2001 From: Troy Chaplin Date: Sat, 18 Apr 2026 13:25:10 -0400 Subject: [PATCH 11/11] docs: audit, remove stale planning artifacts, refresh post-alignment state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive doc pass after the alignment + polish work shipped: - Removed stale planning artifacts whose purpose has been fulfilled - Rewrote docs that still described pre-Batch-1 architecture - Added two new docs: a troubleshooting guide for integrating plugins and a single-page PR-readiness summary for future-you / collaborators Deleted (git history preserves them): - docs/gutenberg-alignment/pass-a.md (convention audit — work complete) - docs/gutenberg-alignment/pass-b.md (architectural audit — work complete) - docs/gutenberg-alignment/pass-c.md (leanness audit — work complete) Rewrote (fixed stale architecture references): - CLAUDE.md: project-structure tree, data-flow diagram, conventions, doc map. Removed src/editor/ + src/shared/ + webpack aliases + renderless-component descriptions. Added AbstractRegistry, hooks (useValidationSync/useValidationLifecycle), editor.preSavePost gate, test + polish script references, TypeScript constants.ts note. - docs/technical/README.md: JS Layer section rewritten for the hook- first architecture. Side-effect modules in src/hooks/ listed with their filter registrations. Render-loop sibling-wrapper rationale documented. Save-locking defense-in-depth explained. - docs/technical/data-flow.md: full 14-step walkthrough from PHP registration through useInvalidBlocks -> useValidationSync -> store -> useValidationLifecycle / pre-save-validation / sidebar. Covers AbstractRegistry normalization steps. - docs/gutenberg-alignment/README.md: points at the three still-useful files (consolidated-plan, core-pr-migration, PR-READINESS). - docs/gutenberg-alignment/consolidated-plan.md: pared down from execution playbook to execution record. Per-batch commits + summary. Polish status with commit hashes. Deferred items (5, 5b, 6, 7) with scope and prerequisites. Updated (targeted fixes): - docs/INTEGRATION.md: Component Mapping now lists hooks not renderless components; Packages Affected section reflects hook-first architecture; editor.preSavePost added to "new to Gutenberg" table; open questions refreshed. - docs/PROPOSAL.md: Reference-Implementation section describes the current hook-based design. Added note about the sibling-wrapper pattern. State-management description updated. - docs/README.md (index): adds PR-READINESS + troubleshooting; reorders into Start-Here / Developer-Guide / Technical / Core-Merge / Working- Notes sections. Created: - docs/PR-READINESS.md: single-page "where are we, what's next" for the Gutenberg PR. TL;DR, status tables, open questions for core team, next-steps checklist for future-you returning after a break. - docs/guide/troubleshooting.md: 10+ common issues developers integrating with the API hit, each with diagnostic steps — sidebar empty, validation not firing, REST 401/404, borders missing, save stuck locked, JSON response errors, React #185 render loops, env-specific failures. Build + tests clean (pnpm build + pnpm test both green). Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 175 ++++---- docs/INTEGRATION.md | 23 +- docs/PR-READINESS.md | 120 ++++++ docs/PROPOSAL.md | 21 +- docs/README.md | 21 +- docs/guide/troubleshooting.md | 160 ++++++++ docs/gutenberg-alignment/README.md | 29 +- docs/gutenberg-alignment/consolidated-plan.md | 317 ++------------- docs/gutenberg-alignment/pass-a.md | 380 ------------------ docs/gutenberg-alignment/pass-b.md | 314 --------------- docs/gutenberg-alignment/pass-c.md | 368 ----------------- docs/technical/README.md | 220 +++++----- docs/technical/data-flow.md | 233 ++++++----- 13 files changed, 736 insertions(+), 1645 deletions(-) create mode 100644 docs/PR-READINESS.md create mode 100644 docs/guide/troubleshooting.md delete mode 100644 docs/gutenberg-alignment/pass-a.md delete mode 100644 docs/gutenberg-alignment/pass-b.md delete mode 100644 docs/gutenberg-alignment/pass-c.md diff --git a/CLAUDE.md b/CLAUDE.md index c73b7fe..f6bd32d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,9 @@ Three validation scopes, each with a PHP registry and JS filter: |---|---|---|---| | Block attributes | `ValidationAPI\Block\Registry` | `wp_register_block_validation_check()` | `editor.validateBlock` | | Post meta fields | `ValidationAPI\Meta\Registry` | `wp_register_meta_validation_check()` | `editor.validateMeta` | -| Editor/document | `ValidationAPI\Editor\Registry` | `wp_register_editor_validation_check()` | `editor.validateEditor` | +| Editor / document | `ValidationAPI\Editor\Registry` | `wp_register_editor_validation_check()` | `editor.validateEditor` | + +All three concrete registries extend `ValidationAPI\AbstractRegistry`, which provides shared defaults, level validation, namespace stamping, priority sort, and `wp_validation_check_level` filter application. All registration functions require `namespace`, `name`, and `error_msg` in the `$args` array. Meta checks also require `meta_key`. @@ -18,105 +20,135 @@ All registration functions require `namespace`, `name`, and `error_msg` in the ` Three levels: `error` (blocks save), `warning` (shows feedback), `none` (disabled). Filterable at runtime via `wp_validation_check_level`. -### Data Flow +### Data flow ``` -PHP Registries - → block_editor_settings_all filter (Assets.php) +PHP registries (Block / Meta / Editor — all extend AbstractRegistry) + → Assets::inject_editor_settings on block_editor_settings_all filter → select('core/editor').getEditorSettings().validationApi - → getValidationConfig.js utility functions - → validateBlock / validateMeta / validateEditor - → core/validation store (actions/selectors) - → ValidationAPI component (lockPostSaving, CSS classes) - → ValidationSidebar component (issue display) + → utils/get-validation-config.js + → utils/validate-block.js / validate-meta.js / validate-editor.js + → useInvalidBlocks / useInvalidMeta / useInvalidEditorChecks (src/utils/) + → useValidationSync dispatches → core/validation store + → useValidationLifecycle (lockPostSaving + body CSS classes) + → pre-save-validation (editor.preSavePost belt-and-suspenders) + → ValidationSidebar component + → block-validation-classes side-effect (per-block CSS) + → validate-block side-effect (per-block toolbar button) ``` -### Key PHP Hooks +### Key PHP hooks -- `wp_validation_check_level` — Override check severity at runtime -- `wp_validation_check_args` — Modify check config before registration -- `wp_validation_should_register_check` — Prevent specific checks from registering +- `wp_validation_check_level` — Override check severity at runtime (the settings-addon extension point) +- `wp_validation_check_args` / `wp_validation_meta_check_args` / `wp_validation_editor_check_args` — Modify check config before registration +- `wp_validation_should_register_check` / `_meta_check` / `_editor_check` — Prevent specific checks from registering - `wp_validation_initialized`, `wp_validation_ready`, `wp_validation_editor_checks_ready` — Lifecycle +- `wp_validation_check_registered`, `wp_validation_meta_check_registered`, `wp_validation_editor_check_registered` — Post-registration notifications + +### JS filters -### JS Store +- `editor.validateBlock` — Per-block validation +- `editor.validateMeta` — Per-meta-field validation +- `editor.validateEditor` — Editor/document-level validation +- `editor.preSavePost` — Save-time gate (async; throws to abort save if errors exist) + +### JS store Store name: `core/validation` -**Selectors:** `getInvalidBlocks()`, `getInvalidMeta()`, `getInvalidEditorChecks()`, `getBlockValidation(clientId)`, `hasErrors()`, `hasWarnings()` +**Selectors** (each has an `@example` block in `src/store/selectors.js`): +`getInvalidBlocks()`, `getInvalidMeta()`, `getInvalidEditorChecks()`, `getBlockValidation(clientId)`, `hasErrors()`, `hasWarnings()` -**Actions:** `setInvalidBlocks()`, `setInvalidMeta()`, `setInvalidEditorChecks()`, `setBlockValidation()`, `clearBlockValidation()` +**Actions** (each has an `@example` block in `src/store/actions.js`): +`setInvalidBlocks()`, `setInvalidMeta()`, `setInvalidEditorChecks()`, `setBlockValidation()`, `clearBlockValidation()` ### REST API `GET /wp-validation/v1/checks` — Returns all registered checks grouped by scope (block, meta, editor). Requires `manage_options`. -## Project Structure +## Project structure ``` includes/ - Block/Registry.php # Block check registration - Editor/Registry.php # Editor check registration - Meta/Registry.php # Meta check registration - Meta/Validator.php # Server-side meta validation helper - Core/Plugin.php # Plugin initialization - Core/Assets.php # Script enqueuing + editor settings injection + script translations - Core/Traits/EditorDetection.php # Post editor context detection - Rest/ChecksController.php # REST endpoint + AbstractRegistry.php # Abstract base for all three registries + Block/Registry.php # Block check registration + Editor/Registry.php # Editor-level check registration + Meta/Registry.php # Meta check registration + Core/Plugin.php # Plugin bootstrap + Core/Assets.php # Script enqueue + block_editor_settings_all injection + wp_set_script_translations + Core/Traits/EditorDetection.php # Post editor context detection + Core/Traits/Logger.php # Shared WP_DEBUG-gated logging + Rest/ChecksController.php # REST endpoint src/ - script.js # Entry point - editor/ - register.js # registerPlugin('core-validation', ...) - store/ # core/validation Redux store - components/ - ValidationProvider.js # Single computation point (renderless) - ValidationSidebar.js # Issue display panel - ValidationToolbarButton.js - validation/ - ValidationAPI.js # Side effects: lockPostSaving, CSS classes (renderless) - blocks/validateBlock.js - meta/validateMeta.js - editor/validateEditor.js - meta/hooks/useMetaField.js - meta/hooks/useMetaValidation.js - hoc/ - withErrorHandling.js # editor.BlockEdit filter - withBlockValidationClasses.js # editor.BlockListBlock filter - shared/ - utils/validation/ - issueHelpers.js # createIssue, createValidationResult, hasErrors, hasWarnings - getValidationConfig.js # Reads from editor settings (replaces window.ValidationAPI) - getInvalidBlocks.js # React hook - getInvalidMeta.js # React hook - getInvalidEditorChecks.js # React hook - hooks/ - useDebouncedValidation.js + index.js # Package entry; imports store + hooks + styles; re-exports public API + store/ # core/validation @wordpress/data store + constants.ts # Typed STORE_NAME, action types, State interface, Issue types + actions.js # Action creators (with @example blocks) + selectors.js # Selectors (with @example blocks) + reducer.js # Reducer + index.js # createReduxStore + register + __tests__/ # Unit tests (reducer, actions, selectors) + hooks/ # Side-effect modules + two real hooks + index.js # Imports each side-effect module + register-sidebar.js # registerPlugin; mounts ValidationSync / ValidationLifecycle / ValidationSidebar as siblings + use-validation-sync.js # Hook: computes invalid results + dispatches to store (replaces the old ValidationProvider) + use-validation-lifecycle.js # Hook: lockPostSaving + body CSS class management (replaces the old ValidationAPI) + validate-block.js # Side-effect: editor.BlockEdit filter + withErrorHandling HOC + toolbar button + block-validation-classes.js # Side-effect: editor.BlockListBlock filter + per-block CSS classes + pre-save-validation.js # Side-effect: editor.preSavePost gate (async filter, throws on errors) + components/ + validation-icon/index.js + validation-sidebar/index.js # PluginSidebar with grouped issue list + validation-toolbar-button/index.js + utils/ # Flat utility tree + issue-helpers.js # createIssue, createValidationResult, hasErrors, hasWarnings (array-scope), filterIssuesByType, isCheckEnabled, getErrors, getWarnings + get-validation-config.js # Reads from editor settings + validate-block.js # Runs editor.validateBlock filter per check + validate-meta.js # Runs editor.validateMeta filter per check; validateAllMetaChecks aggregator + validate-editor.js # Runs editor.validateEditor filter per check + use-invalid-blocks.js # Source hook: walks the block tree, runs validateBlock, collects failures + use-invalid-meta.js # Source hook + use-invalid-editor-checks.js # Source hook + use-validation-issues.js # Reads aggregate state from store (C-9 consolidation) + use-meta-field.js # Meta field integration hook (value + onChange + help + className) + use-meta-validation.js # Meta validation status hook + use-debounced-validation.js # Immediate-then-debounce hook + index.js # Barrel + __tests__/ # Unit tests (issue-helpers) + styles.scss # Entry SCSS + styles/ # Partials (_variables, meta-validation, validation-sidebar, inline-indicators, inline-modal) ``` -## Build +No `src/editor/` or `src/shared/`. No webpack aliases. Entry is `src/index.js`. + +## Build and test ```bash -pnpm build # wp-scripts build → build/validation-api.js -pnpm start # wp-scripts start (watch mode) -pnpm lint # JS + PHP + CSS linting +pnpm build # wp-scripts build → build/validation-api.js +pnpm start # wp-scripts start (watch) +pnpm test # wp-scripts test-unit-js (Jest; 56 tests) +pnpm lint # JS + PHP + CSS linting +pnpm format # auto-fix prettier / phpcbf / stylelint --fix ``` -Webpack aliases: `@` → `src/`, `@editor` → `src/editor/`, `@shared` → `src/shared/` +TypeScript: `src/store/constants.ts` is the only `.ts` file. Other modules run as JS via `@babel/preset-typescript` (bundled in `@wordpress/babel-preset-default`). No `tsconfig.json` — editors read types from the `.ts` source directly. -## Companion Plugins (same local wp-content/plugins/) +## Companion plugins (same local `wp-content/plugins/`) -- **validation-api-integration-example** — Demo plugin with block, meta, and editor checks. Must be rebuilt separately (`npm run build` in its directory) after any JS filter name changes. -- **validation-api-settings** — Admin settings page using WordPress DataForm. Reads checks from REST endpoint, lets admins override severity via `wp_validation_check_level` filter. Must be rebuilt separately. +- **validation-api-integration-example** — Demo plugin with block, meta, and editor checks. Rebuild with `npm run build` in its directory after any JS filter-name changes. +- **validation-api-settings** — Admin settings page. Reads checks from `GET /wp-validation/v1/checks`, lets admins override severity via `wp_validation_check_level`. Rebuild with `npm run build` in its directory after REST path changes. ## Conventions - PHP registration args use snake_case (`error_msg`, `warning_msg`). JS issue objects use camelCase (`errorMsg`, `warningMsg`). Transformation happens in `createIssue()`. - Plugin registers as `registerPlugin('core-validation', ...)` in JS. -- Editor context scoping: validation loads in post editor only (not site editor). Detection via `EditorDetection` trait. -- `PluginContext` was removed. Plugin attribution uses a `namespace` field in registration args, stored as `_namespace` internally. -- PHPCS config (`phpcs.xml.dist`) allows `wp_register` and `wp_validation` as global prefixes. +- Editor context scoping: validation loads in post editor only (standard and template modes); site editor is intentionally excluded. Detection via the `EditorDetection` trait. +- Plugin attribution uses a `namespace` field in registration args, stored internally as `_namespace`. +- `ValidationSync` / `ValidationLifecycle` are renderless sibling wrappers around their hooks (inside the `ValidationPlugin` root). The sibling arrangement is load-bearing — putting both hooks in one parent causes a render loop because `useValidationLifecycle` subscribes to the store that `useValidationSync` dispatches to. +- PHPCS config (`phpcs.xml.dist`) allows `ValidationAPI`, `validation_api`, `wp_register`, and `wp_validation` as global prefixes. -## Integration Pattern +## Integration pattern External plugins register checks like this: @@ -146,10 +178,13 @@ addFilter( 'editor.validateBlock', 'my-plugin/image-alt-text', ); ``` -## Key Docs +## Key docs -- `docs/PROPOSAL.md` — Core merge proposal -- `docs/INTEGRATION.md` — Gutenberg integration strategy -- `docs/TODO.md` — Remaining work (testing, TypeScript, performance, future features) -- `docs/guide/` — Developer integration guides -- `docs/technical/` — Architecture, API reference, hooks reference +- `docs/PR-READINESS.md` — "Where are we with the Gutenberg PR?" (start here if you're returning to this work) +- `docs/PROPOSAL.md` — Core-merge proposal (the RFC-style case for landing this in Gutenberg) +- `docs/gutenberg-alignment/consolidated-plan.md` — Execution record of the 5-batch alignment + polish pass +- `docs/gutenberg-alignment/core-pr-migration.md` — Checklist for when the core PR is actually cut +- `docs/TODO.md` — Active to-do list (testing gaps, perf benchmarks, future features) +- `docs/guide/` — Developer integration guides + troubleshooting +- `docs/technical/` — Architecture, data flow, API reference, hooks reference, decisions +- `docs/INTEGRATION.md` — Gutenberg integration context (what lands where in core) diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index 29c4ee7..77f7378 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -72,12 +72,13 @@ These items were adapted from plugin conventions to core conventions and are now | Component | Purpose | |---|---| | `core/validation` store | Centralized validation state | -| `ValidationProvider` | Single computation point for all validation | -| `ValidationAPI` | Side-effect manager (locks, CSS classes) | +| `useValidationSync` hook | Single computation point — reads source hooks + dispatches to store | +| `useValidationLifecycle` hook | Side-effect manager — `lockPostSaving` + body CSS classes | | `ValidationSidebar` | Consolidated validation results panel | | `ValidationToolbarButton` | Per-block validation toolbar UI | -| Block/Meta/Editor registries | Declarative check registration | +| Block/Meta/Editor registries (+ AbstractRegistry base) | Declarative check registration | | `wp_validation_check_level` filter | Runtime severity override | +| `editor.preSavePost` gate | Async save-time safety net layered on `lockPostSaving` | | REST `wp-validation/v1/checks` | Check introspection for admin tooling | ## Packages Affected @@ -87,7 +88,9 @@ These items were adapted from plugin conventions to core conventions and are now This is where the bulk of the integration lives: - **Store**: Register `core/validation` store alongside `core/editor` -- **Components**: `ValidationProvider`, `ValidationSidebar`, `ValidationAPI` integrated into editor initialization via `ExperimentalEditorProvider` +- **Hooks**: `useValidationSync` + `useValidationLifecycle` invoked from within the editor provider's render tree (or from dedicated renderless sibling wrappers) +- **Components**: `ValidationSidebar` mounted as a `ComplementaryArea` / `PluginSidebar` +- **Pre-save gate**: `editor.preSavePost` filter subscribed in the package (layered on top of `lockPostSaving`) - **Editor settings**: Validation config passed from PHP through the settings object that `setupEditor()` receives, via the `block_editor_settings_all` filter ### `@wordpress/block-editor` @@ -168,9 +171,11 @@ The naming alignment refactor is complete. The plugin now uses core-style names **Deliverables**: - JS filter hooks: `editor.validateBlock`, `editor.validateMeta`, `editor.validateEditor` -- `ValidationProvider` integrated into `ExperimentalEditorProvider` +- `useValidationSync` hook invoked from the editor provider tree (single computation point) +- `useValidationLifecycle` hook managing `lockPostSaving` + body classes +- `editor.preSavePost` async filter gate (belt-and-suspenders on top of lock) - Per-block validation via `editor.BlockEdit` and `editor.BlockListBlock` filters -- Debounced validation (300ms) to prevent performance issues +- Debounced per-block validation (300ms) to prevent performance issues **Why third**: This is where the system becomes usable. Depends on both the store (Phase 1) and config from PHP (Phase 2). @@ -241,7 +246,7 @@ The following naming changes have been applied throughout the codebase. These re ## Risks -1. **Performance at scale** -- Validating every block change in posts with hundreds of blocks needs benchmarking. The current debouncing (300ms) and `ValidationProvider` single-computation pattern help, but core demands higher standards. +1. **Performance at scale** -- Validating every block change in posts with hundreds of blocks needs benchmarking. The current per-block debouncing (300ms) and single-`useValidationSync` computation pattern help, but core demands higher standards. Polish item 6 (deferred) covers measurement; see [docs/TODO.md](TODO.md). 2. **API permanence** -- Once filter names and function signatures land in core, they cannot change without deprecation cycles. The current naming has been chosen to align with existing core conventions. @@ -259,8 +264,8 @@ The following naming changes have been applied throughout the codebase. These re 3. **Async validation** -- Should the framework support async validators from day one, or add it later? -4. **Server-side enforcement** -- Should core provide a `Validator` helper bridging client and server validation for meta fields, or leave server-side to the REST API layer? +4. **Server-side enforcement** -- The reference plugin currently does not ship a `Validator` helper. Plugins use the native `register_post_meta(..., 'validate_callback' => ...)` pattern for server-side enforcement alongside the client-side check. Does core want to bundle a thin helper, or leave this split as-is? 5. **Default checks** -- Should WordPress ship with any validation checks enabled by default? -6. **Relationship to `editor.preSavePost`** -- How does real-time validation relate to save-time validation? Separate concerns or unified framework? +6. **Relationship to `editor.preSavePost`** -- The reference plugin uses `editor.preSavePost` as a second line of defence behind `lockPostSaving` (throws if errors exist at save time). Does core want to formalize both as complementary or document the preference for one over the other? diff --git a/docs/PR-READINESS.md b/docs/PR-READINESS.md new file mode 100644 index 0000000..d73ccf9 --- /dev/null +++ b/docs/PR-READINESS.md @@ -0,0 +1,120 @@ +# Gutenberg PR Readiness + +Single-page answer to *"where is this plugin in the journey to a Gutenberg core PR, and what's next?"* Written for future-you returning after a break, or a collaborator picking up the work. + +**Current branch:** `review/multiple-plugins` +**Last alignment commit:** `5d956d4` (docs refresh after polish 1–4) +**Status:** Aligned + polished; pre-PR test gaps remain. + +## TL;DR + +The plugin has completed a five-batch alignment pass plus a four-item polish pass. Public API matches Gutenberg conventions. The code is ready for a *proposal-stage* PR (RFC post, discussion, initial store + registration surface). It is **not** ready for a *code-lands-in-core* PR until the deferred test and performance items ship. + +Three questions answer "what's next?": + +1. **Are you drafting the RFC / proposal post?** → You can start anytime. See [PROPOSAL.md](PROPOSAL.md) and [INTEGRATION.md](INTEGRATION.md); those are the inputs. +2. **Are you cutting code changes into `gutenberg/`?** → Activate [gutenberg-alignment/core-pr-migration.md](gutenberg-alignment/core-pr-migration.md). That doc is the checklist for translating the standalone plugin into core-style PHP + JS package. +3. **Are you still finishing pre-PR polish on this standalone plugin?** → See the "What's left" section below. + +## What's done + +### Alignment (shipped) + +Five batches on `review/multiple-plugins`. See [gutenberg-alignment/consolidated-plan.md](gutenberg-alignment/consolidated-plan.md) for commit hashes and per-batch detail. + +- **Batch 1** — JS source reshape to Gutenberg package layout; hook-first lifecycle; `editor.preSavePost` gate; `useValidationIssues` + `useMetaField` consolidations; `getInvalid*` → `useInvalid*`; aliases dropped +- **Batch 2** — REST namespace → `wp-validation/v1/checks` (plugin-owned; final core namespace TBD in review) +- **Batch 3** — `Core/I18n.php` deleted, `wp_set_script_translations()` inlined +- **Batch 4** — ~260 LOC of PHP dead code removed (`Meta\Validator`, `Contracts/CheckProvider`, orphan methods + hooks) +- **Batch 5** — `AbstractRegistry` extracted; Block/Meta/Editor registries share defaults / validation / filter plumbing + +### Polish (shipped) + +- **@example JSDoc** on 13 public API entries — store selectors + actions + `useMetaField` + `useMetaValidation` +- **TypeScript start** — `src/store/constants.ts` with typed `State`, `ValidationIssue`, `BlockValidationResult`, `MetaValidationResult`, `ValidationMode`, `IssueType` +- **Unit tests** — 56 tests covering store (reducer, actions, selectors) + `issue-helpers`. Run with `pnpm test`. + +### Docs (shipped) + +- [PROPOSAL.md](PROPOSAL.md) — RFC-style case for adding this to Gutenberg core +- [INTEGRATION.md](INTEGRATION.md) — Gutenberg landscape, component mapping, four-phase contribution plan +- [technical/README.md](technical/README.md) — Current architecture (post-alignment) +- [technical/data-flow.md](technical/data-flow.md) — PHP → JS → UI data path +- [technical/hooks.md](technical/hooks.md) — Every filter and action with parameters +- [technical/api.md](technical/api.md) — Function signatures and arg shapes +- [technical/decisions.md](technical/decisions.md) — Design-decision rationale +- [guide/troubleshooting.md](guide/troubleshooting.md) — Common issues + diagnostics +- [gutenberg-alignment/core-pr-migration.md](gutenberg-alignment/core-pr-migration.md) — Dormant checklist for when the PR branch is cut + +## What's left + +Grouped by whether it blocks RFC-stage vs. code-landing-stage. + +### Blocks code-landing (must ship before the PR is merge-ready) + +1. **Unit tests for validation functions** (Polish 5) + `validateBlock`, `validateMetaField`/`validateAllMetaChecks`, `validateEditor`. Requires mocking `@wordpress/hooks` filters and editor-settings. Target: `src/utils/__tests__/validate-*.test.js`. + +2. **Unit tests for custom hooks** (Polish 5b) + `useMetaField`, `useMetaValidation`, `useInvalidBlocks`, `useInvalidMeta`, `useInvalidEditorChecks`, `useValidationIssues`, `useDebouncedValidation`, `useValidationSync`, `useValidationLifecycle`. Requires `@testing-library/react` + store test harness. + +3. **Performance benchmarks** (Polish 6) + 200 / 500 / 1000 blocks. Measure `useInvalidBlocks` compute time, store-dispatch frequency, memory of `blockValidation` slice. Core reviewers will ask — have answers ready. Outputs determine whether to add per-block diffing or lazy-validation pre-PR. + +4. **E2E integration tests** (Polish 7) + `@wordpress/env` + `@wordpress/e2e-test-utils-playwright`. Full flow: PHP registration → editor settings → JS validation → store dispatch → save-lock → `editor.preSavePost` gate. Gives reviewers a reproducible harness. + +5. **Core-PR code translation** (core-pr-migration checklist) + PSR-4 → flat `WP_*` classes, text domain, `@since` versions, CSS prefix, sidebar mount swap to `ComplementaryArea`, Composer autoload → `require_once` chain. Activate [core-pr-migration.md](gutenberg-alignment/core-pr-migration.md) when drafting. + +### Does NOT block RFC-stage + +The RFC post can land against the current standalone plugin as the reference implementation. Reviewers will read `PROPOSAL.md`, examine the plugin's code, and provide feedback on shape. Only the items above block the follow-up code PR into `gutenberg/`. + +## Open questions that need core-team input + +From [INTEGRATION.md](INTEGRATION.md) "Open Questions" section. Expect these to come up in the RFC discussion: + +1. **Package home** — Should `core/validation` be a new `@wordpress/validation` package, or merged into `@wordpress/editor`? +2. **`block.json` integration** — Should simple validation rules (required attributes, patterns) be declarable in `block.json`? +3. **Async validation** — Ship with `applyFiltersAsync` support from day one, or add later? +4. **Server-side enforcement** — The plugin dropped its `Meta\Validator` helper; consumers use the native `register_post_meta(..., 'validate_callback' => ...)` pattern. Does core want a bundled helper, or leave this split? +5. **Default checks** — Should WordPress ship with any validation checks enabled by default, or strictly framework-only? +6. **`editor.preSavePost` relationship** — The plugin uses `editor.preSavePost` as a belt-and-suspenders over `lockPostSaving`. Does core want to formalize both as complementary or pick one? +7. **REST namespace** — `wp/v2/validation-checks` vs `wp-block-editor/v1/validation-checks` vs new `wp/v2/validation` resource? +8. **Site editor support** — Currently excluded. Template validation is a related but separate problem; reviewers will ask if it's in scope. + +## Risks called out in PROPOSAL / INTEGRATION + +Kept here as a reminder during drafting: + +- **Performance at scale** — per-block debouncing + single `useValidationSync` computation help; benchmarks are Polish 6 +- **API permanence** — filter names and function signatures need careful review before they land; renaming post-merge requires deprecation cycles +- **Scope creep** — discussions may pull in content linting, accessibility auditing, editorial workflows. The framework/rules boundary must hold. +- **Field API overlap** — DataViews/DataForm has its own validation model (Gutenberg #71500). Coordination needed if that pattern expands. +- **Site editor gap** — current scope excludes site editor intentionally; not a blocker but will come up. + +## What to do first when resuming + +1. **Skim this doc** and the commit log since you last touched the code: `git log --oneline review/multiple-plugins` +2. **Run `pnpm test`** — confirm 56 tests still pass +3. **Activate the plugin in your local WP** — confirm nothing regressed since the last verification +4. **Pick one of:** + - If you want to draft the RFC: re-read [PROPOSAL.md](PROPOSAL.md), [INTEGRATION.md](INTEGRATION.md), post in Gutenberg GitHub discussions or Slack #core-editor + - If you want to chip away at pre-PR polish: pick one of Polish 5 / 5b / 6 / 7 from [gutenberg-alignment/consolidated-plan.md](gutenberg-alignment/consolidated-plan.md) deferred list + - If you want to draft the code PR: clone `gutenberg/` trunk fresh, create a feature branch, open [gutenberg-alignment/core-pr-migration.md](gutenberg-alignment/core-pr-migration.md) as your checklist + +## Quick reference — what shipped, what didn't + +| Item | Status | Commit | +|---|---|---| +| 5-batch alignment (plugin stays standalone-friendly) | ✅ Shipped | See consolidated-plan.md | +| @example JSDoc on public API | ✅ Shipped | `561e32a` | +| TypeScript start (constants.ts + types) | ✅ Shipped | `a228e5d` | +| Jest unit tests (56 tests for store + issue-helpers) | ✅ Shipped | `f78624e` | +| Docs refresh / audit / PR-readiness | ✅ Shipped | this commit | +| Unit tests for validate-* functions | ⏳ Deferred | — | +| Unit tests for custom hooks | ⏳ Deferred | — | +| Performance benchmarks | ⏳ Deferred | — | +| E2E tests | ⏳ Deferred | — | +| Core-PR code translation | ⏳ Dormant | See core-pr-migration.md | diff --git a/docs/PROPOSAL.md b/docs/PROPOSAL.md index f182bf0..781bb55 100644 --- a/docs/PROPOSAL.md +++ b/docs/PROPOSAL.md @@ -238,7 +238,7 @@ When any check fails at the `error` level, the API uses `lockPostSaving` to prev ### State Management -Validation results are managed through a dedicated `@wordpress/data` store. A single `ValidationProvider` component computes all validation and dispatches results to the store. All other components read from the store -- no duplicate computation. +Validation results are managed through a dedicated `@wordpress/data` store. A single `useValidationSync` hook computes all validation and dispatches results to the store. All other consumers read from the store -- no duplicate computation. **Store structure:** @@ -260,7 +260,7 @@ Validation results are managed through a dedicated `@wordpress/data` store. A si - `hasErrors()` -- Whether any error-level failures exist - `hasWarnings()` -- Whether warning-level failures exist (and no errors) -This architecture separates concerns cleanly: `ValidationProvider` handles computation, `ValidationAPI` handles side effects (save-locking, body CSS classes), and UI components like `ValidationSidebar` handle display. +This architecture separates concerns cleanly: `useValidationSync` handles computation, `useValidationLifecycle` handles side effects (save-locking, body CSS classes), and UI components like `ValidationSidebar` handle display. A reactive save-lock (`lockPostSaving`) is layered with an async safety net (`editor.preSavePost` throws if errors exist at save time) for defense in depth. ### Validation Results UI @@ -286,13 +286,16 @@ The [Validation API](https://github.com/troychaplin/validation-api) plugin demon ### Architecture -- **PHP Registries** -- Singleton registries (`Block\Registry`, `Meta\Registry`, `Editor\Registry`) manage check registration, configuration, and data export via filters and actions. -- **`@wordpress/data` Store** -- A dedicated Redux store (`core/validation`) centralizes all validation state with actions, selectors, and a reducer. -- **`ValidationProvider`** -- A renderless component that serves as the single computation point. Calls validation hooks for blocks, meta, and editor checks, then dispatches results to the store. -- **`ValidationAPI`** -- A renderless component that manages side effects: `lockPostSaving`/`unlockPostSaving`, `lockPostAutosaving`/`unlockPostAutosaving`, `disablePublishSidebar`/`enablePublishSidebar`, and body CSS classes. -- **JavaScript Validation** -- Validation logic runs entirely in JavaScript via WordPress filters (`editor.validateBlock`, `editor.validateMeta`, `editor.validateEditor`). -- **Configuration Export** -- PHP configuration is passed to JavaScript via the `block_editor_settings_all` filter, delivering validation rules and editor context through editor settings. -- **Plugin Attribution** -- The `namespace` field in check registration args attributes checks to the registering plugin, enabling organized settings and REST API attribution. +- **PHP Registries** — Singleton registries (`Block\Registry`, `Meta\Registry`, `Editor\Registry`) extending a shared `AbstractRegistry` base class. The abstract holds defaults, required-field validation, namespace stamping, priority sort, and `wp_validation_check_level` filter application; the concrete registries differ only in storage shape and scope-specific hook names. +- **`@wordpress/data` Store** — A dedicated Redux store (`core/validation`) centralizes all validation state with actions, selectors, and a reducer. +- **`useValidationSync` hook** — Single computation point. Calls `useInvalidBlocks`/`useInvalidMeta`/`useInvalidEditorChecks`, dispatches the results to the store. +- **`useValidationLifecycle` hook** — Manages editor-wide side effects: `lockPostSaving`/`unlockPostSaving`, `lockPostAutosaving`/`unlockPostAutosaving`, `disablePublishSidebar`/`enablePublishSidebar`, and body CSS classes. +- **Save-time gate** — The `editor.preSavePost` async filter is subscribed as a belt-and-suspenders safety net on top of `lockPostSaving`: if errors exist at save time the filter throws, aborting the save. +- **JavaScript Validation** — Validation logic runs entirely in JavaScript via WordPress filters (`editor.validateBlock`, `editor.validateMeta`, `editor.validateEditor`). +- **Configuration Export** — PHP configuration is passed to JavaScript via the `block_editor_settings_all` filter, delivering validation rules and editor context through editor settings. +- **Plugin Attribution** — The `namespace` field in check registration args attributes checks to the registering plugin, enabling organized settings and REST API attribution. + +> Note on terminology: the two hooks above are called from small renderless sibling wrappers (`ValidationSync`, `ValidationLifecycle`) under the root `ValidationPlugin` component. The sibling arrangement is deliberate — putting both hooks in a single parent component causes an infinite render loop because `useValidationLifecycle` subscribes to the store that `useValidationSync` dispatches to. See [docs/technical/README.md](technical/README.md) for the full explanation. ### REST API diff --git a/docs/README.md b/docs/README.md index 83a3b54..5e675f6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,9 @@ # Validation API Documentation +## Start here + +- **[PR-READINESS.md](PR-READINESS.md)** — Where is this plugin on the road to a Gutenberg core PR? Start here if you're resuming the work or picking it up fresh. + ## Developer Guide For plugin authors integrating with the Validation API. @@ -10,14 +14,27 @@ For plugin authors integrating with the Validation API. - **[Editor Checks](guide/editor-checks.md)** — Validate document-level concerns (heading hierarchy, content structure) - **[Severity Model](guide/severity.md)** — Error vs. warning vs. none, and how to override levels at runtime - **[Examples](guide/examples.md)** — Complete integration examples and common recipes +- **[Troubleshooting](guide/troubleshooting.md)** — Common issues and how to diagnose them ## Technical Reference For contributors and the WordPress core team reviewing this plugin. -- **[Architecture](technical/README.md)** — System design, registries, data store, and UI components +- **[Architecture](technical/README.md)** — System design, registries, data store, hooks layer, UI components - **[Data Flow](technical/data-flow.md)** — How checks move from PHP registration through to JS validation and UI - **[Hooks Reference](technical/hooks.md)** — Every PHP action/filter and JS filter with signatures -- **[API Reference](technical/api.md)** — All public registration functions, registry methods, and contracts +- **[API Reference](technical/api.md)** — All public registration functions and registry methods - **[Companion Package](technical/companion-package.md)** — Settings companion architecture and the filter bridge - **[Design Decisions](technical/decisions.md)** — Why the API is shaped the way it is + +## Core-merge proposal + +For the Gutenberg core team. + +- **[PROPOSAL.md](PROPOSAL.md)** — RFC-style case for adopting this framework into Gutenberg core +- **[INTEGRATION.md](INTEGRATION.md)** — Gutenberg landscape, component mapping, four-phase contribution plan +- **[gutenberg-alignment/](gutenberg-alignment/README.md)** — Execution record of the alignment work and checklist for the actual PR + +## Working notes + +- **[TODO.md](TODO.md)** — Active to-do list (testing gaps, perf benchmarks, future features) diff --git a/docs/guide/troubleshooting.md b/docs/guide/troubleshooting.md new file mode 100644 index 0000000..358f82b --- /dev/null +++ b/docs/guide/troubleshooting.md @@ -0,0 +1,160 @@ +# Troubleshooting + +Common issues you may run into when integrating with the Validation API, and how to diagnose them. + +For a conceptual overview of how data moves through the system, see [docs/technical/data-flow.md](../technical/data-flow.md). + +## My check is registered but doesn't appear in the sidebar + +Most common causes, in order: + +1. **Severity is set to `'none'`.** Checks at level `'none'` are filtered out entirely — they don't surface in the sidebar, don't participate in save locking, and don't appear in the REST response. Check what the effective level is: + + ``` + http:///wp-json/wp-validation/v1/checks + ``` + + Look for your check in the response. If `level: 'none'`, something is overriding it — most likely the settings companion plugin. Open that plugin's settings page (`/wp-admin/?page=validation-api-settings`) and raise the level back to `'warning'` or `'error'`. + +2. **The validation hook returns `true` (valid).** The sidebar only lists *invalid* results. If your `editor.validateBlock` / `editor.validateMeta` / `editor.validateEditor` filter callback always returns `true`, nothing surfaces. Add a `console.log` inside your filter to confirm it's being called and returning the expected value. + +3. **`check_name` mismatch.** The filter callback's `checkName` parameter must match the `name` field you passed to `wp_register_block_validation_check()`. A typo here silently does nothing. + +4. **Wrong scope / post type.** Meta and editor checks register per post type. If you registered for `'post'` but you're editing a `'page'`, the check isn't loaded. + +5. **You're in the site editor.** Validation is intentionally not loaded in the site editor (template/global-styles editing). Switch to the post editor to see checks fire. + +## My validation function isn't being called + +1. **Filter name typo.** The three filter names are: + - `editor.validateBlock` + - `editor.validateMeta` + - `editor.validateEditor` + + (Note the camelCase and the `editor.` prefix.) + +2. **`function_exists` guard returned early.** If the Validation API plugin is deactivated or not loaded yet when your `init` callback runs, registration is skipped. Check: + + ```php + add_action( 'init', function() { + if ( ! function_exists( 'wp_register_block_validation_check' ) ) { + return; // ← if this fires, your check never registers + } + // ... + } ); + ``` + +3. **JS bundle not enqueued.** Your plugin's JS must be loaded in the block editor. If you're using `wp-scripts`, the default build output + enqueue should handle this. Confirm the JS file loads by checking the browser's Network tab. + +4. **Filter added too late.** `addFilter` calls at module scope (top of file) are safe — they register at page load. If you add filters inside a `useEffect` or event handler, they may register after the validation runner has already run. Move them to module scope. + +## The REST endpoint returns 401 Unauthorized + +The endpoint requires `manage_options`. You need to be logged in as an administrator. + +Direct URL access in a browser works if you're logged in via cookies. `curl` needs auth: + +```bash +curl -u admin:password 'http://localhost/wp-json/wp-validation/v1/checks' +``` + +Or with an application password, use the `--user` flag: + +```bash +curl --user admin:abcd-1234-efgh-5678 'http://localhost/wp-json/wp-validation/v1/checks' +``` + +## The REST endpoint returns 404 + +1. **Permalink rewrite cache.** Rare, but if other REST endpoints also 404, go to Settings → Permalinks → Save (no changes needed — just flush). REST itself doesn't use rewrite rules, but some hosting/caching setups interfere with `/wp-json/`. + +2. **The plugin isn't active.** Confirm Validation API is activated in the Plugins screen. The REST controller only registers when the plugin loads. + +3. **Old path cached in your browser/bundle.** The endpoint moved from `wp/v2/validation-checks` to `wp-validation/v1/checks`. If you're seeing a 404 on the old path, rebuild any consuming bundle and hard-reload (Cmd+Shift+R / Ctrl+F5 with "Disable cache" in DevTools). + +## Block borders don't appear on invalid blocks + +The red/yellow border is applied by the `editor.BlockListBlock` filter reading per-block state from the `core/validation` store. Common causes of missing borders: + +1. **Stale browser cache.** Hard-reload the editor page with cache disabled. + +2. **Your check always returns `true`.** No issues = no borders. See the "validation function isn't being called" section above. + +3. **CSS conflict.** The border classes are `validation-api-block-error` and `validation-api-block-warning`. A theme stylesheet may override them. Inspect the block element in DevTools and confirm the class is present and the CSS rule is winning. + +## Meta field doesn't show a border + +Meta fields need the `useMetaField` (or `useMetaValidation`) hook to be wired into the TextControl for border classes to apply. + +If you're using a copy of `useMetaField` in your own plugin (common pattern in integration examples), make sure it reads from the `core/validation` store to get the validation state: + +```js +const invalidMeta = select( 'core/validation' ).getInvalidMeta(); +const thisField = invalidMeta.find( m => m.metaKey === metaKey ); +``` + +If your shim doesn't query the store, it has no way to know the field is invalid and won't apply the class. + +## Save button stays disabled after fixing all issues + +1. **Validation is still computing.** The per-block validation is debounced 300ms. Wait a second after fixing the last issue, then the publish button should re-enable. + +2. **Something else is locking saving.** Other plugins can also call `lockPostSaving()`. In the browser console: + + ```js + wp.data.select( 'core/editor' ).getPostLockUser() + wp.data.select( 'core/editor' ).isPostSavingLocked() + ``` + + If `isPostSavingLocked()` is true, check which plugin owns the lock. Validation API locks under the key `'core/validation'`. + +3. **An `editor.preSavePost` filter is throwing.** If you have other filters on `editor.preSavePost` from other plugins, one may be blocking. Try saving with other plugins temporarily deactivated to isolate. + +## Page shows "The response is not a valid JSON response" when saving + +Usually means the save is hitting a PHP error somewhere. Check `wp-content/debug.log` for the actual error. Common causes: + +- A `validate_callback` in `register_post_meta()` is returning something that isn't `true` or a `WP_Error` (e.g., `false`, which confuses the REST controller) +- Another plugin is injecting invalid HTML into the response +- A PHP fatal error in any hook that fires during `editor.preSavePost` processing + +The Validation API itself doesn't throw JSON-breaking errors on valid input, but its `editor.preSavePost` gate does throw to abort when errors exist — the client treats that as a save failure, which is the intended behavior. + +## "The 'core-validation' plugin has encountered an error and cannot be rendered" + +React error, usually a render loop or a null access. Open the browser console and look for the stack trace: + +- **Error #185 (Maximum update depth exceeded):** Check that you haven't added a hook that both subscribes to `core/validation` and dispatches to it in the same component — that's an infinite loop. See [consolidated-plan.md](../gutenberg-alignment/consolidated-plan.md) Batch 1 section for the historical fix. +- **Cannot read property of undefined:** Usually a missing store (`select( 'core/validation' )` returning `null` because the store isn't registered yet). Confirm the Validation API plugin is active. + +## My integration works locally but not on production + +1. **Text domain mismatch.** The `function_exists` guard is the only cross-plugin dependency check; that works everywhere. But if you see translated strings in one environment and not another, confirm your plugin's text domain is loaded and your `.mo`/`.json` translation files are in place. + +2. **Build not deployed.** The JS bundle has to be rebuilt after any filter-name change, and deployed. Check the file modified time on the production build output. + +3. **Caching layers.** Varnish, page cache, or a CDN can serve a stale bundle. Bust the cache for your plugin's `build/` directory. + +4. **Plugin load order.** Both plugins (Validation API + your plugin) need to load before `init` fires. If some plugin loader activation order differs between environments, your `function_exists` guard may fail in production but pass locally. Check `active_plugins` option in both. + +## My settings override disappeared / reverted + +The settings addon stores overrides in `wp_options['validation_api_settings']`. If you see a level you set revert to the default: + +1. **Key format changed.** The override stored is keyed by scope + identifier + check_name. If you renamed a check, the stored override no longer matches and falls back to the default. +2. **Another filter is running later with a higher priority.** Debug by adding: + ```php + add_filter( 'wp_validation_check_level', function( $level, $context ) { + error_log( 'wp_validation_check_level: ' . print_r( $context, true ) . ' → ' . $level ); + return $level; + }, 999, 2 ); + ``` + This logs every call to the filter and the return value. Check the log to see what each filter in the chain does. + +## I need more info + +- [docs/guide/README.md](README.md) — Quickstart for integrating +- [docs/technical/data-flow.md](../technical/data-flow.md) — Full data flow diagram +- [docs/technical/hooks.md](../technical/hooks.md) — Every hook and filter with parameters +- [docs/technical/api.md](../technical/api.md) — Function signatures +- Browser console `wp.data.select('core/validation')` — Inspect live store state diff --git a/docs/gutenberg-alignment/README.md b/docs/gutenberg-alignment/README.md index 6be3b99..d630d70 100644 --- a/docs/gutenberg-alignment/README.md +++ b/docs/gutenberg-alignment/README.md @@ -1,30 +1,19 @@ # Gutenberg Alignment -Planning docs for aligning the Validation API plugin with current Gutenberg conventions, in preparation for a potential core-merge proposal. These are planning artifacts — not active implementation yet. +Documents tracking the alignment work that brought the plugin to a state ready for a Gutenberg core-merge proposal. The five-batch alignment plan and the four-item polish pass have shipped; remaining work is captured in the files below. -## Read in this order +## Files -1. **[consolidated-plan.md](consolidated-plan.md)** — Start here. The authoritative execution plan: five batches, order, acceptance criteria, verification steps. -2. **[pass-a.md](pass-a.md)** — Convention & alignment findings. Contains the full checklist for every batch. -3. **[pass-b.md](pass-b.md)** — Architectural review. Explains why `ValidationProvider` + `ValidationAPI` convert to hooks and why `editor.preSavePost` gets added. -4. **[pass-c.md](pass-c.md)** — Leanness review. Explains the ~375 LOC of deletable PHP. -5. **[core-pr-migration.md](core-pr-migration.md)** — Deferred items. Activate only when all NOW-batches ship and a core PR is being cut. - -## Scope - -| Document | Covers | Status | +| Document | Purpose | Status | |---|---|---| -| `pass-a.md` | Naming, file layout, style, REST namespace, package-ready src/ layout | Complete | -| `pass-b.md` | Renderless components vs hooks, SlotFills, `registerPlugin`, save-locking pattern, REST permissions | Complete | -| `pass-c.md` | Dead code, duplication, abstraction cost/benefit | Complete | -| `consolidated-plan.md` | Batch sequencing, acceptance criteria, manual verification | Complete | -| `core-pr-migration.md` | PSR-4 → WP-core style, text domain, `@since`, sidebar mount, CSS prefix | Complete (dormant until PR) | +| **[consolidated-plan.md](consolidated-plan.md)** | Execution record of the five alignment batches plus the four-item polish pass. Shows what shipped, with commit hashes, acceptance criteria, and the deferred-polish list (items 5-7). | Complete; kept as reference | +| **[core-pr-migration.md](core-pr-migration.md)** | Checklist activated when the core PR is actually cut. Covers PSR-4 → WP-core PHP style, text domain changes, `@since` versioning, REST-namespace finalization, sidebar mount swap, and other items that only make sense in-core. | Dormant until the PR | +| **[../PR-READINESS.md](../PR-READINESS.md)** | Single-page answer to "where are we, what's next for the Gutenberg PR?" Written for future-you returning after a break. | Active | -## NOW vs. deferred +## History -- **NOW batches** (in `consolidated-plan.md` + `pass-a.md`) preserve standalone-plugin viability. No public API breaking changes except the REST namespace (coordinated with the only consumer, the settings addon). -- **Deferred migrations** (in `core-pr-migration.md`) would break standalone functionality and only make sense when translating the plugin into core-style PHP/JS. Activate when cutting the actual core PR. +Detailed audit + planning notes from the alignment work (pass-a convention review, pass-b architectural review, pass-c leanness review) were deleted after the batches shipped. Git history preserves them if ever needed; the decisions they captured live in the code, in the consolidated plan, and in individual commit messages (`batch 1:`, `batch 2:`, etc. — see `git log`). ## Ownership context -User owns all three related plugins (`validation-api`, `validation-api-settings`, `validation-api-integration-example`). No backward-compat concerns apply to internal consumers; coordinated changes across all three are acceptable. +User owns all three related plugins — `validation-api` (this plugin), `validation-api-settings` (companion admin UI), `validation-api-integration-example` (demo). No backward-compat concerns apply to internal consumers; coordinated changes across all three are acceptable. diff --git a/docs/gutenberg-alignment/consolidated-plan.md b/docs/gutenberg-alignment/consolidated-plan.md index 9d4b929..50614b2 100644 --- a/docs/gutenberg-alignment/consolidated-plan.md +++ b/docs/gutenberg-alignment/consolidated-plan.md @@ -1,264 +1,57 @@ -# Consolidated Action Plan — Gutenberg Alignment +# Gutenberg Alignment — Execution Record -Authoritative execution plan for aligning the Validation API plugin with Gutenberg conventions while preserving standalone-plugin viability. Synthesizes findings from Pass A (conventions), Pass B (architecture), and Pass C (leanness). +Execution record of the five-batch alignment plan and the subsequent polish pass. All batches shipped; polish items 1–4 complete, items 5–7 deferred to the pre-PR phase. Detailed per-batch planning documents (pass-a, pass-b, pass-c) were removed after the work shipped — git history preserves them. -For rationale and evidence behind each change, see: -- [pass-a.md](pass-a.md) — conventions & alignment -- [pass-b.md](pass-b.md) — architecture -- [pass-c.md](pass-c.md) — leanness -- [core-pr-migration.md](core-pr-migration.md) — deferred core-merge-only changes +For the active to-do list, see [../TODO.md](../TODO.md). +For the pre-PR migration checklist (dormant until the core PR is cut), see [core-pr-migration.md](core-pr-migration.md). +For current PR-readiness status, see [../PR-READINESS.md](../PR-READINESS.md). -## Status +## The five batches -- [x] Pass A complete -- [x] Pass B complete -- [x] Pass C complete -- [ ] Consolidated plan approved -- [ ] Execution +All five shipped to `review/multiple-plugins`. Public API unchanged across every batch except the REST endpoint path (coordinated with the settings addon in the same change). -## Scope +| Batch | Commit | Summary | +|---|---|---| +| **1** | `16b1dce` | JS source reshape to Gutenberg-package layout. Flat `src/{store, utils, hooks, components}/`. Renderless `ValidationProvider`/`ValidationAPI` converted to `useValidationSync` + `useValidationLifecycle` hooks (invoked from sibling renderless wrappers to avoid render loops). `editor.preSavePost` save gate added. `useValidationIssues()` consolidated hook. `useMetaField` dual `useSelect` collapsed. `getInvalid*` → `useInvalid*`. Webpack aliases dropped. `package.json` `sideEffects` declared. | +| **2** | `c927184` + `0088bc8` (addon) | REST namespace: `wp/v2/validation-checks` → `wp-validation/v1/checks`. Plugin-owned namespace; final core namespace TBD during PR review. Settings addon updated in lockstep. | +| **3** | `3d35352` | `includes/Core/I18n.php` deleted; `wp_set_script_translations()` inlined in `Core/Assets.php`. 58 LOC removed. | +| **4** | `7ab948d` + `826847d` (addon hot-fix) | PHP dead-code deletions (~260 LOC). Removed: `Meta\Validator` class, `Contracts/CheckProvider` interface, `Block\Registry::unregister_check()` + `set_check_enabled()`, `Editor\Registry::register_editor_check_for_post_types()`, `EditorDetection` `get_current_screen()` fallback, two orphan actions (`wp_validation_check_unregistered`, `wp_validation_check_toggled`), one orphan filter (`wp_validation_validate_meta`). Settings addon hot-fix restored meta-field border styling after a prior commit had broken it. | +| **5** | `c44e389` | `includes/AbstractRegistry.php` extracted. Block/Meta/Editor registries extend it. Shared: defaults merge, required-field check, level validation, `warning_msg` fallback, `namespace` stamping, priority sort, `wp_validation_check_level` filter application. Priority validation now consistent across all three scopes (was only Block). Logger trait methods changed from `private` to `protected` so subclasses inherit. | -**Five batches.** Estimated impact: -- Reduced PHP LOC: ~375 (Batches 3, 4, 5) -- JS restructure: ~40 files moved/renamed, ~22 LOC saved (Batch 1) -- REST namespace cleanup (Batch 2) +### What's unchanged (still true) -All batches preserve the plugin's public API (global PHP registration functions, JS filter names, store name, kept hooks). The REST endpoint path is the only externally visible breaking change, and the only consumer (settings addon) is updated in the same change. +Public API surface preserved through every batch: -| Batch | Summary | Risk | From passes | -|---|---|---|---| -| 1 | JS source reshape: flat package layout, hook-based lifecycle, drop aliases, absorb `useValidationIssues` + `useMetaField` consolidation, add `pre-save-validation`, rename `getInvalid*` → `useInvalid*` | Low-Medium | A + B + C | -| 2 | REST namespace move: `wp/v2/validation-checks` → `wp-validation/v1/checks`; settings addon updated in same change | Low | A | -| 3 | Delete `Core/I18n.php`; inline `wp_set_script_translations()` in `Core/Assets.php` | None | A (confirmed C) | -| 4 | PHP dead-code deletions (~260 LOC): `Meta\Validator`, `Contracts/CheckProvider`, dead registry methods, orphan hooks, unreachable `EditorDetection` branch | Very low | C | -| 5 | Extract `AbstractValidationRegistry` base class (~115 LOC saved via deduplication) | Medium | C | +- Global PHP functions: `wp_register_block_validation_check()`, `wp_register_meta_validation_check()`, `wp_register_editor_validation_check()` +- JS filter names: `editor.validateBlock`, `editor.validateMeta`, `editor.validateEditor` +- Store name: `core/validation` +- PHP hook prefix: `wp_validation_*` +- Registry singleton pattern (`::get_instance()`) +- `block_editor_settings_all` injection mechanism +- Severity model (`error` / `warning` / `none`) +- Editor context scoping (post editor only; site editor intentionally excluded) -## Execution order +## Polish pass -**Recommended order:** cleanup before restructure. Quick wins first, biggest change last. +| Item | Commit | Summary | +|---|---|---| +| **1** | `561e32a` | `@example` JSDoc blocks on 13 public-API entries (6 store selectors, 5 store actions, `useMetaField`, `useMetaValidation`). Matches Gutenberg package convention. | +| **2** | `a228e5d` | TypeScript migration started: `src/store/constants.ts` with typed state, issues, action types. Stale `babel.config.json` deleted so `@wordpress/scripts`' default preset (which includes `@babel/preset-typescript`) takes effect. Incidental bundle-size drop ~94KB → ~70KB. | +| **3 + 4** | `f78624e` | Jest unit-test infrastructure (`pnpm test` via `@wordpress/scripts test-unit-js`) + 56 tests covering store reducer/actions/selectors and `issue-helpers`. All tests pass in ~1s. | -``` -1. Batch 4 (PHP deletions) — fast win, reduces surface for Batch 5 -2. Batch 3 (I18n inline) — trivial, clears PHP clutter -3. Batch 5 (Registry extraction) — PHP refactor, benefits from 4 being done -4. Batch 2 (REST namespace) — coordinated with settings addon -5. Batch 1 (JS restructure) — largest single change; do when PHP is settled -``` +### Deferred polish (pick up before the PR) -**Rationale:** -- Batches 4 + 3 are near-zero risk. Ship them first, enjoy the cleaner baseline. -- Batch 5 is easier after 4 (fewer dead methods to reason about when extracting the base class). -- Batch 2 is self-contained and coordinates with the settings addon — do it before any JS work that might surface REST dependencies. -- Batch 1 is the largest scope. Doing it last means the PHP side is stable and we're not juggling two moving targets. +- **Polish 5** — Unit tests for `validateBlock`, `validateMetaField`/`validateAllMetaChecks`, `validateEditor`. Requires `@wordpress/hooks` filter mocking and editor-settings mocking. Targets: `src/utils/__tests__/validate-*.test.js`. +- **Polish 5b** — Unit tests for custom hooks (`useMetaField`, `useMetaValidation`, `useInvalidBlocks`, `useInvalidMeta`, `useInvalidEditorChecks`, `useValidationIssues`, `useDebouncedValidation`, `useValidationSync`, `useValidationLifecycle`). Requires `@testing-library/react` + store test harness. +- **Polish 6** — Performance benchmarks with 200+/500+/1000+ block posts. Measure `useInvalidBlocks` timing, dispatch churn, memory of `blockValidation` store slice. Outputs inform whether to add per-block diffing or lazy validation before the PR. +- **Polish 7** — Integration / e2e tests via `@wordpress/env` + `@wordpress/e2e-test-utils-playwright`. Covers PHP registration → editor settings injection → JS validation → store dispatch → save-lock → `editor.preSavePost` gate. -**Dependencies (strict):** +## Deferred — core-PR-only changes -- Batch 5 depends on Batch 4 (don't refactor code that's about to be deleted). -- Batch 2 core-plugin change and settings addon change must ship together (either atomic commit or immediate back-to-back). -- No other hard dependencies. Batches can be parallelized if preferred. +Changes that only make sense when translating to core-style code; they would break standalone-plugin viability. Full migration checklist in [core-pr-migration.md](core-pr-migration.md). Summary: -**Alternative orderings** (acceptable): -- Strict "smallest first": 3 → 4 → 2 → 5 → 1 -- "PHP then JS, both internal": 4 → 3 → 5 → 1 → 2 - -## Per-batch detail - -Each batch's full checklist lives in [pass-a.md](pass-a.md). This section provides acceptance criteria and verification steps only. - -### Batch 1 — JS source reshape - -**Full checklist:** See [pass-a.md](pass-a.md) Batch 1. - -**Summary of touched files:** -- All files under `src/editor/` and `src/shared/` move to a flat `src/` layout -- `src/script.js` renamed to `src/index.js` -- `src/editor/hoc/with*.js` files convert from HOC-exports to module-scope side effects in `src/hooks/` -- `ValidationProvider.js` + `ValidationAPI.js` convert from renderless components to hooks (`useValidationSync`, `useValidationLifecycle`) -- New file `src/hooks/pre-save-validation.js` (adds `editor.preSavePost` filter) -- New file `src/utils/use-validation-issues.js` (consolidates duplicate `useSelect`) -- `src/utils/use-meta-field.js` consolidates its dual `useSelect` -- `getInvalid*.js` hook wrappers rename to `useInvalid*.js` -- Webpack aliases dropped; imports become relative -- `package.json` gains `sideEffects` field - -**Acceptance criteria:** -- [ ] `pnpm lint` passes -- [ ] `pnpm build` succeeds; `build/validation-api.js` still produced at expected path -- [ ] No file under `src/editor/` or `src/shared/` remains -- [ ] No `@`, `@editor`, `@shared` alias imports remain -- [ ] Grep for `ValidationProvider\|ValidationAPI` in JS finds no renderless-component definitions (only `useValidationSync` / `useValidationLifecycle`) - -**Manual verification (in WP):** -- [ ] Open a post — editor loads without console errors -- [ ] Integration example plugin's validation checks fire (block check, meta check, editor check all surface in sidebar) -- [ ] Sidebar opens under the validation icon; shows grouped issues -- [ ] Toolbar button appears on blocks with validation errors; clicking shows modal with messages -- [ ] Add/resolve an error; verify `lockPostSaving` toggles (Publish/Update button enables/disables) -- [ ] Attempt to save while in error state; `editor.preSavePost` throws; UI shows save failed -- [ ] Body classes `has-validation-errors` / `has-validation-warnings` apply correctly -- [ ] Click a block error in the sidebar; editor scrolls to and selects the block - -**Post-batch rebuild:** -- [ ] Rebuild `validation-api-integration-example` (`npm run build` in its directory) - -**Rollback:** Single large commit; revert if issues. Or: keep the old `src/editor/` + `src/shared/` tree in a branch until Batch 1 is verified. - ---- - -### Batch 2 — REST namespace move - -**Full checklist:** See [pass-a.md](pass-a.md) Batch 2. - -**Touched files:** -- `includes/Rest/ChecksController.php` — namespace + rest_base -- `validation-api-settings/src/settings/App.js` — fetch path -- Documentation: `docs/PROPOSAL.md`, `docs/technical/*`, `CLAUDE.md`, settings addon README - -**Acceptance criteria:** -- [ ] `curl -u admin:pass http://site/wp-json/wp-validation/v1/checks` returns the grouped-by-scope response -- [ ] `curl http://site/wp-json/wp/v2/validation-checks` returns 404 -- [ ] Grep across all plugins: no references to old path remain - -**Manual verification:** -- [ ] Settings addon page loads; table populates with all registered checks -- [ ] Change a check's level, save; reload; change persists -- [ ] `wp_validation_check_level` filter still applies the override in the editor (confirm by registering a check and setting it to "none" — check should disappear from validation surface) - -**Rollback:** Revert the two file changes and rebuild settings addon. - ---- - -### Batch 3 — I18n class simplification - -**Full checklist:** See [pass-a.md](pass-a.md) Batch 3. - -**Touched files:** -- `includes/Core/I18n.php` (deleted) -- `includes/Core/Assets.php` (inlined `wp_set_script_translations`) -- `includes/Core/Plugin.php` (I18n instantiation removed) - -**Acceptance criteria:** -- [ ] PHP loads without fatal errors -- [ ] `pnpm build` still succeeds -- [ ] `wp_set_script_translations` is called exactly once for the editor script handle (grep to confirm) - -**Manual verification:** -- [ ] If a translation `.json` exists in `languages/`, editor strings render translated -- [ ] No I18n-related PHP warnings in debug log - -**Rollback:** Revert the file changes; `I18n.php` is a pure deletion. - ---- - -### Batch 4 — PHP dead-code deletions - -**Full checklist:** See [pass-a.md](pass-a.md) Batch 4. - -**Touched files:** -- `includes/Meta/Validator.php` (deleted) -- `includes/Contracts/CheckProvider.php` (deleted) -- `includes/Contracts/` (directory removed if empty) -- `includes/Block/Registry.php` (two methods removed, two actions removed) -- `includes/Editor/Registry.php` (one method removed) -- `includes/Core/Traits/EditorDetection.php` (fallback branch removed) -- `docs/guide/check-providers.md` (removed or updated) -- `docs/technical/hooks.md` (two hooks removed from documentation, if listed) - -**Acceptance criteria:** -- [ ] PHP linting passes -- [ ] Plugin activates without fatal errors -- [ ] `includes/Contracts/` directory removed or empty -- [ ] No PHP warnings for missing classes/methods -- [ ] Grep confirms zero remaining references to: `Meta\Validator`, `CheckProvider`, `unregister_check`, `set_check_enabled`, `register_editor_check_for_post_types`, `wp_validation_check_unregistered`, `wp_validation_check_toggled` - -**Manual verification:** -- [ ] Settings addon still reads all checks via REST endpoint -- [ ] `wp_validation_check_level` filter still fires -- [ ] Integration example plugin registers all its checks (visible in settings addon table) -- [ ] Editor post/page editor still enqueues validation scripts (post editor context detection still works after removing the fallback branch) - -**Rollback:** Deletions are recoverable via git. Each deletion is independent — can restore one without affecting others. - ---- - -### Batch 5 — Registry abstract base class extraction - -**Full checklist:** See [pass-a.md](pass-a.md) Batch 5. - -**Touched files:** -- `includes/AbstractRegistry.php` (new file) -- `includes/Block/Registry.php` (extends new abstract class) -- `includes/Meta/Registry.php` (extends new abstract class) -- `includes/Editor/Registry.php` (extends new abstract class) - -**Acceptance criteria:** -- [ ] `pnpm lint` (PHP) passes -- [ ] Each registry's public method signatures unchanged -- [ ] `BlockRegistry::get_instance()`, `MetaRegistry::get_instance()`, `EditorRegistry::get_instance()` still return singletons -- [ ] Total PHP LOC reduced by ~115 compared to pre-Batch-5 -- [ ] REST endpoint response structure unchanged - -**Manual verification:** -- [ ] Register a block check via `wp_register_block_validation_check()` — appears in REST response -- [ ] Register a meta check — appears; the 3-level `[post_type][meta_key][check_name]` structure intact -- [ ] Register an editor check — appears -- [ ] Settings addon lists all checks across all three scopes -- [ ] Changing a check's level via settings still filters through `wp_validation_check_level` -- [ ] Check with duplicate `namespace`+`name` logs expected error (confirm `log_error` path still wired via `Logger` trait) -- [ ] Invalid level parameter (e.g., `'critical'`) logs error and defaults to `'error'` - -**Rollback:** This is the most involved batch. Revert the four file changes. Inspect each registry's `register_check()` method to confirm parity with pre-Batch-5 behavior. - ---- - -## Cross-batch verification (post-all-batches) - -After all five batches ship, run through these end-to-end checks: - -- [ ] Fresh WP install; activate core plugin only — no errors -- [ ] Activate integration example — all checks register, validation surfaces in the editor -- [ ] Activate settings addon — table populates, level overrides save and apply -- [ ] Disable core plugin — integration example's `function_exists` guards kick in, no errors -- [ ] Re-enable core plugin — everything reconnects -- [ ] Create a new post of each registered post type — checks fire appropriately per post type -- [ ] Publish a post with errors resolved — save succeeds -- [ ] Attempt to publish with errors — `lockPostSaving` prevents save; `editor.preSavePost` throws if somehow bypassed -- [ ] Change a check's level via settings addon to `'warning'` — UI updates to warning styling; save allowed -- [ ] Change a check's level to `'none'` — check disappears entirely -- [ ] PHP debug log shows no warnings/notices under WP_DEBUG -- [ ] JS console shows no errors or warnings - -## Post-batch polish - -Orthogonal to Gutenberg alignment; tracked separately. - -### Completed - -- [x] **Polish 1** — `@example` JSDoc blocks added to the public API surface (6 store selectors, 5 store actions, `useMetaField`, `useMetaValidation`). Commit `561e32a`. -- [x] **Polish 2** — TypeScript migration started with `src/store/constants.ts`. Exports `State`, `ValidationIssue`, `BlockValidationResult`, `MetaValidationResult`, `ValidationMode`, `IssueType`, and the action-type constants as narrow literal types. Stale `babel.config.json` deleted in the same change so `@wordpress/scripts`' default preset (which includes `@babel/preset-typescript`) takes effect. Commit `a228e5d`. -- [x] **Polish 3+4** — Unit tests via `@wordpress/scripts test-unit-js`. Test infrastructure added (`test` / `test:watch` npm scripts; Jest env override in `.eslintrc.json`). 56 tests across 4 suites covering store reducer, actions, selectors, and `issue-helpers`. All pure-function coverage in ~1s. Commit `f78624e`. - -### Deferred — pick up before the core PR - -- [ ] **Polish 5** — Unit tests for the validation-dispatch functions. Requires `@wordpress/hooks` filter mocking and editor-settings mocking. Targets: - - `validateBlock()` — block-type rule lookup, `editor.validateBlock` filter application, mode derivation - - `validateMetaField()` / `validateAllMetaChecks()` — per-key rule lookup, `editor.validateMeta` filter, required-field fallback - - `validateEditor()` — per-post-type rules, `editor.validateEditor` filter, priority sort -- [ ] **Polish 5b** — Unit tests for custom hooks. Requires `@testing-library/react` plus `@wordpress/data` store mocking. Targets: `useMetaField`, `useMetaValidation`, `useInvalidBlocks`, `useInvalidMeta`, `useInvalidEditorChecks`, `useValidationIssues`, `useDebouncedValidation`, `useValidationSync`, `useValidationLifecycle`. -- [ ] **Polish 6** — Performance benchmarks with 200+/500+/1000+ block posts. Measures `useInvalidBlocks` re-computation time, dispatch churn, and memory of the `blockValidation` store slice. Outputs inform whether to add per-block diffing or lazy validation before the PR. -- [ ] **Polish 7** — Integration / e2e tests via `@wordpress/env` + `@wordpress/e2e-test-utils-playwright`. Full-stack coverage: PHP check registration → editor settings injection → JS validation → store dispatch → save-lock → `editor.preSavePost` gate. - -### Also worth doing before core PR (from [docs/TODO.md](../TODO.md)) - -- [ ] Further TypeScript migration: `src/store/actions.js`, `src/store/selectors.js`, `src/store/reducer.js`, `src/store/index.js`. Constants are typed already; making the consumers typed closes the loop. -- [ ] Add `.d.ts` or inline JSDoc types for check registration args (the object shape accepted by `wp_register_block_validation_check()` on the JS-filter side). -- [ ] **Future considerations** (design discussions, not straight tasks): `block.json` declarative validation, async validation via `applyFiltersAsync`, site-editor support. See [docs/TODO.md](../TODO.md) "Future Considerations" for rationale. - -## Deferred — not in these batches - -Changes that only make sense when cutting the actual core PR are captured in [core-pr-migration.md](core-pr-migration.md). These include: - PSR-4 namespaced classes → `WP_*` flat files in `lib/validation/` -- Text domain `validation-api` → `default` (or `gutenberg` while in plugin) +- Text domain `validation-api` → `gutenberg` / `default` - `@since 1.0.0` → target WP version - REST namespace from `wp-validation/v1` → whatever core accepts - CSS class prefix rename @@ -267,36 +60,6 @@ Changes that only make sense when cutting the actual core PR are captured in [co - `@package ValidationAPI` → `@package gutenberg` / `@package WordPress` - Composer PSR-4 autoload → `require_once` chain -## Sign-off checklist - -Before execution begins: - -- [ ] Plan reviewed and approved -- [ ] Batch order confirmed (recommended vs. alternative) -- [ ] Acceptance criteria understood for each batch -- [ ] Rollback strategy acceptable (per-batch git revert) -- [ ] Manual verification steps understood (no automated tests available) -- [ ] All three plugins accessible for testing (core, settings, integration example) -- [ ] Test WordPress site available - -During execution (per batch): - -- [ ] Pre-batch git status clean -- [ ] Batch checklist in [pass-a.md](pass-a.md) followed item-by-item -- [ ] Acceptance criteria verified -- [ ] Manual verification complete -- [ ] Commit with batch number in message -- [ ] Move to next batch - -After all batches: - -- [ ] Cross-batch verification passed -- [ ] Post-batch polish items scheduled -- [ ] [core-pr-migration.md](core-pr-migration.md) reviewed for completeness - -## Notes +## Sign-off -- This plan assumes no automated test suite. Manual verification is the gate. -- The user owns all consumers of the plugins (core, settings addon, integration example), so no backward-compat concerns apply. -- Estimated total effort: Batch 4 ~1 hour, Batch 3 ~15 min, Batch 5 ~2-3 hours, Batch 2 ~30 min, Batch 1 ~4-6 hours. Total ~8-11 hours of focused work. -- The JS restructure (Batch 1) is the single biggest time investment; most of it is mechanical file moves + import rewriting. +All five batches manually verified in a running WordPress instance before commit. Public API unchanged; external consumers (integration example, settings addon) work without code changes (settings addon required a fetch-path update for Batch 2, included in the same change). `pnpm test` passes (56/56). `pnpm lint:js` + `pnpm lint:php` + `pnpm lint:css` all clean. `pnpm build` produces `build/validation-api.js` + `.css` + `.asset.php`. diff --git a/docs/gutenberg-alignment/pass-a.md b/docs/gutenberg-alignment/pass-a.md deleted file mode 100644 index f63ff17..0000000 --- a/docs/gutenberg-alignment/pass-a.md +++ /dev/null @@ -1,380 +0,0 @@ -# Pass A — Convention & Alignment - -Review of the Validation API plugin against current Gutenberg conventions. This pass covers naming, file organization, export patterns, and style. It does **not** cover architectural decisions (Pass B) or leanness / duplication (Pass C). - -## Status - -- [x] Review complete -- [x] Decisions locked (see below) -- [ ] Consolidated plan reviewed (awaits Pass B + Pass C) -- [ ] Execution - -## Decisions locked during Pass A - -| Decision | Value | -|---|---| -| REST namespace (standalone phase) | `wp-validation/v1/checks` | -| Batch 1 execution style | Single coherent restructure (owner controls all consumers; no need to split) | -| Backwards compatibility | Not required — owner controls all consuming plugins | - -## Reference sources - -- Gutenberg trunk: `wp-content/plugins/gutenberg/` — sampled `packages/editor`, `packages/block-editor`, `packages/core-data`, `packages/notices`, `packages/plugins`, `packages/interface`, and `lib/`, `lib/experimental/`, `lib/compat/` -- See `docs/PROPOSAL.md` for the core-merge pitch this alignment supports - -## Findings summary - -### What already aligns with Gutenberg (do not change) - -| Area | Current state | -|---|---| -| Store name | `core/validation` — matches `core/*` convention | -| JS filter namespace | `editor.validateBlock` / `editor.validateMeta` / `editor.validateEditor` — matches `editor.*` behavioral-filter pattern | -| Global PHP function names | `wp_register_block_validation_check()` etc. — core-style | -| PHP hook prefix | `wp_validation_*` — positioned for core | -| Registry singleton pattern | Static `get_instance()` — matches `WP_Connector_Registry` | -| Editor settings injection | `block_editor_settings_all` filter — canonical | -| Selector naming | `getInvalidBlocks`, `hasErrors`, `hasWarnings` — matches `get*`/`has*`/`is*` convention | -| Action naming | `setInvalidBlocks` (present tense) — matches `editPost`/`savePost` style | -| Filter name strings | Inline (not constants) — matches Gutenberg | -| CSS class prefix `validation-api-*` | Correct for standalone; renamed at core-PR time | - -### Tier 1 — blockers for core merge (deferred; standalone-safe as-is) - -These are addressed in `core-pr-migration.md` (drafted after all three passes complete). - -- PSR-4 namespaced classes → `WP_*` flat classes under `lib/validation/` -- Text domain `validation-api` → `gutenberg` → `default` -- `@since 1.0.0` → target WP version -- CSS class prefix rename -- Drop text-domain argument from JS `__()` calls - -### Tier 2 — significant alignment (addressed below or deferred to Pass B/C) - -- [Batch 1] JS entry point and package shape: `src/script.js` + `registerPlugin` + `src/editor/` + `src/shared/` split + webpack aliases -- [Batch 2] REST namespace `wp/v2/validation-checks` → `wp-validation/v1/checks` -- [Pass B] REST permission callback `manage_options` — audit consumers -- [Pass B] `registerPlugin('core-validation', ...)` — keep for standalone sidebar mount, but verify against architectural review -- [Pass C] Three duplicate `Registry` classes — collapse candidate -- [Pass C] Six lifecycle hooks — audit consumers, prune unused - -### Tier 3 — polish (addressed below) - -- [Batch 1] `package.json` `sideEffects` declaration -- [Batch 1] `getInvalid*.js` hook wrappers renamed to `useInvalid*.js` -- [Batch 3] `Core/I18n.php` collapsed to an enqueue line -- [Future] JSDoc `@example` blocks on public APIs (optional polish, separate task) -- [Future] TypeScript for `store/constants.ts` (optional, defer until after Pass C) - ---- - -## Action plan - -Three independent batches. Order of execution is decided in the consolidated plan after Pass B and Pass C. - ---- - -### Batch 1 — JS source reshape - -**Goal:** Restructure `src/` to match Gutenberg package layout. Pure reorganization. No public API changes. - -**Risk:** Low. Internal file paths change; public API (store name, filter names, global PHP functions) unchanged. Integration example requires rebuild but no code change. - -#### Checklist - -**Directory flattening — file moves:** - -- [ ] `src/editor/store/` → `src/store/` -- [ ] `src/editor/store/constants.js` → `src/store/constants.js` -- [ ] `src/editor/store/actions.js` → `src/store/actions.js` -- [ ] `src/editor/store/selectors.js` → `src/store/selectors.js` -- [ ] `src/editor/store/reducer.js` → `src/store/reducer.js` -- [ ] `src/editor/store/index.js` → `src/store/index.js` -- [ ] `src/editor/components/ValidationSidebar.js` → `src/components/validation-sidebar/index.js` -- [ ] `src/editor/components/ValidationToolbarButton.js` → `src/components/validation-toolbar-button/index.js` -- [ ] `src/editor/components/ValidationIcon.js` → `src/components/validation-icon/index.js` -- [ ] `src/editor/components/ValidationProvider.js` → `src/hooks/use-validation-sync.js` — **convert from renderless component to hook per Pass B finding B-1**. Export `useValidationSync()`; it runs the three `GetInvalid*` hooks and dispatches to the store via `useEffect`. -- [ ] `src/editor/validation/ValidationAPI.js` → `src/hooks/use-validation-lifecycle.js` — **convert from renderless component to hook per Pass B finding B-1**. Export `useValidationLifecycle()`; it `useSelect`s from the store and runs the two `useEffect`s (save-locking + body CSS classes). -- [ ] `src/editor/validation/blocks/validateBlock.js` → `src/utils/validate-block.js` -- [ ] `src/editor/validation/meta/validateMeta.js` → `src/utils/validate-meta.js` -- [ ] `src/editor/validation/editor/validateEditor.js` → `src/utils/validate-editor.js` -- [ ] `src/editor/validation/meta/hooks/useMetaField.js` → `src/utils/use-meta-field.js` -- [ ] `src/editor/validation/meta/hooks/useMetaValidation.js` → `src/utils/use-meta-validation.js` -- [ ] `src/shared/utils/validation/issueHelpers.js` → `src/utils/issue-helpers.js` -- [ ] `src/shared/utils/validation/getValidationConfig.js` → `src/utils/get-validation-config.js` -- [ ] `src/shared/hooks/useDebouncedValidation.js` → `src/utils/use-debounced-validation.js` -- [ ] `src/editor/register.js` → `src/hooks/register-sidebar.js` — calls `registerPlugin( 'core-validation', { render: ValidationPlugin, icon } )`. The `ValidationPlugin` root component (defined in this file or co-located) calls `useValidationSync()` and `useValidationLifecycle()` inside its body, then returns ``. Keeps hooks running even when the sidebar conditionally returns `null`. (Pass B finding B-1.) -- [ ] **New file `src/hooks/pre-save-validation.js`** — Pass B finding B-2. Registers `addFilter( 'editor.preSavePost', 'validation-api/pre-save-gate', async ( edits ) => { if ( select( validationStore ).hasErrors() ) throw new Error( '...' ); return edits; } )` as a save-time safety net layered on top of `lockPostSaving`. -- [ ] **New file `src/utils/use-validation-issues.js`** — Pass C finding C-9. Extracts the duplicated 7-line `useSelect` block (currently in `ValidationSidebar` and post-Pass-B `useValidationLifecycle`) into a single shared hook returning `{ invalidBlocks, invalidMeta, invalidEditorChecks }`. Update both consumers. -- [ ] **Consolidate dual `useSelect` in `use-meta-field.js`** — Pass C finding C-10. Currently calls `useMetaValidation()` (which has its own `useSelect`) plus a second `useSelect` for the meta value. Merge into one `useSelect` that reads both. -- [ ] Remove empty `src/editor/`, `src/shared/`, index barrel files that no longer serve - -**HOC → side-effect hook files:** - -- [ ] `src/editor/hoc/withErrorHandling.js` → `src/hooks/validate-block.js` - - Convert from `export default createHigherOrderComponent(...)` to a module-scope `addFilter('editor.BlockEdit', 'validation-api/error-handling', withErrorHandling)` side effect -- [ ] `src/editor/hoc/withBlockValidationClasses.js` → `src/hooks/block-validation-classes.js` - - Same conversion: HOC defined locally, `addFilter('editor.BlockListBlock', 'validation-api/block-classes', ...)` at module scope - -**New file — `src/hooks/index.js`:** - -```js -// Side-effect imports — each module registers its filter/plugin on import -import './register-sidebar'; -import './validate-block'; -import './block-validation-classes'; -import './pre-save-validation'; -``` - -*Note: `use-validation-sync.js` and `use-validation-lifecycle.js` are custom hooks (not side-effect modules), so they are NOT imported here. They are imported by `register-sidebar.js`'s `ValidationPlugin` component and invoked inside its render.* - -**Getter-style hooks renamed to `use*`:** - -- [ ] `src/shared/utils/validation/getInvalidBlocks.js` → `src/utils/use-invalid-blocks.js` (rename export `getInvalidBlocks` → `useInvalidBlocks`) -- [ ] `src/shared/utils/validation/getInvalidMeta.js` → `src/utils/use-invalid-meta.js` (rename export `getInvalidMeta` → `useInvalidMeta`) -- [ ] `src/shared/utils/validation/getInvalidEditorChecks.js` → `src/utils/use-invalid-editor-checks.js` (rename export `getInvalidEditorChecks` → `useInvalidEditorChecks`) -- [ ] Update all call sites (likely in `ValidationProvider.js`) -- [ ] **Note:** Store selectors keep `getInvalid*` names — only the React-hook wrapper files rename - -**Entry point:** - -- [ ] `src/script.js` → `src/index.js` -- [ ] `src/index.js` body: - -```js -import './store'; // registers core/validation store (side effect) -import './hooks'; // registers filters + sidebar (side effects) -import './styles.scss'; - -// Public exports (for any future consumer importing from build/) -export * from './store'; -export * from './utils'; -export { default as ValidationSidebar } from './components/validation-sidebar'; -export { default as ValidationToolbarButton } from './components/validation-toolbar-button'; -export { default as ValidationIcon } from './components/validation-icon'; -``` - -- [ ] Update `webpack.config.js` entry: `index: path.resolve(__dirname, 'src/index.js')` — keep output filename `validation-api.js` so `Core/Assets.php` enqueue path doesn't change -- [ ] Verify `Core/Assets.php` still finds the built file - -**Webpack aliases removed:** - -- [ ] Delete `resolve.alias` entries for `@`, `@editor`, `@shared` from `webpack.config.js` -- [ ] Convert every `@/...`, `@editor/...`, `@shared/...` import in JS files to relative path -- [ ] Verify no alias references remain (grep `@editor/`, `@shared/`, `from '@/`) - -**Styles:** - -- [ ] `src/styles.scss` and `src/styles/` tree — keep at `src/styles.scss` + `src/styles/` (Gutenberg packages typically co-locate style per component, but plugin-scoped global stylesheet is acceptable) -- [ ] Verify component folder style co-location works: `src/components/validation-sidebar/style.scss` for component-scoped styles, if any exist - -**`package.json`:** - -- [ ] Add `sideEffects` field: - -```json -"sideEffects": [ - "src/index.js", - "src/hooks/**", - "src/store/index.js", - "src/**/*.scss" -] -``` - -- [ ] Update `main` field if present (probably `build/validation-api.js`) — no change needed unless entry filename changes - -**Integration example plugin rebuild:** - -- [ ] After Batch 1 ships, rebuild `validation-api-integration-example` (`npm run build` in its directory) -- [ ] Verify its validation hooks still fire — the public filter names and PHP functions didn't change, so this should just work - -**Settings addon:** - -- [ ] No changes required from Batch 1 alone (settings addon doesn't import from core plugin JS) - -#### Acceptance criteria - -- [ ] `pnpm build` completes cleanly -- [ ] Plugin loads in `wp-admin` with no console errors -- [ ] Opening a post shows the validation sidebar under its usual icon -- [ ] Integration example plugin's checks still fire (block, meta, editor scopes) -- [ ] Settings addon settings page still loads (REST endpoint unchanged in Batch 1) -- [ ] No file under `src/editor/` or `src/shared/` remains -- [ ] No webpack alias imports remain -- [ ] `pnpm lint` passes - ---- - -### Batch 2 — REST namespace move - -**Goal:** Move REST route off `wp/v2` (reserved core namespace) to plugin-owned `wp-validation/v1`. Settings addon updated in the same change. - -**Risk:** Low for owner (all consumers controlled). Any third-party consumer of old endpoint breaks — acceptable. - -**Decision locked:** `wp-validation/v1/checks` - -#### Checklist - -**Core plugin:** - -- [ ] `includes/Rest/ChecksController.php`: change `$this->namespace = 'wp/v2'` → `$this->namespace = 'wp-validation/v1'` -- [ ] `includes/Rest/ChecksController.php`: change `$this->rest_base = 'validation-checks'` → `$this->rest_base = 'checks'` -- [ ] Verify no other file hard-codes the old path -- [ ] Grep for `wp/v2/validation-checks` across codebase — replace with `wp-validation/v1/checks` - -**Settings addon:** - -- [ ] `validation-api-settings/src/settings/App.js`: update the `apiFetch({ path: '/wp/v2/validation-checks' })` call to `/wp-validation/v1/checks` -- [ ] Grep settings addon for old path — replace -- [ ] Rebuild settings addon (`npm run build` in its directory) - -**Documentation:** - -- [ ] Update `docs/PROPOSAL.md` — the "REST API" paragraph currently cites `GET /wp/v2/validation-checks`. Replace with `GET /wp-validation/v1/checks` and add a note: "final namespace in core TBD during PR — candidates: `wp/v2/validation-checks`, `wp-block-editor/v1/validation-checks`" -- [ ] Update `docs/technical/` REST reference if present -- [ ] Update `CLAUDE.md` REST API section in core plugin (`/wp/v2/validation-checks` → `/wp-validation/v1/checks`) -- [ ] Update settings addon README if it documents the REST integration - -#### Acceptance criteria - -- [ ] `curl -u admin:pass http://site/wp-json/wp-validation/v1/checks` returns the expected grouped-by-scope response -- [ ] `curl http://site/wp-json/wp/v2/validation-checks` returns 404 -- [ ] Settings addon page loads, table populates with checks -- [ ] Settings addon save round-trip works (POST → `wp_options` → reload → table reflects saved levels) - ---- - -### Batch 3 — I18n class simplification - -**Goal:** Match Gutenberg's functional style for script translation loading. - -**Risk:** None. Same behavior, one less class. - -#### Checklist - -- [ ] Identify the `wp_set_script_translations()` call inside `includes/Core/I18n.php` -- [ ] Move the call inline into `includes/Core/Assets.php` wherever `wp_register_script()`/`wp_enqueue_script()` is called for the main validation-api editor script -- [ ] Remove `$this->i18n = new I18n(...)` and associated init from `includes/Core/Plugin.php` -- [ ] Delete `includes/Core/I18n.php` -- [ ] Verify Composer autoload works without the file (PSR-4 discovers by convention, no registry update needed) - -#### Acceptance criteria - -- [ ] `pnpm build` and PHP load still succeed -- [ ] Existing `.json` translation file (if any under `languages/`) still loads for the editor script -- [ ] `wp_set_script_translations` is called exactly once for the editor script handle - ---- - -## Pending Pass B (architectural) — RESOLVED - -Pass B completed. Results folded into Batch 1 above, with full rationale in `pass-b.md`. Summary: - -- [x] **REST permission callback `manage_options`** — **KEEP**. Only consumer is the settings admin page; editor JS uses `block_editor_settings_all` injection, not the REST endpoint. `manage_options` is correct for an admin-only config endpoint. -- [x] **`registerPlugin('core-validation', ...)` for sidebar** — **KEEP** during standalone phase. Canonical for third-party plugins; Gutenberg reserves `ComplementaryArea` direct mount for built-in sidebars. Swap deferred to core-PR. -- [x] **`ValidationProvider` / `ValidationAPI` renderless components** — **CONVERT TO HOOKS** (Batch 1 updated above). `useValidationSync` + `useValidationLifecycle` invoked from a single `ValidationPlugin` root component. -- [x] **`EditorDetection` trait** — **KEEP**. No Gutenberg PHP helper exists to replace it. Gutenberg features detect context per-feature (same pattern). -- [x] **`editor.preSavePost` usage** — **ADD** as belt-and-suspenders safety net (Batch 1 now includes new file `src/hooks/pre-save-validation.js`). - -## Pending Pass C (leanness) — RESOLVED - -Pass C completed. New batches 4 and 5 added below. Results summary: - -- [x] **Collapse three `Registry` classes** — Refined: extract `AbstractRegistry` base class (keep scope-specific subclasses due to state-shape differences). See Batch 5 below. ~115 LOC saved. -- [x] **Prune lifecycle actions** — Delete 2 (undocumented, coupled to dead methods): `wp_validation_check_unregistered`, `wp_validation_check_toggled`. Keep 4 + 3 filters (documented public API). -- [x] **`Contracts/CheckProvider` interface** — DELETE (no implementations found workspace-wide). See Batch 4. -- [x] **`Core/Traits/Logger` trait** — KEEP. 28 active call sites; debugging consistency value. -- [x] **`getValidationConfig.js` utility layer** — KEEP. The indirection earns its file. -- [x] **`EditorDetection` trait internals** — Trim 8 LOC dead `get_current_screen()` fallback branch. See Batch 4. -- [x] **Store-subscription consolidation** (Pass B forward) — Add `useValidationIssues()` hook; absorbed into Batch 1. -- [x] **Global editor-settings injection vs per-context** (Pass B forward) — Skip. No perf concern at current scale. - ---- - -## Batch 4 — PHP dead-code deletions (Pass C) - -**Goal:** Delete public API surfaces that have no consumers and no documented contract. - -**Risk:** Very low. All deletions are of unused code; public filter/action names that remain are those with documentation or active consumers. - -#### Checklist - -- [ ] **Delete `includes/Meta/Validator.php`** (109 LOC). No callers in workspace; server-side meta validation is available via `register_post_meta( ..., 'validate_callback' => ... )` directly. - - [ ] Remove any `use` statements or references to `ValidationAPI\Meta\Validator` elsewhere - - [ ] Remove mention from `docs/guide/` and `docs/technical/` if present -- [ ] **Delete `includes/Contracts/CheckProvider.php`** (47 LOC). No implementations in workspace. - - [ ] Remove the `includes/Contracts/` directory if empty afterward - - [ ] Remove mention from `docs/guide/check-providers.md` (or delete that guide entirely if it was the only content) -- [ ] **Delete `Block\Registry::unregister_check()`** (17 LOC) + the `wp_validation_check_unregistered` action it fires -- [ ] **Delete `Block\Registry::set_check_enabled()`** (12 LOC) + the `wp_validation_check_toggled` action it fires -- [ ] **Delete `Editor\Registry::register_editor_check_for_post_types()`** (9 LOC). Bulk convenience helper, never called. -- [ ] **Delete `EditorDetection::get_current_screen()` fallback branch** (lines where it falls through after the `$pagenow` + post-type checks; ~8 LOC). Unreachable in modern WP admin where `$pagenow` is always set. -- [ ] **Documentation cleanup:** - - [ ] Remove `wp_validation_check_unregistered` and `wp_validation_check_toggled` from `docs/technical/hooks.md` (if present — Pass C found them undocumented, but verify) - - [ ] Remove any `docs/guide/` examples using the deleted methods - -#### Acceptance criteria - -- [ ] `pnpm lint` (PHP portion) passes — no unresolved references to deleted classes/methods -- [ ] Plugin activates without fatal errors -- [ ] Settings addon's `wp_validation_check_level` filter still fires (confirm by loading settings page, toggling a level, saving, confirming the level applies) -- [ ] Integration example plugin still registers all its checks -- [ ] REST endpoint `GET /wp-validation/v1/checks` still returns expected structure - ---- - -## Batch 5 — Registry duplication reduction (Pass C) - -**Goal:** Extract shared registration/validation logic from three `Registry` classes into an abstract base class. Internal refactor; no public API change. - -**Risk:** Medium. Touches all three registries. Must verify behavior parity. - -#### Checklist - -- [ ] **Create `includes/AbstractRegistry.php`** with: - - `normalize_args( array $args, string $scope ): array` — merges defaults, validates level, sets `warning_msg` fallback, stamps `_namespace` from `namespace` arg - - `validate_required_args( array $args, array $required ): bool` — required-field check + log_error on failure - - `sort_by_priority( array &$checks ): void` — extracts the `uasort` pattern - - `use` the `Logger` trait -- [ ] **Refactor `Block\Registry` to extend `AbstractRegistry`** - - Replace duplicated defaults/validation block with `parent::normalize_args()` + `parent::validate_required_args()` - - Keep scope-specific methods (`register_check`, `get_registered_block_types`, etc.) - - Confirm hook firing (`wp_validation_check_registered`) still fires with same args -- [ ] **Refactor `Meta\Registry` to extend `AbstractRegistry`** - - Same approach; scope-specific 3-level storage stays - - Hook firing stays scope-specific (`wp_validation_meta_check_registered` if distinct, or same hook — confirm current behavior) -- [ ] **Refactor `Editor\Registry` to extend `AbstractRegistry`** - - Same approach -- [ ] **Confirm `BlockRegistry::get_instance()`, `MetaRegistry::get_instance()`, `EditorRegistry::get_instance()` all still return the same singleton instances** (important — settings addon expects these) - -#### Acceptance criteria - -- [ ] All existing tests pass (once tests exist — currently there are none; at minimum, manual regression) -- [ ] Integration example plugin registers all its checks without error -- [ ] Settings addon lists all registered checks in the table -- [ ] Total PHP LOC reduced by ~115 compared to pre-Batch-5 -- [ ] No change in public PHP API surface (same global function signatures, same hooks fired with same args) -- [ ] No change in REST endpoint response shape - -## Deferred to core-PR (future `core-pr-migration.md`) - -These are documented here as the authoritative list; full migration steps go in a dedicated doc after all three passes complete. - -| Item | Current | Core-PR target | Trigger | -|---|---|---|---| -| PHP class style | `ValidationAPI\Block\Registry` (PSR-4 namespaced) | `WP_Validation_Block_Registry` in `lib/validation/class-wp-validation-block-registry.php` | PR branch cut against `gutenberg/` | -| Class autoload | Composer PSR-4 | WP-style `require_once` chain in `lib/validation/load.php` | PR branch cut | -| Traits | `Core/Traits/Logger`, `Core/Traits/EditorDetection` | Inline or convert to abstract class (core rarely uses traits) | PR branch cut | -| Text domain | `validation-api` | `gutenberg` (while in plugin) → `default` (in core) | PR branch cut | -| JS `__()` text domain arg | `__( 'text', 'validation-api' )` | `__( 'text' )` (no domain) | PR branch cut | -| `@since` tag | `@since 1.0.0` | `@since 6.x.y` (target WP version) | PR branch cut; WP version confirmed | -| CSS class prefix | `validation-api-*` | `wp-validation-*` or as core review decides | PR branch cut | -| REST namespace | `wp-validation/v1/checks` | `wp/v2/validation-checks` OR `wp-block-editor/v1/validation-checks` | PR review feedback | -| Sidebar mount | `registerPlugin('core-validation', ...)` | Direct `ComplementaryArea` / slot registration from within package | PR branch cut | -| `@package` tag in PHP | `@package ValidationAPI` | `@package gutenberg` → `@package WordPress` | PR branch cut | - -## Notes / open questions - -- Recent-PR sampling for tone/commit style was skipped during Pass A. Trunk code is the style reference. If the eventual PR needs tone examples, do a 1-hour sampling pass just before drafting. -- TypeScript migration is optional polish; defer until after Pass C (no point typing code that may get consolidated or deleted). -- Integration example plugin's build needs to be re-run after Batch 1 ships. diff --git a/docs/gutenberg-alignment/pass-b.md b/docs/gutenberg-alignment/pass-b.md deleted file mode 100644 index 89ef08d..0000000 --- a/docs/gutenberg-alignment/pass-b.md +++ /dev/null @@ -1,314 +0,0 @@ -# Pass B — Architectural Review - -Review of the Validation API plugin's architectural choices against modern Gutenberg patterns. This pass examines whether the plugin re-implements primitives Gutenberg already provides, and whether the patterns it chose are still the current best practice. - -## Status - -- [x] Review complete -- [x] Action items folded into Pass A doc (`pass-a.md`) Batch 1 -- [ ] Execution (awaits consolidated plan) - -## Scope of Pass B - -| Area examined | Verdict | -|---|---| -| Renderless components (`ValidationProvider`, `ValidationAPI`) | **Convert to hooks** (B-1) | -| `registerPlugin` for sidebar mount | Keep (standalone); swap at core-PR | -| `PluginSidebar` from `@wordpress/editor` | Keep | -| HOCs via `editor.BlockEdit` + `editor.BlockListBlock` | Keep | -| `lockPostSaving` / autosave / publish-sidebar locking | Keep | -| `editor.preSavePost` as save-time gate | **Add** (B-2) | -| `EditorDetection` trait (PHP) | Keep | -| `block_editor_settings_all` global vs per-context injection | Keep global for now | -| REST permission callback `manage_options` | Keep | -| Store subscription surface | Keep; possible Pass C consolidation | - -## Reference sources - -- Gutenberg packages sampled: `packages/editor`, `packages/block-editor`, `packages/core-data`, `packages/notices`, `packages/plugins`, `packages/interface`, `packages/components`, `packages/dataviews` -- Gutenberg PHP sampled: `lib/`, `lib/experimental/`, `lib/compat/` -- Key reference files (quoted in findings): - - `packages/plugins/src/api/index.ts` — `registerPlugin` implementation - - `packages/editor/src/components/plugin-sidebar/index.js` — `PluginSidebar` as wrapper - - `packages/interface/src/components/complementary-area/index.js` — `ComplementaryArea` - - `packages/editor/src/components/provider/use-upload-save-lock.js` — canonical hook-based save lock pattern - - `packages/editor/src/store/actions.js` — `editor.preSavePost` application, `lockPostSaving` action creators - - `packages/edit-widgets/src/filters/move-to-widget-area.js` — HOC + BlockControls pattern (modern) - - `packages/editor/src/hooks/pattern-overrides.js` — HOC with `createHigherOrderComponent` - -## Findings - -### B-1: Convert renderless components to hooks - -**Current state:** - -- `src/editor/components/ValidationProvider.js` — renderless component. Calls `GetInvalidBlocks()`, `GetInvalidMeta()`, `GetInvalidEditorChecks()`, then dispatches to `core/validation` store via `useEffect`. Returns `null`. -- `src/editor/validation/ValidationAPI.js` — renderless component. `useSelect`s from `core/validation` store, runs two `useEffect`s (save-locking + body CSS classes). Returns `null`. -- Both mounted as siblings in `registerPlugin`'s render prop alongside `ValidationSidebar`. - -**Gutenberg reference:** - -`packages/editor/src/components/provider/use-upload-save-lock.js`: - -```js -export function useUploadSaveLock() { - const isUploading = useSelect( /* ... */, [] ); - const { lockPostSaving, unlockPostSaving, lockPostAutosaving, unlockPostAutosaving } - = useDispatch( editorStore ); - - useEffect( () => { - if ( isUploading ) { - lockPostSaving( LOCK_NAME ); - lockPostAutosaving( LOCK_NAME ); - } else { - unlockPostSaving( LOCK_NAME ); - unlockPostAutosaving( LOCK_NAME ); - } - }, [ isUploading ] ); -} -``` - -This is the direct pattern match for what `ValidationAPI.js` does today. Gutenberg uses a hook, not a renderless component. - -Renderless components still exist in `packages/editor` (`global-keyboard-shortcuts`, `unsaved-changes-warning`, `theme-support-check`) but newer side-effect code uses hooks. - -**Proposed structure:** - -```js -// src/hooks/use-validation-sync.js -export function useValidationSync() { - const invalidBlocks = useInvalidBlocks(); - const invalidMeta = useInvalidMeta(); - const invalidEditorChecks = useInvalidEditorChecks(); - const { setInvalidBlocks, setInvalidMeta, setInvalidEditorChecks } - = useDispatch( validationStore ); - - useEffect( () => { setInvalidBlocks( invalidBlocks ); }, [ invalidBlocks, setInvalidBlocks ] ); - useEffect( () => { setInvalidMeta( invalidMeta ); }, [ invalidMeta, setInvalidMeta ] ); - useEffect( () => { setInvalidEditorChecks( invalidEditorChecks ); }, [ invalidEditorChecks, setInvalidEditorChecks ] ); -} -``` - -```js -// src/hooks/use-validation-lifecycle.js -export function useValidationLifecycle() { - const editorContext = getEditorContext(); - const isValidContext = editorContext === 'post-editor' || editorContext === 'post-editor-template'; - const { invalidBlocks, invalidMeta, invalidEditorChecks } = useSelect( /* ... */ ); - const { lockPostSaving, unlockPostSaving, /* ... */ } = useDispatch( editorStore ); - - useEffect( () => { /* save-locking logic */ }, [ /* deps */ ] ); - useEffect( () => { /* body CSS class logic */ }, [ /* deps */ ] ); -} -``` - -```js -// src/hooks/register-sidebar.js -import { registerPlugin } from '@wordpress/plugins'; -import { useValidationSync } from './use-validation-sync'; -import { useValidationLifecycle } from './use-validation-lifecycle'; -import ValidationSidebar from '../components/validation-sidebar'; - -function ValidationPlugin() { - useValidationSync(); - useValidationLifecycle(); - return ; -} - -registerPlugin( 'core-validation', { render: ValidationPlugin } ); -``` - -**Why this works:** - -- `ValidationSidebar` already returns `null` when no issues exist. Moving the hooks to `ValidationPlugin` (always rendered) keeps them running regardless of sidebar visibility. -- One root mount point instead of three siblings. -- Each hook is independently testable (no wrapper component needed in tests). -- Matches Gutenberg's `use-upload-save-lock.js` pattern exactly. - -**Why not:** - -- Behavior is identical either way; this is stylistic alignment, not a bug fix. - -**Integration with Batch 1:** Already folded into the Batch 1 checklist in `pass-a.md`. File moves updated to `src/hooks/use-validation-sync.js` and `src/hooks/use-validation-lifecycle.js`. - ---- - -### B-2: Add `editor.preSavePost` as save-time gate - -**Current state:** Plugin does not use `editor.preSavePost`. Save-blocking is entirely `lockPostSaving`-based in `ValidationAPI.js`. - -**Gutenberg reference:** `packages/editor/src/store/actions.js` applies the filter during save: - -```js -try { - edits = await applyFiltersAsync( - 'editor.preSavePost', - edits, - options - ); -} catch ( err ) { - error = err; -} -``` - -The filter is stable since WP 6.7 and is async. Throwing aborts the save. - -**How `lockPostSaving` and `editor.preSavePost` relate:** - -- `lockPostSaving` — guards the `savePost()` action via the `isPostSavingLocked()` selector check. Prevents save attempts from proceeding. -- `editor.preSavePost` — runs inside the save pipeline as an async filter. Can modify `edits` or throw to abort. - -They are complementary: -- Lock is the primary mechanism for a reactive, always-on save-gate. -- `preSavePost` is a per-save interception point, useful for: - - Race-condition safety (lock not yet applied when save fires) - - Async final validation (e.g., server-side check) - - Edge paths that don't go through `savePost`-action-as-normal - -**Proposed addition:** - -New file `src/hooks/pre-save-validation.js`: - -```js -import { addFilter } from '@wordpress/hooks'; -import { select } from '@wordpress/data'; -import { __ } from '@wordpress/i18n'; -import { store as validationStore } from '../store'; - -addFilter( - 'editor.preSavePost', - 'validation-api/pre-save-gate', - async ( edits ) => { - if ( select( validationStore ).hasErrors() ) { - throw new Error( - __( 'Validation errors must be resolved before saving.', 'validation-api' ) - ); - } - return edits; - } -); -``` - -**Why:** - -- Cheap safety net. If the reactive lock is correct, this filter never fires against real errors. -- Matches the designed-in use of `editor.preSavePost`. -- Forward-compatible: if Gutenberg adds non-savePost save paths or plugins dispatch save directly, this catches them. - -**Why not:** - -- Redundant in the happy path. Skippable if minimal-surface-area is preferred. - -**Verdict:** Recommend adding. Low cost, low risk, semantically correct. - -**Integration with Batch 1:** Added as a new line item in `pass-a.md` Batch 1 checklist. File path: `src/hooks/pre-save-validation.js`. Imported from `src/hooks/index.js`. - ---- - -## Confirmed aligned — do not change - -### `registerPlugin('core-validation', ...)` for sidebar mount - -`packages/plugins/src/api/index.ts` confirms `registerPlugin` is designed for third-party plugin authors. Gutenberg's own built-in sidebars (Document, Block) mount `ComplementaryArea` directly in the editor layout — they do NOT use `registerPlugin`. For a third-party plugin, `registerPlugin` is correct. - -**Core-PR migration:** At merge time, the sidebar becomes a direct `` inside the editor's render tree, as Gutenberg does internally. Documented in `core-pr-migration.md` (to be written). - -### HOC pattern via `editor.BlockEdit` and `editor.BlockListBlock` - -Gutenberg's own code actively uses `createHigherOrderComponent` for these filters. Examples: - -- `packages/edit-widgets/src/filters/move-to-widget-area.js` — HOC on `editor.BlockEdit` adding `` fill -- `packages/editor/src/hooks/pattern-overrides.js` — HOC on `editor.BlockEdit` adding conditional controls -- `packages/editor/src/hooks/custom-sources-backwards-compatibility.js` — HOC on `editor.BlockEdit` for attribute shimming - -Plugin's `withErrorHandling` already matches this pattern exactly (HOC + `` fill). `withBlockValidationClasses` matches the `wrapperProps.className` injection pattern Gutenberg uses for `editor.BlockListBlock`. No change. - -### `lockPostSaving` / `lockPostAutosaving` / `disablePublishSidebar` - -Plugin uses all three with a single named lock (`'core/validation'`), reactive via `useEffect`. This matches `packages/editor/src/components/provider/use-upload-save-lock.js` precisely. No change. - -### `EditorDetection` trait (PHP) - -Gutenberg does **not** expose an `is_post_editor()` / `is_site_editor()` PHP helper. Features detect context per-feature using: - -- `$pagenow` -- `get_current_screen()` -- Request parameters (`post`, `post_type`, `postType`) - -The trait's approach is the same. No core helper to adopt. Keep as-is. - -**Note:** Pass C may find opportunities to simplify the trait's internals, but its *role* is correct. - -### `block_editor_settings_all` global injection - -Plugin injects all registered checks (across all post types) into editor settings under `validationApi.*` keys. JS filters to current post type client-side. - -**Gutenberg supports per-context injection** via `WP_Block_Editor_Context` and the REST endpoint context parameter. But for a plugin with a small number of checks per post type, global injection is fine. Revisit only if editor settings payload becomes large (e.g., hundreds of checks). - -Not changing now. Flagged for Pass C performance consideration. - -### REST permission callback `manage_options` - -Endpoint: `GET /wp-validation/v1/checks` (post Batch 2). - -**Consumer audit result:** The only consumer is `validation-api-settings/src/settings/App.js`. Editor JS receives config via `block_editor_settings_all` injection, not REST. Integration example plugin does not fetch the endpoint. - -**Gutenberg pattern:** -- `edit_posts` — editor-facing read config (most of block editor settings controller) -- `manage_options` — admin settings (guidelines, etc.) - -Since the sole consumer is the admin settings page (which already requires `manage_options`), the existing capability is correct. If a future feature needs editor-JS consumption, revisit. - -### Store subscription pattern - -`useSelect` with `[]` deps in `ValidationAPI.js` and `ValidationSidebar.js` is **correct usage** — the deps gate the `mapSelect` identity, not the selectors themselves. Store reactivity is independent. Not a bug. - ---- - -## Action items (added to Pass A Batch 1) - -The Pass B changes are *not* separate batches. They fold into Pass A's Batch 1 because they touch the same files Batch 1 is already moving/renaming. Doing them together avoids touching the same files twice. - -### Checklist additions to `pass-a.md` Batch 1: - -- [x] Rename `ValidationProvider.js` target from `src/validation-provider.js` (renderless) → `src/hooks/use-validation-sync.js` (hook) -- [x] Rename `ValidationAPI.js` target from `src/hooks/validation-lifecycle.js` (renderless) → `src/hooks/use-validation-lifecycle.js` (hook) -- [x] Update `register-sidebar.js` to define `ValidationPlugin` root component that calls both hooks, pass it as `registerPlugin`'s `render` -- [x] Add new file line: `src/hooks/pre-save-validation.js` with `addFilter('editor.preSavePost', ...)` side effect -- [x] Update `src/hooks/index.js` to import `pre-save-validation`; document that `use-validation-*` hooks are NOT imported here - -All edits already applied to `pass-a.md`. - ---- - -## Items forwarded to Pass C - -### Potential store-subscription consolidation - -`ValidationSidebar.js` and `useValidationLifecycle` both `useSelect` the same three selectors (`getInvalidBlocks`, `getInvalidMeta`, `getInvalidEditorChecks`). Each needs the values independently, so this is not a bug. But a shared `useValidationIssues()` hook could consolidate them. - -**Action:** Pass C evaluates whether consolidation is worthwhile or premature abstraction. - -### `EditorDetection` trait internals - -The trait uses `$pagenow`, site-editor post-type checks, and `get_current_screen()` fallbacks. Several branches may be dead code or redundant. Pass C leanness review. - -### `getValidationConfig` utility layer - -Wraps access to `select('core/editor').getEditorSettings().validationApi`. Whether this indirection earns its file is a Pass C call. - -### Global editor settings injection - -All checks for all post types always injected. If settings payload becomes a perf concern, use `WP_Block_Editor_Context` to filter per-request. Not a current issue; flagged for Pass C perf review. - ---- - -## Nothing new deferred to core-PR from Pass B - -All deferred items (registerPlugin → ComplementaryArea direct mount, etc.) were already captured in Pass A's deferred table. No additions from this pass. - -## Notes - -- The plugin's architecture is in better shape than a typical 4,000-LOC codebase would suggest. Most concerns raised during planning (renderless vs. hooks being the only notable one) turn out to be trend shifts rather than mistakes. -- `editor.preSavePost` is the one genuinely *missing* integration. Adding it is cheap and aligned. -- The integration example plugin's validation logic is not architecturally concerning — it uses the documented filter names and PHP registration functions. No changes needed there as a result of Pass B. diff --git a/docs/gutenberg-alignment/pass-c.md b/docs/gutenberg-alignment/pass-c.md deleted file mode 100644 index eb16098..0000000 --- a/docs/gutenberg-alignment/pass-c.md +++ /dev/null @@ -1,368 +0,0 @@ -# Pass C — Leanness Review - -Review of the Validation API plugin for deletion and collapse candidates. Pass A (conventions) and Pass B (architecture) are done. Pass C asks: what can go away without changing behavior, and what can collapse with equivalent behavior? - -## Status - -- [x] Review complete -- [x] Action items folded into `pass-a.md` (Batches 1, 4, 5) -- [ ] Execution (awaits consolidated plan) - -## Scope of Pass C - -| Area examined | Verdict | -|---|---| -| `Meta\Validator` class | **DELETE** (C-1) | -| `Contracts/CheckProvider` interface | **DELETE** (C-2) | -| Dead `Block\Registry` methods (`unregister_check`, `set_check_enabled`) | **DELETE** (C-3) | -| Undocumented lifecycle hooks tied to deleted methods | **DELETE** (C-4) | -| `Editor\Registry::register_editor_check_for_post_types()` | **DELETE** (C-5) | -| `EditorDetection::get_current_screen()` fallback | **DELETE** (C-6) | -| `Core/I18n.php` | **DELETE + inline** (C-7, already in Pass A Batch 3) | -| Registry duplication | **Extract abstract base class** (C-8, new Batch 5) | -| `useValidationIssues()` hook extraction | **ADD** (C-9, absorbed into Batch 1) | -| `useMetaField` dual `useSelect` consolidation | **CONSOLIDATE** (C-10, absorbed into Batch 1) | -| `filterIssuesByType` helper | KEEP (C-11, too marginal to inline) | -| `getValidationConfig` wrapper layer | KEEP (C-12, earns its file) | -| `Logger` trait | KEEP (28 active call sites) | -| `useDebouncedValidation` hook | KEEP (custom "immediate + debounce" not in `@wordpress/compose`) | -| Plugin init chain | KEEP (no no-op steps) | -| Styles | KEEP (no orphaned stylesheets) | - -## Reference sources - -- Full code audit of `/Users/troychaplin/Develop/wp-projects/validation-api/wp-content/plugins/validation-api/includes/` and `src/` -- Workspace-wide grep for consumers of each candidate (all three plugins: core, settings addon, integration example) -- Documentation audit of `docs/guide/`, `docs/technical/` for public-API contracts - ---- - -## Findings - -### C-1: Delete `Meta\Validator` class (109 LOC) - -**File:** `includes/Meta/Validator.php` - -**What it provides:** One static method `Validator::required()` returning a closure usable as `register_post_meta( ..., 'validate_callback' => Validator::required(...) )`. - -**Consumer audit:** - -``` -grep -r "Meta\\Validator\|Meta::Validator" wp-content/plugins/ → 0 external hits -grep -r "Validator::required" wp-content/plugins/ → 0 hits outside Validator.php -``` - -- Not used by core plugin -- Not used by integration example -- Not used by settings addon -- Not referenced in `docs/guide/` - -**Why to delete:** WordPress's native `register_post_meta( ..., 'validate_callback' )` pattern does the same thing with no helper needed. The 109 LOC solves a non-existent problem. - -**Action:** Delete the file. Remove any stale doc references. - -**Risk:** Very low. No consumers. - ---- - -### C-2: Delete `Contracts/CheckProvider` interface (47 LOC) - -**File:** `includes/Contracts/CheckProvider.php` - -``` -grep -r "implements CheckProvider" → 0 hits -grep -r "Contracts\\CheckProvider" → only in docs/guide/check-providers.md -``` - -**Why to delete:** Added speculatively for class-based registration. No one adopted it. Can be reintroduced in v1.1 if demand appears. - -**Action:** Delete the file; delete (or update) `docs/guide/check-providers.md`; remove the `includes/Contracts/` directory if it becomes empty. - -**Risk:** Very low. - ---- - -### C-3 & C-4: Delete dead `Block\Registry` methods and their orphaned hooks - -**Methods to delete:** -- `Block\Registry::unregister_check()` — 17 LOC, never called -- `Block\Registry::set_check_enabled()` — 12 LOC, never called - -**Actions fired only by these methods:** -- `wp_validation_check_unregistered` — no doc entry, no consumer -- `wp_validation_check_toggled` — no doc entry, no consumer - -**Consumer audit:** - -``` -grep -r "unregister_check\|set_check_enabled" → 0 hits outside Block/Registry.php -grep -r "wp_validation_check_unregistered\|wp_validation_check_toggled" → only in Block/Registry.php definition -``` - -**Lifecycle actions that stay** (documented in `docs/technical/hooks.md`, part of declared public API): -- `wp_validation_initialized` -- `wp_validation_ready` -- `wp_validation_editor_checks_ready` -- `wp_validation_check_registered` - -**Filter hooks that stay:** -- `wp_validation_check_args` (documented) -- `wp_validation_should_register_check` (documented) -- `wp_validation_check_level` (actively consumed by settings addon) - -**Action:** Delete both methods + both actions. Keep all other lifecycle hooks. - -**Risk:** Very low. - ---- - -### C-5: Delete `Editor\Registry::register_editor_check_for_post_types()` (9 LOC) - -**Consumer audit:** - -``` -grep -r "register_editor_check_for_post_types" → 0 hits outside Editor/Registry.php -``` - -Bulk-convenience helper that loops `register_editor_check()` over an array of post types. Never called. Users who need the pattern can write a `foreach` in three lines. - -**Action:** Delete the method. - -**Risk:** Very low. - ---- - -### C-6: Delete `EditorDetection::get_current_screen()` fallback (8 LOC) - -**File:** `includes/Core/Traits/EditorDetection.php` - -Inside `get_editor_context()`, after the `$pagenow === 'post.php' || 'post-new.php'` branch and its post-type resolution, there's a fallback: - -```php -if ( function_exists( 'get_current_screen' ) ) { - $current_screen = \get_current_screen(); - if ( $current_screen && isset( $current_screen->post_type ) ) { - if ( ! in_array( $current_screen->post_type, $this->get_site_editor_post_types(), true ) ) { - return 'post-editor'; - } - } -} -``` - -**Why it's unreachable:** `$GLOBALS['pagenow']` is set by WP for every admin page. If `$pagenow` is neither `post.php` nor `post-new.php`, we're not in the post editor. The `get_current_screen()` fallback adds nothing. - -**Pass B context:** Pass B confirmed the trait's overall role is correct (no Gutenberg helper replaces it). Pass C trims the internals. - -**Action:** Delete the fallback block. - -**Risk:** Very low. Even in the edge case where `$pagenow` isn't set (e.g., custom CLI contexts), the function returning `'none'` is the safe default. - ---- - -### C-7: `Core/I18n.php` — delete and inline (58 LOC) - -**Already planned:** Pass A Batch 3 calls for this. Pass C confirms it's correct. - -**File summary:** 58 LOC class with a constructor storing two values and one method calling `wp_set_script_translations()`. - -**Action:** Delete class. Inline the one `wp_set_script_translations()` call in `Core/Assets.php` at the enqueue site. - -**Risk:** None. - ---- - -### C-8: Extract `AbstractValidationRegistry` base class (~115 LOC saved) - -**Pass A context:** Pass A recommended "collapse three registries into one parameterized class." Pass C refines this based on concrete audit of the three files. - -**Current LOC:** -- `Block\Registry` — 300 LOC -- `Meta\Registry` — 240 LOC -- `Editor\Registry` — 244 LOC -- **Total: 784 LOC** - -**Duplicated code across all three (identical or near-identical):** - -1. Defaults + `wp_parse_args` (~10 LOC × 3): - ```php - $defaults = array( - 'error_msg' => '', - 'warning_msg' => '', - 'level' => 'error', - 'priority' => 10, - 'enabled' => true, - 'description' => '', - 'configurable' => true, - ); - $check_args = \wp_parse_args( $check_args, $defaults ); - ``` - -2. Required-field validation (~8 LOC × 3) - -3. Level validation (~8 LOC × 3): - ```php - $valid_levels = array( 'error', 'warning', 'none' ); - if ( ! in_array( $check_args['level'], $valid_levels, true ) ) { ... } - ``` - -4. `warning_msg` fallback to `error_msg` (~3 LOC × 3) - -5. Namespace stamping (`_namespace` internal key) (~5 LOC × 3) - -6. Priority sort via `uasort` (~3 LOC × 3) - -**Why full collapse is wrong:** - -- `Meta\Registry` has 3-level storage (`[post_type][meta_key][check_name]`) vs 2-level for Block/Editor -- Scope-specific methods (`get_registered_block_types` on Block only) -- Different hook-name suffixes per scope - -**Right approach — abstract base class:** - -``` -ValidationAPI\AbstractRegistry (new, ~100 LOC) -├── normalize_args( $args ): array -├── validate_required_args( $args, $required ): bool -├── stamp_namespace( $args ): array -├── sort_by_priority( &$checks ): void -└── uses Logger trait - -Block\Registry extends AbstractRegistry (~200 LOC) -Meta\Registry extends AbstractRegistry (~180 LOC) -Editor\Registry extends AbstractRegistry (~190 LOC) -``` - -**After extraction:** -- Total LOC: ~670 (from 784) -- Saved: ~115 LOC -- Public API unchanged -- Singleton pattern preserved - -**Action:** New Batch 5. Checklist in `pass-a.md`. - -**Risk:** Medium. Requires careful behavior parity verification across all three scopes. - ---- - -### C-9: Extract `useValidationIssues()` hook (saves ~10 LOC) - -**Duplicated block** in two files (14 LOC total): - -```js -// ValidationAPI.js and ValidationSidebar.js, identical: -const { invalidBlocks, invalidMeta, invalidEditorChecks } = useSelect( ( select ) => { - const store = select( STORE_NAME ); - return { - invalidBlocks: store.getInvalidBlocks(), - invalidMeta: store.getInvalidMeta(), - invalidEditorChecks: store.getInvalidEditorChecks(), - }; -}, [] ); -``` - -**Consolidation:** - -```js -// src/utils/use-validation-issues.js -export function useValidationIssues() { - return useSelect( ( select ) => { - const store = select( validationStore ); - return { - invalidBlocks: store.getInvalidBlocks(), - invalidMeta: store.getInvalidMeta(), - invalidEditorChecks: store.getInvalidEditorChecks(), - }; - }, [] ); -} -``` - -Both call sites become: - -```js -const { invalidBlocks, invalidMeta, invalidEditorChecks } = useValidationIssues(); -``` - -**Action:** Fold into Batch 1 (both consumers are already moving). - -**Risk:** Low. - ---- - -### C-10: Consolidate dual `useSelect` in `useMetaField` (saves ~12 LOC) - -**Current:** `useMetaField` calls `useMetaValidation()` (which has its own `useSelect`) AND does a separate `useSelect` for the meta value. Same component fetches from the editor store twice. - -**Fix:** Single `useSelect` reads both meta value and validation-derived state in one pass. - -**Action:** Fold into Batch 1 (file is already moving to `src/utils/use-meta-field.js`). - -**Risk:** Low. - ---- - -### C-11: `filterIssuesByType` — skip - -**Helper:** `issues => issues.filter( i => i.type === type )` — 3 LOC, called 4 times. - -**Inlining savings:** 3 LOC total. Not worth disrupting the helper's existence. - -**Action:** None. - ---- - -### C-12: `getValidationConfig` wrapper layer — skip - -**Analysis:** Five named exports (`getValidationRules`, `getMetaValidationRules`, `getEditorValidationRules`, `getEditorContext`, `getRegisteredBlockTypes`) each doing one line of editor-settings access. Collapsing saves ~30 LOC but forces call sites to nested property access. - -**Verdict:** Named exports are self-documenting; the 30 LOC is earning its keep. - -**Action:** None. - ---- - -## Items KEPT after Pass C — do not change - -| Item | Reason | -|---|---| -| `Core/Traits/Logger` | 28 active call sites; debug consistency valuable | -| `getValidationConfig.js` | Named exports earn their file | -| `useDebouncedValidation` | Custom "immediate + debounce" behavior not in `@wordpress/compose` | -| HOC files (`withErrorHandling`, `withBlockValidationClasses`) | Already minimal | -| Store selectors/actions | All 11 exports used | -| Reducer | All action types have corresponding creators | -| Plugin initialization chain | No no-op steps | -| Styles | All stylesheets correspond to live components | -| `Meta\hooks\useMetaField` / `useMetaValidation` | Actively consumed by integration example | -| Lifecycle hooks (the 4 kept + 3 filters) | Documented public API | - ---- - -## Summary table - -| Item | Current LOC | Action | LOC saved | Risk | Batch | -|---|---|---|---|---|---| -| `Meta\Validator` class | 109 | DELETE | 109 | Very low | 4 | -| `Core/I18n.php` | 58 | DELETE + inline | 58 | Very low | 3 (Pass A) | -| `Contracts/CheckProvider` interface | 47 | DELETE | 47 | Very low | 4 | -| `Block\Registry::unregister_check()` | 17 | DELETE | 17 | Very low | 4 | -| `Block\Registry::set_check_enabled()` | 12 | DELETE | 12 | Very low | 4 | -| `Editor\Registry::register_editor_check_for_post_types()` | 9 | DELETE | 9 | Very low | 4 | -| `EditorDetection` `get_current_screen()` fallback | 8 | DELETE | 8 | Very low | 4 | -| `wp_validation_check_unregistered` action | coupled | DELETE | 0 extra | Very low | 4 | -| `wp_validation_check_toggled` action | coupled | DELETE | 0 extra | Very low | 4 | -| Registry shared logic (AbstractRegistry extraction) | ~60 duplicated × 3 | EXTRACT base class | ~115 | Medium | 5 | -| `useValidationIssues()` extraction | 14 duplicated | EXTRACT hook | ~10 | Low | 1 | -| `useMetaField` dual `useSelect` | 12 | CONSOLIDATE | ~12 | Low | 1 | - -**Totals:** -- PHP deletions (Batch 4): ~260 LOC -- PHP collapse (Batch 5): ~115 LOC -- JS absorbed into Batch 1: ~22 LOC -- I18n (Batch 3): 58 LOC -- **Grand total: ~455 LOC** (~11% of ~4,000 LOC codebase) - -## Notes - -- No dead code found in JS. No TODO/FIXME/XXX comments, no commented-out blocks, no console.log leftovers. JS codebase is already tight. -- No dead code found in PHP either. Deletions are of unused-but-well-written public API surfaces, not neglected code. -- Both the deletions and the registry extraction are reversible — if a consumer later emerges, reintroduction is straightforward. diff --git a/docs/technical/README.md b/docs/technical/README.md index 599b90c..67332f0 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -1,48 +1,47 @@ # Architecture -The Validation API is a two-layer system: PHP registries that collect check definitions, and JavaScript runners that execute validation logic in real-time. This document describes the internal architecture for contributors and core reviewers. +The Validation API is a two-layer system: PHP registries that collect check definitions, and JavaScript hooks that execute validation logic in real time. This document describes the internal architecture for contributors and core reviewers. ## System Overview ``` -┌─────────────────────────────────────────────────────────┐ -│ PHP (Server) │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Block │ │ Meta │ │ Editor │ │ -│ │ Registry │ │ Registry │ │ Registry │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ -│ │ │ │ │ -│ └────────┬────────┴──────────────────┘ │ -│ │ │ -│ block_editor_settings_all filter │ -│ │ │ -│ editorSettings.validationApi │ -└──────────────────┼────────────────────────────────────────┘ - │ -┌──────────────────┼────────────────────────────────────────┐ -│ ▼ JS (Client) │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Block │ │ Meta │ │ Editor │ │ -│ │ Runner │ │ Runner │ │ Runner │ │ -│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ -│ │ │ │ │ -│ └────────┬────────┴──────────────────┘ │ -│ │ │ -│ ┌───────┴────────┐ │ -│ │ Data Store │ │ -│ │ (core/ │ │ -│ │ validation) │ │ -│ └───────┬────────┘ │ -│ │ │ -│ ┌────────────┼────────────┐ │ -│ │ │ │ │ -│ ┌──┴───┐ ┌────┴────┐ ┌───┴──────┐ │ -│ │Block │ │Sidebar │ │Publish │ │ -│ │Indic.│ │Panel │ │Lock │ │ -│ └──────┘ └─────────┘ └──────────┘ │ -└────────────────────────────────────────────────────────────┘ +┌──────────────────────────────────────────────────────────┐ +│ PHP (Server) │ +│ │ +│ AbstractRegistry (shared logic) │ +│ ▲ ▲ ▲ │ +│ │ │ │ │ +│ ┌────┴────┐┌──┴─────┐ ┌─┴──────┐ │ +│ │ Block ││ Meta │ │ Editor │ │ +│ │ Registry││Registry│ │Registry│ │ +│ └─────────┘└────────┘ └────────┘ │ +│ │ │ │ │ +│ └────────┼─────────┘ │ +│ │ │ +│ block_editor_settings_all filter │ +│ │ │ +│ editorSettings.validationApi │ +└────────────────┼──────────────────────────────────────────┘ + │ +┌────────────────┼──────────────────────────────────────────┐ +│ ▼ JS (Client) │ +│ │ +│ useInvalidBlocks useInvalidMeta useInvalidEditorChecks │ +│ │ │ │ │ +│ └────────┬────────┴───────────────────┘ │ +│ │ useValidationSync dispatches │ +│ ▼ │ +│ core/validation store │ +│ │ │ +│ ┌──────────┼────────────┬────────────────┐ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ useValidation Sidebar BlockList preSavePost │ +│ Lifecycle panel block-classes filter gate │ +│ (lockPost- (issue (red/yellow (throws on │ +│ Saving, list, borders) errors at save) │ +│ body CSS) click-nav) │ +└───────────────────────────────────────────────────────────┘ ``` ## PHP Layer @@ -51,88 +50,104 @@ The Validation API is a two-layer system: PHP registries that collect check defi The entry point is `validation_api_init_plugin()`, called on `init`. This bootstraps the `ValidationAPI\Core\Plugin` class, which: -1. Instantiates the three registries (Block, Meta, Editor) as singletons -2. Initializes the `Assets` class for script/style loading -3. Registers the REST API controller -4. Fires `wp_validation_ready` (with Block Registry) and `wp_validation_editor_checks_ready` (with Editor Registry) actions -5. Fires `wp_validation_initialized` when setup is complete +1. Instantiates the `Assets` class and wires `block_editor_settings_all` injection +2. Resolves the three registry singletons +3. Registers the REST API controller on `rest_api_init` +4. Registers `enqueue_block_editor_assets` / `enqueue_block_assets` for script/style loading +5. Fires `wp_validation_ready` (with Block Registry), `wp_validation_editor_checks_ready` (with Editor Registry), and `wp_validation_initialized` (with the Plugin instance) ### Registries -Each registry is a singleton that stores check definitions: +Each registry is a singleton (`::get_instance()`) and extends `ValidationAPI\AbstractRegistry`, which provides shared helpers for every scope: -- **`ValidationAPI\Block\Registry`** — Keyed by `block_type → check_name → config` -- **`ValidationAPI\Meta\Registry`** — Keyed by `post_type → meta_key → check_name → config` -- **`ValidationAPI\Editor\Registry`** — Keyed by `post_type → check_name → config` +- `normalize_args()` — defaults merge, required-field check (`error_msg`), `warning_msg` fallback, level validation, priority coercion +- `stamp_namespace()` — moves the public `namespace` arg to the internal `_namespace` key +- `sort_by_priority()` — `uasort` by the `priority` value +- `apply_level_filter()` — applies `wp_validation_check_level` with a `none` short-circuit -All three follow the same patterns: -- Registration methods that validate input and fire `should_register_*` / `check_args` filters -- Get methods for retrieving checks by type, name, or all at once -- `get_effective_*_level()` methods that apply the `wp_validation_check_level` filter +The concrete registries differ in their storage shape and scope-specific hook names: + +- **`ValidationAPI\Block\Registry`** — `checks[block_type][check_name] = config`. Filters: `wp_validation_check_args`, `wp_validation_should_register_check`. Action: `wp_validation_check_registered`. +- **`ValidationAPI\Meta\Registry`** — `meta_checks[post_type][meta_key][check_name] = config` (3-level). Filters: `wp_validation_meta_check_args`, `wp_validation_should_register_meta_check`. Action: `wp_validation_meta_check_registered`. +- **`ValidationAPI\Editor\Registry`** — `editor_checks[post_type][check_name] = config`. Filters: `wp_validation_editor_check_args`, `wp_validation_should_register_editor_check`. Action: `wp_validation_editor_check_registered`. ### Namespace Field -The `namespace` field in check args tracks which plugin registered each check. All checks with the same `namespace` value are grouped together. This attribution appears in the REST API response and is used by the companion settings package. +The `namespace` field in check args tracks which plugin registered each check. All checks with the same `namespace` value are grouped together. This attribution appears in the REST API response as `_namespace` and is used by the companion settings package for admin grouping. ### Assets The `ValidationAPI\Core\Assets` class handles: - Enqueuing the editor JavaScript bundle via `enqueue_block_editor_assets` +- Calling `wp_set_script_translations()` for the main script handle - Exporting check data via the `block_editor_settings_all` filter to `editorSettings.validationApi` -- Editor context detection (post editor, site editor, template editing) +- Editor context detection (only post editor / post editor with template — site editor is excluded) ### REST API -The `ValidationAPI\Rest\ChecksController` registers `GET /wp-validation/v1/checks`. It requires `manage_options` capability and returns all registered checks across all three scopes, including `_namespace` attribution. +The `ValidationAPI\Rest\ChecksController` registers `GET /wp-validation/v1/checks`. Permission: `manage_options`. Returns all registered checks across all three scopes, including `_namespace` attribution. Response shape: + +```json +{ + "block": { "core/image": { "alt_text": { ... } } }, + "meta": { "post": { "seo_desc": { "required": { ... } } } }, + "editor": { "post": { "heading_hierarchy": { ... } } } +} +``` ### Traits -Two shared traits used by registry classes: +Two shared traits used across registry and asset classes: -- **`Logger`** — Debug logging via `error_log()` when `WP_DEBUG` is enabled -- **`EditorDetection`** — Determines the current editor context for asset loading +- **`Core/Traits/Logger`** — Debug logging via `error_log()` when `WP_DEBUG` is enabled. Methods are `protected` so subclasses (via `AbstractRegistry`) can also log. +- **`Core/Traits/EditorDetection`** — Determines the current editor context for asset loading. Returns one of `'post-editor'`, `'post-editor-template'`, `'site-editor'`, or `'none'`. ## JavaScript Layer -### Validation Runners +### Side-effect modules (`src/hooks/`) -Three runners, one per scope. Each subscribes to relevant store changes and re-runs validation when data changes: +On package load, `src/index.js` imports `src/hooks/index.js`, which imports each side-effect module. Each module registers one filter or plugin at module scope. -- **Block Runner** (`validateBlock.js`) — Watches for block attribute changes. For each block with registered checks, fires the `editor.validateBlock` filter. -- **Meta Runner** (`validateMeta.js`) — Watches for post meta changes. For each meta key with registered checks, fires the `editor.validateMeta` filter. -- **Editor Runner** (`validateEditor.js`) — Watches for block list changes. Fires the `editor.validateEditor` filter with the full blocks array. +- **`register-sidebar.js`** — `registerPlugin('core-validation', { render: ValidationPlugin })`. `ValidationPlugin` is a root component that renders three siblings: ``, ``, ``. The first two are renderless wrappers around the corresponding hooks (see below); the sibling arrangement is deliberate — putting both hooks in the same parent caused a render loop (`core/validation` subscriber re-renders the parent, which re-runs the dispatcher). +- **`validate-block.js`** — `addFilter('editor.BlockEdit', 'validation-api/with-error-handling', withErrorHandling)`. Per-block validation with 300ms debounce, dispatches to the `blockValidation` store slice, renders a `` toolbar button when issues exist. +- **`block-validation-classes.js`** — `addFilter('editor.BlockListBlock', 'validation-api/with-block-validation-classes', withBlockValidationClasses)`. Reads `getBlockValidation(clientId)` from the store and injects CSS classes (`validation-api-block-error`, `validation-api-block-warning`) onto the block's `wrapperProps.className`. +- **`pre-save-validation.js`** — `addFilter('editor.preSavePost', 'validation-api/pre-save-gate', async edits => ...)`. Layered on top of `lockPostSaving`: if `hasErrors()` is true at save time, throws to abort. Belt-and-suspenders against race conditions or direct save dispatches that bypass the reactive lock. -### Data Store +### Hooks (`src/hooks/use-validation-*.js`) -All validation state is centralized in a custom `@wordpress/data` store registered under the `core/validation` namespace. This eliminates duplicate computation and gives all consumers reactive access via selectors. +These are React hooks, not side-effect modules. They are imported by `register-sidebar.js` and called from the sibling wrappers. -**Producers:** +- **`useValidationSync()`** — Calls `useInvalidBlocks`, `useInvalidMeta`, `useInvalidEditorChecks`. Each dispatches its result to the store via three separate `useEffect` calls. Single computation point; all downstream consumers read from the store. +- **`useValidationLifecycle()`** — `useSelect`s the aggregate arrays from the store via `useValidationIssues()`. Two `useEffect` handlers: + - Save-locking: toggles `lockPostSaving` / `unlockPostSaving` / `lockPostAutosaving` / `disablePublishSidebar` based on whether any errors exist + - Body classes: toggles `has-validation-errors` / `has-validation-warnings` on `document.body` -- **ValidationProvider** — Renderless component that calls the three validation hooks and dispatches results into the store. This is the single place where block, meta, and editor validation is computed. -- **withErrorHandling HOC** — Dispatches per-block validation results into the store's `blockValidation` slice for CSS class application. +### Data Store (`core/validation`) -**State shape:** +Centralized `@wordpress/data` store. State shape: ```js { - blocks: [], // Invalid block results from GetInvalidBlocks - meta: [], // Invalid meta results from GetInvalidMeta - editor: [], // Editor check issues from GetInvalidEditorChecks + blocks: [], // Invalid block results from useInvalidBlocks + meta: [], // Invalid meta results from useInvalidMeta + editor: [], // Editor check issues from useInvalidEditorChecks blockValidation: {}, // Per-block results keyed by clientId } ``` -**Selectors:** +**Selectors** (all documented with `@example` in `src/store/selectors.js`): | Selector | Returns | -|----------|---------| +|---|---| | `getInvalidBlocks()` | All invalid block validation results | | `getInvalidMeta()` | All invalid meta validation results | | `getInvalidEditorChecks()` | All editor-level validation issues | | `getBlockValidation(clientId)` | Per-block validation result | -| `hasErrors()` | True if any error exists across all types | -| `hasWarnings()` | True if any warning exists (and no errors) | +| `hasErrors()` | True if any error exists across all scopes | +| `hasWarnings()` | True if warnings exist (and no errors) | + +**Actions** — `setInvalidBlocks`, `setInvalidMeta`, `setInvalidEditorChecks`, `setBlockValidation`, `clearBlockValidation`. All documented with `@example` in `src/store/actions.js`. Consumers can query the store from the browser console: @@ -141,48 +156,47 @@ wp.data.select('core/validation').getInvalidBlocks() wp.data.select('core/validation').hasErrors() ``` -### Coordinator - -The `ValidationAPI` component reads from the data store and manages publish locking: - -- If any check fails at `error` level → `wp.data.dispatch('core/editor').lockPostSaving('core-validation')` -- When all errors resolve → `wp.data.dispatch('core/editor').unlockPostSaving('core-validation')` +### Utility hooks (`src/utils/use-*.js`) -### UI Components +Exposed for external plugins that build custom UI: -- **withErrorHandling** — Higher-order component (via `editor.BlockEdit` filter) that runs per-block validation and renders a toolbar button when issues exist -- **withBlockValidationClasses** — Higher-order component (via `editor.BlockListBlock` filter) that reads per-block validation from the store and applies CSS classes for red/yellow borders -- **ValidationSidebar** — PluginSidebar panel that reads from the store and lists all issues, grouped by severity, with click-to-navigate to the offending block -- **Meta Field Components** — UI elements for displaying meta validation status - -### Registration - -The JS entry point (`register.js`) renders: -- `ValidationProvider` — Computes validation and populates the data store -- `ValidationAPI` — Reads from the store and manages save locking and body classes -- `ValidationSidebar` — Reads from the store and renders the sidebar UI +- **`useMetaField(metaKey, originalHelp)`** — Primary hook for meta-field UI. Returns `{ value, onChange, help, className }` to spread onto a `TextControl`. Handles change dispatch and adds validation-aware classes + help text. +- **`useMetaValidation(metaKey)`** — Lower-level hook for custom meta UIs. Returns the raw validation result object. +- **`useInvalidBlocks / useInvalidMeta / useInvalidEditorChecks`** — Source hooks that compute invalid results on demand. Normally called only by `useValidationSync`; exposed in case a consumer wants the raw compute without store indirection. +- **`useValidationIssues`** — Read-only convenience wrapper around the store's three aggregate selectors in a single `useSelect` call. +- **`useDebouncedValidation`** — Generic immediate-then-debounce hook. Used internally by `validate-block.js`. ## Key Design Properties -### No Storage +### No storage -The core plugin has no `wp_options`, no custom tables, no settings pages. Check definitions live in PHP memory (populated on each request), exported to JS via the `block_editor_settings_all` filter. The `wp_validation_check_level` filter is the extension point for runtime configuration. +The core plugin has no `wp_options`, no custom tables, no settings pages. Check definitions live in PHP memory (populated on each request), exported to JS via the `block_editor_settings_all` filter. The `wp_validation_check_level` filter is the extension point for runtime configuration — the companion settings package hooks into it and reads from its own `wp_options` key. -### Filter-First Architecture +### Filter-first architecture Every significant behavior passes through a filter: + - Check args can be modified before registration (`wp_validation_check_args`) - Checks can be prevented from registering (`wp_validation_should_register_check`) - Severity is overridable at runtime (`wp_validation_check_level`) - Validation results come from JS filters (`editor.validateBlock`, etc.) +- Save-time gating runs via `editor.preSavePost` as a safety net -### Multi-Context Support +### Editor context scoping -The plugin detects the editor context (`editorContext` in the settings data) and works in: -- Post editor (standard editing) -- Post editor in template mode -- Site editor (full site editing) +The plugin loads and runs only in post-editor contexts (standard and template modes). The site editor is intentionally excluded; template-level validation is a separate problem that would need its own design. Detection logic lives in the `EditorDetection` trait (PHP) and is mirrored in the editor settings injection (`editorSettings.validationApi.editorContext`). ### Debouncing -Validation doesn't run on every keystroke. The JS runners debounce validation to avoid performance issues during rapid editing. The debounce timing is managed by the framework — integrating plugins don't need to handle this. +Per-block validation debounces at 300ms (`useDebouncedValidation`). Aggregate validation (via `useValidationSync`) does not debounce — it relies on `useSelect` reactivity, which naturally batches store updates. + +### Save-locking defense in depth + +Two mechanisms layer together: + +1. `lockPostSaving` — reactive, fires whenever validation state changes. This is the primary mechanism and is what disables Publish/Update in the UI. +2. `editor.preSavePost` filter — runs inside the save action as an async filter. Throws if errors exist, aborting the save. Catches edge cases where the lock might not have propagated in time, or where something dispatches `savePost` directly. + +### Singleton registries + +All three registries (`Block`, `Meta`, `Editor`) are singletons accessed via `::get_instance()`. This matches the pattern used by `WP_Block_Type_Registry` and `WP_Connector_Registry` in Gutenberg core. They extend `ValidationAPI\AbstractRegistry`, which consolidates the repeated defaults / validation / filter plumbing. diff --git a/docs/technical/data-flow.md b/docs/technical/data-flow.md index c1636d2..9ffc88d 100644 --- a/docs/technical/data-flow.md +++ b/docs/technical/data-flow.md @@ -4,7 +4,7 @@ This document traces how a registered check moves from PHP registration through ## Registration Phase (PHP, on `init`) -### 1. External Plugin Registers Checks +### 1. External plugin registers checks ```php add_action( 'init', function() { @@ -17,55 +17,76 @@ add_action( 'init', function() { } ); ``` -### 2. Namespace Attribution +### 2. Normalization (shared across scopes) -The `namespace` field in the check args identifies which plugin registered the check. All checks sharing the same `namespace` are grouped together in the REST API and companion settings. +The global function dispatches to `BlockRegistry::get_instance()->register_check()`. The concrete registry calls `AbstractRegistry::normalize_args()`, which: -### 3. Registry Storage +- Merges defaults into the args (`priority: 10`, `enabled: true`, `configurable: true`, etc.) +- Requires `error_msg` (logs + returns `false` if missing) +- Falls back `warning_msg` to `error_msg` +- Validates `level` against `['error', 'warning', 'none']` +- Coerces non-numeric `priority` to `10` -The Block Registry stores the check definition: +### 3. Pre-registration filters + +Two filters fire before storage (scope-specific names; block scope shown): + +- `wp_validation_check_args` — allows modifying the check config +- `wp_validation_should_register_check` — allows preventing registration + +### 4. Namespace attribution + +`AbstractRegistry::stamp_namespace()` moves the public `namespace` arg to the internal `_namespace` key. All checks sharing the same `_namespace` are grouped together in the REST API response and in the companion settings UI. + +### 5. Registry storage + +The check lands in the registry's internal array: ```php +// Block Registry $this->checks['core/image']['alt_text'] = [ 'error_msg' => 'Missing alt text.', - 'warning_msg' => 'Missing alt text.', // defaults to error_msg + 'warning_msg' => 'Missing alt text.', 'level' => 'error', 'priority' => 10, 'enabled' => true, 'description' => '', + 'configurable'=> true, '_namespace' => 'my-rules', ]; ``` -Before storage, two filters fire: -- `wp_validation_check_args` — allows modifying the check config -- `wp_validation_should_register_check` — allows preventing registration +The registry calls `sort_by_priority()` to keep the entries in ascending priority order. -After storage, the `wp_validation_check_registered` action fires. +### 6. Post-registration action -### 4. Effective Level Resolution +After storage, the scope-specific action fires (e.g. `wp_validation_check_registered` for blocks). External plugins can hook it if they need to know when checks land. -When the Assets class exports data, each check's level is resolved through: +## Export Phase (PHP → JS) + +### 7. Effective-level resolution + +When `Assets::inject_editor_settings()` runs on `block_editor_settings_all`, each check's registered level passes through `AbstractRegistry::apply_level_filter()`: ```php $effective_level = apply_filters( 'wp_validation_check_level', $registered_level, [ - 'scope' => 'block', + 'scope' => 'block', // or 'meta' / 'editor' 'block_type' => 'core/image', 'check_name' => 'alt_text', ] ); ``` -If the companion settings package (or any filter) overrides the level, the exported data reflects the override. +Level `'none'` short-circuits — the filter does not fire and the check is skipped entirely in the export. -## Export Phase (PHP → JS) +If the companion settings package (or any filter) overrides the level, the exported data reflects the override. -### 5. Editor Settings via block_editor_settings_all +### 8. Editor settings via `block_editor_settings_all` -On `block_editor_settings_all`, the Assets class exports all registry data to editor settings, accessible via `select('core/editor').getEditorSettings().validationApi`: +The Assets class exports all registry data to editor settings, accessible via `select('core/editor').getEditorSettings().validationApi`: ```javascript // editorSettings.validationApi @@ -74,128 +95,154 @@ On `block_editor_settings_all`, the Assets class exports all registry data to ed validationRules: { 'core/image': { - 'alt_text': { - errorMsg: 'Missing alt text.', - warningMsg: 'Missing alt text.', - level: 'error', // effective level after filters - priority: 10, - enabled: true, + alt_text: { + error_msg: 'Missing alt text.', + warning_msg: 'Missing alt text.', + level: 'error', // effective level after filters + priority: 10, + enabled: true, description: '', } } }, - metaValidationRules: { /* post_type → meta_key → check_name → config */ }, + metaValidationRules: { /* post_type → meta_key → check_name → config */ }, editorValidationRules: { /* post_type → check_name → config */ }, - registeredBlockTypes: [ 'core/image' ], + registeredBlockTypes: [ 'core/image' ], } ``` -This is a one-time export. The JS layer reads this data and uses it for the entire editing session. +This is a one-time export on editor load. The JS layer reads this data and uses it for the entire editing session. ## Validation Phase (JS, in the editor) -### 6. Runner Initialization - -When the editor loads, each runner reads its rules from the editor settings: - -- Block Runner reads `validationRules` -- Meta Runner reads `metaValidationRules` -- Editor Runner reads `editorValidationRules` - -### 7. Change Detection +### 9. Source hooks compute invalid results -Runners subscribe to `wp.data` store changes: +Three utility hooks each compute their scope's invalid results on demand: -- **Block Runner** — Watches `core/block-editor` for block attribute changes -- **Meta Runner** — Watches `core/editor` for post meta changes -- **Editor Runner** — Watches `core/block-editor` for block list changes +- **`useInvalidBlocks()`** — Subscribes to `core/block-editor`. Recursively walks the block tree (or the `core/post-content` block's inner blocks in template mode), calls `validateBlock()` on each, returns the failed results. +- **`useInvalidMeta()`** — Subscribes to `core/editor` for the current post's meta. Calls `validateAllMetaChecks()` per meta key with registered rules. +- **`useInvalidEditorChecks()`** — Subscribes to `core/block-editor` for the block list and to `core/editor` for post type + title. Calls `validateEditor()` with the post type and blocks. -### 8. Filter Execution - -When a change is detected, the runner iterates over registered checks and fires the appropriate filter: +Each hook internally applies the scope-specific filter (`editor.validateBlock`, `editor.validateMeta`, `editor.validateEditor`) through its `validate-*` utility: ```javascript -// Block Runner (simplified) -for ( const [ checkName, rule ] of Object.entries( checks ) ) { - if ( rule.level === 'none' || ! rule.enabled ) continue; - - const isValid = applyFilters( - 'editor.validateBlock', - true, // default: valid - blockType, // e.g., 'core/image' - attributes, // block's current attributes - checkName, // e.g., 'alt_text' - block // full block object - ); - - // Store result +// utils/validate-block.js (simplified) +const isValid = applyFilters( + 'editor.validateBlock', + true, // default: valid + blockType, + attributes, + checkName, + block +); + +if ( ! isValid ) { + issues.push( createIssue( checkConfig, checkName ) ); } ``` -### 9. Data Store Dispatch +### 10. Sync to the store -The `ValidationProvider` component calls all three validation hooks and dispatches results into the `core/validation` data store: +`useValidationSync()` (called from the `` sibling component under `ValidationPlugin`) calls the three source hooks and dispatches their results into the `core/validation` store via three separate `useEffect` blocks: ```javascript -const invalidBlocks = GetInvalidBlocks(); -dispatch( 'core/validation' ).setInvalidBlocks( invalidBlocks ); -``` +// src/hooks/use-validation-sync.js +const invalidBlocks = useInvalidBlocks(); +const invalidMeta = useInvalidMeta(); +const invalidEditorChecks = useInvalidEditorChecks(); -This is the single place where validation is computed. All downstream consumers read from the store via selectors, eliminating duplicate computation. +const { setInvalidBlocks, setInvalidMeta, setInvalidEditorChecks } + = useDispatch( 'core/validation' ); -Additionally, the `withErrorHandling` HOC dispatches per-block validation results into the store's `blockValidation` slice for CSS class application on individual blocks. +useEffect( () => setInvalidBlocks( invalidBlocks ), [ invalidBlocks ] ); +useEffect( () => setInvalidMeta( invalidMeta ), [ invalidMeta ] ); +useEffect( () => setInvalidEditorChecks( invalidEditorChecks ),[ invalidEditorChecks ] ); +``` + +Separately, the `validate-block.js` side-effect module runs per-block validation in a `withErrorHandling` HOC (wired via the `editor.BlockEdit` filter) and dispatches per-block results to the store's `blockValidation` slice via `setBlockValidation(clientId, result)`. ## UI Phase (JS → DOM) -### 10. Publish Locking +All UI-producing components read from the `core/validation` store — never from the source hooks directly. This eliminates duplicate computation and keeps renders predictable. -The `ValidationAPI` component reads from the store and manages save locking: +### 11. Save locking -```javascript -const hasBlockErrors = select( 'core/validation' ).hasErrors(); +`useValidationLifecycle()` (called from the `` sibling) reads aggregate state via `useValidationIssues()` and toggles save-related locks in a `useEffect`: -if ( hasBlockErrors ) { - dispatch( 'core/editor' ).lockPostSaving( 'core-validation' ); +```javascript +if ( hasBlockErrors || hasMetaErrors || hasEditorErrors ) { + lockPostSaving( 'core/validation' ); + lockPostAutosaving( 'core/validation' ); + disablePublishSidebar(); } else { - dispatch( 'core/editor' ).unlockPostSaving( 'core-validation' ); + unlockPostSaving( 'core/validation' ); + unlockPostAutosaving( 'core/validation' ); + enablePublishSidebar(); } ``` -### 11. Block Indicators +A second `useEffect` in the same hook toggles body classes `has-validation-errors` / `has-validation-warnings` for theme/plugin styling hooks. + +### 12. Save-time safety net (`editor.preSavePost`) + +The `pre-save-validation.js` side-effect module adds a second gate at save time: + +```javascript +addFilter( 'editor.preSavePost', 'validation-api/pre-save-gate', async edits => { + if ( select( 'core/validation' ).hasErrors() ) { + throw new Error( 'Validation errors must be resolved before saving.' ); + } + return edits; +} ); +``` + +If `lockPostSaving` is correctly in effect, this never fires. It's a belt-and-suspenders for race conditions or non-standard save paths. + +### 13. Block indicators -Two HOCs work together for block-level feedback: +Two side-effect modules cooperate for per-block feedback: -- **`withErrorHandling`** (via `editor.BlockEdit` filter) — Runs per-block validation with debouncing, dispatches results to the store, and renders a toolbar button when issues exist -- **`withBlockValidationClasses`** (via `editor.BlockListBlock` filter) — Reads per-block validation from the store via `useSelect` and applies CSS classes: +- **`validate-block.js`** (via `editor.BlockEdit` filter, HOC `withErrorHandling`) — Runs per-block validation with 300ms debouncing, dispatches results to the store, and renders a `` toolbar button when issues exist. +- **`block-validation-classes.js`** (via `editor.BlockListBlock` filter) — Reads per-block validation from the store via `useSelect` and applies CSS classes to `wrapperProps.className`: - `validation-api-block-error` → at least one error-level failure - `validation-api-block-warning` → warning-level failures only (no errors) - No class → all checks pass -### 12. Sidebar Panel +### 14. Sidebar panel -The `ValidationSidebar` reads from the store and renders: -- Issues grouped by severity (errors first, then warnings) -- Each issue shows the message from `errorMsg` or `warningMsg` based on level -- Click-to-navigate: clicking an issue selects and scrolls to the block +`` (rendered as the third sibling in `ValidationPlugin`) reads aggregate state via `useValidationIssues()` and renders: -## Summary: The Full Path +- Grouped by severity (errors first, then warnings) +- Within each group, separated by scope (blocks, meta fields, editor checks) +- Click-to-navigate: clicking a block issue selects and scrolls to the offending block +- Deduplicated: multiple blocks with the same issue show once with an `(x3)` count suffix + +## Summary — the full path ``` PHP registration - → wp_validation_check_args filter - → wp_validation_should_register_check filter - → Registry storage - → wp_validation_check_level filter (on export) + → AbstractRegistry::normalize_args (defaults, level validation, required-field check) + → wp_validation_check_args filter (scope-specific name) + → wp_validation_should_register_check filter (scope-specific name) + → AbstractRegistry::stamp_namespace (`namespace` → `_namespace`) + → Registry storage + sort_by_priority + → wp_validation_check_registered action (scope-specific name) + +PHP → JS export (once, on editor load) + → AbstractRegistry::apply_level_filter (wp_validation_check_level) → block_editor_settings_all → editorSettings.validationApi -JS validation - → wp.data change detection - → editor.validateBlock filter (or .validateMeta / .validateEditor) - → ValidationProvider dispatches to core/validation store - -UI rendering (all read from the store) - → ValidationAPI: lockPostSaving / unlockPostSaving, body classes - → withBlockValidationClasses: block border indicators - → ValidationSidebar: sidebar panel +JS validation (continuous, in the editor) + → useInvalidBlocks / useInvalidMeta / useInvalidEditorChecks subscribe to + core/block-editor and core/editor stores + → editor.validateBlock / .validateMeta / .validateEditor filters fire + → useValidationSync dispatches to core/validation store + → validate-block side-effect dispatches per-block results to blockValidation slice + +UI rendering (all read from core/validation store) + → useValidationLifecycle: lockPostSaving + body CSS classes + → pre-save-validation: editor.preSavePost gate (belt-and-suspenders) + → block-validation-classes: per-block border CSS + → ValidationSidebar: issue list panel ```