diff --git a/css/admin-settings.css b/css/admin-settings.css index 1fffb91..7a37f71 100644 --- a/css/admin-settings.css +++ b/css/admin-settings.css @@ -10,27 +10,73 @@ display: block; } +/* 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; +} + #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; +/* Lets the script empty a live region instead of hiding it — see + updatePadTypesHint(). */ +#etherpad-nextcloud-admin-settings .ep-field-hint:empty { + margin: 0; } -#etherpad-nextcloud-admin-settings #external-pad-allowlist-row { - margin-bottom: 6px; +/* A reported value, not an explanation: no muted hint styling. */ +#etherpad-nextcloud-admin-settings .ep-detected-value { + margin-top: 0; + margin-bottom: 14px; } -#etherpad-nextcloud-admin-settings #external-pad-allowlist-hint { - margin-top: 0; +/* 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; +} + +#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; +} + +/* 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; + border-top: 1px solid var(--color-border); + font-size: 1.2em; + font-weight: bold; + color: var(--color-main-text); +} + +#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; field names are primary content here. */ #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 +88,13 @@ max-width: 640px; } +/* Same reason, plus weight: the option heads 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 +103,8 @@ gap: 10px; } -#etherpad-nextcloud-admin-settings #etherpad-nextcloud-admin-status.ep-status-success, -#etherpad-nextcloud-admin-settings #etherpad-nextcloud-admin-status.ep-status-error { +#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..ad50fde 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"),k=document.getElementById("etherpad-nextcloud-diagnostics-status"),o=k instanceof HTMLElement?k:d,L=document.getElementById("etherpad-nextcloud-health-check"),x=document.getElementById("etherpad-nextcloud-consistency-check"),u=document.getElementById("etherpad-nextcloud-retry-pending"),w=document.getElementById("etherpad-nextcloud-pending-actions"),$=document.getElementById("etherpad-nextcloud-pending-count"),p=a?a.querySelector('input[name="allow_external_pads"]'):null,b=a?a.querySelector('input[name="enable_protected_pads"]'):null,E=a?a.querySelector('input[name="enable_public_pads"]'):null,g=document.getElementById("pad-types-none-hint"),T=document.getElementById("external-pad-allowlist-row"),q=document.getElementById("external-pad-allowlist-hint"),v=document.getElementById("external-pad-allowlist"),S={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||!L)return;const A=s.getAttribute("data-save-url")||"",F=s.getAttribute("data-health-url")||"",H=s.getAttribute("data-consistency-url")||"",M=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(A===""||F===""||H===""||M==="")return;function B(){const e=!!(p&&p.checked);if(T&&(T.style.display=e?"":"none"),q&&(q.style.display=e?"":"none"),v instanceof HTMLTextAreaElement&&(v.disabled=!e,!e)){v.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 I(){const e=!!(b&&E)&&!b.checked&&!E.checked;if(g instanceof HTMLElement){const t=e&&g.dataset.message||"";g.textContent!==t&&(g.textContent=t)}}function C(){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 m(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 h(){Object.keys(S).forEach(e=>{const t=S[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 O(e,t){if(!e||!t)return;const n=S[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;w instanceof HTMLElement&&(w.style.display=t>0?"":"none"),$ instanceof HTMLElement&&($.textContent=`${i.pendingDeleteLabel}: ${String(t)}`),u instanceof HTMLButtonElement&&(u.disabled=t<=0)}async function _(e,t){const n=new URLSearchParams;Object.keys(t).forEach(f=>{n.set(f,String(t[f]))});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 y=await l.text();try{r=y!==""?JSON.parse(y):null}catch{r=null}if(!l.ok||!r||r.ok!==!0){const f=r&&r.message?String(r.message):y!==""?y.slice(0,200):i.requestFailed,P=new Error(f);throw P.field=r&&typeof r.field=="string"?r.field:"",P}return r}a.addEventListener("submit",async e=>{e.preventDefault(),h(),m(i.saving);try{const t=await _(A,C()),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!==""&&O(t.field,t.message||i.savingFailed),c(t instanceof Error?t.message:i.savingFailed,"error")}}),L.addEventListener("click",async()=>{h(),m(i.checking,o);try{const e=await _(F,C()),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!==""&&O(e.field,e.message||i.healthFailed),c(e instanceof Error?e.message:i.healthFailed,"error",o)}}),x instanceof HTMLElement&&x.addEventListener("click",async()=>{h(),m(i.consistencyRunning,o);try{const e=await _(H,{}),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()=>{h(),m(i.checking,o);try{const e=await _(M,{}),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",B),B();for(const e of[b,E])e&&e.addEventListener("change",I);I()})(); //# 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..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 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\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":"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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..5d2b252 100644 --- a/src/admin-settings.js +++ b/src/admin-settings.js @@ -4,6 +4,12 @@ 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') + // 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 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,10 +85,15 @@ && !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. - padTypesNoneHint.style.display = noneEnabled ? '' : 'none' + // 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) { + padTypesNoneHint.textContent = message + } } } @@ -103,14 +114,31 @@ } } - function setStatus(message, state) { - statusNode.textContent = message - statusNode.classList.remove('ep-status-success', 'ep-status-error') + // 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 + } + 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 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) { + other.textContent = '' + other.classList.remove('ep-status-success', 'ep-status-error') + } } + setStatus(message, null, node) } function clearFieldErrors() { @@ -197,7 +225,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 +240,7 @@ healthButton.addEventListener('click', async () => { clearFieldErrors() - setStatus(l10n.checking, null) + beginStatus(l10n.checking, diagnosticsTarget) try { const data = await postJson(healthUrl, getPayload()) const details = [] @@ -233,26 +261,26 @@ } const suffix = details.length > 0 ? ` ${details.join(' | ')}` : '' const message = `${String(data.message || l10n.healthOk)}${suffix}` - setStatus(message, 'success') + setStatus(message, 'success', diagnosticsTarget) } 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', diagnosticsTarget) } }) if (consistencyButton instanceof HTMLElement) { consistencyButton.addEventListener('click', async () => { clearFieldErrors() - setStatus(l10n.consistencyRunning, null) + beginStatus(l10n.consistencyRunning, diagnosticsTarget) 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', diagnosticsTarget) } catch (error) { - setStatus(error instanceof Error ? error.message : l10n.consistencyFailed, 'error') + setStatus(error instanceof Error ? error.message : l10n.consistencyFailed, 'error', diagnosticsTarget) } }) } @@ -260,7 +288,7 @@ if (retryPendingButton instanceof HTMLElement) { retryPendingButton.addEventListener('click', async () => { clearFieldErrors() - setStatus(l10n.checking, null) + beginStatus(l10n.checking, diagnosticsTarget) try { const data = await postJson(retryPendingUrl, {}) const details = [] @@ -280,9 +308,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', diagnosticsTarget) } catch (error) { - setStatus(error instanceof Error ? error.message : l10n.retryFailed, 'error') + setStatus(error instanceof Error ? error.message : l10n.retryFailed, 'error', diagnosticsTarget) } }) } diff --git a/templates/admin-settings.php b/templates/admin-settings.php index 7e392bc..5dc7d0a 100644 --- a/templates/admin-settings.php +++ b/templates/admin-settings.php @@ -28,6 +28,8 @@ class="section"

