kempo 4.6.2 → 4.8.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/README.md +8 -1
- package/app-public/account/index.page.html +2 -0
- package/app-public/nav.fragment.html +2 -1
- package/app-templates/emails/email.template.html +14 -0
- package/app-templates/emails/reset-password.page.html +19 -0
- package/app-templates/emails/test.page.html +21 -0
- package/app-templates/emails/verify-email.page.html +18 -0
- package/bin/cli.js +18 -33
- package/dist/admin/components/Dashboard.js +2 -2
- package/dist/admin/components/Dashboard.js.map +1 -1
- package/dist/admin/components/EmailTemplates.js +25 -0
- package/dist/admin/components/EmailTemplates.js.map +1 -0
- package/dist/admin/components/Emails.js +49 -0
- package/dist/admin/components/Emails.js.map +1 -0
- package/dist/admin/components/Menus.js +103 -0
- package/dist/admin/components/Menus.js.map +1 -0
- package/dist/admin/components/settings/AdminSettings.js +7 -0
- package/dist/admin/components/settings/AdminSettings.js.map +1 -0
- package/dist/admin/components/settings/BooleanSetting.js +2 -0
- package/dist/admin/components/settings/BooleanSetting.js.map +1 -0
- package/dist/admin/components/settings/JsonSetting.js +2 -0
- package/dist/admin/components/settings/JsonSetting.js.map +1 -0
- package/dist/admin/components/settings/NumberSetting.js +2 -0
- package/dist/admin/components/settings/NumberSetting.js.map +1 -0
- package/dist/admin/components/settings/SecretSetting.js +2 -0
- package/dist/admin/components/settings/SecretSetting.js.map +1 -0
- package/dist/admin/components/settings/Setting.js +2 -0
- package/dist/admin/components/settings/Setting.js.map +1 -0
- package/dist/admin/components/settings/SettingEditor.js +2 -0
- package/dist/admin/components/settings/SettingEditor.js.map +1 -0
- package/dist/admin/components/settings/SettingsPanel.js +24 -0
- package/dist/admin/components/settings/SettingsPanel.js.map +1 -0
- package/dist/admin/components/settings/StringSetting.js +2 -0
- package/dist/admin/components/settings/StringSetting.js.map +1 -0
- package/dist/admin/components/settings/humanize.js +2 -0
- package/dist/admin/components/settings/humanize.js.map +1 -0
- package/dist/admin/content/menus/index.page.html +11 -0
- package/dist/admin/emails/index.page.html +14 -0
- package/dist/admin/icons/mail.svg +1 -0
- package/dist/admin/nav.fragment.html +12 -0
- package/dist/admin/settings/[owner]/[name]/index.page.html +1 -1
- package/dist/admin/settings/index.page.html +6 -289
- package/dist/kempo/api/email-templates/GET.js +2 -0
- package/dist/kempo/api/email-templates/GET.js.map +1 -0
- package/dist/kempo/api/email-templates/file/DELETE.js +2 -0
- package/dist/kempo/api/email-templates/file/DELETE.js.map +1 -0
- package/dist/kempo/api/email-templates/file/GET.js +2 -0
- package/dist/kempo/api/email-templates/file/GET.js.map +1 -0
- package/dist/kempo/api/email-templates/file/PUT.js +2 -0
- package/dist/kempo/api/email-templates/file/PUT.js.map +1 -0
- package/dist/kempo/api/email-templates/preview/GET.js +2 -0
- package/dist/kempo/api/email-templates/preview/GET.js.map +1 -0
- package/dist/kempo/api/emails/GET.js +2 -0
- package/dist/kempo/api/emails/GET.js.map +1 -0
- package/dist/kempo/api/emails/[id]/GET.js +2 -0
- package/dist/kempo/api/emails/[id]/GET.js.map +1 -0
- package/dist/kempo/api/emails/[id]/retry/POST.js +2 -0
- package/dist/kempo/api/emails/[id]/retry/POST.js.map +1 -0
- package/dist/kempo/api/emails/test/POST.js +2 -0
- package/dist/kempo/api/emails/test/POST.js.map +1 -0
- package/dist/kempo/api/fragments/DELETE.js +1 -1
- package/dist/kempo/api/fragments/DELETE.js.map +1 -1
- package/dist/kempo/api/fragments/GET.js +1 -1
- package/dist/kempo/api/fragments/GET.js.map +1 -1
- package/dist/kempo/api/fragments/POST.js +1 -1
- package/dist/kempo/api/fragments/POST.js.map +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/file/GET.js +1 -1
- package/dist/kempo/api/fragments/file/GET.js.map +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js.map +1 -1
- package/dist/kempo/api/fragments/file/PUT.js +1 -1
- package/dist/kempo/api/fragments/file/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js.map +1 -1
- package/dist/kempo/api/globals/DELETE.js +1 -1
- package/dist/kempo/api/globals/DELETE.js.map +1 -1
- package/dist/kempo/api/globals/GET.js +1 -1
- package/dist/kempo/api/globals/GET.js.map +1 -1
- package/dist/kempo/api/globals/POST.js +1 -1
- package/dist/kempo/api/globals/POST.js.map +1 -1
- package/dist/kempo/api/globals/disable/PUT.js +1 -1
- package/dist/kempo/api/globals/disable/PUT.js.map +1 -1
- package/dist/kempo/api/globals/enable/PUT.js +1 -1
- package/dist/kempo/api/globals/enable/PUT.js.map +1 -1
- package/dist/kempo/api/globals/entry/GET.js +1 -1
- package/dist/kempo/api/globals/entry/GET.js.map +1 -1
- package/dist/kempo/api/globals/entry/PUT.js +1 -1
- package/dist/kempo/api/globals/entry/PUT.js.map +1 -1
- package/dist/kempo/api/globals/lock/PUT.js +1 -1
- package/dist/kempo/api/globals/lock/PUT.js.map +1 -1
- package/dist/kempo/api/menus/DELETE.js +2 -0
- package/dist/kempo/api/menus/DELETE.js.map +1 -0
- package/dist/kempo/api/menus/GET.js +2 -0
- package/dist/kempo/api/menus/GET.js.map +1 -0
- package/dist/kempo/api/menus/POST.js +2 -0
- package/dist/kempo/api/menus/POST.js.map +1 -0
- package/dist/kempo/api/menus/adopt/PUT.js +2 -0
- package/dist/kempo/api/menus/adopt/PUT.js.map +1 -0
- package/dist/kempo/api/menus/items/DELETE.js +2 -0
- package/dist/kempo/api/menus/items/DELETE.js.map +1 -0
- package/dist/kempo/api/menus/items/POST.js +2 -0
- package/dist/kempo/api/menus/items/POST.js.map +1 -0
- package/dist/kempo/api/menus/items/enabled/PUT.js +2 -0
- package/dist/kempo/api/menus/items/enabled/PUT.js.map +1 -0
- package/dist/kempo/api/menus/items/order/PUT.js +2 -0
- package/dist/kempo/api/menus/items/order/PUT.js.map +1 -0
- package/dist/kempo/api/menus/location/PUT.js +2 -0
- package/dist/kempo/api/menus/location/PUT.js.map +1 -0
- package/dist/kempo/api/pages/DELETE.js +1 -1
- package/dist/kempo/api/pages/DELETE.js.map +1 -1
- package/dist/kempo/api/pages/GET.js +1 -1
- package/dist/kempo/api/pages/GET.js.map +1 -1
- package/dist/kempo/api/pages/POST.js +1 -1
- package/dist/kempo/api/pages/POST.js.map +1 -1
- package/dist/kempo/api/pages/directories/GET.js +1 -1
- package/dist/kempo/api/pages/directories/GET.js.map +1 -1
- package/dist/kempo/api/pages/disable/PUT.js +1 -1
- package/dist/kempo/api/pages/disable/PUT.js.map +1 -1
- package/dist/kempo/api/pages/enable/PUT.js +1 -1
- package/dist/kempo/api/pages/enable/PUT.js.map +1 -1
- package/dist/kempo/api/pages/file/GET.js +1 -1
- package/dist/kempo/api/pages/file/GET.js.map +1 -1
- package/dist/kempo/api/pages/file/PATCH.js +1 -1
- package/dist/kempo/api/pages/file/PATCH.js.map +1 -1
- package/dist/kempo/api/pages/file/PUT.js +1 -1
- package/dist/kempo/api/pages/file/PUT.js.map +1 -1
- package/dist/kempo/api/pages/lock/PUT.js +1 -1
- package/dist/kempo/api/pages/lock/PUT.js.map +1 -1
- package/dist/kempo/api/pages/rescan/POST.js +1 -1
- package/dist/kempo/api/pages/rescan/POST.js.map +1 -1
- package/dist/kempo/api/pages/search/GET.js +1 -1
- package/dist/kempo/api/pages/search/GET.js.map +1 -1
- package/dist/kempo/api/pages/templates/GET.js +1 -1
- package/dist/kempo/api/pages/templates/GET.js.map +1 -1
- package/dist/kempo/api/settings/[owner]/[name]/POST.js +1 -1
- package/dist/kempo/api/settings/[owner]/[name]/POST.js.map +1 -1
- package/dist/kempo/api/templates/DELETE.js +1 -1
- package/dist/kempo/api/templates/DELETE.js.map +1 -1
- package/dist/kempo/api/templates/GET.js +1 -1
- package/dist/kempo/api/templates/GET.js.map +1 -1
- package/dist/kempo/api/templates/POST.js +1 -1
- package/dist/kempo/api/templates/POST.js.map +1 -1
- package/dist/kempo/api/templates/disable/PUT.js +1 -1
- package/dist/kempo/api/templates/disable/PUT.js.map +1 -1
- package/dist/kempo/api/templates/enable/PUT.js +1 -1
- package/dist/kempo/api/templates/enable/PUT.js.map +1 -1
- package/dist/kempo/api/templates/file/GET.js +1 -1
- package/dist/kempo/api/templates/file/GET.js.map +1 -1
- package/dist/kempo/api/templates/file/PATCH.js +1 -1
- package/dist/kempo/api/templates/file/PATCH.js.map +1 -1
- package/dist/kempo/api/templates/file/PUT.js +1 -1
- package/dist/kempo/api/templates/file/PUT.js.map +1 -1
- package/dist/kempo/api/templates/lock/PUT.js +1 -1
- package/dist/kempo/api/templates/lock/PUT.js.map +1 -1
- package/dist/kempo/components/DashboardWidget.js +4 -4
- package/dist/kempo/components/DashboardWidget.js.map +1 -1
- package/dist/kempo/icons/campaign.svg +1 -0
- package/dist/kempo/icons/fields.svg +1 -0
- package/dist/kempo/icons/open_in_new.svg +1 -0
- package/dist/kempo/sdk.js +1 -1
- package/dist/kempo/sdk.js.map +1 -1
- package/middleware/kempo.js +69 -12
- package/package.json +3 -4
- package/server/db/schema.js +56 -0
- package/server/sdk.js +26 -0
- package/server/utils/auth/requestPasswordReset.js +9 -3
- package/server/utils/auth/verifyEmail.js +10 -1
- package/server/utils/dashboard/coreWidgets.js +1 -0
- package/server/utils/dashboard/helpers.js +6 -0
- package/server/utils/email/defaultSender.js +2 -0
- package/server/utils/email/dispatchEmail.js +56 -0
- package/server/utils/email/emailSettings.js +21 -0
- package/server/utils/email/emailTemplatePaths.js +36 -0
- package/server/utils/email/getEmail.js +14 -0
- package/server/utils/email/getSenderEmail.js +11 -0
- package/server/utils/email/getSenderFrom.js +18 -0
- package/server/utils/email/isValidSender.js +4 -0
- package/server/utils/email/listEmailTemplates.js +23 -0
- package/server/utils/email/listEmails.js +35 -0
- package/server/utils/email/maintainEmails.js +46 -0
- package/server/utils/email/pruneEmails.js +29 -0
- package/server/utils/email/readEmailTemplate.js +28 -0
- package/server/utils/email/renderEmailTemplate.js +63 -0
- package/server/utils/email/resetEmailTemplate.js +15 -0
- package/server/utils/email/retryEmail.js +21 -0
- package/server/utils/email/saveEmailTemplate.js +38 -0
- package/server/utils/email/sendEmail.js +22 -32
- package/server/utils/email/sendEmailFromTemplate.js +9 -27
- package/server/utils/extensions/aliases.js +71 -0
- package/server/utils/extensions/checkClaims.js +32 -0
- package/server/utils/extensions/claims.js +54 -0
- package/server/utils/extensions/createTablesFromSchema.js +13 -20
- package/server/utils/extensions/enableExtensionTree.js +8 -0
- package/server/utils/extensions/installExtension.js +32 -0
- package/server/utils/extensions/readKempoConfig.js +2 -2
- package/server/utils/extensions/scopeCache.js +14 -1
- package/server/utils/extensions/scopes.js +72 -0
- package/server/utils/extensions/uninstallExtensionTree.js +2 -1
- package/server/utils/extensions/updateExtension.js +35 -0
- package/server/utils/global-content/createGlobalContent.js +8 -18
- package/server/utils/global-content/deleteGlobalContent.js +1 -16
- package/server/utils/global-content/disableGlobalContent.js +1 -16
- package/server/utils/global-content/enableGlobalContent.js +1 -16
- package/server/utils/global-content/helpers.js +40 -6
- package/server/utils/global-content/listGlobalContent.js +2 -2
- package/server/utils/global-content/setGlobalContentLocked.js +1 -16
- package/server/utils/global-content/updateGlobalContent.js +1 -16
- package/server/utils/hooks/triggerHook.js +17 -4
- package/server/utils/menus/adoptMenuItem.js +58 -0
- package/server/utils/menus/assignMenuLocation.js +47 -0
- package/server/utils/menus/deleteMenu.js +27 -0
- package/server/utils/menus/deleteMenuItems.js +27 -0
- package/server/utils/menus/findMenuLocations.js +41 -0
- package/server/utils/menus/findNavItems.js +74 -0
- package/server/utils/menus/findPages.js +73 -0
- package/server/utils/menus/listMenus.js +103 -0
- package/server/utils/menus/markup.js +55 -0
- package/server/utils/menus/reorderMenuItems.js +36 -0
- package/server/utils/menus/saveMenu.js +54 -0
- package/server/utils/menus/saveMenuItem.js +122 -0
- package/server/utils/menus/setMenuItemEnabled.js +31 -0
- package/server/utils/menus/store.js +90 -0
- package/server/utils/scheduler/helpers.js +36 -0
- package/server/utils/scheduler/listJobs.js +11 -0
- package/server/utils/scheduler/registerJobs.js +37 -0
- package/server/utils/scheduler/runJobs.js +90 -0
- package/server/utils/scheduler/scheduler.js +37 -0
- package/server/utils/scheduler/unregisterJobs.js +16 -0
- package/server/utils/settings/ensureEncryptionKey.js +132 -0
- package/server/utils/settings/listSettings.js +10 -5
- package/server/utils/settings/secretCrypto.js +5 -1
- package/server/utils/settings/setSetting.js +4 -0
- package/server/utils/settings/settingRules.js +21 -0
- package/server/utils/settings/systemSettings.js +40 -0
- package/server/utils/setup/seedDatabase.js +5 -60
- package/server/utils/setup/systemPermissions.js +75 -0
- package/dist/kempo/icons/create_new_folder.svg +0 -1
- package/dist/kempo/icons/credit_card.svg +0 -1
- package/dist/kempo/icons/login.svg +0 -1
- package/dist/kempo/icons/payments.svg +0 -1
- package/dist/kempo/icons/toll.svg +0 -1
- package/dist/kempo/icons/wallet.svg +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\Menus.js","names":["ShadowComponent","html","nothing","live","listMenus","deleteMenu","assignMenuLocation","saveMenuItem","deleteMenuItems","setMenuItemEnabled","reorderMenuItems","adoptMenuItem","getAllCurrentUserPermissions","Dialog","Toast","Menus","static","loading","state","failed","menus","locations","navItems","can","constructor","super","this","create","update","delete","pages","connectedCallback","permData","perms","Set","permissions","has","load","async","error","data","Boolean","msg","run","request","success","pageOptions","map","page","value","url","label","title","iconOf","item","navItem","find","n","key","icon","iconSrcOf","iconSrc","menuAt","place","m","id","menuId","otherPlaces","filter","l","handleSort","list","e","currentTarget","ids","querySelectorAll","dataset","menu","handleToggle","enabled","disabled","handleDeleteItem","confirm","confirmed","handleDeleteMenu","name","items","length","handleMenuChange","location","target","handleAdopt","menuName","showKind","$dialog","supplied","querySelector","hidden","$description","textContent","description","extension","openLinkEditor","editing","newTab","confirmText","cancelText","confirmAction","chosen","href","o","trim","checked","close","$href","document","createElement","className","setAttribute","setOptions","appendChild","renderLink","body","available","startsWith","split","editable","renderPlace","shared","declared","join","extensionItems","owner","candidates","renderUnused","unused","render","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing, live } from '/kempo-ui/lit-all.min.js';\nimport {\n listMenus, deleteMenu, assignMenuLocation,\n saveMenuItem, deleteMenuItems, setMenuItemEnabled, reorderMenuItems, adoptMenuItem,\n getAllCurrentUserPermissions\n} from '/kempo/sdk.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport '/kempo-ui/components/Icon.js';\nimport '/kempo-ui/components/Sortable.js';\nimport '/kempo-ui/components/Combobox.js';\n\n/*\n The places in the theme that show a menu (marked `menu=\"true\"`, see Menus in the docs), each with\n its links. Adding a link to a place is the whole job: the menu behind it is made on the first one.\n A place can show another place's links instead, and a menu that no place shows is listed at the end.\n Both are saved as ordinary global content, so nothing here needs the theme to know about menus.\n*/\nexport default class Menus extends ShadowComponent {\n static properties = {\n loading: { state: true },\n failed: { state: true },\n menus: { state: true },\n locations: { state: true },\n navItems: { state: true },\n can: { state: true }\n };\n\n constructor(){\n super();\n this.loading = true;\n this.failed = false;\n this.menus = [];\n this.locations = [];\n this.navItems = [];\n this.can = { create: false, update: false, delete: false };\n this.pages = [];\n }\n\n /*\n Lifecycle Callbacks\n */\n\n async connectedCallback(){\n super.connectedCallback();\n const [, permData] = await getAllCurrentUserPermissions();\n const perms = new Set(permData?.permissions ?? []);\n this.can = {\n create: perms.has('system:globals:create'),\n update: perms.has('system:globals:update'),\n delete: perms.has('system:globals:delete')\n };\n await this.load();\n }\n\n /*\n Data\n */\n\n load = async () => {\n const [error, data] = await listMenus();\n this.loading = false;\n this.failed = Boolean(error);\n if(error){\n Toast.error(error.msg || 'Failed to load menus');\n return;\n }\n this.menus = data.menus;\n this.locations = data.locations;\n this.navItems = data.navItems;\n this.pages = data.pages ?? [];\n };\n\n run = async (request, success) => {\n const [error] = await request;\n if(error){\n Toast.error(error.msg || 'That did not work');\n await this.load();\n return false;\n }\n if(success) Toast.success(success);\n await this.load();\n return true;\n };\n\n /* Every page a link can go to, the site's and its extensions' (/products/, /categories/), for the Link box. */\n get pageOptions(){\n return this.pages.map(page => ({ value: page.url, label: `${page.title} (${page.url})` }));\n }\n\n iconOf = item => item.navItem ? this.navItems.find(n => n.key === item.navItem)?.icon : item.icon;\n\n iconSrcOf = item => item.navItem ? this.navItems.find(n => n.key === item.navItem)?.iconSrc : '';\n\n menuAt = place => this.menus.find(m => m.id === place.menuId);\n\n otherPlaces = place => this.locations.filter(l => l.menuId && l.menuId === place.menuId && l !== place);\n\n /*\n Event Handlers\n */\n\n handleSort = async e => {\n const list = e.currentTarget;\n const ids = [...list.querySelectorAll('k-sortable-item')].map(item => item.dataset.id);\n await this.run(reorderMenuItems({ menuId: list.dataset.menu, ids }));\n };\n\n handleToggle = item => this.run(setMenuItemEnabled({ id: item.id, enabled: item.disabled }));\n\n handleDeleteItem = item => Dialog.confirm(`Delete \"${item.label}\" from the menu?`, async confirmed => {\n if(confirmed) await this.run(deleteMenuItems([item.id]), 'Item deleted');\n });\n\n handleDeleteMenu = menu => Dialog.confirm(\n `Delete the unused \"${menu.name}\" menu and its ${menu.items.length} item${menu.items.length === 1 ? '' : 's'}? This cannot be undone.`,\n async confirmed => {\n if(confirmed) await this.run(deleteMenu(menu.id), 'Menu deleted');\n }\n );\n\n handleMenuChange = (place, e) => this.run(assignMenuLocation({ location: place.location, menuId: e.target.value }));\n\n handleAdopt = (place, item) => this.run(adoptMenuItem({ id: item.id, menuName: place.label }), `\"${item.label}\" can now be reordered and edited here`);\n\n showKind = ($dialog, key) => {\n const supplied = this.navItems.find(n => n.key === key);\n $dialog.querySelector('#menu-link-fields').hidden = Boolean(supplied);\n const $description = $dialog.querySelector('#menu-kind-description');\n $description.hidden = !supplied;\n $description.textContent = supplied ? supplied.description || `${supplied.label} from ${supplied.extension}.` : '';\n };\n\n openLinkEditor = (place, item) => {\n const editing = Boolean(item);\n const $dialog = Dialog.create(html`\n <div class=\"p\">\n ${!editing && this.navItems.length ? html`\n <div class=\"mb\">\n <label class=\"d-b mbq\" for=\"menu-kind\">What to add<small class=\"d-b tc-muted\">A link to a page or address, or something an installed extension offers, such as a cart.</small></label>\n <select id=\"menu-kind\" class=\"full\" @change=${e => this.showKind($dialog, e.target.value)}>\n <option value=\"\">A link</option>\n ${[...new Set(this.navItems.map(n => n.extension))].map(extension => html`\n <optgroup label=${extension === 'kempo' ? 'Built in' : extension}>\n ${this.navItems.filter(n => n.extension === extension).map(n => html`<option value=${n.key}>${n.label}</option>`)}\n </optgroup>\n `)}\n </select>\n </div>\n <p id=\"menu-kind-description\" class=\"tc-muted mb\" hidden></p>\n ` : nothing}\n <div id=\"menu-link-fields\">\n <div class=\"mb\">\n <label class=\"d-b mbq\" for=\"menu-label\">Label</label>\n <input type=\"text\" id=\"menu-label\" class=\"full\" placeholder=\"About\" .value=${item?.label ?? ''}>\n </div>\n <div class=\"mb\">\n <label class=\"d-b mbq\" for=\"menu-href\">Link<small class=\"d-b tc-muted\">Choose one of your pages, or type any address.</small></label>\n <div id=\"menu-href-wrap\"></div>\n </div>\n <div class=\"mb\">\n <label class=\"d-b mbq\" for=\"menu-icon\">Icon (optional)<small class=\"d-b tc-muted\">The name of an icon, such as <strong>home</strong> or <strong>star</strong>.</small></label>\n <input type=\"text\" id=\"menu-icon\" class=\"full\" .value=${item?.icon ?? ''}>\n </div>\n <label class=\"checkbox\"><input type=\"checkbox\" id=\"menu-new-tab\" ?checked=${item?.newTab}> Open in a new tab</label>\n </div>\n </div>\n `, {\n title: editing ? 'Edit link' : `Add an item to ${place.label}`,\n confirmText: editing ? 'Save' : 'Add item',\n cancelText: 'Cancel',\n confirmAction: async () => {\n // A combobox reports the label of a chosen option, so a chosen page is mapped back to its address.\n const chosen = $dialog.querySelector('#menu-href').value;\n const navItem = $dialog.querySelector('#menu-kind')?.value;\n const saved = await this.run(navItem ? saveMenuItem({ location: place.location, navItem }) : saveMenuItem({\n id: item?.id,\n location: place.location,\n label: $dialog.querySelector('#menu-label').value,\n href: this.pageOptions.find(o => o.label === chosen)?.value ?? chosen,\n icon: $dialog.querySelector('#menu-icon').value.trim(),\n newTab: $dialog.querySelector('#menu-new-tab').checked\n }), editing ? 'Link saved' : 'Item added');\n if(saved) $dialog.close();\n }\n });\n\n const $href = document.createElement('k-combobox');\n $href.id = 'menu-href';\n $href.className = 'full';\n $href.setAttribute('placeholder', '/about');\n $href.setOptions(this.pageOptions);\n $dialog.querySelector('#menu-href-wrap').appendChild($href);\n if(item?.href) $href.value = item.href;\n };\n\n /*\n Rendering\n */\n\n renderLink(place, item){\n const body = html`\n <div class=\"d-f\" style=\"align-items: center; gap: var(--spacer_h);\">\n <div class=\"flex\">\n <strong class=${item.disabled || !item.available ? 'tc-muted' : ''}>${this.iconSrcOf(item) ? html`<k-icon src=${this.iconSrcOf(item)}></k-icon> ` : this.iconOf(item) ? html`<k-icon name=${this.iconOf(item)}></k-icon> ` : nothing}${item.label}</strong>\n <span class=\"tc-muted\"> ${item.navItem ? (item.navItem.startsWith('kempo:') ? 'built in' : `from ${item.navItem.split(':')[0]}`) : item.href}</span>\n ${item.available ? nothing : html`<span class=\"tc-danger\"> not on the site: its extension is disabled or removed</span>`}\n ${item.newTab ? html`<k-icon name=\"open_in_new\"></k-icon>` : nothing}\n ${item.disabled ? html`<span class=\"tc-muted\"> (hidden)</span>` : nothing}\n </div>\n ${this.can.update ? html`\n <div>\n ${item.editable ? html`<button class=\"btn\" title=\"Edit\" @click=${() => this.openLinkEditor(place, item)}><k-icon name=\"edit\"></k-icon></button>` : nothing}\n <button class=\"btn\" title=${item.disabled ? 'Show on the site' : 'Hide from the site'} @click=${() => this.handleToggle(item)}><k-icon name=${item.disabled ? 'show' : 'hide'}></k-icon></button>\n ${this.can.delete ? html`<button class=\"btn danger\" title=\"Delete\" @click=${() => this.handleDeleteItem(item)}><k-icon name=\"delete\"></k-icon></button>` : nothing}\n </div>\n ` : nothing}\n </div>\n `;\n return this.can.update\n ? html`<k-sortable-item data-id=${item.id}>${body}</k-sortable-item>`\n : html`<div class=\"b p mbq\">${body}</div>`;\n }\n\n renderPlace(place){\n const menu = this.menuAt(place);\n const shared = this.otherPlaces(place);\n return html`\n <div class=\"mb\">\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer_h);\">\n <div class=\"flex\">\n <h2 class=\"mb0\">${place.label}</h2>\n ${place.declared ? nothing : html`<span class=\"tc-danger\">The theme no longer has this place (<strong>${place.location}</strong>), so nothing shows there.</span>`}\n ${shared.length ? html`<span class=\"tc-muted\">Shows the same links as ${shared.map(l => l.label).join(', ')}.</span>` : nothing}\n </div>\n ${this.can.create && place.declared ? html`<button class=\"primary\" @click=${() => this.openLinkEditor(place)}><k-icon name=\"add\"></k-icon> Add item</button>` : nothing}\n </div>\n ${menu?.items.length ? html`\n <k-sortable data-menu=${menu.id} @sort=${this.handleSort}>\n ${menu.items.map(item => this.renderLink(place, item))}\n </k-sortable>\n ` : nothing}\n ${place.extensionItems.map(item => html`\n <div class=\"b p mbq\"><strong>${item.label}</strong> <span class=\"tc-muted\">${item.href} · added by ${item.owner}</span></div>\n `)}\n ${place.candidates.map(item => html`\n <div class=\"b p mbq d-f\" style=\"align-items: center; gap: var(--spacer_h);\">\n <div class=\"flex\"><strong>${item.label}</strong> <span class=\"tc-muted\">${item.href} · added as global content</span></div>\n ${this.can.update ? html`<button class=\"btn\" @click=${() => this.handleAdopt(place, item)}>Manage here</button>` : nothing}\n </div>\n `)}\n ${!menu?.items.length && !place.extensionItems.length && !place.candidates.length ? html`<p class=\"tc-muted\">Nothing here yet.</p>` : nothing}\n ${this.can.update && this.menus.length > 1 ? html`\n <label class=\"d-b mtq tc-muted\">Show the links of another place here\n <select @change=${e => this.handleMenuChange(place, e)}>\n <option value=\"\" .selected=${live(!place.menuId)}>This place's own links</option>\n ${this.menus.map(m => html`<option value=${m.id} .selected=${live(place.menuId === m.id)}>${m.name}</option>`)}\n </select>\n </label>\n ` : nothing}\n </div>\n `;\n }\n\n renderUnused(){\n const unused = this.menus.filter(m => !m.locations.length);\n if(!unused.length) return nothing;\n return html`\n <h2>Not shown anywhere</h2>\n <p class=\"tc-muted\">These menus are not shown in any place. Pick one in a place's dropdown above to use it again, or delete it.</p>\n ${unused.map(menu => html`\n <div class=\"b p mbq d-f\" style=\"align-items: center; gap: var(--spacer_h);\">\n <div class=\"flex\"><strong>${menu.name}</strong> <span class=\"tc-muted\">${menu.items.length} item${menu.items.length === 1 ? '' : 's'}</span></div>\n ${this.can.delete ? html`<button class=\"btn danger\" title=\"Delete menu\" @click=${() => this.handleDeleteMenu(menu)}><k-icon name=\"delete\"></k-icon></button>` : nothing}\n </div>\n `)}\n `;\n }\n\n render(){\n if(this.loading) return html`<p>Loading...</p>`;\n if(this.failed) return html`<h1>Menus</h1><p class=\"tc-danger\">The menus could not be loaded.</p>`;\n return html`\n <h1>Menus</h1>\n ${this.locations.length ? html`\n ${this.locations.map(place => this.renderPlace(place))}\n ${this.renderUnused()}\n ` : html`\n <p>Your theme does not have a place for a menu yet.</p>\n <p class=\"tc-muted\">Mark a place in your navigation, for example <code><location name=\"main-nav-links\" label=\"Main menu\" menu=\"true\" /></code> in your navigation fragment, and it will be listed here.</p>\n `}\n `;\n }\n}\n\ncustomElements.define('admin-menus', Menus);\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import t from"/kempo-ui/components/ShadowComponent.js";import{html as e}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Tabs.js";import"./SettingsPanel.js";import s from"./humanize.js";export default class n extends t{static properties={extensionTabs:{state:!0}};constructor(){super(),this.extensionTabs=[]}connectedCallback(){super.connectedCallback(),this.extensionTabs=[...this.querySelectorAll(":scope > k-settings-tab")].map((t,e)=>{const n=t.getAttribute("name")||t.getAttribute("label")||`tab-${e}`;return t.slot=`tab-${n}`,t.style.display="block",{id:n,slotName:`tab-${n}`,label:t.getAttribute("label")||s(n),position:Number(t.getAttribute("position"))||500,extension:!0}})}render(){const t=[{id:"custom",label:"Custom",position:100,content:()=>e`<k-settings-panel owner="custom"></k-settings-panel>`},{id:"system",label:"System",position:200,content:()=>e`<k-settings-panel owner="system"></k-settings-panel>`},...this.extensionTabs].sort((t,e)=>t.position-e.position);return e`
|
|
2
|
+
<k-tabs>
|
|
3
|
+
${t.map(t=>e`<k-tab for=${t.id}>${t.label}</k-tab>`)}
|
|
4
|
+
${t.map(t=>e`<k-tab-content name=${t.id} class="pt">${t.extension?e`<slot name=${t.slotName}></slot>`:t.content()}</k-tab-content>`)}
|
|
5
|
+
</k-tabs>
|
|
6
|
+
`}}customElements.define("k-admin-settings",n);
|
|
7
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\AdminSettings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\AdminSettings.js","names":["ShadowComponent","html","humanize","AdminSettings","static","extensionTabs","state","constructor","super","this","connectedCallback","querySelectorAll","map","$tab","index","name","getAttribute","slot","style","display","id","slotName","label","position","Number","extension","render","tabs","content","sort","a","b","tab","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Tabs.js';\nimport './SettingsPanel.js';\nimport humanize from './humanize.js';\n\n/*\n The admin Settings page, served at /admin/components/settings/AdminSettings.js. Tabs: **Custom** (the site's own settings) and **System**\n (kempo's), then a tab for each extension that chooses to add one. An extension that does not add a tab has nothing here: it manages\n its own settings wherever it likes.\n\n **An extension adds a tab** by filling the `admin-settings-tabs` location of the page with a `k-settings-tab` (and loads what the tab\n uses from the `admin-settings-scripts` location). `position` orders the tabs (Custom is 100, System 200, a tab with none 500):\n\n <k-settings-tab name=\"shipping\" label=\"Shipping\" position=\"500\">\n <k-settings-panel owner=\"kempo-shipping\"></k-settings-panel>\n </k-settings-tab>\n\n The tab holds anything: the generic panel above, a few of the extension's settings with their editors, or a screen of its own.\n*/\nconst DEFAULT_TAB_POSITION = 500;\n\nexport default class AdminSettings extends ShadowComponent {\n static properties = {\n extensionTabs: { state: true },\n };\n\n constructor(){\n super();\n this.extensionTabs = [];\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.extensionTabs = [...this.querySelectorAll(':scope > k-settings-tab')].map(($tab, index) => {\n const name = $tab.getAttribute('name') || $tab.getAttribute('label') || `tab-${index}`;\n $tab.slot = `tab-${name}`;\n $tab.style.display = 'block';\n return {\n id: name,\n slotName: `tab-${name}`,\n label: $tab.getAttribute('label') || humanize(name),\n position: Number($tab.getAttribute('position')) || DEFAULT_TAB_POSITION,\n extension: true,\n };\n });\n }\n\n /*\n Rendering\n */\n render(){\n const tabs = [\n { id: 'custom', label: 'Custom', position: 100, content: () => html`<k-settings-panel owner=\"custom\"></k-settings-panel>` },\n { id: 'system', label: 'System', position: 200, content: () => html`<k-settings-panel owner=\"system\"></k-settings-panel>` },\n ...this.extensionTabs,\n ].sort((a, b) => a.position - b.position);\n\n return html`\n <k-tabs>\n ${tabs.map(tab => html`<k-tab for=${tab.id}>${tab.label}</k-tab>`)}\n ${tabs.map(tab => html`<k-tab-content name=${tab.id} class=\"pt\">${tab.extension ? html`<slot name=${tab.slotName}></slot>` : tab.content()}</k-tab-content>`)}\n </k-tabs>\n `;\n }\n}\n\ncustomElements.define('k-admin-settings', AdminSettings);\n"],"mappings":"OAAOA,MAAqB,yDACnBC,MAAY,iCACd,qCACA,4BACAC,MAAc,+BAkBN,MAAMC,UAAsBH,EACzCI,kBAAoB,CAClBC,cAAe,CAAEC,OAAO,IAG1B,WAAAC,GACEC,QACAC,KAAKJ,cAAgB,EACvB,CAKA,iBAAAK,GACEF,MAAME,oBACND,KAAKJ,cAAgB,IAAII,KAAKE,iBAAiB,4BAA4BC,IAAI,CAACC,EAAMC,KACpF,MAAMC,EAAOF,EAAKG,aAAa,SAAWH,EAAKG,aAAa,UAAY,OAAOF,IAG/E,OAFAD,EAAKI,KAAO,OAAOF,IACnBF,EAAKK,MAAMC,QAAU,QACd,CACLC,GAAIL,EACJM,SAAU,OAAON,IACjBO,MAAOT,EAAKG,aAAa,UAAYd,EAASa,GAC9CQ,SAAUC,OAAOX,EAAKG,aAAa,cAzBd,IA0BrBS,WAAW,IAGjB,CAKA,MAAAC,GACE,MAAMC,EAAO,CACX,CAAEP,GAAI,SAAUE,MAAO,SAAUC,SAAU,IAAKK,QAAS,IAAM3B,CAAI,wDACnE,CAAEmB,GAAI,SAAUE,MAAO,SAAUC,SAAU,IAAKK,QAAS,IAAM3B,CAAI,2DAChEQ,KAAKJ,eACRwB,KAAK,CAACC,EAAGC,IAAMD,EAAEP,SAAWQ,EAAER,UAEhC,OAAOtB,CAAI;;UAEL0B,EAAKf,IAAIoB,GAAO/B,CAAI,cAAc+B,EAAIZ,MAAMY,EAAIV;UAChDK,EAAKf,IAAIoB,GAAO/B,CAAI,uBAAuB+B,EAAIZ,iBAAiBY,EAAIP,UAAYxB,CAAI,cAAc+B,EAAIX,mBAAqBW,EAAIJ;;KAGvI,EAGFK,eAAeC,OAAO,mBAAoB/B","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as e}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Toggle.js";import t from"./SettingEditor.js";export default class i extends t{render(){return e`<div class="mb"><k-toggle ?disabled=${this.disabled} .value=${!0===this.value||"true"===this.value} @change=${e=>this.announce(!0===e.target.value)}>${this.heading}</k-toggle>${this.renderHint()}</div>`}}customElements.define("k-setting-boolean",i);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\BooleanSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\BooleanSetting.js","names":["html","SettingEditor","BooleanSetting","render","this","disabled","value","event","announce","target","heading","renderHint","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Toggle.js';\nimport SettingEditor from './SettingEditor.js';\n\n/* A boolean setting: a toggle with the name beside it and the description under it. */\nexport default class BooleanSetting extends SettingEditor {\n render(){\n return html`<div class=\"mb\"><k-toggle ?disabled=${this.disabled} .value=${this.value === true || this.value === 'true'} @change=${event => this.announce(event.target.value === true)}>${this.heading}</k-toggle>${this.renderHint()}</div>`;\n }\n}\n\ncustomElements.define('k-setting-boolean', BooleanSetting);\n"],"mappings":"eAASA,MAAY,iCACd,wCACAC,MAAmB,oCAGX,MAAMC,UAAuBD,EAC1C,MAAAE,GACE,OAAOH,CAAI,uCAAuCI,KAAKC,oBAAkC,IAAfD,KAAKE,OAAiC,SAAfF,KAAKE,iBAA4BC,GAASH,KAAKI,UAAgC,IAAvBD,EAAME,OAAOH,UAAmBF,KAAKM,qBAAqBN,KAAKO,oBAC1N,EAGFC,eAAeC,OAAO,oBAAqBX","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as t}from"/kempo-ui/lit-all.min.js";import e from"./SettingEditor.js";export default class i extends e{text="";willUpdate(t){if(!t.has("value"))return;let e;try{e=JSON.parse(this.text)}catch{e=void 0}void 0!==e&&JSON.stringify(e)===JSON.stringify(this.value)||(this.text=JSON.stringify(this.value??null,null,2))}change=t=>{this.text=t.target.value;try{this.announce(JSON.parse(this.text))}catch{this.announce(this.value,"That is not valid JSON")}};renderControl(){return t`<textarea class="ff-mono" rows="8" ?disabled=${this.disabled} .value=${this.text} @input=${this.change}></textarea>`}}customElements.define("k-setting-json",i);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\JsonSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\JsonSetting.js","names":["html","SettingEditor","JsonSetting","text","willUpdate","changed","has","typed","JSON","parse","this","undefined","stringify","value","change","event","target","announce","renderControl","disabled","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/*\n A JSON setting: the formatted JSON in a box. Text that is not JSON is reported as a problem and is not saved. What is typed is\n kept as typed (it is only formatted again when the value is set from outside), so the cursor does not jump while editing.\n*/\nexport default class JsonSetting extends SettingEditor {\n text = '';\n\n willUpdate(changed){\n if(!changed.has('value')) return;\n let typed;\n try { typed = JSON.parse(this.text); } catch { typed = undefined; }\n if(typed === undefined || JSON.stringify(typed) !== JSON.stringify(this.value)) this.text = JSON.stringify(this.value ?? null, null, 2);\n }\n\n change = event => {\n this.text = event.target.value;\n try {\n this.announce(JSON.parse(this.text));\n } catch {\n this.announce(this.value, 'That is not valid JSON');\n }\n };\n\n renderControl(){\n return html`<textarea class=\"ff-mono\" rows=\"8\" ?disabled=${this.disabled} .value=${this.text} @input=${this.change}></textarea>`;\n }\n}\n\ncustomElements.define('k-setting-json', JsonSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAMX,MAAMC,UAAoBD,EACvCE,KAAO,GAEP,UAAAC,CAAWC,GACT,IAAIA,EAAQC,IAAI,SAAU,OAC1B,IAAIC,EACJ,IAAMA,EAAQC,KAAKC,MAAMC,KAAKP,KAAO,CAAE,MAAQI,OAAQI,CAAW,MACrDA,IAAVJ,GAAuBC,KAAKI,UAAUL,KAAWC,KAAKI,UAAUF,KAAKG,SAAQH,KAAKP,KAAOK,KAAKI,UAAUF,KAAKG,OAAS,KAAM,KAAM,GACvI,CAEAC,OAASC,IACPL,KAAKP,KAAOY,EAAMC,OAAOH,MACzB,IACEH,KAAKO,SAAST,KAAKC,MAAMC,KAAKP,MAChC,CAAE,MACAO,KAAKO,SAASP,KAAKG,MAAO,yBAC5B,GAGF,aAAAK,GACE,OAAOlB,CAAI,gDAAgDU,KAAKS,mBAAmBT,KAAKP,eAAeO,KAAKI,oBAC9G,EAGFM,eAAeC,OAAO,iBAAkBnB","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as e}from"/kempo-ui/lit-all.min.js";import t from"./SettingEditor.js";export default class n extends t{change=e=>{const t=e.target.value,n=Number(t);""!==t.trim()&&Number.isFinite(n)?this.announce(n):this.announce(this.value,"Enter a number")};renderControl(){return e`<input type="number" step="any" ?disabled=${this.disabled} .value=${String(this.value??0)} @input=${this.change}>`}}customElements.define("k-setting-number",n);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\NumberSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\NumberSetting.js","names":["html","SettingEditor","NumberSetting","change","event","text","target","value","number","Number","trim","isFinite","this","announce","renderControl","disabled","String","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/* A number setting: a number field. An empty or non-numeric entry is reported as a problem instead of being saved as NaN. */\nexport default class NumberSetting extends SettingEditor {\n change = event => {\n const text = event.target.value;\n const number = Number(text);\n if(text.trim() === '' || !Number.isFinite(number)) this.announce(this.value, 'Enter a number');\n else this.announce(number);\n };\n\n renderControl(){\n return html`<input type=\"number\" step=\"any\" ?disabled=${this.disabled} .value=${String(this.value ?? 0)} @input=${this.change}>`;\n }\n}\n\ncustomElements.define('k-setting-number', NumberSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAGX,MAAMC,UAAsBD,EACzCE,OAASC,IACP,MAAMC,EAAOD,EAAME,OAAOC,MACpBC,EAASC,OAAOJ,GACH,KAAhBA,EAAKK,QAAkBD,OAAOE,SAASH,GACrCI,KAAKC,SAASL,GADgCI,KAAKC,SAASD,KAAKL,MAAO,mBAI/E,aAAAO,GACE,OAAOd,CAAI,6CAA6CY,KAAKG,mBAAmBC,OAAOJ,KAAKL,OAAS,aAAaK,KAAKT,SACzH,EAGFc,eAAeC,OAAO,mBAAoBhB","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as e}from"/kempo-ui/lit-all.min.js";import t from"./SettingEditor.js";export default class s extends t{static properties={...t.properties,saved:{type:Boolean}};constructor(){super(),this.saved=!0,this.value=""}renderHint(){return e`${super.renderHint()}<small class="tc-muted d-b">${this.saved?"A value is saved. It is stored encrypted and never shown. Type a new one to replace it.":"Not set yet. It is stored encrypted and never shown again."}</small>`}renderControl(){return e`<input type="password" autocomplete="new-password" ?disabled=${this.disabled} .value=${this.value??""} @input=${e=>this.announce(e.target.value)}>`}}customElements.define("k-setting-secret",s);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SecretSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\SecretSetting.js","names":["html","SettingEditor","SecretSetting","static","properties","saved","type","Boolean","constructor","super","this","value","renderHint","renderControl","disabled","event","announce","target","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/*\n A secret setting (an API key, a signing secret). It is replace-only: the saved value is never sent to the browser, so the field starts\n empty and an empty field means \"keep what is saved\". Typing a new value announces it, and clearing the field again takes it back.\n*/\nexport default class SecretSetting extends SettingEditor {\n static properties = { ...SettingEditor.properties, saved: { type: Boolean } };\n\n constructor(){\n super();\n this.saved = true;\n this.value = '';\n }\n\n renderHint(){\n return html`${super.renderHint()}<small class=\"tc-muted d-b\">${this.saved ? 'A value is saved. It is stored encrypted and never shown. Type a new one to replace it.' : 'Not set yet. It is stored encrypted and never shown again.'}</small>`;\n }\n\n renderControl(){\n return html`<input type=\"password\" autocomplete=\"new-password\" ?disabled=${this.disabled} .value=${this.value ?? ''} @input=${event => this.announce(event.target.value)}>`;\n }\n}\n\ncustomElements.define('k-setting-secret', SecretSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAMX,MAAMC,UAAsBD,EACzCE,kBAAoB,IAAKF,EAAcG,WAAYC,MAAO,CAAEC,KAAMC,UAElE,WAAAC,GACEC,QACAC,KAAKL,OAAQ,EACbK,KAAKC,MAAQ,EACf,CAEA,UAAAC,GACE,OAAOZ,CAAI,GAAGS,MAAMG,2CAA2CF,KAAKL,MAAQ,0FAA4F,sEAC1K,CAEA,aAAAQ,GACE,OAAOb,CAAI,gEAAgEU,KAAKI,mBAAmBJ,KAAKC,OAAS,aAAaI,GAASL,KAAKM,SAASD,EAAME,OAAON,SACpK,EAGFO,eAAeC,OAAO,mBAAoBjB","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t}from"/kempo-ui/lit-all.min.js";import"./StringSetting.js";import"./NumberSetting.js";import"./BooleanSetting.js";import"./JsonSetting.js";import"./SecretSetting.js";export default class s extends e{static properties={setting:{attribute:!1},disabled:{type:Boolean}};constructor(){super(),this.setting=null,this.disabled=!1}render(){const{name:e,value:s,type:i,description:n}=this.setting??{};if(!e)return null;switch(i){case"number":return t`<k-setting-number name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-number>`;case"boolean":return t`<k-setting-boolean name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-boolean>`;case"json":return t`<k-setting-json name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-json>`;case"secret":return t`<k-setting-secret name=${e} description=${n??""} ?disabled=${this.disabled} .saved=${null!=s} .value=${""}></k-setting-secret>`;default:return t`<k-setting-string name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s??""}></k-setting-string>`}}}customElements.define("k-setting",s);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\Setting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\Setting.js","names":["ShadowComponent","html","Setting","static","setting","attribute","disabled","type","Boolean","constructor","super","this","render","name","value","description","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport './StringSetting.js';\nimport './NumberSetting.js';\nimport './BooleanSetting.js';\nimport './JsonSetting.js';\nimport './SecretSetting.js';\n\n/*\n The editor for a setting, chosen by its type, served at /admin/components/settings/Setting.js:\n\n <k-setting .setting=${{ name: 'site_name', value: 'My Site', type: 'string', description: 'The name of the website' }}></k-setting>\n\n `name` is the setting's own name without its owner (`site_name`, not `system:site_name`); the readable title is made from it. An\n unknown type is edited as a string. The editor's `setting-change` event bubbles out of this element unchanged.\n*/\nexport default class Setting extends ShadowComponent {\n static properties = {\n setting: { attribute: false },\n disabled: { type: Boolean },\n };\n\n constructor(){\n super();\n this.setting = null;\n this.disabled = false;\n }\n\n render(){\n const { name, value, type, description } = this.setting ?? {};\n if(!name) return null;\n switch(type){\n case 'number': return html`<k-setting-number name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-number>`;\n case 'boolean': return html`<k-setting-boolean name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-boolean>`;\n case 'json': return html`<k-setting-json name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-json>`;\n case 'secret': return html`<k-setting-secret name=${name} description=${description ?? ''} ?disabled=${this.disabled} .saved=${value !== null && value !== undefined} .value=${''}></k-setting-secret>`;\n default: return html`<k-setting-string name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value ?? ''}></k-setting-string>`;\n }\n }\n}\n\ncustomElements.define('k-setting', Setting);\n"],"mappings":"OAAOA,MAAqB,yDACnBC,MAAY,iCACd,2BACA,2BACA,4BACA,yBACA,oCAUQ,MAAMC,UAAgBF,EACnCG,kBAAoB,CAClBC,QAAS,CAAEC,WAAW,GACtBC,SAAU,CAAEC,KAAMC,UAGpB,WAAAC,GACEC,QACAC,KAAKP,QAAU,KACfO,KAAKL,UAAW,CAClB,CAEA,MAAAM,GACE,MAAMC,KAAEA,EAAIC,MAAEA,EAAKP,KAAEA,EAAIQ,YAAEA,GAAgBJ,KAAKP,SAAW,CAAC,EAC5D,IAAIS,EAAM,OAAO,KACjB,OAAON,GACL,IAAK,SAAU,OAAON,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,wBAC/H,IAAK,UAAW,OAAOb,CAAI,2BAA2BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,yBACjI,IAAK,OAAQ,OAAOb,CAAI,wBAAwBY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,sBAC3H,IAAK,SAAU,OAAOb,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,kBAAgD,yBAC/K,QAAS,OAAOb,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,GAAS,yBAEtI,EAGFE,eAAeC,OAAO,YAAaf","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as r}from"/kempo-ui/lit-all.min.js";import s from"./humanize.js";export default class i extends e{static properties={name:{type:String},label:{type:String},description:{type:String},value:{attribute:!1},disabled:{type:Boolean},problem:{state:!0}};constructor(){super(),this.name="",this.label="",this.description="",this.value="",this.disabled=!1,this.problem=""}get heading(){return this.label||s(this.name)}announce=(e,t="")=>{this.problem=t,this.dispatchEvent(new CustomEvent("setting-change",{detail:{name:this.name,value:e,valid:!t},bubbles:!0,composed:!0}))};renderControl(){return r}renderHint(){return this.description?t`<small class="tc-muted d-b">${this.description}</small>`:r}renderProblem(){return this.problem?t`<small class="tc-danger d-b" role="alert">${this.problem}</small>`:r}render(){return t`<label class="mb d-b">${this.heading}${this.renderHint()}${this.renderControl()}</label>${this.renderProblem()}`}}
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SettingEditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\SettingEditor.js","names":["ShadowComponent","html","nothing","humanize","SettingEditor","static","name","type","String","label","description","value","attribute","disabled","Boolean","problem","state","constructor","super","this","heading","announce","dispatchEvent","CustomEvent","detail","valid","bubbles","composed","renderControl","renderHint","renderProblem","render"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport humanize from './humanize.js';\n\n/*\n The base of the editors, one per setting type (StringSetting, NumberSetting, BooleanSetting, JsonSetting, SecretSetting). It draws\n the readable name, the description as a hint under it, and the problem if the value cannot be saved, and leaves the control itself\n to `renderControl()`.\n\n <k-setting-string name=\"site_name\" description=\"The name of the website\" .value=${'My Site'}></k-setting-string>\n\n A change is announced as a bubbling `setting-change` event with `{ name, value, valid }`; `value` is already the right type (a number\n is a Number, JSON is parsed). Use <k-setting> to get the editor for a type without choosing one.\n*/\nexport default class SettingEditor extends ShadowComponent {\n static properties = {\n name: { type: String },\n label: { type: String },\n description: { type: String },\n value: { attribute: false },\n disabled: { type: Boolean },\n problem: { state: true },\n };\n\n constructor(){\n super();\n this.name = '';\n this.label = '';\n this.description = '';\n this.value = '';\n this.disabled = false;\n this.problem = '';\n }\n\n /*\n Public\n */\n get heading(){\n return this.label || humanize(this.name);\n }\n\n /* Tell whoever is listening what the value is now. A `problem` sentence means it cannot be saved yet. */\n announce = (value, problem = '') => {\n this.problem = problem;\n this.dispatchEvent(new CustomEvent('setting-change', { detail: { name: this.name, value, valid: !problem }, bubbles: true, composed: true }));\n };\n\n /*\n Rendering\n */\n renderControl(){\n return nothing;\n }\n\n renderHint(){\n return this.description ? html`<small class=\"tc-muted d-b\">${this.description}</small>` : nothing;\n }\n\n renderProblem(){\n return this.problem ? html`<small class=\"tc-danger d-b\" role=\"alert\">${this.problem}</small>` : nothing;\n }\n\n render(){\n return html`<label class=\"mb d-b\">${this.heading}${this.renderHint()}${this.renderControl()}</label>${this.renderProblem()}`;\n }\n}\n"],"mappings":"OAAOA,MAAqB,yDACnBC,aAAMC,MAAe,kCACvBC,MAAc,+BAYN,MAAMC,UAAsBJ,EACzCK,kBAAoB,CAClBC,KAAM,CAAEC,KAAMC,QACdC,MAAO,CAAEF,KAAMC,QACfE,YAAa,CAAEH,KAAMC,QACrBG,MAAO,CAAEC,WAAW,GACpBC,SAAU,CAAEN,KAAMO,SAClBC,QAAS,CAAEC,OAAO,IAGpB,WAAAC,GACEC,QACAC,KAAKb,KAAO,GACZa,KAAKV,MAAQ,GACbU,KAAKT,YAAc,GACnBS,KAAKR,MAAQ,GACbQ,KAAKN,UAAW,EAChBM,KAAKJ,QAAU,EACjB,CAKA,WAAIK,GACF,OAAOD,KAAKV,OAASN,EAASgB,KAAKb,KACrC,CAGAe,SAAW,CAACV,EAAOI,EAAU,MAC3BI,KAAKJ,QAAUA,EACfI,KAAKG,cAAc,IAAIC,YAAY,iBAAkB,CAAEC,OAAQ,CAAElB,KAAMa,KAAKb,KAAMK,QAAOc,OAAQV,GAAWW,SAAS,EAAMC,UAAU,MAMvI,aAAAC,GACE,OAAO1B,CACT,CAEA,UAAA2B,GACE,OAAOV,KAAKT,YAAcT,CAAI,+BAA+BkB,KAAKT,sBAAwBR,CAC5F,CAEA,aAAA4B,GACE,OAAOX,KAAKJ,QAAUd,CAAI,6CAA6CkB,KAAKJ,kBAAoBb,CAClG,CAEA,MAAA6B,GACE,OAAO9B,CAAI,yBAAyBkB,KAAKC,UAAUD,KAAKU,eAAeV,KAAKS,0BAA0BT,KAAKW,iBAC7G","ignoreList":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as s}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Spinner.js";import"/kempo-ui/components/Icon.js";import i from"/kempo-ui/components/Toast.js";import n from"/kempo-ui/components/Dialog.js";import{listSettings as a,setSetting as r,deleteSetting as o,getAllCurrentUserPermissions as l}from"/kempo/sdk.js";import"./Setting.js";import c from"./humanize.js";const u={system:"system:settings:update",custom:"system:custom-settings:manage"};let h=null;const d={string:"",number:0,boolean:!1,json:{},secret:""},m=e=>{if(null===e.value||void 0===e.value||"secret"===e.type)return e;try{if("number"===e.type)return{...e,value:Number(e.value)};if("boolean"===e.type)return{...e,value:!0===e.value||"true"===e.value};if("json"===e.type)return{...e,value:"string"==typeof e.value?JSON.parse(e.value):e.value}}catch{return e}return e};export default class p extends e{static properties={owner:{type:String},heading:{type:String},settings:{state:!0},changes:{state:!0},canEdit:{state:!0},loading:{state:!0},busy:{state:!0},error:{state:!0}};constructor(){super(),this.owner="",this.heading="",this.settings=[],this.changes={},this.canEdit=!1,this.loading=!0,this.busy=!1,this.error=""}connectedCallback(){super.connectedCallback(),this.load()}shortName=e=>e.name.slice(this.owner.length+1);load=async()=>{const[[e,t],s]=await Promise.all([a({owner:this.owner,limit:1e3}),h??=l().then(([,e])=>new Set(e?.permissions??[]))]);this.canEdit=s.has(u[this.owner]??"system:extensions:manage"),this.loading=!1,e?this.error=e.msg||"The settings could not be loaded":(this.error="",this.settings=t.settings.map(m),this.changes={})};record=e=>{const{name:t,value:s,valid:i}=e.detail,n=this.settings.find(e=>this.shortName(e)===t),a=n&&("secret"===n.type?""===s:JSON.stringify(s)===JSON.stringify(n.value)),{[t]:r,...o}=this.changes;this.changes=a&&i?o:{...o,[t]:{value:s,valid:i}}};get dirty(){return Object.keys(this.changes).length>0}get blocked(){return Object.values(this.changes).some(e=>!e.valid)}save=async()=>{this.busy=!0;let e=0;for(const[t,s]of Object.entries(this.changes)){const n=this.settings.find(e=>this.shortName(e)===t),[a]=await r(this.owner,t,{value:s.value,type:n.type,isPublic:n.isPublic,description:n.description});a&&(e++,i.error(`${c(t)}: ${a.msg||"could not be saved"}`))}this.busy=!1,e||i.success("Settings saved"),await this.load()};create=()=>{const e=n.create(t`
|
|
2
|
+
<div class="p">
|
|
3
|
+
<label class="mb d-b">Name<input type="text" id="newName" placeholder="my_setting" required></label>
|
|
4
|
+
<label class="mb d-b">Type
|
|
5
|
+
<select id="newType">
|
|
6
|
+
<option value="string">String</option>
|
|
7
|
+
<option value="number">Number</option>
|
|
8
|
+
<option value="boolean">Boolean (true or false)</option>
|
|
9
|
+
<option value="json">JSON</option>
|
|
10
|
+
<option value="secret">Secret</option>
|
|
11
|
+
</select>
|
|
12
|
+
</label>
|
|
13
|
+
<label class="mb d-b">Value<input type="text" id="newValue"></label>
|
|
14
|
+
<label class="mb d-b">Description<input type="text" id="newDescription"></label>
|
|
15
|
+
<label class="mb d-b"><input type="checkbox" id="newPublic"> Public (anyone can read it, without signing in)</label>
|
|
16
|
+
</div>
|
|
17
|
+
`,{title:"Create a setting",confirmText:"Create",cancelText:"Cancel",confirmAction:async()=>{const t=t=>e.querySelector(t),s=t("#newName").value.trim(),n=t("#newType").value;if(!s)return void i.error("Give the setting a name");if(s.includes(":"))return void i.error('The name cannot contain ":"');let a;try{a=""===t("#newValue").value?d[n]:((e,t)=>{if("number"===t){const t=Number(e);if(""===String(e).trim()||!Number.isFinite(t))throw new Error("number");return t}return"boolean"===t?"true"===String(e).toLowerCase():"json"===t?JSON.parse(String(e||"{}")):String(e)})(t("#newValue").value,n)}catch{return void i.error(`That is not a valid ${n} value`)}const[o]=await r("custom",s,{value:a,type:n,isPublic:"secret"!==n&&t("#newPublic").checked,description:t("#newDescription").value.trim()});o?i.error(o.msg||"The setting could not be created"):(i.success("Setting created"),await this.load())}});e.querySelector("#newType").addEventListener("change",t=>{e.querySelector("#newValue").type="secret"===t.target.value?"password":"text"})};remove=e=>{n.confirm(`Delete ${c(this.shortName(e))}? This cannot be undone.`,async t=>{if(!t)return;const[s]=await o(this.owner,this.shortName(e));s?i.error(s.msg||"The setting could not be deleted"):(i.success("Setting deleted"),await this.load())},{title:"Delete setting",confirmText:"Delete",confirmClasses:"danger ml",cancelText:"Cancel",cancelClasses:""})};renderSetting(e){const s=this.shortName(e),i=Object.hasOwn(this.changes,s)&&this.changes[s].valid?this.changes[s].value:e.value,n=t`<k-setting .setting=${{name:s,value:i,type:e.type,description:e.description}} ?disabled=${!this.canEdit}></k-setting>`;return"custom"===this.owner&&this.canEdit?t`<div class="d-f"><div class="flex">${n}</div><div><button class="danger" aria-label=${`Delete ${c(s)}`} title="Delete" @click=${()=>this.remove(e)}><k-icon name="delete"></k-icon></button></div></div>`:n}render(){return this.loading?t`<k-spinner></k-spinner>`:this.error?t`<p class="tc-danger" role="alert">${this.error}</p>`:t`
|
|
18
|
+
${this.heading?t`<h2 class="h4">${this.heading}</h2>`:s}
|
|
19
|
+
${"custom"===this.owner&&this.canEdit?t`<p class="mb"><button @click=${this.create}><k-icon name="add"></k-icon> Create a setting</button></p>`:s}
|
|
20
|
+
${this.canEdit?s:t`<p class="tc-muted mb">You can see these settings but not change them.</p>`}
|
|
21
|
+
${this.settings.length?this.settings.map(e=>this.renderSetting(e)):t`<p class="tc-muted mb">${"custom"===this.owner?"There are no custom settings yet.":"There are no settings here."}</p>`}
|
|
22
|
+
${this.canEdit&&this.settings.length?t`<button class="primary" ?disabled=${!this.dirty||this.blocked||this.busy} @click=${this.save}>${this.busy?"Saving…":"Save settings"}</button>`:s}
|
|
23
|
+
`}firstUpdated(){this.renderRoot.addEventListener("setting-change",this.record)}}customElements.define("k-settings-panel",p);
|
|
24
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SettingsPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\SettingsPanel.js","names":["ShadowComponent","html","nothing","Toast","Dialog","listSettings","setSetting","deleteSetting","getAllCurrentUserPermissions","humanize","PERMISSIONS","system","custom","permissionsPromise","DEFAULTS","string","number","boolean","json","secret","typed","record","value","undefined","type","Number","JSON","parse","SettingsPanel","static","owner","String","heading","settings","state","changes","canEdit","loading","busy","error","constructor","super","this","connectedCallback","load","shortName","name","slice","length","async","data","permissions","Promise","all","limit","then","Set","has","msg","map","event","valid","detail","find","entry","unchanged","stringify","removed","rest","dirty","Object","keys","blocked","values","some","change","save","failed","entries","isPublic","description","success","create","dialog","title","confirmText","cancelText","confirmAction","read","id","querySelector","trim","includes","text","isFinite","Error","toLowerCase","coerce","checked","addEventListener","target","remove","confirm","confirmed","confirmClasses","cancelClasses","renderSetting","hasOwn","editor","render","firstUpdated","renderRoot","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Spinner.js';\nimport '/kempo-ui/components/Icon.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport { listSettings, setSetting, deleteSetting, getAllCurrentUserPermissions } from '/kempo/sdk.js';\nimport './Setting.js';\nimport humanize from './humanize.js';\n\n/*\n The settings of one owner, each with the editor for its type, and a Save button for what was changed. Served at\n /admin/components/settings/SettingsPanel.js:\n\n <k-settings-panel owner=\"system\"></k-settings-panel>\n <k-settings-panel owner=\"my-extension\" heading=\"My Extension\"></k-settings-panel>\n\n `owner` is `system`, `custom` or the name of an extension. The Custom panel also creates and deletes settings. An extension drops\n one of these in its own settings tab and needs no editor of its own. Whether the person can change them depends on who owns\n them (`system:settings:update`, `system:custom-settings:manage`, `system:extensions:manage`); without it the fields are read-only.\n Saving sends only what was changed, each as its own setting.\n*/\nconst PERMISSIONS = { system: 'system:settings:update', custom: 'system:custom-settings:manage' };\nconst EXTENSION_PERMISSION = 'system:extensions:manage';\n\nlet permissionsPromise = null;\nconst myPermissions = () => (permissionsPromise ??= getAllCurrentUserPermissions().then(([, data]) => new Set(data?.permissions ?? [])));\n\nconst DEFAULTS = { string: '', number: 0, boolean: false, json: {}, secret: '' };\n\nconst coerce = (text, type) => {\n if(type === 'number'){\n const number = Number(text);\n if(String(text).trim() === '' || !Number.isFinite(number)) throw new Error('number');\n return number;\n }\n if(type === 'boolean') return String(text).toLowerCase() === 'true';\n if(type === 'json') return JSON.parse(String(text || '{}'));\n return String(text);\n};\n\n/*\n The settings API gives each value as it is stored, as text (`'true'`, `'7'`, a JSON string). The editors take real values, so the value\n is turned into the type the setting has. A secret is left as the mask the API sends: it only says that one is saved.\n*/\nconst typed = record => {\n if(record.value === null || record.value === undefined || record.type === 'secret') return record;\n try {\n if(record.type === 'number') return { ...record, value: Number(record.value) };\n if(record.type === 'boolean') return { ...record, value: record.value === true || record.value === 'true' };\n if(record.type === 'json') return { ...record, value: typeof record.value === 'string' ? JSON.parse(record.value) : record.value };\n } catch {\n return record;\n }\n return record;\n};\n\nexport default class SettingsPanel extends ShadowComponent {\n static properties = {\n owner: { type: String },\n heading: { type: String },\n settings: { state: true },\n changes: { state: true },\n canEdit: { state: true },\n loading: { state: true },\n busy: { state: true },\n error: { state: true },\n };\n\n constructor(){\n super();\n this.owner = '';\n this.heading = '';\n this.settings = [];\n this.changes = {};\n this.canEdit = false;\n this.loading = true;\n this.busy = false;\n this.error = '';\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.load();\n }\n\n /*\n Loading and saving\n */\n shortName = record => record.name.slice(this.owner.length + 1);\n\n load = async () => {\n const [[error, data], permissions] = await Promise.all([listSettings({ owner: this.owner, limit: 1000 }), myPermissions()]);\n this.canEdit = permissions.has(PERMISSIONS[this.owner] ?? EXTENSION_PERMISSION);\n this.loading = false;\n if(error){\n this.error = error.msg || 'The settings could not be loaded';\n return;\n }\n this.error = '';\n this.settings = data.settings.map(typed);\n this.changes = {};\n };\n\n record = event => {\n const { name, value, valid } = event.detail;\n const record = this.settings.find(entry => this.shortName(entry) === name);\n const unchanged = record && (record.type === 'secret' ? value === '' : JSON.stringify(value) === JSON.stringify(record.value));\n const { [name]: removed, ...rest } = this.changes;\n this.changes = unchanged && valid ? rest : { ...rest, [name]: { value, valid } };\n };\n\n get dirty(){\n return Object.keys(this.changes).length > 0;\n }\n\n get blocked(){\n return Object.values(this.changes).some(change => !change.valid);\n }\n\n save = async () => {\n this.busy = true;\n let failed = 0;\n for(const [name, change] of Object.entries(this.changes)){\n const record = this.settings.find(entry => this.shortName(entry) === name);\n const [error] = await setSetting(this.owner, name, { value: change.value, type: record.type, isPublic: record.isPublic, description: record.description });\n if(error){\n failed++;\n Toast.error(`${humanize(name)}: ${error.msg || 'could not be saved'}`);\n }\n }\n this.busy = false;\n if(!failed) Toast.success('Settings saved');\n await this.load();\n };\n\n /*\n Custom settings: create and delete\n */\n create = () => {\n const dialog = Dialog.create(html`\n <div class=\"p\">\n <label class=\"mb d-b\">Name<input type=\"text\" id=\"newName\" placeholder=\"my_setting\" required></label>\n <label class=\"mb d-b\">Type\n <select id=\"newType\">\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"boolean\">Boolean (true or false)</option>\n <option value=\"json\">JSON</option>\n <option value=\"secret\">Secret</option>\n </select>\n </label>\n <label class=\"mb d-b\">Value<input type=\"text\" id=\"newValue\"></label>\n <label class=\"mb d-b\">Description<input type=\"text\" id=\"newDescription\"></label>\n <label class=\"mb d-b\"><input type=\"checkbox\" id=\"newPublic\"> Public (anyone can read it, without signing in)</label>\n </div>\n `, {\n title: 'Create a setting',\n confirmText: 'Create',\n cancelText: 'Cancel',\n confirmAction: async () => {\n const read = id => dialog.querySelector(id);\n const name = read('#newName').value.trim();\n const type = read('#newType').value;\n if(!name){ Toast.error('Give the setting a name'); return; }\n if(name.includes(':')){ Toast.error('The name cannot contain \":\"'); return; }\n let value;\n try {\n value = read('#newValue').value === '' ? DEFAULTS[type] : coerce(read('#newValue').value, type);\n } catch {\n Toast.error(`That is not a valid ${type} value`);\n return;\n }\n const [error] = await setSetting('custom', name, { value, type, isPublic: type !== 'secret' && read('#newPublic').checked, description: read('#newDescription').value.trim() });\n if(error){ Toast.error(error.msg || 'The setting could not be created'); return; }\n Toast.success('Setting created');\n await this.load();\n },\n });\n dialog.querySelector('#newType').addEventListener('change', event => {\n dialog.querySelector('#newValue').type = event.target.value === 'secret' ? 'password' : 'text';\n });\n };\n\n remove = record => {\n Dialog.confirm(`Delete ${humanize(this.shortName(record))}? This cannot be undone.`, async confirmed => {\n if(!confirmed) return;\n const [error] = await deleteSetting(this.owner, this.shortName(record));\n if(error){ Toast.error(error.msg || 'The setting could not be deleted'); return; }\n Toast.success('Setting deleted');\n await this.load();\n }, { title: 'Delete setting', confirmText: 'Delete', confirmClasses: 'danger ml', cancelText: 'Cancel', cancelClasses: '' });\n };\n\n /*\n Rendering\n */\n renderSetting(record){\n const name = this.shortName(record);\n const value = Object.hasOwn(this.changes, name) && this.changes[name].valid ? this.changes[name].value : record.value;\n const editor = html`<k-setting .setting=${{ name, value, type: record.type, description: record.description }} ?disabled=${!this.canEdit}></k-setting>`;\n if(this.owner !== 'custom' || !this.canEdit) return editor;\n return html`<div class=\"d-f\"><div class=\"flex\">${editor}</div><div><button class=\"danger\" aria-label=${`Delete ${humanize(name)}`} title=\"Delete\" @click=${() => this.remove(record)}><k-icon name=\"delete\"></k-icon></button></div></div>`;\n }\n\n render(){\n if(this.loading) return html`<k-spinner></k-spinner>`;\n if(this.error) return html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>`;\n return html`\n ${this.heading ? html`<h2 class=\"h4\">${this.heading}</h2>` : nothing}\n ${this.owner === 'custom' && this.canEdit ? html`<p class=\"mb\"><button @click=${this.create}><k-icon name=\"add\"></k-icon> Create a setting</button></p>` : nothing}\n ${this.canEdit ? nothing : html`<p class=\"tc-muted mb\">You can see these settings but not change them.</p>`}\n ${this.settings.length ? this.settings.map(record => this.renderSetting(record)) : html`<p class=\"tc-muted mb\">${this.owner === 'custom' ? 'There are no custom settings yet.' : 'There are no settings here.'}</p>`}\n ${this.canEdit && this.settings.length ? html`<button class=\"primary\" ?disabled=${!this.dirty || this.blocked || this.busy} @click=${this.save}>${this.busy ? 'Saving…' : 'Save settings'}</button>` : nothing}\n `;\n }\n\n firstUpdated(){\n this.renderRoot.addEventListener('setting-change', this.record);\n }\n}\n\ncustomElements.define('k-settings-panel', SettingsPanel);\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as t}from"/kempo-ui/lit-all.min.js";import e from"./SettingEditor.js";export default class i extends e{renderControl(){const e=String(this.value??"");return e.length>120||e.includes("\n")?t`<textarea rows="4" ?disabled=${this.disabled} .value=${e} @input=${t=>this.announce(t.target.value)}></textarea>`:t`<input type="text" ?disabled=${this.disabled} .value=${e} @input=${t=>this.announce(t.target.value)}>`}}customElements.define("k-setting-string",i);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\StringSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\StringSetting.js","names":["html","SettingEditor","StringSetting","renderControl","text","String","this","value","length","includes","disabled","event","announce","target","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/* A string setting: a text field, or a larger box for one that is long or has several lines. */\nexport default class StringSetting extends SettingEditor {\n renderControl(){\n const text = String(this.value ?? '');\n if(text.length > 120 || text.includes('\\n')) return html`<textarea rows=\"4\" ?disabled=${this.disabled} .value=${text} @input=${event => this.announce(event.target.value)}></textarea>`;\n return html`<input type=\"text\" ?disabled=${this.disabled} .value=${text} @input=${event => this.announce(event.target.value)}>`;\n }\n}\n\ncustomElements.define('k-setting-string', StringSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAGX,MAAMC,UAAsBD,EACzC,aAAAE,GACE,MAAMC,EAAOC,OAAOC,KAAKC,OAAS,IAClC,OAAGH,EAAKI,OAAS,KAAOJ,EAAKK,SAAS,MAAcT,CAAI,gCAAgCM,KAAKI,mBAAmBN,YAAeO,GAASL,KAAKM,SAASD,EAAME,OAAON,qBAC5JP,CAAI,gCAAgCM,KAAKI,mBAAmBN,YAAeO,GAASL,KAAKM,SAASD,EAAME,OAAON,SACxH,EAGFO,eAAeC,OAAO,mBAAoBb","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const s=new Set(["api","css","html","id","ip","js","json","seo","smtp","sms","sql","ssl","tls","ui","uri","url","utc","uuid","xml"]);export default e=>String(e??"").replace(/([a-z0-9])([A-Z])/g,"$1 $2").split(/[\s_\-.:]+/).filter(Boolean).map(e=>s.has(e.toLowerCase())?e.toUpperCase():e[0].toUpperCase()+e.slice(1)).join(" ");
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\humanize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\humanize.js","names":["ACRONYMS","Set","name","String","replace","split","filter","Boolean","map","word","has","toLowerCase","toUpperCase","slice","join"],"sources":["0"],"sourcesContent":["/*\n A readable name for a setting, made from its name, so nobody has to write one:\n\n session_duration_days -> Session Duration Days\n verification_url -> Verification URL\n shippoCacheMinutes -> Shippo Cache Minutes\n\n Words are split on `_`, `-`, `.`, `:` and spaces and where a lowercase letter meets a capital. Each gets a capital, and the\n well-known abbreviations are written in capitals.\n*/\nconst ACRONYMS = new Set(['api', 'css', 'html', 'id', 'ip', 'js', 'json', 'seo', 'smtp', 'sms', 'sql', 'ssl', 'tls', 'ui', 'uri', 'url', 'utc', 'uuid', 'xml']);\n\nexport default name => String(name ?? '')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .split(/[\\s_\\-.:]+/)\n .filter(Boolean)\n .map(word => (ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word[0].toUpperCase() + word.slice(1)))\n .join(' ');\n"],"mappings":"AAUA,MAAMA,EAAW,IAAIC,IAAI,CAAC,MAAO,MAAO,OAAQ,KAAM,KAAM,KAAM,OAAQ,MAAO,OAAQ,MAAO,MAAO,MAAO,MAAO,KAAM,MAAO,MAAO,MAAO,OAAQ,uBAEzIC,GAAQC,OAAOD,GAAQ,IACnCE,QAAQ,qBAAsB,SAC9BC,MAAM,cACNC,OAAOC,SACPC,IAAIC,GAAST,EAASU,IAAID,EAAKE,eAAiBF,EAAKG,cAAgBH,EAAK,GAAGG,cAAgBH,EAAKI,MAAM,IACxGC,KAAK","ignoreList":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
locked: true
|
|
3
|
+
-->
|
|
4
|
+
<page title="Emails - Admin">
|
|
5
|
+
<content>
|
|
6
|
+
<script type="module" src="/admin/components/Emails.js"></script>
|
|
7
|
+
|
|
8
|
+
<h1>Emails</h1>
|
|
9
|
+
<p class="tc-muted mb">Every email the site was asked to send, and what happened to it. Kempo does not send email itself: an email extension (such as kempo-resend or kempo-smtp) does. How long this log is kept, and how many times a failed email is tried again, are on the System tab of <a href="/admin/settings">Settings</a>.</p>
|
|
10
|
+
|
|
11
|
+
<k-admin-emails></k-admin-emails>
|
|
12
|
+
|
|
13
|
+
</content>
|
|
14
|
+
</page>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/></svg>
|
|
@@ -66,6 +66,12 @@
|
|
|
66
66
|
group="Content"
|
|
67
67
|
no-expand
|
|
68
68
|
>Global Content</k-aside-item>
|
|
69
|
+
<k-aside-item
|
|
70
|
+
icon="menu"
|
|
71
|
+
href="/admin/content/menus"
|
|
72
|
+
group="Content"
|
|
73
|
+
no-expand
|
|
74
|
+
>Menus</k-aside-item>
|
|
69
75
|
<k-aside-item
|
|
70
76
|
icon="settings"
|
|
71
77
|
href="/admin/settings"
|
|
@@ -78,6 +84,12 @@
|
|
|
78
84
|
group="Settings"
|
|
79
85
|
no-expand
|
|
80
86
|
>Extensions</k-aside-item>
|
|
87
|
+
<k-aside-item
|
|
88
|
+
icon="mail"
|
|
89
|
+
href="/admin/emails"
|
|
90
|
+
group="Settings"
|
|
91
|
+
no-expand
|
|
92
|
+
>Emails</k-aside-item>
|
|
81
93
|
<location name="admin-nav-extensions"></location>
|
|
82
94
|
<k-aside-item
|
|
83
95
|
icon="sensors"
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
? myPerms.has('system:settings:update')
|
|
89
89
|
: owner === 'custom'
|
|
90
90
|
? myPerms.has('system:custom-settings:manage')
|
|
91
|
-
: myPerms.has('system:
|
|
91
|
+
: myPerms.has('system:extensions:manage');
|
|
92
92
|
const canEditMetadata = owner === 'custom' && myPerms.has('system:custom-settings:manage');
|
|
93
93
|
const canDelete = owner === 'custom' && myPerms.has('system:custom-settings:manage');
|
|
94
94
|
|