diff --git a/admin/views/FindReplace/results.php b/admin/views/FindReplace/results.php index 33d7c48..d196f76 100644 --- a/admin/views/FindReplace/results.php +++ b/admin/views/FindReplace/results.php @@ -27,7 +27,6 @@ [$badgeClass, $badgeLabel] = $badges[$search->status()] ?? ['badge-secondary', $search->status()]; $scanned = $search->tablesScanned(); $totalTables = $search->tablesTotal(); -$percent = $totalTables > 0 ? (int) floor(($scanned / $totalTables) * 100) : 0; $running = $search->status() === Search::STATUS_RUNNING; ?> @@ -106,15 +105,11 @@ if ($search->isOpen()) { ?> -

- -

-

> - - - - / -

+
> + Waiting for a worker to start this search. +
+

>Scanning tables.

+
>
span{display:block;height:100%;background:#337ab7}.find-replace-progress{font-variant-numeric:tabular-nums;white-space:nowrap}.find-replace-error{margin-top:.35rem;color:#a94442} +.find-replace-results mark,.find-replace-snippet mark,.find-replace-preview mark{padding:0 .15em}.find-replace-snippet mark{background-color:#c8f7c5}.find-replace-preview mark{background-color:#c5ddf7}.find-replace-results td.actions .btn{margin-bottom:.25rem}[data-find-replace-status]{font-variant-numeric:tabular-nums}.find-replace-searches{margin-top:2.5rem}.find-replace-searches .find-replace-status,.find-replace-searches .find-replace-progress,.find-replace-searches .find-replace-results,.find-replace-searches .find-replace-last-run,.find-replace-searches .actions{width:250px}.find-replace-detail__term{font-size:1.25rem}.find-replace-detail__arrow{margin:0 .35rem;color:#6c757d}.find-replace-detail__meta{display:flex;flex-wrap:wrap;align-items:center;gap:.5rem .75rem}.find-replace-detail__facts{display:flex;flex-wrap:wrap;gap:0 1.5rem}.find-replace-detail__fact{display:flex;flex-direction:column;gap:.15rem}.find-replace-detail__fact+.find-replace-detail__fact{padding-left:1.5rem;border-left:1px solid #dee2e6}.find-replace-detail__fact-label{color:#6c757d;font-size:.75rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase}.find-replace-detail__fact-value{font-size:1rem;font-variant-numeric:tabular-nums}.find-replace-detail__scope{color:#6c757d}.find-replace-progress{font-variant-numeric:tabular-nums;white-space:nowrap}.find-replace-error{margin-top:.35rem;color:#a94442} diff --git a/assets/js/admin.min.js b/assets/js/admin.min.js index 800bb60..2038568 100644 --- a/assets/js/admin.min.js +++ b/assets/js/admin.min.js @@ -1 +1 @@ -!function(){"use strict";var e=class{constructor(e){this.adminController=e,this.pendingScope="selected",this.modal=null,e.onRefreshUi(()=>{this.bind()}),this.bind()}dialog(){return this.modal||(this.modal=this.adminController.getInstance("Modal").create(),this.modal.setTitle("Replace text?").addAction("Replace",["btn-danger"],()=>{this.apply()}).addAction("Cancel",["btn-default"],()=>{this.modal.hide()})),this.modal}show(e){const t=document.createElement("p");t.textContent=e,this.dialog().setBody(t).show()}apply(){const e=document.getElementById("find-replace-results");if(!e)return;let t=e.querySelector('input[type="hidden"][name="scope"]');t||(t=document.createElement("input"),t.type="hidden",t.name="scope",e.appendChild(t)),t.value=this.pendingScope,e.submit()}bind(){const e=document.getElementById("find-replace-results");e&&!e.classList.contains("js-find-replace-bound")&&(e.classList.add("js-find-replace-bound"),e.addEventListener("change",t=>{t.target.matches(".js-toggle-all")&&e.querySelectorAll('input[name="result_ids[]"]').forEach(e=>{e.checked=t.target.checked}),t.target.matches('.js-toggle-all, input[name="result_ids[]"]')&&this.sync(e)})),document.querySelectorAll(".js-find-replace-apply:not(.processed)").forEach(t=>{t.classList.add("processed"),t.addEventListener("click",s=>{s.preventDefault(),this.pendingScope=t.getAttribute("data-scope")||"selected",this.show(this.message(e,this.pendingScope))})}),this.sync(e)}message(e,t){if("all"===t)return e&&"1"===e.getAttribute("data-filtered")?"This updates every saved result that matches the current filters.":"This updates every saved result for this search.";const s=this.selectedCount(e);return`This updates ${s} selected ${1===s?"record":"records"}.`}selectedCount(e){return e?e.querySelectorAll('input[name="result_ids[]"]:checked').length:0}sync(e){if(!e)return;const t=e.querySelector('.js-find-replace-apply[data-scope="selected"]');t&&(t.disabled=0===this.selectedCount(e))}};const t={PENDING:{variant:"badge-warning",label:"Pending"},RUNNING:{variant:"badge-info",label:"Running"},COMPLETE:{variant:"badge-success",label:"Complete"},FAILED:{variant:"badge-danger",label:"Failed"}};function s(e){return t[e]||{variant:"badge-secondary",label:e||"Unknown"}}const n={PENDING:"Waiting for a worker to start this search.",RUNNING:"Scanning tables."};var a=class{constructor(){document.querySelectorAll("[data-find-replace-status]").forEach(e=>{this.watch(e)})}watch(e){const t=e.getAttribute("data-url");t&&window.setTimeout(()=>{fetch(t,{credentials:"same-origin",headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}}).then(e=>e.json()).then(t=>{const s=t&&t.data&&!Array.isArray(t.data)?t.data:t;this.paint(e,s),"COMPLETE"!==s.status&&"FAILED"!==s.status?this.watch(e):window.location.reload()}).catch(()=>{this.watch(e)})},2e3)}paint(e,t){const a=t.status||"",r=e.querySelector("[data-find-replace-badge]"),i=e.querySelector("[data-find-replace-message]"),c=e.querySelector("[data-find-replace-meter]"),l=e.querySelector("[data-find-replace-bar]"),o=e.querySelector(".find-replace-progress"),d=Number(t.tables_scanned)||0,h=Number(t.tables_total)||0,u=h>0?Math.min(100,Math.floor(d/h*100)):0;r&&function(e,t){const n=s(t);e.className="badge "+n.variant,e.textContent=n.label}(r,a),i&&n[a]&&(i.textContent=n[a]),c&&(c.hidden="RUNNING"!==a),l&&(l.style.width=u+"%"),o&&(o.textContent=d+" / "+h);const p=e.querySelector(".find-replace-meter");p&&(p.setAttribute("aria-valuemax",String(h)),p.setAttribute("aria-valuenow",String(d)))}},r=class{constructor(){this.root=document.getElementById("find-replace"),this.form=document.getElementById("find-replace-form"),this.rows=document.getElementById("find-replace-searches"),this.alert=document.getElementById("find-replace-alert"),this.items=[],this.timer=null,this.root&&this.form&&this.rows&&(this.api=this.root.getAttribute("data-api"),this.form.addEventListener("submit",e=>{e.preventDefault(),this.create()}),this.load())}create(){const e=new FormData(this.form),t=this.form.querySelector('button[type="submit"]');t&&(t.disabled=!0),this.request(this.api,"POST",{term:e.get("term")||"",replacement:e.get("replacement")||"",case_sensitive:e.getAll("case_sensitive"),include_tables:e.get("include_tables")||"",exclude_tables:e.get("exclude_tables")||""}).then(()=>(this.message("success","Search queued."),this.form.reset(),this.restoreCaseSensitive(),this.load())).catch(e=>{this.message("danger",e.message)}).finally(()=>{t&&(t.disabled=!1)})}restoreCaseSensitive(){const e=this.form.querySelector('input[type="checkbox"][name="case_sensitive"]');e&&(e.checked=!0)}load(){return window.clearTimeout(this.timer),this.request(this.api,"GET").then(e=>{const t=Array.isArray(e.data)?e.data:[];this.render(t);const s=t.some(e=>"PENDING"===e.status||"RUNNING"===e.status);this.timer=window.setTimeout(()=>this.load(),s?2500:6e4)}).catch(e=>{this.message("danger",e.message),this.timer=window.setTimeout(()=>this.load(),6e4)})}render(e){const t=e.map(e=>e.id);e.forEach(e=>{const t=this.items.find(t=>t.id===e.id);t?this.update(t.node,e):this.items.push({id:e.id,node:this.add(e)})});for(let e=this.items.length-1;e>=0;e--)t.includes(this.items[e].id)||(this.items[e].node.remove(),this.items.splice(e,1));0===this.items.length&&(this.rows.textContent="",this.rows.append(this.empty("No searches yet")))}add(e){const t=this.rows.querySelector(".no-data");t&&t.closest("tr").remove();const s=document.createElement("tr");return s.append(this.cell("term",this.link(e)),this.cell("status"),this.cell("progress"),this.cell("results"),this.cell("last-run"),this.actions(e)),this.update(s,e),this.rows.append(s),s}update(e,t){const n=e.querySelector(".find-replace-term").querySelector("a");n&&(n.href=t.results_url,n.textContent=t.term);const a=e.querySelector(".find-replace-status");if(a.textContent="",a.append(function(e){const t=s(e),n=document.createElement("span");return n.className="badge "+t.variant,n.textContent=t.label,n}(t.status)),t.error){const e=document.createElement("div");e.className="find-replace-error",e.textContent=t.error,a.append(e)}e.querySelector(".find-replace-progress").textContent=t.tables_scanned+" / "+t.tables_total,e.querySelector(".find-replace-results").textContent=String(t.result_count),e.querySelector(".find-replace-last-run").textContent=t.last_run||"";const r=e.querySelector('[data-action="persistent"]');r.textContent=t.persistent?"Stop keeping":"Keep",r.dataset.persistent=t.persistent?"0":"1"}actions(e){const t=document.createElement("td");return t.className="actions",t.append(this.button("Keep","persistent",()=>this.persist(e.id,t)),this.button("Run again","rerun",()=>this.rerun(e.id)),this.button("Delete","delete",()=>this.remove(e.id),"btn-danger")),t}persist(e,t){const s=t.querySelector('[data-action="persistent"]');this.request(this.api+"/persistent","POST",{id:e,persistent:"1"===s.dataset.persistent}).then(()=>this.load()).catch(e=>this.message("danger",e.message))}rerun(e){this.request(this.api+"/rerun","POST",{id:e}).then(()=>this.load()).catch(e=>this.message("danger",e.message))}remove(e){window.confirm("Delete this search and its results?")&&this.request(this.api+"/delete","POST",{id:e}).then(()=>this.load()).catch(e=>this.message("danger",e.message))}button(e,t,s,n){const a=document.createElement("button");return a.type="button",a.className="btn btn-sm "+(n||"btn-default"),a.dataset.action=t,a.textContent=e,a.addEventListener("click",s),a}cell(e,t){const s=document.createElement("td");return s.className="find-replace-"+e,t&&s.append(t),s}link(e){const t=document.createElement("a");return t.href=e.results_url,t.textContent=e.term,t}empty(e){const t=document.createElement("tr"),s=document.createElement("td");return s.colSpan=6,s.className="no-data",s.textContent=e,t.append(s),t}message(e,t){this.alert&&(this.alert.hidden=!1,this.alert.className="alert alert-"+e,this.alert.textContent=t)}request(e,t,s){const n={method:t,credentials:"same-origin",headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}};return s&&(n.headers["Content-Type"]="application/json",n.body=JSON.stringify(s)),fetch(e,n).then(e=>e.json().then(t=>{if(!e.ok)throw new Error(t.error||"The request failed.");return t}))}};window.NAILS.ADMIN.registerPlugin("nails/module-find-replace","Confirm",t=>new e(t)).registerPlugin("nails/module-find-replace","Progress",()=>new a).registerPlugin("nails/module-find-replace","Searches",()=>new r)}(); \ No newline at end of file +(()=>{"use strict";const e=class{constructor(e){this.adminController=e,this.pendingScope="selected",this.modal=null,e.onRefreshUi(()=>{this.bind()}),this.bind()}dialog(){return this.modal||(this.modal=this.adminController.getInstance("Modal").create(),this.modal.setTitle("Replace text?").addAction("Replace",["btn-danger"],()=>{this.apply()}).addAction("Cancel",["btn-default"],()=>{this.modal.hide()})),this.modal}show(e){const t=document.createElement("p");t.textContent=e,this.dialog().setBody(t).show()}apply(){const e=document.getElementById("find-replace-results");if(!e)return;let t=e.querySelector('input[type="hidden"][name="scope"]');t||(t=document.createElement("input"),t.type="hidden",t.name="scope",e.appendChild(t)),t.value=this.pendingScope,e.submit()}bind(){const e=document.getElementById("find-replace-results");e&&!e.classList.contains("js-find-replace-bound")&&(e.classList.add("js-find-replace-bound"),e.addEventListener("change",t=>{t.target.matches(".js-toggle-all")&&e.querySelectorAll('input[name="result_ids[]"]').forEach(e=>{e.checked=t.target.checked}),t.target.matches('.js-toggle-all, input[name="result_ids[]"]')&&this.sync(e)})),document.querySelectorAll(".js-find-replace-apply:not(.processed)").forEach(t=>{t.classList.add("processed"),t.addEventListener("click",s=>{s.preventDefault(),this.pendingScope=t.getAttribute("data-scope")||"selected",this.show(this.message(e,this.pendingScope))})}),this.sync(e)}message(e,t){if("all"===t)return e&&"1"===e.getAttribute("data-filtered")?"This updates every saved result that matches the current filters.":"This updates every saved result for this search.";const s=this.selectedCount(e);return`This updates ${s} selected ${1===s?"record":"records"}.`}selectedCount(e){return e?e.querySelectorAll('input[name="result_ids[]"]:checked').length:0}sync(e){if(!e)return;const t=e.querySelector('.js-find-replace-apply[data-scope="selected"]');t&&(t.disabled=0===this.selectedCount(e))}},t={PENDING:{variant:"badge-warning",label:"Pending"},RUNNING:{variant:"badge-info",label:"Running"},COMPLETE:{variant:"badge-success",label:"Complete"},FAILED:{variant:"badge-danger",label:"Failed"}};function s(e){return t[e]||{variant:"badge-secondary",label:e||"Unknown"}}const n=class{constructor(){document.querySelectorAll("[data-find-replace-status]").forEach(e=>{this.watch(e)})}watch(e){const t=e.getAttribute("data-url");t&&window.setTimeout(()=>{fetch(t,{credentials:"same-origin",headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}}).then(e=>e.json()).then(t=>{const s=t&&t.data&&!Array.isArray(t.data)?t.data:t;this.paint(e,s),"COMPLETE"!==s.status&&"FAILED"!==s.status?this.watch(e):window.location.reload()}).catch(()=>{this.watch(e)})},2e3)}paint(e,t){const n=t.status||"",a=e.querySelector("[data-find-replace-badge]"),r=e.querySelector("[data-find-replace-message]"),i=e.querySelector("[data-find-replace-scanning]"),c=e.querySelector(".js-admin-progress"),l=Number(t.tables_scanned)||0,d=Number(t.tables_total)||0;a&&function(e,t){const n=s(t);e.className="badge "+n.variant,e.textContent=n.label}(a,n),r&&(r.hidden="PENDING"!==n),i&&(i.hidden="RUNNING"!==n),c&&(c.hidden="RUNNING"!==n,c.dataset.max=String(d),c.dataset.value=String(l))}},a=class{constructor(){this.root=document.getElementById("find-replace"),this.form=document.getElementById("find-replace-form"),this.rows=document.getElementById("find-replace-searches"),this.alert=document.getElementById("find-replace-alert"),this.items=[],this.timer=null,this.root&&this.form&&this.rows&&(this.api=this.root.getAttribute("data-api"),this.form.addEventListener("submit",e=>{e.preventDefault(),this.create()}),this.load())}create(){const e=new FormData(this.form),t=this.form.querySelector('button[type="submit"]');t&&(t.disabled=!0),this.request(this.api,"POST",{term:e.get("term")||"",replacement:e.get("replacement")||"",case_sensitive:e.getAll("case_sensitive"),include_tables:e.get("include_tables")||"",exclude_tables:e.get("exclude_tables")||""}).then(()=>(this.message("success","Search queued."),this.form.reset(),this.restoreCaseSensitive(),this.load())).catch(e=>{this.message("danger",e.message)}).finally(()=>{t&&(t.disabled=!1)})}restoreCaseSensitive(){const e=this.form.querySelector('input[type="checkbox"][name="case_sensitive"]');e&&(e.checked=!0)}load(){return window.clearTimeout(this.timer),this.request(this.api,"GET").then(e=>{const t=Array.isArray(e.data)?e.data:[];this.render(t);const s=t.some(e=>"PENDING"===e.status||"RUNNING"===e.status);this.timer=window.setTimeout(()=>this.load(),s?2500:6e4)}).catch(e=>{this.message("danger",e.message),this.timer=window.setTimeout(()=>this.load(),6e4)})}render(e){const t=e.map(e=>e.id);e.forEach(e=>{const t=this.items.find(t=>t.id===e.id);t?this.update(t.node,e):this.items.push({id:e.id,node:this.add(e)})});for(let e=this.items.length-1;e>=0;e--)t.includes(this.items[e].id)||(this.items[e].node.remove(),this.items.splice(e,1));0===this.items.length&&(this.rows.textContent="",this.rows.append(this.empty("No searches yet")))}add(e){const t=this.rows.querySelector(".no-data");t&&t.closest("tr").remove();const s=document.createElement("tr");return s.append(this.cell("term",this.link(e)),this.cell("status"),this.cell("progress"),this.cell("results"),this.cell("last-run"),this.actions(e)),this.update(s,e),this.rows.append(s),s}update(e,t){const n=e.querySelector(".find-replace-term").querySelector("a");n&&(n.href=t.results_url,n.textContent=t.term);const a=e.querySelector(".find-replace-status");if(a.textContent="",a.append(function(e){const t=s(e),n=document.createElement("span");return n.className="badge "+t.variant,n.textContent=t.label,n}(t.status)),t.error){const e=document.createElement("div");e.className="find-replace-error",e.textContent=t.error,a.append(e)}e.querySelector(".find-replace-progress").textContent=t.tables_scanned+" / "+t.tables_total,e.querySelector(".find-replace-results").textContent=String(t.result_count),e.querySelector(".find-replace-last-run").textContent=t.last_run||"";const r=e.querySelector('[data-action="persistent"]');r.textContent=t.persistent?"Stop keeping":"Keep",r.dataset.persistent=t.persistent?"0":"1"}actions(e){const t=document.createElement("td");return t.className="actions",t.append(this.button("Keep","persistent",()=>this.persist(e.id,t)),this.button("Run again","rerun",()=>this.rerun(e.id)),this.button("Delete","delete",()=>this.remove(e.id),"btn-danger")),t}persist(e,t){const s=t.querySelector('[data-action="persistent"]');this.request(this.api+"/persistent","POST",{id:e,persistent:"1"===s.dataset.persistent}).then(()=>this.load()).catch(e=>this.message("danger",e.message))}rerun(e){this.request(this.api+"/rerun","POST",{id:e}).then(()=>this.load()).catch(e=>this.message("danger",e.message))}remove(e){window.confirm("Delete this search and its results?")&&this.request(this.api+"/delete","POST",{id:e}).then(()=>this.load()).catch(e=>this.message("danger",e.message))}button(e,t,s,n){const a=document.createElement("button");return a.type="button",a.className="btn btn-sm "+(n||"btn-default"),a.dataset.action=t,a.textContent=e,a.addEventListener("click",s),a}cell(e,t){const s=document.createElement("td");return s.className="find-replace-"+e,t&&s.append(t),s}link(e){const t=document.createElement("a");return t.href=e.results_url,t.textContent=e.term,t}empty(e){const t=document.createElement("tr"),s=document.createElement("td");return s.colSpan=6,s.className="no-data",s.textContent=e,t.append(s),t}message(e,t){this.alert&&(this.alert.hidden=!1,this.alert.className="alert alert-"+e,this.alert.textContent=t)}request(e,t,s){const n={method:t,credentials:"same-origin",headers:{Accept:"application/json","X-Requested-With":"XMLHttpRequest"}};return s&&(n.headers["Content-Type"]="application/json",n.body=JSON.stringify(s)),fetch(e,n).then(e=>e.json().then(t=>{if(!e.ok)throw new Error(t.error||"The request failed.");return t}))}};window.NAILS.ADMIN.registerPlugin("nails/module-find-replace","Confirm",t=>new e(t)).registerPlugin("nails/module-find-replace","Progress",()=>new n).registerPlugin("nails/module-find-replace","Searches",()=>new a)})(); \ No newline at end of file diff --git a/assets/js/components/Progress.js b/assets/js/components/Progress.js index cba1305..60a5bfd 100644 --- a/assets/js/components/Progress.js +++ b/assets/js/components/Progress.js @@ -1,10 +1,5 @@ import {paintBadge} from './Status.js'; -const MESSAGES = { - PENDING: 'Waiting for a worker to start this search.', - RUNNING: 'Scanning tables.', -}; - class Progress { constructor() { document.querySelectorAll('[data-find-replace-status]').forEach((node) => { @@ -49,38 +44,27 @@ class Progress { const status = payload.status || ''; const badgeNode = node.querySelector('[data-find-replace-badge]'); const message = node.querySelector('[data-find-replace-message]'); - const meter = node.querySelector('[data-find-replace-meter]'); - const bar = node.querySelector('[data-find-replace-bar]'); - const progress = node.querySelector('.find-replace-progress'); + const scanning = node.querySelector('[data-find-replace-scanning]'); + const meter = node.querySelector('.js-admin-progress'); const scanned = Number(payload.tables_scanned) || 0; const total = Number(payload.tables_total) || 0; - const percent = total > 0 ? Math.min(100, Math.floor((scanned / total) * 100)) : 0; if (badgeNode) { paintBadge(badgeNode, status); } - if (message && MESSAGES[status]) { - message.textContent = MESSAGES[status]; - } - - if (meter) { - meter.hidden = status !== 'RUNNING'; - } - - if (bar) { - bar.style.width = percent + '%'; + if (message) { + message.hidden = status !== 'PENDING'; } - if (progress) { - progress.textContent = scanned + ' / ' + total; + if (scanning) { + scanning.hidden = status !== 'RUNNING'; } - const track = node.querySelector('.find-replace-meter'); - - if (track) { - track.setAttribute('aria-valuemax', String(total)); - track.setAttribute('aria-valuenow', String(scanned)); + if (meter) { + meter.hidden = status !== 'RUNNING'; + meter.dataset.max = String(total); + meter.dataset.value = String(scanned); } } } diff --git a/assets/sass/admin.scss b/assets/sass/admin.scss index d6b351e..611b2e8 100644 --- a/assets/sass/admin.scss +++ b/assets/sass/admin.scss @@ -86,26 +86,6 @@ color: #6c757d; } -.find-replace-meter-row { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.find-replace-meter { - flex: 1; - height: 0.5rem; - overflow: hidden; - border-radius: 1rem; - background: #e9ecef; -} - -.find-replace-meter > span { - display: block; - height: 100%; - background: #337ab7; -} - .find-replace-progress { font-variant-numeric: tabular-nums; white-space: nowrap;