domma-cms 1.6.52 → 1.6.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as b}from"../api.js";import{helpPopovers as H}from"./help-popover.js";import{onceClosed as P,promptWith as U}from"./tool-kit.js";import{MAX_TAG as S,UNTAGGED as K,normaliseActive as W,normaliseTag as C,same as N}from"./list-tags-arrange.js";export*from"./list-tags-arrange.js";const x=a=>String(a??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");export async function askTagName({title:a="New tag",value:l="",confirmText:u="Add"}={}){const p=await U(`Up to ${S} characters. Tags are only seen here in the admin.`,{title:a,inputPlaceholder:"e.g. Spring campaign",confirmText:u},l);return p==null?"":C(String(p))}let w=!1;function X(){if(w||document.getElementById("ltg-styles")){w=!0;return}const a=document.createElement("style");a.id="ltg-styles",a.textContent=`
|
|
2
2
|
.ltg-panel { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
|
|
3
3
|
.ltg-panel .form-label { display: flex; align-items: center; gap: .3rem; font-weight: 500; margin-bottom: .3rem; }
|
|
4
4
|
.ltg-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); }
|
|
@@ -14,7 +14,7 @@ import{api as x}from"../api.js";import{helpPopovers as H}from"./help-popover.js"
|
|
|
14
14
|
.ltg-add .form-input { flex: 1; }
|
|
15
15
|
.ltg-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); }
|
|
16
16
|
.ltg-actions { display: flex; align-items: center; gap: .5rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); }
|
|
17
|
-
.ltg-actions .spacer { flex: 1; }`,document.head.appendChild(a),w=!0}export function openTagSettings({kind:a,noun:l,title:
|
|
17
|
+
.ltg-actions .spacer { flex: 1; }`,document.head.appendChild(a),w=!0}export function openTagSettings({kind:a,noun:l,title:u,tags:p,active:i,reload:A}){X();const d=E.slideover({title:u,size:"sm",position:"right"}),$=document.createElement("template");$.innerHTML=`
|
|
18
18
|
<div class="ltg-panel">
|
|
19
19
|
<div>
|
|
20
20
|
<label class="form-label" for="ltg-show">Show
|
|
@@ -34,4 +34,4 @@ import{api as x}from"../api.js";import{helpPopovers as H}from"./help-popover.js"
|
|
|
34
34
|
<small class="ltg-meta">Tags are shared by everyone who edits ${l}.</small>
|
|
35
35
|
<div class="ltg-actions"><span class="spacer"></span>
|
|
36
36
|
<button type="button" class="btn btn-sm btn-primary ltg-done">Done</button></div>
|
|
37
|
-
</div>`;const o=$.content.firstElementChild,g=o.querySelector("#ltg-show"),
|
|
37
|
+
</div>`;const o=$.content.firstElementChild,g=o.querySelector("#ltg-show"),f=o.querySelector(".ltg-list"),q=o.querySelector(".ltg-empty"),m=o.querySelector(".ltg-add input");let h=!1;const y=(e,n)=>{const t=document.createElement("option");return t.value=e,t.textContent=n,t},T=(e,n,t,c)=>{const r=document.createElement("button");r.type="button",r.title=n,r.setAttribute("aria-label",n),t&&(r.className=t);const s=document.createElement("span");return s.setAttribute("data-icon",e),s.setAttribute("data-icon-size","14"),r.appendChild(s),r.addEventListener("click",c),r};async function v(e,n){if(!h){h=!0;try{const t=await e();return await A(),n&&E.toast(n(t),{type:"success"}),!0}catch(t){return E.toast(t.message||"That did not work.",{type:"error"}),!1}finally{h=!1}}}async function D(e){const n=await askTagName({title:`Rename "${x(e)}"`,value:e,confirmText:"Rename"});if(!n||n===e)return;await v(()=>b.listTags.rename(a,e,n),c=>`Renamed - ${c.changed} ${l} updated.`)&&N(i.peek(),e)&&(i.value=n)}async function R(e){const n=e.count?`Delete the tag "${x(e.name)}"? It comes off ${e.count} ${e.count===1?l.replace(/s$/,""):l}; nothing else about them changes.`:`Delete the tag "${x(e.name)}"?`;if(!await E.confirm(n))return;await v(()=>b.listTags.remove(a,e.name),()=>"Tag deleted.")&&N(i.peek(),e.name)&&(i.value="")}async function k(){const e=C(m.value);if(!e){m.focus();return}await v(()=>b.listTags.add(a,e)),m.value="",m.focus()}const L=M.effect(()=>{const e=p.value,n=i.value;g.replaceChildren(y("",`All ${l}`),y(K,"No tag"),...e.map(t=>y(t.name,`${t.name} (${t.count})`))),g.value=n,f.replaceChildren(...e.map(t=>{const c=document.createElement("li"),r=document.createElement("span");r.className="ltg-name",r.textContent=t.name;const s=document.createElement("span");return s.className="ltg-count",s.textContent=String(t.count),c.append(r,s,T("edit",`Rename ${t.name}`,"",()=>D(t.name)),T("trash",`Delete ${t.name}`,"is-danger",()=>R(t))),c})),f.hidden=!e.length,q.hidden=e.length>0,I.scan(f)}),z=()=>{i.value=W(g.value)},G=e=>{e.key==="Enter"&&e.target===m&&(e.preventDefault(),k())};g.addEventListener("change",z),o.addEventListener("keydown",G),o.querySelector(".ltg-add button").addEventListener("click",k),o.querySelector(".ltg-done").addEventListener("click",()=>d.close()),d.element.appendChild(o);const B=H(o);return P(d,()=>{L(),B()}),I.scan(o),d.open(),d}
|
package/admin/js/lib/tool-kit.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export function onceClosed(t,e=()=>{}){const
|
|
1
|
+
export function onceClosed(t,e=()=>{}){const o=t.close.bind(t),c=t.close;let n=!1;t.close=(...r)=>{const i=o(...r);return n||(n=!0,e(),setTimeout(()=>{t.close=c;try{t.destroy?.()}catch{}t.element?.remove()},400)),i}}export function openMenuAt(t){const e=t.getBoundingClientRect();t.dispatchEvent(new MouseEvent("contextmenu",{bubbles:!0,cancelable:!0,clientX:e.left,clientY:e.bottom,view:window}))}export function settingsStore(t,e){return{read(){try{return e(S.get(t))}catch{return e(null)}},write(o){try{S.set(t,o)}catch{}}}}export function viewLifetime(t){let e=[];const o=()=>{const n=e;e=[],n.forEach(r=>{try{r?.()}catch{}})},c=()=>{t.test(location.hash)||o()};return{add(n){return e.push(n),()=>{const r=e.indexOf(n);r!==-1&&e.splice(r,1)}},reset(){o(),window.addEventListener("hashchange",c),e.push(()=>window.removeEventListener("hashchange",c))}}}export function sidebarCount(t,e={}){try{window.dispatchEvent(new CustomEvent("dm:sidebar-count",{detail:{url:t,...e}}))}catch{}}export function promptWith(t,e,o){const c=E.prompt(t,e),n=[...document.querySelectorAll(".dm-dialog-input")].at(-1);return n&&(n.value=String(o??""),setTimeout(()=>{try{n.select()}catch{}},60)),c}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{protectToolEditor as
|
|
1
|
+
import{protectToolEditor as ze,mountToolTrash as Ke,mountGroupBatch as _e}from"../lib/tool-recovery.js";import{apiRequest as Je}from"../api.js";import{addBannerAction as Ve}from"../lib/plugin-chrome.js";import{mountRelated as Qe,relatedMenuItems as Xe,relatedProviders as Ze}from"../lib/related.js";import{helpPopovers as Z}from"../lib/help-popover.js";import{onceClosed as ae,openMenuAt as oe,promptWith as et,settingsStore as tt,viewLifetime as at}from"../lib/tool-kit.js";import{arrangeContacts as ot,countContacts as st,DEFAULT_SETTINGS as we,duplicateBody as nt,initials as ct,normaliseSettings as Ee,primaryEmail as Se,primaryPhone as Me,withGroup as $e}from"../lib/contacts-arrange.js";const Ce=tt("contacts.settings",Ee),v=(N,i="GET",j)=>Je(N,{method:i,...j!==void 0?{body:JSON.stringify(j)}:{}}),se={company:"Company",jobTitle:"Job title",website:"Website",billingEmail:"Billing email",vatNumber:"VAT number",companyNumber:"Company number",reference:"Their reference",paymentTerms:"Payment terms",currency:"Preferred currency",accountName:"Account name",bankName:"Bank",sortCode:"Sort code",accountNumber:"Account number",iban:"IBAN",bic:"BIC / SWIFT"},T={addresses:{title:"Addresses",add:"Add address",empty:"No address yet.",help:'Tick "Use for invoices" on the one an invoice should be addressed to.',columns:[{key:"label",placeholder:"Label (Office, Home\u2026)"},{key:"line1",placeholder:"Address line 1",wide:!0},{key:"line2",placeholder:"Address line 2",wide:!0},{key:"city",placeholder:"City"},{key:"region",placeholder:"County / Region"},{key:"postcode",placeholder:"Postcode"},{key:"country",placeholder:"Country"},{key:"isBilling",label:"Use for invoices",check:!0}]},emails:{title:"More email addresses",add:"Add email",empty:"None.",help:"Besides the main email above. Search finds a contact by these too.",columns:[{key:"label",placeholder:"Label (Accounts, Personal\u2026)"},{key:"address",placeholder:"name@example.com",wide:!0}]},phones:{title:"More phone numbers",add:"Add phone",empty:"None.",help:"Besides the main phone above. Call in the menu uses the main one, or the first of these.",columns:[{key:"label",placeholder:"Label (Mobile, Office\u2026)"},{key:"number",placeholder:"+44 \u2026",wide:!0}]}},lt=`
|
|
2
2
|
<div class="ct-row{{selClass}}" data-id="{{id}}" tabindex="0">
|
|
3
3
|
<input type="checkbox" class="ct-select" aria-label="Select {{name}}" data-bind-checked="selected">
|
|
4
4
|
<button type="button" class="ct-fav{{favClass}}" title="{{favTitle}}" aria-label="{{favTitle}}">\u2605</button>
|
|
@@ -11,13 +11,13 @@ import{protectToolEditor as Ke,mountToolTrash as We,mountGroupBatch as _e}from".
|
|
|
11
11
|
<span class="ct-col ct-phone">{{phone}}</span>
|
|
12
12
|
<span class="ct-col ct-groups"><span class="ct-groups-text" title="{{groupsText}}">{{groupsText}}</span></span>
|
|
13
13
|
<button type="button" class="ct-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
|
|
14
|
-
</div>`,
|
|
14
|
+
</div>`,rt='<button type="button" class="ct-chip{{cls}}" data-chip="{{key}}" title="{{title}}">{{label}} <b>{{count}}</b></button>',it='<button type="button" class="ct-chip{{cls}}" data-group="{{name}}" aria-pressed="{{on}}">{{name}}</button>',dt=`
|
|
15
15
|
<div class="ct-group-row" data-group="{{name}}">
|
|
16
16
|
<span>{{name}}</span><small>{{countText}}</small>
|
|
17
17
|
<button type="button" class="btn btn-sm btn-ghost" data-act="show">Show</button>
|
|
18
18
|
<button type="button" class="btn btn-sm btn-ghost" data-act="rename" data-bind-disabled="locked" title="{{lockTitle}}">Rename</button>
|
|
19
19
|
<button type="button" class="btn btn-sm btn-ghost text-danger" data-act="delete" data-bind-disabled="locked" title="{{lockTitle}}">Delete</button>
|
|
20
|
-
</div>`,
|
|
20
|
+
</div>`,pt="Groups are shared by everyone who uses Contacts, so renaming or deleting one changes their contacts too. Only the person who created a group can do that, or someone whose role has Manage groups in Contacts (admins do, unless it was taken away). Groups made before creators were recorded belong to the Manage groups holders. You can still file your own contacts in any group.";function ne(N){const i=document.createElement("template");return i.innerHTML=N.trim(),i.content.firstElementChild}const R=(N,i,j=`${i}s`)=>`${N} ${N===1?i:j}`,h=at(/^#\/contacts\b/);export const contactsView={templateUrl:"/admin/js/templates/contacts.html",async onMount(N){h.reset();const i=N.get(0);let j=[];Ze("contact").then(t=>{j=t});const x=M.observable(Ce.read()),m=M.observable([]),y=M.observable([]),_=M.observable(!0),Q=M.observable(""),ce=M.observable(""),H=M.observable({kind:"all"}),p=M.observable(new Set),le=M.observable(!1),re=M.computed(()=>st(m.value,y.value)),Te=M.computed(()=>ot(m.value,{query:ce.value,filter:H.value,sort:x.value.sort,favouritesFirst:x.value.favouritesFirst})),A=M.observable([]);h.add(M.effect(()=>{const t=p.value;A.value=Te.value.map(e=>({id:e.id,name:e.name||"Unnamed",sub:[e.jobTitle,e.company&&!x.value.showCompany?e.company:""].filter(Boolean).join(" \xB7 "),initials:ct(e.name),company:e.company||"",email:Se(e),phone:Me(e),groupsText:(e.groups||[]).join(", "),favClass:e.favourite===!0?" is-on":"",favTitle:e.favourite===!0?"Starred - click to unstar":"Star this contact",selected:t.has(e.id),selClass:t.has(e.id)?" is-selected":""}))}));const ie=M.observable([]);h.add(M.effect(()=>{const t=re.value,e=H.value;ie.value=[{key:"all",label:"All",count:t.all,title:"Everyone",cls:e.kind==="all"?" is-active":""},{key:"fav",label:"\u2605 Starred",count:t.fav,title:"Only starred contacts",cls:e.kind==="fav"?" is-active":""},...y.value.map(o=>({key:`group:${o}`,label:o,count:t.groups[o]??0,title:`Only the ${o} group`,cls:e.kind==="group"&&e.value===o?" is-active":""}))]})),h.add(M.effect(()=>{const t=H.value;t.kind==="group"&&!y.value.includes(t.value)&&(H.value={kind:"all"})}));const xe={loadError:Q,rows:A,chips:ie,loading:_,query:ce,searchFocus:le,hasRows:M.computed(()=>A.value.length>0),isEmpty:M.computed(()=>!_.value&&!Q.value&&A.value.length===0),emptyText:M.computed(()=>m.value.length?"No contacts match. Clear the search or pick All.":"No contacts yet. New contact adds one, or \u22EE > Import brings a file in."),countText:M.computed(()=>{if(_.value)return"";const t=A.value.length,e=m.value.length;return t===e?R(e,"contact"):`${t} of ${e}`}),hasSelection:M.computed(()=>p.value.size>0),selectionText:M.computed(()=>`${R(p.value.size,"contact")} selected`),allSelected:M.computed(()=>A.value.length>0&&A.value.every(t=>p.value.has(t.id)))};h.add(M.effect(()=>Ce.write(x.value)));const Ae=(t,e)=>{x.value=Ee({...x.peek(),[t]:e})},q=t=>m.peek().find(e=>e.id===t),Ne=t=>{m.value=m.peek().map(e=>e.id===t.id?t:e)};async function Y(){_.value=!0;try{const[t,e]=await Promise.all([v("/contacts/contacts"),v("/contacts/groups")]);m.value=Array.isArray(t)?t:t?.data??[],y.value=Array.isArray(e)?e:[],Q.value=""}catch{Q.value="Could not load this list. Your items have not been removed.",E.toast("Failed to load contacts.",{type:"error"})}finally{_.value=!1}}async function Le(){try{y.value=await v("/contacts/groups")}catch{}}async function J(t,e,o){try{return Ne(await v(`/contacts/contacts/${t}`,"PUT",e)),o&&E.toast(o,{type:"success"}),!0}catch(s){return E.toast(s?.message||"Failed to update the contact.",{type:"error"}),!1}}async function de(t){const e=q(t);if(x.peek().confirmDelete&&!await E.confirm("Move this contact to Trash? You can restore it later."))return!1;try{await v(`/contacts/contacts/${t}`,"DELETE"),m.value=m.peek().filter(s=>s.id!==t);const o=new Set(p.peek());return o.delete(t)&&(p.value=o),E.toast(`${e?.name?"Contact":"It"} deleted.`,{type:"success"}),!0}catch{return E.toast("Failed to delete the contact.",{type:"error"}),!1}}async function Be(t){const e=q(t);if(e)try{const o=await v("/contacts/contacts","POST",nt(e));m.value=[...m.peek(),o],E.toast("Copied - the copy is open to edit.",{type:"success"}),W(o.id)}catch{E.toast("Failed to duplicate the contact.",{type:"error"})}}async function V(t){if(y.peek().includes(t))return!0;try{return await v("/contacts/groups","POST",{name:t}),await Le(),!0}catch(e){return E.toast(e?.message||"Could not create the group.",{type:"error"}),!1}}async function Oe(t,e,o){const s=q(t);s&&(o&&!await V(e)||await J(t,{groups:$e(s.groups,e,o)},o?`Added to ${e}.`:`Removed from ${e}.`))}async function pe(t){const e=String(await E.prompt("New group name:")??"").trim();e&&await he.run(t,e)}async function ue(t,e){try{await navigator.clipboard.writeText(t),E.toast(`${e} copied.`,{type:"success"})}catch{E.toast("The browser would not allow copying.",{type:"error"})}}async function Pe(){const t=[...p.peek()];if(t.length&&await E.confirm(`Delete ${R(t.length,"contact")}? You can restore it from Trash.`))try{const e=await v("/contacts/contacts/bulk","DELETE",{ids:t}),o=e?.deletedIds??[];m.value=m.peek().filter(s=>!o.includes(s.id)),p.value=new Set(e?.failedIds??[]),E.toast(`${R(e?.deleted??t.length,"contact")} moved to Trash${e?.failedIds?.length?` \xB7 ${e.failedIds.length} failed (still selected)`:""}.`,{type:e?.failedIds?.length?"warning":"success"})}catch{E.toast("Bulk delete failed.",{type:"error"})}}async function Ge(t){await he.run([...p.peek()],t)}const me=t=>{const e=new Set(p.peek());e.delete(t)||e.add(t),p.value=e};async function Ie(){try{const t=await v("/contacts/export"),e=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),o=URL.createObjectURL(e),s=document.createElement("a");s.href=o,s.download="contacts.json",document.body.appendChild(s),s.click(),s.remove(),setTimeout(()=>URL.revokeObjectURL(o),1e3),E.toast("Contacts exported.",{type:"success"})}catch{E.toast("Export failed.",{type:"error"})}}const X=i.querySelector(".ct-import-file");X.addEventListener("change",async()=>{const t=X.files?.[0];if(X.value="",!!t)try{const e=JSON.parse(await t.text()),o=Array.isArray(e)?e:e.contacts;if(!Array.isArray(o))throw new Error("Invalid format");const s=await v("/contacts/import","POST",{contacts:o});E.toast(`Imported ${R(s?.imported??0,"contact")}.`,{type:"success"}),await Y()}catch{E.toast("Import failed. The file must be a contacts JSON export.",{type:"error"})}});function W(t){const e=t?q(t):null;if(t&&!e)return;const o=E.slideover({title:e?"Edit contact":"New contact",size:"lg",position:"right"}),s=(a,c,{type:B="text",mono:f=!1,placeholder:k="",wide:C=!1}={})=>`
|
|
21
21
|
<div${C?' class="ct-wide"':""}>
|
|
22
22
|
<label class="form-label" for="ct-f-${a}">${c}</label>
|
|
23
23
|
<input id="ct-f-${a}" type="${B}" class="form-input${f?" font-mono":""}" placeholder="${k}" data-model="${a}.value">
|
|
@@ -121,7 +121,7 @@ import{protectToolEditor as Ke,mountToolTrash as We,mountGroupBatch as _e}from".
|
|
|
121
121
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannotSave.value">
|
|
122
122
|
<span data-icon="save" data-icon-size="13"></span> Save</button>
|
|
123
123
|
</div>
|
|
124
|
-
</div>`),S=a=>a==null?"":String(a),n={name:M.observable(S(e?.name)),email:M.observable(S(e?.email)),phone:M.observable(S(e?.phone)),notes:M.observable(S(e?.notes)),favourite:M.observable(e?.favourite===!0),chosen:M.observable(Array.isArray(e?.groups)?[...e.groups]:[]),isEdit:!!e,meta:e?`Added ${e.createdAt?D(e.createdAt).format("D MMM YYYY"):"earlier"}${e.updatedAt?`, last changed ${D(e.updatedAt).format("D MMM YYYY HH:mm")}`:""}. Ctrl+Enter saves.`:"Ctrl+Enter saves."};for(const a of Object.keys(se))n[a]=M.observable(S(e?.[a]));const d=Object.fromEntries(Object.keys(T).map(a=>[a,M.observable(0)]));n.pick=M.observable([]);const L=M.effect(()=>{const a=n.chosen.value;n.pick.value=[...new Set([...y.value,...a])].map(c=>({name:c,on:String(a.includes(c)),cls:a.includes(c)?" is-active":""}))});n.noGroups=M.computed(()=>n.pick.value.length===0),n.cannotSave=M.computed(()=>!n.name.value.trim()),n.billingSummary=M.computed(()=>{const a=[];n.company.value.trim()&&a.push(n.company.value.trim()),n.vatNumber.value.trim()&&a.push("VAT"),(n.sortCode.value.trim()||n.iban.value.trim())&&a.push("bank details");const c=d.addresses.value;return c&&a.push(R(c,"address","addresses")),a.join(" \xB7 ")}),n.extraSummary=M.computed(()=>{const a=d.emails.value+d.phones.value;return a?R(a,"more"):""});function g(a,c){const B=T[a],f=l.querySelector(`[data-repeater="${a}"]`),k=f.querySelector(".ct-repeater-list");k.replaceChildren(),f.querySelector(".contacts-empty-row").style.display=c.length?"none":"",c.forEach((C,O)=>{const P=document.createElement("div");P.className="contacts-row";for(const w of B.columns){if(w.check){const ee=document.createElement("label");ee.className="form-check contacts-row-check";const G=document.createElement("input");G.type="checkbox",G.className="form-check-input",G.dataset.key=w.key,G.checked=C[w.key]===!0,G.addEventListener("change",()=>{G.checked&&k.querySelectorAll(`input[data-key="${w.key}"]`).forEach(ke=>{ke!==G&&(ke.checked=!1)})});const te=document.createElement("span");te.className="form-check-label",te.textContent=w.label,ee.append(G,te),P.appendChild(ee);continue}const
|
|
124
|
+
</div>`),S=a=>a==null?"":String(a),n={name:M.observable(S(e?.name)),email:M.observable(S(e?.email)),phone:M.observable(S(e?.phone)),notes:M.observable(S(e?.notes)),favourite:M.observable(e?.favourite===!0),chosen:M.observable(Array.isArray(e?.groups)?[...e.groups]:[]),isEdit:!!e,meta:e?`Added ${e.createdAt?D(e.createdAt).format("D MMM YYYY"):"earlier"}${e.updatedAt?`, last changed ${D(e.updatedAt).format("D MMM YYYY HH:mm")}`:""}. Ctrl+Enter saves.`:"Ctrl+Enter saves."};for(const a of Object.keys(se))n[a]=M.observable(S(e?.[a]));const d=Object.fromEntries(Object.keys(T).map(a=>[a,M.observable(0)]));n.pick=M.observable([]);const L=M.effect(()=>{const a=n.chosen.value;n.pick.value=[...new Set([...y.value,...a])].map(c=>({name:c,on:String(a.includes(c)),cls:a.includes(c)?" is-active":""}))});n.noGroups=M.computed(()=>n.pick.value.length===0),n.cannotSave=M.computed(()=>!n.name.value.trim()),n.billingSummary=M.computed(()=>{const a=[];n.company.value.trim()&&a.push(n.company.value.trim()),n.vatNumber.value.trim()&&a.push("VAT"),(n.sortCode.value.trim()||n.iban.value.trim())&&a.push("bank details");const c=d.addresses.value;return c&&a.push(R(c,"address","addresses")),a.join(" \xB7 ")}),n.extraSummary=M.computed(()=>{const a=d.emails.value+d.phones.value;return a?R(a,"more"):""});function g(a,c){const B=T[a],f=l.querySelector(`[data-repeater="${a}"]`),k=f.querySelector(".ct-repeater-list");k.replaceChildren(),f.querySelector(".contacts-empty-row").style.display=c.length?"none":"",c.forEach((C,O)=>{const P=document.createElement("div");P.className="contacts-row";for(const w of B.columns){if(w.check){const ee=document.createElement("label");ee.className="form-check contacts-row-check";const G=document.createElement("input");G.type="checkbox",G.className="form-check-input",G.dataset.key=w.key,G.checked=C[w.key]===!0,G.addEventListener("change",()=>{G.checked&&k.querySelectorAll(`input[data-key="${w.key}"]`).forEach(ke=>{ke!==G&&(ke.checked=!1)})});const te=document.createElement("span");te.className="form-check-label",te.textContent=w.label,ee.append(G,te),P.appendChild(ee);continue}const K=document.createElement("input");K.type="text",K.className="form-input"+(w.wide?" contacts-row-wide":""),K.dataset.key=w.key,K.placeholder=w.placeholder??"",K.value=C[w.key]??"",P.appendChild(K)}const z=document.createElement("button");z.type="button",z.className="btn btn-sm btn-ghost contacts-row-remove",z.title="Remove",z.textContent="\xD7",z.addEventListener("click",()=>{const w=u(a,{keepBlank:!0});w.splice(O,1),g(a,w)}),P.appendChild(z),k.appendChild(P)}),d[a].value=c.length}function u(a,{keepBlank:c=!1}={}){const B=T[a],f=[...l.querySelectorAll(`[data-repeater="${a}"] .contacts-row`)].map(k=>{const C={};for(const O of B.columns){const P=k.querySelector(`[data-key="${O.key}"]`);C[O.key]=O.check?P?.checked===!0:(P?.value??"").trim()}return C});return c?f:f.filter(k=>Object.entries(k).some(([C,O])=>C!=="label"&&O!==""&&O!==!1))}l.addEventListener("click",a=>{const c=a.target.closest("[data-add]");if(c){const f=c.dataset.add;g(f,[...u(f,{keepBlank:!0}),{}]),l.querySelector(`[data-repeater="${f}"] .contacts-row:last-child input`)?.focus();return}const B=a.target.closest(".ct-pick [data-group]");if(B){const f=B.dataset.group,k=n.chosen.peek();n.chosen.value=k.includes(f)?k.filter(C=>C!==f):[...k,f]}});const $=()=>{const a={name:n.name.peek().trim(),email:n.email.peek().trim(),phone:n.phone.peek().trim(),notes:n.notes.peek().trim(),favourite:n.favourite.peek()===!0,groups:[...n.chosen.peek()]};for(const c of Object.keys(se))a[c]=String(n[c].peek()??"").trim();for(const c of Object.keys(T))a[c]=u(c);return a};let b=!1;const F=async()=>{if(!(b||n.cannotSave.peek())){b=!0;try{const a=$();for(const c of a.groups)y.peek().includes(c)||await V(c);if(e){if(!await J(e.id,a,"Contact saved."))return}else{const c=await v("/contacts/contacts","POST",a);m.value=[...m.peek(),c],E.toast("Contact added.",{type:"success"})}ge.saved(),o.close()}catch(a){E.toast(a?.message||"Failed to save the contact.",{type:"error"})}finally{b=!1}}};o.element.appendChild(l),e&&Qe(l.querySelector(".related-box"),"contact",e.id,{onOpen:()=>o.close(),name:e.name});const Ye=Z(l);for(const a of Object.keys(T))g(a,Array.isArray(e?.[a])?e[a]:[]);l.querySelector(".ct-pick").innerHTML=it;const We=M.applyBindings(n,l,{methods:{save:F,cancel:()=>o.close(),del:async()=>{e&&await de(e.id)&&(ge?.discard(),o.close())},async newGroup(){const a=String(await E.prompt("New group name:")??"").trim();!a||!await V(a)||n.chosen.peek().includes(a)||(n.chosen.value=[...n.chosen.peek(),a])}}});n.billingSummary.peek()&&(l.querySelector(".ct-section").open=!0),l.addEventListener("keydown",a=>{(a.ctrlKey||a.metaKey)&&a.key==="Enter"&&(a.preventDefault(),F())}),I.scan(l),ae(o,()=>{L?.(),We.dispose(),Ye()});const ye=["name","email","phone","notes","favourite","chosen",...Object.keys(se)],ge=ze({so:o,panel:l,tool:"contacts",id:e?.id,lifetime:h,read:()=>({fields:Object.fromEntries(ye.map(a=>[a,n[a].peek()])),lists:Object.fromEntries(Object.keys(T).map(a=>[a,u(a,{keepBlank:!0})]))}),write:a=>{for(const c of ye)c in(a.fields||{})&&(n[c].value=a.fields[c]);for(const c of Object.keys(T))g(c,Array.isArray(a.lists?.[c])?a.lists[c]:[])}});o.open(),setTimeout(()=>l.querySelector("#ct-f-name")?.focus(),60)}function Re(){const t=E.slideover({title:"Contacts settings",size:"sm",position:"right"}),e=(n,d,L)=>`
|
|
125
125
|
<div class="ct-setting-row">
|
|
126
126
|
<label class="form-label" for="ct-set-${n}" style="margin:0">${d}${L?` <span data-help="${L}"></span>`:""}</label>
|
|
127
127
|
<input id="ct-set-${n}" type="checkbox" class="form-switch" data-model="${n}.value">
|
|
@@ -169,9 +169,9 @@ import{protectToolEditor as Ke,mountToolTrash as We,mountGroupBatch as _e}from".
|
|
|
169
169
|
<p class="ct-panel-meta" style="margin:0">Renaming a group renames it on every contact in it - everyone's, since groups are shared; deleting one takes it off them.
|
|
170
170
|
<span data-help="Contacts themselves are never deleted from here."></span></p>
|
|
171
171
|
<p class="ct-panel-meta" style="margin:0" data-bind-hidden="!someLocked.value">Some groups cannot be renamed or deleted by you.
|
|
172
|
-
<span data-help-title="Why is Rename greyed out?" data-help="${
|
|
172
|
+
<span data-help-title="Why is Rename greyed out?" data-help="${pt}"></span></p>
|
|
173
173
|
<div class="ct-groups-list" data-if="hasGroups.value">
|
|
174
174
|
<div class="ct-groups-rows" data-each="list.value key=name"></div>
|
|
175
175
|
</div>
|
|
176
176
|
<p class="text-muted" data-if="noGroups.value">No groups yet.</p>
|
|
177
|
-
</div>`),o=M.observable(""),s=M.observable(null),r={draft:o,list:M.observable([]),someLocked:M.observable(!1)},l=async()=>{try{const g=await
|
|
177
|
+
</div>`),o=M.observable(""),s=M.observable(null),r={draft:o,list:M.observable([]),someLocked:M.observable(!1)},l=async()=>{try{const g=await v("/contacts/groups/access");s.value=new Map((g?.groups||[]).map(u=>[u.name,u.canManage===!0]))}catch{s.value=new Map}};l();const S=M.effect(()=>{const g=re.value.groups,u=s.value,$=y.value.map(b=>{const F=!(u&&u.get(b)===!0);return{name:b,countText:R(g[b]??0,"contact"),locked:F,lockTitle:F?"Only its creator, or someone allowed to manage every group, can change this group":""}});r.list.value=$,r.someLocked.value=u!==null&&$.some(b=>b.locked)});r.hasGroups=M.computed(()=>r.list.value.length>0),r.noGroups=M.computed(()=>r.list.value.length===0),r.cannotAdd=M.computed(()=>!o.value.trim());async function n(){const g=o.peek().trim();g&&await V(g)&&(await l(),o.value="",E.toast("Group added.",{type:"success"}))}e.addEventListener("click",async g=>{const u=g.target.closest("[data-act]"),$=u?.closest("[data-group]")?.dataset.group;if(!(!u||!$||u.disabled)){if(u.dataset.act==="show")H.value={kind:"group",value:$},t.close();else if(u.dataset.act==="rename"){const b=String(await et("Rename the group to:",{},$)??"").trim();if(!b||b===$)return;try{await v(`/contacts/groups/${encodeURIComponent($)}`,"PUT",{newName:b}),await Y(),await l(),E.toast("Group renamed.",{type:"success"})}catch(F){E.toast(F?.message||"Failed to rename the group.",{type:"error"})}}else if(u.dataset.act==="delete"){if(!await E.confirm("Delete this group? Its contacts stay - they are just no longer in it."))return;try{await v(`/contacts/groups/${encodeURIComponent($)}`,"DELETE"),await Y(),await l(),E.toast("Group deleted.",{type:"success"})}catch(b){E.toast(b?.message||"Failed to delete the group.",{type:"error"})}}}}),t.element.appendChild(e);const d=Z(e);e.querySelector(".ct-groups-rows").innerHTML=dt;const L=M.applyBindings(r,e,{methods:{add:()=>(n(),!1)}});ae(t,()=>{S?.(),L.dispose(),d()}),t.open()}Ve(N,{id:"contacts-settings",icon:"settings",title:"Contacts settings",onClick:Re}),i.querySelector(".ct-rows").innerHTML=lt,i.querySelector(".ct-chips").innerHTML=rt;const U=i.querySelector(".ct-list"),be=i.querySelector(".ct-bar"),De=i.querySelector(".ct-chips-row"),fe=i.querySelector(".ct-bulk");h.add(Z(i,{position:"bottom"}));const qe=M.applyBindings(xe,i,{methods:{create:()=>W(null),retry:Y,bulkDelete:Pe,clearSelection:()=>{p.value=new Set}}});h.add(()=>qe.dispose()),Ke({root:i,toolbar:i.querySelector(".ct-bar"),tool:"contacts",endpoint:"/contacts",itemPath:"/contacts",label:"Contacts",titleOf:t=>t.name,reload:Y,lifetime:h});const he=_e({root:i,ensureGroup:V,byId:q,patch:J,withGroup:$e,lifetime:h});h.add(M.effect(()=>{const t=x.value;U.classList.toggle("is-compact",t.density==="compact");const e={company:!t.showCompany,email:!t.showEmail,phone:!t.showPhone,groups:!t.showGroups};for(const[s,r]of Object.entries(e))U.classList.toggle(`hide-${s}`,r);const o=["1.1rem","1.5rem","minmax(10rem, 1.4fr)"];e.company||o.push("minmax(0, 1fr)"),e.email||o.push("minmax(0, 1.2fr)"),e.phone||o.push("minmax(0, .9fr)"),e.groups||o.push("minmax(0, 1fr)"),o.push("1.75rem"),U.style.setProperty("--ct-cols",o.join(" "))}));const Ue=E.contextMenu(U,{cursor:"glyph",match:".ct-row",inherit:!1,density:"compact",items:t=>{const e=t.dataset.id,o=q(e);if(!o)return[];const s=Se(o),r=Me(o),l=o.groups||[],S=d=>d.filter(L=>L.visible!==!1),n=Xe(j,e,d=>d.label.replace("{name}",o.name||"them"));return S([{label:"Edit\u2026",icon:"edit",action:()=>W(e)},...n,{type:"divider"},{label:`Email ${s}`,icon:"mail",visible:!!s,action:()=>{location.href=`mailto:${encodeURIComponent(s).replace(/%40/g,"@")}`}},{label:`Call ${r}`,icon:"phone",visible:!!r,action:()=>{location.href=`tel:${r.replace(/[^\d+]/g,"")}`}},{label:"Copy email",icon:"copy",visible:!!s,action:()=>ue(s,"Email")},{label:"Copy phone",icon:"copy",visible:!!r,action:()=>ue(r,"Phone")},{type:"divider",visible:!!(s||r)},{label:o.favourite===!0?"Unstar":"Star",icon:"star",action:()=>J(e,{favourite:o.favourite!==!0})},{label:"Groups",icon:"folder",submenu:[...y.peek().map(d=>({label:`${l.includes(d)?"\u2713 ":""}${d}`,action:()=>Oe(e,d,!l.includes(d))})),...y.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>pe([e])}]},{label:"Duplicate",icon:"copy",action:()=>Be(e)},{label:p.peek().has(e)?"Deselect":"Select",icon:"check-square",action:()=>me(e)},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>de(e)}])}});h.add(()=>Ue?.destroy?.());const Fe=E.contextMenu(be,{cursor:"glyph",match:".ct-more",inherit:!1,density:"compact",items:()=>[{label:"Manage groups\u2026",icon:"folder",action:je},{type:"divider"},{label:"Export all (JSON)",icon:"download",action:Ie},{label:"Import from file\u2026",icon:"upload",action:()=>X.click()},{type:"divider"},{label:"Select all shown",icon:"check-square",disabled:!A.peek().length,action:()=>{p.value=new Set(A.peek().map(t=>t.id))}},{label:"Clear selection",icon:"x",disabled:!p.peek().size,action:()=>{p.value=new Set}}]});h.add(()=>Fe?.destroy?.());const He=E.contextMenu(fe,{cursor:"glyph",match:".ct-bulk-group",inherit:!1,density:"compact",items:()=>[...y.peek().map(t=>({label:t,action:()=>Ge(t)})),...y.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>pe([...p.peek()])}]});h.add(()=>He?.destroy?.()),be.addEventListener("click",t=>{const e=t.target.closest(".ct-more");e&&oe(e)}),fe.addEventListener("click",t=>{const e=t.target.closest(".ct-bulk-group");e&&oe(e)}),De.addEventListener("click",t=>{const e=t.target.closest(".ct-chip");if(!e)return;const o=e.dataset.chip;H.value=o==="fav"?{kind:"fav"}:o.startsWith("group:")?{kind:"group",value:o.slice(6)}:{kind:"all"}}),U.addEventListener("click",t=>{if(t.target.closest(".ct-select-all"))return;const e=t.target.closest(".ct-row");if(!e)return;const o=e.dataset.id;if(t.target.closest(".ct-kebab"))return oe(t.target.closest(".ct-kebab"));if(!t.target.closest(".ct-select")){if(t.target.closest(".ct-fav")){const s=q(o);s&&J(o,{favourite:s.favourite!==!0});return}W(o)}}),U.addEventListener("change",t=>{if(t.target.matches(".ct-select")&&me(t.target.closest(".ct-row").dataset.id),t.target.matches(".ct-select-all")){const e=A.peek().map(o=>o.id);p.value=t.target.checked?new Set(e):new Set}}),U.addEventListener("keydown",t=>{const e=t.target.closest?.(".ct-row");e&&t.key==="Enter"&&t.target===e&&W(e.dataset.id)});const ve=t=>{t.ctrlKey||t.metaKey||t.altKey||t.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(t.key==="/"?(t.preventDefault(),le.value=!0):(t.key==="n"||t.key==="N")&&(t.preventDefault(),W(null)))};document.addEventListener("keydown",ve),h.add(()=>document.removeEventListener("keydown",ve)),I.scan(i),await Y()}};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{apiRequest as m,getUser as Le}from"../api.js";import{addBannerAction as C}from"../lib/plugin-chrome.js";import{helpPopovers as re}from"../lib/help-popover.js";import{openMenuAt as le}from"../lib/tool-kit.js";import{SIZE_LABELS as
|
|
1
|
+
import{apiRequest as m,getUser as Le}from"../api.js";import{addBannerAction as C}from"../lib/plugin-chrome.js";import{helpPopovers as re}from"../lib/help-popover.js";import{openMenuAt as le,promptWith as De}from"../lib/tool-kit.js";import{SIZE_LABELS as Ce,SIZES as Oe,addWidget as Te,anyOpen as z,boardCards as Me,dropWidget as xe,galleryGroups as qe,removeWidget as ce,resizeWidget as Ne,sameLayout as pe,setAllCollapsed as Re,setCollapsed as Ie,shiftWidget as G,withToday as ze}from"../lib/dashboard-board-arrange.js";import{destroyWidget as Ge,drawError as We,drawWidget as Be}from"../lib/widget-render.js";import{MAX_BOARDS as be,OVERVIEW as W,boardFromHash as ue,boardHref as Fe,dashboardsLabel as _,stripTabs as Pe,tabsMarkup as Ye}from"../lib/dashboards-arrange.js";import{allowOverviewOnce as He,setDashboardsBanner as Ue}from"../lib/dashboards-strip.js";const Je=400,_e=h=>String(h||"").replace(/^\/api(?=\/)/,""),r=h=>String(h??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");export async function mountBoard(h,f,{isAdmin:j=!1}={}){const B=h.get(0),c=B.querySelector(".dbx")||B;c.classList.add("dbp"),c.replaceChildren(),c.insertAdjacentHTML("beforeend",`
|
|
2
2
|
<div class="dbp-tabs" role="tablist" aria-label="Dashboards" hidden></div>
|
|
3
3
|
<p class="dbp-greeting" hidden></p>
|
|
4
4
|
<div class="dbp-head">
|
|
@@ -10,7 +10,7 @@ import{apiRequest as m,getUser as Le}from"../api.js";import{addBannerAction as C
|
|
|
10
10
|
<div class="dbp-empty-board" hidden>
|
|
11
11
|
<p>Nothing on your Dashboard yet.</p>
|
|
12
12
|
<button type="button" class="btn btn-primary dbp-add-first"><span data-icon="plus" data-icon-size="14"></span> Add a widget</button>
|
|
13
|
-
</div>`);const b=c.querySelector(".dbp-board"),he=c.querySelector(".dbp-source"),
|
|
13
|
+
</div>`);const b=c.querySelector(".dbp-board"),he=c.querySelector(".dbp-source"),K=c.querySelector(".dbp-greeting"),fe=c.querySelector(".dbp-hint"),$=c.querySelector(".dbp-tabs"),V=()=>c.isConnected;let y=[],i={version:1,widgets:[]},o={version:2,active:"main",boards:[{id:"main",name:"Main",widgets:[]}]},q="",O="built-in",d={mine:{name:"",greeting:!0,columns:4,density:"normal",updates:"live",home:!1},site:{name:"Dashboards",locked:!1},name:"Dashboards",isAdmin:j,locked:!1};const T=new Map;f.add(()=>{for(const e of T.values())clearInterval(e);T.clear()});async function me(){const[e,t,a]=await Promise.all([m("/widgets",{method:"GET"}).catch(()=>[]),m("/dashboard/boards",{method:"GET"}).catch(()=>null),m("/dashboard/settings",{method:"GET"}).catch(()=>null)]);a&&(d={...d,...a}),y=Array.isArray(e)?e:[],t?.boards?.length&&(o={version:2,active:t.active,boards:t.boards});const s=ue(location.hash).board;s&&o.boards.some(n=>n.id===s)&&(o.active=s),i={version:1,widgets:w().widgets},q=JSON.stringify(o),O=t?.source||"built-in"}const w=()=>o.boards.find(e=>e.id===o.active)||o.boards[0],N=()=>{w().widgets=i.widgets};let F=null;function A(){clearTimeout(F),!d.locked&&(F=setTimeout(async()=>{N();const e=JSON.stringify(o);if(e!==q)try{const t=await m("/dashboard/boards",{method:"PUT",body:e});q=e,O=t?.source||"mine",P()}catch(t){E.toast(`Your layout was not saved: ${t.message||t}`,{type:"error"})}},Je))}f.add(()=>clearTimeout(F));function P(){he.textContent={mine:"Your layout",site:"The site's default layout","built-in":"The starting layout",locked:"Set by your administrator"}[O]||""}function g(e){pe(e,i)||(i=e,S(),A())}function Y(e,t){i=Ie(i,e,t);const a=b.querySelector(`.dbp-card[data-id="${CSS.escape(e)}"]`);a&&X(a,t),A()}function X(e,t){const a=e.classList.contains("is-collapsed");e.classList.toggle("is-collapsed",t);const s=e.querySelector(".dbp-body");s&&(s.hidden=t);const n=e.querySelector(".dbp-fold"),l=e.getAttribute("aria-label")||"";n&&(n.setAttribute("aria-expanded",t?"false":"true"),n.title=t?"Open":"Fold",n.setAttribute("aria-label",`${t?"Open":"Fold"} ${l}`)),a&&!t&&v(e)}function Z(e){i=Re(i,e),b.querySelectorAll(".dbp-card").forEach(t=>X(t,e)),A()}const ge=4,ye=16;function Q(e){const t=e.getBoundingClientRect().height;t&&(e.style.gridRowEnd=`span ${Math.max(1,Math.ceil((t+ye)/ge))}`)}const H=typeof ResizeObserver=="function"?new ResizeObserver(e=>{for(const t of e)Q(t.target)}):null;f.add(()=>H?.disconnect());async function v(e){const t=y.find(s=>s.id===e.dataset.id),a=e.querySelector(".dbp-body");if(!(!t||!a)){e.classList.add("is-loading");try{const s=t.kind==="count"?ze(t.dataUrl):t.dataUrl,n=await m(_e(s),{method:"GET"});if(!V()||!e.isConnected)return;await Be(a,t.kind,n)}catch(s){e.isConnected&&We(a,s.status===403?"You cannot see this any more.":"Could not load.")}finally{e.classList.remove("is-loading")}}}function ve(e){const t=e.href?`<a class="dbp-title" href="${r(e.href)}" title="Open ${r(e.title)}">${r(e.title)}</a>`:`<span class="dbp-title">${r(e.title)}</span>`;return`<section class="dbp-card dbp-size-${r(e.size)}${e.collapsed?" is-collapsed":""}" data-id="${r(e.id)}" role="listitem" tabindex="0" aria-label="${r(e.title)}">
|
|
14
14
|
<header class="dbp-card-head">
|
|
15
15
|
<span class="dbp-grip" title="Drag to move" aria-hidden="true"><span data-icon="menu" data-icon-size="14"></span></span>
|
|
16
16
|
<span class="dbp-icon" aria-hidden="true"><span data-icon="${r(e.icon||"grid")}" data-icon-size="16"></span></span>
|
|
@@ -20,16 +20,16 @@ import{apiRequest as m,getUser as Le}from"../api.js";import{addBannerAction as C
|
|
|
20
20
|
<button type="button" class="dbp-kebab" aria-label="Actions for ${r(e.title)}" title="Actions">\u22EE</button>
|
|
21
21
|
</header>
|
|
22
22
|
<div class="dbp-body"${e.collapsed?" hidden":""}><div class="dbp-skeleton"></div></div>
|
|
23
|
-
</section>`}function k(){$.hidden=!1;const e=d.locked===!0,t=Pe(o,e?"main":o.active,{locked:e,lockedName:_(d.name)});$.innerHTML=
|
|
23
|
+
</section>`}function k(){$.hidden=!1;const e=d.locked===!0,t=Pe(o,e?"main":o.active,{locked:e,lockedName:_(d.name)});$.innerHTML=Ye(t,{canAdd:!e&&o.boards.length<be}),I.scan($);const a=_(d.name),s=t.find(n=>n.active)?.name||w().name;Ue(B,a,s),document.title=`${s} - ${a} - Domma CMS`}function M(){const e=Fe(o.active);if(location.hash!==e)try{history.replaceState(null,"",e)}catch{}}function U(e){if(e===W){He(),location.hash="#/";return}e===o.active||!o.boards.some(t=>t.id===e)||(N(),o.active=e,i={version:1,widgets:w().widgets},k(),M(),S(),A())}const ee=()=>`b-${Date.now().toString(36)}${Math.random().toString(36).slice(2,5)}`;async function te(e,t=""){const a=await De(e,{inputPlaceholder:"Sales, Content, Support\u2026"},t);return a==null?null:String(a).replace(/\s+/g," ").trim().slice(0,40)||null}async function J(){if(o.boards.length>=12)return;const e=await te("Name the new board");if(!e)return;N();const t={id:ee(),name:e,widgets:[]};o.boards.push(t),o.active=t.id,i={version:1,widgets:[]},k(),M(),S(),A(),x()}async function L(e,t){const a=o.boards.findIndex(n=>n.id===e);if(a<0)return;N();const s=o.boards[a];if(t==="rename"){const n=await te("Rename the board",s.name);if(!n)return;s.name=n}else if(t==="duplicate"){if(o.boards.length>=12)return;const n={id:ee(),name:`${s.name} copy`.slice(0,40),widgets:s.widgets.map(l=>({...l}))};o.boards.splice(a+1,0,n),o.active=n.id,M()}else if(t==="delete"){if(o.boards.length<2||!await E.confirm(`Delete the board "${r(s.name)}"? Its widgets go with it (they stay available to add again).`))return;o.boards.splice(a,1),o.active===e&&(o.active=o.boards[Math.max(0,a-1)].id),M()}else t==="left"&&a>0?[o.boards[a-1],o.boards[a]]=[o.boards[a],o.boards[a-1]]:t==="right"&&a<o.boards.length-1&&([o.boards[a+1],o.boards[a]]=[o.boards[a],o.boards[a+1]]);i={version:1,widgets:w().widgets},k(),S(),A()}$.addEventListener("click",e=>{if(e.target.closest("[data-new-board]")){J();return}const t=e.target.closest("[data-board]");t&&U(t.dataset.board)}),$.addEventListener("dblclick",e=>{const t=e.target.closest("[data-board]");t&&t.dataset.board!==W&&!d.locked&&L(t.dataset.board,"rename")});const we=E.contextMenu($,{cursor:"glyph",match:"[data-board]",inherit:!1,density:"compact",items:e=>{const t=e.dataset.board,a=o.boards.findIndex(s=>s.id===t);return t===W||d.locked?[{header:e.textContent.trim()||"Overview"},{label:"Open",icon:"arrow-right",disabled:t!==W,action:()=>U(t)}]:[{header:o.boards[a]?.name||"Board"},{label:"Open",icon:"arrow-right",disabled:t===o.active,action:()=>U(t)},{label:"Rename\u2026",icon:"edit",action:()=>L(t,"rename")},{label:"Duplicate",icon:"copy",disabled:o.boards.length>=12,action:()=>L(t,"duplicate")},{type:"divider"},{label:"Move left",icon:"arrow-left",disabled:a<=0,action:()=>L(t,"left")},{label:"Move right",icon:"arrow-right",disabled:a>=o.boards.length-1,action:()=>L(t,"right")},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,disabled:o.boards.length<2,action:()=>L(t,"delete")}]}});f.add(()=>we?.destroy?.());function S(){for(const t of T.values())clearInterval(t);T.clear(),b.querySelectorAll(".dbp-body").forEach(Ge);const e=Me(i,y);b.innerHTML=e.map(ve).join(""),c.querySelector(".dbp-empty-board").hidden=e.length>0,H?.disconnect();for(const t of b.querySelectorAll(".dbp-card"))Q(t),H?.observe(t);I.scan(b);for(const t of b.querySelectorAll(".dbp-card")){t.classList.contains("is-collapsed")||v(t);const a=y.find(n=>n.id===t.dataset.id);if(d.mine.updates==="paused")continue;const s=(a?.refresh||60)*1e3*(d.mine.updates==="slower"?3:1);T.set(t.dataset.id,setInterval(()=>{!document.hidden&&t.isConnected&&!t.classList.contains("is-collapsed")&&v(t)},s))}P()}const ae=e=>{const t=e.detail?.url;if(t)for(const a of b.querySelectorAll(".dbp-card"))y.find(n=>n.id===a.dataset.id)?.href===t&&v(a)};window.addEventListener("dm:sidebar-count",ae),f.add(()=>window.removeEventListener("dm:sidebar-count",ae));function ke(e){const t=e.dataset.id,a=y.find(p=>p.id===t),s=i.widgets.find(p=>p.id===t);if(!a||!s)return[];const n=i.widgets.findIndex(p=>p.id===t),l=[{header:a.title},...a.href?[{label:`Open ${a.title}`,icon:"arrow-right",action:()=>{location.hash=a.href}}]:[],{label:s.collapsed?"Open":"Fold",icon:s.collapsed?"chevron-down":"chevron-up",action:()=>Y(t,!s.collapsed)},{label:"Refresh",icon:"refresh-cw",disabled:s.collapsed===!0,action:()=>v(e)}];return d.locked?l:[{header:a.title},{label:"Add widget\u2026",icon:"plus",action:()=>x()},{type:"divider"},...a.href?[{label:`Open ${a.title}`,icon:"arrow-right",action:()=>{location.hash=a.href}}]:[],{label:s.collapsed?"Open":"Fold",icon:s.collapsed?"chevron-down":"chevron-up",action:()=>Y(t,!s.collapsed)},{label:"Refresh",icon:"refresh-cw",disabled:s.collapsed===!0,action:()=>v(e)},{type:"divider"},...Oe.map(p=>({label:Ce[p],icon:s.size===p?"check":"maximize-2",disabled:s.size===p,action:()=>g(Ne(i,t,p))})),{type:"divider"},{label:"Move earlier",icon:"arrow-left",disabled:n<=0,action:()=>g(G(i,t,-1))},{label:"Move later",icon:"arrow-right",disabled:n>=i.widgets.length-1,action:()=>g(G(i,t,1))},{type:"divider"},{label:"Remove from the Dashboard",icon:"x",action:()=>g(ce(i,t))}]}const Ee=E.contextMenu(b,{cursor:"glyph",match:".dbp-card",inherit:!1,density:"compact",items:e=>ke(e)});f.add(()=>Ee?.destroy?.());const se=e=>{const t=e.target.closest(".dbp-kebab");if(t){le(t);return}const a=e.target.closest(".dbp-fold");if(a){const s=a.closest(".dbp-card");s&&Y(s.dataset.id,!s.classList.contains("is-collapsed"))}},oe=e=>{const t=e.target.closest(".dbp-card");if(t){if(e.key==="F10"&&e.shiftKey){e.preventDefault(),le(t);return}e.target===t&&(e.altKey&&(e.key==="ArrowLeft"||e.key==="ArrowUp")?(e.preventDefault(),g(G(i,t.dataset.id,-1)),ne(t.dataset.id)):e.altKey&&(e.key==="ArrowRight"||e.key==="ArrowDown")?(e.preventDefault(),g(G(i,t.dataset.id,1)),ne(t.dataset.id)):e.key==="Delete"&&(e.preventDefault(),g(ce(i,t.dataset.id))))}},ne=e=>requestAnimationFrame(()=>b.querySelector(`.dbp-card[data-id="${CSS.escape(e)}"]`)?.focus());if(b.addEventListener("click",se),b.addEventListener("keydown",oe),f.add(()=>{b.removeEventListener("click",se),b.removeEventListener("keydown",oe)}),typeof E.sortable=="function"){const e=E.sortable(b,{items:".dbp-card",handle:".dbp-grip",live:!1,axis:"x",onDrop({key:t,targetKey:a,zone:s}){if(d.locked||s!=="before"&&s!=="after")return!1;const n=xe(i,t,a,s);return pe(n,i)?!1:(g(n),!0)}});f.add(()=>e?.destroy?.())}function x(){const e=E.slideover({title:"Add a widget",size:"md",position:"right"}),t=document.createElement("div");t.className="dbp-gallery",t.innerHTML=`<input type="search" class="form-input dbp-gallery-search" placeholder="Search widgets" aria-label="Search widgets">
|
|
24
24
|
<p class="dbp-gallery-hint">Widgets from the Tools you can open. Each one is added at its usual size - resize it from its \u22EE menu.</p>
|
|
25
|
-
<div class="dbp-gallery-list"></div>`,e.element.appendChild(t);const a=t.querySelector(".dbp-gallery-list"),s=t.querySelector(".dbp-gallery-search");function n(){const l=
|
|
25
|
+
<div class="dbp-gallery-list"></div>`,e.element.appendChild(t);const a=t.querySelector(".dbp-gallery-list"),s=t.querySelector(".dbp-gallery-search");function n(){const l=qe(y,i,s.value);a.innerHTML=l.length?l.map(p=>`
|
|
26
26
|
<h4 class="dbp-gallery-group">${r(p.source)}</h4>
|
|
27
27
|
${p.widgets.map(u=>`
|
|
28
28
|
<div class="dbp-gallery-item">
|
|
29
29
|
<span class="dbp-icon"><span data-icon="${r(u.icon||"grid")}" data-icon-size="16"></span></span>
|
|
30
30
|
<span class="dbp-gallery-main"><strong>${r(u.title)}</strong><small>${r(u.description||"")}</small></span>
|
|
31
31
|
${u.added?'<span class="dbp-gallery-added">On the board</span>':`<button type="button" class="btn btn-sm btn-primary" data-add="${r(u.id)}">Add</button>`}
|
|
32
|
-
</div>`).join("")}`).join(""):'<p class="dbp-empty">No widgets match.</p>',I.scan(a)}a.addEventListener("click",l=>{const p=l.target.closest("[data-add]")?.dataset.add,u=p&&y.find(R=>R.id===p);u&&(g(
|
|
32
|
+
</div>`).join("")}`).join(""):'<p class="dbp-empty">No widgets match.</p>',I.scan(a)}a.addEventListener("click",l=>{const p=l.target.closest("[data-add]")?.dataset.add,u=p&&y.find(R=>R.id===p);u&&(g(Te(i,u)),E.toast(`${u.title} added.`,{type:"success",duration:2e3}),n())}),s.addEventListener("input",n),e.open(),n(),setTimeout(()=>s.focus(),50)}c.querySelector(".dbp-add-first").addEventListener("click",x);function Se(){const e=new Date().getHours(),t=e<12?"Good morning":e<18?"Good afternoon":"Good evening",a=String(Le()?.name||"").trim().split(/\s+/)[0],s=new Date().toLocaleDateString("en-GB",{weekday:"long",day:"numeric",month:"long"});return`${t}${a?`, ${a}`:""}. ${s}.`}function ie(){const e=d.mine;k(),K.hidden=!e.greeting,K.textContent=e.greeting?Se():"",c.classList.remove("dbp-cols-3","dbp-cols-4","dbp-cols-6"),c.classList.add(`dbp-cols-${e.columns||4}`),c.classList.toggle("is-compact",e.density==="compact"),c.classList.toggle("is-locked",d.locked===!0),fe.firstChild.textContent=d.locked?"Your administrator has set this layout. You can fold and refresh widgets; the cog has your own settings. ":"Your own boards. Right-click the board to add widgets; drag a card by its grip; the chevron folds it; a card's \u22EE resizes or removes it. Overview, the first tab, is the site's Dashboard. ",d.locked&&(O="locked"),P()}const D=(e,t,a)=>`<option value="${r(e)}"${String(a)===String(e)?" selected":""}>${r(t)}</option>`;function de(){const e=d.mine,t=E.slideover({title:"Dashboard settings",size:"md",position:"right"}),a=document.createElement("form");a.className="dbp-settings",a.noValidate=!0,a.innerHTML=`
|
|
33
33
|
<h4 class="dbp-settings-section">Yours</h4>
|
|
34
34
|
<label class="dbp-field"><span class="form-label">Name
|
|
35
35
|
<span data-help="What your Dashboards are called - in the banner and the sidebar entry. Leave it empty to use the site's name. Each board's own name is its tab: right-click a tab to rename it."></span></span>
|
|
@@ -59,4 +59,4 @@ import{apiRequest as m,getUser as Le}from"../api.js";import{addBannerAction as C
|
|
|
59
59
|
<div class="dbp-settings-actions">
|
|
60
60
|
<button type="button" class="btn btn-ghost" data-act="cancel">Cancel</button>
|
|
61
61
|
<button type="submit" class="btn btn-primary"><span data-icon="check" data-icon-size="14"></span> Save</button>
|
|
62
|
-
</div>`,t.element.appendChild(a);const s=re(a,{position:"left"});I.scan(a),a.querySelector('[data-act="cancel"]').addEventListener("click",()=>t.close()),a.addEventListener("submit",async n=>{n.preventDefault();const l=a.elements,p={name:l.name.value.trim(),greeting:l.greeting.checked,columns:Number(l.columns.value),density:l.density.value,updates:l.updates.value,home:l.home.checked};try{const u=await m("/dashboard/settings",{method:"PUT",body:JSON.stringify(p)});if(d={...d,mine:u.mine,site:u.site,name:u.name},d.isAdmin&&l.siteName){const R=await m("/dashboard/settings/site",{method:"PUT",body:JSON.stringify({name:l.siteName.value.trim(),locked:l.locked.checked})});d.site=R.site,d.name=u.mine.name||R.site.name}ie(),k(),S();try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}E.toast("Dashboard settings saved.",{type:"success"}),s?.(),t.close()}catch(u){E.toast(`Not saved: ${u.message||u}`,{type:"error"})}}),t.open()}if(await me(),!
|
|
62
|
+
</div>`,t.element.appendChild(a);const s=re(a,{position:"left"});I.scan(a),a.querySelector('[data-act="cancel"]').addEventListener("click",()=>t.close()),a.addEventListener("submit",async n=>{n.preventDefault();const l=a.elements,p={name:l.name.value.trim(),greeting:l.greeting.checked,columns:Number(l.columns.value),density:l.density.value,updates:l.updates.value,home:l.home.checked};try{const u=await m("/dashboard/settings",{method:"PUT",body:JSON.stringify(p)});if(d={...d,mine:u.mine,site:u.site,name:u.name},d.isAdmin&&l.siteName){const R=await m("/dashboard/settings/site",{method:"PUT",body:JSON.stringify({name:l.siteName.value.trim(),locked:l.locked.checked})});d.site=R.site,d.name=u.mine.name||R.site.name}ie(),k(),S();try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}E.toast("Dashboard settings saved.",{type:"success"}),s?.(),t.close()}catch(u){E.toast(`Not saved: ${u.message||u}`,{type:"error"})}}),t.open()}if(await me(),!V())return;d.locked||C(h,{id:"dashboard-add-widget",icon:"plus",title:"Add widget",onClick:x}),C(h,{id:"dashboard-fold-all",icon:"chevrons-up",title:"Fold or open every widget",onClick:()=>Z(z(i))}),C(h,{id:"dashboard-refresh",icon:"refresh-cw",title:"Refresh every widget",onClick:()=>b.querySelectorAll(".dbp-card").forEach(v)}),d.locked||C(h,{id:"dashboard-reset",icon:"rotate-ccw",title:"Reset to the site's default layout",onClick:async()=>{if(await E.confirm("Go back to the site's default layout? All your boards are replaced by the one default board."))try{await m("/dashboard/layout",{method:"DELETE"});const e=await m("/dashboard/boards",{method:"GET"});o={version:2,active:e.active,boards:e.boards},i={version:1,widgets:w().widgets},q=JSON.stringify(o),O=e?.source||"built-in",k(),S(),E.toast("Back to the default layout.",{type:"success"})}catch(e){E.toast(`Could not reset: ${e.message||e}`,{type:"error"})}}}),j&&C(h,{id:"dashboard-make-default",icon:"users",title:"Make this the default for everyone",onClick:async()=>{if(await E.confirm("Make this layout the default? Everyone who has not arranged their own Dashboard sees it. (Widgets they may not use are left out for them.)"))try{await m("/dashboard/layout/default",{method:"PUT",body:JSON.stringify(i)}),E.toast("This is now the default layout.",{type:"success"})}catch(e){E.toast(`Could not set the default: ${e.message||e}`,{type:"error"})}}}),C(h,{id:"dashboard-settings",icon:"settings",title:"Dashboard settings",onClick:de});const $e=E.contextMenu(c,{cursor:!1,inherit:!1,density:"compact",items:()=>[{header:w().name},...d.locked?[]:[{label:"Add widget\u2026",icon:"plus",action:()=>x()},{label:"New board\u2026",icon:"layout",disabled:o.boards.length>=12,action:()=>J()},{type:"divider"}],{label:z(i)?"Fold every widget":"Open every widget",icon:z(i)?"chevrons-up":"chevrons-down",action:()=>Z(z(i))},{label:"Refresh every widget",icon:"refresh-cw",action:()=>b.querySelectorAll(".dbp-card").forEach(v)},{type:"divider"},{label:"Dashboard settings\u2026",icon:"settings",action:()=>de()}]});f.add(()=>$e?.destroy?.()),f.add(re(c,{position:"bottom"})),I.scan(c),ie(),k(),S();const Ae=ue(location.hash).isNew&&!d.locked&&o.boards.length<be;M(),Ae&&J()}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{apiRequest as m}from"../api.js";import{addBannerAction as
|
|
1
|
+
import{apiRequest as m}from"../api.js";import{addBannerAction as H}from"../lib/plugin-chrome.js";import{helpPopovers as G}from"../lib/help-popover.js";import{openMenuAt as R,promptWith as J,sidebarCount as g,viewLifetime as Y}from"../lib/tool-kit.js";import{FILTERS as z,arrangeViews as Q,countViews as X,newViewHref as Z,projectOf as y,projectScope as ee,shortcodeFor as te,viewRow as se}from"../lib/views-arrange.js";const L=/^#\/(views|projects\/[^/]+\/views)\/?(\?.*)?$/,b="#/views",f="views.filter",oe=`
|
|
2
2
|
<div class="vwx-row" data-slug="{{slug}}" tabindex="0" data-bind-class="rowCls">
|
|
3
3
|
<span class="vwx-main"><strong>{{title}}</strong><small>{{meta}}</small>
|
|
4
4
|
<small class="is-bad" data-bind-hidden="!broken">{{brokenText}}</small></span>
|
|
@@ -8,4 +8,4 @@ import{apiRequest as m}from"../api.js";import{addBannerAction as W}from"../lib/p
|
|
|
8
8
|
<span class="vwx-chip" data-bind-class="accessCls" title="Who can see it">{{access}}</span>
|
|
9
9
|
</span>
|
|
10
10
|
<button type="button" class="vwx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
|
|
11
|
-
</div>`,
|
|
11
|
+
</div>`,A=c=>String(c??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),o=Y(L);export const viewsListView={templateUrl:"/admin/js/templates/views-list.html",async onMount(c){o.reset();const r=c.get(0).querySelector(".vwx")||c.get(0),u=()=>L.test(location.hash)&&r.isConnected,a=ee(location.hash),i=M.observable([]),p=M.observable(!0),w=M.observable("");let x="all";try{x=z.includes(S.get(f))?S.get(f):"all"}catch{}const s=M.observable(x),k=M.observable("");o.add(M.effect(()=>{const e=s.value;try{S.set(f,e)}catch{}}));const $=M.observable([]);o.add(M.effect(()=>{$.value=Q(i.value,{filter:s.value,query:k.value,project:a}).map(se)}));const d=M.computed(()=>X(i.value,{project:a}));o.add(M.effect(()=>{s.value==="broken"&&!d.value.broken&&!p.value&&(s.value="all")}));const D=M.computed(()=>{const e=i.value.length-d.value.all;return`Showing only views in ${a}.${e?` ${e} other${e===1?" is":"s are"} filed elsewhere.`:""}`}),N=M.computed(()=>d.value.all?"No views match.":"No views yet - New view in the banner makes one."),P=e=>i.peek().find(t=>t.slug===e);async function v(){try{const e=await m("/views?sources=1",{method:"GET"});if(!u())return;i.value=Array.isArray(e)?e:[],w.value=""}catch(e){w.value=`Could not load the views: ${e.message||e}`}finally{p.value=!1}}const h=e=>{location.hash=`#/views/edit/${encodeURIComponent(e)}`},U=e=>{location.hash=`#/views/${encodeURIComponent(e)}/preview`};async function q(e){const t=te(e.slug);try{await navigator.clipboard.writeText(t),E.toast(`Copied ${t}`,{type:"success"})}catch{E.toast(`Copy this: ${t}`,{type:"info",duration:8e3})}}async function O(e){const t=await J("Title of the copy",{},`${e.title||e.slug} copy`);if(!(!t||!String(t).trim()||!u()))try{const{id:l,slug:ae,meta:j,sourceInfo:ie,...K}=e,W=await m("/views",{method:"POST",body:JSON.stringify({...K,title:String(t).trim(),...j?.project!=null&&{meta:{project:j.project}}})});E.toast(`Made "${W.title}".`,{type:"success"}),g(b),await v()}catch(l){E.toast(`Not duplicated: ${l.message||l}`,{type:"error"})}}async function _(e){if(await E.confirm(`Delete the view "${A(e.title||e.slug)}"? Pages that show it with [view slug="${A(e.slug)}"] will show nothing. This cannot be undone.`))try{await m(`/views/${encodeURIComponent(e.slug)}`,{method:"DELETE"}),E.toast(`Deleted "${e.title||e.slug}".`,{type:"success"}),g(b),await v()}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}function B(e){const t=P(e);return t?[{header:t.title||t.slug},{label:"Edit",icon:"edit",action:()=>h(t.slug)},{label:"Preview results",icon:"eye",action:()=>U(t.slug)},{label:"Copy shortcode",icon:"copy",action:()=>q(t)},...y(t)!=="core"&&!a?[{label:`Open project "${y(t)}"`,icon:"folder",action:()=>{location.hash=`#/projects/${encodeURIComponent(y(t))}/views`}}]:[],{type:"divider"},{label:"Duplicate\u2026",icon:"layers",action:()=>O(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>_(t)}]:[]}const n=r.querySelector(".vwx-list");n.innerHTML=oe;const F=M.applyBindings({views:i,loading:p,loadError:w,filter:s,query:k,rows:$,counts:d,emptyText:N,scopeText:D,project:a},r,{methods:{showAll:()=>{s.value="all"},showPublic:()=>{s.value="public"},showRestricted:()=>{s.value="restricted"},showBroken:()=>{s.value="broken"}}});o.add(()=>F.dispose()),o.add(G(r,{position:"bottom"}));const V=E.contextMenu(n,{cursor:"glyph",match:".vwx-row",inherit:!1,density:"compact",items:e=>B(e.dataset.slug)});o.add(()=>V?.destroy?.());const T=e=>{const t=e.target.closest(".vwx-row");if(!t)return;const l=e.target.closest(".vwx-kebab");if(l){R(l);return}h(t.dataset.slug)},C=e=>{const t=e.target.closest(".vwx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),R(t)):e.key==="Enter"&&e.target===t&&h(t.dataset.slug))};n.addEventListener("click",T),n.addEventListener("keydown",C),o.add(()=>{n.removeEventListener("click",T),n.removeEventListener("keydown",C)}),H(c,{id:"views-new",icon:"plus",title:"New view",onClick:()=>{location.hash=Z(a)}}),I.scan(r),await v(),u()&&g(b)}};
|