Repository navigation
feat: composed-view ribbon and site-editor slug deep links #696
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -153,6 +153,56 @@ entity work: | |
| These are shareable — bookmark or send to a colleague to jump straight | ||
| into editing that entity. | ||
|
|
||
| ### Deep links by slug | ||
|
|
||
| Path routes address entities by database id. Callers outside the SPA | ||
| usually do not have one — the post editor's composed view, for instance, | ||
| knows only the slug of the template it composed against. For those, the | ||
| site editor accepts a query-string deep link on the mount URL: | ||
|
|
||
| ```text | ||
| /visual-editor/site?entity=template&slug=single | ||
| ``` | ||
|
|
||
| On mount the SPA parses the query string, resolves the slug through the | ||
| templates list endpoint, and navigates to that template's editor view. | ||
| The `pushState` that follows rewrites the address bar to the canonical | ||
| path route (`/visual-editor/site/templates/12`), so the query-string form | ||
| is an entry point rather than a URL the user is left holding. | ||
|
|
||
| | Parameter | Required | Notes | | ||
| |-----------|----------|-------| | ||
| | `entity` | yes | Only `template` is supported today. Unknown values are ignored. | | ||
| | `slug` | yes | Entity slug. Blank or whitespace-only values are ignored. | | ||
| | `entity_id` | no | Pre-resolved id. When supplied the slug lookup is skipped. Reserved for entity kinds that have no slug. | | ||
|
|
||
| Behaviour worth relying on: | ||
|
|
||
| - **Additive.** Mounting with no query string behaves exactly as it did | ||
| before — no lookup is issued and the SPA lands on its default section. | ||
| - **Unknown entity is a no-op**, not an error. A link written against a | ||
| future package version lands on the default section rather than | ||
| throwing. | ||
| - **Unresolvable slug lands on the Templates index** with a toast reading | ||
| `The template "{slug}" was not found.` A failed lookup does the same. | ||
| - **The access gate is unchanged.** `SiteEditorAccessGate` runs | ||
| server-side, before the SPA mounts, so a denied request still renders | ||
| the deny-by-default page and never reaches this parsing. | ||
|
|
||
| Build links with `buildTemplateDeepLink()` from | ||
| `site-editor/deep-link.ts` rather than assembling the query string by | ||
| hand, so both sides of the contract move together. The first consumer is | ||
| the post editor's composed-view ribbon, whose **Edit template ↗** button | ||
| opens one of these links in a new tab. | ||
|
|
||
| `buildTemplateDeepLink()` defaults to the package's own mount path | ||
| (`/visual-editor/site`) and takes the route base as an optional second | ||
| argument. The composed-view ribbon currently relies on that default: the | ||
| post editor has no way to discover a `data-route-base` the host may have | ||
| overridden on the site-editor mount element. A host that mounts the site | ||
| editor at a non-default path will get a ribbon CTA pointing at the | ||
| default path until that value is threaded through to the post editor. | ||
|
Comment on lines
+198
to
+204
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline resources/js/visual-editor/editor/editor-app.tsx --view expanded
ast-grep outline resources/js/visual-editor/editor/editor-canvas.tsx --view expanded
ast-grep outline resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx --view expanded
rg -n -C 5 --glob '*.{ts,tsx}' \
'siteEditorRouteBase|routeBase|buildTemplateDeepLink|ComposedViewRibbon' \
resources/js/visual-editorRepository: ArtisanPack-UI/visual-editor Length of output: 50384 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== editor-app outline =="
ast-grep outline resources/js/visual-editor/editor/editor-app.tsx --view expanded
echo "== editor-canvas relevant lines =="
sed -n '1,220p' resources/js/visual-editor/editor/editor-canvas.tsx
echo "== composed-view-ribbon relevant lines =="
sed -n '1,130p' resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx
echo "== focused call sites for ComposedViewRibbon and route/data propagation =="
rg -n -C 4 --glob '*.{ts,tsx}' 'EditorApp|<EditorApp|EditorAppShell|<EditorAppShell|<ComposedViewRibbon|siteEditorRouteBase|data-route-base|routeBase' resources/js/visual-editor/editor resources/js/visual-editor/site-editor || true
echo "== visual-editor mount / post-editor related files =="
fd -a '.*post.*|.*visual.*|editor-app' resources/js/visual-editor | sed 's#^\./##'Repository: ArtisanPack-UI/visual-editor Length of output: 50384 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== editor main config/readers =="
sed -n '80,180p' resources/js/visual-editor/editor/main.tsx
sed -n '300,460p' resources/js/visual-editor/editor/main.tsx
echo "== EditorCanvas / EditorApp rendering =="
sed -n '365,460p' resources/js/visual-editor/editor/editor-app.tsx
echo "== CanvasChrome / EditorCanvas call sites =="
rg -n -C 3 --glob '*.{ts,tsx}' 'type CanvasChrome|interface CanvasChrome|<EditorCanvas|EditorCanvas\(' resources/js/visual-editor/editor resources/js/visual-editor/site-editor
echo "== routeBase usages in editor/main/editor-app with file names =="
for f in $(rg -l --glob '*.{ts,tsx}' 'routeBase|data-route' resources/js/visual-editor/editor resources/js/visual-editor/site-editor); do
echo "--- $f"
rg -n -C 2 'routeBase|data-route' "$f"
done
echo "== docs limitation text =="
sed -n '190,210p' docs/site-editor.mdRepository: ArtisanPack-UI/visual-editor Length of output: 34656 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
paths = [
Path('resources/js/visual-editor/editor/editor-app.tsx'),
Path('resources/js/visual-editor/editor/editor-canvas.tsx'),
Path('resources/js/visual-editor/editor/composed-view/composed-view- ribbon.tsx'),
Path('resources/js/visual-editor/editor/main.tsx'),
]
for p in paths:
print('path exists', p, p.exists())
files = {
'editor-app.tsx': Path('resources/js/visual-editor/editor/editor-app.tsx').read_text(),
'editor-canvas.tsx': Path('resources/js/visual-editor/editor/editor-canvas.tsx').read_text(),
'composed-view-ribbon.tsx': Path('resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx').read_text(),
'main.tsx': Path('resources/js/visual-editor/editor/main.tsx').read_text(),
}
props = re.findall(r'^export interface EditorAppProps \{(?P<body>.*?)^\}', files['editor-app.tsx'], re.M|re.S)
prop_names = re.findall(r'^\s*(?:export )?([A-Za-z_$][\w$]*)\?', props[0], re.M) if props else []
print('EditorAppProps has apiBase and routeBase?', 'apiBase' in prop_names, 'routeBase' in prop_names)
print('EditorAppProps routeBase mentions data-route-base/config?', any('routeBase' in prop_names and ('data-route' in props[0] or 'route-base' in props[0])) for _ in [1])
print('EditorCanvasProps has routeBase?', 'routeBase' in files['editor-canvas.tsx'])
print('EditorAppShell routeBase parsed/used?', bool(re.search(r'routeBase', files['editor-app.tsx'], re.I)))
print('EditorAppShell EditorCanvas chrome props routeBase?', bool(re.search(r'<EditorCanvas[\s\S]{0,2500}routeBase', files['editor-app.tsx'])))
print('MountConfig has routeBase?', 'routeBase:' in re.search(r'export interface MountConfig \{(?P<body>.*?)\n\}', files['main.tsx'], re.S).group(0))
PYRepository: ArtisanPack-UI/visual-editor Length of output: 496 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
files = {
'editor-app': Path('resources/js/visual-editor/editor/editor-app.tsx').read_text(),
'editor-canvas': Path('resources/js/visual-editor/editor/editor-canvas.tsx').read_text(),
'composed-view-ribbon': Path('resources/js/visual-editor/editor/composed-view/composed-view-ribbon.tsx').read_text(),
'main': Path('resources/js/visual-editor/editor/main.tsx').read_text(),
}
app_props = re.search(r'export interface EditorAppProps \{(?P<body>.*?)\n\}', files['editor-app'], re.S).group('body')
app_props_names = re.findall(r'^\s*([A-Za-z_$][\w$]*)\?', app_props, re.M)
print('EditorAppProps contains routeBase:', 'routeBase' in app_props_names)
print('EditorAppProps contains apiBase:', 'apiBase' in app_props_names)
print('EditorCanvasProps contains routeBase:', bool(re.search(r'\brouteBase\b', files['editor-canvas'])))
print('EditorAppShell EditorCanvas props contain routeBase:', bool(re.search(r'<EditorCanvas(?:[\\s\\S]*?)routeBase', files['editor-app'])))
editor_app_route_mentions = re.findall(r'\brouteBase\b', files['editor-app'], re.I)
print('EditorApp routeBase mentions:', len(editor_app_route_mentions))
mount_config_lines = re.search(r'export interface MountConfig \{(?P<body>.*?)\n\}', files['main'], re.S).group('body')
print('Editor mount MountConfig routeBase:', 'routeBase' in mount_config_lines)
print('Editor mount MountConfig data-route/base text:', 'data-route' in mount_config_lines or 'route-base' in mount_config_lines)
PY
echo "== EditorApp Shell / EditorCanvas call from editor-app =="
sed -n '1420,1475p' resources/js/visual-editor/editor/editor-app.tsxRepository: ArtisanPack-UI/visual-editor Length of output: 2795 Thread the configured route base through the post editor.
🤖 Prompt for AI Agents |
||
|
|
||
| --- | ||
|
|
||
| ## 6. Preview | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { render, screen } from '@testing-library/react'; | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { ComposedViewRibbon } from '../composed-view-ribbon'; | ||
|
|
||
| describe('ComposedViewRibbon', () => { | ||
| it('names the resolved template', () => { | ||
| render( | ||
| <ComposedViewRibbon templateName="Single Post" templateSlug="single" /> | ||
| ); | ||
|
|
||
| expect( | ||
| screen.getByText('Editing content inside Single Post') | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('labels itself as a landmark region for assistive tech', () => { | ||
| render( | ||
| <ComposedViewRibbon templateName="Single Post" templateSlug="single" /> | ||
| ); | ||
|
|
||
| expect( | ||
| screen.getByRole('region', { name: 'Composed-view controls' }) | ||
| ).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('deep-links the CTA at the site editor in a new tab', () => { | ||
| render( | ||
| <ComposedViewRibbon templateName="Single Post" templateSlug="single" /> | ||
| ); | ||
|
|
||
| const cta = screen.getByTestId('ap-composed-view-ribbon-cta'); | ||
|
|
||
| expect(cta).toHaveAttribute( | ||
| 'href', | ||
| '/visual-editor/site?entity=template&slug=single' | ||
| ); | ||
| expect(cta).toHaveAttribute('target', '_blank'); | ||
| expect(cta).toHaveAttribute('rel', 'noopener noreferrer'); | ||
| }); | ||
|
|
||
| it('encodes reserved characters in the slug', () => { | ||
| render( | ||
| <ComposedViewRibbon | ||
| templateName="Odd" | ||
| templateSlug="single/post&more" | ||
| /> | ||
| ); | ||
|
|
||
| expect( | ||
| screen.getByTestId('ap-composed-view-ribbon-cta') | ||
| ).toHaveAttribute( | ||
| 'href', | ||
| '/visual-editor/site?entity=template&slug=single%2Fpost%26more' | ||
| ); | ||
| }); | ||
|
|
||
| it('honours a host-supplied site-editor route base', () => { | ||
| render( | ||
| <ComposedViewRibbon | ||
| templateName="Single Post" | ||
| templateSlug="single" | ||
| siteEditorRouteBase="/admin/site-editor/" | ||
| /> | ||
| ); | ||
|
|
||
| expect( | ||
| screen.getByTestId('ap-composed-view-ribbon-cta') | ||
| ).toHaveAttribute( | ||
| 'href', | ||
| '/admin/site-editor?entity=template&slug=single' | ||
| ); | ||
| }); | ||
|
|
||
| it('hides the CTA on the fallback template, keeping the name', () => { | ||
| render( | ||
| <ComposedViewRibbon | ||
| templateName="Default template" | ||
| templateSlug={null} | ||
| /> | ||
| ); | ||
|
|
||
| expect( | ||
| screen.getByText('Editing content inside Default template') | ||
| ).toBeInTheDocument(); | ||
| expect( | ||
| screen.queryByTestId('ap-composed-view-ribbon-cta') | ||
| ).not.toBeInTheDocument(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /** | ||
| * Composed-view ribbon — #623. | ||
| * | ||
| * Injected into the canvas iframe via `canvas-styles.ts` (`?inline`), | ||
| * NOT imported for side effects by the component: the iframe document | ||
| * sees none of the parent document's Vite-injected stylesheets. | ||
| * | ||
| * Colours resolve from the ArtisanPack UI tokens re-established on the | ||
| * iframe root by `canvas-theme-tokens.css`, with hex fallbacks so the | ||
| * ribbon still reads correctly when the editor is mounted outside a | ||
| * DaisyUI host. | ||
| */ | ||
|
|
||
| .ap-visual-editor__composed-ribbon { | ||
| position: sticky; | ||
| top: 0; | ||
| z-index: 20; | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| align-items: center; | ||
| justify-content: space-between; | ||
| gap: 0.75rem; | ||
| padding: 0.5rem 1rem; | ||
| background-color: var( --color-base-200, #f3f4f6 ); | ||
| border-bottom: 1px solid var( --color-base-300, #e5e7eb ); | ||
| color: var( --color-base-content, #1f2937 ); | ||
| font-size: 0.8125rem; | ||
| line-height: 1.4; | ||
|
|
||
| /* The ribbon is editor chrome that happens to live in the canvas — | ||
| selecting its text on the way to selecting a block is never what | ||
| the author meant. */ | ||
| user-select: none; | ||
| } | ||
|
|
||
| .ap-visual-editor__composed-ribbon-label { | ||
| font-weight: 500; | ||
| } | ||
|
|
||
| .ap-visual-editor__composed-ribbon-cta { | ||
| flex-shrink: 0; | ||
| padding: 0.25rem 0.625rem; | ||
| border: 1px solid var( --color-base-300, #e5e7eb ); | ||
| border-radius: 0.25rem; | ||
| background-color: var( --color-base-100, #ffffff ); | ||
| color: var( --color-primary, #2563eb ); | ||
| font-weight: 600; | ||
| text-decoration: none; | ||
| white-space: nowrap; | ||
| } | ||
|
|
||
| .ap-visual-editor__composed-ribbon-cta:hover, | ||
| .ap-visual-editor__composed-ribbon-cta:focus-visible { | ||
| text-decoration: underline; | ||
| } | ||
|
|
||
| .ap-visual-editor__composed-ribbon-cta:focus-visible { | ||
| outline: var( --wp-admin-border-width-focus, 2px ) solid | ||
| var( --color-primary, #2563eb ); | ||
| outline-offset: 1px; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use one identifier model for path routes.
Lines 158-171 define path routes as database-ID routes and show
/visual-editor/site/templates/12. Lines 146-150 show slug-like values such assingle. Update the examples or the contract so both describe the same route identifier.🤖 Prompt for AI Agents