From 8bd283436def29884cbb1b33bc9a38b4362a27db Mon Sep 17 00:00:00 2001 From: fahimreza-dev Date: Mon, 24 Aug 2026 21:43:06 +0600 Subject: [PATCH] fix(admin): show the native Pro SEO integrations on the Integrations screen KNOWN gains Yoast SEO / Rank Math / AIOSEO display entries, and row keys now try the two-segment prefix against KNOWN first, so rank-math-* files under 'Rank Math' instead of a row named 'rank'. Rows only appear when the plugin is really present: Saddle Pro gates its saddle_integration_ui_prefixes hook on detection, so this stays an honest 'Detected integrations' list. Tracked admin bundle rebuilt. Closes #143 --- admin/build/index.asset.php | 2 +- admin/build/index.js | 2 +- admin/src/components/Integrations.jsx | 33 +++++++++++++++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/admin/build/index.asset.php b/admin/build/index.asset.php index 0ae337a..3e82ceb 100644 --- a/admin/build/index.asset.php +++ b/admin/build/index.asset.php @@ -1 +1 @@ - array('react', 'react-dom', 'wp-api-fetch', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '5c98d4553b8e40a8ff82'); + array('react', 'react-dom', 'wp-api-fetch', 'wp-element', 'wp-hooks', 'wp-i18n'), 'version' => '1b462813058d33cb12bb'); diff --git a/admin/build/index.js b/admin/build/index.js index c1b1912..e4b3abc 100644 --- a/admin/build/index.js +++ b/admin/build/index.js @@ -26,7 +26,7 @@ (0,Zp.__)("acting as %s","saddle"),e.user_login)),actions:(0,a.createElement)(a.Fragment,null,(0,a.createElement)(mu,{tone:e.verified?void 0:"warning"},e.verified?(0,Zp.__)("Verified","saddle"):(0,Zp.__)("Unverified","saddle")),(0,a.createElement)(Jd,{variant:"ghost",size:"sm",onClick:()=>(async e=>{await c({title:(0,Zp.sprintf)(/* translators: %s: the app name. */ /* translators: %s: the app name. */ (0,Zp.__)("Disconnect “%s”?","saddle"),e.name),description:(0,Zp.__)("It loses access the moment you confirm — no waiting for anything to expire. To use it again you’ll approve it once more.","saddle"),danger:!0,confirmLabel:(0,Zp.__)("Disconnect","saddle"),cancelLabel:(0,Zp.__)("Keep it connected","saddle")})&&(y(t=>t.filter(t=>t.id!==e.id)),Up(`oauth-connections/${e.id}`,{method:"DELETE"}).then(v).catch(e=>{ro.error(e.message),v()}))})(e)},(0,Zp.__)("Disconnect","saddle")))})))),e.length>0&&window.saddleData?.profileUrl&&(0,a.createElement)("p",{className:"saddle-apps__cardfoot"},(0,a.createElement)("a",{href:window.saddleData.profileUrl},(0,Zp.__)("Manage these keys in WordPress","saddle")),(0,a.createElement)(Bu,null,(0,Zp.__)("These keys also appear under Users → Profile → Application Passwords — the same keys, and either place works for revoking.","saddle"))))),(0,a.createElement)(ju,{className:"saddle-apps__more",open:d,onOpenChange:u,trigger:(0,Zp.__)("Connection details & health","saddle")},(0,a.createElement)("div",{className:"saddle-apps__advanced"},(0,a.createElement)("div",{className:"saddle-apps__field"},(0,a.createElement)("span",{className:"saddle-apps__fieldlabel"},(0,Zp.__)("Your site’s address for AI apps","saddle")),(0,a.createElement)(Hu,{value:Om})),(0,a.createElement)("div",{className:"saddle-apps__test"},(0,a.createElement)(Jd,{variant:"secondary",onClick:()=>{m({state:"running"});const e=window.performance?window.performance.now():0,t="application/json, text/event-stream",n=()=>window.performance?Math.round(window.performance.now()-e):null;o()({url:Om,method:"POST",parse:!1,headers:{Accept:t},data:{jsonrpc:"2.0",id:0,method:"initialize",params:{protocolVersion:"2025-11-25",capabilities:{},clientInfo:{name:"Saddle Admin",version:"1"}}}}).then(e=>{const t=e.headers.get("Mcp-Session-Id");return e.json().then(e=>({sid:t,init:e}))}).then(({sid:e,init:a})=>{if(a&&a.error)throw new Error(a.error.message);if(e)return o()({url:Om,method:"POST",headers:{Accept:t,"Mcp-Session-Id":e},data:{jsonrpc:"2.0",id:1,method:"tools/list"}}).then(e=>{if(e&&e.error)throw new Error(e.error.message);const t=e&&e.result&&Array.isArray(e.result.tools)?e.result.tools.length:null;m({state:"ok",count:t,ms:n()})});m({state:"ok",count:null,ms:n()})}).catch(e=>m({state:"error",message:e.message}))},loading:p&&"running"===p.state,disabled:p&&"running"===p.state},(0,Zp.__)("Test the endpoint","saddle")),p&&"ok"===p.state&&(0,a.createElement)(mu,{tone:"success"},null!==p.count?(0,Zp.sprintf)(/* translators: 1: tool count, 2: milliseconds. */ /* translators: 1: tool count, 2: milliseconds. */ (0,Zp.__)("%1$d tools · %2$dms","saddle"),p.count,p.ms):`${(0,Zp.__)("Responding","saddle")} · ${p.ms}ms`),p&&"error"===p.state&&(0,a.createElement)(mu,{tone:"danger"},p.message)),(0,a.createElement)(km,null),(0,a.createElement)("p",{className:"saddle-apps__transport"},(0,a.createElement)("span",null,(0,Zp.__)("Transport","saddle")),(0,a.createElement)(Bu,null,Wp.adapter?(0,Zp.__)("Saddle is using the MCP Adapter plugin, which is active on this site. Your tools, access levels and approvals are unchanged — this only affects how requests are carried.","saddle"):(0,Zp.__)("Saddle speaks MCP itself, so there is nothing else to install. If you ever add the separate MCP Adapter plugin, Saddle will use it automatically; your tools, access levels and approvals stay the same either way.","saddle"))),(0,a.createElement)(Em,null))),h&&(0,a.createElement)(Pm,{open:!!h,onOpenChange:e=>!e&&f(null),app:h.app,label:h.label,password:h.password}))}function Um(){const e=tp(),[t,n]=(0,i.useState)(null),[r,s]=(0,i.useState)(""),[o,l]=(0,i.useState)(!1),[c,d]=(0,i.useState)(!1);(0,i.useEffect)(()=>{Up("preferences").then(e=>n(e.unsplash||{configured:!1,key_hint:""})).catch(()=>n({configured:!1,key_hint:""}))},[]);const u=e=>{d(!0),Up("preferences",{method:"POST",data:{unsplash_access_key:e}}).then(t=>{n(t.unsplash||{configured:!1,key_hint:""}),s(""),l(!1),ro.success(e?(0,Zp.__)("Unsplash key saved.","saddle"):(0,Zp.__)("Unsplash key removed.","saddle"))}).catch(e=>ro.error(e.message)).finally(()=>d(!1))};if(!t)return null;const p=!t.configured||o;return(0,a.createElement)(iu,{className:"saddle-unsplash"},(0,a.createElement)(lu,{title:(0,Zp.__)("Unsplash stock photos","saddle"),description:(0,Zp.__)("Lets your AI search Unsplash and import photos into the media library, with photographer credit added automatically.","saddle")}),(0,a.createElement)(cu,null,p?(0,a.createElement)(a.Fragment,null,(0,a.createElement)(eu,{label:(0,Zp.__)("Access Key","saddle")},e=>(0,a.createElement)(tu,{...e,type:"password",value:r,onChange:e=>s(e.target.value),placeholder:(0,Zp.__)("Paste your Unsplash Access Key…","saddle"),autoComplete:"off"})),(0,a.createElement)("p",{className:"saddle-unsplash__hint"},(0,Zp.__)("Free from unsplash.com/developers (the Access Key, not the Secret Key). It is stored only on this site and sent only to Unsplash — never anywhere else.","saddle")),(0,a.createElement)("div",{className:"saddle-unsplash__actions"},(0,a.createElement)(Jd,{variant:"secondary",onClick:()=>u(r.trim()),loading:c,disabled:c||!r.trim()},(0,Zp.__)("Save key","saddle")),o&&(0,a.createElement)(Jd,{variant:"ghost",onClick:()=>{l(!1),s("")},disabled:c},(0,Zp.__)("Cancel","saddle")))):(0,a.createElement)("div",{className:"saddle-unsplash__configured"},(0,a.createElement)("span",null,(0,Zp.sprintf)(/* translators: %s: last four characters of the key. */ /* translators: %s: last four characters of the key. */ -(0,Zp.__)("Configured — key ends in ••••%s.","saddle"),t.key_hint)),(0,a.createElement)("div",{className:"saddle-unsplash__actions"},(0,a.createElement)(Jd,{variant:"ghost",onClick:()=>l(!0),disabled:c},(0,Zp.__)("Replace","saddle")),(0,a.createElement)(Jd,{variant:"ghost",onClick:async()=>{await e({title:(0,Zp.__)("Remove the Unsplash key?","saddle"),description:(0,Zp.__)("Your AI loses stock-photo search and import until a new key is added. Photos already in the media library are kept.","saddle"),danger:!0,confirmLabel:(0,Zp.__)("Remove","saddle"),cancelLabel:(0,Zp.__)("Cancel","saddle")})&&u("")},loading:c,disabled:c},(0,Zp.__)("Remove","saddle"))))))}const Fm={waggle:{title:(0,Zp.__)("Waggle","saddle"),description:(0,Zp.__)("SEO & AEO tools from the Waggle plugin, wrapped with Saddle’s safety model.","saddle")},knovia:{title:(0,Zp.__)("Knovia","saddle"),description:(0,Zp.__)("Documentation tools from the Knovia plugin, wrapped with Saddle’s safety model.","saddle")},unsplash:{title:(0,Zp.__)("Unsplash","saddle"),description:(0,Zp.__)("Stock-photo search and import, built into Saddle. Needs the Access Key above.","saddle")}};function Vm({caps:e}){const t=(e=>{const t=new Map;return e.forEach(e=>{if("Integrations"!==e.category)return;const n=(e.short||"").split("-")[0];n&&t.set(n,(t.get(n)||0)+1)}),[...t.entries()].map(([e,t])=>({key:e,count:t,...Fm[e]||{title:e,description:""}}))})(e||[]),n=!t.some(e=>"waggle"===e.key||"knovia"===e.key);return(0,a.createElement)("div",{className:"saddle-integrations"},(0,a.createElement)(_p,{title:(0,Zp.__)("Integrations","saddle"),description:(0,Zp.__)("Extra services your AI can use through Saddle — every tool still follows your access level and approval rules.","saddle")}),(0,a.createElement)(Um,null),(0,a.createElement)(iu,null,(0,a.createElement)(lu,{title:(0,Zp.__)("Detected integrations","saddle"),description:(0,Zp.__)("Tools these integrations currently add. Turn individual tools off on the Permissions screen.","saddle")}),(0,a.createElement)(cu,null,(0,a.createElement)(_u,null,t.map(e=>(0,a.createElement)(wu,{key:e.key,title:e.title,description:e.description,actions:(0,a.createElement)(mu,null,(0,Zp.sprintf)(/* translators: %d: number of tools. */ /* translators: %d: number of tools. */ +(0,Zp.__)("Configured — key ends in ••••%s.","saddle"),t.key_hint)),(0,a.createElement)("div",{className:"saddle-unsplash__actions"},(0,a.createElement)(Jd,{variant:"ghost",onClick:()=>l(!0),disabled:c},(0,Zp.__)("Replace","saddle")),(0,a.createElement)(Jd,{variant:"ghost",onClick:async()=>{await e({title:(0,Zp.__)("Remove the Unsplash key?","saddle"),description:(0,Zp.__)("Your AI loses stock-photo search and import until a new key is added. Photos already in the media library are kept.","saddle"),danger:!0,confirmLabel:(0,Zp.__)("Remove","saddle"),cancelLabel:(0,Zp.__)("Cancel","saddle")})&&u("")},loading:c,disabled:c},(0,Zp.__)("Remove","saddle"))))))}const Fm={waggle:{title:(0,Zp.__)("Waggle","saddle"),description:(0,Zp.__)("SEO & AEO tools from the Waggle plugin, wrapped with Saddle’s safety model.","saddle")},knovia:{title:(0,Zp.__)("Knovia","saddle"),description:(0,Zp.__)("Documentation tools from the Knovia plugin, wrapped with Saddle’s safety model.","saddle")},unsplash:{title:(0,Zp.__)("Unsplash","saddle"),description:(0,Zp.__)("Stock-photo search and import, built into Saddle. Needs the Access Key above.","saddle")},yoast:{title:(0,Zp.__)("Yoast SEO","saddle"),description:(0,Zp.__)("Yoast’s own SEO fields — titles, descriptions, robots — edited natively by Saddle Pro.","saddle")},"rank-math":{title:(0,Zp.__)("Rank Math","saddle"),description:(0,Zp.__)("Rank Math’s own SEO fields, edited natively by Saddle Pro.","saddle")},aioseo:{title:(0,Zp.__)("AIOSEO","saddle"),description:(0,Zp.__)("AIOSEO’s own SEO fields, edited natively by Saddle Pro.","saddle")}};function Vm({caps:e}){const t=(e=>{const t=new Map;return e.forEach(e=>{if("Integrations"!==e.category)return;const n=(e.short||"").split("-"),a=n.slice(0,2).join("-"),r=Fm[a]?a:n[0];r&&t.set(r,(t.get(r)||0)+1)}),[...t.entries()].map(([e,t])=>({key:e,count:t,...Fm[e]||{title:e,description:""}}))})(e||[]),n=!t.some(e=>"waggle"===e.key||"knovia"===e.key);return(0,a.createElement)("div",{className:"saddle-integrations"},(0,a.createElement)(_p,{title:(0,Zp.__)("Integrations","saddle"),description:(0,Zp.__)("Extra services your AI can use through Saddle — every tool still follows your access level and approval rules.","saddle")}),(0,a.createElement)(Um,null),(0,a.createElement)(iu,null,(0,a.createElement)(lu,{title:(0,Zp.__)("Detected integrations","saddle"),description:(0,Zp.__)("Tools these integrations currently add. Turn individual tools off on the Permissions screen.","saddle")}),(0,a.createElement)(cu,null,(0,a.createElement)(_u,null,t.map(e=>(0,a.createElement)(wu,{key:e.key,title:e.title,description:e.description,actions:(0,a.createElement)(mu,null,(0,Zp.sprintf)(/* translators: %d: number of tools. */ /* translators: %d: number of tools. */ (0,Zp._n)("%d tool","%d tools",e.count,"saddle"),e.count))}))),n&&(0,a.createElement)("p",{className:"saddle-integrations__hint"},(0,Zp.__)("Partner plugins are detected automatically: install Waggle (SEO) or Knovia (docs) and their tools appear here — no setup needed.","saddle")))))}const Bm=[{key:"",label:(0,Zp.__)("Everything","saddle")},{key:"executed",label:(0,Zp.__)("Changes","saddle")},{key:"denied",label:(0,Zp.__)("Blocked","saddle")}];function Qm(){const[e,t]=(0,i.useState)([]),[n,r]=(0,i.useState)(0),[s,o]=(0,i.useState)(1),[l,c]=(0,i.useState)(""),[d,u]=(0,i.useState)(!0),[p,m]=(0,i.useState)(!1),[h,f]=(0,i.useState)(null),g=(0,i.useCallback)((e,n,a)=>{a?m(!0):u(!0),Up(`audit-log?per_page=25&page=${e}${n?`&type=${n}`:""}`).then(n=>{t(e=>a?[...e,...n.entries]:n.entries),r(n.total||0),o(e)}).catch(e=>f(e.message)).finally(()=>{u(!1),m(!1)})},[]);(0,i.useEffect)(()=>{g(1,"",!1)},[g]);const y=(e=>{const t=[];return e.forEach(e=>{const n=mm(e.date),a=n?(e=>{const t=new Date,n=new Date(e.getFullYear(),e.getMonth(),e.getDate()),a=new Date(t.getFullYear(),t.getMonth(),t.getDate()),r=Math.round((a-n)/864e5);return 0===r?(0,Zp.__)("Today","saddle"):1===r?(0,Zp.__)("Yesterday","saddle"):e.toLocaleDateString(void 0,{weekday:"short",month:"short",day:"numeric",...e.getFullYear()!==t.getFullYear()?{year:"numeric"}:{}})})(n):(0,Zp.__)("Earlier","saddle"),r=t[t.length-1];r&&r.label===a?r.items.push({...e,d:n}):t.push({label:a,items:[{...e,d:n}]})}),t})(e);return(0,a.createElement)("div",{className:"saddle-activity"},(0,a.createElement)(_p,{title:(0,Zp.__)("Activity","saddle"),description:(0,Zp.__)("Everything your connected apps have changed through Saddle — and every attempt that was blocked. Reading is never logged; only changes are.","saddle")}),(0,a.createElement)("div",{className:"saddle-activity__filters"},(0,a.createElement)(Xu,{"aria-label":(0,Zp.__)("Filter activity","saddle"),items:Bm.map(e=>({value:e.key,label:e.label})),value:l,onChange:e=>{e!==l&&(c(e),g(1,e,!1))}}),n>0&&(0,a.createElement)("span",{className:"saddle-activity__total"},(0,Zp.sprintf)(/* translators: 1: entries shown, 2: total entries. */ /* translators: 1: entries shown, 2: total entries. */ (0,Zp.__)("%1$d of %2$d","saddle"),e.length,n))),h&&(0,a.createElement)(Tu,{tone:"danger"},h),d&&(0,a.createElement)("div",{className:"saddle-activity__loading"},(0,a.createElement)(Nu,null)),!d&&0===e.length&&!h&&(0,a.createElement)(gu,{title:"denied"===l?(0,Zp.__)("Nothing has been blocked","saddle"):(0,Zp.__)("No activity yet","saddle"),description:"denied"===l?(0,Zp.__)("When an app tries something outside its permissions, the attempt shows up here.","saddle"):(0,Zp.__)("Once a connected app makes a change, it shows up here.","saddle")}),!d&&y.map(e=>(0,a.createElement)("section",{className:"saddle-activity__day",key:e.label},(0,a.createElement)("h3",{className:"saddle-activity__daylabel"},e.label),(0,a.createElement)("ul",{className:"saddle-activity__list"},e.items.map((t,n)=>(0,a.createElement)("li",{key:`${e.label}-${n}`,className:"saddle-activity__row"+("denied"===t.type?" is-denied":"")},(0,a.createElement)("span",{className:"saddle-activity__mark","aria-hidden":"true"}),(0,a.createElement)("div",{className:"saddle-activity__body"},(0,a.createElement)("span",{className:"saddle-activity__summary"},t.summary),(0,a.createElement)("span",{className:"saddle-activity__meta"},"denied"===t.type&&(0,a.createElement)(mu,{tone:"danger"},(0,Zp.__)("Blocked","saddle")),t.action&&(0,a.createElement)("code",{className:"saddle-activity__action"},t.action),t.user&&(0,Zp.sprintf)(/* translators: %s: user login. */ /* translators: %s: user login. */ (0,Zp.__)("via %s","saddle"),t.user))),(0,a.createElement)("time",{className:"saddle-activity__time",title:t.d?t.d.toLocaleString():void 0},t.d?t.d.toLocaleTimeString(void 0,{hour:"numeric",minute:"2-digit"}):"")))))),!d&&e.lengthg(s+1,l,!0),loading:p,disabled:p},(0,Zp.__)("Show more","saddle"))))}const Hm=window.wp.hooks,Xm={Badge:mu,Button:Jd,Card:iu,CardContent:cu,CardHeader:lu,Field:eu,Input:tu,Notice:Tu,PageHeader:_p,Row:wu,RowList:_u,Snippet:Hu,Spinner:Nu,Switch:ru,toast:ro,useConfirm:tp},Jm={key:Lp,plug:Ap,settings:Rp,shield:zp},$m=(e,t)=>n=>{var a;return!(!n||t.some(e=>!n[e])||(null!==(a=n.requiresShell)&&void 0!==a?a:1)>1&&(console.warn(`[saddle] ${e} "${n.id}" needs shell v${n.requiresShell}, this is v1 — skipped. Update Saddle.`),1))},Km=(e,t)=>{var n,a;return(null!==(n=e.order)&&void 0!==n?n:50)-(null!==(a=t.order)&&void 0!==a?a:50)};function qm(){const e=(0,Hm.applyFilters)("saddle.admin.settingsCards",[],{shellVersion:1,ui:Xm});return(Array.isArray(e)?e:[]).filter($m("settings card",["id","Component"])).sort(Km)}function eh(){const e=(0,Hm.applyFilters)("saddle.admin.tabs",[],{shellVersion:1,ui:Xm,icons:Object.keys(Jm)});return(Array.isArray(e)?e:[]).filter($m("tab",["id","label","Component"])).map(e=>({...e,group:e.group||"footer",Icon:Jm[e.icon]||Jm.plug})).sort(Km)}const th=e=>"ok"===e?(0,Zp.__)("Yes","saddle"):"slow"===e?(0,Zp.__)("Too slow","saddle"):(0,Zp.__)("Maybe not","saddle");function nh({paused:e,pausing:t,onTogglePause:n}){const[r,s]=(0,i.useState)(null),[o,l]=(0,i.useState)(null),[c,d]=(0,i.useState)(!1),[u,p]=(0,i.useState)(""),m=(0,i.useMemo)(qm,[]);(0,i.useEffect)(()=>{Up("preferences").then(e=>s(e.domain||null)).catch(()=>s(null)),Up("oauth-settings").then(l).catch(()=>l(null))},[]);const h=r&&r.recorded&&r.current!==r.recorded,f=e=>{d(!0),p(""),Up("oauth-settings",{method:"POST",data:e}).then(l).catch(e=>p(e?.message||(0,Zp.__)("Could not save that setting.","saddle"))).finally(()=>d(!1))};return(0,a.createElement)("div",{className:"saddle-settings"},(0,a.createElement)(_p,{title:(0,Zp.__)("Settings","saddle"),description:(0,Zp.__)("Site-wide switches and connection facts. Access levels live on Permissions; integration keys on Integrations.","saddle")}),(0,a.createElement)(iu,null,(0,a.createElement)(lu,{title:(0,Zp.__)("AI access","saddle"),description:(0,Zp.__)("The master switch. Pausing blocks every tool call from every connected app until you resume — nothing is disconnected or forgotten.","saddle")}),(0,a.createElement)(cu,null,(0,a.createElement)("label",{className:"saddle-toggle-row",htmlFor:"saddle-pause-switch"},(0,a.createElement)(ru,{id:"saddle-pause-switch",checked:!e,disabled:t,onChange:n,"aria-label":(0,Zp.__)("Saddle is answering connected apps","saddle")}),(0,a.createElement)("span",null,e?(0,Zp.__)("Paused — every request is refused until you resume.","saddle"):(0,Zp.__)("Active — connected apps can use their allowed tools.","saddle"))))),(0,a.createElement)(iu,null,(0,a.createElement)(lu,{title:(0,Zp.__)("Sign-in for ChatGPT","saddle"),description:(0,Zp.__)("Most AI apps let you paste a sign-in key. ChatGPT does not — its connector screen has no field for one. Turn this on and ChatGPT can send you here to approve it instead, the same way “Sign in with Google” works. Off by default; everything else keeps working either way.","saddle")}),(0,a.createElement)(cu,null,o&&!o.ready&&(0,a.createElement)("p",{className:"saddle-settings__note"},o.permalinks?(0,Zp.__)("This needs your site to be served over HTTPS first — a sign-in token sent over plain HTTP can be read in transit.","saddle"):(0,Zp.__)("This needs pretty permalinks. Go to Settings → Permalinks, choose anything other than Plain, and come back.","saddle")),(0,a.createElement)("label",{className:"saddle-toggle-row",htmlFor:"saddle-oauth-switch"},(0,a.createElement)(ru,{id:"saddle-oauth-switch",checked:!!o?.enabled,disabled:!o||!o.ready||c,onChange:()=>f({enabled:!o.enabled}),"aria-label":(0,Zp.__)("Allow apps to sign in with your WordPress account","saddle")}),(0,a.createElement)("span",null,o?.enabled?(0,Zp.__)("On — ChatGPT and other apps can ask to connect. You approve each one.","saddle"):(0,Zp.__)("Off — no app can start a sign-in, and nothing is published for them to find.","saddle"))),u&&(0,a.createElement)("p",{className:"saddle-settings__note"},u),o?.enabled&&(0,a.createElement)(_u,null,(0,a.createElement)(wu,{title:(0,Zp.__)("Discoverable","saddle"),description:(g=o.discovery,"ok"===g?(0,Zp.__)("Apps can find this site’s sign-in details on their own.","saddle"):"slow"===g?(0,Zp.__)("This site answers too slowly for some apps to finish connecting. The sign-in details are correct and in the right place, but ChatGPT gives up after a few seconds and then reports that this site doesn’t support signing in. A page cache or a faster host usually fixes it.","saddle"):(0,Zp.__)("Apps may not be able to find the sign-in details automatically. This usually means WordPress lives in a subfolder, or your host blocks addresses starting with a dot. Most apps will still connect; ChatGPT may not.","saddle")),actions:(0,a.createElement)(mu,{tone:"ok"===o.discovery?void 0:"warning"},th(o.discovery))}),(0,a.createElement)(wu,{title:(0,Zp.__)("Let apps register themselves","saddle"),description:(0,Zp.__)("Needed by ChatGPT. An app that registers still cannot do anything until you approve it on screen.","saddle"),actions:(0,a.createElement)(ru,{checked:!!o.dcr,disabled:c,onChange:()=>f({dcr:!o.dcr}),"aria-label":(0,Zp.__)("Let apps register themselves","saddle")})}),(0,a.createElement)(wu,{title:(0,Zp.__)("Check app identity","saddle"),description:(0,Zp.__)("When an app identifies itself by web address, Saddle fetches that address to confirm it vouches for the app. Turning this off means every app shows as unverified.","saddle"),actions:(0,a.createElement)(ru,{checked:!!o.cimd,disabled:c,onChange:()=>f({cimd:!o.cimd}),"aria-label":(0,Zp.__)("Check app identity","saddle")})})))),(0,a.createElement)(iu,null,(0,a.createElement)(lu,{title:(0,Zp.__)("Connection","saddle"),description:(0,Zp.__)("Read-only facts about how apps reach this site. Connect and test apps on the Connections screen.","saddle")}),(0,a.createElement)(cu,null,Wp.mcpUrl&&(0,a.createElement)(Hu,{label:(0,Zp.__)("MCP endpoint","saddle"),value:Wp.mcpUrl}),(0,a.createElement)(_u,null,(0,a.createElement)(wu,{title:(0,Zp.__)("Transport","saddle"),actions:(0,a.createElement)(mu,null,Wp.adapter?(0,Zp.__)("Official MCP adapter","saddle"):(0,Zp.__)("Built-in fallback","saddle"))}),r&&(0,a.createElement)(wu,{title:(0,Zp.__)("Site address","saddle"),description:h?(0,Zp.__)("The address has changed since write access was granted — review connected apps if this wasn’t a planned move.","saddle"):void 0,actions:(0,a.createElement)(mu,{tone:h?"warning":void 0},r.current)})))),m.map(({id:e,Component:t})=>(0,a.createElement)(t,{key:e,ui:Xm,shellVersion:1})),(0,a.createElement)(iu,null,(0,a.createElement)(lu,{title:(0,Zp.__)("About","saddle"),description:Wp.version?(0,Zp.sprintf)(/* translators: %s: plugin version number. */ /* translators: %s: plugin version number. */ diff --git a/admin/src/components/Integrations.jsx b/admin/src/components/Integrations.jsx index efdfc94..5cacd7c 100644 --- a/admin/src/components/Integrations.jsx +++ b/admin/src/components/Integrations.jsx @@ -43,10 +43,37 @@ const KNOWN = { 'saddle' ), }, + // Saddle Pro's native SEO integrations. Their tools join the Integrations + // category only while the plugin is detected (Pro gates its + // saddle_integration_ui_prefixes hook on detection), so a row here always + // reflects a plugin that is really present. + yoast: { + title: __( 'Yoast SEO', 'saddle' ), + description: __( + 'Yoast’s own SEO fields — titles, descriptions, robots — edited natively by Saddle Pro.', + 'saddle' + ), + }, + 'rank-math': { + title: __( 'Rank Math', 'saddle' ), + description: __( + 'Rank Math’s own SEO fields, edited natively by Saddle Pro.', + 'saddle' + ), + }, + aioseo: { + title: __( 'AIOSEO', 'saddle' ), + description: __( + 'AIOSEO’s own SEO fields, edited natively by Saddle Pro.', + 'saddle' + ), + }, }; // Group the Integrations-category capabilities by their prefix (waggle-…, -// knovia-…, unsplash-…) into { key, count } rows. +// knovia-…, unsplash-…) into { key, count } rows. A two-segment prefix is +// tried against KNOWN first, so rank-math-* files under "Rank Math" instead +// of a row named "rank". const detectIntegrations = ( caps ) => { const counts = new Map(); caps.forEach( ( c ) => { @@ -54,7 +81,9 @@ const detectIntegrations = ( caps ) => { if ( 'Integrations' !== c.category ) { return; } - const prefix = ( c.short || '' ).split( '-' )[ 0 ]; + const parts = ( c.short || '' ).split( '-' ); + const two = parts.slice( 0, 2 ).join( '-' ); + const prefix = KNOWN[ two ] ? two : parts[ 0 ]; if ( ! prefix ) { return; }