Skip to content

feat: C2 · Template parts model, migration, API - #363

Merged
ViewFromTheBox merged 2 commits into
release/1.0from
feature/358-c2-template-parts-backend
Apr 23, 2026
Merged

ViewFromTheBox merged 2 commits into
release/1.0from
feature/358-c2-template-parts-backend

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Apr 23, 2026 •

Copy link
Copy Markdown
Member

Description

Lands the server-side backing for the wp_template_part entity the B1 core-data shim dispatches at /visual-editor/api/template-parts. Adds the VisualEditorTemplatePart model, migration, REST controller, form requests, policy, and routes so the D2 site-editor browser and the E2 front-end template-part renderer have a real store to resolve against.

Closes: #358

Type of Change

  • New feature (adds new functionality)

Related Issue

Issue: #358

Motivation and Context

Template parts are reusable fragments of a template — header, footer, sidebar, etc. — referenced from templates via core/template-part blocks. B1 stubbed out the REST contract and B2 shipped fixtures demonstrating the shape; C2 makes those calls resolve against real data.

Because a single part can be used by several templates (the B2 header.json + footer.json are referenced by every template fixture), the show response carries a referenced_by pointer listing the template slugs that currently include it. That lets D2 warn before deleting a part still in use.

Changes Made

  • VisualEditorTemplatePart Eloquent model with the { raw, blocks } content envelope accessors and area / theme / slug scopes, mirroring VisualEditorTemplate so block renderers can consume either surface without translation.
  • Migration for visual_editor_template_parts (id, slug, title, content JSON, area, theme, timestamps) with composite unique (slug, theme).
  • TemplatePartController with index, show, store, update, destroy. show merges a derived referenced_by list — computed by recursively scanning block trees of templates in the same theme for core/template-part blocks whose slug + theme match the current part.
  • StoreTemplatePartRequest + UpdateTemplatePartRequest — area validated against the enum, slug unique per theme (mirrored on both slug + theme fields so theme-only updates still trigger the check), bare-list content payloads rejected to keep TemplateBlockTreeRule reachable.
  • TemplatePartResource emitting the shim-compatible { id, slug, title.rendered, content.{raw,blocks}, area, theme, type } shape.
  • VisualEditorTemplatePartPolicy — auth-only CRUD, mirroring VisualEditorTemplatePolicy. Host apps tighten access by rebinding the policy.
  • 5 routes registered under /visual-editor/api/template-parts with the package's existing auth middleware stack.

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS (Darwin 25.3.0)
  • PHP Version: 8.4.3
  • Project Version: 1.0.0-alpha.x (release/1.0)

Tests Performed:

  1. Full Pest suite: 252 tests pass (31 new, 221 existing — nothing regressed).
  2. Targeted: ./vendor/bin/pest tests/Feature/VisualEditor/TemplatePartControllerTest.php — 31/31 green.
  3. CodeRabbit local review (cr review --base release/1.0 --prompt-only): No findings.

Accessibility Tests Run

  • Not applicable — backend REST surface, no UI introduced in this PR.

Tests Added

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

Test details:

tests/Feature/VisualEditor/TemplatePartControllerTest.php (31 tests, 144 assertions):

  • index: unauthenticated 401, empty { data, meta } envelope, populated list, filters by slug / theme / area.
  • show: unauthenticated 401, record with content envelope + rendered title, 404 on missing id.
  • referenced_by: correctly lists referencing templates; ignores parts in other themes; traverses nested innerBlocks; deduplicates when a template embeds a part twice.
  • store: all four valid areas (header, footer, sidebar, uncategorized) — dataset-driven; 422 on invalid/missing area with a message that names every legal value; 422 on duplicate (slug, theme); success on same slug across themes; 422 on malformed block tree; 422 on null title.
  • update: partial payload, area enum validation, area update, content-envelope persistence, 422 on theme-only change that would collide with an existing (slug, theme), 422 on null title, 422 on bare-list content.
  • destroy: happy path + unauthenticated 401.
  • B2 round-trip: header/footer/sidebar fixtures post through store and come back unchanged from show.

Documentation

  • Inline code documentation added

Documentation details: Every new class carries a PHPDoc block explaining its role and, where relevant, the design decisions (why referenced_by is derived rather than persisted, why area is a plain string column gated by application-layer validation, why uniqueness is mirrored on both slug + theme in the update request).

The B1 core-data shim contract at docs/core-data-shim.md §Template parts already documented the endpoint shape, so no doc changes are needed.

Screenshots

N/A — backend change.

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted (CodeRabbit: no findings)
  • Accessibility tests completed (N/A — backend)
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

