Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Added

- **cms-framework integration (Phase G).** First-class pairing with [`artisanpack-ui/cms-framework`](https://github.com/ArtisanPack-UI/cms-framework). When both packages are installed, the visual editor can edit cms-framework's `Post` and `Page` content end-to-end; `core/site-*` blocks read from cms-framework's settings store via `apGetSetting('site.*')`; `core/post-*`, `core/archives`, `core/categories`, `core/tag-cloud`, `core/query`, and `core/query-loop` come off the V1 deny-list and resolve against cms-framework's models and term endpoints. Loose coupling preserved — both packages remain usable standalone; cms-framework's editor wiring is guarded by `class_exists(\ArtisanPackUI\VisualEditor\VisualEditor::class)`. Pair-versioning matrix lives in the README; the [`docs/g6-smoke-flow.md`](docs/g6-smoke-flow.md) flow runs against the version pair before every release tag. Full integration contract: [`docs/plans/12-cms-framework-integration.md`](docs/plans/12-cms-framework-integration.md).
- "Using with cms-framework" README section. Covers install, migrations, the merged resource map under `ap.visual-editor.resources`, and the version-pair contract.
- `visual_editor.*` permissions schema seeded into cms-framework's RBAC when both packages are installed (G5). Policies still use the "any authenticated user" baseline in V1.0; delegation lands in V1.1 behind `artisanpack.visual-editor.authorization.delegate_to_cms_framework`.
- `artisanpack/form` block. Dynamic block that lets authors pick a form from the artisanpack-ui/forms package via the InspectorControls sidebar, and renders a `<div data-keystone-form="…" data-form-id="…">` mount-point on the public site. The host application supplies a JS island that hydrates the mount-point with the forms package's React `FormRenderer`. Registration is gated on `class_exists(ArtisanPackUI\Forms\Models\Form::class)` so visual-editor still boots when forms is absent.
- Stale-selection guard in the form block's editor preview. When the persisted `formId` no longer matches any form returned by `/api/v1/forms`, the canvas renders a distinct `<Placeholder>` with a "Reset selection" button that clears the attribute back to `0`. Replaces the previous silent fall-through to the 404 "inactive" message, which mis-coded deleted forms as merely deactivated.

Expand Down
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ gate (#432) surfaces a "cms-framework required" page instead of mounting.
| v0.x | v0.x | Pre-v1 — no site-editor integration |

Bumping the major on either package without bumping the partner is
unsupported. The H8 smoke flow ([`docs/h8-smoke-flow.md`](docs/h8-smoke-flow.md))
runs against this version pair before every release tag.
unsupported. Both smoke flows run against this version pair before every
release tag:
[`docs/g6-smoke-flow.md`](docs/g6-smoke-flow.md) covers Phase G
(cms-framework content integration — posts, pages, site-meta, query
loop); [`docs/h8-smoke-flow.md`](docs/h8-smoke-flow.md) covers Phase H
(site-editor — templates, parts, patterns, global styles, navigation).

## Peer Dependencies

Expand Down Expand Up @@ -65,6 +69,47 @@ These blocks will be re-enabled once `artisanpack-ui/cms-framework` replaces the

Override the defaults by publishing the config to `config/artisanpack/visual-editor.php` and editing the `enabled_blocks` / `disabled_blocks` arrays. The deny-list always wins over the allow-list.

## Using with cms-framework

The visual editor is fully usable standalone, but pairs with [`artisanpack-ui/cms-framework`](https://github.com/ArtisanPack-UI/cms-framework) to unlock editable `Post` and `Page` content, a real backing for `core/site-*` blocks, working `core/post-*` / `core/query` / taxonomy widget blocks, and seeded `visual_editor.*` permissions. The full integration contract lives in [`docs/plans/12-cms-framework-integration.md`](docs/plans/12-cms-framework-integration.md).

### Install both packages

```bash
composer require artisanpack-ui/visual-editor artisanpack-ui/cms-framework
```

Both packages are loosely coupled — cms-framework's editor wiring is guarded by `class_exists(\ArtisanPackUI\VisualEditor\VisualEditor::class)`, so each package remains usable on its own.

### Run migrations

```bash
php artisan migrate
```

cms-framework's V1.x migration set adds a `block_content json nullable` column to its `posts` and `pages` tables (the legacy `content` longText column is preserved for search / excerpt / backwards-compatibility — see plan 12 §4.2 for the dual-state guidance).

### Resource map

When both packages are installed, cms-framework registers its `Post` and `Page` into the `ap.visual-editor.resources` filter automatically. The merged map ends up shaped like:

```php
// Effective config('artisanpack.visual-editor.resources') after the
// ap.visual-editor.resources filter has run with both packages installed.
[
'posts' => \ArtisanPackUI\CMSFramework\Modules\Blog\Models\Post::class,
'pages' => \ArtisanPackUI\CMSFramework\Modules\Pages\Models\Page::class,
// …plus any slug → model entries the host app added to
// config/artisanpack/visual-editor.php (static config wins on collision).
]
```

Host-app overrides in `config/artisanpack/visual-editor.php` always take precedence on key collision, so swapping `posts` to a custom `App\Models\Post` is just a config edit.

### Version pairing

visual-editor V1.0.x ships against cms-framework V1.x. The compatibility matrix is in [Version compatibility](#version-compatibility) above; the [`docs/g6-smoke-flow.md`](docs/g6-smoke-flow.md) and [`docs/h8-smoke-flow.md`](docs/h8-smoke-flow.md) flows run against the version pair before every release tag.

## Extensibility

The package exposes a small filter surface so other packages can contribute editor wiring at runtime without forcing host apps to publish-and-edit the package config.
Expand Down
192 changes: 192 additions & 0 deletions docs/g6-smoke-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
# G6 smoke flow

Manual QA checklist that exercises the cms-framework integration
(Phase G) end-to-end. Run it on every release candidate before tagging
visual-editor v1.0.x and on every cms-framework v1.x release that pairs
with it.

Not automated — Phase G spans two Composer packages, a real database,
the editor SPA, public-route rendering across the Blade renderer, and
cms-framework's admin / settings UI. Reproducing that in Testbench
hides more failure modes than it catches. The smoke flow stays a manual
regression net by design; see
[`plans/12-cms-framework-integration.md`](plans/12-cms-framework-integration.md) §3 (Phase G6).

This flow is complementary to [`h8-smoke-flow.md`](h8-smoke-flow.md):
G6 verifies post / page editing, site-meta, and query-loop blocks
against cms-framework's content stores; H8 verifies the site-editor
SPA (templates, parts, patterns, global styles, navigation). Run G6
before H8 on a release candidate so a content-side regression is
caught before the site-editor pass.

## Test surface

The `~/Herd/artisanpack-ui` dev app is the reference fixture. Both
packages are wired in via path repositories, so source changes in
either package land in the dev app on the next refresh.

## Prerequisites

- Composer-installable Laravel host with PHP 8.4+.
- Both `artisanpack-ui/visual-editor` and `artisanpack-ui/cms-framework`
installed and version-paired (see
[`README.md` § Version compatibility](../README.md#version-compatibility)).
- A user with permission to access the editor — the dev app's default
admin user works; on a host with RBAC wired, give the user the
`visual_editor.access` and `visual_editor.posts.edit` permissions.

## Steps

### A. Install + migrate

1. From the dev app root, run package migrations: `php artisan migrate`.
2. Confirm the cms-framework content tables — `posts`, `pages` —
carry both the legacy `content` longText column and the new
`block_content` json column. The dual-state is intentional (plan
12 §4.2); the legacy column stays untouched for backwards
compatibility.
3. Confirm cms-framework's `settings` table has rows for
`site.title`, `site.tagline`, `site.url`, `site.logo_id`,
`site.icon_id`. Defaults seed from `config('app.name')` /
`config('app.url')` on install.

**Assert:** `php artisan tinker` → `apGetSetting('site.title')` returns
the dev app's configured app name, not `null`.

### B. Create a post

1. Create a `Post` via cms-framework's admin (or via factory in
tinker: `Post::factory()->create(['title' => 'G6 smoke post'])`).
2. Note the post id.

**Assert:** the post exists with `block_content` set to `null` and
`content` carrying whatever the factory / admin emitted.

### C. Open the post in the visual editor

1. Navigate to `/admin/visual-editor/posts/{id}` (or the route the
host app mounts the post editor on — the visual-editor entity for
`postType:post` resolves through `/visual-editor/api/posts/{id}`).
2. The editor shell loads with the post's existing content rendered as
blocks.

**Assert:**
- The inspector sidebar's document panel surfaces status, slug,
author, taxonomies, featured image, and excerpt (G3 sidebar fields).
- No console errors about missing entities. `dispatch('core').addEntities`
has registered `postType:post` and `postType:page` against
`/visual-editor/api/posts` and `/visual-editor/api/pages`.

### D. Add and save site-context blocks

1. From the inserter, add a `core/site-title` block.
2. Add a `core/site-tagline` below it.
3. Add a `core/post-title` and a `core/post-excerpt` block.
4. Save the post (top-right Save button or `⌘S`).

**Assert:**
- The post row's `block_content` column carries the saved block tree
(JSON with `core/site-title`, `core/site-tagline`, `core/post-title`,
`core/post-excerpt`).
- The legacy `content` column is unchanged from step B.

### E. Add a query block

1. From the inserter, add a `core/query` block scoped to `postType:post`.
2. Leave the inner `core/post-template` defaults in place (post title,
excerpt).
3. Save.

**Assert:**
- `block_content` now carries the query block with normalized attrs
(postType, perPage, etc.) and the inner template.
- No console errors from the editor preview — the query block resolves
through `POST /visual-editor/api/query/resolve` which proxies to
cms-framework's `QueryRuntime::resolve()` (plan 12 §4.5).

### F. Render on the public front-end

1. Visit the public route for the post (the dev app exposes it under
whatever pattern cms-framework's `BlogManager` is wired to —
typically `/blog/{slug}` or `/posts/{slug}`).
2. Inspect the rendered page.

**Assert:**
- `core/site-title` renders the value from `apGetSetting('site.title')`.
- `core/site-tagline` renders `apGetSetting('site.tagline')` (empty
string acceptable if the dev app hasn't set a tagline).
- `core/post-title` renders the post's title.
- `core/post-excerpt` renders the post's excerpt.
- `core/query` resolves the configured post type and renders the
inner template once per result (limited by `perPage`).
- The Blade renderer is calling `QueryRuntime::resolve()` directly
in-process — confirm with a query-log tap (`DB::listen(…)`) that the
SQL hits run on the same request cycle, no HTTP round-trip.

### G. Site-meta parity (editor canvas ↔ front-end)

1. From cms-framework's admin settings UI (or via tinker:
`apUpdateSetting('site.title', 'G6 parity check')`), change
`site.title`.
2. Reload the post in the editor canvas.
3. Reload the public post page.

**Assert:**
- Both surfaces render the updated title without a cache clear.
- The editor's `core/site-title` block reads the new value through
`GET /api/settings/site` (cms-framework's WP-shape settings endpoint,
plan 12 §4.3).
- The Blade renderer reads it through `apGetSetting('site.title')`
via `SiteMetaResolver` (`packages/visual-editor-renderer-blade/
src/Resolvers/SiteMetaResolver.php`).

### H. Standalone cms-framework still boots

1. In a scratch directory, install cms-framework without
visual-editor (`composer require artisanpack-ui/cms-framework`).
2. Run `php artisan migrate`.
3. Boot a tinker session.

**Assert:**
- No errors at boot. cms-framework's `class_exists` guards keep its
editor-side hooks dormant.
- `apGetSetting('site.title')` works (settings registration is not
guarded — it's a cms-framework primitive).
- `addFilter('ap.visual-editor.resources', …)` registration silently
no-ops because visual-editor's filter dispatcher isn't loaded.

## When something fails

- **Editor canvas blank on the saved post:** check
`/visual-editor/api/posts/{id}` returns a WP-shape envelope with
`content.blocks` populated. The Phase G3 transformer in
`src/Http/Resources/Adapters/CmsFramework/` is responsible for
parsing `block_content` into the `content` field.
- **`core/site-*` blocks render the config fallback instead of the
setting value:** confirm `apGetSetting()` is reachable from the
renderer (`function_exists('apGetSetting')` in `SiteMetaResolver`).
If cms-framework's settings module didn't boot, the helper won't be
registered and the resolver falls back to
`config('artisanpack.visual-editor.site_meta')`.
- **`core/query` returns an empty list:** confirm cms-framework's
`QueryRuntime::resolve()` is dispatching to the right manager for
the requested `postType`. The dispatch table lives in
`ArtisanPackUI\CMSFramework\Modules\Blog\Services\QueryRuntime`.
- **Permissions show up in the editor but the user can't save:**
expected in V1.0 — visual-editor's policies still use the "any
authenticated user" baseline. Permission delegation lands in V1.1
behind the
`artisanpack.visual-editor.authorization.delegate_to_cms_framework`
flag (plan 12 §4.6).

## Logging

Each release that runs this flow should record:

- Date + git SHA of visual-editor + cms-framework
- Pass / fail per step
- Any deviations from the assertions (and the issue ID filed for the
deviation)

Keep the log in the release PR description or `docs/releases/` —
wherever the release-engineering process lives.
58 changes: 47 additions & 11 deletions docs/plans/12-cms-framework-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,14 +371,50 @@ Per [`11-v1-expansion.md`](11-v1-expansion.md) §6: create milestone-level track

---

## 8. Open questions to answer at phase kickoff

Not blocking this plan, but worth naming so they don't surface mid-implementation:

- **G1a column name** — `block_content` (chosen) vs. `editor_content`. Settled: `block_content`.
- **G1c custom-type criteria** — auto-pickup if `HasBlockContent` present, or also require an explicit `supports => ['editor']` flag in the type registration? Leaning purely trait-based; the trait *is* the opt-in.
- **G2a `site.url` default** — use `config('app.url')` as default, or require explicit set during cms-framework install? Leaning: default to `config('app.url')`, override via admin UI.
- **G3 inspector sidebar fields** — beyond status / slug / author / taxonomies / featured-image: include excerpt, parent (pages), menu-order (pages), template (pages). Custom fields defer to V1.1.
- **G4c renderer/runtime contract** — REST-only call from all renderers (uniform) or direct manager call from Blade (faster, but two seams)? Leaning: direct call from Blade because the in-process win is real and the contract stays the same shape; document both code paths.
- **G5 permission list completeness** — confirmed list above covers core entities. Add per-action granularity (`visual_editor.posts.publish`) at G5 kickoff if RBAC users want it; otherwise keep the coarser list.
- **Site-meta read on the public front-end** — visual-editor's renderers use `apGetSetting()` when present, but what does the dev-app *itself* use for the site title in its layout chrome? Should be consistent. Decide at G2 kickoff.
## 8. Decisions made during phase implementation

The phase-kickoff open questions resolved as follows. Recording the actual
decisions here (rather than the original "leanings") so the plan doc stays
the canonical reference once each phase ships.

- **G1a column name** — **`block_content`**. The `HasBlockContent`
trait reads `$blockContentColumn`, and both cms-framework's `Post`
and `Page` migrations add `block_content json nullable` after the
legacy `content` column. The original column stays untouched for
search / excerpt compatibility.
- **G1c custom-type criteria** — **trait-based opt-in only**. Any model
registered through `ContentTypeManager` that uses `HasBlockContent`
is auto-registered into the resource filter; no extra
`supports => ['editor']` flag is required. cms-framework logs a
warning when a registered content type's model lacks the trait so
the silent skip is observable (§5.3).
- **G2a `site.url` default** — **`config('app.url')`**. cms-framework
registers `site.url` with `config('app.url')` as the default during
install. Hosts override via the admin settings UI or by writing the
setting directly with `apUpdateSetting()`.
- **G3 inspector sidebar fields** — **status, slug, author,
taxonomies, featured image, excerpt, plus parent / menu-order /
template for pages**. Custom-fields surface deferred to V1.1 as
originally scoped (§2.7).
- **G4c renderer/runtime contract** — **direct call from Blade,
REST from React/Vue clients**. `QueryRuntime::resolve()` is the
single source of truth; the Blade renderer calls it in-process,
and the React/Vue renderers hit `POST /visual-editor/api/query/resolve`
which wraps the same service. Both code paths are documented in §4.5
and exercised by the smoke flow in §G6 / `docs/g6-smoke-flow.md`.
- **G5 permission list completeness** — **coarse per-entity list ships
in V1.0**. The eight permissions listed in §4.6 (`visual_editor.access`
plus seven entity-specific `*.edit` permissions) cover the entities
V1 surfaces. Per-action granularity
(`visual_editor.posts.publish` etc.) waits until V1.1 when the
delegation flag (`artisanpack.visual-editor.authorization.delegate_to_cms_framework`)
is exercised in real apps — adding granularity earlier would seed
permissions that no policy ever checks.
- **Site-meta read on the public front-end** — **the dev app reads
the same `apGetSetting('site.*')` values** through cms-framework's
settings helper. Layout chrome (site title in the navbar, footer
copyright) reads `apGetSetting('site.title')` directly so the editor
preview and the public site can never drift. The G6 smoke flow
asserts this parity by changing `site.title` from the admin and
verifying both the editor canvas and the rendered front-end pick up
the new value without a cache clear.