domma-cms 0.92.1 → 0.94.0
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.
- package/CLAUDE.md +5 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/action-editor-arrange.js +1 -1
- package/admin/js/lib/api-tokens-arrange.js +2 -2
- package/admin/js/lib/block-editor-arrange.js +1 -1
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/components-arrange.js +1 -1
- package/admin/js/lib/dashboard-arrange.js +1 -1
- package/admin/js/lib/dates.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/notifications-arrange.js +1 -1
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/lib/related.js +1 -1
- package/admin/js/lib/timeline-builder.js +2 -2
- package/admin/js/templates/action-editor.html +6 -5
- package/admin/js/templates/actions-list.html +1 -1
- package/admin/js/templates/contacts.html +1 -1
- package/admin/js/templates/docs/api-actions.html +86 -60
- package/admin/js/templates/docs/api-authentication.html +159 -123
- package/admin/js/templates/docs/api-builder.html +197 -0
- package/admin/js/templates/docs/api-collections.html +199 -259
- package/admin/js/templates/docs/api-external.html +225 -0
- package/admin/js/templates/docs/api-forms.html +268 -0
- package/admin/js/templates/docs/api-layouts.html +70 -45
- package/admin/js/templates/docs/api-media.html +57 -80
- package/admin/js/templates/docs/api-navigation.html +66 -22
- package/admin/js/templates/docs/api-pages.html +109 -129
- package/admin/js/templates/docs/api-plugins.html +123 -61
- package/admin/js/templates/docs/api-scaffold.html +185 -0
- package/admin/js/templates/docs/api-settings.html +72 -64
- package/admin/js/templates/docs/api-users.html +74 -107
- package/admin/js/templates/docs/api-views.html +68 -54
- package/admin/js/templates/docs/components-howto.html +20 -17
- package/admin/js/templates/docs/components-reference.html +13 -16
- package/admin/js/templates/docs/components-rules.html +7 -6
- package/admin/js/templates/docs/components-walkthrough.html +19 -19
- package/admin/js/templates/docs/tutorial-crud.html +71 -40
- package/admin/js/templates/docs/tutorial-forms.html +51 -35
- package/admin/js/templates/docs/tutorial-plugin.html +132 -56
- package/admin/js/templates/docs/usage-actions.html +61 -15
- package/admin/js/templates/docs/usage-collections.html +108 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
- package/admin/js/templates/docs/usage-dconfig.html +0 -3
- package/admin/js/templates/docs/usage-editions.html +213 -0
- package/admin/js/templates/docs/usage-media.html +22 -6
- package/admin/js/templates/docs/usage-navigation.html +74 -18
- package/admin/js/templates/docs/usage-pages.html +60 -20
- package/admin/js/templates/docs/usage-plugins.html +89 -17
- package/admin/js/templates/docs/usage-shortcodes.html +123 -70
- package/admin/js/templates/docs/usage-site-settings.html +50 -18
- package/admin/js/templates/docs/usage-tools.html +73 -0
- package/admin/js/templates/docs/usage-users-roles.html +99 -20
- package/admin/js/templates/docs/usage-views.html +36 -19
- package/admin/js/templates/documentation.html +153 -32
- package/admin/js/templates/page-editor.html +0 -5
- package/admin/js/templates/plugin-guide.html +15 -0
- package/admin/js/templates/plugin-guides.html +21 -0
- package/admin/js/templates/pro-docs.html +53 -234
- package/admin/js/templates/tutorials.html +5 -4
- package/admin/js/views/actions-list.js +3 -3
- package/admin/js/views/analytics.js +5 -5
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/component-editor.js +2 -2
- package/admin/js/views/contacts.js +22 -20
- package/admin/js/views/context-menu-editor.js +5 -5
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/form-editor.js +4 -4
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/media.js +3 -3
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/menu-locations.js +2 -2
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/page-editor.js +8 -8
- package/admin/js/views/plugin-guides.js +5 -0
- package/admin/js/views/project-detail.js +2 -2
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +4 -4
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +17 -17
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +3 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/bin/cli.js +13 -13
- package/bin/lib/node-version.js +29 -0
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +3 -2
- package/plugins/_lib/admin/mail/reader-view.js +7 -6
- package/plugins/_lib/admin/mail/scheduling.js +4 -2
- package/plugins/_lib/admin/mail/templates.js +4 -4
- package/plugins/_lib/admin/ui/dates.js +85 -0
- package/plugins/blog/CLAUDE.md +31 -22
- package/plugins/blog/admin/views/blog.js +3 -2
- package/plugins/blog/admin/views/comments.js +2 -1
- package/plugins/blog/admin/views/post-editor.js +4 -4
- package/plugins/blog/blocks/blog-card-row.html +1 -1
- package/plugins/blog/blocks/blog-card.html +2 -2
- package/plugins/blog/blocks/blog-post-classic.html +2 -2
- package/plugins/blog/blocks/blog-post-essay.html +2 -2
- package/plugins/blog/blocks/blog-post-feature.html +2 -2
- package/plugins/blog/blocks/blog-post-minimal.html +2 -2
- package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
- package/plugins/blog/blocks/blog-post-split.html +2 -2
- package/plugins/blog/docs/guide.md +205 -0
- package/plugins/blog/lib/layouts.js +3 -3
- package/plugins/blog/lib/page.js +2 -1
- package/plugins/blog/plugin.js +3 -3
- package/plugins/blog/plugin.json +4 -4
- package/plugins/blog/tests/layouts.test.js +6 -0
- package/plugins/feedback/CLAUDE.md +22 -3
- package/plugins/feedback/admin/lib/kit.js +6 -7
- package/plugins/feedback/admin/views/feedback.js +79 -10
- package/plugins/feedback/admin/views/send.js +28 -6
- package/plugins/feedback/docs/guide.md +95 -0
- package/plugins/feedback/lib/receiver.js +9 -2
- package/plugins/feedback/lib/sender.js +3 -2
- package/plugins/feedback/plugin.js +54 -6
- package/plugins/feedback/plugin.json +4 -4
- package/plugins/feedback/tests/api.test.js +74 -2
- package/plugins/free-tier.lock.json +49 -44
- package/plugins/mail-reader/CLAUDE.md +33 -18
- package/plugins/mail-reader/docs/guide.md +147 -0
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/security/CLAUDE.md +4 -1
- package/plugins/security/admin/views/security.js +5 -5
- package/plugins/security/docs/guide.md +170 -0
- package/plugins/security/plugin.js +2 -1
- package/plugins/security/plugin.json +2 -1
- package/plugins/shopping-cart/CLAUDE.md +7 -1
- package/plugins/shopping-cart/admin/lib/kit.js +5 -2
- package/plugins/shopping-cart/admin/views/orders.js +4 -4
- package/plugins/shopping-cart/admin/views/overview.js +2 -2
- package/plugins/shopping-cart/docs/guide.md +191 -0
- package/plugins/shopping-cart/lib/render.js +2 -1
- package/plugins/shopping-cart/plugin.json +3 -3
- package/public/js/collection-browser.js +2 -2
- package/public/js/site.js +1 -1
- package/scripts/gen-instance-secret.js +3 -1
- package/scripts/setup.js +3 -1
- package/server/middleware/auth.js +2 -1
- package/server/routes/api/actions.js +47 -27
- package/server/routes/api/blocks.js +2 -1
- package/server/routes/api/collections.js +16 -52
- package/server/routes/api/contacts.js +66 -3
- package/server/routes/api/documentation.js +42 -0
- package/server/routes/api/notifications.js +3 -2
- package/server/routes/api/users.js +10 -6
- package/server/server.js +16 -1
- package/server/services/actions.js +110 -34
- package/server/services/adapterRegistry.js +169 -16
- package/server/services/adapters/FileAdapter.js +25 -0
- package/server/services/adapters/MongoAdapter.js +23 -0
- package/server/services/collections.js +104 -1
- package/server/services/connectionManager.js +12 -0
- package/server/services/dates.js +81 -0
- package/server/services/docs.js +13 -2
- package/server/services/markdown.js +75 -26
- package/server/services/notification-sources.js +6 -5
- package/server/services/passwordReset.js +2 -1
- package/server/services/permissionRegistry.js +3 -2
- package/server/services/pluginGuides.js +255 -0
- package/server/services/pluginInstaller.js +54 -13
- package/server/services/plugins.js +29 -1
- package/server/services/presetCollections.js +31 -5
- package/server/services/renderer.js +2 -2
- package/server/services/sidebarBadges.js +3 -1
- package/server/services/tools.js +4 -2
- package/server/templates/page.html +2 -2
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import{api as g,apiRequest as
|
|
1
|
+
import{api as g,apiRequest as it}from"../api.js";import{addBannerAction as z}from"../lib/plugin-chrome.js";import{helpPopovers as Oe}from"../lib/help-popover.js";import{onceClosed as rt,openMenuAt as Re,sidebarCount as j,viewLifetime as dt}from"../lib/tool-kit.js";import{MAX_COLUMNS as ut,PAGE_SIZE as se,SHOW_STEP as X,arrangeEntries as pt,canClear as ft,cellText as Ue,countText as mt,draftable as ht,filterChoices as yt,filterText as vt,hasForm as bt,shownValue as he,snapshot as ze,typedValue as gt,valueText as je}from"../lib/collection-entries-arrange.js";import{fmtDate as Et,fmtDateTime as B}from"../lib/dates.js";const Be=/^#\/collections\/[^/?#]+\/entries\/?(\?.*)?$/,P=m=>`collection-entries.draft.${m}`,D=m=>String(m??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function oe(m){const h=document.createElement("template");return h.innerHTML=m.trim(),h.content.firstElementChild}const kt='<span class="cex-chip">{{text}}<button type="button" data-idx="{{idx}}" title="Remove this filter" aria-label="Remove filter {{text}}">\xD7</button></span>';function xt(m){return`
|
|
2
2
|
<tr class="cex-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
|
|
3
3
|
<td class="cex-check"><input type="checkbox" class="cex-sel" aria-label="Select this entry" data-bind-checked="sel"></td>
|
|
4
4
|
${Array.from({length:m},(v,a)=>`<td data-col="${a}" title="{{t${a}}}">{{c${a}}}</td>`).join("")}
|
|
5
5
|
<td class="cex-when" title="{{whenFull}}">{{when}}</td>
|
|
6
6
|
<td class="cex-tools"><button type="button" class="cex-run" data-bind-hidden="!runs" title="Run one of this collection's actions">\u26A1 {{runs}}</button><button type="button" class="cex-kebab" aria-label="Actions for this entry" title="Actions">\u22EE</button></td>
|
|
7
|
-
</tr>`}function
|
|
7
|
+
</tr>`}function wt(m,h){const v={};return m.forEach(a=>{const u={};a.fullWidth?u.span=h:a.span>1&&(u.span=a.span),a.placeholder&&(u.placeholder=a.placeholder),a.helper&&(u.hint=a.helper);const r=a.type==="checkbox"?"boolean":a.type==="date"?"string":a.type;v[a.name]={type:r,label:a.label,required:a.required,options:a.options,...Object.keys(u).length?{formConfig:u}:{}}}),v}function ce(m,h){const v={};for(const a of h){const u=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!u.length)continue;const r=u[0];r.type==="radio"?v[a.name]=u.find(p=>p.checked)?.value??"":r.type==="checkbox"&&u.length>1?v[a.name]=u.filter(p=>p.checked).map(p=>p.value):r.type==="checkbox"?v[a.name]=r.checked:r.tagName==="SELECT"&&r.multiple?v[a.name]=[...r.selectedOptions].map(p=>p.value):v[a.name]=gt(a,r.value)}return v}function Fe(m,h,v){for(const a of h){if(!(a.name in v))continue;const u=v[a.name],r=[...m.querySelectorAll(`[name="${CSS.escape(a.name)}"]`)];if(!r.length)continue;const p=r[0];p.type==="radio"?r.forEach(c=>{c.checked=c.value===String(u)}):p.type==="checkbox"&&r.length>1?r.forEach(c=>{c.checked=Array.isArray(u)&&u.includes(c.value)}):p.type==="checkbox"?p.checked=u===!0:p.tagName==="SELECT"&&p.multiple?[...p.options].forEach(c=>{c.selected=Array.isArray(u)&&u.includes(c.value)}):p.value=u??"",r.forEach(c=>c.dispatchEvent(new Event("input",{bubbles:!0})))}}const Pe=dt(Be);export const collectionEntriesView={templateUrl:"/admin/js/templates/collection-entries.html",async onMount(m){Pe.reset();const h=m.get(0).querySelector(".cex")||m.get(0),v=location.hash.match(/^#\/collections\/([^/?#]+)\/entries/),a=v?decodeURIComponent(v[1]):null,u=()=>Be.test(location.hash)&&h.isConnected&&location.hash.includes(`/collections/${encodeURIComponent(a)}/entries`),r=e=>(Pe.add(e),e);if(!a){E.toast("No collection selected.",{type:"error"});return}const p=M.observable(null),c=M.observable([]),x=M.observable(0),Y=M.observable(!0),_=M.observable(""),_e=M.computed(()=>p.value!==null),le=M.observable(""),w=M.observable([]),Z=M.observable({key:"created",dir:"desc"}),y=M.observable([]),q=M.observable(X),J=M.observable([]),Q=M.observable({}),A=M.observable(!1);let ee=1;const ie=()=>p.peek()?.fields||[],re=M.computed(()=>(p.value?.fields||[]).slice(0,ut)),V=e=>c.peek().find(t=>t.id===e),ye=e=>J.peek().filter(t=>{const n=Q.peek()[t.slug];return n==null||n.has(e)}),T=M.observable([]),ve=M.observable([]);r(M.effect(()=>{const e=pt(c.value,{filters:w.value,query:le.value,sort:Z.value});T.value=e})),r(M.effect(()=>{const e=re.value,t=new Set(y.value);Q.value,J.value,ve.value=T.value.slice(0,q.value).map(n=>{const s={id:n.id,sel:t.has(n.id),rowCls:t.has(n.id)?"is-selected":""};e.forEach((i,d)=>{const f=he(i,n.data?.[i.name]);s[`c${d}`]=Ue(f),s[`t${d}`]=je(f)});const o=n.meta?.createdAt;s.when=o?Et(o):"",s.whenFull=o?`Created ${B(o)}${n.meta?.updatedAt?` \xB7 changed ${B(n.meta.updatedAt)}`:""}`:"";const l=ye(n.id).length;return s.runs=l?String(l):"",s})}));const Je=M.computed(()=>w.value.map((e,t)=>({key:`${t}:${e.field}:${e.op}:${e.value}`,idx:t,text:vt(e)}))),Ve=M.computed(()=>y.value.length),He={schema:p,loaded:c,loading:Y,loadError:_,ready:_e,query:le,rows:ve,chips:Je,busy:A,selectedCount:Ve,countLine:M.computed(()=>mt(T.value.length,c.value.length,x.value)),selectedText:M.computed(()=>`${y.value.length} selected`),canClear:M.computed(()=>ft(a)),introText:M.computed(()=>{const e=p.value;return e?`${e.description?`${e.description} - `:""}${(e.fields||[]).length} field${(e.fields||[]).length===1?"":"s"}${e.storage?.adapter==="mongodb"?", on MongoDB":""}.`:""}),emptyText:M.computed(()=>c.value.length?"No entries match the search or filters.":"No entries yet - Add entry in the banner makes one, or Import brings many."),moreText:M.computed(()=>{const e=T.value.length-q.value;if(e>0)return`Show ${Math.min(X,e)} more (${e} not shown)`;const t=x.value-c.value.length;return t>0?`Load the next ${Math.min(se,t)} from the server`:""})};async function H({reset:e=!0}={}){Y.value=!0;try{const t=e?Math.max(se,c.peek().length):se;e&&(ee=1);const n=await g.collections.listEntries(a,{limit:t,page:ee});if(!u())return;const s=n?.entries||(Array.isArray(n)?n:[]);c.value=e?s:[...c.peek(),...s.filter(l=>!V(l.id))],x.value=Number(n?.total)||c.peek().length,e&&(ee=Math.max(1,Math.ceil(c.peek().length/se)));const o=new Set(c.peek().map(l=>l.id));y.value=y.peek().filter(l=>o.has(l)),_.value="",await We()}catch(t){_.value=`Could not load the entries: ${t.message||t}`}finally{Y.value=!1}}async function Ke(){ee+=1,await H({reset:!1})}async function Ge(){try{if((await it("/actions/_count",{method:"GET"}).catch(()=>null))?.available===!1)return;const t=await g.actions.forCollection(a);J.value=Array.isArray(t)?t:[]}catch{J.value=[]}}async function We(){const e=J.peek().filter(s=>s.access?.rowLevel);if(!e.length||!c.peek().length)return;const t=c.peek().map(s=>s.id),n={...Q.peek()};await Promise.all(e.map(async s=>{try{n[s.slug]=new Set((await g.actions.checkAccess(s.slug,t)).allowed)}catch{n[s.slug]=null}})),Q.value=n}const K=e=>{const t=ie()[0];return(t?Ue(he(t,e?.data?.[t.name]),40):"")||`Entry ${String(e?.id||"").slice(0,8)}`};async function be(e){if(await E.confirm(`Delete \u201C${D(K(e))}\u201D? This cannot be undone.`))try{await g.collections.deleteEntry(a,e.id),c.value=c.peek().filter(t=>t.id!==e.id),x.value=Math.max(0,x.peek()-1),y.value=y.peek().filter(t=>t!==e.id),j("#/collections"),E.toast("Entry deleted.",{type:"success"})}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function Xe(){const e=[...y.peek()];if(!e.length||!await E.confirm(`Delete ${e.length} entr${e.length===1?"y":"ies"}? This cannot be undone.`))return;A.value=!0;let t=0;const n=[];for(const s of e)try{await g.collections.deleteEntry(a,s),t++}catch(o){n.push(o.message||String(o))}A.value=!1,y.value=[],j("#/collections"),E.toast(n.length?`Deleted ${t}; ${n.length} refused (${n[0]}).`:`Deleted ${t} entr${t===1?"y":"ies"}.`,{type:n.length?"warning":"success",duration:n.length?7e3:4e3}),u()&&await H()}async function Ye(){const e=p.peek();if(await E.confirm(`Delete ALL ${x.peek()} entries in \u201C${D(e?.title||a)}\u201D? This cannot be undone.`)){A.value=!0;try{await g.collections.clearEntries(a),c.value=[],x.value=0,y.value=[],j("#/collections"),E.toast("All entries deleted.",{type:"success"})}catch(t){E.toast(`Not cleared: ${t.message||t}`,{type:"error"})}finally{A.value=!1}}}async function Ze(e){try{const t=await g.collections.createEntry(a,{...e.data||{}});c.value=[t,...c.peek()],x.value=x.peek()+1,j("#/collections"),E.toast("Duplicated - the copy is at the top.",{type:"success"})}catch(t){E.toast(`Not duplicated: ${t.message||t}`,{type:"error"})}}async function Qe(e,t){const n=e.trigger?.confirmMessage;if(!(n&&!await E.confirm(D(n)))){A.value=!0;try{const s=await g.actions.execute(e.slug,t.id);if(s?.success){const o=s.stepsCompleted??0;E.toast(`${e.trigger?.label||e.title||"Action"} done (${o} step${o===1?"":"s"}).`,{type:"success"}),u()&&await H()}else E.toast(s?.results?.find(o=>!o.success)?.error||"The action failed.",{type:"error"})}catch(s){E.toast(s.message||"The action failed.",{type:"error"})}finally{A.value=!1}}}async function ge(e){try{await navigator.clipboard.writeText(e),E.toast("Copied.",{type:"success",duration:1500})}catch{E.toast("Copy failed - your browser refused the clipboard.",{type:"error"})}}function de(e){const t=y.peek();y.value=t.includes(e)?t.filter(n=>n!==e):[...t,e]}function Ee(e,t,n){w.value=[...w.peek(),{field:e.name,label:e.label||e.name,op:t,value:n}],q.value=X}const ue=new Set;let N=null,ke=!1;function xe(e,t,n){const s=e.close.bind(e);let o=!1,l=!1;e.close=async i=>{if(!o&&i!==!0&&!ke&&t()){if(l)return;l=!0;const d=await E.confirm(n);if(l=!1,!d)return}return o=!0,s()}}function te(e,t=()=>{}){ue.add(e),rt(e,()=>{ue.delete(e),t()})}function O(e,t=null){const n=p.peek(),s=ie(),o=!!e,l=E.slideover({title:o?`Edit ${D(K(e))}`:"New entry",size:"md",position:"right"}),i=oe(`
|
|
8
8
|
<div class="cex-form">
|
|
9
9
|
<div class="cex-fields"></div>
|
|
10
10
|
<small class="cex-hint cex-when-line"></small>
|
|
@@ -15,7 +15,7 @@ import{api as g,apiRequest as lt}from"../api.js";import{addBannerAction as U}fro
|
|
|
15
15
|
<button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
|
|
16
16
|
<button type="button" class="btn btn-sm btn-primary" data-act="save"><span data-icon="check" data-icon-size="13"></span> <span class="cex-save-text"></span></button>
|
|
17
17
|
</div>
|
|
18
|
-
</div>`);
|
|
18
|
+
</div>`);i.querySelector(".cex-save-text").textContent=o?"Save":"Add entry";const d=i.querySelector(".cex-fields");F.create(wt(s,n?.columns||2),e?.data||{},{layout:n?.layout||"stacked",columns:n?.columns||2,showSubmitButton:!1}).renderTo(d),s.forEach(k=>{if(k.type!=="date"||!k.name)return;const U=d.querySelector(`[name="${CSS.escape(k.name)}"]`);U&&U.type!=="date"&&(U.type="date")}),e?.data&&Fe(d,s,e.data),o&&e.meta?.createdAt&&(i.querySelector(".cex-when-line").textContent=`Created ${B(e.meta.createdAt)}${e.meta.updatedAt?`, last changed ${B(e.meta.updatedAt)}`:""}${e.meta.source?` \xB7 via ${e.meta.source}`:""}.`),l.element.appendChild(i);const f=ze(ce(d,s));t&&Fe(d,s,t);const b=()=>ze(ce(d,s))!==f,lt=i.querySelector(".cex-dirty"),fe=()=>{lt.hidden=!b()};d.addEventListener("input",fe),d.addEventListener("change",fe),fe();let me=!1;async function Ne(){if(me)return;document.activeElement?.dispatchEvent?.(new Event("change",{bubbles:!0}));const k=ce(d,s);me=!0;const U=i.querySelector('[data-act="save"]');U.disabled=!0;try{if(o){const W=await g.collections.updateEntry(a,e.id,k);c.value=c.peek().map(De=>De.id===e.id?W:De),E.toast("Entry saved.",{type:"success"})}else{const W=await g.collections.createEntry(a,k);c.value=[W,...c.peek()],x.value=x.peek()+1,j("#/collections"),E.toast("Entry added.",{type:"success"})}try{S.remove?S.remove(P(a)):S.set(P(a),null)}catch{}l.close(!0)}catch(W){E.toast(W.message||"Not saved.",{type:"error"})}finally{me=!1,U.disabled=!1}}i.querySelector('[data-act="save"]').addEventListener("click",Ne),i.querySelector('[data-act="cancel"]').addEventListener("click",()=>l.close()),i.addEventListener("keydown",k=>{(k.ctrlKey||k.metaKey)&&(k.key==="Enter"||k.key==="s")&&(k.preventDefault(),Ne())});const Ie={id:e?.id??null,dirty:b,values:()=>ht(ce(d,s),s)};N=Ie,te(l,()=>{N===Ie&&(N=null)}),xe(l,b,"Discard the changes you have not saved?"),I.scan(i),l.open(),d.querySelector("input, select, textarea")?.focus()}function et(e){const t=E.slideover({title:D(K(e)),size:"md",position:"right"}),n=oe(`
|
|
19
19
|
<div class="cex-form">
|
|
20
20
|
<dl class="cex-detail"></dl>
|
|
21
21
|
<div class="cex-meta"></div>
|
|
@@ -24,10 +24,10 @@ import{api as g,apiRequest as lt}from"../api.js";import{addBannerAction as U}fro
|
|
|
24
24
|
<span class="cex-spacer"></span>
|
|
25
25
|
<button type="button" class="btn btn-sm btn-primary" data-act="edit"><span data-icon="edit" data-icon-size="13"></span> Edit</button>
|
|
26
26
|
</div>
|
|
27
|
-
</div>`),s=n.querySelector(".cex-detail");for(const
|
|
27
|
+
</div>`),s=n.querySelector(".cex-detail");for(const i of ie()){const d=je(he(i,e.data?.[i.name])),f=document.createElement("dt");f.textContent=i.label||i.name;const b=document.createElement("dd");b.textContent=d===""?"-":d,s.append(f,b)}const o=n.querySelector(".cex-meta"),l=i=>{const d=document.createElement("span");d.textContent=i,o.appendChild(d)};l(`Id: ${e.id}`),e.meta?.createdAt&&l(`Created: ${B(e.meta.createdAt)}`),e.meta?.updatedAt&&l(`Changed: ${B(e.meta.updatedAt)}`),e.meta?.source&&l(`Source: ${e.meta.source}`),n.querySelector('[data-act="copy"]').addEventListener("click",()=>ge(JSON.stringify(e,null,2))),n.querySelector('[data-act="edit"]').addEventListener("click",()=>{t.close(!0),O(e)}),t.element.appendChild(n),te(t),I.scan(n),t.open()}function tt(){const e=E.slideover({title:"Import entries",size:"md",position:"right"}),t=oe(`
|
|
28
28
|
<div class="cex-form">
|
|
29
29
|
<p class="cex-hint">A JSON array of entries, each with a "data" object of field values - what Export gives you.
|
|
30
|
-
<span data-help="
|
|
30
|
+
<span data-help="Each entry is added as new, with a new id and today's date (the Data Transfer plugin from the Marketplace keeps ids and dates). Entries are checked like any other save - one that fails, or points a reference at an entry that does not exist, is skipped and reported. Values are stored as given, including fields the collection does not have."></span></p>
|
|
31
31
|
<div class="cex-actions" style="border:0;padding:0">
|
|
32
32
|
<button type="button" class="btn btn-sm btn-ghost" data-act="file"><span data-icon="upload" data-icon-size="13"></span> Choose a file\u2026</button>
|
|
33
33
|
<input type="file" accept=".json,application/json" hidden>
|
|
@@ -40,11 +40,11 @@ import{api as g,apiRequest as lt}from"../api.js";import{addBannerAction as U}fro
|
|
|
40
40
|
<button type="button" class="btn btn-sm btn-ghost" data-act="cancel">Cancel</button>
|
|
41
41
|
<button type="button" class="btn btn-sm btn-primary" data-act="import"><span data-icon="upload" data-icon-size="13"></span> Import</button>
|
|
42
42
|
</div>
|
|
43
|
-
</div>`),n=t.querySelector("textarea"),s=t.querySelector(".cex-err"),o=t.querySelector(".cex-ok"),l=t.querySelector('input[type="file"]');t.querySelector('[data-act="file"]').addEventListener("click",()=>l.click()),l.addEventListener("change",async()=>{const d=l.files?.[0];l.value="",d&&(n.value=await d.text())}),t.querySelector('[data-act="cancel"]').addEventListener("click",()=>e.close());let
|
|
43
|
+
</div>`),n=t.querySelector("textarea"),s=t.querySelector(".cex-err"),o=t.querySelector(".cex-ok"),l=t.querySelector('input[type="file"]');t.querySelector('[data-act="file"]').addEventListener("click",()=>l.click()),l.addEventListener("change",async()=>{const d=l.files?.[0];l.value="",d&&(n.value=await d.text())}),t.querySelector('[data-act="cancel"]').addEventListener("click",()=>e.close());let i=!1;t.querySelector('[data-act="import"]').addEventListener("click",async()=>{s.hidden=!0,o.hidden=!0;let d;try{if(d=JSON.parse(n.value),!Array.isArray(d))throw new Error("it must be a JSON array")}catch(b){s.textContent=`Not valid: ${b.message}`,s.hidden=!1;return}const f=t.querySelector('[data-act="import"]');f.disabled=!0;try{const b=await g.collections.import(a,d);o.textContent=`Imported ${b.imported}; skipped ${b.skipped}.`,o.hidden=!1,i=!0,j("#/collections"),u()&&await H(),setTimeout(()=>e.close(!0),1200)}catch(b){s.textContent=b.message||"Import failed.",s.hidden=!1}finally{f.disabled=!1}}),e.element.appendChild(t),te(e),xe(e,()=>!i&&n.value.trim()!=="","Discard the entries you pasted?"),Oe(t),I.scan(t),e.open(),n.focus()}function nt(){const e=E.slideover({title:"Export entries",size:"sm",position:"right"}),t=oe(`
|
|
44
44
|
<div class="cex-form">
|
|
45
45
|
<p class="cex-hint">Every entry in the collection - not only the ones shown.</p>
|
|
46
46
|
<div class="cex-export">
|
|
47
47
|
<button type="button" class="btn btn-ghost" data-fmt="json"><span data-icon="download" data-icon-size="14"></span> Export as JSON</button>
|
|
48
48
|
<button type="button" class="btn btn-ghost" data-fmt="csv"><span data-icon="download" data-icon-size="14"></span> Export as CSV</button>
|
|
49
49
|
</div>
|
|
50
|
-
</div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const s=n.dataset.fmt;try{const o=await fetch(`/api/collections/${encodeURIComponent(a)}/export?format=${s}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!o.ok)throw new Error(`${o.status}`);const l=await o.blob(),
|
|
50
|
+
</div>`);t.querySelectorAll("[data-fmt]").forEach(n=>n.addEventListener("click",async()=>{const s=n.dataset.fmt;try{const o=await fetch(`/api/collections/${encodeURIComponent(a)}/export?format=${s}`,{headers:{Authorization:`Bearer ${S.get("auth_token")||""}`}});if(!o.ok)throw new Error(`${o.status}`);const l=await o.blob(),i=document.createElement("a");i.href=URL.createObjectURL(l),i.download=`${a}-entries.${s}`,document.body.appendChild(i),i.click(),i.remove(),setTimeout(()=>URL.revokeObjectURL(i.href),1e3),e.close(!0)}catch(o){E.toast(`Export failed (${o.message}).`,{type:"error"})}})),e.element.appendChild(t),te(e),I.scan(t),e.open()}function at(e,t){const n=V(e.dataset.id);if(!n)return[];const s=t?.event?.target?.closest?.("td[data-col]"),o=s?re.peek()[Number(s.dataset.col)]:null,l=ye(n.id),i=y.peek().includes(n.id),d=o?yt(o,n.data?.[o.name]):[];return[{header:K(n)},{label:"Edit\u2026",icon:"edit",action:()=>O(n)},{label:"See all of it\u2026",icon:"eye",action:()=>et(n)},{label:"Duplicate",icon:"copy",action:()=>Ze(n)},{label:"Copy id",icon:"hash",action:()=>ge(n.id)},{label:i?"Deselect":"Select",icon:"check",action:()=>de(n.id)},...l.length?[{type:"divider"},...l.map(f=>({label:`${f.trigger?.label||f.title||f.slug}\u2026`,icon:f.trigger?.icon||"zap",disabled:A.peek(),action:()=>Qe(f,n)}))]:[],...o?[{type:"divider"},{label:`Filter by ${o.label||o.name}`,icon:"filter",submenu:[...d.map(f=>({label:f.label,action:()=>Ee(o,f.op,f.value)})),{label:`${o.label||o.name} contains\u2026`,action:async()=>{const f=await E.prompt(`Show entries whose ${D(o.label||o.name)} contains`,{inputPlaceholder:"text"});f&&String(f).trim()&&Ee(o,"contains",String(f).trim())}}]}]:[],...w.peek().length?[{label:`Clear filters (${w.peek().length})`,icon:"x",action:()=>{w.value=[]}}]:[],{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>be(n)}]}const L=h.querySelector(".cex-rows"),G=h.querySelector(".cex-head"),pe=h.querySelector(".cex-chips-list");pe.innerHTML=kt;const st=E.loader(h,{type:"dots"});try{p.value=await g.collections.get(a)}catch(e){_.value=`Could not load the collection: ${e.message||e}`}finally{st.destroy()}if(!u())return;if(!p.peek()&&!_.peek()){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const we=re.peek();L.innerHTML=xt(we.length),G.replaceChildren();const ne=(e,t)=>{const n=document.createElement("th");return e&&(n.className=e),t&&n.append(t),G.appendChild(n),n},$=document.createElement("input");$.type="checkbox",$.title="Select every entry shown",$.setAttribute("aria-label","Select every entry shown"),ne("cex-check",$);const Se=(e,t)=>{const n=document.createElement("button");return n.type="button",n.dataset.sort=e,n.append(document.createTextNode(t),Object.assign(document.createElement("span"),{className:"cex-sort"})),n};we.forEach(e=>ne("",Se(e.name,e.label||e.name))),ne("cex-when",Se("created","Created")),ne("cex-tools"),r(M.effect(()=>{const e=Z.value;G.querySelectorAll("[data-sort]").forEach(t=>{t.querySelector(".cex-sort").textContent=t.dataset.sort===e.key?e.dir==="asc"?"\u25B2":"\u25BC":"",t.parentElement.setAttribute("aria-sort",t.dataset.sort===e.key?e.dir==="asc"?"ascending":"descending":"none")})})),r(M.effect(()=>{const e=T.value.map(n=>n.id),t=new Set(y.value);$.checked=e.length>0&&e.every(n=>t.has(n)),$.indeterminate=!$.checked&&e.some(n=>t.has(n))}));const Ce=e=>{const t=e.target.closest("[data-sort]");if(!t)return;const n=Z.peek();Z.value={key:t.dataset.sort,dir:n.key===t.dataset.sort&&n.dir==="asc"?"desc":"asc"}};G.addEventListener("click",Ce),$.addEventListener("change",()=>{const e=T.peek().map(t=>t.id);y.value=$.checked?[...new Set([...y.peek(),...e])]:y.peek().filter(t=>!e.includes(t))}),r(()=>G.removeEventListener("click",Ce));const ot=M.applyBindings(He,h,{methods:{clearSelection:()=>{y.value=[]},deleteSelected:()=>Xe(),clearAll:()=>Ye(),clearFilters:()=>{w.value=[]},showMore:()=>{T.peek().length>q.peek()?q.value=q.peek()+X:Ke()}}});r(()=>ot.dispose()),r(Oe(h,{position:"bottom"})),r(M.effect(()=>{le.value,w.value,q.value=X}));const $e=e=>{const t=e.target.closest("button[data-idx]");if(!t)return;const n=Number(t.dataset.idx);w.value=w.peek().filter((s,o)=>o!==n)};pe.addEventListener("click",$e),r(()=>pe.removeEventListener("click",$e));const ct=E.contextMenu(L,{cursor:"glyph",match:".cex-row",inherit:!1,density:"compact",items:(e,t)=>at(e,t)});r(()=>ct?.destroy?.());const Ae=e=>{const t=e.target.closest(".cex-row");if(!t)return;if(e.target.closest(".cex-kebab, .cex-run")){Re(e.target.closest(".cex-kebab, .cex-run"));return}if(e.target.closest(".cex-check"))return;const n=V(t.dataset.id);n&&O(n)},Me=e=>{if(!e.target.matches(".cex-sel"))return;const t=e.target.closest(".cex-row");t&&de(t.dataset.id)},Le=e=>{const t=e.target.closest(".cex-row");if(!t||e.target!==t)return;const n=V(t.dataset.id);n&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Re(t)):e.key==="Enter"?(e.preventDefault(),O(n)):e.key===" "?(e.preventDefault(),de(n.id)):e.key==="Delete"&&(e.preventDefault(),be(n)))};L.addEventListener("click",Ae),L.addEventListener("change",Me),L.addEventListener("keydown",Le),r(()=>{L.removeEventListener("click",Ae),L.removeEventListener("change",Me),L.removeEventListener("keydown",Le)});const qe=p.peek(),ae=m.get(0).querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(ae&&qe?.title){for(;ae.nextSibling;)ae.nextSibling.remove();ae.after(document.createTextNode(qe.title))}z(m,{id:"entries-back",icon:"arrow-left",title:"All collections",onClick:()=>{location.hash="#/collections"}}),z(m,{id:"entries-fields",icon:"edit-3",title:"Edit its fields and settings",onClick:()=>{location.hash=`#/collections/edit/${encodeURIComponent(a)}`}}),bt(a)&&z(m,{id:"entries-form",icon:"layout",title:"Open its form",onClick:()=>{location.hash=`#/forms/edit/${encodeURIComponent(a)}`}}),z(m,{id:"entries-export",icon:"download",title:"Export",onClick:()=>nt()}),z(m,{id:"entries-import",icon:"upload",title:"Import",onClick:()=>tt()}),z(m,{id:"entries-add",icon:"plus",title:"Add entry",onClick:()=>O(null)});const Te=e=>{N?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Te),r(()=>window.removeEventListener("beforeunload",Te)),r(()=>{if(N?.dirty())try{S.set(P(a),{id:N.id,values:N.values(),at:new Date().toISOString()})}catch{}ke=!0,[...ue].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(h),!p.peek()){Y.value=!1;return}if(await Ge(),!u()||(await H(),!u()))return;let C=null;try{C=S.get(P(a))}catch{}if(C?.values){try{S.remove?S.remove(P(a)):S.set(P(a),null)}catch{}let e=C.id?V(C.id):null;if(C.id&&!e&&(e=await g.collections.getEntry(a,C.id).catch(()=>null)),!u()||C.id&&!e)return;const t=e?`your changes to \u201C${D(K(e))}\u201D`:"a new entry",n=e?.meta?.updatedAt&&C.at&&e.meta.updatedAt>C.at?" It has been changed since - check before saving.":"";await E.confirm(`You left ${t} unsaved.${n} Pick up where you left off?`)&&u()&&O(e,C.values)}}};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{api as P}from"../api.js";import{createSimpleEditor as
|
|
2
|
-
`),
|
|
1
|
+
import{api as P}from"../api.js";import{createSimpleEditor as De}from"../lib/simple-editor.js";import{addBannerAction as H}from"../lib/plugin-chrome.js";import{helpPopovers as Ie}from"../lib/help-popover.js";import{openMenuAt as Ce,sidebarCount as Ue}from"../lib/tool-kit.js";import{PANES as F,STARTER_SOURCE as Ke,assembleSource as Be,attributeFor as Fe,draftKey as J,draftStatus as Je,nameProblem as Ye,previewRows as xe,readValue as We,snapshot as se,splitSource as _e,tagFor as ze,writeValue as Me}from"../lib/component-editor-arrange.js";import{fmtDateTime as Ge}from"../lib/dates.js";const He=/^\/components\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,Q=n=>String(n||"").split("?")[0],$e=n=>String(n??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),Y={get(n){try{return S.get(n)}catch{return null}},set(n,i){try{S.set(n,i)}catch{}},drop(n){try{S.remove?S.remove(n):S.set(n,null)}catch{}}};let h=null,Ne=!1;function Qe(){Ne||!window.R?.use||(Ne=!0,R.use((n,i,c)=>{const p=i?.path||"";return!(He.test(p)&&Q(p)!==Q(n?.path))||!h?.dirty()?c():E.confirm(h.question()).then(W=>{W?c():history.replaceState(null,"",`#${p}`)})}),M.subscribe("router:afterChange",({to:n}={})=>{if(h&&Q(n?.path)!==h.path){const i=h;h=null,i.dispose()}}))}function Xe(n,i){const c=n.querySelector(".dm-plugin-banner-name");if(!c)return;const p=c.querySelector(".dm-plugin-banner-sep");if(p){for(;p.nextSibling;)p.nextSibling.remove();p.after(document.createTextNode(i))}else c.textContent=`Components \u203A ${i}`}export const componentEditorView={templateUrl:"/admin/js/templates/component-editor.html",async onMount(n){if(Qe(),h){const e=h;h=null,e.dispose()}const i=n.get(0),c=i.querySelector(".cex")||i,p=[],v=e=>(p.push(e),e);let W=!1;const x=()=>!W&&c.isConnected,ce=location.hash.match(/^#\/components\/edit\/([^/?#]+)/);let a=ce?decodeURIComponent(ce[1]):null;const je=Q(location.hash.slice(1)),C=M.observable(!a),u=M.observable(!1),m=M.observable(a||""),d=M.observable(""),$=M.observable(!1),O=M.observable(0),j=M.observable(""),A=M.observable(!1),b=M.observable("compiling"),V=M.observable(""),X=M.observable("template"),le=M.observable([]),Z={value:{}},g={},D=()=>Be(Object.fromEntries(F.map(e=>[e,g[e]?.getValue()??""]))),ee=()=>({name:m.peek(),project:d.peek(),bundled:$.peek(),source:D()}),I=M.computed(()=>(O.value,u.value?!1:se({name:m.value,project:d.value,bundled:$.value,source:D()})!==j.value)),te=M.computed(()=>u.value?"":Ye(m.value,{isNew:C.value,taken:le.value}));let U=null;v(()=>clearTimeout(U));const Te=()=>{O.value=O.peek()+1,clearTimeout(U),U=setTimeout(B,300)};for(const e of F){const t=c.querySelector(`#component-src-${e}`);t&&(g[e]=De(t,{initialValue:"",onChange:Te}))}v(()=>{for(const e of F)try{g[e]?.destroy?.()}catch{}});const _=e=>{const t=_e(e);for(const o of F)g[o]?.setValue(t[o]);O.value=O.peek()+1},K=c.querySelector("#component-project"),ie=E.loader(i,{type:"dots"});let T;try{const[e,t,o]=await Promise.all([a?P.components.get(a):Promise.resolve(null),P.projects.list().catch(()=>[]),P.components.list().catch(()=>[])]);T={comp:e,projects:t,list:o}}catch(e){ie.destroy(),E.toast(e.message||"Component not found.",{type:"error"}),x()&&(location.hash="#/components");return}if(ie.destroy(),!c.isConnected)return;for(const e of T.projects||[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,K?.appendChild(t)}le.value=(T.list||[]).map(e=>e.name);const N=T.comp;if(N)u.value=N.readOnly===!0||N.origin==="plugin",Z.value=N.meta||{},d.value=N.meta?.project||"",$.value=!!N.bundled,_(N.source);else{const e=new URLSearchParams(location.hash.split("?")[1]||"").get("project");e&&(T.projects||[]).some(t=>t.slug===e)&&(d.value=e),_(Ke)}if(j.value=se(ee()),u.peek())for(const e of F)g[e]?.textarea&&(g[e].textarea.readOnly=!0);Xe(i,u.peek()?`View source: ${a}`:a||"New component");const f=c.querySelector("#component-preview-iframe");let oe=null,y={},z=null,ne=0;const G=e=>{try{f?.contentWindow?.postMessage(e,"*")}catch{}};function Le(e){if(!f)return;if(!f.dataset.dmMounted){f.dataset.dmMounted="true",G({type:"mount",payload:e});return}z=e;const t=()=>{f.removeEventListener("load",t),z&&x()&&G({type:"mount",payload:z}),z=null};f.addEventListener("load",t),f.src=f.src}async function B(){const e=++ne,t=m.peek().trim()||"preview";b.value="compiling";let o;try{o=await P.components.compile(t,D())}catch(r){if(e!==ne||!x())return;b.value="error",V.value=r.message||"Compile request failed.";return}if(!(e!==ne||!x())){if(o.errors?.length){b.value="error",V.value=o.errors.map(r=>`[${r.type}] ${r.message}`).join(`
|
|
2
|
+
`),oe=null;return}oe=o.js,b.value="ok",V.value="",me(),Le({compiledJs:oe,tagName:`dm-${t}`,props:y})}}const w=c.querySelector("#component-preview-props-panel");let pe=[];function ue(e){w.replaceChildren();const t=document.createElement("p");t.className="cex-note",t.textContent=e,w.appendChild(t)}function me(){const{rows:e,error:t}=xe(g.props?.getValue(),y);if(pe=e,t){ue("The <props> block is not valid JSON - fix it to see the props here.");return}if(!e.length){y={},ue("No props declared. Add them in the <props> tab.");return}y=Object.fromEntries(e.map(o=>[o.name,o.value])),w.replaceChildren();for(const o of e){const r=document.createElement("div");r.className="cex-prop",r.tabIndex=0,r.dataset.prop=o.name;const l=document.createElement("label");l.className="cex-prop-label",l.textContent=o.label,l.title=`${o.name} (${o.type})`;const s=o.type==="array"||o.type==="object"?document.createElement("textarea"):document.createElement("input");o.type==="boolean"?(s.type="checkbox",s.className="form-check",s.checked=Me(o.type,o.value)):(s.tagName==="INPUT"?s.type=o.type==="number"?"number":"text":s.rows=2,s.className="form-input form-input--sm cex-prop-input",s.value=Me(o.type,o.value)),s.setAttribute("aria-label",o.label);const Se=()=>{y[o.name]=We(o.type,o.type==="boolean"?s.checked:s.value),G({type:"update",payload:{props:y}})};s.addEventListener("input",Se),s.addEventListener("change",Se);const q=document.createElement("button");q.type="button",q.className="cex-kebab",q.setAttribute("aria-label",`Actions for ${o.label}`),q.title="Actions",q.textContent="\u22EE",r.append(l,s,q),w.appendChild(r)}}const ae=async(e,t)=>{try{await navigator.clipboard.writeText(e),E.toast(`${t} copied.`,{type:"success"})}catch{E.toast(e,{type:"info",duration:8e3})}};function Re(e){const t=pe.find(l=>l.name===e);if(!t)return[];const o=y[t.name],r=Fe(t.name,t.type,o);return[{header:t.label},{label:"Reset to its default",icon:"rotate-ccw",action:()=>{delete y[t.name];const l=xe(g.props?.getValue(),{}).rows.find(s=>s.name===t.name);y[t.name]=l?l.value:null,me(),G({type:"update",payload:{props:y}})}},{label:"Copy as an attribute",icon:"copy",disabled:!r,action:()=>ae(r,"Attribute")},{label:"Copy the tag with this value",icon:"code",action:()=>ae(`<dm-${m.peek().trim()||"component"}${r?` ${r}`:""}></dm-${m.peek().trim()||"component"}>`,"Tag")},{label:"Copy the prop name",icon:"copy",action:()=>ae(t.name,"Name")},{type:"divider"},{label:"Edit in <props>",icon:"edit",action:()=>{X.value="props";const l=g.props?.textarea;if(!l)return;const s=l.value.indexOf(`"${t.name}"`);l.focus(),s>=0&&l.setSelectionRange(s,s+t.name.length+2)}}]}const qe=E.contextMenu(w,{cursor:"glyph",match:".cex-prop",inherit:!1,density:"compact",items:e=>Re(e.dataset.prop)});v(()=>qe?.destroy?.());const de=e=>{const t=e.target.closest(".cex-kebab");t&&Ce(t)},ve=e=>{const t=e.target.closest(".cex-prop");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),Ce(t))};w.addEventListener("click",de),w.addEventListener("keydown",ve),v(()=>{w.removeEventListener("click",de),w.removeEventListener("keydown",ve)});async function re(){if(u.peek()||A.peek())return;const e=te.peek();if(e){E.toast(e,{type:"warning"});return}if(b.peek()==="error"){E.toast("Fix the compile errors first - a component that does not compile is not saved.",{type:"warning"});return}const t=m.peek().trim(),o={...Z.value||{}};delete o.bundled,d.peek()?o.project=d.peek():delete o.project,A.value=!0;try{if(await P.components.put(t,{source:D(),bundled:$.peek(),meta:o}),!x())return;Z.value=o,j.value=se(ee()),Y.drop(J(a)),E.toast(C.peek()?`Created the component "${t}".`:"Saved.",{type:"success"}),C.peek()&&(Ue("#/components"),a=t,location.hash=`#/components/edit/${encodeURIComponent(t)}`)}catch(r){E.toast(`Not saved: ${r.message||r}`,{type:"error"})}finally{A.value=!1}}async function Pe(){if(!I.peek()||!await E.confirm("Throw away the changes you have not saved?"))return;const e=JSON.parse(j.peek());C.peek()&&(m.value=e[0]),d.value=e[1],$.value=e[2],_(e[3]),clearTimeout(U),B()}function fe(){Y.set(J(null),{name:`${a}-copy`,project:"",bundled:!1,source:D(),base:null,seed:!0,at:new Date().toISOString()}),location.hash="#/components/new"}const Oe={name:m,project:d,bundled:$,dirty:I,saving:A,readOnly:u,isNew:C,nameErr:te,compileErrors:V,activeTab:X,notNew:M.computed(()=>!C.value),canEdit:M.computed(()=>!u.value),tagText:M.computed(()=>ze(m.value)),statusText:M.computed(()=>({compiling:"Compiling\u2026",ok:"Compiles",error:"Does not compile - see below"})[b.value]),statusCls:M.computed(()=>`is-${b.value}`),hasErrors:M.computed(()=>b.value==="error"&&!!V.value),saveBlocked:M.computed(()=>A.value||!!te.value||b.value==="error"),nameLocked:M.computed(()=>!C.value||u.value)},Ae=M.applyBindings(Oe,c,{methods:{save:()=>re(),discard:()=>Pe(),copyToNew:()=>fe(),showTab:e=>{X.value=e}}});v(()=>Ae.dispose()),v(Ie(c,{position:"bottom"})),K&&(K.value=d.peek());const L=c.querySelector("#component-quick-switch");if(L&&a){for(const t of[...T.list||[]].sort((o,r)=>o.name.localeCompare(r.name))){const o=document.createElement("option");o.value=t.name,o.textContent=t.origin==="plugin"?`${t.name} (plugin)`:t.name,t.name===a&&(o.selected=!0),L.appendChild(o)}L.hidden=!1;const e=()=>{const t=L.value;t&&t!==a&&(location.hash=`#/components/edit/${encodeURIComponent(t)}`)};L.addEventListener("change",e),v(()=>L.removeEventListener("change",e))}H(n,{id:"component-editor-back",icon:"arrow-left",title:"All components",onClick:()=>{location.hash="#/components"}}),a&&H(n,{id:"component-editor-export",icon:"download",title:"Export .dmcomponent.json",onClick:async()=>{try{await P.components.exportBundle(a)}catch(e){E.toast(e.message||"Export failed.",{type:"error"})}}}),u.peek()?H(n,{id:"component-editor-copy",icon:"copy",title:"Copy to a new component",onClick:fe}):H(n,{id:"component-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>re()});const ye=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),re())};document.addEventListener("keydown",ye,!0),v(()=>document.removeEventListener("keydown",ye,!0));const he=()=>{I.peek()&&Y.set(J(a),{...ee(),base:j.peek(),at:new Date().toISOString()})},be=e=>{I.peek()&&(he(),e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",be),v(()=>window.removeEventListener("beforeunload",be)),h={path:je,dirty:()=>I.peek(),question:()=>`You have unsaved changes to ${a?`<dm-${$e(a)}>`:"this new component"}. Leave anyway? They are kept as a draft for your next visit.`,dispose(){W=!0,he(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},Domma.icons.scan(c),f?.contentDocument?.readyState==="complete"?B():f?.addEventListener("load",()=>{x()&&B()},{once:!0}),u.peek())return;const k=Y.get(J(a)),ge=Je(k,j.peek());if(!ge.ok)return;Y.drop(J(a));const we=()=>{C.peek()&&typeof k.name=="string"&&(m.value=k.name),d.value=String(k.project??""),K&&(K.value=d.peek()),$.value=k.bundled===!0,_(k.source),clearTimeout(U),B()};if(k.seed){we();return}const ke=k.at?` (${Ge(k.at)})`:"",Ee=a?`<dm-${$e(a)}>`:"a new component",Ve=ge.stale?`You left unsaved changes to ${Ee}${ke}, but the saved version has changed since. Restore your version anyway?`:`You left unsaved changes to ${Ee}${ke}. Restore them?`;await E.confirm(Ve)&&x()&&we()}};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{apiRequest as Fe}from"../api.js";import{addBannerAction as He}from"../lib/plugin-chrome.js";import{mountRelated as ze,relatedMenuItems as Ke,relatedProviders as We}from"../lib/related.js";import{helpPopovers as
|
|
1
|
+
import{apiRequest as Fe}from"../api.js";import{addBannerAction as He}from"../lib/plugin-chrome.js";import{mountRelated as ze,relatedMenuItems as Ke,relatedProviders as We}from"../lib/related.js";import{helpPopovers as X}from"../lib/help-popover.js";import{onceClosed as te,openMenuAt as ae,settingsStore as Ye,viewLifetime as _e}from"../lib/tool-kit.js";import{arrangeContacts as Je,countContacts as Ve,DEFAULT_SETTINGS as ve,duplicateBody as Qe,initials as Xe,normaliseSettings as ye,primaryEmail as ge,primaryPhone as ke,withGroup as oe}from"../lib/contacts-arrange.js";const we=Ye("contacts.settings",ye),h=(A,d="GET",q)=>Fe(A,{method:d,...q!==void 0?{body:JSON.stringify(q)}:{}}),Ee={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"},N={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}]}},Ze=`
|
|
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>
|
|
@@ -15,22 +15,22 @@ import{apiRequest as Fe}from"../api.js";import{addBannerAction as He}from"../lib
|
|
|
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
|
-
<button type="button" class="btn btn-sm btn-ghost" data-act="rename">Rename</button>
|
|
19
|
-
<button type="button" class="btn btn-sm btn-ghost text-danger" data-act="delete">Delete</button>
|
|
20
|
-
</div
|
|
21
|
-
<div${
|
|
18
|
+
<button type="button" class="btn btn-sm btn-ghost" data-act="rename" data-bind-disabled="locked" title="{{lockTitle}}">Rename</button>
|
|
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>`,ot="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 se(A){const d=document.createElement("template");return d.innerHTML=A.trim(),d.content.firstElementChild}const L=(A,d,q=`${d}s`)=>`${A} ${A===1?d:q}`,w=_e(/^#\/contacts\b/);export const contactsView={templateUrl:"/admin/js/templates/contacts.html",async onMount(A){w.reset();const d=A.get(0);let q=[];We("contact").then(t=>{q=t});const x=M.observable(we.read()),m=M.observable([]),v=M.observable([]),J=M.observable(!0),ne=M.observable(""),H=M.observable({kind:"all"}),p=M.observable(new Set),ce=M.observable(!1),le=M.computed(()=>Ve(m.value,v.value)),Se=M.computed(()=>Je(m.value,{query:ne.value,filter:H.value,sort:x.value.sort,favouritesFirst:x.value.favouritesFirst})),T=M.observable([]);w.add(M.effect(()=>{const t=p.value;T.value=Se.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:Xe(e.name),company:e.company||"",email:ge(e),phone:ke(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 re=M.observable([]);w.add(M.effect(()=>{const t=le.value,e=H.value;re.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":""},...v.value.map(a=>({key:`group:${a}`,label:a,count:t.groups[a]??0,title:`Only the ${a} group`,cls:e.kind==="group"&&e.value===a?" is-active":""}))]})),w.add(M.effect(()=>{const t=H.value;t.kind==="group"&&!v.value.includes(t.value)&&(H.value={kind:"all"})}));const Me={rows:T,chips:re,loading:J,query:ne,searchFocus:ce,hasRows:M.computed(()=>T.value.length>0),isEmpty:M.computed(()=>!J.value&&T.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(J.value)return"";const t=T.value.length,e=m.value.length;return t===e?L(e,"contact"):`${t} of ${e}`}),hasSelection:M.computed(()=>p.value.size>0),selectionText:M.computed(()=>`${L(p.value.size,"contact")} selected`),allSelected:M.computed(()=>T.value.length>0&&T.value.every(t=>p.value.has(t.id)))};w.add(M.effect(()=>we.write(x.value)));const $e=(t,e)=>{x.value=ye({...x.peek(),[t]:e})},B=t=>m.peek().find(e=>e.id===t),Ce=t=>{m.value=m.peek().map(e=>e.id===t.id?t:e)};async function V(){J.value=!0;try{const[t,e]=await Promise.all([h("/contacts/contacts"),h("/contacts/groups")]);m.value=Array.isArray(t)?t:t?.data??[],v.value=Array.isArray(e)?e:[]}catch{E.toast("Failed to load contacts.",{type:"error"})}finally{J.value=!1}}async function xe(){try{v.value=await h("/contacts/groups")}catch{}}async function z(t,e,a){try{return Ce(await h(`/contacts/contacts/${t}`,"PUT",e)),a&&E.toast(a,{type:"success"}),!0}catch(s){return E.toast(s?.message||"Failed to update the contact.",{type:"error"}),!1}}async function ie(t){const e=B(t);if(x.peek().confirmDelete&&!await E.confirm("Delete this contact? This cannot be undone."))return!1;try{await h(`/contacts/contacts/${t}`,"DELETE"),m.value=m.peek().filter(s=>s.id!==t);const a=new Set(p.peek());return a.delete(t)&&(p.value=a),E.toast(`${e?.name?"Contact":"It"} deleted.`,{type:"success"}),!0}catch{return E.toast("Failed to delete the contact.",{type:"error"}),!1}}async function Te(t){const e=B(t);if(e)try{const a=await h("/contacts/contacts","POST",Qe(e));m.value=[...m.peek(),a],E.toast("Copied - the copy is open to edit.",{type:"success"}),W(a.id)}catch{E.toast("Failed to duplicate the contact.",{type:"error"})}}async function K(t){if(v.peek().includes(t))return!0;try{return await h("/contacts/groups","POST",{name:t}),await xe(),!0}catch(e){return E.toast(e?.message||"Could not create the group.",{type:"error"}),!1}}async function Ae(t,e,a){const s=B(t);s&&(a&&!await K(e)||await z(t,{groups:oe(s.groups,e,a)},a?`Added to ${e}.`:`Removed from ${e}.`))}async function de(t){const e=String(await E.prompt("New group name:")??"").trim();if(!(!e||!await K(e))){for(const a of t){const s=B(a);s&&!(s.groups||[]).includes(e)&&await z(a,{groups:oe(s.groups,e,!0)})}E.toast(`Filed in ${e}.`,{type:"success"})}}async function pe(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 Ne(){const t=[...p.peek()];if(t.length&&await E.confirm(`Delete ${L(t.length,"contact")}? This cannot be undone.`))try{const e=await h("/contacts/contacts/bulk","DELETE",{ids:t});m.value=m.peek().filter(a=>!t.includes(a.id)),p.value=new Set,E.toast(`${L(e?.deleted??t.length,"contact")} deleted.`,{type:"success"})}catch{E.toast("Bulk delete failed.",{type:"error"})}}async function Le(t){const e=[...p.peek()];if(!(!e.length||!await K(t))){for(const a of e){const s=B(a);s&&!(s.groups||[]).includes(t)&&await z(a,{groups:oe(s.groups,t,!0)})}E.toast(`${L(e.length,"contact")} filed in ${t}.`,{type:"success"})}}const ue=t=>{const e=new Set(p.peek());e.delete(t)||e.add(t),p.value=e};async function Be(){try{const t=await h("/contacts/export"),e=new Blob([JSON.stringify(t,null,2)],{type:"application/json"}),a=URL.createObjectURL(e),s=document.createElement("a");s.href=a,s.download="contacts.json",document.body.appendChild(s),s.click(),s.remove(),setTimeout(()=>URL.revokeObjectURL(a),1e3),E.toast("Contacts exported.",{type:"success"})}catch{E.toast("Export failed.",{type:"error"})}}const Q=d.querySelector(".ct-import-file");Q.addEventListener("change",async()=>{const t=Q.files?.[0];if(Q.value="",!!t)try{const e=JSON.parse(await t.text()),a=Array.isArray(e)?e:e.contacts;if(!Array.isArray(a))throw new Error("Invalid format");const s=await h("/contacts/import","POST",{contacts:a});E.toast(`Imported ${L(s?.imported??0,"contact")}.`,{type:"success"}),await V()}catch{E.toast("Import failed. The file must be a contacts JSON export.",{type:"error"})}});function W(t){const e=t?B(t):null;if(t&&!e)return;const a=E.slideover({title:e?"Edit contact":"New contact",size:"lg",position:"right"}),s=(o,c,{type:P="text",mono:f=!1,placeholder:y="",wide:C=!1}={})=>`
|
|
21
|
+
<div${C?' class="ct-wide"':""}>
|
|
22
22
|
<label class="form-label" for="ct-f-${o}">${c}</label>
|
|
23
|
-
<input id="ct-f-${o}" type="${
|
|
24
|
-
</div>`,
|
|
23
|
+
<input id="ct-f-${o}" type="${P}" class="form-input${f?" font-mono":""}" placeholder="${y}" data-model="${o}.value">
|
|
24
|
+
</div>`,l=o=>`
|
|
25
25
|
<div class="ct-repeater" data-repeater="${o}">
|
|
26
26
|
<div class="ct-subhead">
|
|
27
|
-
<strong>${
|
|
27
|
+
<strong>${N[o].title} <span data-help="${N[o].help}"></span></strong>
|
|
28
28
|
<button type="button" class="btn btn-sm btn-ghost" data-add="${o}">
|
|
29
|
-
<span data-icon="plus" data-icon-size="13"></span> ${
|
|
29
|
+
<span data-icon="plus" data-icon-size="13"></span> ${N[o].add}</button>
|
|
30
30
|
</div>
|
|
31
31
|
<div class="ct-repeater-list"></div>
|
|
32
|
-
<p class="text-muted text-sm contacts-empty-row">${
|
|
33
|
-
</div>`,r=
|
|
32
|
+
<p class="text-muted text-sm contacts-empty-row">${N[o].empty}</p>
|
|
33
|
+
</div>`,r=se(`
|
|
34
34
|
<div class="ct-panel">
|
|
35
35
|
<div class="ct-grid">
|
|
36
36
|
<div class="ct-wide">
|
|
@@ -98,15 +98,15 @@ import{apiRequest as Fe}from"../api.js";import{addBannerAction as He}from"../lib
|
|
|
98
98
|
${s("bic","BIC / SWIFT",{mono:!0,placeholder:"NWBKGB2L"})}
|
|
99
99
|
</div>
|
|
100
100
|
</div>
|
|
101
|
-
${
|
|
101
|
+
${l("addresses")}
|
|
102
102
|
</div>
|
|
103
103
|
</details>
|
|
104
104
|
|
|
105
105
|
<details class="ct-section">
|
|
106
106
|
<summary>More emails & phones <span class="ct-section-sum" data-bind-text="extraSummary.value"></span></summary>
|
|
107
107
|
<div class="ct-section-body">
|
|
108
|
-
${
|
|
109
|
-
${
|
|
108
|
+
${l("emails")}
|
|
109
|
+
${l("phones")}
|
|
110
110
|
</div>
|
|
111
111
|
</details>
|
|
112
112
|
|
|
@@ -121,11 +121,11 @@ import{apiRequest as Fe}from"../api.js";import{addBannerAction as He}from"../lib
|
|
|
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>`),
|
|
124
|
+
</div>`),S=o=>o==null?"":String(o),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 o of Object.keys(Ee))n[o]=M.observable(S(e?.[o]));const i=Object.fromEntries(Object.keys(N).map(o=>[o,M.observable(0)]));n.pick=M.observable([]);const O=M.effect(()=>{const o=n.chosen.value;n.pick.value=[...new Set([...v.value,...o])].map(c=>({name:c,on:String(o.includes(c)),cls:o.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 o=[];n.company.value.trim()&&o.push(n.company.value.trim()),n.vatNumber.value.trim()&&o.push("VAT"),(n.sortCode.value.trim()||n.iban.value.trim())&&o.push("bank details");const c=i.addresses.value;return c&&o.push(L(c,"address","addresses")),o.join(" \xB7 ")}),n.extraSummary=M.computed(()=>{const o=i.emails.value+i.phones.value;return o?L(o,"more"):""});function g(o,c){const P=N[o],f=r.querySelector(`[data-repeater="${o}"]`),y=f.querySelector(".ct-repeater-list");y.replaceChildren(),f.querySelector(".contacts-empty-row").style.display=c.length?"none":"",c.forEach((C,G)=>{const R=document.createElement("div");R.className="contacts-row";for(const k of P.columns){if(k.check){const Z=document.createElement("label");Z.className="form-check contacts-row-check";const j=document.createElement("input");j.type="checkbox",j.className="form-check-input",j.dataset.key=k.key,j.checked=C[k.key]===!0,j.addEventListener("change",()=>{j.checked&&y.querySelectorAll(`input[data-key="${k.key}"]`).forEach(he=>{he!==j&&(he.checked=!1)})});const ee=document.createElement("span");ee.className="form-check-label",ee.textContent=k.label,Z.append(j,ee),R.appendChild(Z);continue}const _=document.createElement("input");_.type="text",_.className="form-input"+(k.wide?" contacts-row-wide":""),_.dataset.key=k.key,_.placeholder=k.placeholder??"",_.value=C[k.key]??"",R.appendChild(_)}const Y=document.createElement("button");Y.type="button",Y.className="btn btn-sm btn-ghost contacts-row-remove",Y.title="Remove",Y.textContent="\xD7",Y.addEventListener("click",()=>{const k=u(o,{keepBlank:!0});k.splice(G,1),g(o,k)}),R.appendChild(Y),y.appendChild(R)}),i[o].value=c.length}function u(o,{keepBlank:c=!1}={}){const P=N[o],f=[...r.querySelectorAll(`[data-repeater="${o}"] .contacts-row`)].map(y=>{const C={};for(const G of P.columns){const R=y.querySelector(`[data-key="${G.key}"]`);C[G.key]=G.check?R?.checked===!0:(R?.value??"").trim()}return C});return c?f:f.filter(y=>Object.entries(y).some(([C,G])=>C!=="label"&&G!==""&&G!==!1))}r.addEventListener("click",o=>{const c=o.target.closest("[data-add]");if(c){const f=c.dataset.add;g(f,[...u(f,{keepBlank:!0}),{}]),r.querySelector(`[data-repeater="${f}"] .contacts-row:last-child input`)?.focus();return}const P=o.target.closest(".ct-pick [data-group]");if(P){const f=P.dataset.group,y=n.chosen.peek();n.chosen.value=y.includes(f)?y.filter(C=>C!==f):[...y,f]}});const $=()=>{const o={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(Ee))o[c]=String(n[c].peek()??"").trim();for(const c of Object.keys(N))o[c]=u(c);return o};let b=!1;const F=async()=>{if(!(b||n.cannotSave.peek())){b=!0;try{const o=$();for(const c of o.groups)v.peek().includes(c)||await K(c);if(e){if(!await z(e.id,o,"Contact saved."))return}else{const c=await h("/contacts/contacts","POST",o);m.value=[...m.peek(),c],E.toast("Contact added.",{type:"success"})}a.close()}catch(o){E.toast(o?.message||"Failed to save the contact.",{type:"error"})}finally{b=!1}}};a.element.appendChild(r),e&&ze(r.querySelector(".related-box"),"contact",e.id,{onOpen:()=>a.close(),name:e.name});const qe=X(r);for(const o of Object.keys(N))g(o,Array.isArray(e?.[o])?e[o]:[]);r.querySelector(".ct-pick").innerHTML=tt;const Ue=M.applyBindings(n,r,{methods:{save:F,cancel:()=>a.close(),del:async()=>{e&&await ie(e.id)&&a.close()},async newGroup(){const o=String(await E.prompt("New group name:")??"").trim();!o||!await K(o)||n.chosen.peek().includes(o)||(n.chosen.value=[...n.chosen.peek(),o])}}});n.billingSummary.peek()&&(r.querySelector(".ct-section").open=!0),r.addEventListener("keydown",o=>{(o.ctrlKey||o.metaKey)&&o.key==="Enter"&&(o.preventDefault(),F())}),I.scan(r),te(a,()=>{O?.(),Ue.dispose(),qe()}),a.open(),setTimeout(()=>r.querySelector("#ct-f-name")?.focus(),60)}function Oe(){const t=E.slideover({title:"Contacts settings",size:"sm",position:"right"}),e=(n,i,O)=>`
|
|
125
125
|
<div class="ct-setting-row">
|
|
126
|
-
<label class="form-label" for="ct-set-${n}" style="margin:0">${
|
|
126
|
+
<label class="form-label" for="ct-set-${n}" style="margin:0">${i}${O?` <span data-help="${O}"></span>`:""}</label>
|
|
127
127
|
<input id="ct-set-${n}" type="checkbox" class="form-switch" data-model="${n}.value">
|
|
128
|
-
</div>`,a=
|
|
128
|
+
</div>`,a=se(`
|
|
129
129
|
<div class="ct-panel">
|
|
130
130
|
<div>
|
|
131
131
|
<label class="form-label" for="ct-set-sort">Sort by</label>
|
|
@@ -160,16 +160,18 @@ import{apiRequest as Fe}from"../api.js";import{addBannerAction as He}from"../lib
|
|
|
160
160
|
<span class="spacer"></span>
|
|
161
161
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
|
|
162
162
|
</div>
|
|
163
|
-
</div>`),s=n=>M.computed({read:()=>
|
|
163
|
+
</div>`),s=n=>M.computed({read:()=>x.value[n],write:i=>$e(n,i)}),l=Object.fromEntries(Object.keys(ve).map(n=>[n,s(n)]));t.element.appendChild(a);const r=X(a),S=M.applyBindings(l,a,{methods:{done:()=>t.close(),reset:()=>{x.value={...ve}}}});te(t,()=>{S.dispose(),r()}),t.open()}function Pe(){const t=E.slideover({title:"Groups",size:"sm",position:"right"}),e=se(`
|
|
164
164
|
<div class="ct-panel">
|
|
165
165
|
<form class="ct-add-group" data-on-submit="add">
|
|
166
166
|
<input type="text" class="form-input" placeholder="New group name\u2026" aria-label="New group name" data-model="draft.value">
|
|
167
167
|
<button type="submit" class="btn btn-sm btn-primary" data-bind-disabled="cannotAdd.value">Add</button>
|
|
168
168
|
</form>
|
|
169
|
-
<p class="ct-panel-meta" style="margin:0">Renaming a group renames it on every contact in it; deleting one takes it off them.
|
|
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
|
+
<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="${ot}"></span></p>
|
|
171
173
|
<div class="ct-groups-list" data-if="hasGroups.value">
|
|
172
174
|
<div class="ct-groups-rows" data-each="list.value key=name"></div>
|
|
173
175
|
</div>
|
|
174
176
|
<p class="text-muted" data-if="noGroups.value">No groups yet.</p>
|
|
175
|
-
</div>`),a=M.observable(""),s={draft:a,list:M.observable([])},u=M.effect(()=>{const
|
|
177
|
+
</div>`),a=M.observable(""),s=M.observable(null),l={draft:a,list:M.observable([]),someLocked:M.observable(!1)},r=async()=>{try{const g=await h("/contacts/groups/access");s.value=new Map((g?.groups||[]).map(u=>[u.name,u.canManage===!0]))}catch{s.value=new Map}};r();const S=M.effect(()=>{const g=le.value.groups,u=s.value,$=v.value.map(b=>{const F=!(u&&u.get(b)===!0);return{name:b,countText:L(g[b]??0,"contact"),locked:F,lockTitle:F?"Only its creator, or someone allowed to manage every group, can change this group":""}});l.list.value=$,l.someLocked.value=u!==null&&$.some(b=>b.locked)});l.hasGroups=M.computed(()=>l.list.value.length>0),l.noGroups=M.computed(()=>l.list.value.length===0),l.cannotAdd=M.computed(()=>!a.value.trim());async function n(){const g=a.peek().trim();g&&await K(g)&&(await r(),a.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 E.prompt("Rename the group to:",{inputValue:$})??"").trim();if(!b||b===$)return;try{await h(`/contacts/groups/${encodeURIComponent($)}`,"PUT",{newName:b}),await V(),await r(),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 h(`/contacts/groups/${encodeURIComponent($)}`,"DELETE"),await V(),await r(),E.toast("Group deleted.",{type:"success"})}catch(b){E.toast(b?.message||"Failed to delete the group.",{type:"error"})}}}}),t.element.appendChild(e);const i=X(e);e.querySelector(".ct-groups-rows").innerHTML=at;const O=M.applyBindings(l,e,{methods:{add:()=>(n(),!1)}});te(t,()=>{S?.(),O.dispose(),i()}),t.open()}He(A,{id:"contacts-settings",icon:"settings",title:"Contacts settings",onClick:Oe}),d.querySelector(".ct-rows").innerHTML=Ze,d.querySelector(".ct-chips").innerHTML=et;const U=d.querySelector(".ct-list"),me=d.querySelector(".ct-bar"),Ge=d.querySelector(".ct-chips-row"),be=d.querySelector(".ct-bulk");w.add(X(d,{position:"bottom"}));const Re=M.applyBindings(Me,d,{methods:{create:()=>W(null),bulkDelete:Ne,clearSelection:()=>{p.value=new Set}}});w.add(()=>Re.dispose()),w.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,l]of Object.entries(e))U.classList.toggle(`hide-${s}`,l);const a=["1.1rem","1.5rem","minmax(10rem, 1.4fr)"];e.company||a.push("minmax(0, 1fr)"),e.email||a.push("minmax(0, 1.2fr)"),e.phone||a.push("minmax(0, .9fr)"),e.groups||a.push("minmax(0, 1fr)"),a.push("1.75rem"),U.style.setProperty("--ct-cols",a.join(" "))}));const De=E.contextMenu(U,{cursor:"glyph",match:".ct-row",inherit:!1,density:"compact",items:t=>{const e=t.dataset.id,a=B(e);if(!a)return[];const s=ge(a),l=ke(a),r=a.groups||[],S=i=>i.filter(O=>O.visible!==!1),n=Ke(q,e,i=>i.label.replace("{name}",a.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 ${l}`,icon:"phone",visible:!!l,action:()=>{location.href=`tel:${l.replace(/[^\d+]/g,"")}`}},{label:"Copy email",icon:"copy",visible:!!s,action:()=>pe(s,"Email")},{label:"Copy phone",icon:"copy",visible:!!l,action:()=>pe(l,"Phone")},{type:"divider",visible:!!(s||l)},{label:a.favourite===!0?"Unstar":"Star",icon:"star",action:()=>z(e,{favourite:a.favourite!==!0})},{label:"Groups",icon:"folder",submenu:[...v.peek().map(i=>({label:`${r.includes(i)?"\u2713 ":""}${i}`,action:()=>Ae(e,i,!r.includes(i))})),...v.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>de([e])}]},{label:"Duplicate",icon:"copy",action:()=>Te(e)},{label:p.peek().has(e)?"Deselect":"Select",icon:"check-square",action:()=>ue(e)},{type:"divider"},{label:"Delete",icon:"trash",danger:!0,action:()=>ie(e)}])}});w.add(()=>De?.destroy?.());const Ie=E.contextMenu(me,{cursor:"glyph",match:".ct-more",inherit:!1,density:"compact",items:()=>[{label:"Manage groups\u2026",icon:"folder",action:Pe},{type:"divider"},{label:"Export all (JSON)",icon:"download",action:Be},{label:"Import from file\u2026",icon:"upload",action:()=>Q.click()},{type:"divider"},{label:"Select all shown",icon:"check-square",disabled:!T.peek().length,action:()=>{p.value=new Set(T.peek().map(t=>t.id))}},{label:"Clear selection",icon:"x",disabled:!p.peek().size,action:()=>{p.value=new Set}}]});w.add(()=>Ie?.destroy?.());const je=E.contextMenu(be,{cursor:"glyph",match:".ct-bulk-group",inherit:!1,density:"compact",items:()=>[...v.peek().map(t=>({label:t,action:()=>Le(t)})),...v.peek().length?[{type:"divider"}]:[],{label:"New group\u2026",action:()=>de([...p.peek()])}]});w.add(()=>je?.destroy?.()),me.addEventListener("click",t=>{const e=t.target.closest(".ct-more");e&&ae(e)}),be.addEventListener("click",t=>{const e=t.target.closest(".ct-bulk-group");e&&ae(e)}),Ge.addEventListener("click",t=>{const e=t.target.closest(".ct-chip");if(!e)return;const a=e.dataset.chip;H.value=a==="fav"?{kind:"fav"}:a.startsWith("group:")?{kind:"group",value:a.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 a=e.dataset.id;if(t.target.closest(".ct-kebab"))return ae(t.target.closest(".ct-kebab"));if(!t.target.closest(".ct-select")){if(t.target.closest(".ct-fav")){const s=B(a);s&&z(a,{favourite:s.favourite!==!0});return}W(a)}}),U.addEventListener("change",t=>{if(t.target.matches(".ct-select")&&ue(t.target.closest(".ct-row").dataset.id),t.target.matches(".ct-select-all")){const e=T.peek().map(a=>a.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 fe=t=>{t.ctrlKey||t.metaKey||t.altKey||t.target.closest?.("input, textarea, select, [contenteditable], .dm-slideover")||(t.key==="/"?(t.preventDefault(),ce.value=!0):(t.key==="n"||t.key==="N")&&(t.preventDefault(),W(null)))};document.addEventListener("keydown",fe),w.add(()=>document.removeEventListener("keydown",fe)),I.scan(d),await V()}};
|