Skip to content

G4a: verify core/post-* blocks fully wired against G3 entities - #425

Merged
ViewFromTheBox merged 1 commit into
release/1.0from
chore/400-verify-core-post-blocks-g3
Apr 30, 2026
Merged

ViewFromTheBox merged 1 commit into
release/1.0from
chore/400-verify-core-post-blocks-g3

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Apr 30, 2026 •

Copy link
Copy Markdown
Member

Description

Verification + cleanup pass for G4a (issue #400). Surfaced and closed three real gaps in the G3 entity adapter that prevented core/post-* blocks from actually round-tripping their content in the editor canvas.

Closes: #400

Type of Change

  • Bug fix (fixes an issue)
  • Enhancement (improves existing functionality)
  • Documentation update

Related Issue

Issue: #400

Motivation and Context

Per the issue's acceptance criteria, each core/post-* block needs to render editable content in the editor against cms-framework Post and Page. Verification revealed three blockers in code that had shipped through G0/G3:

  1. The core-data shim's useEntityProp was a no-op stub returning [undefined, noop, undefined]. Every block-library Edit component that reads via useEntityProp (post-title, post-content, post-excerpt, post-author, post-date, post-featured-image, cover) saw nothing.
  2. The block-editor's BlockContextProvider was never wrapped around <BlockList>, so blocks declaring usesContext: ["postId", "postType", "queryId"] had context.postType/context.postId undefined and short-circuited to their placeholder shells.
  3. The persistence loop only saved the block tree via the legacy /content endpoint. Sidebar metadata edits (excerpt, featured_media, author, etc.) accumulated in local React state and were dropped on reload because no save channel sent them anywhere.

A fourth gap surfaced once 1–3 landed: core/post-featured-image and core/cover resolve their featured_media id via getEntityRecord('postType', 'attachment', id), which our shim had no entity registration or endpoint for.

Changes Made

  • useEntityProp (core-data-shim.ts) — replace the no-op with a real read+edit hook. Reads via getEditedEntityRecord (flattened) + getEntityRecord (full shape), returns a setter that dispatches editEntityRecord. Falls back to ambient EntityProvider id when the explicit id arg is omitted.
  • <BlockContextProvider> wraps <BlockList> with a memoized {postType, postId} value when the editor mounts against a cms-framework Post/Page. Skipped for non-entity resources.
  • usePersistence accepts an optional entity: {kind, name, id} identity. Save flow now flushes block tree first via the existing /content PUT, then drains any staged editEntityRecord edits to a new saveEntityRecord API client method that PUTs partial WP-shape data to /visual-editor/api/{resource}/{id}. Sidebar handlers dispatch editEntityRecord alongside their existing setLocalState mirror; a useSelect subscription on the entity edits bag re-arms the metadata-save debounce when block-level setters fire (e.g. core/post-title typing).
  • AttachmentController + WP REST attachment shape — new GET /visual-editor/api/attachments/{id} endpoint emits the WP REST media envelope (source_url, alt_text, media_details) by delegating to apGetMedia(). GutenbergAttachmentAdapter::toWpRestShape() produces the new shape; the existing toGutenberg() (legacy MediaUpload shape) is unchanged. Shim's DEFAULT_ENTITIES adds an attachment postType entity pointing at the new endpoint.
  • docs/block-library-audit.md — flips the six entity-wired post blocks out of the M5-era "Crash or backend-required" deny-list section into a new "Entity-wired" bucket documenting the resolved-attribute contract that the Blade/React/Vue renderer packages consume. Adds the bucket to the bucket-classification preamble.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS 25.4.0
  • Browser: Safari 26.4
  • PHP Version: 8.4.3
  • Project Version: 1.0 (release/1.0 branch)

Tests Performed:

  1. Automated: 463 PHP tests + 970 JS tests (1 skipped), all green.
  2. Manual smoke test in the dev app (/packages/visual-editor/m3/post/1):
    • core/post-title block reads + edits the saved title and persists on reload.
    • Sidebar title field, excerpt, and featured-image picker all round-trip metadata edits to /visual-editor/api/posts/1 and persist on reload.
    • core/post-featured-image block renders the saved image after the new attachments entity resolves.
    • Inspector "Block" tab continues to track the selected block during entity-edit cycles (the memoization fix on BlockContextProvider).
  3. Two PUT requests fire on save when both blocks and metadata changed: PUT /content for the block tree and PUT / for the partial entity edits, in order.

Accessibility Tests Run

  • Keyboard navigation tested
  • Screen reader tested
  • Color contrast verified
  • ARIA labels checked

Details: No new UI surfaces; the inspector panels, block toolbar, and canvas markup are unchanged. Existing accessibility coverage in upstream Gutenberg components applies. The new attachment endpoint is a JSON-only API — no a11y surface.

Tests Added

  • Unit tests added/updated
  • Integration tests added/updated
  • All tests passing

Test details:

  • core-data-shim.test.tsx: 5 new useEntityProp cases covering missing-args early return, edited-prop read with {raw, rendered} flattening, full-shape return, setter dispatching editEntityRecord and surfacing on next read, ambient EntityProvider id fallback, REST resolver path with mocked fetcher, and same-component re-render after the resolver settles. Plus the attachment entity registration assertion.
  • GutenbergAttachmentAdapterTest.php: 3 new cases for toWpRestShape covering core field mapping, sizes reshape into WP {source_url, width, height} shape, and null caption collapse to {rendered: ''}.
  • PostControllerTest.php: 1 new feature test exercising the partial metadata-only PUT path ({excerpt, featured_media}) end-to-end against TestBlockContentModel.

Documentation

  • Inline code documentation added
  • README updated
  • Wiki updated
  • API documentation updated

Documentation details: docs/block-library-audit.md updated to reflect the Entity-wired bucket. Inline PHPDoc + JSDoc on the new methods (useEntityProp, saveEntityRecord API client, AttachmentController::show, toWpRestShape, usePersistence entity identity), and explanatory comments at each non-obvious wiring site (BlockContextProvider memoization rationale, entity-edits subscription, dual-flush ordering).

Companion dev-app changes

To make the manual smoke test possible, the dev-app (Herd/artisanpack-ui) needs:

  • A migration adding slug, excerpt, featured_image_id to visual_editor_posts and the page-only fields to visual_editor_pages.
  • App\Models\Post / App\Models\Page $fillable updates to expose those columns.
  • VisualEditorTestController::resolveFeaturedImage() and the m3-editor.blade.php template passing data-excerpt / data-featured-image / data-author-id so the sidebar's local state hydrates from the DB on reload.

These are tracked separately on the dev-app working tree and not part of this PR.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted
  • Accessibility tests completed
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

Summary by CodeRabbit

  • New Features

    • Added support for persisting post metadata (slug, excerpt, featured image) alongside block content
    • Core post blocks are now enabled by default and render with proper entity context
    • Enhanced editing capabilities for featured image, author, and other post properties
  • Documentation

    • Updated block library audit to clarify entity-context-dependent blocks that are enabled by default

Closes the gaps surfaced by the G4a verification pass:

* core-data shim's `useEntityProp` was a no-op stub. Implement the real
  read+edit hook so `core/post-*` Edit components round-trip the post
  prop through the shim's edits bag instead of always seeing undefined.
* `<BlockContextProvider>` now wraps `<BlockList>` for cms-framework
  Post/Page edits so blocks declaring `usesContext: ["postId",
  "postType", "queryId"]` pick up the active entity from block context.
  The `value` is memoized so editor-app re-renders don't fan a new
  context object out to every block (which clobbers the inspector's
  Block tab selection state mid-edit).
* `usePersistence` accepts an optional core-data entity identity. The
  save loop now flushes any staged `editEntityRecord` edits to the G3
  PUT endpoint after the block save lands so sidebar metadata edits
  (excerpt, featured_media, slug, status, author, categories, tags,
  parent, menu_order, template) and post-title block edits both
  round-trip on the same cycle. Sidebar handlers dispatch
  `editEntityRecord` next to their existing `setLocalState` mirror.
* New `AttachmentController` at `GET /visual-editor/api/attachments/{id}`
  emits the WP REST media envelope (source_url, alt_text, media_details)
  by delegating to `apGetMedia()`. `core/post-featured-image` and
  `core/cover` resolve their `featured_media` id through the shim's new
  `attachment` postType entity. Adapter gains a `toWpRestShape` method
  alongside the existing `toGutenberg` (legacy MediaUpload) shape.
* `docs/block-library-audit.md` flips the six entity-wired post blocks
  out of the deny-list "Crash or backend-required" section into a new
  "Entity-wired" bucket documenting the resolved-attribute contract
  the renderers consume.

Tests: 463 PHP + 970 JS, all green. New coverage:

* core-data-shim.test.tsx: useEntityProp read, write, ambient id
  fallback, REST resolver path, same-component re-render after fetch.
* GutenbergAttachmentAdapterTest: WP REST shape, sizes reshape, null
  caption collapse.
* PostControllerTest: partial metadata-only PUT round-trips excerpt +
  featured_media end-to-end.

Closes #400

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 30, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR implements entity metadata persistence in the visual editor by integrating core-data with the block editor's persistence layer. It adds API endpoints for saving attachment records, updates the core-data shim to support entity property editing, and wires sidebar metadata changes to dispatch core-data record updates and flush staged edits through a new metadata-save API flow.

Changes

Cohort / File(s) Summary
Database & Documentation
database/migrations/testing/2026_04_19_000000_create_test_block_content_tables.php, docs/block-library-audit.md
Extends test fixtures with slug, excerpt, and featured_image_id fields; adds new "entity-wired" block category and updates Post-context block classifications to reflect core/post-* blocks as enabled-by-default with entity-dependent rendering.
API Layer
routes/api.php, src/Http/Controllers/AttachmentController.php
Introduces new GET /attachments/{id} endpoint returning attachment data via AttachmentController::show, which validates media availability and adapts it to WP REST shape.
Media Bridge
src/MediaBridge/GutenbergAttachmentAdapter.php
Adds toWpRestShape() method to normalize single media records into WP REST-compatible JSON with source_url, media_details, and size transformations.
Core-Data Editing
resources/js/visual-editor/vendor/core-data-shim.ts, resources/js/visual-editor/vendor/__tests__/core-data-shim.test.tsx
Updates useEntityProp hook to accept kind/name/prop/id parameters and return a mutable setter dispatching editEntityRecord, enabling entity property edits; expands default entity registry with postType:attachment for featured-image resolution.
API Client & Persistence
resources/js/visual-editor/editor/api-client.ts, resources/js/visual-editor/editor/use-persistence.ts
Adds saveEntityRecord function for PUT requests to {apiBase}/{resource}/{id} with CSRF protection; updates persistence hook to conditionally flush staged core-data entity edits alongside block content, with new queueMetadataForSave entrypoint.
Editor Integration
resources/js/visual-editor/editor/editor-app.tsx
Wires sidebar metadata handlers to stage entity fields via core-data, watches getEntityRecordEdits store to re-arm metadata flush, passes { postType, postId } context to block renderer for core/post-* block resolution, and derives entity identity from documentType and numericId.
Tests
tests/Feature/VisualEditor/Adapters/CmsFramework/PostControllerTest.php, tests/Unit/VisualEditor/MediaBridge/GutenbergAttachmentAdapterTest.php
Adds coverage for partial metadata PUT requests (excerpt, featured_media) and validates toWpRestShape() output structure including source_url, media_details, and caption rendering.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Sidebar as Sidebar UI
    participant EditorApp as Editor App
    participant CoreData as Core-Data Store
    participant PersistenceHook as Persistence Hook
    participant ApiClient as API Client
    participant Backend as Backend API

    User->>Sidebar: Edit metadata (title, excerpt, featured_image)
    Sidebar->>EditorApp: onChange handler triggered
    EditorApp->>CoreData: dispatch editEntityRecord(field, value)
    EditorApp->>PersistenceHook: queueMetadataForSave()
    
    PersistenceHook->>CoreData: select getEntityRecordEdits
    CoreData-->>PersistenceHook: return staged edits {excerpt, featured_media, ...}
    
    alt Block content also changed
        PersistenceHook->>Backend: PUT /posts/{id} with content
        Backend-->>PersistenceHook: block save response
    end
    
    PersistenceHook->>ApiClient: saveEntityRecord(edits)
    ApiClient->>Backend: PUT /posts/{id} with metadata only
    Backend-->>ApiClient: updated record JSON
    ApiClient-->>PersistenceHook: return parsed response
    
    PersistenceHook->>CoreData: dispatch clearEntityRecord edits
    PersistenceHook->>User: metadata persisted
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 Metadata hops down the core-data lane,
Saving titles, excerpts, featured frames,
Featured images flutter, attached with care,
The editor persists what's edited there! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly references the main objective—wiring core/post-* blocks against G3 entities—and accurately reflects the core technical change in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/400-verify-core-post-blocks-g3

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.51)

