domma-cms 1.6.55 → 1.6.57
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/admin/css/calculator.css +1 -1
- package/admin/js/lib/analytics-shape.js +1 -1
- package/admin/js/lib/calculator-engine.js +1 -1
- package/admin/js/lib/calculator.js +22 -10
- package/admin/js/lib/form-editor-arrange.js +1 -1
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/notes-arrange.js +1 -1
- package/admin/js/lib/search-settings-arrange.js +1 -1
- package/admin/js/lib/seo-arrange.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -1
- package/admin/js/lib/tool-kit.js +1 -1
- package/admin/js/lib/tool-recovery.js +4 -4
- package/admin/js/templates/analytics.html +26 -14
- package/admin/js/templates/contacts.html +26 -5
- package/admin/js/templates/form-editor.html +2 -0
- package/admin/js/templates/notes.html +11 -8
- package/admin/js/templates/search.html +9 -3
- package/admin/js/templates/seo.html +23 -17
- package/admin/js/templates/todo.html +18 -14
- package/admin/js/views/analytics.js +10 -10
- package/admin/js/views/contacts.js +47 -47
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/forms.js +5 -5
- package/admin/js/views/notes.js +6 -6
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +43 -43
- package/admin/js/views/todo.js +8 -8
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +4 -1
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/reader-kit.js +364 -0
- package/plugins/_lib/admin/mail/reader-view.js +1009 -161
- package/plugins/_lib/admin/mail/when-closed.js +33 -3
- package/plugins/_lib/mail/readRoutes.js +48 -10
- package/plugins/_lib/mail/unreadBadge.js +4 -1
- package/plugins/blog/CLAUDE.md +14 -0
- package/plugins/blog/admin/css/index.css +5 -0
- package/plugins/blog/admin/lib/drafts.js +95 -0
- package/plugins/blog/admin/templates/blog.html +1 -1
- package/plugins/blog/admin/templates/categories.html +1 -1
- package/plugins/blog/admin/templates/comments.html +1 -1
- package/plugins/blog/admin/templates/post-editor.html +5 -1
- package/plugins/blog/admin/views/blog.js +44 -19
- package/plugins/blog/admin/views/categories.js +138 -20
- package/plugins/blog/admin/views/comments.js +32 -23
- package/plugins/blog/admin/views/kit.js +232 -22
- package/plugins/blog/admin/views/post-editor.js +348 -178
- package/plugins/blog/admin/views/settings.js +17 -3
- package/plugins/blog/plugin.js +1 -1
- package/plugins/blog/plugin.json +7 -7
- package/plugins/blog/plugin.public.js +1 -1
- package/plugins/blog/tests/drafts.test.js +50 -0
- package/plugins/booking/CLAUDE.md +13 -0
- package/plugins/booking/admin/lib/drafts.js +139 -0
- package/plugins/booking/admin/lib/kit.js +246 -25
- package/plugins/booking/admin/views/booking.js +11 -2
- package/plugins/booking/admin/views/bookings.js +7 -2
- package/plugins/booking/admin/views/diary.js +13 -6
- package/plugins/booking/admin/views/panels.js +85 -12
- package/plugins/booking/admin/views/people.js +41 -2
- package/plugins/booking/admin/views/services.js +45 -3
- package/plugins/booking/admin/views/settings.js +22 -2
- package/plugins/booking/plugin.json +2 -2
- package/plugins/booking/tests/admin-drafts.test.js +67 -0
- package/plugins/feedback/CLAUDE.md +12 -0
- package/plugins/feedback/admin/css/index.css +5 -0
- package/plugins/feedback/admin/lib/drafts.js +145 -0
- package/plugins/feedback/admin/lib/kit.js +161 -5
- package/plugins/feedback/admin/lib/settings.js +46 -28
- package/plugins/feedback/admin/views/feedback.js +1 -1
- package/plugins/feedback/admin/views/inbox.js +110 -46
- package/plugins/feedback/admin/views/send.js +174 -83
- package/plugins/feedback/plugin.json +3 -3
- package/plugins/feedback/tests/drafts.test.js +93 -0
- package/plugins/free-tier.lock.json +75 -63
- package/plugins/security/CLAUDE.md +9 -0
- package/plugins/security/admin/lib/screen-state.js +120 -0
- package/plugins/security/admin/templates/security.html +15 -10
- package/plugins/security/admin/views/security.js +270 -132
- package/plugins/security/plugin.json +2 -2
- package/plugins/security/tests/screen-state.test.js +69 -0
- package/plugins/shopping-cart/CLAUDE.md +13 -0
- package/plugins/shopping-cart/admin/css/index.css +21 -5
- package/plugins/shopping-cart/admin/lib/drafts.js +174 -0
- package/plugins/shopping-cart/admin/lib/kit.js +136 -28
- package/plugins/shopping-cart/admin/views/orders.js +117 -36
- package/plugins/shopping-cart/admin/views/overview.js +38 -8
- package/plugins/shopping-cart/admin/views/products.js +317 -68
- package/plugins/shopping-cart/admin/views/sales.js +21 -3
- package/plugins/shopping-cart/admin/views/settings.js +62 -3
- package/plugins/shopping-cart/admin/views/shop.js +18 -3
- package/plugins/shopping-cart/plugin.json +3 -3
- package/plugins/shopping-cart/tests/accounts.test.js +11 -0
- package/plugins/shopping-cart/tests/drafts.test.js +118 -0
- package/plugins/support/CLAUDE.md +14 -0
- package/plugins/support/admin/css/index.css +10 -0
- package/plugins/support/admin/lib/drafts.js +162 -0
- package/plugins/support/admin/lib/kit.js +156 -16
- package/plugins/support/admin/lib/settings.js +52 -29
- package/plugins/support/admin/views/desk.js +114 -33
- package/plugins/support/admin/views/inbox.js +85 -26
- package/plugins/support/admin/views/support.js +1 -1
- package/plugins/support/plugin.json +3 -3
- package/plugins/support/tests/drafts.test.js +92 -0
- package/public/js/site.js +1 -1
- package/server/routes/api/analytics.js +5 -1
- package/server/routes/api/contacts.js +42 -0
- package/server/routes/api/forms.js +8 -0
- package/server/routes/api/seo.js +4 -1
- package/server/routes/api/todo.js +50 -10
- package/server/server.js +1 -1
- package/server/templates/page.html +1 -1
|
@@ -75,6 +75,7 @@ import {fmtDateTime} from '../ui/dates.js';
|
|
|
75
75
|
import {applyTemplate, expandTemplate} from './templates.js';
|
|
76
76
|
import {makeResizable} from './resizable.js';
|
|
77
77
|
import {whenClosed} from './when-closed.js';
|
|
78
|
+
import {esc} from './reader-kit.js';
|
|
78
79
|
import {
|
|
79
80
|
collectGroups,
|
|
80
81
|
currentFragment,
|
|
@@ -864,7 +865,9 @@ export function openCompose({
|
|
|
864
865
|
askingSend = true;
|
|
865
866
|
try {
|
|
866
867
|
const q = sendQuestion({...collect(), sendAt: scheduledAt.peek(), undoSeconds});
|
|
867
|
-
|
|
868
|
+
// The subject and the recipients' names are typed text, and
|
|
869
|
+
// E.confirm renders HTML - escaped, or `<b>` in a subject is bold.
|
|
870
|
+
const ok = await E.confirm(esc(q.text), {title: esc(q.title), confirmText: q.confirmText, cancelText: 'Keep writing'});
|
|
868
871
|
if (ok && !closed && !cannotSend.peek()) await send();
|
|
869
872
|
} finally {
|
|
870
873
|
askingSend = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
#view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface, #fff)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border, #e2e8f0);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border, #e2e8f0);color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted, #64748b);border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary, #2563eb);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text, #fff);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text, #fff) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg, rgba(15, 23, 42, .07));box-shadow:inset 3px 0 0 var(--dm-primary, #2563eb)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg, rgba(15, 23, 42, .07));border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border, #e2e8f0);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary, #2563eb)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface, #fff);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted, #64748b)}.mail-account:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-account.is-active{color:var(--dm-text, #1e293b);position:sticky;top:0;z-index:1;background:var(--dm-surface, #fff)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text, #1e293b)}.mail-folder:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-folder.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07));color:var(--dm-primary, #2563eb);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border, #e2e8f0);background:none;cursor:pointer;color:var(--dm-text, #1e293b)}.mail-item:hover{background:var(--dm-hover-bg, rgba(15, 23, 42, .04))}.mail-item.is-active{background:var(--dm-active-bg, rgba(15, 23, 42, .07))}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted, #64748b)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border, #e2e8f0);font-size:.75rem;color:var(--dm-text-muted, #64748b);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border, #e2e8f0);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text, #1e293b)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted, #64748b)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface, #fff)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning, #8a6d1f);color:var(--dm-warning-text, #fff);border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-notice .btn{color:var(--dm-warning-text, #fff);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text, #fff) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border, #e2e8f0);background:var(--dm-background-alt, rgba(15, 23, 42, .03))}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted, #94a3b8);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 6px)}.mail-default-badge{margin-left:.4rem;font-size:.7rem;font-weight:500;vertical-align:middle}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff);outline:2px solid var(--dm-primary, #2563eb);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted, #64748b);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary, #2563eb)}.mail-item-tick:hover{color:var(--dm-primary, #2563eb);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-replied{color:var(--dm-success)}.mail-item-meta .is-forwarded{color:var(--dm-info, var(--dm-primary))}.mail-item-meta .is-high{color:var(--dm-danger, #dc2626)}.mail-item-meta .is-low{color:var(--dm-text-muted, #64748b);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted, #64748b);background:var(--dm-active-bg, rgba(15, 23, 42, .07));cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary, #2563eb)}.mail-thread-chip.is-unread{background:var(--dm-primary, #2563eb);color:var(--dm-primary-text, #fff)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border, #e2e8f0)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary, #2563eb);background:var(--dm-active-bg, rgba(15, 23, 42, .07))}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger, #dc2626)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border, #e2e8f0);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border, #e2e8f0)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary, #2563eb)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted, #64748b)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted, #64748b);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary, #2563eb)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border, #e2e8f0)}.mail-pager-middle{display:flex;align-items:center;gap:.5rem;min-width:0}.mail-page-size{font:inherit;color:inherit;background:transparent;border:1px solid var(--dm-border, #e2e8f0);border-radius:4px;padding:.1rem .25rem}.mail-auth-choice{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-oauth{display:flex;flex-direction:column;gap:.75rem}.mail-oauth-warn{margin:0;padding:.5rem .7rem;border:1px solid var(--dm-warning);border-radius:var(--dm-radius-md);color:var(--dm-warning-text)}.mail-oauth-state{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.mail-oauth-chip{margin-left:.4rem;border:1px solid var(--dm-border);background:var(--dm-surface);color:var(--dm-text-muted)}.mail-oauth-state .mail-oauth-chip{margin-left:0}.mail-oauth-chip--ok{border-color:var(--dm-success);color:var(--dm-success)}.mail-oauth-chip--bad{border-color:var(--dm-danger);color:var(--dm-danger)}.mail-oauth-guide{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);padding:.5rem .75rem;background:var(--dm-background-alt)}.mail-oauth-guide>summary{cursor:pointer;font-weight:600}.mail-oauth-guide-body ol{padding-left:1.2rem;margin:.5rem 0 0}.mail-oauth-guide-body li{margin-bottom:.45rem}.mail-oauth-code{word-break:break-all;user-select:all}
|
|
1
|
+
#view-container.mail-fullbleed{max-width:none}.mail-layout [hidden],.dm-slideover [hidden]{display:none!important}.mail-reader-summary{display:flex;align-items:baseline;gap:.5rem;flex-wrap:wrap}.mail-header-toggle,.mail-extras-toggle{padding:0!important;font-size:.78rem;opacity:.75;text-decoration:underline}.mail-header-toggle:hover,.mail-extras-toggle:hover{opacity:1}.mail-header-details{display:grid;grid-template-columns:max-content 1fr;gap:.15rem .75rem;margin:.5rem 0 0;font-size:.8rem}.mail-header-details dt{opacity:.65}.mail-header-details dd{margin:0;overflow-wrap:anywhere}.mail-layout{display:grid;grid-template-columns:var(--mail-col-1, 190px) auto var(--mail-col-2, 340px) auto 1fr;height:calc(100vh - 140px);min-height:320px;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 8px);overflow:hidden;background:var(--dm-surface)}.mail-folders-pane,.mail-list-pane{display:flex;flex-direction:column;border-right:1px solid var(--dm-border);min-width:0;min-height:0}.mail-reader-pane{display:flex;flex-direction:column;min-width:0;min-height:0}.mail-pane-head{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;font-size:.8rem;font-weight:600;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-folders-list,.mail-list{overflow-y:auto;flex:1;min-height:0}.mail-search{display:flex;align-items:center;gap:.4rem;padding:.4rem .6rem;border-bottom:1px solid var(--dm-border);color:var(--dm-text-muted);flex-shrink:0}.mail-search input{flex:1;min-width:0;font-size:.85rem;padding:.25rem .4rem}.mail-search-note{padding:.3rem .6rem;font-size:.74rem;color:var(--dm-text-muted);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-slideover-resizer{padding:0;position:absolute;left:0;top:0;bottom:0;width:6px;cursor:col-resize;z-index:2;background:transparent}.mail-slideover-resizer:hover,.is-resizing>.mail-slideover-resizer{background:var(--dm-primary);opacity:.7}.is-resizing{user-select:none;transition:none!important}.mail-selection{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap;padding:.35rem .6rem;font-size:.78rem;background:var(--dm-primary);color:var(--dm-primary-text);flex-shrink:0}.mail-selection span{flex:1;min-width:0}.mail-selection .btn{color:var(--dm-primary-text);border:1px solid currentColor;background:transparent}.mail-selection .btn:hover{background:color-mix(in srgb,var(--dm-primary-text) 15%,transparent)}.mail-item.is-selected{background:var(--dm-active-bg);box-shadow:inset 3px 0 0 var(--dm-primary)}.mail-undo{display:flex;align-items:center;gap:.5rem;padding:.4rem .6rem;font-size:.78rem;background:var(--dm-active-bg);border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-undo span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-splitter{width:5px;cursor:col-resize;background:var(--dm-border);flex-shrink:0;background-clip:content-box;border-left:2px solid transparent;border-right:2px solid transparent;box-sizing:content-box}.mail-splitter:hover,.mail-layout.is-resizing .mail-splitter{background-color:var(--dm-primary)}.mail-layout.is-resizing{user-select:none}.mail-layout.is-resizing .mail-folders-pane,.mail-layout.is-resizing .mail-list-pane,.mail-layout.is-resizing .mail-reader-pane,.mail-layout.is-resizing iframe{pointer-events:none}.mail-unread-filter{display:flex;align-items:center;gap:.3rem;font-size:.7rem;text-transform:none;letter-spacing:0;cursor:pointer;margin-left:auto;margin-right:.25rem}.mail-unread-pill{flex-shrink:0;min-width:1.15rem;padding:0 .3rem;border-radius:999px;background:var(--dm-primary);color:var(--dm-primary-text);font-size:.66rem;font-weight:700;line-height:1.35;text-align:center}.mail-folder.is-context .mail-folder-name{opacity:.6}.mail-account{display:flex;align-items:center;gap:.4rem;width:100%;padding:.45rem .6rem;border:none;background:var(--dm-surface);cursor:pointer;text-align:left;font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--dm-text-muted)}.mail-account:hover{background:var(--dm-hover-bg)}.mail-account.is-active{color:var(--dm-text);position:sticky;top:0;z-index:1;background:var(--dm-surface)}.mail-account-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-folder{display:flex;align-items:center;gap:.5rem;padding:.45rem .75rem;font-size:.875rem;cursor:pointer;border:none;background:none;width:100%;text-align:left;color:var(--dm-text)}.mail-folder:hover{background:var(--dm-hover-bg)}.mail-folder.is-active{background:var(--dm-active-bg);color:var(--dm-primary);font-weight:600}.mail-folder.is-unselectable{opacity:.6;cursor:default}.mail-folder.is-unselectable:hover{background:none}.mail-folder-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item{display:block;width:100%;text-align:left;padding:.55rem .75rem;border:none;border-bottom:1px solid var(--dm-border);background:none;cursor:pointer;color:var(--dm-text)}.mail-item:hover{background:var(--dm-hover-bg)}.mail-item.is-active{background:var(--dm-active-bg)}.mail-item-top{display:flex;justify-content:space-between;gap:.5rem;align-items:baseline}.mail-item-from{font-size:.85rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item.is-unread .mail-item-from,.mail-item.is-unread .mail-item-subject{font-weight:700}.mail-item-date{font-size:.72rem;color:var(--dm-text-muted);flex-shrink:0}.mail-item-subject{font-size:.82rem;margin-top:.1rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.mail-item-meta{display:flex;align-items:center;gap:.35rem;margin-top:.15rem;color:var(--dm-text-muted)}.mail-pager{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.4rem .6rem;border-top:1px solid var(--dm-border);font-size:.75rem;color:var(--dm-text-muted);flex-shrink:0}.mail-reader-head{padding:.75rem 1rem;border-bottom:1px solid var(--dm-border);flex-shrink:0}.mail-reader-head:empty{display:none}.mail-reader-subject{font-size:1.05rem;font-weight:600;margin:0 0 .3rem;color:var(--dm-text)}.mail-reader-actions{display:flex;gap:.25rem;margin:.1rem 0 .35rem;flex-wrap:wrap}.mail-reader-meta{font-size:.8rem;color:var(--dm-text-muted)}.mail-reader-body{flex:1;position:relative;overflow:hidden;display:flex;flex-direction:column;min-height:0;background:var(--dm-surface)}.mail-reader-body iframe{width:100%;flex:1;border:none;display:block;background:#fff}.mail-notice{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;padding:.4rem .75rem;font-size:.78rem;background:var(--dm-warning);color:var(--dm-warning-text);border-bottom:1px solid var(--dm-border)}.mail-notice .btn{color:var(--dm-warning-text);border:1px solid currentColor;background:transparent;opacity:.95}.mail-notice .btn:hover{background:color-mix(in srgb,var(--dm-warning-text) 15%,transparent);opacity:1}.mail-attachments{display:flex;flex-wrap:wrap;gap:.4rem;padding:.5rem .75rem;border-bottom:1px solid var(--dm-border);background:var(--dm-background-alt)}.mail-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;height:100%;color:var(--dm-text-muted);text-align:center;padding:1rem}.mail-empty p{margin:0;font-size:.875rem}@media(max-width:900px){.mail-layout{grid-template-columns:1fr;height:auto!important}.mail-splitter{display:none}.mail-folders-pane,.mail-list-pane{border-right:none;border-bottom:1px solid var(--dm-border)}.mail-folders-list,.mail-list{max-height:240px}.mail-reader-body{min-height:60vh}}.mail-box-row{display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px)}.mail-default-badge{margin-left:.4rem;font-size:.7rem;font-weight:500;vertical-align:middle}.mail-item.is-dragging{opacity:.45}.mail-folder.is-drop-target{background:var(--dm-primary);color:var(--dm-primary-text);outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-folder.is-drop-target .mail-folder-name,.mail-folder.is-drop-target .mail-unread-pill{color:inherit}.mail-item.is-dragging *{pointer-events:none}.mail-item{position:relative}.mail-item-tick{position:absolute;left:.45rem;top:.55rem;display:inline-flex;opacity:0;color:var(--dm-text-muted);transition:opacity .12s ease;padding:2px;margin:-2px;border-radius:var(--dm-radius-sm, 3px)}.mail-item:hover .mail-item-tick,.mail-item:focus-visible .mail-item-tick,.mail-list.is-selecting .mail-item-tick,.mail-item-tick[aria-checked=true]{opacity:1}.mail-item-tick[aria-checked=true]{color:var(--dm-primary)}.mail-item-tick:hover{color:var(--dm-primary);background:var(--dm-hover-bg)}.mail-item-top,.mail-item-subject,.mail-item-meta{padding-left:1.5rem}.mail-select-all{display:inline-flex;align-items:center;margin-right:.1rem;cursor:pointer}.mail-select-all input{cursor:pointer;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.mail-item-meta>span{display:inline-flex}.mail-item-meta .is-replied{color:var(--dm-success)}.mail-item-meta .is-forwarded{color:var(--dm-info, var(--dm-primary))}.mail-item-meta .is-high{color:var(--dm-danger)}.mail-item-meta .is-low{color:var(--dm-text-muted);opacity:.7}.mail-folder.is-hidden-folder .mail-folder-name{opacity:.5;font-style:italic}.mail-thread-chip{display:inline-flex;align-items:center;margin-left:.4rem;padding:0 .35rem;border-radius:999px;font-size:.68rem;font-weight:700;line-height:1.5;color:var(--dm-text-muted);background:var(--dm-active-bg);cursor:pointer}.mail-thread-chip:hover{color:var(--dm-primary)}.mail-thread-chip.is-unread{background:var(--dm-primary);color:var(--dm-primary-text)}.mail-item.is-in-thread{padding-left:1.6rem;box-shadow:inset 3px 0 0 var(--dm-border)}.mail-item.is-in-thread .mail-item-from{font-size:.8rem}.mail-pane-head .btn.is-on{color:var(--dm-primary);background:var(--dm-active-bg)}#mail-outbox-btn{font-size:.72rem;font-weight:700}#mail-outbox-btn.is-failed{color:var(--dm-danger)}.mail-attachment{display:inline-flex;align-items:center;border:1px solid var(--dm-border);border-radius:var(--dm-radius-md, 6px);overflow:hidden}.mail-attachment .btn{border:none;border-radius:0}.mail-attachment-save{padding-left:.4rem!important;padding-right:.4rem!important;border-left:1px solid var(--dm-border)!important;opacity:.7}.mail-attachment-save:hover{opacity:1;color:var(--dm-primary)}.mail-section-heading{margin:1rem 0 .1rem;font-size:.8rem;text-transform:uppercase;letter-spacing:.04em;color:var(--dm-text-muted)}.mail-section-heading:first-child{margin-top:.25rem}.mail-section-form .domma-form{margin:0}.mail-section-form .domma-form-field:last-child{margin-bottom:0}.mail-item-card{margin:0}.mail-item-card .card-header{padding:.5rem .6rem}.mail-item-card .card-body{padding:.6rem}.mail-ellipsis{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.mail-status{min-height:1.2em}.mail-empty-line{margin:.25rem 0}.mail-editor-fallback{min-height:8rem;font-family:inherit;resize:vertical}.mail-clause{display:flex;gap:.3rem;align-items:center;flex-wrap:wrap}.mail-clause .form-select,.mail-clause .form-input{width:auto;flex:0 1 auto}.mail-clause .mail-clause-value{flex:1 1 8rem;min-width:8rem}.mail-section-list{display:flex;flex-direction:column;gap:.5rem}.mail-scroll-sm{max-height:20vh;overflow-y:auto}.mail-scroll-md{max-height:46vh;overflow-y:auto}.mail-section-form .domma-form-field:has(select) .form-label .text-danger{display:none}.mail-compose-form{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.6rem;height:100%}.mail-compose-body{display:flex;flex-direction:column;flex:1;min-height:0}.mail-compose-editor{flex:1;min-height:0}.mail-compose-textarea{flex:1;min-height:34vh;font-family:inherit;resize:vertical}.mail-attach-list{display:flex;flex-direction:column;gap:.2rem}.mail-template-row{margin:-.15rem 0 .35rem}.mail-template-picker{max-width:16rem}.mail-suggest-row{display:flex;flex-direction:column;line-height:1.25}.mail-suggest-detail{opacity:.7}.mail-rule-off{opacity:.55}.mail-panel{padding:.25rem 0 .5rem}.mail-panel-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:1rem}.mail-form-stack{padding:.25rem 0 .5rem;display:flex;flex-direction:column;gap:.75rem}.mail-dialog-stack{display:flex;flex-direction:column;gap:.5rem;padding:.25rem 0}.mail-hint{margin-top:.2rem}.mail-box-sub{overflow:hidden;text-overflow:ellipsis}.mail-add-btn{align-self:flex-start;margin-top:.5rem}.mail-none{margin:0;padding:.5rem .6rem}.mail-kv{padding:.5rem 0;display:flex;flex-direction:column;gap:.35rem}.mail-kv-row{display:flex;gap:.75rem}.mail-kv-key{min-width:8.5rem;flex-shrink:0}.mail-toggle-row{display:flex;align-items:center;gap:.5rem;margin-bottom:.75rem}.mail-sender-list{display:flex;flex-direction:column;gap:.2rem;max-height:40vh;overflow-y:auto}.mail-folder-pick{display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:var(--dm-radius-md, 6px)}.mail-diag-log{width:100%;height:30vh;margin-top:.5rem;font-family:monospace;font-size:.72rem}.mail-facts{display:flex;flex-direction:column;gap:.2rem;margin-bottom:.75rem}.mail-item-more{position:absolute;right:.4rem;bottom:.35rem;display:inline-flex;align-items:center;justify-content:center;min-width:1.4rem;height:1.4rem;border-radius:var(--dm-radius-sm, 3px);font-size:1rem;line-height:1;color:var(--dm-text-muted);background:inherit;opacity:0;transition:opacity .12s ease}.mail-item:hover .mail-item-more,.mail-item:focus-visible .mail-item-more,.mail-item.is-active .mail-item-more{opacity:1}.mail-item-more:hover{color:var(--dm-primary)}.mail-section-intro{display:flex;align-items:center;gap:.35rem;margin:0 0 .75rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-pager-middle{display:flex;align-items:center;gap:.5rem;min-width:0}.mail-page-size{font:inherit;color:inherit;background:transparent;border:1px solid var(--dm-border);border-radius:4px;padding:.1rem .25rem}.mail-auth-choice{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;padding-bottom:.5rem;border-bottom:1px solid var(--dm-border)}.mail-oauth{display:flex;flex-direction:column;gap:.75rem}.mail-oauth-warn{margin:0;padding:.5rem .7rem;border:1px solid var(--dm-warning);border-radius:var(--dm-radius-md);color:var(--dm-warning-text)}.mail-oauth-state{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.mail-oauth-chip{margin-left:.4rem;border:1px solid var(--dm-border);background:var(--dm-surface);color:var(--dm-text-muted)}.mail-oauth-state .mail-oauth-chip{margin-left:0}.mail-oauth-chip--ok{border-color:var(--dm-success);color:var(--dm-success)}.mail-oauth-chip--bad{border-color:var(--dm-danger);color:var(--dm-danger)}.mail-oauth-guide{border:1px solid var(--dm-border);border-radius:var(--dm-radius-md);padding:.5rem .75rem;background:var(--dm-background-alt)}.mail-oauth-guide>summary{cursor:pointer;font-weight:600}.mail-oauth-guide-body ol{padding-left:1.2rem;margin:.5rem 0 0}.mail-oauth-guide-body li{margin-bottom:.45rem}.mail-oauth-code{word-break:break-all;user-select:all}.mail-loading-slot{display:block;min-height:3rem;padding:.5rem .6rem}.mail-row-more{flex-shrink:0;margin-left:auto;display:inline-flex;align-items:center;justify-content:center;min-width:1.3rem;height:1.3rem;border-radius:var(--dm-radius-sm);font-size:.95rem;line-height:1;color:var(--dm-text-muted);opacity:0;transition:opacity .12s ease}.mail-unread-pill+.mail-row-more{margin-left:0}.mail-folder:hover .mail-row-more,.mail-folder:focus-visible .mail-row-more,.mail-folder.is-active .mail-row-more,.mail-account:hover .mail-row-more,.mail-account:focus-visible .mail-row-more,.mail-account.is-active .mail-row-more{opacity:1}.mail-row-more:hover{color:var(--dm-primary)}.mail-box-row .mail-row-more{opacity:1;margin-left:0}.mail-box-row:focus-visible{outline:2px solid var(--dm-primary);outline-offset:-2px}.mail-restored-note{margin:0;padding:.4rem .6rem;border-left:3px solid var(--dm-warning);background:var(--dm-background-alt)}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reader view's decisions that need no DOM - escaping, the mailbox form's
|
|
3
|
+
* kept draft, the once-only claim a panel takes, and what the folder pane's
|
|
4
|
+
* menus offer. Pure, so `tests/services/mail-reader-kit.test.js` can hold
|
|
5
|
+
* them still; `reader-view.js` does the drawing.
|
|
6
|
+
*
|
|
7
|
+
* @module _lib/admin/mail/reader-kit
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Escape text for HTML. E.confirm and E.prompt put their message through
|
|
12
|
+
* Domma's sanitiser, which keeps markup it likes - so a subject reading
|
|
13
|
+
* `<b>Sale</b>` came out bold, and `<img ...>` came out as an image. Anything
|
|
14
|
+
* a person or a stranger typed goes through this first. NOT for E.toast,
|
|
15
|
+
* which sets textContent and would show `"` literally.
|
|
16
|
+
*
|
|
17
|
+
* @param {*} value
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function esc(value) {
|
|
21
|
+
return String(value ?? '')
|
|
22
|
+
.replace(/&/g, '&')
|
|
23
|
+
.replace(/</g, '<')
|
|
24
|
+
.replace(/>/g, '>')
|
|
25
|
+
.replace(/"/g, '"')
|
|
26
|
+
.replace(/'/g, ''');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// The mailbox form's draft
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Where unsaved mailbox forms are kept: ONE map, `{[id or 'new']: draft}`, so
|
|
35
|
+
* the drafts for mailboxes since removed can be pruned without enumerating
|
|
36
|
+
* storage. Read fresh and written back whole each time - nothing holds a copy.
|
|
37
|
+
*/
|
|
38
|
+
export const MAILBOX_DRAFTS_KEY = 'domma-mail.mailbox-drafts';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The storage key for ONE admin user's drafts. Browser storage is shared by
|
|
42
|
+
* everyone who signs in on that browser, and mailboxes are per user, so the
|
|
43
|
+
* map is per user too: B is never offered A's half-typed mailbox, and B's
|
|
44
|
+
* prune never touches A's. No user, no key - and no draft kept.
|
|
45
|
+
*
|
|
46
|
+
* @param {string|number|null|undefined} userId
|
|
47
|
+
* @returns {string|null}
|
|
48
|
+
*/
|
|
49
|
+
export function mailboxDraftsKey(userId) {
|
|
50
|
+
const id = userId === null || userId === undefined ? '' : String(userId).trim();
|
|
51
|
+
return id ? `${MAILBOX_DRAFTS_KEY}.${id}` : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The key in that map for one mailbox; a new one has no id yet. */
|
|
55
|
+
export function mailboxDraftId(boxId) {
|
|
56
|
+
return boxId ? String(boxId) : 'new';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The fields a draft keeps. The password is NOT one of them, and never will
|
|
61
|
+
* be: storage is plain text in the browser, readable by anything on the page.
|
|
62
|
+
*/
|
|
63
|
+
export const MAILBOX_DRAFT_FIELDS = Object.freeze(['label', 'host', 'port', 'secure', 'username']);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* What a draft holds, from the form's values (or a stored mailbox).
|
|
67
|
+
*
|
|
68
|
+
* @param {object} values
|
|
69
|
+
* @returns {{label: string, host: string, port: number, secure: boolean, username: string}}
|
|
70
|
+
*/
|
|
71
|
+
export function mailboxDraftData(values = {}) {
|
|
72
|
+
const port = Number(values?.port);
|
|
73
|
+
return {
|
|
74
|
+
label: String(values?.label ?? '').trim(),
|
|
75
|
+
host: String(values?.host ?? '').trim(),
|
|
76
|
+
port: Number.isFinite(port) && port > 0 ? Math.round(port) : 993,
|
|
77
|
+
secure: values?.secure !== false,
|
|
78
|
+
username: String(values?.username ?? '').trim()
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A stable fingerprint of what the form shows for a mailbox - what `was`
|
|
84
|
+
* records, so a later question can say the mailbox changed in the meantime.
|
|
85
|
+
*
|
|
86
|
+
* @param {object|null} box - null for a new mailbox
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
export function mailboxFingerprint(box) {
|
|
90
|
+
return box ? JSON.stringify(mailboxDraftData(box)) : '';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A stored draft, or null if it is not one.
|
|
95
|
+
*
|
|
96
|
+
* @param {*} stored
|
|
97
|
+
* @returns {{data: object, was: string, at: number}|null}
|
|
98
|
+
*/
|
|
99
|
+
export function normaliseMailboxDraft(stored) {
|
|
100
|
+
if (!stored || typeof stored !== 'object' || !stored.data || typeof stored.data !== 'object') return null;
|
|
101
|
+
return {
|
|
102
|
+
data: mailboxDraftData(stored.data),
|
|
103
|
+
was: typeof stored.was === 'string' ? stored.was : '',
|
|
104
|
+
at: Number.isFinite(stored.at) ? stored.at : 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read the drafts map, tolerating anything stored badly.
|
|
110
|
+
*
|
|
111
|
+
* @param {*} stored
|
|
112
|
+
* @returns {Object.<string, object>}
|
|
113
|
+
*/
|
|
114
|
+
export function normaliseDraftMap(stored) {
|
|
115
|
+
if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return {};
|
|
116
|
+
const out = {};
|
|
117
|
+
for (const [id, value] of Object.entries(stored)) {
|
|
118
|
+
const draft = normaliseMailboxDraft(value);
|
|
119
|
+
if (draft) out[id] = draft;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The map without drafts for mailboxes that no longer exist: there is nothing
|
|
126
|
+
* left to restore them into. The `new` draft always stays.
|
|
127
|
+
*
|
|
128
|
+
* @param {Object.<string, object>} map
|
|
129
|
+
* @param {string[]} ids - the mailboxes that exist
|
|
130
|
+
* @returns {Object.<string, object>}
|
|
131
|
+
*/
|
|
132
|
+
export function pruneDraftMap(map, ids) {
|
|
133
|
+
const keep = new Set(ids.map(String));
|
|
134
|
+
return Object.fromEntries(Object.entries(map ?? {}).filter(([id]) => id === 'new' || keep.has(id)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* What to do with a kept draft when the form opens.
|
|
139
|
+
*
|
|
140
|
+
* none - there is none
|
|
141
|
+
* drop - it says nothing the mailbox does not already (remove it quietly)
|
|
142
|
+
* ask - it holds changes; `changed` = the mailbox moved since it was kept
|
|
143
|
+
*
|
|
144
|
+
* @param {object|null} draft - normalised
|
|
145
|
+
* @param {object|null} box - the stored mailbox, null for a new one
|
|
146
|
+
* @returns {{kind: 'none'|'drop'|'ask', changed?: boolean}}
|
|
147
|
+
*/
|
|
148
|
+
export function mailboxDraftDecision(draft, box) {
|
|
149
|
+
if (!draft) return {kind: 'none'};
|
|
150
|
+
const now = mailboxFingerprint(box);
|
|
151
|
+
const data = JSON.stringify(draft.data);
|
|
152
|
+
if (data === (box ? now : JSON.stringify(mailboxDraftData({})))) return {kind: 'drop'};
|
|
153
|
+
return {kind: 'ask', changed: Boolean(box) && draft.was !== '' && draft.was !== now};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* How long ago, in words.
|
|
158
|
+
*
|
|
159
|
+
* @param {number} at - ms
|
|
160
|
+
* @param {number} now - ms
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
export function describeAgo(at, now = Date.now()) {
|
|
164
|
+
if (!at) return 'earlier';
|
|
165
|
+
const minutes = Math.round((now - at) / 60_000);
|
|
166
|
+
if (minutes < 1) return 'just now';
|
|
167
|
+
if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
|
|
168
|
+
const hours = Math.round(minutes / 60);
|
|
169
|
+
if (hours < 24) return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
|
170
|
+
const days = Math.round(hours / 24);
|
|
171
|
+
return `${days} day${days === 1 ? '' : 's'} ago`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The "unsaved changes from last time" question - asked so that OK throws
|
|
176
|
+
* them away and Cancel, Escape and the backdrop (which E.confirm cannot tell
|
|
177
|
+
* apart) all KEEP them. Plain text: the caller escapes it.
|
|
178
|
+
*
|
|
179
|
+
* @param {{label?: string, at?: number, changed?: boolean, isNew?: boolean, now?: number}} o
|
|
180
|
+
* @returns {string}
|
|
181
|
+
*/
|
|
182
|
+
export function mailboxRestoreQuestion({label = '', at = 0, changed = false, isNew = false, now = Date.now()} = {}) {
|
|
183
|
+
const what = isNew ? 'the new mailbox you were adding' : `the mailbox "${label || 'Mailbox'}"`;
|
|
184
|
+
return `You left changes to ${what} unsaved (${describeAgo(at, now)}). `
|
|
185
|
+
+ (changed ? 'The mailbox has been changed since then. ' : '')
|
|
186
|
+
+ 'Throw away the changes you left unsaved? Keep them to carry on where you left off.';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The buttons for that question: OK discards, Cancel keeps. */
|
|
190
|
+
export const RESTORE_BUTTONS = Object.freeze({title: 'Unsaved changes', confirmText: 'Throw away', cancelText: 'Keep them'});
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The question asked when the settings panel closes over sections with
|
|
194
|
+
* unsaved work (`ctx.guard`). Plain text: the caller escapes it.
|
|
195
|
+
*
|
|
196
|
+
* @param {string[]} labels
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
export function settingsDiscardQuestion(labels = []) {
|
|
200
|
+
const names = [...new Set(labels.filter(Boolean).map(String))];
|
|
201
|
+
if (!names.length) return 'Discard your unsaved changes?';
|
|
202
|
+
const list = names.length === 1 ? names[0]
|
|
203
|
+
: `${names.slice(0, -1).join(', ')} and ${names.at(-1)}`;
|
|
204
|
+
return `Discard your unsaved changes in ${list}?`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Once-only panels
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A claim a panel takes so a second click (or a key repeat) cannot open a
|
|
213
|
+
* second copy. Released when the panel closes - and by the caller on any
|
|
214
|
+
* error, or it could never open again until a reload.
|
|
215
|
+
*
|
|
216
|
+
* @returns {{claim: () => boolean, release: () => void, readonly busy: boolean}}
|
|
217
|
+
*/
|
|
218
|
+
export function onceOnly() {
|
|
219
|
+
let busy = false;
|
|
220
|
+
return {
|
|
221
|
+
claim() {
|
|
222
|
+
if (busy) return false;
|
|
223
|
+
busy = true;
|
|
224
|
+
return true;
|
|
225
|
+
},
|
|
226
|
+
release() { busy = false; },
|
|
227
|
+
get busy() { return busy; }
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// The folder pane's menus
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* What a mailbox row's menu offers, as ids the view maps to actions.
|
|
237
|
+
*
|
|
238
|
+
* @param {{isCurrent: boolean, isDefault: boolean, count: number, canWrite?: boolean}} o
|
|
239
|
+
* @returns {{id: string, label: string, icon: string, disabled?: boolean, danger?: boolean}[]|object[]}
|
|
240
|
+
*/
|
|
241
|
+
export function accountMenuModel({isCurrent = false, isDefault = false, count = 1} = {}) {
|
|
242
|
+
return [
|
|
243
|
+
{id: 'open', label: isCurrent ? 'Open (already open)' : 'Open', icon: 'inbox', disabled: isCurrent},
|
|
244
|
+
{id: 'refresh', label: 'Check for new mail', icon: 'refresh-cw', disabled: !isCurrent},
|
|
245
|
+
{type: 'divider'},
|
|
246
|
+
{id: 'edit', label: 'Edit…', icon: 'edit-3'},
|
|
247
|
+
...(count > 1 ? [{id: 'default', label: isDefault ? 'Unset default' : 'Make default', icon: 'star'}] : []),
|
|
248
|
+
{type: 'divider'},
|
|
249
|
+
{id: 'remove', label: 'Remove…', icon: 'trash', danger: true}
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* The read-only part of a folder row's menu - every edition gets it; the
|
|
255
|
+
* edition that can manage folders adds its own items below.
|
|
256
|
+
*
|
|
257
|
+
* @param {{selectable: boolean, hasChildren: boolean, collapsed: boolean, isCurrent: boolean}} o
|
|
258
|
+
* @returns {object[]}
|
|
259
|
+
*/
|
|
260
|
+
export function folderReadMenuModel({selectable = true, hasChildren = false, collapsed = false, isCurrent = false} = {}) {
|
|
261
|
+
return [
|
|
262
|
+
{id: 'open', label: isCurrent ? 'Open (already open)' : 'Open', icon: 'folder', disabled: !selectable || isCurrent},
|
|
263
|
+
...(hasChildren ? [{id: 'fold', label: collapsed ? 'Show folders inside' : 'Hide folders inside', icon: collapsed ? 'chevron-down' : 'chevron-right'}] : []),
|
|
264
|
+
{id: 'copy', label: 'Copy folder path', icon: 'copy'}
|
|
265
|
+
];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* What the mailbox list in Settings offers per row.
|
|
270
|
+
*
|
|
271
|
+
* @param {{isDefault: boolean, count: number}} o
|
|
272
|
+
* @returns {object[]}
|
|
273
|
+
*/
|
|
274
|
+
export function mailboxRowMenuModel({isDefault = false, count = 1} = {}) {
|
|
275
|
+
return [
|
|
276
|
+
{id: 'edit', label: 'Edit…', icon: 'edit-3'},
|
|
277
|
+
{id: 'open', label: 'Open in the reader', icon: 'inbox'},
|
|
278
|
+
...(count > 1 ? [{id: 'default', label: isDefault ? 'Unset default' : 'Make default', icon: 'star'}] : []),
|
|
279
|
+
{type: 'divider'},
|
|
280
|
+
{id: 'remove', label: 'Remove…', icon: 'trash', danger: true}
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Turn a model into Domma context-menu items.
|
|
286
|
+
*
|
|
287
|
+
* @param {object[]} model
|
|
288
|
+
* @param {Object.<string, Function>} actions - by id; an id without one is left out
|
|
289
|
+
* @returns {object[]}
|
|
290
|
+
*/
|
|
291
|
+
export function menuItems(model, actions) {
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const entry of model) {
|
|
294
|
+
if (entry.type === 'divider') {
|
|
295
|
+
if (out.length && out.at(-1).type !== 'divider') out.push({type: 'divider'});
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const run = actions[entry.id];
|
|
299
|
+
if (typeof run !== 'function') continue;
|
|
300
|
+
out.push({
|
|
301
|
+
label: entry.label,
|
|
302
|
+
icon: entry.icon,
|
|
303
|
+
...(entry.danger ? {danger: true} : {}),
|
|
304
|
+
...(entry.disabled ? {disabled: () => true} : {}),
|
|
305
|
+
action: run
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
while (out.length && out.at(-1).type === 'divider') out.pop();
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The sidebar url of a mail plugin's screen - what `dm:sidebar-count` names.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} pluginName
|
|
316
|
+
* @returns {string}
|
|
317
|
+
*/
|
|
318
|
+
export function sidebarUrl(pluginName) {
|
|
319
|
+
return `#/plugins/${pluginName}`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Did a refresh bring new mail? Compares a folder's newest UIDs and total.
|
|
324
|
+
*
|
|
325
|
+
* @param {{total: number, uids: number[]}|null} before
|
|
326
|
+
* @param {{total: number, uids: number[]}} after
|
|
327
|
+
* @returns {boolean}
|
|
328
|
+
*/
|
|
329
|
+
export function hasNewMail(before, after) {
|
|
330
|
+
if (!before || !after) return false;
|
|
331
|
+
const seen = new Set(before.uids ?? []);
|
|
332
|
+
return (after.uids ?? []).some(uid => !seen.has(uid)) || (after.total ?? 0) > (before.total ?? 0);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Stop a slideover closing on Escape while something above it answers Escape
|
|
337
|
+
* itself - a second panel, a question, a modal. Domma's slideover listens for
|
|
338
|
+
* Escape on the document, so without this one key press closed the mailbox
|
|
339
|
+
* form AND the Settings panel under it (and a question's Escape closed the
|
|
340
|
+
* panel it was asked over).
|
|
341
|
+
*
|
|
342
|
+
* Holds are COUNTED: the first records the panel's own setting and the last
|
|
343
|
+
* release puts it back. The counter lives on the panel under the name Email
|
|
344
|
+
* Pro's sections already use (`__emailProEscape`, its drafts.js), so a hold
|
|
345
|
+
* taken there and one taken here add up rather than each restoring the
|
|
346
|
+
* other's "off" as the original.
|
|
347
|
+
*
|
|
348
|
+
* @param {object} so - an E.slideover instance
|
|
349
|
+
* @returns {() => void} release (idempotent)
|
|
350
|
+
*/
|
|
351
|
+
export function holdEscapeOn(so) {
|
|
352
|
+
if (!so?.options) return () => {};
|
|
353
|
+
const hold = so.__emailProEscape ?? (so.__emailProEscape = {count: 0, original: undefined});
|
|
354
|
+
if (hold.count === 0) hold.original = so.options.closeOnEscape;
|
|
355
|
+
hold.count += 1;
|
|
356
|
+
so.options.closeOnEscape = false;
|
|
357
|
+
let released = false;
|
|
358
|
+
return () => {
|
|
359
|
+
if (released) return;
|
|
360
|
+
released = true;
|
|
361
|
+
hold.count -= 1;
|
|
362
|
+
if (hold.count === 0) so.options.closeOnEscape = hold.original;
|
|
363
|
+
};
|
|
364
|
+
}
|