Summary by CodeRabbit

  • New Features
    • Added REST API for managing template parts with full CRUD, filtering by theme/slug/area, and authenticated access.
    • Persisted template parts via a new database table and model with normalized content envelope and uniqueness per (slug, theme).
    • Automatic reference tracking for template-part usage and a JSON resource shape matching expected responses.
  • Tests
    • Comprehensive feature tests covering auth, listing/filtering, CRUD, validation, reference resolution, and fixture round-trips.

Lands the server-side backing for the `wp_template_part` entity the B1
core-data shim addresses at `/visual-editor/api/template-parts`. Adds
the `VisualEditorTemplatePart` model, migration, REST controller, form
requests, policy, and routes so the D2 site-editor browser and the E2
front-end template-part renderer have a real store to resolve against.

The `show` response carries a `referenced_by` list — template slugs
whose block tree embeds this part via a `core/template-part` block —
computed by scanning templates in the same theme at read time (no FK,
since parts and templates can outlive each other). D2 will use it to
warn before deleting a part that is still in use.

Area is validated against the enum (header, footer, sidebar,
uncategorized) with a clear 422 message; `slug` is unique per theme so
the same `header` slug can coexist across installed themes.

Closes #358
@coderabbitai

coderabbitai Bot commented Apr 23, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds full server-side support for wp_template_part: migration, Eloquent model, REST CRUD endpoints, validation, JSON resource, authorization policy, referenced_by computation (templates that embed a part), service-provider policy registration, and comprehensive feature tests.

Changes

