From 409ec1c404b63d0d3ad5cac2f8d313fbc4a2945b Mon Sep 17 00:00:00 2001 From: Juan Lentino <74268474+juanlentino@users.noreply.github.com> Date: Fri, 25 Sep 2026 12:24:24 -0400 Subject: [PATCH] fix(widget): register the recent-entries widget in window.openStationWidgets OpenStation looks up mount callbacks by id in window.openStationWidgets (window.desktopModeWidgets on older Desktop Mode builds). The widget only exported render on window.allTerrainFormsWidget, so the shell never found it and logged a mount failure on every desktop load. Fixes #59 --- assets/js/widget.js | 4 ++++ assets/js/widget.min.js | 2 +- src/widget.ts | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/assets/js/widget.js b/assets/js/widget.js index f7455c8..699c5e6 100644 --- a/assets/js/widget.js +++ b/assets/js/widget.js @@ -284,6 +284,10 @@ var allTerrainFormsWidget = function(exports) { unsubscribe?.(); }; } + const registryHost = window; + const registry = registryHost.openStationWidgets || registryHost.desktopModeWidgets || {}; + registry["allterrain-forms/recent"] = renderWidget; + registryHost.openStationWidgets = registryHost.desktopModeWidgets = registry; function mountStandalone() { document.querySelectorAll("[data-atfw-root]").forEach((host) => { if (host.dataset.atfwMounted) { diff --git a/assets/js/widget.min.js b/assets/js/widget.min.js index 04c97e9..c6e5346 100644 --- a/assets/js/widget.min.js +++ b/assets/js/widget.min.js @@ -1 +1 @@ -var allTerrainFormsWidget=function(p){"use strict";function S(){return window.wp?.os??null}const d=window.allTerrainForms;class w extends Error{constructor(t,r,n="",s){super(t),this.name="ApiError",this.status=r,this.code=n,this.data=s}}function E(e,t){return e.includes("?")?`${e}${t.replace("?","&")}`:`${e}${t}`}async function u(e,t={}){if(!d?.restUrl)throw new w("AllTerrain Forms is not configured on this page.",0);const r=E(d.restUrl,e),n={"Content-Type":"application/json",...t.headers??{}};d.nonce&&(n["X-WP-Nonce"]=d.nonce);const s=S(),i=await(s?.fetch?(f,h)=>s.fetch(f,h,{source:"allterrain-forms"}):(f,h)=>fetch(f,h))(r,{credentials:"same-origin",...t,headers:n});if(!i.ok){let f=`Request failed with status ${i.status}.`,h="",x;try{const b=await i.json();f=b.message??f,h=b.code??"",x=b.data}catch{}throw new w(f,i.status,h,x)}if(i.status!==204)return await i.json()}const o=e=>u(e);async function j(e){if(!d?.wpRestUrl)throw new w("AllTerrain Forms is not configured on this page.",0);const t=d.nonce?{"X-WP-Nonce":d.nonce}:{},r=S(),n=E(d.wpRestUrl,e),s=r?.fetch?await r.fetch(n,{credentials:"same-origin",headers:t},{source:"allterrain-forms"}):await fetch(n,{credentials:"same-origin",headers:t});if(!s.ok)throw new w(`Request failed with status ${s.status}.`,s.status);return await s.json()}function R(e){if(!e||!e.includes("&"))return e;const t=document.createElement("textarea");return t.innerHTML=e,t.value}const a=(e,t)=>u(e,{method:"POST",body:JSON.stringify(t)}),g=e=>u(e,{method:"DELETE"});function F(e){const t=new URLSearchParams;for(const[n,s]of Object.entries(e))s===void 0||s===""||s===!1||t.set(n,String(s));const r=t.toString();return r?`?${r}`:""}function m(e){const t=e?.schema?.settings;return t&&(!t.themeOverrides||Array.isArray(t.themeOverrides))&&(t.themeOverrides={...t.themeOverrides}),e}const O={assistantRevision:(e,t)=>u(`/assistant/forms/${e}`,{signal:t}),assistantValidate:(e,t)=>u("/assistant/validate",{method:"POST",body:JSON.stringify({draft:e}),signal:t}),assistantApply:(e,t,r,n,s)=>u("/assistant/apply",{method:"POST",body:JSON.stringify({draft:e,formId:t,revision:r,operationKey:s}),signal:n}).then(l=>({...m(l),operation:l.operation})),assistantOperation:(e,t)=>u(`/assistant/operations/${encodeURIComponent(e)}`,{signal:t}),config:()=>o("/config"),mailpoet:()=>o("/mailpoet"),listForms:()=>o("/forms"),listArchivedForms:()=>o("/forms?archived=1"),archiveForm:e=>a(`/forms/${e}/archive`,{}),unarchiveForm:e=>a(`/forms/${e}/unarchive`,{}),getForm:(e,t)=>u(`/forms/${e}`,{signal:t}).then(m),createForm:e=>a("/forms",e).then(m),updateForm:(e,t)=>a(`/forms/${e}`,t).then(m),exportForm:(e,t)=>a(`/forms/${e}/export`,t),validateFormPackage:e=>a("/form-packages/validate",{package:e}),importFormPackage:e=>a("/form-packages/import",{package:e}).then(t=>({...m(t),importWarnings:t.importWarnings})),duplicateForm:e=>a(`/forms/${e}/duplicate`,{}).then(m),deleteForm:e=>g(`/forms/${e}`),preview:(e,t)=>a(`/forms/${e}/preview`,t),pages:()=>j("wp/v2/pages?per_page=100&status=publish&orderby=title&order=asc&_fields=id,title").then(e=>e.map(t=>({id:t.id,title:R(t.title?.rendered??"")||`#${t.id}`}))),mergeTags:e=>o(`/forms/${e}/merge-tags`).then(t=>t.groups),analytics:(e,t="")=>o(`/forms/${e}/analytics${t?`?dimension=${encodeURIComponent(t)}`:""}`),demoStatus:()=>o("/demo"),demoSeed:e=>a("/demo",e?{count:e}:{}),demoRemove:()=>g("/demo"),listEntries:e=>o(`/entries${F(e)}`),getEntry:e=>o(`/entries/${e}`),updateEntry:(e,t)=>a(`/entries/${e}`,t),deleteEntry:e=>g(`/entries/${e}`),exportEntries:e=>o(`/entries/export${F(e)}`),listThemes:()=>o("/themes"),saveTheme:e=>a("/themes",e),deleteTheme:e=>g(`/themes/${e}`)};function c(e,t={}){const r=document.createElement(e);t.class&&(r.className=t.class),t.text!==void 0&&(r.textContent=t.text),t.html!==void 0&&(r.innerHTML=t.html),t.title&&(r.title=t.title),t.type&&"type"in r&&(r.type=t.type),t.value!==void 0&&"value"in r&&(r.value=t.value),t.placeholder!==void 0&&"placeholder"in r&&(r.placeholder=t.placeholder),t.href&&"href"in r&&(r.href=t.href);for(const[n,s]of Object.entries(t.attrs??{}))s===void 0||s===!1||r.setAttribute(n,s===!0?"":String(s));Object.assign(r.style,t.style??{});for(const[n,s]of Object.entries(t.on??{}))r.addEventListener(n,s);for(const n of t.children??[])n==null||n===!1||r.append(n);return r}function v(e){e.replaceChildren()}const A=12e4;async function y(e){try{const t=await O.listForms();if(!t.length){v(e),e.append(c("p",{class:"atfw__empty",text:"No forms yet."}));return}const{entries:r}=await O.listEntries({per_page:8});v(e),e.append(P(t),k(r))}catch(t){v(e),e.append(c("p",{class:"atfw__empty",text:t instanceof Error?t.message:"Could not load submissions."}))}}function P(e){const t=e.reduce((l,i)=>l+i.submissions,0),r=e.reduce((l,i)=>l+i.views,0),n=e.reduce((l,i)=>l+i.unread,0),s=r>0?Math.floor(t/r*100):0;return c("div",{class:"atfw__summary",children:[$(String(t),"submissions"),$(`${s}%`,"converted"),$(String(n),"unread")]})}function $(e,t){return c("div",{class:"atfw__stat",children:[c("strong",{class:"atfw__stat-value",text:e}),c("span",{class:"atfw__stat-label",text:t})]})}function k(e){return e.length?c("ul",{class:"atfw__list",children:e.map(t=>c("li",{class:`atfw__item${t.status==="alltfo-unread"?" is-unread":""}`,children:[c("span",{class:"atfw__title",text:t.title}),c("span",{class:"atfw__meta",text:`${t.formTitle} · ${t.dateHuman}`})]}))}):c("p",{class:"atfw__empty",text:"Nothing submitted yet."})}function _(e){e.classList.add("atfw"),y(e);const t=window.setInterval(()=>void y(e),A),n=window.wp?.os?.subscribe?.("os.alltfo_entry.changed",()=>void y(e));return()=>{window.clearInterval(t),n?.()}}function T(){document.querySelectorAll("[data-atfw-root]").forEach(e=>{e.dataset.atfwMounted||(e.dataset.atfwMounted="1",_(e))})}return document.readyState==="loading"?document.addEventListener("DOMContentLoaded",T):T(),p.render=_,p.renderWidget=_,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}),p}({}); +var allTerrainFormsWidget=function(p){"use strict";function E(){return window.wp?.os??null}const l=window.allTerrainForms;class g extends Error{constructor(t,s,r="",n){super(t),this.name="ApiError",this.status=s,this.code=r,this.data=n}}function F(e,t){return e.includes("?")?`${e}${t.replace("?","&")}`:`${e}${t}`}async function u(e,t={}){if(!l?.restUrl)throw new g("AllTerrain Forms is not configured on this page.",0);const s=F(l.restUrl,e),r={"Content-Type":"application/json",...t.headers??{}};l.nonce&&(r["X-WP-Nonce"]=l.nonce);const n=E(),i=await(n?.fetch?(f,h)=>n.fetch(f,h,{source:"allterrain-forms"}):(f,h)=>fetch(f,h))(s,{credentials:"same-origin",...t,headers:r});if(!i.ok){let f=`Request failed with status ${i.status}.`,h="",k;try{const b=await i.json();f=b.message??f,h=b.code??"",k=b.data}catch{}throw new g(f,i.status,h,k)}if(i.status!==204)return await i.json()}const o=e=>u(e);async function R(e){if(!l?.wpRestUrl)throw new g("AllTerrain Forms is not configured on this page.",0);const t=l.nonce?{"X-WP-Nonce":l.nonce}:{},s=E(),r=F(l.wpRestUrl,e),n=s?.fetch?await s.fetch(r,{credentials:"same-origin",headers:t},{source:"allterrain-forms"}):await fetch(r,{credentials:"same-origin",headers:t});if(!n.ok)throw new g(`Request failed with status ${n.status}.`,n.status);return await n.json()}function W(e){if(!e||!e.includes("&"))return e;const t=document.createElement("textarea");return t.innerHTML=e,t.value}const a=(e,t)=>u(e,{method:"POST",body:JSON.stringify(t)}),w=e=>u(e,{method:"DELETE"});function O(e){const t=new URLSearchParams;for(const[r,n]of Object.entries(e))n===void 0||n===""||n===!1||t.set(r,String(n));const s=t.toString();return s?`?${s}`:""}function m(e){const t=e?.schema?.settings;return t&&(!t.themeOverrides||Array.isArray(t.themeOverrides))&&(t.themeOverrides={...t.themeOverrides}),e}const T={assistantRevision:(e,t)=>u(`/assistant/forms/${e}`,{signal:t}),assistantValidate:(e,t)=>u("/assistant/validate",{method:"POST",body:JSON.stringify({draft:e}),signal:t}),assistantApply:(e,t,s,r,n)=>u("/assistant/apply",{method:"POST",body:JSON.stringify({draft:e,formId:t,revision:s,operationKey:n}),signal:r}).then(d=>({...m(d),operation:d.operation})),assistantOperation:(e,t)=>u(`/assistant/operations/${encodeURIComponent(e)}`,{signal:t}),config:()=>o("/config"),mailpoet:()=>o("/mailpoet"),listForms:()=>o("/forms"),listArchivedForms:()=>o("/forms?archived=1"),archiveForm:e=>a(`/forms/${e}/archive`,{}),unarchiveForm:e=>a(`/forms/${e}/unarchive`,{}),getForm:(e,t)=>u(`/forms/${e}`,{signal:t}).then(m),createForm:e=>a("/forms",e).then(m),updateForm:(e,t)=>a(`/forms/${e}`,t).then(m),exportForm:(e,t)=>a(`/forms/${e}/export`,t),validateFormPackage:e=>a("/form-packages/validate",{package:e}),importFormPackage:e=>a("/form-packages/import",{package:e}).then(t=>({...m(t),importWarnings:t.importWarnings})),duplicateForm:e=>a(`/forms/${e}/duplicate`,{}).then(m),deleteForm:e=>w(`/forms/${e}`),preview:(e,t)=>a(`/forms/${e}/preview`,t),pages:()=>R("wp/v2/pages?per_page=100&status=publish&orderby=title&order=asc&_fields=id,title").then(e=>e.map(t=>({id:t.id,title:W(t.title?.rendered??"")||`#${t.id}`}))),mergeTags:e=>o(`/forms/${e}/merge-tags`).then(t=>t.groups),analytics:(e,t="")=>o(`/forms/${e}/analytics${t?`?dimension=${encodeURIComponent(t)}`:""}`),demoStatus:()=>o("/demo"),demoSeed:e=>a("/demo",e?{count:e}:{}),demoRemove:()=>w("/demo"),listEntries:e=>o(`/entries${O(e)}`),getEntry:e=>o(`/entries/${e}`),updateEntry:(e,t)=>a(`/entries/${e}`,t),deleteEntry:e=>w(`/entries/${e}`),exportEntries:e=>o(`/entries/export${O(e)}`),listThemes:()=>o("/themes"),saveTheme:e=>a("/themes",e),deleteTheme:e=>w(`/themes/${e}`)};function c(e,t={}){const s=document.createElement(e);t.class&&(s.className=t.class),t.text!==void 0&&(s.textContent=t.text),t.html!==void 0&&(s.innerHTML=t.html),t.title&&(s.title=t.title),t.type&&"type"in s&&(s.type=t.type),t.value!==void 0&&"value"in s&&(s.value=t.value),t.placeholder!==void 0&&"placeholder"in s&&(s.placeholder=t.placeholder),t.href&&"href"in s&&(s.href=t.href);for(const[r,n]of Object.entries(t.attrs??{}))n===void 0||n===!1||s.setAttribute(r,n===!0?"":String(n));Object.assign(s.style,t.style??{});for(const[r,n]of Object.entries(t.on??{}))s.addEventListener(r,n);for(const r of t.children??[])r==null||r===!1||s.append(r);return s}function $(e){e.replaceChildren()}const A=12e4;async function _(e){try{const t=await T.listForms();if(!t.length){$(e),e.append(c("p",{class:"atfw__empty",text:"No forms yet."}));return}const{entries:s}=await T.listEntries({per_page:8});$(e),e.append(M(t),P(s))}catch(t){$(e),e.append(c("p",{class:"atfw__empty",text:t instanceof Error?t.message:"Could not load submissions."}))}}function M(e){const t=e.reduce((d,i)=>d+i.submissions,0),s=e.reduce((d,i)=>d+i.views,0),r=e.reduce((d,i)=>d+i.unread,0),n=s>0?Math.floor(t/s*100):0;return c("div",{class:"atfw__summary",children:[S(String(t),"submissions"),S(`${n}%`,"converted"),S(String(r),"unread")]})}function S(e,t){return c("div",{class:"atfw__stat",children:[c("strong",{class:"atfw__stat-value",text:e}),c("span",{class:"atfw__stat-label",text:t})]})}function P(e){return e.length?c("ul",{class:"atfw__list",children:e.map(t=>c("li",{class:`atfw__item${t.status==="alltfo-unread"?" is-unread":""}`,children:[c("span",{class:"atfw__title",text:t.title}),c("span",{class:"atfw__meta",text:`${t.formTitle} · ${t.dateHuman}`})]}))}):c("p",{class:"atfw__empty",text:"Nothing submitted yet."})}function v(e){e.classList.add("atfw"),_(e);const t=window.setInterval(()=>void _(e),A),r=window.wp?.os?.subscribe?.("os.alltfo_entry.changed",()=>void _(e));return()=>{window.clearInterval(t),r?.()}}const y=window,x=y.openStationWidgets||y.desktopModeWidgets||{};x["allterrain-forms/recent"]=v,y.openStationWidgets=y.desktopModeWidgets=x;function j(){document.querySelectorAll("[data-atfw-root]").forEach(e=>{e.dataset.atfwMounted||(e.dataset.atfwMounted="1",v(e))})}return document.readyState==="loading"?document.addEventListener("DOMContentLoaded",j):j(),p.render=v,p.renderWidget=v,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}),p}({}); diff --git a/src/widget.ts b/src/widget.ts index 05b8167..c5032a1 100644 --- a/src/widget.ts +++ b/src/widget.ts @@ -135,12 +135,22 @@ export function renderWidget( host: HTMLElement ): () => void { * overwritten a moment later by the bundle itself, which is a bug that only * shows up in the built file and never in a test. * - * So the contract is expressed as an export instead: the shell finds - * `window.allTerrainFormsWidget.render` because `render` is what this module - * exports. + * The export stays for anything that reads that global. The shell itself does + * not: OpenStation looks up mount callbacks by widget id in + * `window.openStationWidgets` (`window.desktopModeWidgets` on older Desktop + * Mode builds), so the callback is registered there too. That is a different + * global from the one the IIFE overwrites, so assigning it here is safe. */ export { renderWidget as render }; +type WidgetRegistry = Record< string, ( host: HTMLElement ) => ( () => void ) | void >; + +// Merge, never replace: other plugins' widgets live in the same object. +const registryHost = window as unknown as { openStationWidgets?: WidgetRegistry; desktopModeWidgets?: WidgetRegistry }; +const registry: WidgetRegistry = registryHost.openStationWidgets || registryHost.desktopModeWidgets || {}; +registry[ 'allterrain-forms/recent' ] = renderWidget; +registryHost.openStationWidgets = registryHost.desktopModeWidgets = registry; + /** * Mounts into a plain container, for the admin page. *