+

+

-

+

+ + + +

+

+ +

+ +

-

+

-

+

-

+ -

- - - +

+ +

+

+

-

- +

+

+

+ +

-

+

-

+

-

+

-

+

-

- -

-

- -

- -

-

-

style="display: none">

+

-

+

@@ -152,10 +168,11 @@ class="section"

-

+

@@ -165,6 +182,13 @@ class="section"

+
+

+ +

+

+ +
@@ -172,7 +196,6 @@ class="section" - -

+

diff --git a/tests/e2e/fixtures/nextcloud.ts b/tests/e2e/fixtures/nextcloud.ts index a56dbe9..0bf3c2a 100644 --- a/tests/e2e/fixtures/nextcloud.ts +++ b/tests/e2e/fixtures/nextcloud.ts @@ -49,7 +49,7 @@ export const gotoAdminPadSettings = async (page: Page): Promise => { /** Run the admin Etherpad health check and assert the configured pad server responds. */ export const runAdminEtherpadHealthCheck = async (page: Page): Promise => { - const status = page.locator('#etherpad-nextcloud-admin-status') + const status = page.locator('#etherpad-nextcloud-diagnostics-status') await page.locator('#etherpad-nextcloud-health-check').click() await expect(status).toHaveClass(/ep-status-success/, { timeout: 30_000 }) diff --git a/tests/js/admin-settings.test.js b/tests/js/admin-settings.test.js new file mode 100644 index 0000000..842a3e2 --- /dev/null +++ b/tests/js/admin-settings.test.js @@ -0,0 +1,211 @@ +/** + * 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 = ` +
+
+ + + + + + + +

+ + + + + +

+ + +

+
+
+ ` +} + +const saveStatus = () => document.getElementById('etherpad-nextcloud-admin-status') +const diagnosticsStatus = () => document.getElementById('etherpad-nextcloud-diagnostics-status') + +/** The client only treats a body carrying `ok: true` as success. */ +const okResponse = (body) => ({ + ok: true, + status: 200, + text: () => Promise.resolve(JSON.stringify({ ok: true, ...body })), +}) + +/** A response the test resolves itself, to complete overlapping requests + * in a deliberately reversed order. */ +const deferred = () => { + let resolve + const promise = new Promise((r) => { + resolve = r + }) + return { + promise, + respond: (body) => resolve(okResponse(body)), + } +} + +const flush = async () => { + for (let i = 0; i < 8; i += 1) { + await Promise.resolve() + } +} + +describe('admin settings status areas', () => { + beforeEach(async () => { + vi.resetModules() + global.OC = { requestToken: 'token' } + setupAdminDom() + }) + + afterEach(() => { + document.body.innerHTML = '' + delete global.OC + vi.unstubAllGlobals() + }) + + it('reports saving and diagnostics next to their own actions', async () => { + vi.stubGlobal('fetch', vi.fn(() => Promise.resolve(okResponse({ message: 'Connection ok.' })))) + await import(MODULE) + + document.getElementById('etherpad-nextcloud-health-check').click() + await flush() + + expect(diagnosticsStatus().textContent).toContain('Connection ok.') + // Success, not the error path. + expect(diagnosticsStatus().classList.contains('ep-status-success')).toBe(true) + expect(saveStatus().textContent).toBe('') + }) + + it('keeps a diagnostics result when a later save response arrives', async () => { + const health = deferred() + const save = deferred() + vi.stubGlobal('fetch', vi.fn((url) => (url === '/health' ? health.promise : save.promise))) + await import(MODULE) + + document.getElementById('etherpad-nextcloud-health-check').click() + await flush() + document.getElementById('etherpad-nextcloud-admin-form').requestSubmit() + await flush() + + // Diagnostics finishes first, the save afterwards. + health.respond({ message: 'Connection ok.' }) + await flush() + save.respond({ message: 'Saved.' }) + await flush() + + expect(diagnosticsStatus().textContent).toContain('Connection ok.') + expect(diagnosticsStatus().classList.contains('ep-status-success')).toBe(true) + expect(saveStatus().textContent).toContain('Saved.') + expect(saveStatus().classList.contains('ep-status-success')).toBe(true) + }) + + it('clears the other area when a new action starts', async () => { + vi.stubGlobal('fetch', vi.fn(() => Promise.resolve(okResponse({ message: 'Connection ok.' })))) + await import(MODULE) + + document.getElementById('etherpad-nextcloud-health-check').click() + await flush() + expect(diagnosticsStatus().textContent).toContain('Connection ok.') + + document.getElementById('etherpad-nextcloud-admin-form').requestSubmit() + await flush() + + expect(diagnosticsStatus().textContent).toBe('') + expect(diagnosticsStatus().classList.contains('ep-status-success')).toBe(false) + }) + + it('falls back to the save area when the diagnostics area is missing', async () => { + diagnosticsStatus().remove() + vi.stubGlobal('fetch', vi.fn(() => Promise.resolve(okResponse({ message: 'Connection ok.' })))) + await import(MODULE) + + document.getElementById('etherpad-nextcloud-health-check').click() + await flush() + + expect(saveStatus().textContent).toContain('Connection ok.') + 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()) + + 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: a rewrite would announce it as if the admin + // had just changed something. + 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) + }) +})