Cohort / File(s) Summary
Database & Model
database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.php, src/Models/VisualEditorTemplatePart.php
New migration creates visual_editor_template_parts with id, slug, title, content (JSON), area, theme, timestamps and unique (slug,theme). Model adds content-envelope normalization, accessors (getBlocks, getRawContent), mutator, and scopes (forTheme, forArea, forSlug).
API Routing
routes/api.php
Registers five REST routes under /visual-editor/api/template-parts mapping to TemplatePartController (index, store, show, update, destroy).
Controller & Resource
src/Http/Controllers/TemplatePartController.php, src/Http/Resources/TemplatePartResource.php
New controller implements paginated index (filters: theme/slug/area, per_page clamp), show (adds referenced_by), store (201), update (partial updates), destroy (204). Resource formats model to B1 wp_template_part shape with title.rendered and content.raw/blocks.
Validation
src/Http/Requests/StoreTemplatePartRequest.php, src/Http/Requests/UpdateTemplatePartRequest.php
New FormRequests enforce slug unique per theme, required area from enum, theme string, and content envelope shape (rejects bare block lists) with custom error messages; update request resolves slug/theme for uniqueness on partial updates.
Authorization & Provider
src/Policies/VisualEditorTemplatePartPolicy.php, src/VisualEditorServiceProvider.php
New policy gates all actions to authenticated users; service provider registers model→policy mapping via Gate::policy.
Tests & Fixtures
tests/Feature/VisualEditor/TemplatePartControllerTest.php, tests/Fixtures/sample-content/template-parts/*
Comprehensive feature tests: auth gating, index/show/store/update/destroy, filtering, validation failures, referenced_by accuracy (nested/deduped), duplicate (slug,theme) checks, and fixture round-trip assertions.

Sequence Diagram

sequenceDiagram
    actor Client
    participant API as "HTTP API\n/routes/api.php"
    participant Controller as "TemplatePartController"
    participant DB as "VisualEditorTemplatePart\n(database)"
    participant Templates as "Template fixtures\n/template storage"

    Client->>API: GET /visual-editor/api/template-parts/{id}
    API->>Controller: route → show(request, templatePart)
    Controller->>DB: load template part record
    DB-->>Controller: record (content envelope)
    Controller->>Templates: scan templates in same theme\nfor core/template-part blocks (slug, theme)
    Templates-->>Controller: matching template slugs (preserve order, dedupe)
    Controller->>Controller: build referenced_by array
    Controller->>API: TemplatePartResource + referenced_by (200)
    API-->>Client: 200 OK + wp_template_part JSON
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I nibble migrations, hop through code,
Parts and blocks now find a home,
I trace each reference, neat and bold,
CRUD carrots line the route I roam,
Backend burrows — templates safely grown.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.71% 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 'feat: C2 · Template parts model, migration, API' accurately summarizes the main changes: adding the VisualEditorTemplatePart model, database migration, and REST API for template parts.
Linked Issues check ✅ Passed All primary coding objectives from #358 are met: VisualEditorTemplatePart model with content accessors and scopes, migration with correct schema and composite unique constraint, complete REST controller with CRUD endpoints, form-request validation for area and slug, TemplatePartResource with correct response shape, authorization policy, referenced_by computation, and comprehensive Pest tests.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #358 requirements. No UI implementation, enabled_blocks modification, site-editor integration, bulk operations, or cross-theme duplication is present. The changes are strictly server-side backing.

✏️ 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 feature/358-c2-template-parts-backend

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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 23, 2026
@ViewFromTheBox ViewFromTheBox linked an issue Apr 23, 2026 that may be closed by this pull request
8 tasks
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review April 23, 2026 16:22

@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.

Actionable comments posted: 1

🧹 Nitpick comments (2)
database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.php (1)

31-42: Add a theme-leading index to support common filters efficiently.

forTheme()/theme-scoped listing will not benefit from unique(slug, theme) because theme is not the leading column. Add index(theme) or index(theme, area).

📈 Suggested migration tweak
 Schema::create( 'visual_editor_template_parts', function ( Blueprint $table ) {
 	$table->id();
 	$table->string( 'slug' );
 	$table->string( 'title' )->default( '' );
 	$table->json( 'content' )->nullable();
 	$table->string( 'area' )->default( 'uncategorized' )->index();
 	$table->string( 'theme' );
+	$table->index( [ 'theme', 'area' ] );
 	$table->timestamps();

 	$table->unique( [ 'slug', 'theme' ] );
 } );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.php`
around lines 31 - 42, The migration currently indexes area and defines
unique(slug, theme) but does not make theme the leading column for common
theme-scoped queries; update the schema in the migration where $table is defined
(the create_visual_editor_template_parts table) to add an index on theme (e.g.
$table->index('theme')) or a composite index with theme first (e.g.
$table->index(['theme', 'area'])) so theme-scoped lookups
(forTheme()/theme-scoped listing) are efficient.
tests/Feature/VisualEditor/TemplatePartControllerTest.php (1)

147-233: Add a referenced_by regression case for blocks without attributes.theme.

All of these reference tests set theme on the core/template-part block. The controller is intended to tolerate older exports that omit that attribute, so this path can regress unnoticed and under-report live references before delete.

➕ Suggested test
+it( 'treats same-theme template-part blocks without a theme attribute as references', function () {
+	actingAsTemplatePartUser();
+
+	$part = createTemplatePart( [ 'slug' => 'header', 'theme' => 'artisanpack-base' ] );
+
+	VisualEditorTemplate::create( [
+		'slug'    => 'single',
+		'title'   => 'Single',
+		'content' => [
+			'raw'    => '',
+			'blocks' => [
+				[
+					'name'        => 'core/template-part',
+					'attributes'  => [ 'slug' => 'header' ],
+					'innerBlocks' => [],
+				],
+			],
+		],
+		'status'  => 'publish',
+		'theme'   => 'artisanpack-base',
+		'source'  => 'theme',
+		'origin'  => 'theme',
+	] );
+
+	$this->getJson( "/visual-editor/api/template-parts/{$part->id}" )
+		->assertOk()
+		->assertJsonPath( 'referenced_by', [ 'single' ] );
+} );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Feature/VisualEditor/TemplatePartControllerTest.php` around lines 147 -
233, Add a regression test that ensures template references are detected when
the core/template-part block omits the attributes.theme field: create a
TemplatePart via
createTemplatePart(['slug'=>'header','theme'=>'artisanpack-base']), then create
a VisualEditorTemplate::create(...) whose blocks include a core/template-part
entry that has 'attributes' => ['slug'=>'header'] (no 'theme'), call
$this->getJson("/visual-editor/api/template-parts/{$part->id}") and
assertJsonPath('referenced_by', ['single']) to verify the controller tolerates
older exports missing the theme attribute.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Http/Controllers/TemplatePartController.php`:
- Around line 264-279: The blockTreeReferencesPart function currently only
treats 'core/template-part' blocks as references when the block's attributes
include an explicit theme; update the matching logic so that a block with a
matching slug is considered a reference if its attributes.theme is either equal
to the provided $theme OR is missing/empty (treat empty $attributeTheme as
same-theme). Modify the condition that returns true (inside
blockTreeReferencesPart) to allow $attributeTheme === '' as a valid match when
$slug === $attributeSlug, ensuring older exports without attributes.theme are
counted as references by resolveReferencedBy().

---

Nitpick comments:
In
`@database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.php`:
- Around line 31-42: The migration currently indexes area and defines
unique(slug, theme) but does not make theme the leading column for common
theme-scoped queries; update the schema in the migration where $table is defined
(the create_visual_editor_template_parts table) to add an index on theme (e.g.
$table->index('theme')) or a composite index with theme first (e.g.
$table->index(['theme', 'area'])) so theme-scoped lookups
(forTheme()/theme-scoped listing) are efficient.

In `@tests/Feature/VisualEditor/TemplatePartControllerTest.php`:
- Around line 147-233: Add a regression test that ensures template references
are detected when the core/template-part block omits the attributes.theme field:
create a TemplatePart via
createTemplatePart(['slug'=>'header','theme'=>'artisanpack-base']), then create
a VisualEditorTemplate::create(...) whose blocks include a core/template-part
entry that has 'attributes' => ['slug'=>'header'] (no 'theme'), call
$this->getJson("/visual-editor/api/template-parts/{$part->id}") and
assertJsonPath('referenced_by', ['single']) to verify the controller tolerates
older exports missing the theme attribute.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 70498dc7-4ea0-4c12-ba96-01fa7a2e4eb6

📥 Commits

Reviewing files that changed from the base of the PR and between 2bdd354 and 9d3ce9a.

📒 Files selected for processing (10)
  • database/migrations/2026_04_23_100000_create_visual_editor_template_parts_table.php
  • routes/api.php
  • src/Http/Controllers/TemplatePartController.php
  • src/Http/Requests/StoreTemplatePartRequest.php
  • src/Http/Requests/UpdateTemplatePartRequest.php
  • src/Http/Resources/TemplatePartResource.php
  • src/Models/VisualEditorTemplatePart.php
  • src/Policies/VisualEditorTemplatePartPolicy.php
  • src/VisualEditorServiceProvider.php
  • tests/Feature/VisualEditor/TemplatePartControllerTest.php

Comment thread src/Http/Controllers/TemplatePartController.php
resolveReferencedBy() already scopes the template scan to the target
theme, so an untagged `core/template-part` block inside one of those
templates can only resolve to a part in that same theme. The docblock
on blockTreeReferencesPart() already promised this behavior, but the
implementation required an exact theme match — older exports without
attributes.theme would silently drop out of the referenced_by list.

Adds a regression test covering the untagged-reference case.

@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 (1)
src/Http/Controllers/TemplatePartController.php (1)

177-184: Consider using response()->noContent() for 204 responses.

response()->json(null, 204) sends a body containing null. For a true 204 No Content response, Laravel's response()->noContent() is more semantically correct and cleaner.

♻️ Suggested improvement
 public function destroy( VisualEditorTemplatePart $templatePart ): JsonResponse
 {
     Gate::authorize( 'delete', $templatePart );

     $templatePart->delete();

-    return response()->json( null, Response::HTTP_NO_CONTENT );
+    return response()->noContent();
 }

Note: The return type would need to change to Response instead of JsonResponse.

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

In `@src/Http/Controllers/TemplatePartController.php` around lines 177 - 184, In
the destroy method for VisualEditorTemplatePart, replace the JSON 204 response
with Laravel's response()->noContent() to send a proper 204 No Content response;
update the method return type from JsonResponse to Illuminate\Http\Response (or
Response) and ensure the Gate::authorize and $templatePart->delete() calls
remain unchanged so the method returns response()->noContent() instead of
response()->json(null, Response::HTTP_NO_CONTENT).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Http/Controllers/TemplatePartController.php`:
- Around line 177-184: In the destroy method for VisualEditorTemplatePart,
replace the JSON 204 response with Laravel's response()->noContent() to send a
proper 204 No Content response; update the method return type from JsonResponse
to Illuminate\Http\Response (or Response) and ensure the Gate::authorize and
$templatePart->delete() calls remain unchanged so the method returns
response()->noContent() instead of response()->json(null,
Response::HTTP_NO_CONTENT).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f80c5846-9bf4-44cd-a4af-211bfd6707a8

📥 Commits

Reviewing files that changed from the base of the PR and between 9d3ce9a and b455f16.

📒 Files selected for processing (2)
  • src/Http/Controllers/TemplatePartController.php
  • tests/Feature/VisualEditor/TemplatePartControllerTest.php

@ViewFromTheBox
ViewFromTheBox merged commit 1adcaf0 into release/1.0 Apr 23, 2026
1 check passed
@ViewFromTheBox
ViewFromTheBox deleted the feature/358-c2-template-parts-backend branch April 23, 2026 16:37
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Apr 23, 2026
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.

C2 · Template parts: model, migration, API

1 participant