diff --git a/assets/js/dock.js b/assets/js/dock.js index acd9067..0de9010 100644 --- a/assets/js/dock.js +++ b/assets/js/dock.js @@ -48,6 +48,12 @@ var allTerrainFormsDock = function(exports) { windowId: THEMES }); } + if (config2?.canEdit && config2?.adminUrl) { + submenu.push({ + title: "Import forms", + url: `${config2.adminUrl}admin.php?page=allterrain-forms-import` + }); + } if (config2?.canEdit && config2?.devMode) { submenu.push({ title: "Demo data", diff --git a/assets/js/dock.min.js b/assets/js/dock.min.js index 96867d6..0b1c417 100644 --- a/assets/js/dock.min.js +++ b/assets/js/dock.min.js @@ -1 +1 @@ -var allTerrainFormsDock=function(s){"use strict";const u=window.allTerrainForms,r="allterrain-forms",o="allterrain-forms-entries",a="allterrain-forms-themes",i="allterrain-forms-analytics";function t(e){l()?.openWindow?.(e,{source:"dock"})}function l(){return window.wp?.os??null}function c(e){const n=[];return e?.canEdit&&n.push({title:"Forms",url:"",onSelect:()=>t(r),windowId:r}),e?.canRead&&n.push({title:"Form entries",url:"",onSelect:()=>t(o),windowId:o}),e?.canRead&&n.push({title:"Analytics",url:"",onSelect:()=>t(i),windowId:i}),e?.canEdit&&n.push({title:"Themes",url:"",onSelect:()=>t(a),windowId:a}),e?.canEdit&&e?.devMode&&n.push({title:"Demo data",url:"",onSelect:()=>{t(i),document.dispatchEvent(new CustomEvent("atf-open-demo-panel"))},windowId:i}),n}function d(){const e=l();if(!e?.registerSystemTile)return;const n=c(u);try{e.registerSystemTile({id:"allterrain-forms",title:"AllTerrain Forms",icon:"dashicons-feedback",order:5,onOpen:()=>t(u?.canEdit?r:o),isOpen:()=>!!(e.windowManager?.getById?.(r)||e.windowManager?.getById?.(o)||e.windowManager?.getById?.(a)),submenu:n})}catch{}}function m(){const e=l();return e?.ready?(e.ready(d),!0):e?.whenReady?(e.whenReady(d),!0):e?.registerSystemTile?(d(),!0):!1}return m()||document.addEventListener("os-init",()=>void m(),{once:!0}),s.submenuFor=c,Object.defineProperty(s,Symbol.toStringTag,{value:"Module"}),s}({}); +var allTerrainFormsDock=function(a){"use strict";const u=window.allTerrainForms,r="allterrain-forms",i="allterrain-forms-entries",s="allterrain-forms-themes",o="allterrain-forms-analytics";function t(e){l()?.openWindow?.(e,{source:"dock"})}function l(){return window.wp?.os??null}function c(e){const n=[];return e?.canEdit&&n.push({title:"Forms",url:"",onSelect:()=>t(r),windowId:r}),e?.canRead&&n.push({title:"Form entries",url:"",onSelect:()=>t(i),windowId:i}),e?.canRead&&n.push({title:"Analytics",url:"",onSelect:()=>t(o),windowId:o}),e?.canEdit&&n.push({title:"Themes",url:"",onSelect:()=>t(s),windowId:s}),e?.canEdit&&e?.adminUrl&&n.push({title:"Import forms",url:`${e.adminUrl}admin.php?page=allterrain-forms-import`}),e?.canEdit&&e?.devMode&&n.push({title:"Demo data",url:"",onSelect:()=>{t(o),document.dispatchEvent(new CustomEvent("atf-open-demo-panel"))},windowId:o}),n}function d(){const e=l();if(!e?.registerSystemTile)return;const n=c(u);try{e.registerSystemTile({id:"allterrain-forms",title:"AllTerrain Forms",icon:"dashicons-feedback",order:5,onOpen:()=>t(u?.canEdit?r:i),isOpen:()=>!!(e.windowManager?.getById?.(r)||e.windowManager?.getById?.(i)||e.windowManager?.getById?.(s)),submenu:n})}catch{}}function m(){const e=l();return e?.ready?(e.ready(d),!0):e?.whenReady?(e.whenReady(d),!0):e?.registerSystemTile?(d(),!0):!1}return m()||document.addEventListener("os-init",()=>void m(),{once:!0}),a.submenuFor=c,Object.defineProperty(a,Symbol.toStringTag,{value:"Module"}),a}({}); diff --git a/docs/architecture.md b/docs/architecture.md index be4bbef..9505885 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -232,6 +232,19 @@ can be read aloud, selected and searched; resizing is a reflow rather than a redraw; and the plugin ships no charting dependency to every site that installs it. +### Import, the one page with no native window + +**Forms → Import** is deliberately not a native window. It is a server-rendered +page whose buttons `POST` to `admin-post.php`, which is the right shape for a +one-time migration and the wrong shape for a window rendering into the shell's +own DOM. With the shell up it is reached instead through the dock tile's +`Import forms` row — the one row carrying a real `url`, which the shell opens +as a window of its own. + +That row is load-bearing rather than convenient. With the shell active the +Forms admin menu is not registered at all (see below), so a page with neither a +native window nor a dock row exists at a URL nobody can navigate to. + ### The admin-URL handoff Every surface — builder, entries, analytics, Theme Studio — also has an admin diff --git a/src/dock.ts b/src/dock.ts index c45e2b6..bced9d7 100644 --- a/src/dock.ts +++ b/src/dock.ts @@ -42,6 +42,7 @@ interface RuntimeConfig { canEdit?: boolean; canRead?: boolean; devMode?: boolean; + adminUrl?: string; } const config: RuntimeConfig | undefined = ( window as unknown as { allTerrainForms?: RuntimeConfig } ).allTerrainForms; @@ -130,6 +131,19 @@ export function submenuFor( config: RuntimeConfig | undefined ): SubmenuRow[] { } ); } + // The one row that is a URL rather than a native window, because the import + // page is one: a server-rendered page whose buttons POST to `admin-post.php`. + // The shell opens a row with a real `url` as a window of its own, which is + // the only way this page is reachable on a desktop — with the shell up, the + // Forms admin menu is not registered, so a page with no native window and no + // row here exists at a URL nobody can get to. + if ( config?.canEdit && config?.adminUrl ) { + submenu.push( { + title: 'Import forms', + url: `${ config.adminUrl }admin.php?page=allterrain-forms-import`, + } ); + } + // Developer mode only, and last — it writes several hundred entries into the // database, which is not something to leave one hover away from "Themes" on a // site that is collecting real enquiries. diff --git a/tests/vitest/dock.test.ts b/tests/vitest/dock.test.ts index 364fa80..0f7d9c4 100644 --- a/tests/vitest/dock.test.ts +++ b/tests/vitest/dock.test.ts @@ -98,3 +98,30 @@ describe( 'the rest of the menu', () => { expect( submenuFor( config( {} ) ) ).toEqual( [] ); } ); } ); + +describe( 'the import row', () => { + const admin = { canEdit: true, canRead: true, adminUrl: 'http://example.com/wp-admin/' }; + + it( 'is offered to somebody who may build forms', () => { + expect( titles( admin ) ).toContain( 'Import forms' ); + } ); + + it( 'carries a real URL rather than a callback', () => { + // The import page is a server-rendered page whose buttons POST, not a + // native window — the shell opens a row with a `url` as its own window, + // and that is the only way the page is reachable with the shell up. + const row = submenuFor( config( admin ) ).find( ( candidate ) => candidate.title === 'Import forms' ); + + expect( row?.url ).toBe( 'http://example.com/wp-admin/admin.php?page=allterrain-forms-import' ); + expect( row?.onSelect ).toBeUndefined(); + } ); + + it( 'is absent for somebody who may only read entries', () => { + expect( titles( { canRead: true, adminUrl: 'http://example.com/wp-admin/' } ) ).not.toContain( 'Import forms' ); + } ); + + it( 'is absent when there is no admin URL to point at', () => { + // Rather than a row that opens `undefinedadmin.php`. + expect( titles( { canEdit: true, canRead: true } ) ).not.toContain( 'Import forms' ); + } ); +} );