From b0eb34ae6887668da6927f6dd8e30c2cb1402531 Mon Sep 17 00:00:00 2001 From: Jaggob <37583151+Jaggob@users.noreply.github.com> Date: Sat, 1 Aug 2026 00:52:26 +0200 Subject: [PATCH 1/4] refactor(admin): group the settings page into sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The admin page was one long list of fields. It is now split into "Etherpad server", "Pad types and behaviour", "External pads and embedding" and "Diagnostics", with the pad type toggles at the top of their section and the field hints tied to their inputs. Saving and the diagnostic tools report next to their own buttons instead of into a single shared status line, so feedback appears where the action was triggered. The two areas update independently – a late response no longer clears the other one's result. Hints are linked with aria-describedby, the dynamic "no pad type enabled" hint announces itself via role="status", and spacing is set with explicit row classes rather than negative margins. Adds tests/js/admin-settings.test.js, the first coverage for src/admin-settings.js. --- css/admin-settings.css | 78 +++++++++-- js/etherpad_nextcloud-admin-settings.mjs | 2 +- js/etherpad_nextcloud-admin-settings.mjs.map | 2 +- l10n/de.js | 5 + l10n/de.json | 5 + l10n/es.js | 5 + l10n/es.json | 5 + l10n/fr.js | 5 + l10n/fr.json | 5 + lib/Settings/AdminSettings.php | 5 + src/admin-settings.js | 56 +++++--- templates/admin-settings.php | 113 ++++++++------- tests/js/admin-settings.test.js | 136 +++++++++++++++++++ 13 files changed, 345 insertions(+), 77 deletions(-) create mode 100644 tests/js/admin-settings.test.js diff --git a/css/admin-settings.css b/css/admin-settings.css index 1fffb91..256b230 100644 --- a/css/admin-settings.css +++ b/css/admin-settings.css @@ -10,27 +10,79 @@ display: block; } +/* Muted like Nextcloud's own setting descriptions, but at body text size. + Capped at the input width so the text column lines up with the fields. + + A row followed by a hint is marked as such and drops its own bottom + margin, instead of the hint pulling itself up with a negative one — that + kept the spacing dependent on whichever `p` rule happened to apply. */ +#etherpad-nextcloud-admin-settings .ep-field-row { + margin-bottom: 4px; +} + #etherpad-nextcloud-admin-settings .ep-field-hint { - margin-top: -8px; + margin-top: 0; margin-bottom: 14px; + max-width: 640px; + color: var(--color-text-maxcontrast); } -#etherpad-nextcloud-admin-settings #allow-external-pads-row { - margin-bottom: 6px; +/* Not an explanation but a reported value, so it keeps normal body text + styling instead of the muted hint treatment. */ +#etherpad-nextcloud-admin-settings .ep-detected-value { + margin-top: 0; + margin-bottom: 14px; } -#etherpad-nextcloud-admin-settings #external-pad-allowlist-row { - margin-bottom: 6px; +/* Hints belonging to a checkbox line up under its label text rather than + under the box itself, so the pairing reads at a glance. */ +/* An option and its description form one block: no gap worth mentioning + between them, a clear one to the next option. Both margins are set + outright rather than pulled back with negative values, so the result + doesn't depend on which surrounding rule happens to win. */ +#etherpad-nextcloud-admin-settings .ep-checkbox-row { + margin-bottom: 0; } -#etherpad-nextcloud-admin-settings #external-pad-allowlist-hint { - margin-top: 0; +#etherpad-nextcloud-admin-settings .ep-checkbox-row label { + margin-bottom: 0; } +#etherpad-nextcloud-admin-settings .ep-checkbox-hint { + margin-top: 2px; + margin-bottom: 22px; + padding-left: 26px; +} + +/* Smaller than a bare h3 so it doesn't compete with the page title, but + dark and bold like Nextcloud's own section names — muted or uppercase + headings would read as secondary text next to the field labels. */ +#etherpad-nextcloud-admin-settings .ep-section-heading { + margin: 0 0 14px; + padding-top: 22px; + border-top: 1px solid var(--color-border); + font-size: 1.2em; + font-weight: bold; + color: var(--color-main-text); +} + +/* The first heading follows the page intro, so it needs no divider. */ +#etherpad-nextcloud-admin-settings form > .ep-section-heading:first-child { + padding-top: 0; + border-top: none; +} + +#etherpad-nextcloud-admin-settings #allow-external-pads-row { + margin-bottom: 6px; +} + +/* Nextcloud mutes form labels by default; field names are primary content + here, so they keep full contrast like the section headings. */ #etherpad-nextcloud-admin-settings label { display: block; font-weight: 600; margin-bottom: 6px; + color: var(--color-main-text); } #etherpad-nextcloud-admin-settings input[type="url"], @@ -42,11 +94,15 @@ max-width: 640px; } +/* Nextcloud's checkbox styling mutes the label, which left it looking the + same as the description underneath. Restore full contrast and a little + extra weight so the option reads as the heading of its own block. */ #etherpad-nextcloud-admin-settings label.checkbox { display: inline-flex; align-items: center; gap: 8px; - font-weight: 400; + font-weight: 600; + color: var(--color-main-text); } #etherpad-nextcloud-admin-settings .etherpad-nextcloud-admin-actions { @@ -55,8 +111,10 @@ gap: 10px; } -#etherpad-nextcloud-admin-settings #etherpad-nextcloud-admin-status.ep-status-success, -#etherpad-nextcloud-admin-settings #etherpad-nextcloud-admin-status.ep-status-error { +/* Both the save and the diagnostics area carry `ep-status`, so the rule keys + off the class rather than naming a single element. */ +#etherpad-nextcloud-admin-settings .ep-status.ep-status-success, +#etherpad-nextcloud-admin-settings .ep-status.ep-status-error { color: var(--color-main-text, var(--color-text-maxcontrast, #1f2937)); } diff --git a/js/etherpad_nextcloud-admin-settings.mjs b/js/etherpad_nextcloud-admin-settings.mjs index 29bc490..f741209 100644 --- a/js/etherpad_nextcloud-admin-settings.mjs +++ b/js/etherpad_nextcloud-admin-settings.mjs @@ -1,2 +1,2 @@ -(function(){const s=document.getElementById("etherpad-nextcloud-admin-settings"),a=document.getElementById("etherpad-nextcloud-admin-form"),c=document.getElementById("etherpad-nextcloud-admin-status"),E=document.getElementById("etherpad-nextcloud-health-check"),v=document.getElementById("etherpad-nextcloud-consistency-check"),d=document.getElementById("etherpad-nextcloud-retry-pending"),S=document.getElementById("etherpad-nextcloud-pending-actions"),k=document.getElementById("etherpad-nextcloud-pending-count"),u=a?a.querySelector('input[name="allow_external_pads"]'):null,_=a?a.querySelector('input[name="enable_protected_pads"]'):null,y=a?a.querySelector('input[name="enable_public_pads"]'):null,L=document.getElementById("pad-types-none-hint"),x=document.getElementById("external-pad-allowlist-row"),w=document.getElementById("external-pad-allowlist-hint"),f=document.getElementById("external-pad-allowlist"),b={etherpad_host:a.querySelector('[name="etherpad_host"]'),etherpad_api_host:a.querySelector('[name="etherpad_api_host"]'),etherpad_cookie_domain:a.querySelector('[name="etherpad_cookie_domain"]'),etherpad_api_key:a.querySelector('[name="etherpad_api_key"]'),sync_interval_seconds:a.querySelector('[name="sync_interval_seconds"]'),external_pad_allowlist:a.querySelector('[name="external_pad_allowlist"]'),trusted_embed_origins:a.querySelector('[name="trusted_embed_origins"]')};if(!s||!a||!c||!E)return;const $=s.getAttribute("data-save-url")||"",q=s.getAttribute("data-health-url")||"",T=s.getAttribute("data-consistency-url")||"",A=s.getAttribute("data-retry-pending-url")||"",i={saving:s.getAttribute("data-l10n-saving")||"Saving settings...",saved:s.getAttribute("data-l10n-saved")||"Settings saved.",checking:s.getAttribute("data-l10n-checking")||"Testing Etherpad connection...",consistencyRunning:s.getAttribute("data-l10n-consistency-running")||"Running consistency check...",healthOk:s.getAttribute("data-l10n-health-ok")||"Etherpad connection test successful.",consistencyOk:s.getAttribute("data-l10n-consistency-ok")||"Consistency check successful.",requestFailed:s.getAttribute("data-l10n-request-failed")||"Request failed.",savingFailed:s.getAttribute("data-l10n-saving-failed")||"Failed to save settings.",healthFailed:s.getAttribute("data-l10n-health-failed")||"Etherpad connection test failed.",consistencyFailed:s.getAttribute("data-l10n-consistency-failed")||"Consistency check failed.",pendingDeleteLabel:s.getAttribute("data-l10n-pending-delete-label")||"Pending Etherpad deletes",retryFailed:s.getAttribute("data-l10n-retry-failed")||"Pending delete retry failed."};if($===""||q===""||T===""||A==="")return;function F(){const e=!!(u&&u.checked);if(x&&(x.style.display=e?"":"none"),w&&(w.style.display=e?"":"none"),f instanceof HTMLTextAreaElement&&(f.disabled=!e,!e)){f.classList.remove("ep-input-error");const t=a.querySelector('[data-field-error="external_pad_allowlist"]');t instanceof HTMLElement&&(t.textContent="",t.classList.remove("is-visible"))}}function B(){const e=!!(_&&y)&&!_.checked&&!y.checked;L instanceof HTMLElement&&(L.style.display=e?"":"none")}function H(){const e=new FormData(a);return{etherpad_host:String(e.get("etherpad_host")||"").trim(),etherpad_api_host:String(e.get("etherpad_api_host")||"").trim(),etherpad_cookie_domain:String(e.get("etherpad_cookie_domain")||"").trim(),etherpad_api_key:String(e.get("etherpad_api_key")||"").trim(),sync_interval_seconds:Number(e.get("sync_interval_seconds")||120),delete_on_trash:e.has("delete_on_trash"),enable_protected_pads:e.has("enable_protected_pads"),enable_public_pads:e.has("enable_public_pads"),allow_external_pads:e.has("allow_external_pads"),external_pad_allowlist:String(e.get("external_pad_allowlist")||""),trusted_embed_origins:String(e.get("trusted_embed_origins")||"")}}function r(e,t){c.textContent=e,c.classList.remove("ep-status-success","ep-status-error"),t==="success"?c.classList.add("ep-status-success"):t==="error"&&c.classList.add("ep-status-error")}function p(){Object.keys(b).forEach(e=>{const t=b[e];t instanceof HTMLElement&&t.classList.remove("ep-input-error");const n=a.querySelector(`[data-field-error="${e}"]`);n instanceof HTMLElement&&(n.textContent="",n.classList.remove("is-visible"))})}function M(e,t){if(!e||!t)return;const n=b[e];n instanceof HTMLElement&&n.classList.add("ep-input-error");const l=a.querySelector(`[data-field-error="${e}"]`);l instanceof HTMLElement&&(l.textContent=t,l.classList.add("is-visible")),n instanceof HTMLElement&&typeof n.focus=="function"&&n.focus()}function I(e){const t=Number.isFinite(Number(e))?Number(e):0;S instanceof HTMLElement&&(S.style.display=t>0?"":"none"),k instanceof HTMLElement&&(k.textContent=`${i.pendingDeleteLabel}: ${String(t)}`),d instanceof HTMLButtonElement&&(d.disabled=t<=0)}async function g(e,t){const n=new URLSearchParams;Object.keys(t).forEach(h=>{n.set(h,String(t[h]))});const l=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8",requesttoken:String(OC.requestToken||"")},body:n.toString()});let o=null;const m=await l.text();try{o=m!==""?JSON.parse(m):null}catch{o=null}if(!l.ok||!o||o.ok!==!0){const h=o&&o.message?String(o.message):m!==""?m.slice(0,200):i.requestFailed,C=new Error(h);throw C.field=o&&typeof o.field=="string"?o.field:"",C}return o}a.addEventListener("submit",async e=>{e.preventDefault(),p(),r(i.saving,null);try{const t=await g($,H()),n=t&&t.api_version?` api=${String(t.api_version)}`:"";r(`${String(t.message||i.saved)}${n}`,"success")}catch(t){t&&typeof t.field=="string"&&t.field!==""&&M(t.field,t.message||i.savingFailed),r(t instanceof Error?t.message:i.savingFailed,"error")}}),E.addEventListener("click",async()=>{p(),r(i.checking,null);try{const e=await g(q,H()),t=[];typeof e.pad_count<"u"&&t.push(`pad_count=${String(e.pad_count)}`),typeof e.api_version=="string"&&e.api_version.trim()!==""&&t.push(`api=${e.api_version}`),typeof e.latency_ms<"u"&&t.push(`latency=${String(e.latency_ms)}ms`),typeof e.target=="string"&&e.target.trim()!==""&&t.push(`target=${e.target}`),typeof e.pending_delete_count<"u"&&I(Number(e.pending_delete_count));const n=t.length>0?` ${t.join(" | ")}`:"",l=`${String(e.message||i.healthOk)}${n}`;r(l,"success")}catch(e){e&&typeof e.field=="string"&&e.field!==""&&M(e.field,e.message||i.healthFailed),r(e instanceof Error?e.message:i.healthFailed,"error")}}),v instanceof HTMLElement&&v.addEventListener("click",async()=>{p(),r(i.consistencyRunning,null);try{const e=await g(T,{}),t=Number(e.binding_without_file_count||0),n=`${String(e.message||i.consistencyOk)} binding_without_file=${String(t)}`;r(n,t>0?"error":"success")}catch(e){r(e instanceof Error?e.message:i.consistencyFailed,"error")}}),d instanceof HTMLElement&&d.addEventListener("click",async()=>{p(),r(i.checking,null);try{const e=await g(A,{}),t=[];typeof e.attempted<"u"&&t.push(`attempted=${String(e.attempted)}`),typeof e.resolved<"u"&&t.push(`resolved=${String(e.resolved)}`),typeof e.failed<"u"&&t.push(`failed=${String(e.failed)}`),typeof e.remaining<"u"&&t.push(`remaining=${String(e.remaining)}`),typeof e.remaining<"u"&&I(Number(e.remaining||0));const n=t.length>0?` ${t.join(" | ")}`:"";r(`${String(e.message||"OK")}${n}`,"success")}catch(e){r(e instanceof Error?e.message:i.retryFailed,"error")}}),u&&u.addEventListener("change",F),F();for(const e of[_,y])e&&e.addEventListener("change",B);B()})(); +(function(){const s=document.getElementById("etherpad-nextcloud-admin-settings"),a=document.getElementById("etherpad-nextcloud-admin-form"),d=document.getElementById("etherpad-nextcloud-admin-status"),o=document.getElementById("etherpad-nextcloud-diagnostics-status"),S=document.getElementById("etherpad-nextcloud-health-check"),k=document.getElementById("etherpad-nextcloud-consistency-check"),u=document.getElementById("etherpad-nextcloud-retry-pending"),L=document.getElementById("etherpad-nextcloud-pending-actions"),x=document.getElementById("etherpad-nextcloud-pending-count"),p=a?a.querySelector('input[name="allow_external_pads"]'):null,f=a?a.querySelector('input[name="enable_protected_pads"]'):null,b=a?a.querySelector('input[name="enable_public_pads"]'):null,w=document.getElementById("pad-types-none-hint"),$=document.getElementById("external-pad-allowlist-row"),T=document.getElementById("external-pad-allowlist-hint"),E=document.getElementById("external-pad-allowlist"),v={etherpad_host:a.querySelector('[name="etherpad_host"]'),etherpad_api_host:a.querySelector('[name="etherpad_api_host"]'),etherpad_cookie_domain:a.querySelector('[name="etherpad_cookie_domain"]'),etherpad_api_key:a.querySelector('[name="etherpad_api_key"]'),sync_interval_seconds:a.querySelector('[name="sync_interval_seconds"]'),external_pad_allowlist:a.querySelector('[name="external_pad_allowlist"]'),trusted_embed_origins:a.querySelector('[name="trusted_embed_origins"]')};if(!s||!a||!d||!S)return;const q=s.getAttribute("data-save-url")||"",A=s.getAttribute("data-health-url")||"",F=s.getAttribute("data-consistency-url")||"",H=s.getAttribute("data-retry-pending-url")||"",i={saving:s.getAttribute("data-l10n-saving")||"Saving settings...",saved:s.getAttribute("data-l10n-saved")||"Settings saved.",checking:s.getAttribute("data-l10n-checking")||"Testing Etherpad connection...",consistencyRunning:s.getAttribute("data-l10n-consistency-running")||"Running consistency check...",healthOk:s.getAttribute("data-l10n-health-ok")||"Etherpad connection test successful.",consistencyOk:s.getAttribute("data-l10n-consistency-ok")||"Consistency check successful.",requestFailed:s.getAttribute("data-l10n-request-failed")||"Request failed.",savingFailed:s.getAttribute("data-l10n-saving-failed")||"Failed to save settings.",healthFailed:s.getAttribute("data-l10n-health-failed")||"Etherpad connection test failed.",consistencyFailed:s.getAttribute("data-l10n-consistency-failed")||"Consistency check failed.",pendingDeleteLabel:s.getAttribute("data-l10n-pending-delete-label")||"Pending Etherpad deletes",retryFailed:s.getAttribute("data-l10n-retry-failed")||"Pending delete retry failed."};if(q===""||A===""||F===""||H==="")return;function M(){const e=!!(p&&p.checked);if($&&($.style.display=e?"":"none"),T&&(T.style.display=e?"":"none"),E instanceof HTMLTextAreaElement&&(E.disabled=!e,!e)){E.classList.remove("ep-input-error");const t=a.querySelector('[data-field-error="external_pad_allowlist"]');t instanceof HTMLElement&&(t.textContent="",t.classList.remove("is-visible"))}}function B(){const e=!!(f&&b)&&!f.checked&&!b.checked;w instanceof HTMLElement&&(w.style.display=e?"":"none")}function I(){const e=new FormData(a);return{etherpad_host:String(e.get("etherpad_host")||"").trim(),etherpad_api_host:String(e.get("etherpad_api_host")||"").trim(),etherpad_cookie_domain:String(e.get("etherpad_cookie_domain")||"").trim(),etherpad_api_key:String(e.get("etherpad_api_key")||"").trim(),sync_interval_seconds:Number(e.get("sync_interval_seconds")||120),delete_on_trash:e.has("delete_on_trash"),enable_protected_pads:e.has("enable_protected_pads"),enable_public_pads:e.has("enable_public_pads"),allow_external_pads:e.has("allow_external_pads"),external_pad_allowlist:String(e.get("external_pad_allowlist")||""),trusted_embed_origins:String(e.get("trusted_embed_origins")||"")}}function c(e,t,n=d){n instanceof HTMLElement&&(n.textContent=e,n.classList.remove("ep-status-success","ep-status-error"),t==="success"?n.classList.add("ep-status-success"):t==="error"&&n.classList.add("ep-status-error"))}function g(e,t=d){for(const n of[d,o])n instanceof HTMLElement&&n!==t&&(n.textContent="",n.classList.remove("ep-status-success","ep-status-error"));c(e,null,t)}function m(){Object.keys(v).forEach(e=>{const t=v[e];t instanceof HTMLElement&&t.classList.remove("ep-input-error");const n=a.querySelector(`[data-field-error="${e}"]`);n instanceof HTMLElement&&(n.textContent="",n.classList.remove("is-visible"))})}function C(e,t){if(!e||!t)return;const n=v[e];n instanceof HTMLElement&&n.classList.add("ep-input-error");const l=a.querySelector(`[data-field-error="${e}"]`);l instanceof HTMLElement&&(l.textContent=t,l.classList.add("is-visible")),n instanceof HTMLElement&&typeof n.focus=="function"&&n.focus()}function N(e){const t=Number.isFinite(Number(e))?Number(e):0;L instanceof HTMLElement&&(L.style.display=t>0?"":"none"),x instanceof HTMLElement&&(x.textContent=`${i.pendingDeleteLabel}: ${String(t)}`),u instanceof HTMLButtonElement&&(u.disabled=t<=0)}async function h(e,t){const n=new URLSearchParams;Object.keys(t).forEach(y=>{n.set(y,String(t[y]))});const l=await fetch(e,{method:"POST",credentials:"same-origin",headers:{"Content-Type":"application/x-www-form-urlencoded; charset=UTF-8",requesttoken:String(OC.requestToken||"")},body:n.toString()});let r=null;const _=await l.text();try{r=_!==""?JSON.parse(_):null}catch{r=null}if(!l.ok||!r||r.ok!==!0){const y=r&&r.message?String(r.message):_!==""?_.slice(0,200):i.requestFailed,O=new Error(y);throw O.field=r&&typeof r.field=="string"?r.field:"",O}return r}a.addEventListener("submit",async e=>{e.preventDefault(),m(),g(i.saving);try{const t=await h(q,I()),n=t&&t.api_version?` api=${String(t.api_version)}`:"";c(`${String(t.message||i.saved)}${n}`,"success")}catch(t){t&&typeof t.field=="string"&&t.field!==""&&C(t.field,t.message||i.savingFailed),c(t instanceof Error?t.message:i.savingFailed,"error")}}),S.addEventListener("click",async()=>{m(),g(i.checking,o);try{const e=await h(A,I()),t=[];typeof e.pad_count<"u"&&t.push(`pad_count=${String(e.pad_count)}`),typeof e.api_version=="string"&&e.api_version.trim()!==""&&t.push(`api=${e.api_version}`),typeof e.latency_ms<"u"&&t.push(`latency=${String(e.latency_ms)}ms`),typeof e.target=="string"&&e.target.trim()!==""&&t.push(`target=${e.target}`),typeof e.pending_delete_count<"u"&&N(Number(e.pending_delete_count));const n=t.length>0?` ${t.join(" | ")}`:"",l=`${String(e.message||i.healthOk)}${n}`;c(l,"success",o)}catch(e){e&&typeof e.field=="string"&&e.field!==""&&C(e.field,e.message||i.healthFailed),c(e instanceof Error?e.message:i.healthFailed,"error",o)}}),k instanceof HTMLElement&&k.addEventListener("click",async()=>{m(),g(i.consistencyRunning,o);try{const e=await h(F,{}),t=Number(e.binding_without_file_count||0),n=`${String(e.message||i.consistencyOk)} binding_without_file=${String(t)}`;c(n,t>0?"error":"success",o)}catch(e){c(e instanceof Error?e.message:i.consistencyFailed,"error",o)}}),u instanceof HTMLElement&&u.addEventListener("click",async()=>{m(),g(i.checking,o);try{const e=await h(H,{}),t=[];typeof e.attempted<"u"&&t.push(`attempted=${String(e.attempted)}`),typeof e.resolved<"u"&&t.push(`resolved=${String(e.resolved)}`),typeof e.failed<"u"&&t.push(`failed=${String(e.failed)}`),typeof e.remaining<"u"&&t.push(`remaining=${String(e.remaining)}`),typeof e.remaining<"u"&&N(Number(e.remaining||0));const n=t.length>0?` ${t.join(" | ")}`:"";c(`${String(e.message||"OK")}${n}`,"success",o)}catch(e){c(e instanceof Error?e.message:i.retryFailed,"error",o)}}),p&&p.addEventListener("change",M),M();for(const e of[f,b])e&&e.addEventListener("change",B);B()})(); //# sourceMappingURL=etherpad_nextcloud-admin-settings.mjs.map diff --git a/js/etherpad_nextcloud-admin-settings.mjs.map b/js/etherpad_nextcloud-admin-settings.mjs.map index aa5a302..71c26e5 100644 --- a/js/etherpad_nextcloud-admin-settings.mjs.map +++ b/js/etherpad_nextcloud-admin-settings.mjs.map @@ -1 +1 @@ -{"version":3,"file":"etherpad_nextcloud-admin-settings.mjs","sources":["../src/admin-settings.js"],"sourcesContent":["(function () {\n\t'use strict'\n\n\tconst root = document.getElementById('etherpad-nextcloud-admin-settings')\n\tconst form = document.getElementById('etherpad-nextcloud-admin-form')\n\tconst statusNode = document.getElementById('etherpad-nextcloud-admin-status')\n\tconst healthButton = document.getElementById('etherpad-nextcloud-health-check')\n\tconst consistencyButton = document.getElementById('etherpad-nextcloud-consistency-check')\n\tconst retryPendingButton = document.getElementById('etherpad-nextcloud-retry-pending')\n\tconst pendingActions = document.getElementById('etherpad-nextcloud-pending-actions')\n\tconst pendingCountNode = document.getElementById('etherpad-nextcloud-pending-count')\n\tconst allowExternalCheckbox = form ? form.querySelector('input[name=\"allow_external_pads\"]') : null\n\tconst protectedPadsCheckbox = form ? form.querySelector('input[name=\"enable_protected_pads\"]') : null\n\tconst publicPadsCheckbox = form ? form.querySelector('input[name=\"enable_public_pads\"]') : null\n\tconst padTypesNoneHint = document.getElementById('pad-types-none-hint')\n\tconst allowlistRow = document.getElementById('external-pad-allowlist-row')\n\tconst allowlistHint = document.getElementById('external-pad-allowlist-hint')\n\tconst allowlistTextarea = document.getElementById('external-pad-allowlist')\n\tconst fieldNodes = {\n\t\tetherpad_host: form.querySelector('[name=\"etherpad_host\"]'),\n\t\tetherpad_api_host: form.querySelector('[name=\"etherpad_api_host\"]'),\n\t\tetherpad_cookie_domain: form.querySelector('[name=\"etherpad_cookie_domain\"]'),\n\t\tetherpad_api_key: form.querySelector('[name=\"etherpad_api_key\"]'),\n\t\tsync_interval_seconds: form.querySelector('[name=\"sync_interval_seconds\"]'),\n\t\texternal_pad_allowlist: form.querySelector('[name=\"external_pad_allowlist\"]'),\n\t\ttrusted_embed_origins: form.querySelector('[name=\"trusted_embed_origins\"]'),\n\t}\n\n\tif (!root || !form || !statusNode || !healthButton) {\n\t\treturn\n\t}\n\n\tconst saveUrl = root.getAttribute('data-save-url') || ''\n\tconst healthUrl = root.getAttribute('data-health-url') || ''\n\tconst consistencyUrl = root.getAttribute('data-consistency-url') || ''\n\tconst retryPendingUrl = root.getAttribute('data-retry-pending-url') || ''\n\tconst l10n = {\n\t\tsaving: root.getAttribute('data-l10n-saving') || 'Saving settings...',\n\t\tsaved: root.getAttribute('data-l10n-saved') || 'Settings saved.',\n\t\tchecking: root.getAttribute('data-l10n-checking') || 'Testing Etherpad connection...',\n\t\tconsistencyRunning: root.getAttribute('data-l10n-consistency-running') || 'Running consistency check...',\n\t\thealthOk: root.getAttribute('data-l10n-health-ok') || 'Etherpad connection test successful.',\n\t\tconsistencyOk: root.getAttribute('data-l10n-consistency-ok') || 'Consistency check successful.',\n\t\trequestFailed: root.getAttribute('data-l10n-request-failed') || 'Request failed.',\n\t\tsavingFailed: root.getAttribute('data-l10n-saving-failed') || 'Failed to save settings.',\n\t\thealthFailed: root.getAttribute('data-l10n-health-failed') || 'Etherpad connection test failed.',\n\t\tconsistencyFailed: root.getAttribute('data-l10n-consistency-failed') || 'Consistency check failed.',\n\t\tpendingDeleteLabel: root.getAttribute('data-l10n-pending-delete-label') || 'Pending Etherpad deletes',\n\t\tretryFailed: root.getAttribute('data-l10n-retry-failed') || 'Pending delete retry failed.',\n\t}\n\n\tif (saveUrl === '' || healthUrl === '' || consistencyUrl === '' || retryPendingUrl === '') {\n\t\treturn\n\t}\n\n\tfunction updateExternalSettingsVisibility() {\n\t\tconst enabled = !!(allowExternalCheckbox && allowExternalCheckbox.checked)\n\t\tif (allowlistRow) {\n\t\t\tallowlistRow.style.display = enabled ? '' : 'none'\n\t\t}\n\t\tif (allowlistHint) {\n\t\t\tallowlistHint.style.display = enabled ? '' : 'none'\n\t\t}\n\t\tif (allowlistTextarea instanceof HTMLTextAreaElement) {\n\t\t\tallowlistTextarea.disabled = !enabled\n\t\t\tif (!enabled) {\n\t\t\t\tallowlistTextarea.classList.remove('ep-input-error')\n\t\t\t\tconst errorNode = form.querySelector('[data-field-error=\"external_pad_allowlist\"]')\n\t\t\t\tif (errorNode instanceof HTMLElement) {\n\t\t\t\t\terrorNode.textContent = ''\n\t\t\t\t\terrorNode.classList.remove('is-visible')\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction updatePadTypesHint() {\n\t\tconst noneEnabled = !!(protectedPadsCheckbox && publicPadsCheckbox)\n\t\t\t&& !protectedPadsCheckbox.checked\n\t\t\t&& !publicPadsCheckbox.checked\n\t\tif (padTypesNoneHint instanceof HTMLElement) {\n\t\t\t// Toggle `display` rather than the `hidden` attribute: the element\n\t\t\t// also carries Nextcloud's `.settings-hint` class, whose rule wins\n\t\t\t// over the user-agent `[hidden]` default.\n\t\t\tpadTypesNoneHint.style.display = noneEnabled ? '' : 'none'\n\t\t}\n\t}\n\n\tfunction getPayload() {\n\t\tconst data = new FormData(form)\n\t\treturn {\n\t\t\tetherpad_host: String(data.get('etherpad_host') || '').trim(),\n\t\t\tetherpad_api_host: String(data.get('etherpad_api_host') || '').trim(),\n\t\t\tetherpad_cookie_domain: String(data.get('etherpad_cookie_domain') || '').trim(),\n\t\t\tetherpad_api_key: String(data.get('etherpad_api_key') || '').trim(),\n\t\t\tsync_interval_seconds: Number(data.get('sync_interval_seconds') || 120),\n\t\t\tdelete_on_trash: data.has('delete_on_trash'),\n\t\t\tenable_protected_pads: data.has('enable_protected_pads'),\n\t\t\tenable_public_pads: data.has('enable_public_pads'),\n\t\t\tallow_external_pads: data.has('allow_external_pads'),\n\t\t\texternal_pad_allowlist: String(data.get('external_pad_allowlist') || ''),\n\t\t\ttrusted_embed_origins: String(data.get('trusted_embed_origins') || ''),\n\t\t}\n\t}\n\n\tfunction setStatus(message, state) {\n\t\tstatusNode.textContent = message\n\t\tstatusNode.classList.remove('ep-status-success', 'ep-status-error')\n\t\tif (state === 'success') {\n\t\t\tstatusNode.classList.add('ep-status-success')\n\t\t} else if (state === 'error') {\n\t\t\tstatusNode.classList.add('ep-status-error')\n\t\t}\n\t}\n\n\tfunction clearFieldErrors() {\n\t\tObject.keys(fieldNodes).forEach((field) => {\n\t\t\tconst input = fieldNodes[field]\n\t\t\tif (input instanceof HTMLElement) {\n\t\t\t\tinput.classList.remove('ep-input-error')\n\t\t\t}\n\t\t\tconst errorNode = form.querySelector(`[data-field-error=\"${field}\"]`)\n\t\t\tif (errorNode instanceof HTMLElement) {\n\t\t\t\terrorNode.textContent = ''\n\t\t\t\terrorNode.classList.remove('is-visible')\n\t\t\t}\n\t\t})\n\t}\n\n\tfunction showFieldError(field, message) {\n\t\tif (!field || !message) {\n\t\t\treturn\n\t\t}\n\t\tconst input = fieldNodes[field]\n\t\tif (input instanceof HTMLElement) {\n\t\t\tinput.classList.add('ep-input-error')\n\t\t}\n\t\tconst errorNode = form.querySelector(`[data-field-error=\"${field}\"]`)\n\t\tif (errorNode instanceof HTMLElement) {\n\t\t\terrorNode.textContent = message\n\t\t\terrorNode.classList.add('is-visible')\n\t\t}\n\t\tif (input instanceof HTMLElement && typeof input.focus === 'function') {\n\t\t\tinput.focus()\n\t\t}\n\t}\n\n\tfunction updatePendingDeleteUi(count) {\n\t\tconst pendingCount = Number.isFinite(Number(count)) ? Number(count) : 0\n\t\tif (pendingActions instanceof HTMLElement) {\n\t\t\tpendingActions.style.display = pendingCount > 0 ? '' : 'none'\n\t\t}\n\t\tif (pendingCountNode instanceof HTMLElement) {\n\t\t\tpendingCountNode.textContent = `${l10n.pendingDeleteLabel}: ${String(pendingCount)}`\n\t\t}\n\t\tif (retryPendingButton instanceof HTMLButtonElement) {\n\t\t\tretryPendingButton.disabled = pendingCount <= 0\n\t\t}\n\t}\n\n\tasync function postJson(url, payload) {\n\t\tconst body = new URLSearchParams()\n\t\tObject.keys(payload).forEach((key) => {\n\t\t\tbody.set(key, String(payload[key]))\n\t\t})\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\tcredentials: 'same-origin',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',\n\t\t\t\trequesttoken: String(OC.requestToken || ''),\n\t\t\t},\n\t\t\tbody: body.toString(),\n\t\t})\n\n\t\tlet data = null\n\t\tconst responseText = await response.text()\n\t\ttry {\n\t\t\tdata = responseText !== '' ? JSON.parse(responseText) : null\n\t\t} catch (error) {\n\t\t\tdata = null\n\t\t}\n\n\t\tif (!response.ok || !data || data.ok !== true) {\n\t\t\tconst message = (data && data.message)\n\t\t\t\t? String(data.message)\n\t\t\t\t: (responseText !== '' ? responseText.slice(0, 200) : l10n.requestFailed)\n\t\t\tconst err = new Error(message)\n\t\t\terr.field = (data && typeof data.field === 'string') ? data.field : ''\n\t\t\tthrow err\n\t\t}\n\n\t\treturn data\n\t}\n\n\tform.addEventListener('submit', async (event) => {\n\t\tevent.preventDefault()\n\t\tclearFieldErrors()\n\t\tsetStatus(l10n.saving, null)\n\t\ttry {\n\t\t\tconst data = await postJson(saveUrl, getPayload())\n\t\t\tconst versionSuffix = data && data.api_version ? ` api=${String(data.api_version)}` : ''\n\t\t\tsetStatus(`${String(data.message || l10n.saved)}${versionSuffix}`, 'success')\n\t\t} catch (error) {\n\t\t\tif (error && typeof error.field === 'string' && error.field !== '') {\n\t\t\t\tshowFieldError(error.field, error.message || l10n.savingFailed)\n\t\t\t}\n\t\t\tsetStatus(error instanceof Error ? error.message : l10n.savingFailed, 'error')\n\t\t}\n\t})\n\n\thealthButton.addEventListener('click', async () => {\n\t\tclearFieldErrors()\n\t\tsetStatus(l10n.checking, null)\n\t\ttry {\n\t\t\tconst data = await postJson(healthUrl, getPayload())\n\t\t\tconst details = []\n\t\t\tif (typeof data.pad_count !== 'undefined') {\n\t\t\t\tdetails.push(`pad_count=${String(data.pad_count)}`)\n\t\t\t}\n\t\t\tif (typeof data.api_version === 'string' && data.api_version.trim() !== '') {\n\t\t\t\tdetails.push(`api=${data.api_version}`)\n\t\t\t}\n\t\t\tif (typeof data.latency_ms !== 'undefined') {\n\t\t\t\tdetails.push(`latency=${String(data.latency_ms)}ms`)\n\t\t\t}\n\t\t\tif (typeof data.target === 'string' && data.target.trim() !== '') {\n\t\t\t\tdetails.push(`target=${data.target}`)\n\t\t\t}\n\t\t\tif (typeof data.pending_delete_count !== 'undefined') {\n\t\t\t\tupdatePendingDeleteUi(Number(data.pending_delete_count))\n\t\t\t}\n\t\t\tconst suffix = details.length > 0 ? ` ${details.join(' | ')}` : ''\n\t\t\tconst message = `${String(data.message || l10n.healthOk)}${suffix}`\n\t\t\tsetStatus(message, 'success')\n\t\t} catch (error) {\n\t\t\tif (error && typeof error.field === 'string' && error.field !== '') {\n\t\t\t\tshowFieldError(error.field, error.message || l10n.healthFailed)\n\t\t\t}\n\t\t\tsetStatus(error instanceof Error ? error.message : l10n.healthFailed, 'error')\n\t\t}\n\t})\n\n\tif (consistencyButton instanceof HTMLElement) {\n\t\tconsistencyButton.addEventListener('click', async () => {\n\t\t\tclearFieldErrors()\n\t\t\tsetStatus(l10n.consistencyRunning, null)\n\t\t\ttry {\n\t\t\t\tconst data = await postJson(consistencyUrl, {})\n\t\t\t\tconst bindingWithoutFile = Number(data.binding_without_file_count || 0)\n\t\t\t\tconst message = `${String(data.message || l10n.consistencyOk)} binding_without_file=${String(bindingWithoutFile)}`\n\t\t\t\tsetStatus(message, bindingWithoutFile > 0 ? 'error' : 'success')\n\t\t\t} catch (error) {\n\t\t\t\tsetStatus(error instanceof Error ? error.message : l10n.consistencyFailed, 'error')\n\t\t\t}\n\t\t})\n\t}\n\n\tif (retryPendingButton instanceof HTMLElement) {\n\t\tretryPendingButton.addEventListener('click', async () => {\n\t\t\tclearFieldErrors()\n\t\t\tsetStatus(l10n.checking, null)\n\t\t\ttry {\n\t\t\t\tconst data = await postJson(retryPendingUrl, {})\n\t\t\t\tconst details = []\n\t\t\t\tif (typeof data.attempted !== 'undefined') {\n\t\t\t\t\tdetails.push(`attempted=${String(data.attempted)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.resolved !== 'undefined') {\n\t\t\t\t\tdetails.push(`resolved=${String(data.resolved)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.failed !== 'undefined') {\n\t\t\t\t\tdetails.push(`failed=${String(data.failed)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.remaining !== 'undefined') {\n\t\t\t\t\tdetails.push(`remaining=${String(data.remaining)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.remaining !== 'undefined') {\n\t\t\t\t\tupdatePendingDeleteUi(Number(data.remaining || 0))\n\t\t\t\t}\n\t\t\t\tconst suffix = details.length > 0 ? ` ${details.join(' | ')}` : ''\n\t\t\t\tsetStatus(`${String(data.message || 'OK')}${suffix}`, 'success')\n\t\t\t} catch (error) {\n\t\t\t\tsetStatus(error instanceof Error ? error.message : l10n.retryFailed, 'error')\n\t\t\t}\n\t\t})\n\t}\n\n\tif (allowExternalCheckbox) {\n\t\tallowExternalCheckbox.addEventListener('change', updateExternalSettingsVisibility)\n\t}\n\tupdateExternalSettingsVisibility()\n\n\tfor (const checkbox of [protectedPadsCheckbox, publicPadsCheckbox]) {\n\t\tif (checkbox) {\n\t\t\tcheckbox.addEventListener('change', updatePadTypesHint)\n\t\t}\n\t}\n\tupdatePadTypesHint()\n})()\n"],"names":["root","form","statusNode","healthButton","consistencyButton","retryPendingButton","pendingActions","pendingCountNode","allowExternalCheckbox","protectedPadsCheckbox","publicPadsCheckbox","padTypesNoneHint","allowlistRow","allowlistHint","allowlistTextarea","fieldNodes","saveUrl","healthUrl","consistencyUrl","retryPendingUrl","l10n","updateExternalSettingsVisibility","enabled","errorNode","updatePadTypesHint","noneEnabled","getPayload","data","setStatus","message","state","clearFieldErrors","field","input","showFieldError","updatePendingDeleteUi","count","pendingCount","postJson","url","payload","body","key","response","responseText","err","event","versionSuffix","error","details","suffix","bindingWithoutFile","checkbox"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"etherpad_nextcloud-admin-settings.mjs","sources":["../src/admin-settings.js"],"sourcesContent":["(function () {\n\t'use strict'\n\n\tconst root = document.getElementById('etherpad-nextcloud-admin-settings')\n\tconst form = document.getElementById('etherpad-nextcloud-admin-form')\n\tconst statusNode = document.getElementById('etherpad-nextcloud-admin-status')\n\tconst diagnosticsStatusNode = document.getElementById('etherpad-nextcloud-diagnostics-status')\n\tconst healthButton = document.getElementById('etherpad-nextcloud-health-check')\n\tconst consistencyButton = document.getElementById('etherpad-nextcloud-consistency-check')\n\tconst retryPendingButton = document.getElementById('etherpad-nextcloud-retry-pending')\n\tconst pendingActions = document.getElementById('etherpad-nextcloud-pending-actions')\n\tconst pendingCountNode = document.getElementById('etherpad-nextcloud-pending-count')\n\tconst allowExternalCheckbox = form ? form.querySelector('input[name=\"allow_external_pads\"]') : null\n\tconst protectedPadsCheckbox = form ? form.querySelector('input[name=\"enable_protected_pads\"]') : null\n\tconst publicPadsCheckbox = form ? form.querySelector('input[name=\"enable_public_pads\"]') : null\n\tconst padTypesNoneHint = document.getElementById('pad-types-none-hint')\n\tconst allowlistRow = document.getElementById('external-pad-allowlist-row')\n\tconst allowlistHint = document.getElementById('external-pad-allowlist-hint')\n\tconst allowlistTextarea = document.getElementById('external-pad-allowlist')\n\tconst fieldNodes = {\n\t\tetherpad_host: form.querySelector('[name=\"etherpad_host\"]'),\n\t\tetherpad_api_host: form.querySelector('[name=\"etherpad_api_host\"]'),\n\t\tetherpad_cookie_domain: form.querySelector('[name=\"etherpad_cookie_domain\"]'),\n\t\tetherpad_api_key: form.querySelector('[name=\"etherpad_api_key\"]'),\n\t\tsync_interval_seconds: form.querySelector('[name=\"sync_interval_seconds\"]'),\n\t\texternal_pad_allowlist: form.querySelector('[name=\"external_pad_allowlist\"]'),\n\t\ttrusted_embed_origins: form.querySelector('[name=\"trusted_embed_origins\"]'),\n\t}\n\n\tif (!root || !form || !statusNode || !healthButton) {\n\t\treturn\n\t}\n\n\tconst saveUrl = root.getAttribute('data-save-url') || ''\n\tconst healthUrl = root.getAttribute('data-health-url') || ''\n\tconst consistencyUrl = root.getAttribute('data-consistency-url') || ''\n\tconst retryPendingUrl = root.getAttribute('data-retry-pending-url') || ''\n\tconst l10n = {\n\t\tsaving: root.getAttribute('data-l10n-saving') || 'Saving settings...',\n\t\tsaved: root.getAttribute('data-l10n-saved') || 'Settings saved.',\n\t\tchecking: root.getAttribute('data-l10n-checking') || 'Testing Etherpad connection...',\n\t\tconsistencyRunning: root.getAttribute('data-l10n-consistency-running') || 'Running consistency check...',\n\t\thealthOk: root.getAttribute('data-l10n-health-ok') || 'Etherpad connection test successful.',\n\t\tconsistencyOk: root.getAttribute('data-l10n-consistency-ok') || 'Consistency check successful.',\n\t\trequestFailed: root.getAttribute('data-l10n-request-failed') || 'Request failed.',\n\t\tsavingFailed: root.getAttribute('data-l10n-saving-failed') || 'Failed to save settings.',\n\t\thealthFailed: root.getAttribute('data-l10n-health-failed') || 'Etherpad connection test failed.',\n\t\tconsistencyFailed: root.getAttribute('data-l10n-consistency-failed') || 'Consistency check failed.',\n\t\tpendingDeleteLabel: root.getAttribute('data-l10n-pending-delete-label') || 'Pending Etherpad deletes',\n\t\tretryFailed: root.getAttribute('data-l10n-retry-failed') || 'Pending delete retry failed.',\n\t}\n\n\tif (saveUrl === '' || healthUrl === '' || consistencyUrl === '' || retryPendingUrl === '') {\n\t\treturn\n\t}\n\n\tfunction updateExternalSettingsVisibility() {\n\t\tconst enabled = !!(allowExternalCheckbox && allowExternalCheckbox.checked)\n\t\tif (allowlistRow) {\n\t\t\tallowlistRow.style.display = enabled ? '' : 'none'\n\t\t}\n\t\tif (allowlistHint) {\n\t\t\tallowlistHint.style.display = enabled ? '' : 'none'\n\t\t}\n\t\tif (allowlistTextarea instanceof HTMLTextAreaElement) {\n\t\t\tallowlistTextarea.disabled = !enabled\n\t\t\tif (!enabled) {\n\t\t\t\tallowlistTextarea.classList.remove('ep-input-error')\n\t\t\t\tconst errorNode = form.querySelector('[data-field-error=\"external_pad_allowlist\"]')\n\t\t\t\tif (errorNode instanceof HTMLElement) {\n\t\t\t\t\terrorNode.textContent = ''\n\t\t\t\t\terrorNode.classList.remove('is-visible')\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction updatePadTypesHint() {\n\t\tconst noneEnabled = !!(protectedPadsCheckbox && publicPadsCheckbox)\n\t\t\t&& !protectedPadsCheckbox.checked\n\t\t\t&& !publicPadsCheckbox.checked\n\t\tif (padTypesNoneHint instanceof HTMLElement) {\n\t\t\t// Toggle `display` rather than the `hidden` attribute: our own\n\t\t\t// `.settings-hint { display: block }` rule is ID-scoped and so\n\t\t\t// outweighs the user-agent `[hidden]` default.\n\t\t\tpadTypesNoneHint.style.display = noneEnabled ? '' : 'none'\n\t\t}\n\t}\n\n\tfunction getPayload() {\n\t\tconst data = new FormData(form)\n\t\treturn {\n\t\t\tetherpad_host: String(data.get('etherpad_host') || '').trim(),\n\t\t\tetherpad_api_host: String(data.get('etherpad_api_host') || '').trim(),\n\t\t\tetherpad_cookie_domain: String(data.get('etherpad_cookie_domain') || '').trim(),\n\t\t\tetherpad_api_key: String(data.get('etherpad_api_key') || '').trim(),\n\t\t\tsync_interval_seconds: Number(data.get('sync_interval_seconds') || 120),\n\t\t\tdelete_on_trash: data.has('delete_on_trash'),\n\t\t\tenable_protected_pads: data.has('enable_protected_pads'),\n\t\t\tenable_public_pads: data.has('enable_public_pads'),\n\t\t\tallow_external_pads: data.has('allow_external_pads'),\n\t\t\texternal_pad_allowlist: String(data.get('external_pad_allowlist') || ''),\n\t\t\ttrusted_embed_origins: String(data.get('trusted_embed_origins') || ''),\n\t\t}\n\t}\n\n\t// Saving and the diagnostic tools live in separate sections, so each\n\t// reports next to its own buttons. Writing a status only ever touches its\n\t// own area — the two run independently, and a response arriving late must\n\t// not wipe out the other action's result.\n\tfunction setStatus(message, state, node = statusNode) {\n\t\tif (!(node instanceof HTMLElement)) {\n\t\t\treturn\n\t\t}\n\t\tnode.textContent = message\n\t\tnode.classList.remove('ep-status-success', 'ep-status-error')\n\t\tif (state === 'success') {\n\t\t\tnode.classList.add('ep-status-success')\n\t\t} else if (state === 'error') {\n\t\t\tnode.classList.add('ep-status-error')\n\t\t}\n\t}\n\n\t// Starting an action does clear the other area: its result predates this\n\t// action and could otherwise be read as belonging to it.\n\tfunction beginStatus(message, node = statusNode) {\n\t\tfor (const other of [statusNode, diagnosticsStatusNode]) {\n\t\t\tif (other instanceof HTMLElement && other !== node) {\n\t\t\t\tother.textContent = ''\n\t\t\t\tother.classList.remove('ep-status-success', 'ep-status-error')\n\t\t\t}\n\t\t}\n\t\tsetStatus(message, null, node)\n\t}\n\n\tfunction clearFieldErrors() {\n\t\tObject.keys(fieldNodes).forEach((field) => {\n\t\t\tconst input = fieldNodes[field]\n\t\t\tif (input instanceof HTMLElement) {\n\t\t\t\tinput.classList.remove('ep-input-error')\n\t\t\t}\n\t\t\tconst errorNode = form.querySelector(`[data-field-error=\"${field}\"]`)\n\t\t\tif (errorNode instanceof HTMLElement) {\n\t\t\t\terrorNode.textContent = ''\n\t\t\t\terrorNode.classList.remove('is-visible')\n\t\t\t}\n\t\t})\n\t}\n\n\tfunction showFieldError(field, message) {\n\t\tif (!field || !message) {\n\t\t\treturn\n\t\t}\n\t\tconst input = fieldNodes[field]\n\t\tif (input instanceof HTMLElement) {\n\t\t\tinput.classList.add('ep-input-error')\n\t\t}\n\t\tconst errorNode = form.querySelector(`[data-field-error=\"${field}\"]`)\n\t\tif (errorNode instanceof HTMLElement) {\n\t\t\terrorNode.textContent = message\n\t\t\terrorNode.classList.add('is-visible')\n\t\t}\n\t\tif (input instanceof HTMLElement && typeof input.focus === 'function') {\n\t\t\tinput.focus()\n\t\t}\n\t}\n\n\tfunction updatePendingDeleteUi(count) {\n\t\tconst pendingCount = Number.isFinite(Number(count)) ? Number(count) : 0\n\t\tif (pendingActions instanceof HTMLElement) {\n\t\t\tpendingActions.style.display = pendingCount > 0 ? '' : 'none'\n\t\t}\n\t\tif (pendingCountNode instanceof HTMLElement) {\n\t\t\tpendingCountNode.textContent = `${l10n.pendingDeleteLabel}: ${String(pendingCount)}`\n\t\t}\n\t\tif (retryPendingButton instanceof HTMLButtonElement) {\n\t\t\tretryPendingButton.disabled = pendingCount <= 0\n\t\t}\n\t}\n\n\tasync function postJson(url, payload) {\n\t\tconst body = new URLSearchParams()\n\t\tObject.keys(payload).forEach((key) => {\n\t\t\tbody.set(key, String(payload[key]))\n\t\t})\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\tcredentials: 'same-origin',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',\n\t\t\t\trequesttoken: String(OC.requestToken || ''),\n\t\t\t},\n\t\t\tbody: body.toString(),\n\t\t})\n\n\t\tlet data = null\n\t\tconst responseText = await response.text()\n\t\ttry {\n\t\t\tdata = responseText !== '' ? JSON.parse(responseText) : null\n\t\t} catch (error) {\n\t\t\tdata = null\n\t\t}\n\n\t\tif (!response.ok || !data || data.ok !== true) {\n\t\t\tconst message = (data && data.message)\n\t\t\t\t? String(data.message)\n\t\t\t\t: (responseText !== '' ? responseText.slice(0, 200) : l10n.requestFailed)\n\t\t\tconst err = new Error(message)\n\t\t\terr.field = (data && typeof data.field === 'string') ? data.field : ''\n\t\t\tthrow err\n\t\t}\n\n\t\treturn data\n\t}\n\n\tform.addEventListener('submit', async (event) => {\n\t\tevent.preventDefault()\n\t\tclearFieldErrors()\n\t\tbeginStatus(l10n.saving)\n\t\ttry {\n\t\t\tconst data = await postJson(saveUrl, getPayload())\n\t\t\tconst versionSuffix = data && data.api_version ? ` api=${String(data.api_version)}` : ''\n\t\t\tsetStatus(`${String(data.message || l10n.saved)}${versionSuffix}`, 'success')\n\t\t} catch (error) {\n\t\t\tif (error && typeof error.field === 'string' && error.field !== '') {\n\t\t\t\tshowFieldError(error.field, error.message || l10n.savingFailed)\n\t\t\t}\n\t\t\tsetStatus(error instanceof Error ? error.message : l10n.savingFailed, 'error')\n\t\t}\n\t})\n\n\thealthButton.addEventListener('click', async () => {\n\t\tclearFieldErrors()\n\t\tbeginStatus(l10n.checking, diagnosticsStatusNode)\n\t\ttry {\n\t\t\tconst data = await postJson(healthUrl, getPayload())\n\t\t\tconst details = []\n\t\t\tif (typeof data.pad_count !== 'undefined') {\n\t\t\t\tdetails.push(`pad_count=${String(data.pad_count)}`)\n\t\t\t}\n\t\t\tif (typeof data.api_version === 'string' && data.api_version.trim() !== '') {\n\t\t\t\tdetails.push(`api=${data.api_version}`)\n\t\t\t}\n\t\t\tif (typeof data.latency_ms !== 'undefined') {\n\t\t\t\tdetails.push(`latency=${String(data.latency_ms)}ms`)\n\t\t\t}\n\t\t\tif (typeof data.target === 'string' && data.target.trim() !== '') {\n\t\t\t\tdetails.push(`target=${data.target}`)\n\t\t\t}\n\t\t\tif (typeof data.pending_delete_count !== 'undefined') {\n\t\t\t\tupdatePendingDeleteUi(Number(data.pending_delete_count))\n\t\t\t}\n\t\t\tconst suffix = details.length > 0 ? ` ${details.join(' | ')}` : ''\n\t\t\tconst message = `${String(data.message || l10n.healthOk)}${suffix}`\n\t\t\tsetStatus(message, 'success', diagnosticsStatusNode)\n\t\t} catch (error) {\n\t\t\tif (error && typeof error.field === 'string' && error.field !== '') {\n\t\t\t\tshowFieldError(error.field, error.message || l10n.healthFailed)\n\t\t\t}\n\t\t\tsetStatus(error instanceof Error ? error.message : l10n.healthFailed, 'error', diagnosticsStatusNode)\n\t\t}\n\t})\n\n\tif (consistencyButton instanceof HTMLElement) {\n\t\tconsistencyButton.addEventListener('click', async () => {\n\t\t\tclearFieldErrors()\n\t\t\tbeginStatus(l10n.consistencyRunning, diagnosticsStatusNode)\n\t\t\ttry {\n\t\t\t\tconst data = await postJson(consistencyUrl, {})\n\t\t\t\tconst bindingWithoutFile = Number(data.binding_without_file_count || 0)\n\t\t\t\tconst message = `${String(data.message || l10n.consistencyOk)} binding_without_file=${String(bindingWithoutFile)}`\n\t\t\t\tsetStatus(message, bindingWithoutFile > 0 ? 'error' : 'success', diagnosticsStatusNode)\n\t\t\t} catch (error) {\n\t\t\t\tsetStatus(error instanceof Error ? error.message : l10n.consistencyFailed, 'error', diagnosticsStatusNode)\n\t\t\t}\n\t\t})\n\t}\n\n\tif (retryPendingButton instanceof HTMLElement) {\n\t\tretryPendingButton.addEventListener('click', async () => {\n\t\t\tclearFieldErrors()\n\t\t\tbeginStatus(l10n.checking, diagnosticsStatusNode)\n\t\t\ttry {\n\t\t\t\tconst data = await postJson(retryPendingUrl, {})\n\t\t\t\tconst details = []\n\t\t\t\tif (typeof data.attempted !== 'undefined') {\n\t\t\t\t\tdetails.push(`attempted=${String(data.attempted)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.resolved !== 'undefined') {\n\t\t\t\t\tdetails.push(`resolved=${String(data.resolved)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.failed !== 'undefined') {\n\t\t\t\t\tdetails.push(`failed=${String(data.failed)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.remaining !== 'undefined') {\n\t\t\t\t\tdetails.push(`remaining=${String(data.remaining)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.remaining !== 'undefined') {\n\t\t\t\t\tupdatePendingDeleteUi(Number(data.remaining || 0))\n\t\t\t\t}\n\t\t\t\tconst suffix = details.length > 0 ? ` ${details.join(' | ')}` : ''\n\t\t\t\tsetStatus(`${String(data.message || 'OK')}${suffix}`, 'success', diagnosticsStatusNode)\n\t\t\t} catch (error) {\n\t\t\t\tsetStatus(error instanceof Error ? error.message : l10n.retryFailed, 'error', diagnosticsStatusNode)\n\t\t\t}\n\t\t})\n\t}\n\n\tif (allowExternalCheckbox) {\n\t\tallowExternalCheckbox.addEventListener('change', updateExternalSettingsVisibility)\n\t}\n\tupdateExternalSettingsVisibility()\n\n\tfor (const checkbox of [protectedPadsCheckbox, publicPadsCheckbox]) {\n\t\tif (checkbox) {\n\t\t\tcheckbox.addEventListener('change', updatePadTypesHint)\n\t\t}\n\t}\n\tupdatePadTypesHint()\n})()\n"],"names":["root","form","statusNode","diagnosticsStatusNode","healthButton","consistencyButton","retryPendingButton","pendingActions","pendingCountNode","allowExternalCheckbox","protectedPadsCheckbox","publicPadsCheckbox","padTypesNoneHint","allowlistRow","allowlistHint","allowlistTextarea","fieldNodes","saveUrl","healthUrl","consistencyUrl","retryPendingUrl","l10n","updateExternalSettingsVisibility","enabled","errorNode","updatePadTypesHint","noneEnabled","getPayload","data","setStatus","message","state","node","beginStatus","other","clearFieldErrors","field","input","showFieldError","updatePendingDeleteUi","count","pendingCount","postJson","url","payload","body","key","response","responseText","err","event","versionSuffix","error","details","suffix","bindingWithoutFile","checkbox"],"mappings":"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"} \ No newline at end of file diff --git a/l10n/de.js b/l10n/de.js index f59b7f3..768a3e9 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -53,6 +53,7 @@ OC.L10N.register( "Creating...": "Wird erstellt ...", "Delete linked Etherpad pad when .pad file is moved to trash": "Verknüpftes Etherpad-Pad löschen, wenn die .pad-Datei in den Papierkorb verschoben wird", "Detected API version:": "Erkannte API-Version:", + "Diagnostics": "Diagnose", "Embed configuration is incomplete.": "Embed-Konfiguration ist unvollständig.", "Etherpad API URL (optional)": "Etherpad-API-URL (optional)", "Etherpad API URL must not include query or fragment.": "Die Etherpad-API-URL darf keinen Query-String und kein Fragment enthalten.", @@ -66,12 +67,14 @@ OC.L10N.register( "Etherpad connection test failed.": "Etherpad-Verbindung konnte nicht geprüft werden.", "Etherpad connection test failed: {detail}": "Etherpad-Verbindung konnte nicht geprüft werden: {detail}", "Etherpad connection test successful.": "Etherpad-Verbindung erfolgreich geprüft.", + "Etherpad server": "Etherpad-Server", "Etherpad session cookie domain (optional)": "Etherpad-Session-Cookie-Domain (optional)", "External Etherpad host allowlist (optional)": "Erlaubte externe Etherpad-Hosts (optional)", "External allowlist URL must use https: {host}": "Die externe Allowlist-URL muss HTTPS verwenden: {host}", "External allowlist contains invalid host: {host}": "Die externe Allowlist enthält einen ungültigen Host: {host}", "External pad URL": "Externe Pad-URL", "External pad URL is required.": "Externe Pad-URL ist erforderlich.", + "External pads and embedding": "Externe Pads und Einbettung", "Failed to save settings.": "Einstellungen konnten nicht gespeichert werden.", "Failed to update test fault.": "Testfehler konnte nicht aktualisiert werden.", "File name": "Dateiname", @@ -106,6 +109,7 @@ OC.L10N.register( "Optional internal URL for server-side API calls. Leave empty to use Etherpad Base URL.": "Optionale interne URL für serverseitige API-Aufrufe. Leer lassen, um die Etherpad-Basis-URL zu verwenden.", "Pad from another server": "Pad von einem anderen Server", "Pad name is required.": "Pad-Name ist erforderlich.", + "Pad types and behaviour": "Pad-Typen und Verhalten", "Pads": "Pads", "Pending Etherpad deletes": "Ausstehende Etherpad-Löschungen", "Pending delete retry failed.": "Wiederholung ausstehender Löschungen fehlgeschlagen.", @@ -131,6 +135,7 @@ OC.L10N.register( "Test fault set: {fault}": "Testfehler gesetzt: {fault}", "Test faults are available only when Nextcloud debug mode is enabled.": "Testfehler sind nur verfügbar, wenn der Nextcloud-Debug-Modus aktiviert ist.", "Testing Etherpad connection...": "Etherpad-Verbindung wird geprüft ...", + "The connection test checks that Nextcloud reaches the Etherpad API with the configured key. The consistency check looks for pad links whose .pad file no longer exists.": "Der Verbindungstest prüft, ob Nextcloud die Etherpad-API mit dem hinterlegten Schlüssel erreicht. Die Konsistenzprüfung sucht nach Pad-Verknüpfungen, deren .pad-Datei nicht mehr existiert.", "This .pad file has no matching pad in this Nextcloud.": "Für diese .pad-Datei existiert in dieser Nextcloud kein passendes Pad.", "This file looks like a copy of an existing .pad file in your account. Open the original to keep editing the linked pad, or create a new pad to fork the content stored in this file.": "Diese Datei sieht aus wie eine Kopie einer bereits vorhandenen .pad-Datei in deinem Konto. Öffne das Original, um am verknüpften Pad weiterzuarbeiten, oder erstelle aus dem in dieser Datei gespeicherten Inhalt ein neues Pad als eigene Kopie.", "Trusted embed origins (optional)": "Vertrauenswürdige Embed-Origins (optional)", diff --git a/l10n/de.json b/l10n/de.json index 022818a..961f63f 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -52,6 +52,7 @@ "Creating...": "Wird erstellt ...", "Delete linked Etherpad pad when .pad file is moved to trash": "Verknüpftes Etherpad-Pad löschen, wenn die .pad-Datei in den Papierkorb verschoben wird", "Detected API version:": "Erkannte API-Version:", + "Diagnostics": "Diagnose", "Embed configuration is incomplete.": "Embed-Konfiguration ist unvollständig.", "Etherpad API URL (optional)": "Etherpad-API-URL (optional)", "Etherpad API URL must not include query or fragment.": "Die Etherpad-API-URL darf keinen Query-String und kein Fragment enthalten.", @@ -65,12 +66,14 @@ "Etherpad connection test failed.": "Etherpad-Verbindung konnte nicht geprüft werden.", "Etherpad connection test failed: {detail}": "Etherpad-Verbindung konnte nicht geprüft werden: {detail}", "Etherpad connection test successful.": "Etherpad-Verbindung erfolgreich geprüft.", + "Etherpad server": "Etherpad-Server", "Etherpad session cookie domain (optional)": "Etherpad-Session-Cookie-Domain (optional)", "External Etherpad host allowlist (optional)": "Erlaubte externe Etherpad-Hosts (optional)", "External allowlist URL must use https: {host}": "Die externe Allowlist-URL muss HTTPS verwenden: {host}", "External allowlist contains invalid host: {host}": "Die externe Allowlist enthält einen ungültigen Host: {host}", "External pad URL": "Externe Pad-URL", "External pad URL is required.": "Externe Pad-URL ist erforderlich.", + "External pads and embedding": "Externe Pads und Einbettung", "Failed to save settings.": "Einstellungen konnten nicht gespeichert werden.", "Failed to update test fault.": "Testfehler konnte nicht aktualisiert werden.", "File name": "Dateiname", @@ -105,6 +108,7 @@ "Optional internal URL for server-side API calls. Leave empty to use Etherpad Base URL.": "Optionale interne URL für serverseitige API-Aufrufe. Leer lassen, um die Etherpad-Basis-URL zu verwenden.", "Pad from another server": "Pad von einem anderen Server", "Pad name is required.": "Pad-Name ist erforderlich.", + "Pad types and behaviour": "Pad-Typen und Verhalten", "Pads": "Pads", "Pending Etherpad deletes": "Ausstehende Etherpad-Löschungen", "Pending delete retry failed.": "Wiederholung ausstehender Löschungen fehlgeschlagen.", @@ -130,6 +134,7 @@ "Test fault set: {fault}": "Testfehler gesetzt: {fault}", "Test faults are available only when Nextcloud debug mode is enabled.": "Testfehler sind nur verfügbar, wenn der Nextcloud-Debug-Modus aktiviert ist.", "Testing Etherpad connection...": "Etherpad-Verbindung wird geprüft ...", + "The connection test checks that Nextcloud reaches the Etherpad API with the configured key. The consistency check looks for pad links whose .pad file no longer exists.": "Der Verbindungstest prüft, ob Nextcloud die Etherpad-API mit dem hinterlegten Schlüssel erreicht. Die Konsistenzprüfung sucht nach Pad-Verknüpfungen, deren .pad-Datei nicht mehr existiert.", "This .pad file has no matching pad in this Nextcloud.": "Für diese .pad-Datei existiert in dieser Nextcloud kein passendes Pad.", "This file looks like a copy of an existing .pad file in your account. Open the original to keep editing the linked pad, or create a new pad to fork the content stored in this file.": "Diese Datei sieht aus wie eine Kopie einer bereits vorhandenen .pad-Datei in deinem Konto. Öffne das Original, um am verknüpften Pad weiterzuarbeiten, oder erstelle aus dem in dieser Datei gespeicherten Inhalt ein neues Pad als eigene Kopie.", "Trusted embed origins (optional)": "Vertrauenswürdige Embed-Origins (optional)", diff --git a/l10n/es.js b/l10n/es.js index 3c79b02..d953816 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -53,6 +53,7 @@ OC.L10N.register( "Creating...": "Creando...", "Delete linked Etherpad pad when .pad file is moved to trash": "Eliminar el pad de Etherpad enlazado cuando el archivo .pad se mueve a la papelera", "Detected API version:": "Versión de API detectada:", + "Diagnostics": "Diagnóstico", "Embed configuration is incomplete.": "La configuración de incrustación está incompleta.", "Etherpad API URL (optional)": "URL de API de Etherpad (opcional)", "Etherpad API URL must not include query or fragment.": "La URL API de Etherpad no debe incluir cadena de consulta ni fragmento.", @@ -66,12 +67,14 @@ OC.L10N.register( "Etherpad connection test failed.": "No se pudo comprobar la conexión con Etherpad.", "Etherpad connection test failed: {detail}": "No se pudo comprobar la conexión con Etherpad: {detail}", "Etherpad connection test successful.": "Conexión con Etherpad comprobada correctamente.", + "Etherpad server": "Servidor de Etherpad", "Etherpad session cookie domain (optional)": "Dominio de cookie de sesión de Etherpad (opcional)", "External Etherpad host allowlist (optional)": "Lista de hosts Etherpad externos permitidos (opcional)", "External allowlist URL must use https: {host}": "La URL de la lista de permitidos externa debe usar HTTPS: {host}", "External allowlist contains invalid host: {host}": "La lista de permitidos externa contiene un host no válido: {host}", "External pad URL": "URL de pad externo", "External pad URL is required.": "La URL de pad externo es obligatoria.", + "External pads and embedding": "Pads externos e incrustación", "Failed to save settings.": "No se han podido guardar los ajustes.", "Failed to update test fault.": "No se pudo actualizar el fallo de prueba.", "File name": "Nombre de archivo", @@ -106,6 +109,7 @@ OC.L10N.register( "Optional internal URL for server-side API calls. Leave empty to use Etherpad Base URL.": "URL interna opcional para llamadas a la API del lado del servidor. Déjala vacía para usar la URL base de Etherpad.", "Pad from another server": "Pad de otro servidor", "Pad name is required.": "Se requiere el nombre del pad.", + "Pad types and behaviour": "Tipos de pad y comportamiento", "Pads": "Pads", "Pending Etherpad deletes": "Eliminaciones pendientes en Etherpad", "Pending delete retry failed.": "No se pudieron reintentar las eliminaciones pendientes.", @@ -131,6 +135,7 @@ OC.L10N.register( "Test fault set: {fault}": "Fallo de prueba establecido: {fault}", "Test faults are available only when Nextcloud debug mode is enabled.": "Los fallos de prueba solo están disponibles cuando el modo debug de Nextcloud está activado.", "Testing Etherpad connection...": "Comprobando la conexión con Etherpad...", + "The connection test checks that Nextcloud reaches the Etherpad API with the configured key. The consistency check looks for pad links whose .pad file no longer exists.": "La prueba de conexión comprueba que Nextcloud alcanza la API de Etherpad con la clave configurada. La comprobación de consistencia busca enlaces de pad cuyo archivo .pad ya no existe.", "This .pad file has no matching pad in this Nextcloud.": "Este archivo .pad no tiene ningún pad correspondiente en este Nextcloud.", "This file looks like a copy of an existing .pad file in your account. Open the original to keep editing the linked pad, or create a new pad to fork the content stored in this file.": "Este archivo parece ser una copia de un archivo .pad existente en tu cuenta. Abre el original para seguir editando el pad enlazado, o crea un nuevo pad para crear una copia independiente del contenido almacenado en este archivo.", "Trusted embed origins (optional)": "Orígenes de incrustación de confianza (opcional)", diff --git a/l10n/es.json b/l10n/es.json index 096b3fc..c0c109f 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -52,6 +52,7 @@ "Creating...": "Creando...", "Delete linked Etherpad pad when .pad file is moved to trash": "Eliminar el pad de Etherpad enlazado cuando el archivo .pad se mueve a la papelera", "Detected API version:": "Versión de API detectada:", + "Diagnostics": "Diagnóstico", "Embed configuration is incomplete.": "La configuración de incrustación está incompleta.", "Etherpad API URL (optional)": "URL de API de Etherpad (opcional)", "Etherpad API URL must not include query or fragment.": "La URL API de Etherpad no debe incluir cadena de consulta ni fragmento.", @@ -65,12 +66,14 @@ "Etherpad connection test failed.": "No se pudo comprobar la conexión con Etherpad.", "Etherpad connection test failed: {detail}": "No se pudo comprobar la conexión con Etherpad: {detail}", "Etherpad connection test successful.": "Conexión con Etherpad comprobada correctamente.", + "Etherpad server": "Servidor de Etherpad", "Etherpad session cookie domain (optional)": "Dominio de cookie de sesión de Etherpad (opcional)", "External Etherpad host allowlist (optional)": "Lista de hosts Etherpad externos permitidos (opcional)", "External allowlist URL must use https: {host}": "La URL de la lista de permitidos externa debe usar HTTPS: {host}", "External allowlist contains invalid host: {host}": "La lista de permitidos externa contiene un host no válido: {host}", "External pad URL": "URL de pad externo", "External pad URL is required.": "La URL de pad externo es obligatoria.", + "External pads and embedding": "Pads externos e incrustación", "Failed to save settings.": "No se han podido guardar los ajustes.", "Failed to update test fault.": "No se pudo actualizar el fallo de prueba.", "File name": "Nombre de archivo", @@ -105,6 +108,7 @@ "Optional internal URL for server-side API calls. Leave empty to use Etherpad Base URL.": "URL interna opcional para llamadas a la API del lado del servidor. Déjala vacía para usar la URL base de Etherpad.", "Pad from another server": "Pad de otro servidor", "Pad name is required.": "Se requiere el nombre del pad.", + "Pad types and behaviour": "Tipos de pad y comportamiento", "Pads": "Pads", "Pending Etherpad deletes": "Eliminaciones pendientes en Etherpad", "Pending delete retry failed.": "No se pudieron reintentar las eliminaciones pendientes.", @@ -130,6 +134,7 @@ "Test fault set: {fault}": "Fallo de prueba establecido: {fault}", "Test faults are available only when Nextcloud debug mode is enabled.": "Los fallos de prueba solo están disponibles cuando el modo debug de Nextcloud está activado.", "Testing Etherpad connection...": "Comprobando la conexión con Etherpad...", + "The connection test checks that Nextcloud reaches the Etherpad API with the configured key. The consistency check looks for pad links whose .pad file no longer exists.": "La prueba de conexión comprueba que Nextcloud alcanza la API de Etherpad con la clave configurada. La comprobación de consistencia busca enlaces de pad cuyo archivo .pad ya no existe.", "This .pad file has no matching pad in this Nextcloud.": "Este archivo .pad no tiene ningún pad correspondiente en este Nextcloud.", "This file looks like a copy of an existing .pad file in your account. Open the original to keep editing the linked pad, or create a new pad to fork the content stored in this file.": "Este archivo parece ser una copia de un archivo .pad existente en tu cuenta. Abre el original para seguir editando el pad enlazado, o crea un nuevo pad para crear una copia independiente del contenido almacenado en este archivo.", "Trusted embed origins (optional)": "Orígenes de incrustación de confianza (opcional)", diff --git a/l10n/fr.js b/l10n/fr.js index 91674c7..559c41a 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -53,6 +53,7 @@ OC.L10N.register( "Creating...": "Création...", "Delete linked Etherpad pad when .pad file is moved to trash": "Supprimer le pad Etherpad lié lorsque le fichier .pad est déplacé vers la corbeille", "Detected API version:": "Version API détectée :", + "Diagnostics": "Diagnostic", "Embed configuration is incomplete.": "La configuration d'intégration est incomplète.", "Etherpad API URL (optional)": "URL API Etherpad (optionnel)", "Etherpad API URL must not include query or fragment.": "L'URL API Etherpad ne doit pas contenir de chaîne de requête ni de fragment.", @@ -66,12 +67,14 @@ OC.L10N.register( "Etherpad connection test failed.": "Impossible de vérifier la connexion à Etherpad.", "Etherpad connection test failed: {detail}": "Impossible de vérifier la connexion à Etherpad : {detail}", "Etherpad connection test successful.": "Connexion à Etherpad vérifiée avec succès.", + "Etherpad server": "Serveur Etherpad", "Etherpad session cookie domain (optional)": "Domaine du cookie de session Etherpad (optionnel)", "External Etherpad host allowlist (optional)": "Liste des hôtes Etherpad externes autorisés (facultatif)", "External allowlist URL must use https: {host}": "L'URL de la liste d'autorisation externe doit utiliser HTTPS : {host}", "External allowlist contains invalid host: {host}": "La liste d’autorisation externe contient un hôte invalide : {host}", "External pad URL": "URL du pad externe", "External pad URL is required.": "L'URL du pad externe est requise.", + "External pads and embedding": "Pads externes et intégration", "Failed to save settings.": "Impossible d'enregistrer les paramètres.", "Failed to update test fault.": "Échec de la mise à jour de l'erreur de test.", "File name": "Nom de fichier", @@ -106,6 +109,7 @@ OC.L10N.register( "Optional internal URL for server-side API calls. Leave empty to use Etherpad Base URL.": "URL interne optionnelle pour les appels à l'API côté serveur. Laisser vide pour utiliser l'URL de base Etherpad.", "Pad from another server": "Pad d'un autre serveur", "Pad name is required.": "Le nom du pad est obligatoire.", + "Pad types and behaviour": "Types de pads et comportement", "Pads": "Pads", "Pending Etherpad deletes": "Suppressions Etherpad en attente", "Pending delete retry failed.": "Impossible de réessayer les suppressions en attente.", @@ -131,6 +135,7 @@ OC.L10N.register( "Test fault set: {fault}": "Défaut de test défini : {fault}", "Test faults are available only when Nextcloud debug mode is enabled.": "Les défauts de test sont disponibles uniquement lorsque le mode debug Nextcloud est activé.", "Testing Etherpad connection...": "Vérification de la connexion à Etherpad...", + "The connection test checks that Nextcloud reaches the Etherpad API with the configured key. The consistency check looks for pad links whose .pad file no longer exists.": "Le test de connexion vérifie que Nextcloud atteint l'API Etherpad avec la clé configurée. Le contrôle de cohérence recherche les liens de pad dont le fichier .pad n'existe plus.", "This .pad file has no matching pad in this Nextcloud.": "Ce fichier .pad n'a pas de pad correspondant dans ce Nextcloud.", "This file looks like a copy of an existing .pad file in your account. Open the original to keep editing the linked pad, or create a new pad to fork the content stored in this file.": "Ce fichier ressemble à une copie d'un fichier .pad existant dans votre compte. Ouvrez l'original pour continuer à modifier le pad lié, ou créez un nouveau pad pour dupliquer le contenu stocké dans ce fichier.", "Trusted embed origins (optional)": "Origines d'intégration de confiance (optionnel)", diff --git a/l10n/fr.json b/l10n/fr.json index 65a4e67..0a99e2d 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -52,6 +52,7 @@ "Creating...": "Création...", "Delete linked Etherpad pad when .pad file is moved to trash": "Supprimer le pad Etherpad lié lorsque le fichier .pad est déplacé vers la corbeille", "Detected API version:": "Version API détectée :", + "Diagnostics": "Diagnostic", "Embed configuration is incomplete.": "La configuration d'intégration est incomplète.", "Etherpad API URL (optional)": "URL API Etherpad (optionnel)", "Etherpad API URL must not include query or fragment.": "L'URL API Etherpad ne doit pas contenir de chaîne de requête ni de fragment.", @@ -65,12 +66,14 @@ "Etherpad connection test failed.": "Impossible de vérifier la connexion à Etherpad.", "Etherpad connection test failed: {detail}": "Impossible de vérifier la connexion à Etherpad : {detail}", "Etherpad connection test successful.": "Connexion à Etherpad vérifiée avec succès.", + "Etherpad server": "Serveur Etherpad", "Etherpad session cookie domain (optional)": "Domaine du cookie de session Etherpad (optionnel)", "External Etherpad host allowlist (optional)": "Liste des hôtes Etherpad externes autorisés (facultatif)", "External allowlist URL must use https: {host}": "L'URL de la liste d'autorisation externe doit utiliser HTTPS : {host}", "External allowlist contains invalid host: {host}": "La liste d’autorisation externe contient un hôte invalide : {host}", "External pad URL": "URL du pad externe", "External pad URL is required.": "L'URL du pad externe est requise.", + "External pads and embedding": "Pads externes et intégration", "Failed to save settings.": "Impossible d'enregistrer les paramètres.", "Failed to update test fault.": "Échec de la mise à jour de l'erreur de test.", "File name": "Nom de fichier", @@ -105,6 +108,7 @@ "Optional internal URL for server-side API calls. Leave empty to use Etherpad Base URL.": "URL interne optionnelle pour les appels à l'API côté serveur. Laisser vide pour utiliser l'URL de base Etherpad.", "Pad from another server": "Pad d'un autre serveur", "Pad name is required.": "Le nom du pad est obligatoire.", + "Pad types and behaviour": "Types de pads et comportement", "Pads": "Pads", "Pending Etherpad deletes": "Suppressions Etherpad en attente", "Pending delete retry failed.": "Impossible de réessayer les suppressions en attente.", @@ -130,6 +134,7 @@ "Test fault set: {fault}": "Défaut de test défini : {fault}", "Test faults are available only when Nextcloud debug mode is enabled.": "Les défauts de test sont disponibles uniquement lorsque le mode debug Nextcloud est activé.", "Testing Etherpad connection...": "Vérification de la connexion à Etherpad...", + "The connection test checks that Nextcloud reaches the Etherpad API with the configured key. The consistency check looks for pad links whose .pad file no longer exists.": "Le test de connexion vérifie que Nextcloud atteint l'API Etherpad avec la clé configurée. Le contrôle de cohérence recherche les liens de pad dont le fichier .pad n'existe plus.", "This .pad file has no matching pad in this Nextcloud.": "Ce fichier .pad n'a pas de pad correspondant dans ce Nextcloud.", "This file looks like a copy of an existing .pad file in your account. Open the original to keep editing the linked pad, or create a new pad to fork the content stored in this file.": "Ce fichier ressemble à une copie d'un fichier .pad existant dans votre compte. Ouvrez l'original pour continuer à modifier le pad lié, ou créez un nouveau pad pour dupliquer le contenu stocké dans ce fichier.", "Trusted embed origins (optional)": "Origines d'intégration de confiance (optionnel)", diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index cbe10b1..6fe12ae 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -73,6 +73,11 @@ public function getForm(): TemplateResponse { 'l10n' => [ 'section_title' => $this->l10n->t('Pads'), 'intro' => $this->l10n->t('Configure the Etherpad server and external pad security policy for the Etherpad Nextcloud app.'), + 'section_server' => $this->l10n->t('Etherpad server'), + 'section_pad_types' => $this->l10n->t('Pad types and behaviour'), + 'section_external' => $this->l10n->t('External pads and embedding'), + 'section_diagnostics' => $this->l10n->t('Diagnostics'), + 'section_diagnostics_hint' => $this->l10n->t('The connection test checks that Nextcloud reaches the Etherpad API with the configured key. The consistency check looks for pad links whose .pad file no longer exists.'), 'etherpad_base_url' => $this->l10n->t('Etherpad Base URL'), 'etherpad_api_url' => $this->l10n->t('Etherpad API URL (optional)'), 'etherpad_api_url_hint' => $this->l10n->t('Optional internal URL for server-side API calls. Leave empty to use Etherpad Base URL.'), diff --git a/src/admin-settings.js b/src/admin-settings.js index 5a13674..c57cf90 100644 --- a/src/admin-settings.js +++ b/src/admin-settings.js @@ -4,6 +4,7 @@ const root = document.getElementById('etherpad-nextcloud-admin-settings') const form = document.getElementById('etherpad-nextcloud-admin-form') const statusNode = document.getElementById('etherpad-nextcloud-admin-status') + const diagnosticsStatusNode = document.getElementById('etherpad-nextcloud-diagnostics-status') const healthButton = document.getElementById('etherpad-nextcloud-health-check') const consistencyButton = document.getElementById('etherpad-nextcloud-consistency-check') const retryPendingButton = document.getElementById('etherpad-nextcloud-retry-pending') @@ -79,9 +80,9 @@ && !protectedPadsCheckbox.checked && !publicPadsCheckbox.checked if (padTypesNoneHint instanceof HTMLElement) { - // Toggle `display` rather than the `hidden` attribute: the element - // also carries Nextcloud's `.settings-hint` class, whose rule wins - // over the user-agent `[hidden]` default. + // Toggle `display` rather than the `hidden` attribute: our own + // `.settings-hint { display: block }` rule is ID-scoped and so + // outweighs the user-agent `[hidden]` default. padTypesNoneHint.style.display = noneEnabled ? '' : 'none' } } @@ -103,14 +104,33 @@ } } - function setStatus(message, state) { - statusNode.textContent = message - statusNode.classList.remove('ep-status-success', 'ep-status-error') + // Saving and the diagnostic tools live in separate sections, so each + // reports next to its own buttons. Writing a status only ever touches its + // own area — the two run independently, and a response arriving late must + // not wipe out the other action's result. + function setStatus(message, state, node = statusNode) { + if (!(node instanceof HTMLElement)) { + return + } + node.textContent = message + node.classList.remove('ep-status-success', 'ep-status-error') if (state === 'success') { - statusNode.classList.add('ep-status-success') + node.classList.add('ep-status-success') } else if (state === 'error') { - statusNode.classList.add('ep-status-error') + node.classList.add('ep-status-error') + } + } + + // Starting an action does clear the other area: its result predates this + // action and could otherwise be read as belonging to it. + function beginStatus(message, node = statusNode) { + for (const other of [statusNode, diagnosticsStatusNode]) { + if (other instanceof HTMLElement && other !== node) { + other.textContent = '' + other.classList.remove('ep-status-success', 'ep-status-error') + } } + setStatus(message, null, node) } function clearFieldErrors() { @@ -197,7 +217,7 @@ form.addEventListener('submit', async (event) => { event.preventDefault() clearFieldErrors() - setStatus(l10n.saving, null) + beginStatus(l10n.saving) try { const data = await postJson(saveUrl, getPayload()) const versionSuffix = data && data.api_version ? ` api=${String(data.api_version)}` : '' @@ -212,7 +232,7 @@ healthButton.addEventListener('click', async () => { clearFieldErrors() - setStatus(l10n.checking, null) + beginStatus(l10n.checking, diagnosticsStatusNode) try { const data = await postJson(healthUrl, getPayload()) const details = [] @@ -233,26 +253,26 @@ } const suffix = details.length > 0 ? ` ${details.join(' | ')}` : '' const message = `${String(data.message || l10n.healthOk)}${suffix}` - setStatus(message, 'success') + setStatus(message, 'success', diagnosticsStatusNode) } catch (error) { if (error && typeof error.field === 'string' && error.field !== '') { showFieldError(error.field, error.message || l10n.healthFailed) } - setStatus(error instanceof Error ? error.message : l10n.healthFailed, 'error') + setStatus(error instanceof Error ? error.message : l10n.healthFailed, 'error', diagnosticsStatusNode) } }) if (consistencyButton instanceof HTMLElement) { consistencyButton.addEventListener('click', async () => { clearFieldErrors() - setStatus(l10n.consistencyRunning, null) + beginStatus(l10n.consistencyRunning, diagnosticsStatusNode) try { const data = await postJson(consistencyUrl, {}) const bindingWithoutFile = Number(data.binding_without_file_count || 0) const message = `${String(data.message || l10n.consistencyOk)} binding_without_file=${String(bindingWithoutFile)}` - setStatus(message, bindingWithoutFile > 0 ? 'error' : 'success') + setStatus(message, bindingWithoutFile > 0 ? 'error' : 'success', diagnosticsStatusNode) } catch (error) { - setStatus(error instanceof Error ? error.message : l10n.consistencyFailed, 'error') + setStatus(error instanceof Error ? error.message : l10n.consistencyFailed, 'error', diagnosticsStatusNode) } }) } @@ -260,7 +280,7 @@ if (retryPendingButton instanceof HTMLElement) { retryPendingButton.addEventListener('click', async () => { clearFieldErrors() - setStatus(l10n.checking, null) + beginStatus(l10n.checking, diagnosticsStatusNode) try { const data = await postJson(retryPendingUrl, {}) const details = [] @@ -280,9 +300,9 @@ updatePendingDeleteUi(Number(data.remaining || 0)) } const suffix = details.length > 0 ? ` ${details.join(' | ')}` : '' - setStatus(`${String(data.message || 'OK')}${suffix}`, 'success') + setStatus(`${String(data.message || 'OK')}${suffix}`, 'success', diagnosticsStatusNode) } catch (error) { - setStatus(error instanceof Error ? error.message : l10n.retryFailed, 'error') + setStatus(error instanceof Error ? error.message : l10n.retryFailed, 'error', diagnosticsStatusNode) } }) } diff --git a/templates/admin-settings.php b/templates/admin-settings.php index 7e392bc..711779d 100644 --- a/templates/admin-settings.php +++ b/templates/admin-settings.php @@ -28,6 +28,8 @@ class="section"
diff --git a/tests/js/admin-settings.test.js b/tests/js/admin-settings.test.js new file mode 100644 index 0000000..1b5d930 --- /dev/null +++ b/tests/js/admin-settings.test.js @@ -0,0 +1,136 @@ +/** + * SPDX-License-Identifier: AGPL-3.0-or-later + * Copyright (c) 2026 Jacob Bühler + */ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const MODULE = '../../src/admin-settings.js' + +const setupAdminDom = () => { + document.body.innerHTML = ` +- + @@ -89,34 +89,39 @@ class="section" checked> - +
- -
style="display: none">
+ + +- +
- + diff --git a/tests/js/admin-settings.test.js b/tests/js/admin-settings.test.js index b515985..f13c864 100644 --- a/tests/js/admin-settings.test.js +++ b/tests/js/admin-settings.test.js @@ -25,6 +25,7 @@ const setupAdminDom = () => { + @@ -147,3 +148,69 @@ describe('admin settings status areas', () => { expect(saveStatus().classList.contains('ep-status-success')).toBe(true) }) }) + +describe('pad types live region', () => { + const hint = () => document.getElementById('pad-types-none-hint') + const protectedBox = () => document.querySelector('[name="enable_protected_pads"]') + const publicBox = () => document.querySelector('[name="enable_public_pads"]') + + const uncheck = (box) => { + box.checked = false + box.dispatchEvent(new Event('change', { bubbles: true })) + } + + beforeEach(() => { + vi.resetModules() + global.OC = { requestToken: 'token' } + setupAdminDom() + vi.stubGlobal('fetch', vi.fn()) + }) + + afterEach(() => { + document.body.innerHTML = '' + delete global.OC + vi.unstubAllGlobals() + }) + + it('writes the message as text so the live region announces it', async () => { + await import(MODULE) + expect(hint().textContent).toBe('') + + uncheck(protectedBox()) + uncheck(publicBox()) + + // Text, not visibility: an element returning from display:none is not + // a content change and would go unannounced. + expect(hint().textContent).toBe('No pad type is enabled.') + expect(hint().style.display).toBe('') + }) + + it('clears the text again once a pad type is re-enabled', async () => { + await import(MODULE) + uncheck(protectedBox()) + uncheck(publicBox()) + expect(hint().textContent).not.toBe('') + + publicBox().checked = true + publicBox().dispatchEvent(new Event('change', { bubbles: true })) + + expect(hint().textContent).toBe('') + }) + + it('leaves already-rendered text untouched on load', async () => { + // Server-rendered state: both types off, message already present. A + // rewrite would announce it as if the admin had just changed it. + hint().textContent = 'No pad type is enabled.' + protectedBox().checked = false + publicBox().checked = false + const observed = [] + new MutationObserver((records) => observed.push(...records)) + .observe(hint(), { childList: true, characterData: true, subtree: true }) + + await import(MODULE) + await flush() + + expect(hint().textContent).toBe('No pad type is enabled.') + expect(observed).toHaveLength(0) + }) +}) From c36c93061c3443b41f2a285e19201229ee555563 Mon Sep 17 00:00:00 2001 From: Jaggob <37583151+Jaggob@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:08:31 +0200 Subject: [PATCH 4/4] Trim the comments added while reviewing The explanations were written for the review and read as heavy in the final code. Kept only the reasoning that is not visible from the code itself, at roughly a third the length. --- css/admin-settings.css | 39 ++++++-------------- js/etherpad_nextcloud-admin-settings.mjs.map | 2 +- src/admin-settings.js | 25 +++++-------- templates/admin-settings.php | 5 +-- tests/e2e/fixtures/nextcloud.ts | 1 - tests/js/admin-settings.test.js | 19 ++++------ 6 files changed, 31 insertions(+), 60 deletions(-) diff --git a/css/admin-settings.css b/css/admin-settings.css index a56e3fd..7a37f71 100644 --- a/css/admin-settings.css +++ b/css/admin-settings.css @@ -10,12 +10,8 @@ display: block; } -/* Muted like Nextcloud's own setting descriptions, but at body text size. - Capped at the input width so the text column lines up with the fields. - - A row followed by a hint is marked as such and drops its own bottom - margin, instead of the hint pulling itself up with a negative one — that - kept the spacing dependent on whichever `p` rule happened to apply. */ +/* Rows followed by a hint set their own gap, so it does not depend on + whichever `p` rule wins. */ #etherpad-nextcloud-admin-settings .ep-field-row { margin-bottom: 4px; } @@ -27,26 +23,20 @@ color: var(--color-text-maxcontrast); } -/* A hint that carries no text takes up no space either. Lets the script - clear a live region's text instead of hiding the element, which would - take it out of the accessibility tree and silence the announcement. */ +/* Lets the script empty a live region instead of hiding it — see + updatePadTypesHint(). */ #etherpad-nextcloud-admin-settings .ep-field-hint:empty { margin: 0; } -/* Not an explanation but a reported value, so it keeps normal body text - styling instead of the muted hint treatment. */ +/* A reported value, not an explanation: no muted hint styling. */ #etherpad-nextcloud-admin-settings .ep-detected-value { margin-top: 0; margin-bottom: 14px; } -/* Hints belonging to a checkbox line up under its label text rather than - under the box itself, so the pairing reads at a glance. */ -/* An option and its description form one block: no gap worth mentioning - between them, a clear one to the next option. Both margins are set - outright rather than pulled back with negative values, so the result - doesn't depend on which surrounding rule happens to win. */ +/* An option and its description form one block: tight between them, + clear gap to the next option, hint indented under the label text. */ #etherpad-nextcloud-admin-settings .ep-checkbox-row { margin-bottom: 0; } @@ -61,9 +51,8 @@ padding-left: 26px; } -/* Smaller than a bare h3 so it doesn't compete with the page title, but - dark and bold like Nextcloud's own section names — muted or uppercase - headings would read as secondary text next to the field labels. */ +/* Dark and bold like Nextcloud's own section names; muted or uppercase + would read as secondary text next to the field labels. */ #etherpad-nextcloud-admin-settings .ep-section-heading { margin: 0 0 14px; padding-top: 22px; @@ -73,7 +62,6 @@ color: var(--color-main-text); } -/* The first heading follows the page intro, so it needs no divider. */ #etherpad-nextcloud-admin-settings form > .ep-section-heading:first-child { padding-top: 0; border-top: none; @@ -83,8 +71,7 @@ margin-bottom: 6px; } -/* Nextcloud mutes form labels by default; field names are primary content - here, so they keep full contrast like the section headings. */ +/* Nextcloud mutes form labels; field names are primary content here. */ #etherpad-nextcloud-admin-settings label { display: block; font-weight: 600; @@ -101,9 +88,7 @@ max-width: 640px; } -/* Nextcloud's checkbox styling mutes the label, which left it looking the - same as the description underneath. Restore full contrast and a little - extra weight so the option reads as the heading of its own block. */ +/* Same reason, plus weight: the option heads its own block. */ #etherpad-nextcloud-admin-settings label.checkbox { display: inline-flex; align-items: center; @@ -118,8 +103,6 @@ gap: 10px; } -/* Both the save and the diagnostics area carry `ep-status`, so the rule keys - off the class rather than naming a single element. */ #etherpad-nextcloud-admin-settings .ep-status.ep-status-success, #etherpad-nextcloud-admin-settings .ep-status.ep-status-error { color: var(--color-main-text, var(--color-text-maxcontrast, #1f2937)); diff --git a/js/etherpad_nextcloud-admin-settings.mjs.map b/js/etherpad_nextcloud-admin-settings.mjs.map index bd3f25d..933fcfb 100644 --- a/js/etherpad_nextcloud-admin-settings.mjs.map +++ b/js/etherpad_nextcloud-admin-settings.mjs.map @@ -1 +1 @@ -{"version":3,"file":"etherpad_nextcloud-admin-settings.mjs","sources":["../src/admin-settings.js"],"sourcesContent":["(function () {\n\t'use strict'\n\n\tconst root = document.getElementById('etherpad-nextcloud-admin-settings')\n\tconst form = document.getElementById('etherpad-nextcloud-admin-form')\n\tconst statusNode = document.getElementById('etherpad-nextcloud-admin-status')\n\tconst diagnosticsStatusNode = document.getElementById('etherpad-nextcloud-diagnostics-status')\n\t// Only the save area is required at startup. Markup rendered before the\n\t// diagnostics area existed still has to show diagnostics feedback\n\t// somewhere, so fall back to the save area instead of dropping it.\n\tconst diagnosticsTarget = diagnosticsStatusNode instanceof HTMLElement\n\t\t? diagnosticsStatusNode\n\t\t: statusNode\n\tconst healthButton = document.getElementById('etherpad-nextcloud-health-check')\n\tconst consistencyButton = document.getElementById('etherpad-nextcloud-consistency-check')\n\tconst retryPendingButton = document.getElementById('etherpad-nextcloud-retry-pending')\n\tconst pendingActions = document.getElementById('etherpad-nextcloud-pending-actions')\n\tconst pendingCountNode = document.getElementById('etherpad-nextcloud-pending-count')\n\tconst allowExternalCheckbox = form ? form.querySelector('input[name=\"allow_external_pads\"]') : null\n\tconst protectedPadsCheckbox = form ? form.querySelector('input[name=\"enable_protected_pads\"]') : null\n\tconst publicPadsCheckbox = form ? form.querySelector('input[name=\"enable_public_pads\"]') : null\n\tconst padTypesNoneHint = document.getElementById('pad-types-none-hint')\n\tconst allowlistRow = document.getElementById('external-pad-allowlist-row')\n\tconst allowlistHint = document.getElementById('external-pad-allowlist-hint')\n\tconst allowlistTextarea = document.getElementById('external-pad-allowlist')\n\tconst fieldNodes = {\n\t\tetherpad_host: form.querySelector('[name=\"etherpad_host\"]'),\n\t\tetherpad_api_host: form.querySelector('[name=\"etherpad_api_host\"]'),\n\t\tetherpad_cookie_domain: form.querySelector('[name=\"etherpad_cookie_domain\"]'),\n\t\tetherpad_api_key: form.querySelector('[name=\"etherpad_api_key\"]'),\n\t\tsync_interval_seconds: form.querySelector('[name=\"sync_interval_seconds\"]'),\n\t\texternal_pad_allowlist: form.querySelector('[name=\"external_pad_allowlist\"]'),\n\t\ttrusted_embed_origins: form.querySelector('[name=\"trusted_embed_origins\"]'),\n\t}\n\n\tif (!root || !form || !statusNode || !healthButton) {\n\t\treturn\n\t}\n\n\tconst saveUrl = root.getAttribute('data-save-url') || ''\n\tconst healthUrl = root.getAttribute('data-health-url') || ''\n\tconst consistencyUrl = root.getAttribute('data-consistency-url') || ''\n\tconst retryPendingUrl = root.getAttribute('data-retry-pending-url') || ''\n\tconst l10n = {\n\t\tsaving: root.getAttribute('data-l10n-saving') || 'Saving settings...',\n\t\tsaved: root.getAttribute('data-l10n-saved') || 'Settings saved.',\n\t\tchecking: root.getAttribute('data-l10n-checking') || 'Testing Etherpad connection...',\n\t\tconsistencyRunning: root.getAttribute('data-l10n-consistency-running') || 'Running consistency check...',\n\t\thealthOk: root.getAttribute('data-l10n-health-ok') || 'Etherpad connection test successful.',\n\t\tconsistencyOk: root.getAttribute('data-l10n-consistency-ok') || 'Consistency check successful.',\n\t\trequestFailed: root.getAttribute('data-l10n-request-failed') || 'Request failed.',\n\t\tsavingFailed: root.getAttribute('data-l10n-saving-failed') || 'Failed to save settings.',\n\t\thealthFailed: root.getAttribute('data-l10n-health-failed') || 'Etherpad connection test failed.',\n\t\tconsistencyFailed: root.getAttribute('data-l10n-consistency-failed') || 'Consistency check failed.',\n\t\tpendingDeleteLabel: root.getAttribute('data-l10n-pending-delete-label') || 'Pending Etherpad deletes',\n\t\tretryFailed: root.getAttribute('data-l10n-retry-failed') || 'Pending delete retry failed.',\n\t}\n\n\tif (saveUrl === '' || healthUrl === '' || consistencyUrl === '' || retryPendingUrl === '') {\n\t\treturn\n\t}\n\n\tfunction updateExternalSettingsVisibility() {\n\t\tconst enabled = !!(allowExternalCheckbox && allowExternalCheckbox.checked)\n\t\tif (allowlistRow) {\n\t\t\tallowlistRow.style.display = enabled ? '' : 'none'\n\t\t}\n\t\tif (allowlistHint) {\n\t\t\tallowlistHint.style.display = enabled ? '' : 'none'\n\t\t}\n\t\tif (allowlistTextarea instanceof HTMLTextAreaElement) {\n\t\t\tallowlistTextarea.disabled = !enabled\n\t\t\tif (!enabled) {\n\t\t\t\tallowlistTextarea.classList.remove('ep-input-error')\n\t\t\t\tconst errorNode = form.querySelector('[data-field-error=\"external_pad_allowlist\"]')\n\t\t\t\tif (errorNode instanceof HTMLElement) {\n\t\t\t\t\terrorNode.textContent = ''\n\t\t\t\t\terrorNode.classList.remove('is-visible')\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction updatePadTypesHint() {\n\t\tconst noneEnabled = !!(protectedPadsCheckbox && publicPadsCheckbox)\n\t\t\t&& !protectedPadsCheckbox.checked\n\t\t\t&& !publicPadsCheckbox.checked\n\t\tif (padTypesNoneHint instanceof HTMLElement) {\n\t\t\t// Set the text rather than toggling visibility. A live region only\n\t\t\t// announces content *changes*; an element coming back from\n\t\t\t// `display: none` was not in the accessibility tree before, so its\n\t\t\t// text counts as initial content and commonly stays unspoken. The\n\t\t\t// empty paragraph collapses via `.ep-field-hint:empty`, so this\n\t\t\t// looks the same as hiding it.\n\t\t\tconst message = noneEnabled ? (padTypesNoneHint.dataset.message || '') : ''\n\t\t\t// Rewriting the same text would announce it again on load.\n\t\t\tif (padTypesNoneHint.textContent !== message) {\n\t\t\t\tpadTypesNoneHint.textContent = message\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction getPayload() {\n\t\tconst data = new FormData(form)\n\t\treturn {\n\t\t\tetherpad_host: String(data.get('etherpad_host') || '').trim(),\n\t\t\tetherpad_api_host: String(data.get('etherpad_api_host') || '').trim(),\n\t\t\tetherpad_cookie_domain: String(data.get('etherpad_cookie_domain') || '').trim(),\n\t\t\tetherpad_api_key: String(data.get('etherpad_api_key') || '').trim(),\n\t\t\tsync_interval_seconds: Number(data.get('sync_interval_seconds') || 120),\n\t\t\tdelete_on_trash: data.has('delete_on_trash'),\n\t\t\tenable_protected_pads: data.has('enable_protected_pads'),\n\t\t\tenable_public_pads: data.has('enable_public_pads'),\n\t\t\tallow_external_pads: data.has('allow_external_pads'),\n\t\t\texternal_pad_allowlist: String(data.get('external_pad_allowlist') || ''),\n\t\t\ttrusted_embed_origins: String(data.get('trusted_embed_origins') || ''),\n\t\t}\n\t}\n\n\t// Saving and the diagnostic tools live in separate sections, so each\n\t// reports next to its own buttons. Writing a status only ever touches its\n\t// own area — the two run independently, and a response arriving late must\n\t// not wipe out the other action's result.\n\tfunction setStatus(message, state, node = statusNode) {\n\t\tif (!(node instanceof HTMLElement)) {\n\t\t\treturn\n\t\t}\n\t\tnode.textContent = message\n\t\tnode.classList.remove('ep-status-success', 'ep-status-error')\n\t\tif (state === 'success') {\n\t\t\tnode.classList.add('ep-status-success')\n\t\t} else if (state === 'error') {\n\t\t\tnode.classList.add('ep-status-error')\n\t\t}\n\t}\n\n\t// Starting an action does clear the other area: its result predates this\n\t// action and could otherwise be read as belonging to it.\n\tfunction beginStatus(message, node = statusNode) {\n\t\tfor (const other of [statusNode, diagnosticsTarget]) {\n\t\t\tif (other instanceof HTMLElement && other !== node) {\n\t\t\t\tother.textContent = ''\n\t\t\t\tother.classList.remove('ep-status-success', 'ep-status-error')\n\t\t\t}\n\t\t}\n\t\tsetStatus(message, null, node)\n\t}\n\n\tfunction clearFieldErrors() {\n\t\tObject.keys(fieldNodes).forEach((field) => {\n\t\t\tconst input = fieldNodes[field]\n\t\t\tif (input instanceof HTMLElement) {\n\t\t\t\tinput.classList.remove('ep-input-error')\n\t\t\t}\n\t\t\tconst errorNode = form.querySelector(`[data-field-error=\"${field}\"]`)\n\t\t\tif (errorNode instanceof HTMLElement) {\n\t\t\t\terrorNode.textContent = ''\n\t\t\t\terrorNode.classList.remove('is-visible')\n\t\t\t}\n\t\t})\n\t}\n\n\tfunction showFieldError(field, message) {\n\t\tif (!field || !message) {\n\t\t\treturn\n\t\t}\n\t\tconst input = fieldNodes[field]\n\t\tif (input instanceof HTMLElement) {\n\t\t\tinput.classList.add('ep-input-error')\n\t\t}\n\t\tconst errorNode = form.querySelector(`[data-field-error=\"${field}\"]`)\n\t\tif (errorNode instanceof HTMLElement) {\n\t\t\terrorNode.textContent = message\n\t\t\terrorNode.classList.add('is-visible')\n\t\t}\n\t\tif (input instanceof HTMLElement && typeof input.focus === 'function') {\n\t\t\tinput.focus()\n\t\t}\n\t}\n\n\tfunction updatePendingDeleteUi(count) {\n\t\tconst pendingCount = Number.isFinite(Number(count)) ? Number(count) : 0\n\t\tif (pendingActions instanceof HTMLElement) {\n\t\t\tpendingActions.style.display = pendingCount > 0 ? '' : 'none'\n\t\t}\n\t\tif (pendingCountNode instanceof HTMLElement) {\n\t\t\tpendingCountNode.textContent = `${l10n.pendingDeleteLabel}: ${String(pendingCount)}`\n\t\t}\n\t\tif (retryPendingButton instanceof HTMLButtonElement) {\n\t\t\tretryPendingButton.disabled = pendingCount <= 0\n\t\t}\n\t}\n\n\tasync function postJson(url, payload) {\n\t\tconst body = new URLSearchParams()\n\t\tObject.keys(payload).forEach((key) => {\n\t\t\tbody.set(key, String(payload[key]))\n\t\t})\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\tcredentials: 'same-origin',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',\n\t\t\t\trequesttoken: String(OC.requestToken || ''),\n\t\t\t},\n\t\t\tbody: body.toString(),\n\t\t})\n\n\t\tlet data = null\n\t\tconst responseText = await response.text()\n\t\ttry {\n\t\t\tdata = responseText !== '' ? JSON.parse(responseText) : null\n\t\t} catch (error) {\n\t\t\tdata = null\n\t\t}\n\n\t\tif (!response.ok || !data || data.ok !== true) {\n\t\t\tconst message = (data && data.message)\n\t\t\t\t? String(data.message)\n\t\t\t\t: (responseText !== '' ? responseText.slice(0, 200) : l10n.requestFailed)\n\t\t\tconst err = new Error(message)\n\t\t\terr.field = (data && typeof data.field === 'string') ? data.field : ''\n\t\t\tthrow err\n\t\t}\n\n\t\treturn data\n\t}\n\n\tform.addEventListener('submit', async (event) => {\n\t\tevent.preventDefault()\n\t\tclearFieldErrors()\n\t\tbeginStatus(l10n.saving)\n\t\ttry {\n\t\t\tconst data = await postJson(saveUrl, getPayload())\n\t\t\tconst versionSuffix = data && data.api_version ? ` api=${String(data.api_version)}` : ''\n\t\t\tsetStatus(`${String(data.message || l10n.saved)}${versionSuffix}`, 'success')\n\t\t} catch (error) {\n\t\t\tif (error && typeof error.field === 'string' && error.field !== '') {\n\t\t\t\tshowFieldError(error.field, error.message || l10n.savingFailed)\n\t\t\t}\n\t\t\tsetStatus(error instanceof Error ? error.message : l10n.savingFailed, 'error')\n\t\t}\n\t})\n\n\thealthButton.addEventListener('click', async () => {\n\t\tclearFieldErrors()\n\t\tbeginStatus(l10n.checking, diagnosticsTarget)\n\t\ttry {\n\t\t\tconst data = await postJson(healthUrl, getPayload())\n\t\t\tconst details = []\n\t\t\tif (typeof data.pad_count !== 'undefined') {\n\t\t\t\tdetails.push(`pad_count=${String(data.pad_count)}`)\n\t\t\t}\n\t\t\tif (typeof data.api_version === 'string' && data.api_version.trim() !== '') {\n\t\t\t\tdetails.push(`api=${data.api_version}`)\n\t\t\t}\n\t\t\tif (typeof data.latency_ms !== 'undefined') {\n\t\t\t\tdetails.push(`latency=${String(data.latency_ms)}ms`)\n\t\t\t}\n\t\t\tif (typeof data.target === 'string' && data.target.trim() !== '') {\n\t\t\t\tdetails.push(`target=${data.target}`)\n\t\t\t}\n\t\t\tif (typeof data.pending_delete_count !== 'undefined') {\n\t\t\t\tupdatePendingDeleteUi(Number(data.pending_delete_count))\n\t\t\t}\n\t\t\tconst suffix = details.length > 0 ? ` ${details.join(' | ')}` : ''\n\t\t\tconst message = `${String(data.message || l10n.healthOk)}${suffix}`\n\t\t\tsetStatus(message, 'success', diagnosticsTarget)\n\t\t} catch (error) {\n\t\t\tif (error && typeof error.field === 'string' && error.field !== '') {\n\t\t\t\tshowFieldError(error.field, error.message || l10n.healthFailed)\n\t\t\t}\n\t\t\tsetStatus(error instanceof Error ? error.message : l10n.healthFailed, 'error', diagnosticsTarget)\n\t\t}\n\t})\n\n\tif (consistencyButton instanceof HTMLElement) {\n\t\tconsistencyButton.addEventListener('click', async () => {\n\t\t\tclearFieldErrors()\n\t\t\tbeginStatus(l10n.consistencyRunning, diagnosticsTarget)\n\t\t\ttry {\n\t\t\t\tconst data = await postJson(consistencyUrl, {})\n\t\t\t\tconst bindingWithoutFile = Number(data.binding_without_file_count || 0)\n\t\t\t\tconst message = `${String(data.message || l10n.consistencyOk)} binding_without_file=${String(bindingWithoutFile)}`\n\t\t\t\tsetStatus(message, bindingWithoutFile > 0 ? 'error' : 'success', diagnosticsTarget)\n\t\t\t} catch (error) {\n\t\t\t\tsetStatus(error instanceof Error ? error.message : l10n.consistencyFailed, 'error', diagnosticsTarget)\n\t\t\t}\n\t\t})\n\t}\n\n\tif (retryPendingButton instanceof HTMLElement) {\n\t\tretryPendingButton.addEventListener('click', async () => {\n\t\t\tclearFieldErrors()\n\t\t\tbeginStatus(l10n.checking, diagnosticsTarget)\n\t\t\ttry {\n\t\t\t\tconst data = await postJson(retryPendingUrl, {})\n\t\t\t\tconst details = []\n\t\t\t\tif (typeof data.attempted !== 'undefined') {\n\t\t\t\t\tdetails.push(`attempted=${String(data.attempted)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.resolved !== 'undefined') {\n\t\t\t\t\tdetails.push(`resolved=${String(data.resolved)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.failed !== 'undefined') {\n\t\t\t\t\tdetails.push(`failed=${String(data.failed)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.remaining !== 'undefined') {\n\t\t\t\t\tdetails.push(`remaining=${String(data.remaining)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.remaining !== 'undefined') {\n\t\t\t\t\tupdatePendingDeleteUi(Number(data.remaining || 0))\n\t\t\t\t}\n\t\t\t\tconst suffix = details.length > 0 ? ` ${details.join(' | ')}` : ''\n\t\t\t\tsetStatus(`${String(data.message || 'OK')}${suffix}`, 'success', diagnosticsTarget)\n\t\t\t} catch (error) {\n\t\t\t\tsetStatus(error instanceof Error ? error.message : l10n.retryFailed, 'error', diagnosticsTarget)\n\t\t\t}\n\t\t})\n\t}\n\n\tif (allowExternalCheckbox) {\n\t\tallowExternalCheckbox.addEventListener('change', updateExternalSettingsVisibility)\n\t}\n\tupdateExternalSettingsVisibility()\n\n\tfor (const checkbox of [protectedPadsCheckbox, publicPadsCheckbox]) {\n\t\tif (checkbox) {\n\t\t\tcheckbox.addEventListener('change', updatePadTypesHint)\n\t\t}\n\t}\n\tupdatePadTypesHint()\n})()\n"],"names":["root","form","statusNode","diagnosticsStatusNode","diagnosticsTarget","healthButton","consistencyButton","retryPendingButton","pendingActions","pendingCountNode","allowExternalCheckbox","protectedPadsCheckbox","publicPadsCheckbox","padTypesNoneHint","allowlistRow","allowlistHint","allowlistTextarea","fieldNodes","saveUrl","healthUrl","consistencyUrl","retryPendingUrl","l10n","updateExternalSettingsVisibility","enabled","errorNode","updatePadTypesHint","noneEnabled","message","getPayload","data","setStatus","state","node","beginStatus","other","clearFieldErrors","field","input","showFieldError","updatePendingDeleteUi","count","pendingCount","postJson","url","payload","body","key","response","responseText","err","event","versionSuffix","error","details","suffix","bindingWithoutFile","checkbox"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"etherpad_nextcloud-admin-settings.mjs","sources":["../src/admin-settings.js"],"sourcesContent":["(function () {\n\t'use strict'\n\n\tconst root = document.getElementById('etherpad-nextcloud-admin-settings')\n\tconst form = document.getElementById('etherpad-nextcloud-admin-form')\n\tconst statusNode = document.getElementById('etherpad-nextcloud-admin-status')\n\tconst diagnosticsStatusNode = document.getElementById('etherpad-nextcloud-diagnostics-status')\n\t// Only the save area is required at startup; older markup without the\n\t// diagnostics area still has to show its feedback somewhere.\n\tconst diagnosticsTarget = diagnosticsStatusNode instanceof HTMLElement\n\t\t? diagnosticsStatusNode\n\t\t: statusNode\n\tconst healthButton = document.getElementById('etherpad-nextcloud-health-check')\n\tconst consistencyButton = document.getElementById('etherpad-nextcloud-consistency-check')\n\tconst retryPendingButton = document.getElementById('etherpad-nextcloud-retry-pending')\n\tconst pendingActions = document.getElementById('etherpad-nextcloud-pending-actions')\n\tconst pendingCountNode = document.getElementById('etherpad-nextcloud-pending-count')\n\tconst allowExternalCheckbox = form ? form.querySelector('input[name=\"allow_external_pads\"]') : null\n\tconst protectedPadsCheckbox = form ? form.querySelector('input[name=\"enable_protected_pads\"]') : null\n\tconst publicPadsCheckbox = form ? form.querySelector('input[name=\"enable_public_pads\"]') : null\n\tconst padTypesNoneHint = document.getElementById('pad-types-none-hint')\n\tconst allowlistRow = document.getElementById('external-pad-allowlist-row')\n\tconst allowlistHint = document.getElementById('external-pad-allowlist-hint')\n\tconst allowlistTextarea = document.getElementById('external-pad-allowlist')\n\tconst fieldNodes = {\n\t\tetherpad_host: form.querySelector('[name=\"etherpad_host\"]'),\n\t\tetherpad_api_host: form.querySelector('[name=\"etherpad_api_host\"]'),\n\t\tetherpad_cookie_domain: form.querySelector('[name=\"etherpad_cookie_domain\"]'),\n\t\tetherpad_api_key: form.querySelector('[name=\"etherpad_api_key\"]'),\n\t\tsync_interval_seconds: form.querySelector('[name=\"sync_interval_seconds\"]'),\n\t\texternal_pad_allowlist: form.querySelector('[name=\"external_pad_allowlist\"]'),\n\t\ttrusted_embed_origins: form.querySelector('[name=\"trusted_embed_origins\"]'),\n\t}\n\n\tif (!root || !form || !statusNode || !healthButton) {\n\t\treturn\n\t}\n\n\tconst saveUrl = root.getAttribute('data-save-url') || ''\n\tconst healthUrl = root.getAttribute('data-health-url') || ''\n\tconst consistencyUrl = root.getAttribute('data-consistency-url') || ''\n\tconst retryPendingUrl = root.getAttribute('data-retry-pending-url') || ''\n\tconst l10n = {\n\t\tsaving: root.getAttribute('data-l10n-saving') || 'Saving settings...',\n\t\tsaved: root.getAttribute('data-l10n-saved') || 'Settings saved.',\n\t\tchecking: root.getAttribute('data-l10n-checking') || 'Testing Etherpad connection...',\n\t\tconsistencyRunning: root.getAttribute('data-l10n-consistency-running') || 'Running consistency check...',\n\t\thealthOk: root.getAttribute('data-l10n-health-ok') || 'Etherpad connection test successful.',\n\t\tconsistencyOk: root.getAttribute('data-l10n-consistency-ok') || 'Consistency check successful.',\n\t\trequestFailed: root.getAttribute('data-l10n-request-failed') || 'Request failed.',\n\t\tsavingFailed: root.getAttribute('data-l10n-saving-failed') || 'Failed to save settings.',\n\t\thealthFailed: root.getAttribute('data-l10n-health-failed') || 'Etherpad connection test failed.',\n\t\tconsistencyFailed: root.getAttribute('data-l10n-consistency-failed') || 'Consistency check failed.',\n\t\tpendingDeleteLabel: root.getAttribute('data-l10n-pending-delete-label') || 'Pending Etherpad deletes',\n\t\tretryFailed: root.getAttribute('data-l10n-retry-failed') || 'Pending delete retry failed.',\n\t}\n\n\tif (saveUrl === '' || healthUrl === '' || consistencyUrl === '' || retryPendingUrl === '') {\n\t\treturn\n\t}\n\n\tfunction updateExternalSettingsVisibility() {\n\t\tconst enabled = !!(allowExternalCheckbox && allowExternalCheckbox.checked)\n\t\tif (allowlistRow) {\n\t\t\tallowlistRow.style.display = enabled ? '' : 'none'\n\t\t}\n\t\tif (allowlistHint) {\n\t\t\tallowlistHint.style.display = enabled ? '' : 'none'\n\t\t}\n\t\tif (allowlistTextarea instanceof HTMLTextAreaElement) {\n\t\t\tallowlistTextarea.disabled = !enabled\n\t\t\tif (!enabled) {\n\t\t\t\tallowlistTextarea.classList.remove('ep-input-error')\n\t\t\t\tconst errorNode = form.querySelector('[data-field-error=\"external_pad_allowlist\"]')\n\t\t\t\tif (errorNode instanceof HTMLElement) {\n\t\t\t\t\terrorNode.textContent = ''\n\t\t\t\t\terrorNode.classList.remove('is-visible')\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction updatePadTypesHint() {\n\t\tconst noneEnabled = !!(protectedPadsCheckbox && publicPadsCheckbox)\n\t\t\t&& !protectedPadsCheckbox.checked\n\t\t\t&& !publicPadsCheckbox.checked\n\t\tif (padTypesNoneHint instanceof HTMLElement) {\n\t\t\t// Write the text instead of toggling visibility: a live region only\n\t\t\t// announces content changes, and an element returning from\n\t\t\t// `display: none` reads as initial content. `.ep-field-hint:empty`\n\t\t\t// collapses the empty paragraph, so it looks the same.\n\t\t\tconst message = noneEnabled ? (padTypesNoneHint.dataset.message || '') : ''\n\t\t\t// Rewriting the same text would announce it again on load.\n\t\t\tif (padTypesNoneHint.textContent !== message) {\n\t\t\t\tpadTypesNoneHint.textContent = message\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction getPayload() {\n\t\tconst data = new FormData(form)\n\t\treturn {\n\t\t\tetherpad_host: String(data.get('etherpad_host') || '').trim(),\n\t\t\tetherpad_api_host: String(data.get('etherpad_api_host') || '').trim(),\n\t\t\tetherpad_cookie_domain: String(data.get('etherpad_cookie_domain') || '').trim(),\n\t\t\tetherpad_api_key: String(data.get('etherpad_api_key') || '').trim(),\n\t\t\tsync_interval_seconds: Number(data.get('sync_interval_seconds') || 120),\n\t\t\tdelete_on_trash: data.has('delete_on_trash'),\n\t\t\tenable_protected_pads: data.has('enable_protected_pads'),\n\t\t\tenable_public_pads: data.has('enable_public_pads'),\n\t\t\tallow_external_pads: data.has('allow_external_pads'),\n\t\t\texternal_pad_allowlist: String(data.get('external_pad_allowlist') || ''),\n\t\t\ttrusted_embed_origins: String(data.get('trusted_embed_origins') || ''),\n\t\t}\n\t}\n\n\t// Writing a status touches only its own area, so a late response cannot\n\t// wipe out the other action's result.\n\tfunction setStatus(message, state, node = statusNode) {\n\t\tif (!(node instanceof HTMLElement)) {\n\t\t\treturn\n\t\t}\n\t\tnode.textContent = message\n\t\tnode.classList.remove('ep-status-success', 'ep-status-error')\n\t\tif (state === 'success') {\n\t\t\tnode.classList.add('ep-status-success')\n\t\t} else if (state === 'error') {\n\t\t\tnode.classList.add('ep-status-error')\n\t\t}\n\t}\n\n\t// Starting one does clear the other: its result predates this action and\n\t// would be read as belonging to it.\n\tfunction beginStatus(message, node = statusNode) {\n\t\tfor (const other of [statusNode, diagnosticsTarget]) {\n\t\t\tif (other instanceof HTMLElement && other !== node) {\n\t\t\t\tother.textContent = ''\n\t\t\t\tother.classList.remove('ep-status-success', 'ep-status-error')\n\t\t\t}\n\t\t}\n\t\tsetStatus(message, null, node)\n\t}\n\n\tfunction clearFieldErrors() {\n\t\tObject.keys(fieldNodes).forEach((field) => {\n\t\t\tconst input = fieldNodes[field]\n\t\t\tif (input instanceof HTMLElement) {\n\t\t\t\tinput.classList.remove('ep-input-error')\n\t\t\t}\n\t\t\tconst errorNode = form.querySelector(`[data-field-error=\"${field}\"]`)\n\t\t\tif (errorNode instanceof HTMLElement) {\n\t\t\t\terrorNode.textContent = ''\n\t\t\t\terrorNode.classList.remove('is-visible')\n\t\t\t}\n\t\t})\n\t}\n\n\tfunction showFieldError(field, message) {\n\t\tif (!field || !message) {\n\t\t\treturn\n\t\t}\n\t\tconst input = fieldNodes[field]\n\t\tif (input instanceof HTMLElement) {\n\t\t\tinput.classList.add('ep-input-error')\n\t\t}\n\t\tconst errorNode = form.querySelector(`[data-field-error=\"${field}\"]`)\n\t\tif (errorNode instanceof HTMLElement) {\n\t\t\terrorNode.textContent = message\n\t\t\terrorNode.classList.add('is-visible')\n\t\t}\n\t\tif (input instanceof HTMLElement && typeof input.focus === 'function') {\n\t\t\tinput.focus()\n\t\t}\n\t}\n\n\tfunction updatePendingDeleteUi(count) {\n\t\tconst pendingCount = Number.isFinite(Number(count)) ? Number(count) : 0\n\t\tif (pendingActions instanceof HTMLElement) {\n\t\t\tpendingActions.style.display = pendingCount > 0 ? '' : 'none'\n\t\t}\n\t\tif (pendingCountNode instanceof HTMLElement) {\n\t\t\tpendingCountNode.textContent = `${l10n.pendingDeleteLabel}: ${String(pendingCount)}`\n\t\t}\n\t\tif (retryPendingButton instanceof HTMLButtonElement) {\n\t\t\tretryPendingButton.disabled = pendingCount <= 0\n\t\t}\n\t}\n\n\tasync function postJson(url, payload) {\n\t\tconst body = new URLSearchParams()\n\t\tObject.keys(payload).forEach((key) => {\n\t\t\tbody.set(key, String(payload[key]))\n\t\t})\n\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: 'POST',\n\t\t\tcredentials: 'same-origin',\n\t\t\theaders: {\n\t\t\t\t'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',\n\t\t\t\trequesttoken: String(OC.requestToken || ''),\n\t\t\t},\n\t\t\tbody: body.toString(),\n\t\t})\n\n\t\tlet data = null\n\t\tconst responseText = await response.text()\n\t\ttry {\n\t\t\tdata = responseText !== '' ? JSON.parse(responseText) : null\n\t\t} catch (error) {\n\t\t\tdata = null\n\t\t}\n\n\t\tif (!response.ok || !data || data.ok !== true) {\n\t\t\tconst message = (data && data.message)\n\t\t\t\t? String(data.message)\n\t\t\t\t: (responseText !== '' ? responseText.slice(0, 200) : l10n.requestFailed)\n\t\t\tconst err = new Error(message)\n\t\t\terr.field = (data && typeof data.field === 'string') ? data.field : ''\n\t\t\tthrow err\n\t\t}\n\n\t\treturn data\n\t}\n\n\tform.addEventListener('submit', async (event) => {\n\t\tevent.preventDefault()\n\t\tclearFieldErrors()\n\t\tbeginStatus(l10n.saving)\n\t\ttry {\n\t\t\tconst data = await postJson(saveUrl, getPayload())\n\t\t\tconst versionSuffix = data && data.api_version ? ` api=${String(data.api_version)}` : ''\n\t\t\tsetStatus(`${String(data.message || l10n.saved)}${versionSuffix}`, 'success')\n\t\t} catch (error) {\n\t\t\tif (error && typeof error.field === 'string' && error.field !== '') {\n\t\t\t\tshowFieldError(error.field, error.message || l10n.savingFailed)\n\t\t\t}\n\t\t\tsetStatus(error instanceof Error ? error.message : l10n.savingFailed, 'error')\n\t\t}\n\t})\n\n\thealthButton.addEventListener('click', async () => {\n\t\tclearFieldErrors()\n\t\tbeginStatus(l10n.checking, diagnosticsTarget)\n\t\ttry {\n\t\t\tconst data = await postJson(healthUrl, getPayload())\n\t\t\tconst details = []\n\t\t\tif (typeof data.pad_count !== 'undefined') {\n\t\t\t\tdetails.push(`pad_count=${String(data.pad_count)}`)\n\t\t\t}\n\t\t\tif (typeof data.api_version === 'string' && data.api_version.trim() !== '') {\n\t\t\t\tdetails.push(`api=${data.api_version}`)\n\t\t\t}\n\t\t\tif (typeof data.latency_ms !== 'undefined') {\n\t\t\t\tdetails.push(`latency=${String(data.latency_ms)}ms`)\n\t\t\t}\n\t\t\tif (typeof data.target === 'string' && data.target.trim() !== '') {\n\t\t\t\tdetails.push(`target=${data.target}`)\n\t\t\t}\n\t\t\tif (typeof data.pending_delete_count !== 'undefined') {\n\t\t\t\tupdatePendingDeleteUi(Number(data.pending_delete_count))\n\t\t\t}\n\t\t\tconst suffix = details.length > 0 ? ` ${details.join(' | ')}` : ''\n\t\t\tconst message = `${String(data.message || l10n.healthOk)}${suffix}`\n\t\t\tsetStatus(message, 'success', diagnosticsTarget)\n\t\t} catch (error) {\n\t\t\tif (error && typeof error.field === 'string' && error.field !== '') {\n\t\t\t\tshowFieldError(error.field, error.message || l10n.healthFailed)\n\t\t\t}\n\t\t\tsetStatus(error instanceof Error ? error.message : l10n.healthFailed, 'error', diagnosticsTarget)\n\t\t}\n\t})\n\n\tif (consistencyButton instanceof HTMLElement) {\n\t\tconsistencyButton.addEventListener('click', async () => {\n\t\t\tclearFieldErrors()\n\t\t\tbeginStatus(l10n.consistencyRunning, diagnosticsTarget)\n\t\t\ttry {\n\t\t\t\tconst data = await postJson(consistencyUrl, {})\n\t\t\t\tconst bindingWithoutFile = Number(data.binding_without_file_count || 0)\n\t\t\t\tconst message = `${String(data.message || l10n.consistencyOk)} binding_without_file=${String(bindingWithoutFile)}`\n\t\t\t\tsetStatus(message, bindingWithoutFile > 0 ? 'error' : 'success', diagnosticsTarget)\n\t\t\t} catch (error) {\n\t\t\t\tsetStatus(error instanceof Error ? error.message : l10n.consistencyFailed, 'error', diagnosticsTarget)\n\t\t\t}\n\t\t})\n\t}\n\n\tif (retryPendingButton instanceof HTMLElement) {\n\t\tretryPendingButton.addEventListener('click', async () => {\n\t\t\tclearFieldErrors()\n\t\t\tbeginStatus(l10n.checking, diagnosticsTarget)\n\t\t\ttry {\n\t\t\t\tconst data = await postJson(retryPendingUrl, {})\n\t\t\t\tconst details = []\n\t\t\t\tif (typeof data.attempted !== 'undefined') {\n\t\t\t\t\tdetails.push(`attempted=${String(data.attempted)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.resolved !== 'undefined') {\n\t\t\t\t\tdetails.push(`resolved=${String(data.resolved)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.failed !== 'undefined') {\n\t\t\t\t\tdetails.push(`failed=${String(data.failed)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.remaining !== 'undefined') {\n\t\t\t\t\tdetails.push(`remaining=${String(data.remaining)}`)\n\t\t\t\t}\n\t\t\t\tif (typeof data.remaining !== 'undefined') {\n\t\t\t\t\tupdatePendingDeleteUi(Number(data.remaining || 0))\n\t\t\t\t}\n\t\t\t\tconst suffix = details.length > 0 ? ` ${details.join(' | ')}` : ''\n\t\t\t\tsetStatus(`${String(data.message || 'OK')}${suffix}`, 'success', diagnosticsTarget)\n\t\t\t} catch (error) {\n\t\t\t\tsetStatus(error instanceof Error ? error.message : l10n.retryFailed, 'error', diagnosticsTarget)\n\t\t\t}\n\t\t})\n\t}\n\n\tif (allowExternalCheckbox) {\n\t\tallowExternalCheckbox.addEventListener('change', updateExternalSettingsVisibility)\n\t}\n\tupdateExternalSettingsVisibility()\n\n\tfor (const checkbox of [protectedPadsCheckbox, publicPadsCheckbox]) {\n\t\tif (checkbox) {\n\t\t\tcheckbox.addEventListener('change', updatePadTypesHint)\n\t\t}\n\t}\n\tupdatePadTypesHint()\n})()\n"],"names":["root","form","statusNode","diagnosticsStatusNode","diagnosticsTarget","healthButton","consistencyButton","retryPendingButton","pendingActions","pendingCountNode","allowExternalCheckbox","protectedPadsCheckbox","publicPadsCheckbox","padTypesNoneHint","allowlistRow","allowlistHint","allowlistTextarea","fieldNodes","saveUrl","healthUrl","consistencyUrl","retryPendingUrl","l10n","updateExternalSettingsVisibility","enabled","errorNode","updatePadTypesHint","noneEnabled","message","getPayload","data","setStatus","state","node","beginStatus","other","clearFieldErrors","field","input","showFieldError","updatePendingDeleteUi","count","pendingCount","postJson","url","payload","body","key","response","responseText","err","event","versionSuffix","error","details","suffix","bindingWithoutFile","checkbox"],"mappings":"CAAC,UAAY,CAGZ,MAAMA,EAAO,SAAS,eAAe,mCAAmC,EAClEC,EAAO,SAAS,eAAe,+BAA+B,EAC9DC,EAAa,SAAS,eAAe,iCAAiC,EACtEC,EAAwB,SAAS,eAAe,uCAAuC,EAGvFC,EAAoBD,aAAiC,YACxDA,EACAD,EACGG,EAAe,SAAS,eAAe,iCAAiC,EACxEC,EAAoB,SAAS,eAAe,sCAAsC,EAClFC,EAAqB,SAAS,eAAe,kCAAkC,EAC/EC,EAAiB,SAAS,eAAe,oCAAoC,EAC7EC,EAAmB,SAAS,eAAe,kCAAkC,EAC7EC,EAAwBT,EAAOA,EAAK,cAAc,mCAAmC,EAAI,KACzFU,EAAwBV,EAAOA,EAAK,cAAc,qCAAqC,EAAI,KAC3FW,EAAqBX,EAAOA,EAAK,cAAc,kCAAkC,EAAI,KACrFY,EAAmB,SAAS,eAAe,qBAAqB,EAChEC,EAAe,SAAS,eAAe,4BAA4B,EACnEC,EAAgB,SAAS,eAAe,6BAA6B,EACrEC,EAAoB,SAAS,eAAe,wBAAwB,EACpEC,EAAa,CAClB,cAAehB,EAAK,cAAc,wBAAwB,EAC1D,kBAAmBA,EAAK,cAAc,4BAA4B,EAClE,uBAAwBA,EAAK,cAAc,iCAAiC,EAC5E,iBAAkBA,EAAK,cAAc,2BAA2B,EAChE,sBAAuBA,EAAK,cAAc,gCAAgC,EAC1E,uBAAwBA,EAAK,cAAc,iCAAiC,EAC5E,sBAAuBA,EAAK,cAAc,gCAAgC,CAC5E,EAEC,GAAI,CAACD,GAAQ,CAACC,GAAQ,CAACC,GAAc,CAACG,EACrC,OAGD,MAAMa,EAAUlB,EAAK,aAAa,eAAe,GAAK,GAChDmB,EAAYnB,EAAK,aAAa,iBAAiB,GAAK,GACpDoB,EAAiBpB,EAAK,aAAa,sBAAsB,GAAK,GAC9DqB,EAAkBrB,EAAK,aAAa,wBAAwB,GAAK,GACjEsB,EAAO,CACZ,OAAQtB,EAAK,aAAa,kBAAkB,GAAK,qBACjD,MAAOA,EAAK,aAAa,iBAAiB,GAAK,kBAC/C,SAAUA,EAAK,aAAa,oBAAoB,GAAK,iCACrD,mBAAoBA,EAAK,aAAa,+BAA+B,GAAK,+BAC1E,SAAUA,EAAK,aAAa,qBAAqB,GAAK,uCACtD,cAAeA,EAAK,aAAa,0BAA0B,GAAK,gCAChE,cAAeA,EAAK,aAAa,0BAA0B,GAAK,kBAChE,aAAcA,EAAK,aAAa,yBAAyB,GAAK,2BAC9D,aAAcA,EAAK,aAAa,yBAAyB,GAAK,mCAC9D,kBAAmBA,EAAK,aAAa,8BAA8B,GAAK,4BACxE,mBAAoBA,EAAK,aAAa,gCAAgC,GAAK,2BAC3E,YAAaA,EAAK,aAAa,wBAAwB,GAAK,8BAC9D,EAEC,GAAIkB,IAAY,IAAMC,IAAc,IAAMC,IAAmB,IAAMC,IAAoB,GACtF,OAGD,SAASE,GAAmC,CAC3C,MAAMC,EAAU,CAAC,EAAEd,GAAyBA,EAAsB,SAOlE,GANII,IACHA,EAAa,MAAM,QAAUU,EAAU,GAAK,QAEzCT,IACHA,EAAc,MAAM,QAAUS,EAAU,GAAK,QAE1CR,aAA6B,sBAChCA,EAAkB,SAAW,CAACQ,EAC1B,CAACA,GAAS,CACbR,EAAkB,UAAU,OAAO,gBAAgB,EACnD,MAAMS,EAAYxB,EAAK,cAAc,6CAA6C,EAC9EwB,aAAqB,cACxBA,EAAU,YAAc,GACxBA,EAAU,UAAU,OAAO,YAAY,EAEzC,CAEF,CAEA,SAASC,GAAqB,CAC7B,MAAMC,EAAc,CAAC,EAAEhB,GAAyBC,IAC5C,CAACD,EAAsB,SACvB,CAACC,EAAmB,QACxB,GAAIC,aAA4B,YAAa,CAK5C,MAAMe,EAAUD,GAAed,EAAiB,QAAQ,SAAW,GAE/DA,EAAiB,cAAgBe,IACpCf,EAAiB,YAAce,EAEjC,CACD,CAEA,SAASC,GAAa,CACrB,MAAMC,EAAO,IAAI,SAAS7B,CAAI,EAC9B,MAAO,CACN,cAAe,OAAO6B,EAAK,IAAI,eAAe,GAAK,EAAE,EAAE,KAAI,EAC3D,kBAAmB,OAAOA,EAAK,IAAI,mBAAmB,GAAK,EAAE,EAAE,KAAI,EACnE,uBAAwB,OAAOA,EAAK,IAAI,wBAAwB,GAAK,EAAE,EAAE,KAAI,EAC7E,iBAAkB,OAAOA,EAAK,IAAI,kBAAkB,GAAK,EAAE,EAAE,KAAI,EACjE,sBAAuB,OAAOA,EAAK,IAAI,uBAAuB,GAAK,GAAG,EACtE,gBAAiBA,EAAK,IAAI,iBAAiB,EAC3C,sBAAuBA,EAAK,IAAI,uBAAuB,EACvD,mBAAoBA,EAAK,IAAI,oBAAoB,EACjD,oBAAqBA,EAAK,IAAI,qBAAqB,EACnD,uBAAwB,OAAOA,EAAK,IAAI,wBAAwB,GAAK,EAAE,EACvE,sBAAuB,OAAOA,EAAK,IAAI,uBAAuB,GAAK,EAAE,CACxE,CACC,CAIA,SAASC,EAAUH,EAASI,EAAOC,EAAO/B,EAAY,CAC/C+B,aAAgB,cAGtBA,EAAK,YAAcL,EACnBK,EAAK,UAAU,OAAO,oBAAqB,iBAAiB,EACxDD,IAAU,UACbC,EAAK,UAAU,IAAI,mBAAmB,EAC5BD,IAAU,SACpBC,EAAK,UAAU,IAAI,iBAAiB,EAEtC,CAIA,SAASC,EAAYN,EAASK,EAAO/B,EAAY,CAChD,UAAWiC,IAAS,CAACjC,EAAYE,CAAiB,EAC7C+B,aAAiB,aAAeA,IAAUF,IAC7CE,EAAM,YAAc,GACpBA,EAAM,UAAU,OAAO,oBAAqB,iBAAiB,GAG/DJ,EAAUH,EAAS,KAAMK,CAAI,CAC9B,CAEA,SAASG,GAAmB,CAC3B,OAAO,KAAKnB,CAAU,EAAE,QAASoB,GAAU,CAC1C,MAAMC,EAAQrB,EAAWoB,CAAK,EAC1BC,aAAiB,aACpBA,EAAM,UAAU,OAAO,gBAAgB,EAExC,MAAMb,EAAYxB,EAAK,cAAc,sBAAsBoC,CAAK,IAAI,EAChEZ,aAAqB,cACxBA,EAAU,YAAc,GACxBA,EAAU,UAAU,OAAO,YAAY,EAEzC,CAAC,CACF,CAEA,SAASc,EAAeF,EAAOT,EAAS,CACvC,GAAI,CAACS,GAAS,CAACT,EACd,OAED,MAAMU,EAAQrB,EAAWoB,CAAK,EAC1BC,aAAiB,aACpBA,EAAM,UAAU,IAAI,gBAAgB,EAErC,MAAMb,EAAYxB,EAAK,cAAc,sBAAsBoC,CAAK,IAAI,EAChEZ,aAAqB,cACxBA,EAAU,YAAcG,EACxBH,EAAU,UAAU,IAAI,YAAY,GAEjCa,aAAiB,aAAe,OAAOA,EAAM,OAAU,YAC1DA,EAAM,MAAK,CAEb,CAEA,SAASE,EAAsBC,EAAO,CACrC,MAAMC,EAAe,OAAO,SAAS,OAAOD,CAAK,CAAC,EAAI,OAAOA,CAAK,EAAI,EAClEjC,aAA0B,cAC7BA,EAAe,MAAM,QAAUkC,EAAe,EAAI,GAAK,QAEpDjC,aAA4B,cAC/BA,EAAiB,YAAc,GAAGa,EAAK,kBAAkB,KAAK,OAAOoB,CAAY,CAAC,IAE/EnC,aAA8B,oBACjCA,EAAmB,SAAWmC,GAAgB,EAEhD,CAEA,eAAeC,EAASC,EAAKC,EAAS,CACrC,MAAMC,EAAO,IAAI,gBACjB,OAAO,KAAKD,CAAO,EAAE,QAASE,GAAQ,CACrCD,EAAK,IAAIC,EAAK,OAAOF,EAAQE,CAAG,CAAC,CAAC,CACnC,CAAC,EAED,MAAMC,EAAW,MAAM,MAAMJ,EAAK,CACjC,OAAQ,OACR,YAAa,cACb,QAAS,CACR,eAAgB,mDAChB,aAAc,OAAO,GAAG,cAAgB,EAAE,CAC9C,EACG,KAAME,EAAK,SAAQ,CACtB,CAAG,EAED,IAAIhB,EAAO,KACX,MAAMmB,EAAe,MAAMD,EAAS,KAAI,EACxC,GAAI,CACHlB,EAAOmB,IAAiB,GAAK,KAAK,MAAMA,CAAY,EAAI,IACzD,MAAgB,CACfnB,EAAO,IACR,CAEA,GAAI,CAACkB,EAAS,IAAM,CAAClB,GAAQA,EAAK,KAAO,GAAM,CAC9C,MAAMF,EAAWE,GAAQA,EAAK,QAC3B,OAAOA,EAAK,OAAO,EAClBmB,IAAiB,GAAKA,EAAa,MAAM,EAAG,GAAG,EAAI3B,EAAK,cACtD4B,EAAM,IAAI,MAAMtB,CAAO,EAC7B,MAAAsB,EAAI,MAASpB,GAAQ,OAAOA,EAAK,OAAU,SAAYA,EAAK,MAAQ,GAC9DoB,CACP,CAEA,OAAOpB,CACR,CAEA7B,EAAK,iBAAiB,SAAU,MAAOkD,GAAU,CAChDA,EAAM,eAAc,EACpBf,EAAgB,EAChBF,EAAYZ,EAAK,MAAM,EACvB,GAAI,CACH,MAAMQ,EAAO,MAAMa,EAASzB,EAASW,EAAU,CAAE,EAC3CuB,EAAgBtB,GAAQA,EAAK,YAAc,QAAQ,OAAOA,EAAK,WAAW,CAAC,GAAK,GACtFC,EAAU,GAAG,OAAOD,EAAK,SAAWR,EAAK,KAAK,CAAC,GAAG8B,CAAa,GAAI,SAAS,CAC7E,OAASC,EAAO,CACXA,GAAS,OAAOA,EAAM,OAAU,UAAYA,EAAM,QAAU,IAC/Dd,EAAec,EAAM,MAAOA,EAAM,SAAW/B,EAAK,YAAY,EAE/DS,EAAUsB,aAAiB,MAAQA,EAAM,QAAU/B,EAAK,aAAc,OAAO,CAC9E,CACD,CAAC,EAEDjB,EAAa,iBAAiB,QAAS,SAAY,CAClD+B,EAAgB,EAChBF,EAAYZ,EAAK,SAAUlB,CAAiB,EAC5C,GAAI,CACH,MAAM0B,EAAO,MAAMa,EAASxB,EAAWU,EAAU,CAAE,EAC7CyB,EAAU,CAAA,EACZ,OAAOxB,EAAK,UAAc,KAC7BwB,EAAQ,KAAK,aAAa,OAAOxB,EAAK,SAAS,CAAC,EAAE,EAE/C,OAAOA,EAAK,aAAgB,UAAYA,EAAK,YAAY,KAAI,IAAO,IACvEwB,EAAQ,KAAK,OAAOxB,EAAK,WAAW,EAAE,EAEnC,OAAOA,EAAK,WAAe,KAC9BwB,EAAQ,KAAK,WAAW,OAAOxB,EAAK,UAAU,CAAC,IAAI,EAEhD,OAAOA,EAAK,QAAW,UAAYA,EAAK,OAAO,KAAI,IAAO,IAC7DwB,EAAQ,KAAK,UAAUxB,EAAK,MAAM,EAAE,EAEjC,OAAOA,EAAK,qBAAyB,KACxCU,EAAsB,OAAOV,EAAK,oBAAoB,CAAC,EAExD,MAAMyB,EAASD,EAAQ,OAAS,EAAI,IAAIA,EAAQ,KAAK,KAAK,CAAC,GAAK,GAC1D1B,EAAU,GAAG,OAAOE,EAAK,SAAWR,EAAK,QAAQ,CAAC,GAAGiC,CAAM,GACjExB,EAAUH,EAAS,UAAWxB,CAAiB,CAChD,OAASiD,EAAO,CACXA,GAAS,OAAOA,EAAM,OAAU,UAAYA,EAAM,QAAU,IAC/Dd,EAAec,EAAM,MAAOA,EAAM,SAAW/B,EAAK,YAAY,EAE/DS,EAAUsB,aAAiB,MAAQA,EAAM,QAAU/B,EAAK,aAAc,QAASlB,CAAiB,CACjG,CACD,CAAC,EAEGE,aAA6B,aAChCA,EAAkB,iBAAiB,QAAS,SAAY,CACvD8B,EAAgB,EAChBF,EAAYZ,EAAK,mBAAoBlB,CAAiB,EACtD,GAAI,CACH,MAAM0B,EAAO,MAAMa,EAASvB,EAAgB,CAAA,CAAE,EACxCoC,EAAqB,OAAO1B,EAAK,4BAA8B,CAAC,EAChEF,EAAU,GAAG,OAAOE,EAAK,SAAWR,EAAK,aAAa,CAAC,yBAAyB,OAAOkC,CAAkB,CAAC,GAChHzB,EAAUH,EAAS4B,EAAqB,EAAI,QAAU,UAAWpD,CAAiB,CACnF,OAASiD,EAAO,CACftB,EAAUsB,aAAiB,MAAQA,EAAM,QAAU/B,EAAK,kBAAmB,QAASlB,CAAiB,CACtG,CACD,CAAC,EAGEG,aAA8B,aACjCA,EAAmB,iBAAiB,QAAS,SAAY,CACxD6B,EAAgB,EAChBF,EAAYZ,EAAK,SAAUlB,CAAiB,EAC5C,GAAI,CACH,MAAM0B,EAAO,MAAMa,EAAStB,EAAiB,CAAA,CAAE,EACzCiC,EAAU,CAAA,EACZ,OAAOxB,EAAK,UAAc,KAC7BwB,EAAQ,KAAK,aAAa,OAAOxB,EAAK,SAAS,CAAC,EAAE,EAE/C,OAAOA,EAAK,SAAa,KAC5BwB,EAAQ,KAAK,YAAY,OAAOxB,EAAK,QAAQ,CAAC,EAAE,EAE7C,OAAOA,EAAK,OAAW,KAC1BwB,EAAQ,KAAK,UAAU,OAAOxB,EAAK,MAAM,CAAC,EAAE,EAEzC,OAAOA,EAAK,UAAc,KAC7BwB,EAAQ,KAAK,aAAa,OAAOxB,EAAK,SAAS,CAAC,EAAE,EAE/C,OAAOA,EAAK,UAAc,KAC7BU,EAAsB,OAAOV,EAAK,WAAa,CAAC,CAAC,EAElD,MAAMyB,EAASD,EAAQ,OAAS,EAAI,IAAIA,EAAQ,KAAK,KAAK,CAAC,GAAK,GAChEvB,EAAU,GAAG,OAAOD,EAAK,SAAW,IAAI,CAAC,GAAGyB,CAAM,GAAI,UAAWnD,CAAiB,CACnF,OAASiD,EAAO,CACftB,EAAUsB,aAAiB,MAAQA,EAAM,QAAU/B,EAAK,YAAa,QAASlB,CAAiB,CAChG,CACD,CAAC,EAGEM,GACHA,EAAsB,iBAAiB,SAAUa,CAAgC,EAElFA,EAAgC,EAEhC,UAAWkC,IAAY,CAAC9C,EAAuBC,CAAkB,EAC5D6C,GACHA,EAAS,iBAAiB,SAAU/B,CAAkB,EAGxDA,EAAkB,CACnB,GAAC"} \ No newline at end of file diff --git a/src/admin-settings.js b/src/admin-settings.js index a734c60..5d2b252 100644 --- a/src/admin-settings.js +++ b/src/admin-settings.js @@ -5,9 +5,8 @@ const form = document.getElementById('etherpad-nextcloud-admin-form') const statusNode = document.getElementById('etherpad-nextcloud-admin-status') const diagnosticsStatusNode = document.getElementById('etherpad-nextcloud-diagnostics-status') - // Only the save area is required at startup. Markup rendered before the - // diagnostics area existed still has to show diagnostics feedback - // somewhere, so fall back to the save area instead of dropping it. + // Only the save area is required at startup; older markup without the + // diagnostics area still has to show its feedback somewhere. const diagnosticsTarget = diagnosticsStatusNode instanceof HTMLElement ? diagnosticsStatusNode : statusNode @@ -86,12 +85,10 @@ && !protectedPadsCheckbox.checked && !publicPadsCheckbox.checked if (padTypesNoneHint instanceof HTMLElement) { - // Set the text rather than toggling visibility. A live region only - // announces content *changes*; an element coming back from - // `display: none` was not in the accessibility tree before, so its - // text counts as initial content and commonly stays unspoken. The - // empty paragraph collapses via `.ep-field-hint:empty`, so this - // looks the same as hiding it. + // Write the text instead of toggling visibility: a live region only + // announces content changes, and an element returning from + // `display: none` reads as initial content. `.ep-field-hint:empty` + // collapses the empty paragraph, so it looks the same. const message = noneEnabled ? (padTypesNoneHint.dataset.message || '') : '' // Rewriting the same text would announce it again on load. if (padTypesNoneHint.textContent !== message) { @@ -117,10 +114,8 @@ } } - // Saving and the diagnostic tools live in separate sections, so each - // reports next to its own buttons. Writing a status only ever touches its - // own area — the two run independently, and a response arriving late must - // not wipe out the other action's result. + // Writing a status touches only its own area, so a late response cannot + // wipe out the other action's result. function setStatus(message, state, node = statusNode) { if (!(node instanceof HTMLElement)) { return @@ -134,8 +129,8 @@ } } - // Starting an action does clear the other area: its result predates this - // action and could otherwise be read as belonging to it. + // Starting one does clear the other: its result predates this action and + // would be read as belonging to it. function beginStatus(message, node = statusNode) { for (const other of [statusNode, diagnosticsTarget]) { if (other instanceof HTMLElement && other !== node) { diff --git a/templates/admin-settings.php b/templates/admin-settings.php index ee77069..5dc7d0a 100644 --- a/templates/admin-settings.php +++ b/templates/admin-settings.php @@ -109,9 +109,8 @@ class="section" - + diff --git a/tests/e2e/fixtures/nextcloud.ts b/tests/e2e/fixtures/nextcloud.ts index 1908718..0bf3c2a 100644 --- a/tests/e2e/fixtures/nextcloud.ts +++ b/tests/e2e/fixtures/nextcloud.ts @@ -49,7 +49,6 @@ export const gotoAdminPadSettings = async (page: Page): Promise