domma-cms 1.6.59 → 1.7.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/CHANGELOG.md +4344 -0
- package/README.md +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/draft-owner.js +1 -0
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +2 -2
- package/admin/js/lib/page-editor-reference.js +5 -5
- package/admin/js/lib/shortcode-tags.js +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +39 -0
- package/admin/js/views/user-editor.js +1 -1
- package/docs/DommaFeatures.md +3005 -0
- package/docs/MultiTenant.md +176 -0
- package/docs/api-reference.md +1667 -0
- package/docs/cache.md +96 -0
- package/docs/configuration.md +1811 -0
- package/docs/editions-and-licences.md +190 -0
- package/docs/getting-started.md +201 -0
- package/docs/guides/form-triggers.md +260 -0
- package/docs/guides/mongodb-storage-adapter.md +282 -0
- package/docs/guides/multi-page-forms.md +121 -0
- package/docs/images/scribe-composer.png +0 -0
- package/docs/markdown-shortcodes.md +1738 -0
- package/docs/pentest-report-2026-04-03.md +181 -0
- package/docs/plans/2026-02-28-admin-topbar-login-forms.md +349 -0
- package/docs/plans/2026-02-28-domma-ui-polish.md +286 -0
- package/docs/plans/2026-02-28-reset-seed-fresh-commands.md +520 -0
- package/docs/plans/2026-03-02-cookie-consent-design.md +86 -0
- package/docs/plans/2026-03-02-cookie-consent-plugin.md +453 -0
- package/docs/plans/2026-03-02-tooltip-sweep-and-back-to-top-plugin-design.md +126 -0
- package/docs/plans/2026-03-02-tooltip-sweep-back-to-top.md +427 -0
- package/docs/plans/2026-03-05-cookie-consent-btt-core.md +115 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core-design.md +35 -0
- package/docs/plans/2026-03-05-custom-css-migrate-to-core.md +352 -0
- package/docs/plans/2026-03-05-scribe-script-shortcode.md +373 -0
- package/docs/plans/2026-03-05-user-types-design.md +139 -0
- package/docs/plans/2026-03-05-user-types-implementation.md +1035 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts-design.md +78 -0
- package/docs/plans/2026-03-09-page-editor-view-cta-inserts.md +388 -0
- package/docs/plans/2026-03-09-pro-documentation-design.md +80 -0
- package/docs/plans/2026-03-09-pro-documentation.md +1017 -0
- package/docs/plans/2026-06-10-core-project-design.md +240 -0
- package/docs/plugin-development.md +827 -0
- package/docs/recruitment-recipe.md +501 -0
- package/docs/scaffolding.md +312 -0
- package/docs/screenshots/calculator-floating.png +0 -0
- package/docs/screenshots/calculator-standard.png +0 -0
- package/docs/screenshots/email-settings.png +0 -0
- package/docs/screenshots/messages-count.png +0 -0
- package/docs/structure/user-types.md +107 -0
- package/docs/superpowers/plans/2026-05-25-admin-sidebar-via-menus.md +1112 -0
- package/docs/superpowers/plans/2026-06-08-navbar-mobile-drawer-and-menu-enrichment.md +1161 -0
- package/docs/superpowers/plans/2026-06-25-domma-docs-project.md +695 -0
- package/docs/superpowers/plans/2026-06-26-surveys-HANDOFF.md +68 -0
- package/docs/superpowers/plans/2026-06-26-surveys-pro-plugin.md +1902 -0
- package/docs/superpowers/plans/2026-08-19-collection-context-sort-export.md +1577 -0
- package/docs/superpowers/plans/2026-10-09-content-api.md +1192 -0
- package/docs/superpowers/specs/2026-03-12-form-followup-actions-design.md +152 -0
- package/docs/superpowers/specs/2026-03-12-page-editor-form-insert-and-per-page-theme-design.md +115 -0
- package/docs/superpowers/specs/2026-03-17-nav-hidden-items-design.md +85 -0
- package/docs/superpowers/specs/2026-03-19-accordion-insert-design.md +175 -0
- package/docs/superpowers/specs/2026-03-25-static-block-shortcode-design.md +153 -0
- package/docs/superpowers/specs/2026-03-25-text-shortcode-design.md +168 -0
- package/docs/superpowers/specs/2026-04-05-card-builder-design.md +315 -0
- package/docs/superpowers/specs/2026-04-06-insert-builders-design.md +278 -0
- package/docs/superpowers/specs/2026-04-06-layouts-design.md +226 -0
- package/docs/superpowers/specs/2026-04-07-blog-plugin-design.md +300 -0
- package/docs/superpowers/specs/2026-04-07-calendar-plugin-design.md +538 -0
- package/docs/superpowers/specs/2026-04-17-components-area-design.md +368 -0
- package/docs/superpowers/specs/2026-05-02-effects-slideover-overhaul-design.md +193 -0
- package/docs/superpowers/specs/2026-05-06-dashboard-rebuild-design.md +257 -0
- package/docs/superpowers/specs/2026-05-24-menus-design.md +466 -0
- package/docs/superpowers/specs/2026-05-25-admin-sidebar-via-menus-design.md +358 -0
- package/docs/superpowers/specs/2026-05-25-projects-design.md +380 -0
- package/docs/superpowers/specs/2026-06-08-navbar-mobile-drawer-and-menu-enrichment-design.md +292 -0
- package/docs/superpowers/specs/2026-06-08-sidebar-tools-plugins-folders-design.md +59 -0
- package/docs/superpowers/specs/2026-06-25-domma-docs-project-design.md +192 -0
- package/docs/superpowers/specs/2026-06-26-domma-charts-upstream-proposal.md +44 -0
- package/docs/superpowers/specs/2026-06-26-manager-scheduler-upstream-proposal.md +46 -0
- package/docs/superpowers/specs/2026-06-26-surveys-pro-plugin-design.md +206 -0
- package/docs/superpowers/specs/2026-07-02-shortcode-context-menu-design.md +96 -0
- package/docs/superpowers/specs/2026-08-19-collection-context-sort-export-design.md +330 -0
- package/docs/superpowers/specs/2026-09-22-dcms-marketplace-design.md +1150 -0
- package/docs/superpowers/specs/2026-10-09-content-api-design.md +325 -0
- package/docs/superpowers/specs/plans/2026-03-12-form-followup-actions.md +624 -0
- package/docs/superpowers/specs/plans/2026-03-12-page-editor-form-insert-and-per-page-theme.md +125 -0
- package/docs/superpowers/specs/plans/2026-03-17-nav-hidden-items.md +578 -0
- package/docs/superpowers/specs/plans/2026-03-19-accordion-insert.md +635 -0
- package/docs/superpowers/specs/plans/2026-03-25-text-shortcode.md +545 -0
- package/docs/superpowers/specs/plans/2026-04-05-card-builder.md +1970 -0
- package/docs/superpowers/specs/plans/2026-04-06-insert-builders.md +1049 -0
- package/docs/superpowers/specs/plans/2026-04-06-layouts.md +1015 -0
- package/docs/superpowers/specs/plans/2026-04-07-calendar-plugin.md +1526 -0
- package/docs/superpowers/specs/plans/2026-04-08-plugin-monetisation-split.md +242 -0
- package/docs/superpowers/specs/plans/2026-04-17-components-area.md +2038 -0
- package/docs/superpowers/specs/plans/2026-05-02-effects-slideover-overhaul.md +2335 -0
- package/docs/superpowers/specs/plans/2026-05-06-dashboard-rebuild.md +1999 -0
- package/docs/superpowers/specs/plans/2026-05-17-cache-system.md +1436 -0
- package/docs/superpowers/specs/plans/2026-05-24-menus.md +3096 -0
- package/docs/superpowers/specs/plans/2026-05-25-projects.md +2869 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step2-consolidation.md +505 -0
- package/docs/superpowers/specs/plans/2026-09-22-marketplace-step3-catalogue-and-key.md +154 -0
- package/docs/superpowers/specs/plans/2026-09-22-tools-promotion.md +260 -0
- package/docs/theming.md +320 -0
- package/package.json +2 -2
- package/plugins/_lib/admin/mail/keyboard.js +49 -0
- package/plugins/_lib/admin/mail/reader-view.js +4 -2
- package/public/js/celebrations/core/canvas.js +1 -1
- package/public/js/celebrations/index.js +1 -1
- package/public/js/celebrations/licenses/ABeeZee-Regular-OFL.txt +93 -0
- package/public/js/celebrations/licenses/Allura-Regular-OFL.txt +93 -0
- package/public/js/celebrations/message-attrs.mjs +1 -0
- package/public/js/celebrations/themes/anniversary.js +1 -0
- package/public/js/celebrations/themes/autumn.js +1 -0
- package/public/js/celebrations/themes/birthday.js +1 -0
- package/public/js/celebrations/themes/butterfly-flight.js +1 -0
- package/public/js/celebrations/themes/calendar.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year-dates.js +1 -0
- package/public/js/celebrations/themes/chinese-new-year.js +1 -0
- package/public/js/celebrations/themes/christmas.js +1 -1
- package/public/js/celebrations/themes/easter.js +1 -0
- package/public/js/celebrations/themes/garden-drawing.js +1 -0
- package/public/js/celebrations/themes/garden.js +1 -0
- package/public/js/celebrations/themes/graduation.js +1 -0
- package/public/js/celebrations/themes/greeting.js +1 -0
- package/public/js/celebrations/themes/make-it-rain.js +1 -0
- package/public/js/celebrations/themes/money-rain.js +1 -0
- package/public/js/celebrations/themes/new-year.js +1 -0
- package/public/js/celebrations/themes/occasion-drawing.js +1 -0
- package/public/js/celebrations/themes/occasions.js +1 -0
- package/public/js/celebrations/themes/rain-control.js +1 -0
- package/public/js/celebrations/themes/sparkler-font-data.js +1 -0
- package/public/js/celebrations/themes/sparkler-font.js +1 -0
- package/public/js/celebrations/themes/sparkler-writing.js +1 -0
- package/public/js/celebrations/themes/summer-garden.js +1 -0
- package/public/js/celebrations/themes/winter.js +1 -0
- package/public/js/celebrations/trait-data.mjs +1 -1
- package/public/js/effects.js +1 -1
- package/scripts/build.js +4 -0
- package/server/services/markdown.js +5 -2
- package/server/templates/page.html +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{onceClosed as p}from"./tool-kit.js";const u=([o,a,
|
|
1
|
+
import{onceClosed as p}from"./tool-kit.js";const u=([o,a,r])=>r===void 0?{code:o,where:"",text:a}:{code:o,where:a,text:r},e=(o,a,r="")=>({title:o,code:a,note:r||""}),t=(o,{intro:a="",head:r=null,rows:s=[],block:l="",examples:n=[],outro:i=""}={})=>({title:o,intro:a,head:(r||[]).map((d,c)=>({i:c,text:d})),rows:s.map(u),block:l,examples:n,outro:i}),m=[t("Markdown Basics",{head:["Syntax","Result"],rows:[["**bold**","bold (rendered bold)"],["_italic_","italic (rendered italic)"],["## Heading","Heading (h2)"],["[text](url)","Link"],["","Image"],["- item","Bullet list"],["`code`","Inline code"],["---","Divider"]]}),t("Keyboard Shortcuts",{rows:[["Ctrl + /","Toggle a comment around selected text or the current paragraph"],["Ctrl + B","Bold selected text"],["Ctrl + I","Italic selected text"],["Ctrl + K","Insert link dialog"],["Tab","Indent 2 spaces at cursor"],["Shift + Tab","Dedent 2 spaces on current line"],["Ctrl + X","Cut current line (no selection) or cut selection"],["Ctrl + C","Copy current line (no selection) or copy selection"],["Ctrl + V","Paste from clipboard"],["Ctrl + Z","Undo"],["Ctrl + Y","Redo"]]}),t("Tips",{rows:[['class="text-center"',"Centre text and inline elements on any shortcode"],['class="mx-auto"',"Centre a block element horizontally"],["[center]...[/center]","Shorthand wrapper for centred content"]],outro:"Most shortcodes accept a class attribute for custom styling."})],g=[t("Grid & Columns",{rows:[['cols="N"',"[grid] only","Number of columns (1-12)"],['gap="N"',"[grid], [row]","Gap between columns/rows (1-6)"],['span="N"',"[col] only","How many columns this cell spans"],['fullwidth="true"',"[grid] only","Break out of page container to span full viewport"],['class="x"',"all","Add extra CSS classes"]],examples:[e("Equal columns",`[grid cols="3" gap="4"]
|
|
2
2
|
[col]One[/col]
|
|
3
3
|
[col]Two[/col]
|
|
4
4
|
[col]Three[/col]
|
|
@@ -30,7 +30,7 @@ Any content - text, buttons, a grid or a card.
|
|
|
30
30
|
Lifted off the page.
|
|
31
31
|
[/box]`,"Title, icon and image are ignored - a box never has a header. A [card] can go inside a [box], and a [box] inside a [card].")]}),t("Spacer",{intro:"Self-closing shortcode that inserts a fixed-height blank gap.",rows:[['size="N"',"Height in px (default: 8)"],['class="..."',"Extra CSS class on the spacer div"]],examples:[e("32px gap",'[spacer size="32" /]')]}),t("Center",{intro:"Wraps content in a centred div. Works with any content including cards, grids, and text.",examples:[e("Example","[center]Centred content here[/center]",'Accepts an optional class="..." attribute for extra styling.')]}),t("Hero",{intro:"Full-width hero sections - no plugin required. Uses Domma's built-in Hero CSS component.",rows:[['title="..."',"Heading text"],['tagline="..."',"Subtitle text"],['size="sm|lg|full"',"Height variant (default: normal)"],['variant="dark|primary|gradient-blue|gradient-purple|gradient-sunset|gradient-ocean"',"Colour / gradient preset"],['image="url"',"Background image URL (adds cover mode)"],['overlay="light|dark|darker|gradient|gradient-reverse"',"Image overlay style"],['align="center|left"',"Content alignment (default: center)"],['fullwidth="true"',"Break out of the page container to span full viewport width"],['bg="..."',"Background colour CSS value"],["twinkle","Flag: adds particle overlay (requires Effects plugin)"],['twinkle-count="N"',"Number of particles"],['twinkle-colour="..."',"Particle colour CSS value"],["blobs","Flag: adds ambient blob background"],['blobs-type="..."',"Blob animation type (default: float-blobs)"],['class="..."',"Extra CSS classes"],['id="..."',"Element id"]],examples:[e("Basic hero",'[hero title="Welcome" tagline="Build something great"][/hero]'),e("Gradient with body content",`[hero title="Get Started" tagline="Everything you need." size="lg" variant="gradient-blue" align="center"]
|
|
32
32
|
Some introductory **Markdown** content here.
|
|
33
|
-
[/hero]`),e("Background image with overlay",'[hero title="Our Story" image="/media/hero.jpg" overlay="dark" size="full"][/hero]',"Combine image + overlay for text legibility over photos.")]})],
|
|
33
|
+
[/hero]`),e("Background image with overlay",'[hero title="Our Story" image="/media/hero.jpg" overlay="dark" size="full"][/hero]',"Combine image + overlay for text legibility over photos.")]})],h=[t("Cards",{examples:[e("Basic card",`[card title="Optional Title"]
|
|
34
34
|
Markdown **works** here.
|
|
35
35
|
[/card]`,"Omit title for a card with no header."),e("Icon - inline (default)",`[card title="Feature" icon="star"]
|
|
36
36
|
Icon sits left of the title in a flex row.
|
|
@@ -91,7 +91,7 @@ In active development.
|
|
|
91
91
|
[/table]`,"The inner content must be a valid GFM Markdown table. The shortcode adds Domma CSS classes and wraps in a responsive scroll container.")]}),t("Slideover",{intro:"A trigger button that opens a slide-in panel with Markdown content. No plugin required.",rows:[['title="..."',"Panel header text"],['trigger="..."','Button label (default: "Open")'],['size="sm|md|lg"',"Panel width (default: md)"],['position="right|left"',"Slide direction (default: right)"]],examples:[e("Example",`[slideover title="More Info" trigger="Read more" size="md"]
|
|
92
92
|
## Details
|
|
93
93
|
Markdown content here.
|
|
94
|
-
[/slideover]`,"Nested [card] and [grid] shortcodes work inside the slideover body.")]})],b=[t("Embedding a Form",{rows:[['name="..."',"Form slug (required). Also accepted as slug=."],['class="..."',"Extra CSS classes on the wrapper div"],['id="..."',"id attribute on the wrapper div"]],examples:[e("Basic",'[form name="contact" /]',"Forms are managed under Forms in the sidebar."),e("With wrapper styling",'[form name="newsletter" class="mt-4" id="newsletter-form" /]')]}),t("Displaying a Collection",{block:'[collection slug="enquiries" display="table" /]',rows:[["slug","Required. The collection slug."],["display","table (default), cards, or list."],["fields","Comma-separated field keys to show. Defaults to all."],["limit","Maximum number of entries to display."],["sort","Field key to sort by."],["order","asc or desc (default asc)."],["title-field","Field used as the card/list title (cards + list)."],["columns","Grid columns for cards display (2-4, default 3)."],["search","true/false - enable search in table mode (default true)."],["sortable","true/false - enable column sorting in table mode (default true)."],["exportable","true/false - show CSV export button in table mode (default false)."],["page-size","Rows per page in table mode (default 25)."],["empty","Text shown when there are no entries."],["cta","Action slug: enables per-entry CTA buttons."],["cta-label",'CTA button label (default: "Run").'],["cta-icon","CTA button icon name."],["cta-style","CTA button variant (default: primary)."],["cta-confirm","CTA confirmation prompt text."],["class","Extra CSS classes on the wrapper div."],["id","id attribute on the wrapper div."]],outro:"Table display uses Domma Table - search, sort, pagination, column selector, and CSV export are all built in. Cards and lists are static."}),t("View - Pro",{intro:"Renders a Collection with full filtering, pagination, and display options. Requires a Pro MongoDB collection.",rows:[["slug","Required. The collection slug."],["display","table (default), cards, or list."],["limit","Maximum number of entries to display."],["fields","Comma-separated field keys to show."],["title-field","Field used as the card/list title."],["columns","Grid columns for cards display (2-4, default 3)."],["empty","Text shown when there are no entries."],["class","Extra CSS classes on the wrapper div."],["id","id attribute on the wrapper div."]],examples:[e("Table display",'[view slug="products" display="table" limit="50" /]'),e("Cards display",'[view slug="team" display="cards" columns="3" title-field="name" /]')]}),t("CTA - Pro",{intro:"Renders an action button that triggers a Pro Action against an entry. Requires the Pro Actions feature.",rows:[["action","Required. The action slug."],["entry","Required. The entry id to act on."],["style","Button variant (default: primary)."],["icon","Domma icon name for the button."],["size","Button size (sm, lg)."],["confirm","Confirmation prompt text before running."],["label","Self-closing only: button label text."]],examples:[e("Wrapping form",'[cta action="approve" entry="abc123" style="success" confirm="Approve this entry?"]Approve[/cta]'),e("Self-closing",'[cta action="archive" entry="abc123" icon="archive" label="Archive" /]')]})],f=[t("Effects",{intro:"Shortcodes for scroll-triggered animations, counters, typewriter text, and more.
|
|
94
|
+
[/slideover]`,"Nested [card] and [grid] shortcodes work inside the slideover body.")]})],b=[t("Embedding a Form",{rows:[['name="..."',"Form slug (required). Also accepted as slug=."],['class="..."',"Extra CSS classes on the wrapper div"],['id="..."',"id attribute on the wrapper div"]],examples:[e("Basic",'[form name="contact" /]',"Forms are managed under Forms in the sidebar."),e("With wrapper styling",'[form name="newsletter" class="mt-4" id="newsletter-form" /]')]}),t("Displaying a Collection",{block:'[collection slug="enquiries" display="table" /]',rows:[["slug","Required. The collection slug."],["display","table (default), cards, or list."],["fields","Comma-separated field keys to show. Defaults to all."],["limit","Maximum number of entries to display."],["sort","Field key to sort by."],["order","asc or desc (default asc)."],["title-field","Field used as the card/list title (cards + list)."],["columns","Grid columns for cards display (2-4, default 3)."],["search","true/false - enable search in table mode (default true)."],["sortable","true/false - enable column sorting in table mode (default true)."],["exportable","true/false - show CSV export button in table mode (default false)."],["page-size","Rows per page in table mode (default 25)."],["empty","Text shown when there are no entries."],["cta","Action slug: enables per-entry CTA buttons."],["cta-label",'CTA button label (default: "Run").'],["cta-icon","CTA button icon name."],["cta-style","CTA button variant (default: primary)."],["cta-confirm","CTA confirmation prompt text."],["class","Extra CSS classes on the wrapper div."],["id","id attribute on the wrapper div."]],outro:"Table display uses Domma Table - search, sort, pagination, column selector, and CSV export are all built in. Cards and lists are static."}),t("View - Pro",{intro:"Renders a Collection with full filtering, pagination, and display options. Requires a Pro MongoDB collection.",rows:[["slug","Required. The collection slug."],["display","table (default), cards, or list."],["limit","Maximum number of entries to display."],["fields","Comma-separated field keys to show."],["title-field","Field used as the card/list title."],["columns","Grid columns for cards display (2-4, default 3)."],["empty","Text shown when there are no entries."],["class","Extra CSS classes on the wrapper div."],["id","id attribute on the wrapper div."]],examples:[e("Table display",'[view slug="products" display="table" limit="50" /]'),e("Cards display",'[view slug="team" display="cards" columns="3" title-field="name" /]')]}),t("CTA - Pro",{intro:"Renders an action button that triggers a Pro Action against an entry. Requires the Pro Actions feature.",rows:[["action","Required. The action slug."],["entry","Required. The entry id to act on."],["style","Button variant (default: primary)."],["icon","Domma icon name for the button."],["size","Button size (sm, lg)."],["confirm","Confirmation prompt text before running."],["label","Self-closing only: button label text."]],examples:[e("Wrapping form",'[cta action="approve" entry="abc123" style="success" confirm="Approve this entry?"]Approve[/cta]'),e("Self-closing",'[cta action="archive" entry="abc123" icon="archive" label="Archive" /]')]})],f=[t("Effects",{intro:"Shortcodes for scroll-triggered animations, counters, typewriter text, and more. Built into Domma CMS; configure site-wide defaults in System > Effects.",rows:[['[reveal animation="fade"]...[/reveal]',"Fade/slide/zoom on scroll"],["[breathe]...[/breathe]","Continuous gentle scale loop"],["[pulse]...[/pulse]","Repeating scale pulse"],["[shake]...[/shake]","One-shot shake"],['[scribe mode="word" effect="bounce" speed="140" loop="3" cursor-blink="false" controls="true"]...[/scribe]',"Scribe - characters, words, or sentences; effects, counted loops, cursor and playback settings"],['[scribe loop="true"][render]Text[/render][wait]1500[/wait][undo /][/scribe]',"Typewriter - script mode (sequenced render/wait/undo actions)"],["[scramble]...[/scramble]","Character-scramble reveal"],['[counter to="100" /]',"Animated number counter (self-closing)"],["[ripple]...[/ripple]","Click-triggered ripple"],['[twinkle count="50"]...[/twinkle]',"Particle/star overlay"],['[animate type="fade-in-up"]...[/animate]',"CSS-only animation (no plugin needed)"],['[ambient type="aurora"]...[/ambient]',"CSS-only animated background"],['[firework type="burst" colour="rainbow" /]',"CSS firework (self-closing)"],["[fireworks]...[/fireworks]","Fireworks display container"],['[celebrate theme="auto" /]',"Seasonal canvas celebration (auto-detects date)"],["[christmas /]","Christmas - snowflakes, trees, snowmen, sleigh"],["[new-year /]","Sparkler greeting, confetti, streamers, fireworks, balloons and a midnight clock"],["[chinese-new-year /]","Chinese dragon, red lanterns, fireworks, confetti and a sparkler greeting"],['sparkler-message="off"',"Hide the lettering on any scene with a sparkler greeting"],['sparkler-style="cursive" sparkler-colour="silver"',"Cursive Latin lettering with silver sparks (defaults: stroke and gold)"],['sparkler-greeting="best-wishes" sparkler-year="2027"',"Write Best wishes for 2027! in either style and colour"],["[easter /]","Blossom, decorated eggs, a bunny, flowers and butterflies"],["[summer-garden /]","Petals, flowers, butterflies, bumblebees and ladybirds"],["[winter /]","Snow, frosted pines, snowdrifts, icicles and winter stars"],["[autumn /]","Falling leaves, acorns and mushrooms"],['[graduation message="Congratulations, Chlo\xE9!" /]',"Tossed mortarboards, diplomas, confetti and your greeting"],['[birthday message="Happy Birthday, Sarah!" /]',"Cake, balloons, confetti and your greeting"],['[anniversary message="Happy Anniversary!" /]',"Rings, hearts, ribbons and your greeting"],['[celebrate message="Best wishes for {year}!" sparkler-year="2027" /]',"Standalone sparkler lettering; custom messages allow 120 characters"],['[money-rain duration="8000" /]',"Illustrated banknotes, separate from actual weather rain"],['[make-it-rain intensity="heavy" duration="8000" /]',"Weather rain and ground splashes; duration 0 keeps raining"],["[halloween /]","Halloween - pumpkins, ghosts, bats, spiders"],["[st-patricks /]","St Patrick's Day - shamrocks, coins, rainbows, leprechaun"],["[valentines /]","Valentine's Day - hearts, roses"],["[guy-fawkes /]","Guy Fawkes - fireworks, sparkles"],["[st-andrews /]","St Andrew's Day - heather, thistles"],["[st-davids /]","St David's Day - daffodils, dragons"],["[st-georges /]","St George's Day - roses, flags"]],examples:[e("Scribe script - looping multi-phrase typewriter",`[scribe loop="true" loop-delay="2000" delete-speed="20"]
|
|
95
95
|
[render]We build things that matter.[/render]
|
|
96
96
|
[wait]1200[/wait]
|
|
97
97
|
[undo all="true" /]
|
|
@@ -100,7 +100,7 @@ Markdown content here.
|
|
|
100
100
|
[undo all="true" /]
|
|
101
101
|
[/scribe]`,'Use [render]...[/render] to type text, [wait]Ms[/wait] to pause, and [undo /] to delete. loop="true" replays the sequence. Only [render], [wait], and [undo] shortcodes are parsed - plain text between actions is ignored.'),e("Christmas",'[christmas intensity="medium" snowmen="off" trees="off" wreaths="off" northStars="off" sleigh="off" train="off" elf="off" robin="off" firework="off" /]',"intensity: light | medium | heavy. Count-based (off disables entirely): snowmen, trees, wreaths, northStars. Probability-based specials (off prevents spawning): sleigh, train, elf, robin, firework. Snowflakes always fall."),e("Halloween",'[halloween intensity="medium" gravestones="off" jackOLanterns="off" spiders="off" scarecrows="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): gravestones, jackOLanterns, spiders, scarecrows, twinklingStars. Particles: bats, ghosts, pumpkins (always on). Fixed decorations: haunted-house, cauldron (always present)."),e("St Patrick's Day",'[st-patricks intensity="medium" pots="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): pots (pots of gold), twinklingStars. Particles: shamrocks, clover petals, gold coins, sparkles (always on). Fixed decorations: rainbow, harp, moon. Leprechaun appears randomly - more likely at higher intensity."),e("Valentine's Day",'[valentines intensity="medium" garlands="off" butterflies="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): garlands (heart garlands), butterflies. Particles: hearts, rose petals, sparkles, lips (always on). Fixed decorations: envelopes (always present). Cupid appears randomly - more likely at higher intensity."),e("Guy Fawkes Night",'[guy-fawkes intensity="medium" bonfires="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): bonfires. Particles: embers, fireworks, sparks, rockets, bursts, trails (always on). Fixed decorations: guy-effigy, catherine-wheel, roman-candle, sparkler-bundle, moon. Starts at 25% particle density and builds up gradually."),e("St Andrew's Day",'[st-andrews intensity="medium" thistles="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): thistles, twinklingStars. Particles: heather petals, saltire sparkles (always on). Fixed decorations: bagpiper, saltire-flag, tartan-pattern, highland-scene (always present). Bagpiper probability increases with intensity."),e("St David's Day",'[st-davids intensity="medium" daffodilFields="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): daffodilFields, twinklingStars. Particles: daffodil petals, spring sparkles (always on). Fixed decorations: welsh-dragon, harp, leek-bundle, flag (always present). Flying dragon appears randomly - more likely at higher intensity."),e("St George's Day",'[st-georges intensity="medium" roses="off" twinklingStars="off" /]',"intensity: light | medium | heavy. Decoration toggles (off disables): roses (English roses), twinklingStars. Particles: rose petals, Tudor roses, oak leaves, sparkles (always on). Fixed decorations: st-georges-cross, dragon, shield, castle (always present). Knight appears randomly - more likely at higher intensity.")],outro:"Enable the Domma Effects plugin to activate these shortcodes on your site."}),t("DConfig - Declarative Behaviour",{intro:"Define click handlers and class toggles without writing JavaScript. Use the DConfig section above or embed inline with [dconfig]...[/dconfig] in the content body. Inline shortcodes win on selector conflict.",examples:[e("Toggle a class on click",'{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }','Selector keys use standard CSS selectors. "target" is optional - defaults to the element itself.'),e("Inline shortcode syntax",`[dconfig]
|
|
102
102
|
{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }
|
|
103
|
-
[/dconfig]`)]}),t("CSS Editor",{intro:"The toolbar's </> button (left of the Split View icon) swaps the left pane between Markdown and Custom CSS. Edits are saved together with the page - one Save, one toast.",rows:[["</> button","Toggle between Markdown editor and Custom CSS editor"],["Split / Write / Preview","Standard view mode buttons to the right of </>"],["Save","Saves both page content and CSS in a single operation"]]}),t("Colour Picker",{intro:"Insert \u2192 Colour Picker opens a centred colour picker. Pick a colour, then use Copy to copy the hex code to the clipboard or Insert to drop it at the cursor. Click outside the panel to dismiss."})];export const REFERENCE=[{id:"basics",label:"Basics",sections:m},{id:"layout",label:"Layout",sections:
|
|
103
|
+
[/dconfig]`)]}),t("CSS Editor",{intro:"The toolbar's </> button (left of the Split View icon) swaps the left pane between Markdown and Custom CSS. Edits are saved together with the page - one Save, one toast.",rows:[["</> button","Toggle between Markdown editor and Custom CSS editor"],["Split / Write / Preview","Standard view mode buttons to the right of </>"],["Save","Saves both page content and CSS in a single operation"]]}),t("Colour Picker",{intro:"Insert \u2192 Colour Picker opens a centred colour picker. Pick a colour, then use Copy to copy the hex code to the clipboard or Insert to drop it at the cursor. Click outside the panel to dismiss."})];export const REFERENCE=[{id:"basics",label:"Basics",sections:m},{id:"layout",label:"Layout",sections:g},{id:"components",label:"Components",sections:h},{id:"data",label:"Data",sections:b},{id:"advanced",label:"Advanced",sections:f}];const y=`
|
|
104
104
|
<div class="per">
|
|
105
105
|
<div class="per-tabs" role="tablist" data-each="tabs key=id">
|
|
106
106
|
<button type="button" class="per-tab" role="tab" data-on-click="$root.show(id)"
|
|
@@ -166,4 +166,4 @@ pre.per-block { padding-right: .75rem; margin-bottom: .5rem; }
|
|
|
166
166
|
.per-copy { position: absolute; top: .3rem; right: .3rem; background: none; border: none; cursor: pointer; opacity: .45; padding: .15rem;
|
|
167
167
|
line-height: 1; color: inherit; }
|
|
168
168
|
.per-copy:hover, .per-copy:focus-visible { opacity: 1; }
|
|
169
|
-
</style>`;async function
|
|
169
|
+
</style>`;async function w(o){try{return await navigator.clipboard.writeText(o),!0}catch{const a=document.createElement("textarea");a.value=o,a.style.cssText="position:fixed;opacity:0;",document.body.appendChild(a),a.select();let r=!1;try{r=document.execCommand("copy")}catch{}return a.remove(),r}}export function openEditorReference(){const o=E.slideover({title:"Editor Reference",size:"md",position:"right"}),a=document.createElement("div");a.innerHTML=y;const r=M.observable("basics"),s=M.applyBindings({tabs:REFERENCE,tab:r,show:n=>{r.value=n},copy:async n=>{const i=await w(n);E.toast(i?"Copied!":"Could not copy - select the code and copy it.",{type:i?"success":"warning",duration:1200})}},a);o.element.appendChild(a);const l=M.effect(()=>{r.value,requestAnimationFrame(()=>I.scan(a))});p(o,()=>{try{l()}catch{}try{s.dispose()}catch{}}),o.open()}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{encodeMessageAttribute as p,decodeMessageAttribute as g}from"../../../public/js/celebrations/message-attrs.mjs";import{replaceRange as d}from"./markdown-toolbar.js";export function parseAttrs(n){const t=String(n??""),r={};for(const[,f,o,e]of t.matchAll(/([\w-]+)=(?:"([^"]*)"|'([^']*)')/g))r[f]=f==="message"?g(o??e??""):o??e??"";const c=t.replace(/([\w-]+)=(?:"[^"]*"|'[^']*')/g,f=>" ".repeat(f.length));for(const[,f]of c.matchAll(/\b([\w-]+)\b/g))f in r||(r[f]=!0);return r}export function serialiseTag(n,t,r){const c=[];for(const[o,e]of Object.entries(t))if(e===!0)c.push(o);else{if(e===!1||e===null||e===void 0||e==="")continue;c.push(`${o}="${o==="message"?p(e):String(e).replace(/"/g,""")}"`)}const f=c.length?` ${c.join(" ")}`:"";return r?`[${n}${f} /]`:`[${n}${f}]`}function a(n,t,r,c){const f=n.slice(t,r),o=/\/\s*\]$/.test(f),e=f.match(new RegExp(`^\\[${c}([\\s\\S]*?)\\s*/?\\]$`)),s=e?e[1]:"";return{name:c,openTagStart:t,openTagEnd:r,attrStr:s,attrs:parseAttrs(s),selfClosing:o}}function u(n,t,r){const c=new RegExp(`\\[${r}(?=[\\s\\]/])([^\\]]*)\\]`,"g"),f=new RegExp(`\\[/${r}\\]`,"g"),o=[];for(const l of n.matchAll(c)){if(l.index>=t)break;o.push({type:"open",start:l.index,end:l.index+l[0].length})}for(const l of n.matchAll(f)){if(l.index>=t)break;o.push({type:"close",start:l.index})}o.sort((l,i)=>l.start-i.start);let e=0,s=null;for(const l of o)l.type==="open"?(e++,s=l):(e--,e<0&&(e=0,s=null));return e>0&&s&&new RegExp(`\\[/${r}\\]`).test(n.slice(t))?{start:s.start,end:s.end}:null}export function findEnclosingShortcode(n,t){const r=n.value,c=n.selectionStart,f=new Map(t.map(s=>[s.name,s]));let o=null;for(const s of r.matchAll(/\[(\/)?([a-zA-Z][\w-]*)((?:[^\]]*?))\]/g)){const l=s.index,i=l+s[0].length;c<l||c>i||f.has(s[2])&&(o={name:s[2],start:l,end:i,close:!!s[1]})}if(o)if(o.close){const s=u(r,o.start,o.name);if(s)return a(r,s.start,s.end,o.name)}else return a(r,o.start,o.end,o.name);let e=null;for(const s of t){if(s.selfClosing)continue;const l=u(r,c,s.name);l&&(!e||l.start>e.start)&&(e={...l,name:s.name})}return e?a(r,e.start,e.end,e.name):null}export function setAttr(n,t,r,c){return setAttrs(n,t,{[r]:c})}export function setAttrs(n,t,r){const c={...t.attrs};for(const[s,l]of Object.entries(r))l===!1||l===null||l===void 0||l===""?delete c[s]:c[s]=l;const f=serialiseTag(t.name,c,t.selfClosing),o=typeof document<"u"?document.activeElement:null;d(n,t.openTagStart,t.openTagEnd,f),o&&o!==n&&typeof o.focus=="function"&&o.focus();const e=t.openTagStart+f.length;return a(n.value,t.openTagStart,e,t.name)}export function effectAttrKeys(n,t){const r=`${t}-`;return Object.keys(n).filter(c=>c===t||c.startsWith(r))}export function boolState(n,t){return n==null?!!t:!(n===!1||n==="false"||n==="0")}export function boolValue(n,t){return t===!!n.default?null:t?"true":"false"}
|
|
@@ -950,6 +950,45 @@ Body content in **Markdown** is supported.
|
|
|
950
950
|
<code>[christmas /]</code>; <code>[christmas snow-only="true" /]</code> is just the falling snow). Many can also be added to a card, box, hero, banner, column or
|
|
951
951
|
button as attributes: <code>[card reveal reveal-animation="zoom"]</code>. Settings are at
|
|
952
952
|
<strong>System > Effects</strong>.</p>
|
|
953
|
+
<h3>Celebrations, seasons and occasions</h3>
|
|
954
|
+
<p>Every site includes 20 celebration scenes. In the page editor, use
|
|
955
|
+
<strong>Insert > Effects > Celebrate</strong> or right-click and open
|
|
956
|
+
<strong>Celebrations</strong>. Select a theme, intensity and the decorations you want.
|
|
957
|
+
Right-click an existing shortcode to change its settings. Use <code>falling-only="true"</code>
|
|
958
|
+
for particles without the fixed decorations.</p>
|
|
959
|
+
<p><code>theme="auto"</code> follows the visitor's local calendar. Seasonal themes are
|
|
960
|
+
<code>christmas</code>, <code>new-year</code>, <code>chinese-new-year</code>,
|
|
961
|
+
<code>easter</code>, <code>summer-garden</code>, <code>autumn</code>, <code>winter</code>,
|
|
962
|
+
<code>halloween</code>, <code>valentines</code>, <code>guy-fawkes</code>,
|
|
963
|
+
<code>st-patricks</code>, <code>st-andrews</code>, <code>st-davids</code> and <code>st-georges</code>.
|
|
964
|
+
Specific holidays take priority over Autumn and Winter. Easter and Chinese New Year move
|
|
965
|
+
with their calendars. Any theme can be selected manually throughout the year.</p>
|
|
966
|
+
<p>The manual scenes are <code>graduation</code> (tossed caps and diplomas),
|
|
967
|
+
<code>birthday</code> (cake and balloons), <code>anniversary</code> (rings and hearts),
|
|
968
|
+
<code>greeting</code> (lettering alone), <code>make-it-rain</code> (actual water and splashes)
|
|
969
|
+
and <code>money-rain</code> (illustrated banknotes). Summer Garden reuses the illustrated
|
|
970
|
+
butterflies and has bees visiting flowers; Easter's bunny hops between rests.</p>
|
|
971
|
+
<pre><code>[graduation message="Congratulations, Chloé!" sparkler-style="cursive" sparkler-colour="silver" /]
|
|
972
|
+
[birthday message="Happy Birthday, Sarah!" /]
|
|
973
|
+
[anniversary message="Happy Anniversary, Mum & Dad!" /]
|
|
974
|
+
[celebrate message="Best wishes for {year}!" sparkler-year="2027" /]
|
|
975
|
+
[make-it-rain intensity="heavy" duration="8000" /]
|
|
976
|
+
[money-rain duration="4000" /]</code></pre>
|
|
977
|
+
<p><strong>Personalise the sky writing:</strong> choose <code>stroke</code> or <code>cursive</code>
|
|
978
|
+
lettering and <code>gold</code> or <code>silver</code> sparks. <code>message</code> accepts
|
|
979
|
+
up to 120 characters, including names, punctuation and Latin accents. It replaces the
|
|
980
|
+
default greeting on Christmas, New Year, Chinese New Year and the occasion scenes.
|
|
981
|
+
Without a theme it selects standalone lettering; in the editor choose <strong>Sparkler greeting</strong>.
|
|
982
|
+
<code>{year}</code> inserts <code>sparkler-year</code>, which defaults to the current year.</p>
|
|
983
|
+
<p>For a preset, use <code>sparkler-greeting="best-wishes" sparkler-year="2027"</code>.
|
|
984
|
+
Omit it for Merry Christmas, Happy New Year, <span lang="zh">新年快乐</span>, or the
|
|
985
|
+
occasion's default message. <code>sparkler-message="off"</code> hides the lettering.
|
|
986
|
+
In hand-written messages, encode quotes as <code>&quot;</code> and square brackets as
|
|
987
|
+
<code>&#91;</code> and <code>&#93;</code>; the editor handles this for you.</p>
|
|
988
|
+
<p>Both rain effects accept <code>duration</code> in milliseconds: default 8000, maximum 600000,
|
|
989
|
+
or 0 to keep running until the effect is destroyed. Effects respect the site's reduced-motion
|
|
990
|
+
setting, stay transparent to clicks and remove their canvases when stopped.</p>
|
|
991
|
+
|
|
953
992
|
<p><code>[celebrate character="robin" /]</code> puts one character on a page at any time of year,
|
|
954
993
|
whatever the theme or date: <code>elf</code>, <code>leprechaun</code>, <code>bagpiper</code>,
|
|
955
994
|
<code>knight</code>, <code>dragon</code>, <code>welsh-dragon</code>, <code>cupid</code>,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as _,getUser as Ve}from"../api.js";import{addBannerAction as Te}from"../lib/plugin-chrome.js";import{resetPassword as Ge}from"../lib/password-reset-actions.js";import{helpPopovers as Je}from"../lib/help-popover.js";import{sidebarCount as Re}from"../lib/tool-kit.js";import{roleIndex as We,initials as Xe}from"../lib/users-arrange.js";import{fieldKind as ce,fieldOptions as ue}from"../lib/my-profile-arrange.js";import{buildUserPayload as ze,checkUserForm as Ne,draftOf as Qe,editRights as Ze,emailConflict as et,formFromUser as de,roleChoices as tt,snapshot as se}from"../lib/user-editor-arrange.js";import{fmtDate as pe,fmtDateTime as at}from"../lib/dates.js";const st=/^\/users\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,oe=a=>String(a||"").split("?")[0].replace(/\/$/,""),p=a=>String(a??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),ot=`<button type="button" class="uex-pick-chip" data-role="{{name}}" data-bind-class="on && 'is-on'" data-bind-disabled="disabled" title="{{title}}" data-bind-aria-pressed="on ? 'true' : 'false'">{{label}}</button>`,nt=`<button type="button" class="uex-pick-chip" data-project="{{slug}}" data-bind-class="on && 'is-on'" data-bind-aria-pressed="on ? 'true' : 'false'" title="{{title}}">{{name}}</button>`;let y=null,Oe=!1;function lt(){Oe||!window.R?.use||(Oe=!0,R.use((a,l,v)=>{const c=oe(l?.path);return!(st.test(l?.path||"")&&oe(a?.path)!==c)||!y?.dirty()||y.path!==c?v():E.confirm("You have unsaved changes to this user. Leave anyway? They are kept for your next visit - except a password, which is typed again.").then(A=>{A?v():history.replaceState(null,"",`#${c}`)})}),M.subscribe("router:afterChange",({to:a}={})=>{if(y&&oe(a?.path)!==y.path){const l=y;y=null,l.dispose()}}))}function rt(a,l){const v=ce(a.type),c=`uex-pf-${l}`,s=a.required?' <span class="text-danger">*</span>':"",A=a.help||a.description?` <span data-help="${p(a.help||a.description)}"></span>`:"",T=`<label class="form-label" for="${c}">${p(a.label||a.name)}${s}${A}</label>`,i=`<small class="uex-err" data-bind-hidden="!pe_${l}.value" data-bind-text="pe_${l}.value"></small>`,K=a.placeholder?` placeholder="${p(a.placeholder)}"`:"";switch(v){case"textarea":return`<div class="uex-wide">${T}<textarea id="${c}" class="form-input" rows="3"${K} data-model="pf_${l}.value"></textarea>${i}</div>`;case"select":return`<div>${T}<select id="${c}" class="form-input" data-model="pf_${l}.value"><option value="">-</option>${ue(a).map(m=>`<option value="${p(m.value)}">${p(m.label)}</option>`).join("")}</select>${i}</div>`;case"checkbox":return`<div><label class="uex-check"><input id="${c}" type="checkbox" data-model="pf_${l}.value"> ${p(a.label||a.name)}${s}</label>${A}${i}</div>`;case"multi":return`<div class="uex-wide">${T}<div data-multi="${l}">${ue(a).map((m,u)=>`<label class="uex-check"><input type="checkbox" data-opt="${p(m.value)}" data-bind-checked="po_${l}_${u}.value"> ${p(m.label)}</label>`).join("")}</div>${i}</div>`;default:return`<div>${T}<input id="${c}" type="${v}" class="form-input"${K} data-model="pf_${l}.value">${i}</div>`}}function ve(a,l){const v=a.querySelector(".dm-plugin-banner-name");if(!v)return;const c=v.querySelector(".dm-plugin-banner-parent")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-parent",textContent:"Users"}),s=v.querySelector(".dm-plugin-banner-sep")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-sep",textContent:"\u203A"});s.setAttribute("aria-hidden","true"),v.replaceChildren(c,s,document.createTextNode(l))}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(a){if(lt(),y){const e=y;y=null,e.dispose()}const l=oe(location.hash.slice(1)),v=l.match(/^\/users\/edit\/([^/?#]+)/),c=v?decodeURIComponent(v[1]):null,s=!c,A=s&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",T=[],i=e=>(T.push(e),e);let K=!1;const m=a.get(0),u=m.querySelector(".uex")||m,N=()=>!K&&u.isConnected,B=M.observable(!0),me=M.observable(""),Y=M.observable(!1),f=M.observable(null),q=M.observable(""),w=M.observable(""),J=M.observable(""),_e=M.observable(!1),j=M.observable("user"),b=M.observable([]),W=M.observable(""),C=M.observable([]),X=M.observable(!0),U=M.observable(""),V=M.observable(""),fe=M.observable(!1),he=M.observable(!1),be=M.observable([]),ge=M.observable([]),$=M.observable({self:!1,canEdit:!0,reason:""}),D=M.observable([]);let x={};const ke=M.observable([]);let g=[];const O=new Map,ye=M.observable(0),k=M.observable(de(null,[])),Be=()=>(ye.value,Object.fromEntries([...O].map(([e,t])=>[e,t.value]))),qe=()=>Object.fromEntries([...O].map(([e,t])=>[e,t.peek()])),we=()=>({name:q.value,email:w.value,password:J.value,role:j.value,additionalRoles:b.value,ownedByProject:W.value,projects:C.value,isActive:X.value,profile:Be()}),ne=()=>({name:q.peek(),email:w.peek(),password:J.peek(),role:j.peek(),additionalRoles:b.peek(),ownedByProject:W.peek(),projects:C.peek(),isActive:X.peek(),profile:qe()}),z=M.computed(()=>Ne(we(),{isNew:s,fields:g,saved:s?void 0:k.value,choices:D.value})),H=M.computed(()=>!B.value&&$.value.canEdit&&se(we())!==se(k.value)),F=M.computed(()=>!$.value.canEdit);i(M.effect(()=>{const e=new Set(b.value);be.value=D.value.filter(t=>t.name!==j.value).map(t=>({name:t.name,label:t.label,on:e.has(t.name),disabled:!t.assignable&&!e.has(t.name),title:t.assignable||e.has(t.name)?`Level ${t.level}`:t.reason}))})),i(M.effect(()=>{const e=new Set(C.value);ge.value=ke.value.map(t=>({slug:t.slug,name:t.name||t.slug,on:e.has(t.slug),title:t.slug}))}));const le={loading:B,loadError:me,saving:Y,name:q,email:w,password:J,showPw:_e,role:j,ownedByProject:W,isActive:X,hasProfile:fe,hasProjects:he,extraChips:be,projectChips:ge,err:z,dirty:H,locked:F,isSelf:M.computed(()=>$.value.self),lockedReason:M.computed(()=>$.value.reason),emailErr:M.computed(()=>z.value.email||(U.value&&w.value.trim().toLowerCase()===V.value?U.value:"")),cannotSave:M.computed(()=>Y.value||F.value||!H.value||Object.keys(z.value).length>0||!!(U.value&&w.value.trim().toLowerCase()===V.value)),saveText:s?"Create user":"Save",dirtyText:s?"This user has not been created yet.":"You have unsaved changes.",pwLabel:M.computed(()=>s?"Password":"New password"),pwPlaceholder:M.computed(()=>s?"At least 8 characters":"Leave empty to keep it"),avatar:M.computed(()=>Xe({name:q.value,email:w.value})),shownName:M.computed(()=>q.value.trim()||(s?"New user":f.value?.name||"")),shownEmail:M.computed(()=>w.value.trim()||""),roleText:M.computed(()=>[j.value,...b.value.filter(t=>t!==j.value)].filter(Boolean).map(t=>x[t]?.label||t).join(" + ")||"-"),accessHint:M.computed(()=>C.value.length?`Sees only ${C.value.length===1?"that project":"those projects"}, plus Core.`:"Nothing ticked: no restriction - sees every project."),factsText:M.computed(()=>{const e=f.value;return s||!e?"Not created yet":[e.createdAt&&`Added ${pe(e.createdAt)}`,e.lastLogin?`last signed in ${pe(e.lastLogin)}`:"never signed in",e.updatedAt&&`changed ${pe(e.updatedAt)}`].filter(Boolean).join(" \xB7 ")})},Q=`user-editor.draft.${c||"new"}`;function Z(e){q.value=e.name,w.value=e.email,J.value="",j.value=e.role,b.value=[...e.additionalRoles||[]],W.value=e.ownedByProject||"",C.value=[...e.projects||[]],X.value=e.isActive!==!1;for(const[t,o]of O)t in(e.profile||{})&&(o.value=e.profile[t])}function Ie(e,t){const o=et(e);return o?(U.value=o,V.value=String(t||"").trim().toLowerCase(),u.querySelector("#uex-email")?.focus(),!0):!1}async function re(){if(Y.peek()||B.peek()||F.peek())return;const e=ne(),t=Ne(e,{isNew:s,fields:g,saved:s?void 0:k.peek(),choices:D.peek()});if(Object.keys(t).length){E.toast("Check the fields marked in red first.",{type:"warning"});return}const o=ze(k.peek(),e,{isNew:s,fields:g,meta:f.peek()?.meta||{}});if(!o){E.toast("Nothing has changed.",{type:"info"});return}if(!(!s&&$.peek().self&&o.update.role&&(x[o.update.role]?.level??99)>(x[k.peek().role]?.level??99)&&!await E.confirm(`Give yourself the less senior role "${p(x[o.update.role]?.label||o.update.role)}"? You will not be able to raise it again yourself.`))){Y.value=!0;try{if(s){const d=await _.users.create(o.create);if(!N())return;ee(),k.value=ne(),E.toast(`Created ${d?.name||e.name.trim()}.`,{type:"success"}),Re("#/users"),location.hash=d?.id?`#/users/edit/${encodeURIComponent(d.id)}`:"#/users";return}const n=await _.users.update(c,o.update);if(!N())return;f.value=n;const r=de(n,g);if(k.value=r,Z(r),ee(),ve(m,n.name||n.email||"Edit"),$.peek().self){try{S.set("auth_user",{...S.get("auth_user")||{},name:n.name,email:n.email})}catch{}const d=document.getElementById("topbar-user-name");d&&n.name&&(d.textContent=n.name)}Re("#/users"),E.toast("Saved.",{type:"success"})}catch(n){Ie(n,e.email)||E.toast(`Not saved: ${n.message||n}`,{type:"error"})}finally{Y.value=!1}}}async function Ue(){H.peek()&&(!await E.confirm("Throw away the changes you have not saved?")||!N()||(Z(k.peek()),ee()))}function $e(){if(!H.peek())return;const e=Qe(ne());if(se({...e,password:""})!==se(k.peek()))try{S.set(Q,{form:e,at:new Date().toISOString(),basedOn:f.peek()?.updatedAt||null})}catch{}}function ee(){try{S.remove?S.remove(Q):S.set(Q,null)}catch{}}u.querySelector(".uex-roles").innerHTML=ot,u.querySelector(".uex-projects").innerHTML=nt;const Me=e=>{if(F.peek())return;const t=e.target.closest(".uex-pick-chip");if(!(!t||t.disabled)){if(t.dataset.role){const o=t.dataset.role,n=b.peek();b.value=n.includes(o)?n.filter(r=>r!==o):[...n,o]}else if(t.dataset.project){const o=t.dataset.project,n=C.peek();C.value=n.includes(o)?n.filter(r=>r!==o):[...n,o]}}};u.addEventListener("click",Me),i(()=>u.removeEventListener("click",Me)),i(M.effect(()=>{U.peek()&&w.value.trim().toLowerCase()!==V.peek()&&(U.value="",V.value="")})),i(M.effect(()=>{const e=j.value;b.peek().includes(e)&&(b.value=b.peek().filter(t=>t!==e))})),Te(a,{id:"user-editor-back",icon:"arrow-left",title:"Back to users",onClick:()=>{location.hash="#/users"}}),Te(a,{id:"user-editor-save",icon:"save",title:s?"Create user (Ctrl+S)":"Save (Ctrl+S)",onClick:()=>re()}),ve(m,s?"New user":"Edit");const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key.toLowerCase()!=="s"||!N()||(e.preventDefault(),e.stopImmediatePropagation(),re())};document.addEventListener("keydown",Ee,!0),i(()=>document.removeEventListener("keydown",Ee,!0));const xe=e=>{H.peek()&&($e(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",xe),i(()=>window.removeEventListener("beforeunload",xe)),y={path:l,dirty:()=>H.peek(),dispose(){K=!0,$e(),T.splice(0).reverse().forEach(e=>{try{e()}catch{}})}};let h=null,Se=null,je=null,L=[];try{[h,je,Se,L]=await Promise.all([s?Promise.resolve(null):_.users.get(c),_.collections.listEntries("roles",{limit:1e3}).catch(()=>null),_.collections.get("user-profiles").catch(()=>null),_.projects.list().catch(()=>[])])}catch(e){if(!N())return;me.value=e?.status===404?"There is no such user - it may have been deleted.":`Could not load this user: ${e.message||e}`,B.value=!1,I.scan(u);return}if(!N())return;L=Array.isArray(L)?L:[],ke.value=L,x=We(je?.entries||[]),Object.keys(x).length||(x={admin:{label:"Admin",level:1},user:{label:"User",level:2}});const Ce=Ve()||{};$.value=Ze(Ce,h,x),D.value=tt(x,Ce,h),f.value=h;const De=u.querySelector(".uex-role-select");De.innerHTML=D.peek().map(e=>`<option value="${p(e.name)}"${e.assignable?"":" disabled"}>${p(e.label)}${e.assignable?"":" - only a more senior role can give it"}</option>`).join("");const He=u.querySelector(".uex-owner-select");He.innerHTML=`<option value="">(none)</option>${L.map(e=>`<option value="${p(e.slug)}">${p(e.name||e.slug)}</option>`).join("")}`,he.value=L.length>0;const Le=D.peek().filter(e=>e.assignable).map(e=>e.name),Fe=Le.includes("user")?"user":Le.at(-1)||"user";g=(Se?.fields||[]).filter(e=>e&&e.name);const G=de(h,g,{project:L.some(e=>e.slug===A)?A:"",defaultRole:Fe});g.forEach(e=>O.set(e.name,M.observable(G.profile[e.name])));const Pe=async e=>{const t=f.peek();if(t&&await Ge(t,e))try{f.value={...t,...await _.users.get(t.id)}}catch{}};le.canReset=M.computed(()=>!s&&!B.value&&$.value.canEdit&&!$.value.self&&f.value?.isActive!==!1),le.resetOpen=M.computed(()=>!!f.value?.resetLinkExpires&&Date.parse(f.value.resetLinkExpires)>Date.now());const Ke=M.applyBindings(le,u,{methods:{save:re,discard:Ue,resetEmail:()=>Pe("email"),resetLink:()=>Pe("link")}});i(()=>Ke.dispose());const ie=g.map((e,t)=>({f:e,i:t})).filter(({f:e})=>ce(e.type)!=="hidden");if(ie.length){const e=u.querySelector(".uex-profile");e.innerHTML=ie.map(({f:r,i:d})=>rt(r,d)).join("");const t={};g.forEach((r,d)=>{t[`pf_${d}`]=O.get(r.name),t[`pe_${d}`]=M.computed(()=>z.value[`profile.${r.name}`]||""),ce(r.type)==="multi"&&ue(r).forEach((te,ae)=>{t[`po_${d}_${ae}`]=M.computed(()=>(O.get(r.name).value||[]).includes(te.value))})});const o=M.applyBindings(t,e);i(()=>o.dispose());const n=r=>{const d=r.target.closest("[data-multi]");if(!d||!r.target.matches("input[data-opt]"))return;const te=O.get(g[Number(d.dataset.multi)].name),ae=r.target.dataset.opt,Ae=te.peek()||[];te.value=r.target.checked?[...new Set([...Ae,ae])]:Ae.filter(Ye=>Ye!==ae)};e.addEventListener("change",n),i(()=>e.removeEventListener("change",n))}fe.value=ie.length>0,i(Je(u,{position:"bottom"})),k.value=G,Z(G),ye.value++,B.value=!1,ve(m,s?"New user":h.name||h.email||"Edit"),I.scan(u),F.peek()||u.querySelector("#uex-name")?.focus();let P=null;try{P=S.get(Q)}catch{}if(P?.form&&!F.peek()){ee();const e=P.at?` (${at(P.at)})`:"",t=!s&&P.basedOn&&h?.updatedAt&&P.basedOn!==h.updatedAt?" This user has been changed since - restoring puts your version over it.":"",o=s?"the new user you started":`"${p(h.name||h.email)}"`;await E.confirm(`You left unsaved changes to ${o}${e}.${t} Restore them?`)&&N()&&Z({...G,...P.form,profile:{...G.profile,...P.form.profile||{}}})}}};
|
|
1
|
+
import{api as O,getUser as Ge}from"../api.js";import{addBannerAction as Re}from"../lib/plugin-chrome.js";import{resetPassword as Je}from"../lib/password-reset-actions.js";import{helpPopovers as We}from"../lib/help-popover.js";import{sidebarCount as _e}from"../lib/tool-kit.js";import{roleIndex as Xe,initials as ze}from"../lib/users-arrange.js";import{fieldKind as ce,fieldOptions as ue}from"../lib/my-profile-arrange.js";import{buildUserPayload as Qe,checkUserForm as Ne,draftOf as Ze,editRights as et,emailConflict as tt,formFromUser as de,roleChoices as at,snapshot as se}from"../lib/user-editor-arrange.js";import{fmtDate as pe,fmtDateTime as st}from"../lib/dates.js";const ot=/^\/users\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,oe=a=>String(a||"").split("?")[0].replace(/\/$/,""),p=a=>String(a??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),nt=`<button type="button" class="uex-pick-chip" data-role="{{name}}" data-bind-class="on && 'is-on'" data-bind-disabled="disabled" title="{{title}}" data-bind-aria-pressed="on ? 'true' : 'false'">{{label}}</button>`,rt=`<button type="button" class="uex-pick-chip" data-project="{{slug}}" data-bind-class="on && 'is-on'" data-bind-aria-pressed="on ? 'true' : 'false'" title="{{title}}">{{name}}</button>`;let y=null,Oe=!1;function lt(){Oe||!window.R?.use||(Oe=!0,R.use((a,r,v)=>{const c=oe(r?.path);return!(ot.test(r?.path||"")&&oe(a?.path)!==c)||!y?.dirty()||y.path!==c?v():E.confirm("You have unsaved changes to this user. Leave anyway? They are kept for your next visit - except a password, which is typed again.").then(A=>{A?v():history.replaceState(null,"",`#${c}`)})}),M.subscribe("router:afterChange",({to:a}={})=>{if(y&&oe(a?.path)!==y.path){const r=y;y=null,r.dispose()}}))}function it(a,r){const v=ce(a.type),c=`uex-pf-${r}`,s=a.required?' <span class="text-danger">*</span>':"",A=a.help||a.description?` <span data-help="${p(a.help||a.description)}"></span>`:"",T=`<label class="form-label" for="${c}">${p(a.label||a.name)}${s}${A}</label>`,i=`<small class="uex-err" data-bind-hidden="!pe_${r}.value" data-bind-text="pe_${r}.value"></small>`,K=a.placeholder?` placeholder="${p(a.placeholder)}"`:"";switch(v){case"textarea":return`<div class="uex-wide">${T}<textarea id="${c}" class="form-input" rows="3"${K} data-model="pf_${r}.value"></textarea>${i}</div>`;case"select":return`<div>${T}<select id="${c}" class="form-input" data-model="pf_${r}.value"><option value="">-</option>${ue(a).map(m=>`<option value="${p(m.value)}">${p(m.label)}</option>`).join("")}</select>${i}</div>`;case"checkbox":return`<div><label class="uex-check"><input id="${c}" type="checkbox" data-model="pf_${r}.value"> ${p(a.label||a.name)}${s}</label>${A}${i}</div>`;case"multi":return`<div class="uex-wide">${T}<div data-multi="${r}">${ue(a).map((m,u)=>`<label class="uex-check"><input type="checkbox" data-opt="${p(m.value)}" data-bind-checked="po_${r}_${u}.value"> ${p(m.label)}</label>`).join("")}</div>${i}</div>`;default:return`<div>${T}<input id="${c}" type="${v}" class="form-input"${K} data-model="pf_${r}.value">${i}</div>`}}function ve(a,r){const v=a.querySelector(".dm-plugin-banner-name");if(!v)return;const c=v.querySelector(".dm-plugin-banner-parent")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-parent",textContent:"Users"}),s=v.querySelector(".dm-plugin-banner-sep")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-sep",textContent:"\u203A"});s.setAttribute("aria-hidden","true"),v.replaceChildren(c,s,document.createTextNode(r))}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(a){if(lt(),y){const e=y;y=null,e.dispose()}const r=oe(location.hash.slice(1)),v=r.match(/^\/users\/edit\/([^/?#]+)/),c=v?decodeURIComponent(v[1]):null,s=!c,A=s&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",T=[],i=e=>(T.push(e),e);let K=!1;const m=a.get(0),u=m.querySelector(".uex")||m,_=()=>!K&&u.isConnected,B=M.observable(!0),me=M.observable(""),Y=M.observable(!1),f=M.observable(null),q=M.observable(""),w=M.observable(""),J=M.observable(""),Be=M.observable(!1),j=M.observable("user"),b=M.observable([]),W=M.observable(""),C=M.observable([]),X=M.observable(!0),U=M.observable(""),V=M.observable(""),fe=M.observable(!1),he=M.observable(!1),be=M.observable([]),ge=M.observable([]),$=M.observable({self:!1,canEdit:!0,reason:""}),D=M.observable([]);let x={};const ke=M.observable([]);let g=[];const N=new Map,ye=M.observable(0),k=M.observable(de(null,[])),qe=()=>(ye.value,Object.fromEntries([...N].map(([e,t])=>[e,t.value]))),Ie=()=>Object.fromEntries([...N].map(([e,t])=>[e,t.peek()])),we=()=>({name:q.value,email:w.value,password:J.value,role:j.value,additionalRoles:b.value,ownedByProject:W.value,projects:C.value,isActive:X.value,profile:qe()}),ne=()=>({name:q.peek(),email:w.peek(),password:J.peek(),role:j.peek(),additionalRoles:b.peek(),ownedByProject:W.peek(),projects:C.peek(),isActive:X.peek(),profile:Ie()}),z=M.computed(()=>Ne(we(),{isNew:s,fields:g,saved:s?void 0:k.value,choices:D.value})),H=M.computed(()=>!B.value&&$.value.canEdit&&se(we())!==se(k.value)),F=M.computed(()=>!$.value.canEdit);i(M.effect(()=>{const e=new Set(b.value);be.value=D.value.filter(t=>t.name!==j.value).map(t=>({name:t.name,label:t.label,on:e.has(t.name),disabled:!t.assignable&&!e.has(t.name),title:t.assignable||e.has(t.name)?`Level ${t.level}`:t.reason}))})),i(M.effect(()=>{const e=new Set(C.value);ge.value=ke.value.map(t=>({slug:t.slug,name:t.name||t.slug,on:e.has(t.slug),title:t.slug}))}));const re={loading:B,loadError:me,saving:Y,name:q,email:w,password:J,showPw:Be,role:j,ownedByProject:W,isActive:X,hasProfile:fe,hasProjects:he,extraChips:be,projectChips:ge,err:z,dirty:H,locked:F,isSelf:M.computed(()=>$.value.self),lockedReason:M.computed(()=>$.value.reason),emailErr:M.computed(()=>z.value.email||(U.value&&w.value.trim().toLowerCase()===V.value?U.value:"")),cannotSave:M.computed(()=>Y.value||F.value||!H.value||Object.keys(z.value).length>0||!!(U.value&&w.value.trim().toLowerCase()===V.value)),saveText:s?"Create user":"Save",dirtyText:s?"This user has not been created yet.":"You have unsaved changes.",pwLabel:M.computed(()=>s?"Password":"New password"),pwPlaceholder:M.computed(()=>s?"At least 8 characters":"Leave empty to keep it"),avatar:M.computed(()=>ze({name:q.value,email:w.value})),shownName:M.computed(()=>q.value.trim()||(s?"New user":f.value?.name||"")),shownEmail:M.computed(()=>w.value.trim()||""),roleText:M.computed(()=>[j.value,...b.value.filter(t=>t!==j.value)].filter(Boolean).map(t=>x[t]?.label||t).join(" + ")||"-"),accessHint:M.computed(()=>C.value.length?`Sees only ${C.value.length===1?"that project":"those projects"}, plus Core.`:"Nothing ticked: no restriction - sees every project."),factsText:M.computed(()=>{const e=f.value;return s||!e?"Not created yet":[e.createdAt&&`Added ${pe(e.createdAt)}`,e.lastLogin?`last signed in ${pe(e.lastLogin)}`:"never signed in",e.updatedAt&&`changed ${pe(e.updatedAt)}`].filter(Boolean).join(" \xB7 ")})};let $e="";try{$e=S.get("auth_user")?.id||""}catch{}const Q=`user-editor.draft.${$e||"anyone"}.for-${c||"new"}`;function Z(e){q.value=e.name,w.value=e.email,J.value="",j.value=e.role,b.value=[...e.additionalRoles||[]],W.value=e.ownedByProject||"",C.value=[...e.projects||[]],X.value=e.isActive!==!1;for(const[t,o]of N)t in(e.profile||{})&&(o.value=e.profile[t])}function Ue(e,t){const o=tt(e);return o?(U.value=o,V.value=String(t||"").trim().toLowerCase(),u.querySelector("#uex-email")?.focus(),!0):!1}async function le(){if(Y.peek()||B.peek()||F.peek())return;const e=ne(),t=Ne(e,{isNew:s,fields:g,saved:s?void 0:k.peek(),choices:D.peek()});if(Object.keys(t).length){E.toast("Check the fields marked in red first.",{type:"warning"});return}const o=Qe(k.peek(),e,{isNew:s,fields:g,meta:f.peek()?.meta||{}});if(!o){E.toast("Nothing has changed.",{type:"info"});return}if(!(!s&&$.peek().self&&o.update.role&&(x[o.update.role]?.level??99)>(x[k.peek().role]?.level??99)&&!await E.confirm(`Give yourself the less senior role "${p(x[o.update.role]?.label||o.update.role)}"? You will not be able to raise it again yourself.`))){Y.value=!0;try{if(s){const d=await O.users.create(o.create);if(!_())return;ee(),k.value=ne(),E.toast(`Created ${d?.name||e.name.trim()}.`,{type:"success"}),_e("#/users"),location.hash=d?.id?`#/users/edit/${encodeURIComponent(d.id)}`:"#/users";return}const n=await O.users.update(c,o.update);if(!_())return;f.value=n;const l=de(n,g);if(k.value=l,Z(l),ee(),ve(m,n.name||n.email||"Edit"),$.peek().self){try{S.set("auth_user",{...S.get("auth_user")||{},name:n.name,email:n.email})}catch{}const d=document.getElementById("topbar-user-name");d&&n.name&&(d.textContent=n.name)}_e("#/users"),E.toast("Saved.",{type:"success"})}catch(n){Ue(n,e.email)||E.toast(`Not saved: ${n.message||n}`,{type:"error"})}finally{Y.value=!1}}}async function De(){H.peek()&&(!await E.confirm("Throw away the changes you have not saved?")||!_()||(Z(k.peek()),ee()))}function Me(){if(!H.peek())return;const e=Ze(ne());if(se({...e,password:""})!==se(k.peek()))try{S.set(Q,{form:e,at:new Date().toISOString(),basedOn:f.peek()?.updatedAt||null})}catch{}}function ee(){try{S.remove?S.remove(Q):S.set(Q,null)}catch{}}u.querySelector(".uex-roles").innerHTML=nt,u.querySelector(".uex-projects").innerHTML=rt;const Ee=e=>{if(F.peek())return;const t=e.target.closest(".uex-pick-chip");if(!(!t||t.disabled)){if(t.dataset.role){const o=t.dataset.role,n=b.peek();b.value=n.includes(o)?n.filter(l=>l!==o):[...n,o]}else if(t.dataset.project){const o=t.dataset.project,n=C.peek();C.value=n.includes(o)?n.filter(l=>l!==o):[...n,o]}}};u.addEventListener("click",Ee),i(()=>u.removeEventListener("click",Ee)),i(M.effect(()=>{U.peek()&&w.value.trim().toLowerCase()!==V.peek()&&(U.value="",V.value="")})),i(M.effect(()=>{const e=j.value;b.peek().includes(e)&&(b.value=b.peek().filter(t=>t!==e))})),Re(a,{id:"user-editor-back",icon:"arrow-left",title:"Back to users",onClick:()=>{location.hash="#/users"}}),Re(a,{id:"user-editor-save",icon:"save",title:s?"Create user (Ctrl+S)":"Save (Ctrl+S)",onClick:()=>le()}),ve(m,s?"New user":"Edit");const xe=e=>{!(e.ctrlKey||e.metaKey)||e.key.toLowerCase()!=="s"||!_()||(e.preventDefault(),e.stopImmediatePropagation(),le())};document.addEventListener("keydown",xe,!0),i(()=>document.removeEventListener("keydown",xe,!0));const Se=e=>{H.peek()&&(Me(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Se),i(()=>window.removeEventListener("beforeunload",Se)),y={path:r,dirty:()=>H.peek(),dispose(){K=!0,Me(),T.splice(0).reverse().forEach(e=>{try{e()}catch{}})}};let h=null,je=null,Ce=null,L=[];try{[h,Ce,je,L]=await Promise.all([s?Promise.resolve(null):O.users.get(c),O.collections.listEntries("roles",{limit:1e3}).catch(()=>null),O.collections.get("user-profiles").catch(()=>null),O.projects.list().catch(()=>[])])}catch(e){if(!_())return;me.value=e?.status===404?"There is no such user - it may have been deleted.":`Could not load this user: ${e.message||e}`,B.value=!1,I.scan(u);return}if(!_())return;L=Array.isArray(L)?L:[],ke.value=L,x=Xe(Ce?.entries||[]),Object.keys(x).length||(x={admin:{label:"Admin",level:1},user:{label:"User",level:2}});const Le=Ge()||{};$.value=et(Le,h,x),D.value=at(x,Le,h),f.value=h;const He=u.querySelector(".uex-role-select");He.innerHTML=D.peek().map(e=>`<option value="${p(e.name)}"${e.assignable?"":" disabled"}>${p(e.label)}${e.assignable?"":" - only a more senior role can give it"}</option>`).join("");const Fe=u.querySelector(".uex-owner-select");Fe.innerHTML=`<option value="">(none)</option>${L.map(e=>`<option value="${p(e.slug)}">${p(e.name||e.slug)}</option>`).join("")}`,he.value=L.length>0;const Pe=D.peek().filter(e=>e.assignable).map(e=>e.name),Ke=Pe.includes("user")?"user":Pe.at(-1)||"user";g=(je?.fields||[]).filter(e=>e&&e.name);const G=de(h,g,{project:L.some(e=>e.slug===A)?A:"",defaultRole:Ke});g.forEach(e=>N.set(e.name,M.observable(G.profile[e.name])));const Ae=async e=>{const t=f.peek();if(t&&await Je(t,e))try{f.value={...t,...await O.users.get(t.id)}}catch{}};re.canReset=M.computed(()=>!s&&!B.value&&$.value.canEdit&&!$.value.self&&f.value?.isActive!==!1),re.resetOpen=M.computed(()=>!!f.value?.resetLinkExpires&&Date.parse(f.value.resetLinkExpires)>Date.now());const Ye=M.applyBindings(re,u,{methods:{save:le,discard:De,resetEmail:()=>Ae("email"),resetLink:()=>Ae("link")}});i(()=>Ye.dispose());const ie=g.map((e,t)=>({f:e,i:t})).filter(({f:e})=>ce(e.type)!=="hidden");if(ie.length){const e=u.querySelector(".uex-profile");e.innerHTML=ie.map(({f:l,i:d})=>it(l,d)).join("");const t={};g.forEach((l,d)=>{t[`pf_${d}`]=N.get(l.name),t[`pe_${d}`]=M.computed(()=>z.value[`profile.${l.name}`]||""),ce(l.type)==="multi"&&ue(l).forEach((te,ae)=>{t[`po_${d}_${ae}`]=M.computed(()=>(N.get(l.name).value||[]).includes(te.value))})});const o=M.applyBindings(t,e);i(()=>o.dispose());const n=l=>{const d=l.target.closest("[data-multi]");if(!d||!l.target.matches("input[data-opt]"))return;const te=N.get(g[Number(d.dataset.multi)].name),ae=l.target.dataset.opt,Te=te.peek()||[];te.value=l.target.checked?[...new Set([...Te,ae])]:Te.filter(Ve=>Ve!==ae)};e.addEventListener("change",n),i(()=>e.removeEventListener("change",n))}fe.value=ie.length>0,i(We(u,{position:"bottom"})),k.value=G,Z(G),ye.value++,B.value=!1,ve(m,s?"New user":h.name||h.email||"Edit"),I.scan(u),F.peek()||u.querySelector("#uex-name")?.focus();let P=null;try{P=S.get(Q)}catch{}if(P?.form&&!F.peek()){ee();const e=P.at?` (${st(P.at)})`:"",t=!s&&P.basedOn&&h?.updatedAt&&P.basedOn!==h.updatedAt?" This user has been changed since - restoring puts your version over it.":"",o=s?"the new user you started":`"${p(h.name||h.email)}"`;await E.confirm(`You left unsaved changes to ${o}${e}.${t} Restore them?`)&&_()&&Z({...G,...P.form,profile:{...G.profile,...P.form.profile||{}}})}}};
|