PHPStan was skipped because the sandbox runner could not parse its output.


Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Apr 30, 2026
@ViewFromTheBox ViewFromTheBox linked an issue Apr 30, 2026 that may be closed by this pull request
5 tasks
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review April 30, 2026 16:01

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
resources/js/visual-editor/editor/editor-app.tsx (1)

971-977: 💤 Low value

Consider using blockContextValue directly in the conditional for type safety.

TypeScript won't narrow blockContextValue based on checking documentType and numericId separately. If BlockContextProvider strictly types its value prop as non-nullable, this could cause a compile error in stricter configurations.

Suggested refinement
-                            {documentType !== null && numericId !== null ? (
-                                <BlockContextProvider value={blockContextValue}>
+                            {blockContextValue !== null ? (
+                                <BlockContextProvider value={blockContextValue}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/visual-editor/editor/editor-app.tsx` around lines 971 - 977, The
conditional should check blockContextValue directly so TypeScript can narrow its
nullability before passing into BlockContextProvider; replace the condition
(documentType !== null && numericId !== null) with a direct check of
blockContextValue (e.g., blockContextValue != null) and wrap BlockList in
<BlockContextProvider value={blockContextValue}> only when that value is
non-null, referencing BlockContextProvider, blockContextValue and BlockList to
locate the change.
resources/js/visual-editor/editor/use-persistence.ts (1)

262-317: 💤 Low value

Consider handling partial success more gracefully.

If block content saves successfully but saveEntityRecord fails, the error status is set and lastSavedAt is not updated, even though the blocks were persisted. This could confuse users who might believe their block changes were lost.

A possible improvement: track whether blocks saved successfully and update lastSavedAt before attempting the metadata save, then set status to a "partial error" or still update the timestamp while reporting the metadata failure.

This is an edge case, so the current behavior (prioritizing error signaling) is acceptable for now.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/visual-editor/editor/use-persistence.ts` around lines 262 - 317,
When saveContent succeeds but saveEntityRecord may fail, update savedAt and call
setLastSavedAt(savedAt) immediately after the saveContent/response path (inside
the block that sets savedAt and dispatchEditorEvent) so the block timestamp
reflects persisted blocks, then wrap the saveEntityRecord call in its own
try/catch (referencing saveEntityRecord, readEntityEdits, clearEntityEdits,
savedAt, setLastSavedAt, setSaveStatus) and on entity-save failure set a
distinct status (e.g., setSaveStatus('partial-error') or keep 'saved' while
reporting the metadata error) without clearing the lastSavedAt; ensure the catch
still returns early if unmountedRef.current or version mismatch requires
aborting and only clears edits on success.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@resources/js/visual-editor/editor/editor-app.tsx`:
- Around line 971-977: The conditional should check blockContextValue directly
so TypeScript can narrow its nullability before passing into
BlockContextProvider; replace the condition (documentType !== null && numericId
!== null) with a direct check of blockContextValue (e.g., blockContextValue !=
null) and wrap BlockList in <BlockContextProvider value={blockContextValue}>
only when that value is non-null, referencing BlockContextProvider,
blockContextValue and BlockList to locate the change.

In `@resources/js/visual-editor/editor/use-persistence.ts`:
- Around line 262-317: When saveContent succeeds but saveEntityRecord may fail,
update savedAt and call setLastSavedAt(savedAt) immediately after the
saveContent/response path (inside the block that sets savedAt and
dispatchEditorEvent) so the block timestamp reflects persisted blocks, then wrap
the saveEntityRecord call in its own try/catch (referencing saveEntityRecord,
readEntityEdits, clearEntityEdits, savedAt, setLastSavedAt, setSaveStatus) and
on entity-save failure set a distinct status (e.g.,
setSaveStatus('partial-error') or keep 'saved' while reporting the metadata
error) without clearing the lastSavedAt; ensure the catch still returns early if
unmountedRef.current or version mismatch requires aborting and only clears edits
on success.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 104277ac-7b26-46ab-a3c3-b063e4598231

📥 Commits

Reviewing files that changed from the base of the PR and between 9e737f2 and e9d0ed4.

📒 Files selected for processing (12)
  • database/migrations/testing/2026_04_19_000000_create_test_block_content_tables.php
  • docs/block-library-audit.md
  • resources/js/visual-editor/editor/api-client.ts
  • resources/js/visual-editor/editor/editor-app.tsx
  • resources/js/visual-editor/editor/use-persistence.ts
  • resources/js/visual-editor/vendor/__tests__/core-data-shim.test.tsx
  • resources/js/visual-editor/vendor/core-data-shim.ts
  • routes/api.php
  • src/Http/Controllers/AttachmentController.php
  • src/MediaBridge/GutenbergAttachmentAdapter.php
  • tests/Feature/VisualEditor/Adapters/CmsFramework/PostControllerTest.php
  • tests/Unit/VisualEditor/MediaBridge/GutenbergAttachmentAdapterTest.php

@ViewFromTheBox
ViewFromTheBox merged commit 33d3efc into release/1.0 Apr 30, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Apr 30, 2026
@ViewFromTheBox
ViewFromTheBox deleted the chore/400-verify-core-post-blocks-g3 branch April 30, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

G4a — Verify core/post-* blocks fully wired against G3 entities

1 participant