domma-cms 0.55.1 → 0.69.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +194 -3
- package/README.md +18 -14
- package/admin/css/admin.css +1 -1
- package/admin/css/contacts.css +1 -0
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +1 -0
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/analytics-shape.js +1 -0
- package/admin/js/lib/contacts-arrange.js +1 -0
- package/admin/js/lib/help-popover.js +1 -0
- package/admin/js/lib/notes-arrange.js +1 -0
- package/admin/js/lib/page-picker.js +1 -0
- package/admin/js/lib/plugin-accent.js +1 -0
- package/admin/js/lib/plugin-chrome.js +1 -0
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/sidebar-grouping.js +1 -1
- package/admin/js/lib/sidebar-grouping.test.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +4 -4
- package/admin/js/lib/slideover-resizable.js +1 -0
- package/admin/js/lib/todo-arrange.js +1 -0
- package/admin/js/lib/tool-kit.js +1 -0
- package/admin/js/templates/analytics.html +138 -0
- package/admin/js/templates/contacts.html +156 -0
- package/admin/js/templates/context-menu-editor.html +212 -0
- package/admin/js/templates/context-menus.html +16 -0
- package/admin/js/templates/notes.html +137 -0
- package/admin/js/templates/plugin-code.html +1 -1
- package/admin/js/templates/plugin-marketplace.html +17 -4
- package/admin/js/templates/plugins.html +13 -7
- package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
- package/admin/js/templates/settings.html +16 -97
- package/admin/js/templates/theme.html +211 -0
- package/admin/js/templates/todo.html +113 -0
- package/admin/js/views/analytics.js +62 -0
- package/admin/js/views/contacts.js +174 -0
- package/admin/js/views/context-menu-editor.js +55 -0
- package/admin/js/views/context-menus.js +5 -0
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/notes.js +104 -0
- package/admin/js/views/plugin-marketplace.js +1 -1
- package/admin/js/views/plugins.js +28 -24
- package/admin/js/views/search.js +1 -0
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +1 -0
- package/admin/js/views/todo.js +96 -0
- package/bin/cli.js +6 -10
- package/bin/lib/plugin-version.js +28 -0
- package/bin/update.js +27 -2
- package/config/menus/admin-sidebar.json +129 -23
- package/config/plugins.json +5 -5
- package/config/search.json +13 -0
- package/config/server.json +3 -1
- package/package.json +14 -10
- package/plugins/_lib/admin/mail/compose-window.js +914 -0
- package/plugins/_lib/admin/mail/contacts.js +301 -0
- package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
- package/plugins/_lib/admin/mail/folder-tree.js +254 -0
- package/plugins/_lib/admin/mail/identity.js +480 -0
- package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
- package/plugins/_lib/admin/mail/keyboard.js +136 -0
- package/plugins/_lib/admin/mail/mail.css +1 -0
- package/plugins/_lib/admin/mail/mail.html +71 -0
- package/plugins/_lib/admin/mail/panes.js +253 -0
- package/plugins/_lib/admin/mail/reader-view.js +4473 -0
- package/plugins/_lib/admin/mail/resizable.js +26 -0
- package/plugins/_lib/admin/mail/rules.js +343 -0
- package/plugins/_lib/admin/mail/scheduling.js +203 -0
- package/plugins/_lib/admin/mail/section-kit.js +277 -0
- package/plugins/_lib/admin/mail/templates.js +238 -0
- package/plugins/_lib/admin/mail/threads.js +200 -0
- package/plugins/_lib/admin/mail/vacation.js +269 -0
- package/plugins/_lib/admin/ui/help.css +1 -0
- package/plugins/_lib/admin/ui/help.js +174 -0
- package/plugins/_lib/admin/ui/resizable.js +151 -0
- package/plugins/_lib/dataStore.js +117 -0
- package/plugins/_lib/mail/accounts.js +919 -0
- package/plugins/_lib/mail/bodyTokens.js +101 -0
- package/plugins/_lib/mail/compose.js +256 -0
- package/plugins/_lib/mail/defaults.js +57 -0
- package/plugins/_lib/mail/diagnostics.js +154 -0
- package/plugins/_lib/mail/envelope.js +161 -0
- package/plugins/_lib/mail/folders.js +192 -0
- package/plugins/_lib/mail/handoff.js +274 -0
- package/plugins/_lib/mail/imapPool.js +293 -0
- package/plugins/_lib/mail/mbox.js +74 -0
- package/plugins/_lib/mail/pollSchedule.js +79 -0
- package/plugins/_lib/mail/poller.js +135 -0
- package/plugins/_lib/mail/priority.js +138 -0
- package/plugins/_lib/mail/readRoutes.js +680 -0
- package/plugins/_lib/mail/render.js +291 -0
- package/plugins/_lib/mail/ruleRunner.js +152 -0
- package/plugins/_lib/mail/scheduler.js +254 -0
- package/plugins/_lib/mail/secretbox.js +229 -0
- package/plugins/_lib/mail/send.js +396 -0
- package/plugins/_lib/mail/store.js +1022 -0
- package/plugins/_lib/mail/sync.js +292 -0
- package/plugins/_lib/mail/syncPlan.js +126 -0
- package/plugins/_lib/mail/syncSelection.js +82 -0
- package/plugins/_lib/mail/unsubscribe.js +183 -0
- package/plugins/_lib/mail/vacationRunner.js +114 -0
- package/plugins/_lib/mail/write.js +473 -0
- package/plugins/_lib/schemaSync.js +83 -0
- package/plugins/_template/admin/css/index.css +0 -0
- package/plugins/_template/admin/templates/index.html +4 -4
- package/plugins/_template/admin/views/index.js +7 -0
- package/plugins/blog/CLAUDE.md +237 -0
- package/plugins/blog/admin/css/index.css +178 -0
- package/plugins/blog/admin/templates/blog.html +35 -41
- package/plugins/blog/admin/templates/categories.html +19 -6
- package/plugins/blog/admin/templates/comments.html +26 -9
- package/plugins/blog/admin/templates/post-editor.html +63 -40
- package/plugins/blog/admin/templates/settings.html +3 -9
- package/plugins/blog/admin/views/blog.js +288 -159
- package/plugins/blog/admin/views/categories.js +212 -206
- package/plugins/blog/admin/views/comments.js +207 -158
- package/plugins/blog/admin/views/kit.js +372 -0
- package/plugins/blog/admin/views/post-editor.js +306 -39
- package/plugins/blog/admin/views/settings-cog.js +56 -0
- package/plugins/blog/admin/views/settings.js +16 -84
- package/plugins/blog/blocks/blog-card-compact.css +17 -0
- package/plugins/blog/blocks/blog-card-compact.html +8 -0
- package/plugins/blog/blocks/blog-card-row.css +22 -0
- package/plugins/blog/blocks/blog-card-row.html +11 -0
- package/plugins/blog/blocks/blog-card.css +35 -0
- package/plugins/blog/blocks/blog-card.html +16 -0
- package/plugins/blog/blocks/blog-post-classic.css +15 -0
- package/plugins/blog/blocks/blog-post-classic.html +28 -0
- package/plugins/blog/blocks/blog-post-feature.css +20 -0
- package/plugins/blog/blocks/blog-post-feature.html +30 -0
- package/plugins/blog/blocks/blog-post-minimal.css +10 -0
- package/plugins/blog/blocks/blog-post-minimal.html +23 -0
- package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
- package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
- package/plugins/blog/collections/categories/schema.json +7 -6
- package/plugins/blog/collections/comments/schema.json +11 -10
- package/plugins/blog/collections/posts/schema.json +16 -13
- package/plugins/blog/config.js +6 -0
- package/plugins/blog/lib/layouts.js +304 -0
- package/plugins/blog/lib/page.js +158 -0
- package/plugins/blog/lib/render.js +119 -0
- package/plugins/blog/lib/samples.js +234 -0
- package/plugins/blog/plugin.js +259 -26
- package/plugins/blog/plugin.json +21 -73
- package/plugins/blog/plugin.public.js +217 -0
- package/plugins/blog/public/blog.css +177 -0
- package/plugins/blog/public/blog.js +586 -0
- package/plugins/blog/public/samples/aurora.svg +7 -0
- package/plugins/blog/public/samples/dusk.svg +7 -0
- package/plugins/blog/public/samples/ember.svg +7 -0
- package/plugins/blog/public/samples/harbour.svg +7 -0
- package/plugins/blog/public/samples/meadow.svg +7 -0
- package/plugins/blog/public/samples/rosewood.svg +7 -0
- package/plugins/blog/templates/index.html +9 -8
- package/plugins/blog/templates/post.html +4 -17
- package/plugins/blog/tests/layouts.test.js +97 -0
- package/plugins/blog/tests/public.test.js +71 -0
- package/plugins/free-tier.lock.json +74 -0
- package/plugins/mail-reader/CLAUDE.md +78 -0
- package/plugins/mail-reader/admin/views/mail.js +19 -0
- package/plugins/mail-reader/config.js +7 -0
- package/plugins/mail-reader/plugin.js +48 -0
- package/plugins/mail-reader/plugin.json +34 -0
- package/public/css/forms.css +1 -1
- package/public/css/search.css +1 -0
- package/public/css/site.css +1 -1
- package/public/css/theme-switcher.css +1 -0
- package/public/js/analytics.js +1 -0
- package/public/js/collection-context.js +2 -2
- package/public/js/context-menus.js +1 -0
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -2
- package/public/js/search.js +1 -0
- package/public/js/site.js +1 -1
- package/public/js/theme-switcher.js +1 -0
- package/scripts/build.js +77 -4
- package/scripts/copy-domma.js +48 -0
- package/scripts/run-tests.mjs +92 -0
- package/scripts/seed.js +1996 -0
- package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
- package/server/routes/api/collections.js +34 -0
- package/server/routes/api/contacts.js +506 -0
- package/server/routes/api/context-menus.js +104 -0
- package/server/routes/api/dashboard.js +8 -2
- package/server/routes/api/forms.js +42 -3
- package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
- package/server/routes/api/notifications.js +69 -19
- package/server/routes/api/plugin-marketplace.js +78 -23
- package/server/routes/api/plugins.js +198 -9
- package/server/routes/api/search.js +43 -0
- package/server/routes/api/theme.js +69 -0
- package/server/routes/api/todo.js +178 -0
- package/server/routes/public.js +42 -7
- package/server/server.js +135 -1
- package/server/services/adapters/FileAdapter.js +6 -1
- package/server/services/collections.js +13 -3
- package/server/services/content.js +26 -0
- package/server/services/contextMenus.js +477 -0
- package/server/services/managerClient.js +72 -136
- package/server/services/markdown.js +124 -10
- package/server/services/permissionRegistry.js +74 -0
- package/server/services/pluginEntitlement.js +171 -0
- package/server/services/pluginEnvelope.js +242 -0
- package/server/services/pluginFiles.js +52 -11
- package/server/services/pluginInstaller.js +246 -26
- package/server/services/pluginScaffold.js +5 -0
- package/server/services/plugins.js +340 -7
- package/server/services/presetCollections.js +344 -0
- package/server/services/promoted-plugins-migration.js +254 -0
- package/server/services/publicCors.js +46 -0
- package/server/services/renderer.js +224 -22
- package/server/services/roles.js +1 -1
- package/server/services/search-migration.js +82 -0
- package/server/services/search.js +413 -0
- package/server/services/sidebar-migration.js +1 -0
- package/server/services/sidebarBadges.js +37 -0
- package/server/services/siteGitignore.js +307 -0
- package/server/services/themeSettings.js +580 -0
- package/server/services/users.js +8 -0
- package/server/templates/page.html +4 -2
- package/plugins/analytics/admin/templates/analytics.html +0 -61
- package/plugins/analytics/admin/views/analytics.js +0 -176
- package/plugins/analytics/config.js +0 -14
- package/plugins/analytics/plugin.json +0 -57
- package/plugins/analytics/public/inject-body.html +0 -60
- package/plugins/analytics/public/inject-head.html +0 -1
- package/plugins/blog/templates/author.html +0 -9
- package/plugins/blog/templates/category.html +0 -9
- package/plugins/blog/templates/tag.html +0 -9
- package/plugins/contacts/admin/templates/contacts.html +0 -126
- package/plugins/contacts/admin/views/contacts.js +0 -710
- package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
- package/plugins/contacts/collections/user-contacts/schema.json +0 -71
- package/plugins/contacts/config.js +0 -6
- package/plugins/contacts/data/contacts.json +0 -20
- package/plugins/contacts/plugin.js +0 -291
- package/plugins/contacts/plugin.json +0 -33
- package/plugins/demo-viewer/config.js +0 -4
- package/plugins/demo-viewer/plugin.js +0 -36
- package/plugins/demo-viewer/plugin.json +0 -9
- package/plugins/invoice/admin/templates/editor.html +0 -129
- package/plugins/invoice/admin/templates/index.html +0 -43
- package/plugins/invoice/admin/templates/issuers.html +0 -5
- package/plugins/invoice/admin/templates/receivers.html +0 -5
- package/plugins/invoice/admin/views/editor.js +0 -267
- package/plugins/invoice/admin/views/index.js +0 -155
- package/plugins/invoice/admin/views/issuers.js +0 -23
- package/plugins/invoice/admin/views/party-view.js +0 -148
- package/plugins/invoice/admin/views/receivers.js +0 -22
- package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
- package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
- package/plugins/invoice/collections/invoices/schema.json +0 -27
- package/plugins/invoice/config.js +0 -16
- package/plugins/invoice/plugin.js +0 -283
- package/plugins/invoice/plugin.json +0 -85
- package/plugins/invoice/templates/invoice-print.html +0 -213
- package/plugins/notes/admin/templates/notes.html +0 -83
- package/plugins/notes/admin/views/notes.js +0 -281
- package/plugins/notes/collections/user-notes/schema.json +0 -54
- package/plugins/notes/config.js +0 -6
- package/plugins/notes/data/notes.json +0 -1
- package/plugins/notes/plugin.json +0 -33
- package/plugins/site-search/admin/views/site-search.js +0 -116
- package/plugins/site-search/config.js +0 -15
- package/plugins/site-search/plugin.js +0 -188
- package/plugins/site-search/plugin.json +0 -40
- package/plugins/site-search/public/inject-body.html +0 -17
- package/plugins/site-search/public/inject-head.html +0 -1
- package/plugins/site-search/public/search.css +0 -1
- package/plugins/site-search/public/search.js +0 -1
- package/plugins/surveys/admin/templates/audience.html +0 -47
- package/plugins/surveys/admin/templates/results.html +0 -58
- package/plugins/surveys/admin/templates/survey-editor.html +0 -141
- package/plugins/surveys/admin/templates/surveys.html +0 -25
- package/plugins/surveys/admin/views/audience.js +0 -301
- package/plugins/surveys/admin/views/results.js +0 -172
- package/plugins/surveys/admin/views/survey-editor.js +0 -211
- package/plugins/surveys/admin/views/surveys.js +0 -161
- package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
- package/plugins/surveys/collections/survey-groups/schema.json +0 -11
- package/plugins/surveys/collections/survey-invites/schema.json +0 -16
- package/plugins/surveys/collections/surveys/schema.json +0 -23
- package/plugins/surveys/config.js +0 -8
- package/plugins/surveys/plugin.js +0 -174
- package/plugins/surveys/plugin.json +0 -65
- package/plugins/surveys/public/dist-shared.mjs +0 -1
- package/plugins/surveys/public/survey.css +0 -1
- package/plugins/surveys/public/survey.mjs +0 -1
- package/plugins/surveys/templates/survey-page.html +0 -63
- package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
- package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
- package/plugins/theme-switcher/config.js +0 -10
- package/plugins/theme-switcher/plugin.js +0 -26
- package/plugins/theme-switcher/plugin.json +0 -74
- package/plugins/theme-switcher/public/inject-body.html +0 -153
- package/plugins/theme-switcher/public/inject-head.html +0 -260
- package/plugins/todo/admin/templates/todo.html +0 -158
- package/plugins/todo/admin/views/todo.js +0 -343
- package/plugins/todo/collections/todos/schema.json +0 -60
- package/plugins/todo/config.js +0 -7
- package/plugins/todo/data/todos.json +0 -1
- package/plugins/todo/plugin.js +0 -102
- package/plugins/todo/plugin.json +0 -33
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"slug": "contacts-groups",
|
|
3
|
-
"title": "Contact Groups",
|
|
4
|
-
"description": "Groups for the Contacts plugin.",
|
|
5
|
-
"plugin": "contacts",
|
|
6
|
-
"fields": [
|
|
7
|
-
{
|
|
8
|
-
"name": "name",
|
|
9
|
-
"label": "Group Name",
|
|
10
|
-
"type": "text",
|
|
11
|
-
"required": true
|
|
12
|
-
}
|
|
13
|
-
],
|
|
14
|
-
"api": {
|
|
15
|
-
"create": {
|
|
16
|
-
"enabled": true,
|
|
17
|
-
"access": "admin"
|
|
18
|
-
},
|
|
19
|
-
"read": {
|
|
20
|
-
"enabled": true,
|
|
21
|
-
"access": "admin"
|
|
22
|
-
},
|
|
23
|
-
"update": {
|
|
24
|
-
"enabled": false,
|
|
25
|
-
"access": "admin"
|
|
26
|
-
},
|
|
27
|
-
"delete": {
|
|
28
|
-
"enabled": false,
|
|
29
|
-
"access": "admin"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"storage": {
|
|
33
|
-
"adapter": "file"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"slug": "contacts-contacts",
|
|
3
|
-
"title": "Contacts",
|
|
4
|
-
"description": "Contacts managed by the Contacts plugin.",
|
|
5
|
-
"plugin": "contacts",
|
|
6
|
-
"fields": [
|
|
7
|
-
{
|
|
8
|
-
"name": "name",
|
|
9
|
-
"label": "Name",
|
|
10
|
-
"type": "text",
|
|
11
|
-
"required": true
|
|
12
|
-
},
|
|
13
|
-
{
|
|
14
|
-
"name": "email",
|
|
15
|
-
"label": "Email",
|
|
16
|
-
"type": "text",
|
|
17
|
-
"required": false
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
"name": "phone",
|
|
21
|
-
"label": "Phone",
|
|
22
|
-
"type": "text",
|
|
23
|
-
"required": false
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
"name": "groups",
|
|
27
|
-
"label": "Groups",
|
|
28
|
-
"type": "text",
|
|
29
|
-
"required": false
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
"name": "notes",
|
|
33
|
-
"label": "Notes",
|
|
34
|
-
"type": "textarea",
|
|
35
|
-
"required": false
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"name": "favourite",
|
|
39
|
-
"label": "Favourite",
|
|
40
|
-
"type": "text",
|
|
41
|
-
"required": false
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"name": "userId",
|
|
45
|
-
"label": "User ID",
|
|
46
|
-
"type": "text",
|
|
47
|
-
"required": false
|
|
48
|
-
}
|
|
49
|
-
],
|
|
50
|
-
"api": {
|
|
51
|
-
"create": {
|
|
52
|
-
"enabled": true,
|
|
53
|
-
"access": "admin"
|
|
54
|
-
},
|
|
55
|
-
"read": {
|
|
56
|
-
"enabled": true,
|
|
57
|
-
"access": "admin"
|
|
58
|
-
},
|
|
59
|
-
"update": {
|
|
60
|
-
"enabled": false,
|
|
61
|
-
"access": "admin"
|
|
62
|
-
},
|
|
63
|
-
"delete": {
|
|
64
|
-
"enabled": false,
|
|
65
|
-
"access": "admin"
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
"storage": {
|
|
69
|
-
"adapter": "file"
|
|
70
|
-
}
|
|
71
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"contacts": [
|
|
3
|
-
{
|
|
4
|
-
"id": "97bbf711-84ce-41cc-898f-811936e051bc",
|
|
5
|
-
"name": "Darryl Waterhouse",
|
|
6
|
-
"email": "",
|
|
7
|
-
"phone": "+447835449292",
|
|
8
|
-
"groups": [
|
|
9
|
-
"Coders"
|
|
10
|
-
],
|
|
11
|
-
"notes": "Ledge",
|
|
12
|
-
"favourite": true,
|
|
13
|
-
"userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
|
|
14
|
-
"createdAt": "2026-03-24T16:43:39.907Z"
|
|
15
|
-
}
|
|
16
|
-
],
|
|
17
|
-
"groups": [
|
|
18
|
-
"Coders"
|
|
19
|
-
]
|
|
20
|
-
}
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import defaultConfig from './config.js';
|
|
2
|
-
import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
|
|
3
|
-
|
|
4
|
-
const CONTACTS_SLUG = 'contacts-contacts';
|
|
5
|
-
const GROUPS_SLUG = 'contacts-groups';
|
|
6
|
-
|
|
7
|
-
/** Map a collection entry to the shape the admin view expects. */
|
|
8
|
-
function toContact(entry) {
|
|
9
|
-
let groups = entry.data.groups;
|
|
10
|
-
if (typeof groups === 'string') {
|
|
11
|
-
try {
|
|
12
|
-
groups = JSON.parse(groups);
|
|
13
|
-
} catch {
|
|
14
|
-
groups = [];
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return {
|
|
18
|
-
id: entry.id,
|
|
19
|
-
name: entry.data.name ?? '',
|
|
20
|
-
email: entry.data.email ?? '',
|
|
21
|
-
phone: entry.data.phone ?? '',
|
|
22
|
-
groups: Array.isArray(groups) ? groups : [],
|
|
23
|
-
notes: entry.data.notes ?? '',
|
|
24
|
-
favourite: entry.data.favourite === true || entry.data.favourite === 'true',
|
|
25
|
-
userId: entry.data.userId ?? null,
|
|
26
|
-
createdAt: entry.meta.createdAt,
|
|
27
|
-
updatedAt: entry.meta.updatedAt
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function toGroupName(entry) {
|
|
32
|
-
return entry.data.name ?? '';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export default async function contactsPlugin(fastify, options) {
|
|
36
|
-
const {authenticate} = options.auth;
|
|
37
|
-
const config = {...defaultConfig, ...(options.settings || {})};
|
|
38
|
-
const scope = config.scope ?? 'user';
|
|
39
|
-
|
|
40
|
-
function userId(request) {
|
|
41
|
-
return scope === 'user' ? (request.user?.id ?? request.user?.sub ?? null) : null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function loadContacts(uid) {
|
|
45
|
-
const {entries} = await listEntries(CONTACTS_SLUG, {limit: 100000, sort: 'createdAt', order: 'asc'});
|
|
46
|
-
let items = entries.map(toContact);
|
|
47
|
-
if (uid) items = items.filter(c => c.userId === uid);
|
|
48
|
-
return items;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function loadGroups() {
|
|
52
|
-
const {entries} = await listEntries(GROUPS_SLUG, {limit: 10000, sort: 'createdAt', order: 'asc'});
|
|
53
|
-
return entries.map(e => ({id: e.id, name: toGroupName(e)}));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// -------------------------------------------------------------------------
|
|
57
|
-
// Contacts CRUD
|
|
58
|
-
// -------------------------------------------------------------------------
|
|
59
|
-
|
|
60
|
-
/** GET /api/plugins/contacts/contacts - supports ?q=, ?group=, ?favourites=true */
|
|
61
|
-
fastify.get('/contacts', {preHandler: [authenticate]}, async (request, reply) => {
|
|
62
|
-
const uid = userId(request);
|
|
63
|
-
const { q, group, favourites } = request.query;
|
|
64
|
-
|
|
65
|
-
let result = await loadContacts(uid);
|
|
66
|
-
|
|
67
|
-
if (q) {
|
|
68
|
-
const query = q.toLowerCase();
|
|
69
|
-
result = result.filter(c =>
|
|
70
|
-
c.name.toLowerCase().includes(query) ||
|
|
71
|
-
c.email.toLowerCase().includes(query) ||
|
|
72
|
-
c.phone.includes(query)
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
if (group) {
|
|
76
|
-
result = result.filter(c => Array.isArray(c.groups) && c.groups.includes(group));
|
|
77
|
-
}
|
|
78
|
-
if (favourites === 'true') {
|
|
79
|
-
result = result.filter(c => c.favourite === true);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
result.sort((a, b) => a.name.localeCompare(b.name));
|
|
83
|
-
return reply.send(result);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
/** POST /api/plugins/contacts/contacts */
|
|
87
|
-
fastify.post('/contacts', {preHandler: [authenticate]}, async (request, reply) => {
|
|
88
|
-
const uid = userId(request);
|
|
89
|
-
const { name, email, phone, groups, notes, favourite } = request.body ?? {};
|
|
90
|
-
|
|
91
|
-
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
92
|
-
return reply.code(400).send({ error: 'name is required' });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const entry = await createEntry(CONTACTS_SLUG, {
|
|
96
|
-
name: name.trim(),
|
|
97
|
-
email: email?.trim() ?? '',
|
|
98
|
-
phone: phone?.trim() ?? '',
|
|
99
|
-
groups: Array.isArray(groups) ? groups : [],
|
|
100
|
-
notes: notes?.trim() ?? '',
|
|
101
|
-
favourite: favourite === true,
|
|
102
|
-
userId: uid ?? null
|
|
103
|
-
}, {createdBy: uid, source: 'admin'});
|
|
104
|
-
|
|
105
|
-
return reply.code(201).send(toContact(entry));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
/** PUT /api/plugins/contacts/contacts/:id */
|
|
109
|
-
fastify.put('/contacts/:id', {preHandler: [authenticate]}, async (request, reply) => {
|
|
110
|
-
const uid = userId(request);
|
|
111
|
-
const { id } = request.params;
|
|
112
|
-
const updates = request.body ?? {};
|
|
113
|
-
|
|
114
|
-
const entry = await getEntry(CONTACTS_SLUG, id);
|
|
115
|
-
if (!entry) return reply.code(404).send({error: 'Contact not found'});
|
|
116
|
-
if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Contact not found'});
|
|
117
|
-
|
|
118
|
-
const merged = {...entry.data};
|
|
119
|
-
for (const key of ['name', 'email', 'phone', 'groups', 'notes', 'favourite']) {
|
|
120
|
-
if (key in updates) merged[key] = updates[key];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const updated = await updateEntry(CONTACTS_SLUG, id, merged);
|
|
124
|
-
return reply.send(toContact(updated));
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
/** DELETE /api/plugins/contacts/contacts/bulk - must be before /:id */
|
|
128
|
-
fastify.delete('/contacts/bulk', {preHandler: [authenticate]}, async (request, reply) => {
|
|
129
|
-
const uid = userId(request);
|
|
130
|
-
const { ids } = request.body ?? {};
|
|
131
|
-
|
|
132
|
-
if (!Array.isArray(ids) || ids.length === 0) {
|
|
133
|
-
return reply.code(400).send({ error: 'ids array is required' });
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let deleted = 0;
|
|
137
|
-
for (const id of ids) {
|
|
138
|
-
const entry = await getEntry(CONTACTS_SLUG, id).catch(() => null);
|
|
139
|
-
if (!entry) continue;
|
|
140
|
-
if (uid && entry.data.userId !== uid) continue;
|
|
141
|
-
await deleteEntry(CONTACTS_SLUG, id).catch(() => {
|
|
142
|
-
});
|
|
143
|
-
deleted++;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return reply.send({ deleted });
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
/** DELETE /api/plugins/contacts/contacts/:id */
|
|
150
|
-
fastify.delete('/contacts/:id', {preHandler: [authenticate]}, async (request, reply) => {
|
|
151
|
-
const uid = userId(request);
|
|
152
|
-
const { id } = request.params;
|
|
153
|
-
|
|
154
|
-
const entry = await getEntry(CONTACTS_SLUG, id);
|
|
155
|
-
if (!entry) return reply.code(404).send({error: 'Contact not found'});
|
|
156
|
-
if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Contact not found'});
|
|
157
|
-
|
|
158
|
-
await deleteEntry(CONTACTS_SLUG, id);
|
|
159
|
-
return reply.send({ ok: true });
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// -------------------------------------------------------------------------
|
|
163
|
-
// Groups CRUD
|
|
164
|
-
// -------------------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
/** GET /api/plugins/contacts/groups */
|
|
167
|
-
fastify.get('/groups', {preHandler: [authenticate]}, async (request, reply) => {
|
|
168
|
-
const groups = await loadGroups();
|
|
169
|
-
return reply.send(groups.map(g => g.name));
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
/** POST /api/plugins/contacts/groups */
|
|
173
|
-
fastify.post('/groups', {preHandler: [authenticate]}, async (request, reply) => {
|
|
174
|
-
const { name } = request.body ?? {};
|
|
175
|
-
|
|
176
|
-
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
177
|
-
return reply.code(400).send({ error: 'name is required' });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const trimmed = name.trim();
|
|
181
|
-
const existing = await loadGroups();
|
|
182
|
-
if (existing.some(g => g.name === trimmed)) {
|
|
183
|
-
return reply.code(400).send({ error: 'Group already exists' });
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
await createEntry(GROUPS_SLUG, {name: trimmed}, {source: 'admin'});
|
|
187
|
-
return reply.code(201).send({ name: trimmed });
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
/** PUT /api/plugins/contacts/groups/:name - rename + cascade */
|
|
191
|
-
fastify.put('/groups/:name', {preHandler: [authenticate]}, async (request, reply) => {
|
|
192
|
-
const oldName = decodeURIComponent(request.params.name);
|
|
193
|
-
const { newName } = request.body ?? {};
|
|
194
|
-
|
|
195
|
-
if (!newName || typeof newName !== 'string' || !newName.trim()) {
|
|
196
|
-
return reply.code(400).send({ error: 'newName is required' });
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const groups = await loadGroups();
|
|
200
|
-
const grpEntry = groups.find(g => g.name === oldName);
|
|
201
|
-
if (!grpEntry) return reply.code(404).send({error: 'Group not found'});
|
|
202
|
-
|
|
203
|
-
const trimmed = newName.trim();
|
|
204
|
-
if (groups.some(g => g.name === trimmed)) {
|
|
205
|
-
return reply.code(400).send({ error: 'A group with that name already exists' });
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Rename the group entry
|
|
209
|
-
await updateEntry(GROUPS_SLUG, grpEntry.id, {name: trimmed});
|
|
210
|
-
|
|
211
|
-
// Cascade rename across all contacts
|
|
212
|
-
const {entries} = await listEntries(CONTACTS_SLUG, {limit: 100000});
|
|
213
|
-
for (const entry of entries) {
|
|
214
|
-
const grps = Array.isArray(entry.data.groups) ? entry.data.groups : [];
|
|
215
|
-
if (grps.includes(oldName)) {
|
|
216
|
-
await updateEntry(CONTACTS_SLUG, entry.id, {
|
|
217
|
-
...entry.data,
|
|
218
|
-
groups: grps.map(g => (g === oldName ? trimmed : g))
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return reply.send({ name: trimmed });
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
/** DELETE /api/plugins/contacts/groups/:name - delete + cascade */
|
|
227
|
-
fastify.delete('/groups/:name', {preHandler: [authenticate]}, async (request, reply) => {
|
|
228
|
-
const name = decodeURIComponent(request.params.name);
|
|
229
|
-
|
|
230
|
-
const groups = await loadGroups();
|
|
231
|
-
const grpEntry = groups.find(g => g.name === name);
|
|
232
|
-
if (!grpEntry) return reply.code(404).send({error: 'Group not found'});
|
|
233
|
-
|
|
234
|
-
// Remove the group entry
|
|
235
|
-
await deleteEntry(GROUPS_SLUG, grpEntry.id);
|
|
236
|
-
|
|
237
|
-
// Cascade: strip group from all contacts
|
|
238
|
-
const {entries} = await listEntries(CONTACTS_SLUG, {limit: 100000});
|
|
239
|
-
for (const entry of entries) {
|
|
240
|
-
const grps = Array.isArray(entry.data.groups) ? entry.data.groups : [];
|
|
241
|
-
if (grps.includes(name)) {
|
|
242
|
-
await updateEntry(CONTACTS_SLUG, entry.id, {
|
|
243
|
-
...entry.data,
|
|
244
|
-
groups: grps.filter(g => g !== name)
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return reply.send({ ok: true });
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
// -------------------------------------------------------------------------
|
|
253
|
-
// Import / Export
|
|
254
|
-
// -------------------------------------------------------------------------
|
|
255
|
-
|
|
256
|
-
/** GET /api/plugins/contacts/export */
|
|
257
|
-
fastify.get('/export', {preHandler: [authenticate]}, async (request, reply) => {
|
|
258
|
-
const uid = userId(request);
|
|
259
|
-
const contacts = await loadContacts(uid);
|
|
260
|
-
return reply
|
|
261
|
-
.header('Content-Disposition', 'attachment; filename="contacts.json"')
|
|
262
|
-
.send(contacts);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
/** POST /api/plugins/contacts/import */
|
|
266
|
-
fastify.post('/import', {preHandler: [authenticate]}, async (request, reply) => {
|
|
267
|
-
const uid = userId(request);
|
|
268
|
-
const { contacts: incoming } = request.body ?? {};
|
|
269
|
-
|
|
270
|
-
if (!Array.isArray(incoming)) {
|
|
271
|
-
return reply.code(400).send({ error: 'contacts array is required' });
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
let imported = 0;
|
|
275
|
-
for (const raw of incoming) {
|
|
276
|
-
if (!raw || typeof raw.name !== 'string') continue;
|
|
277
|
-
await createEntry(CONTACTS_SLUG, {
|
|
278
|
-
name: raw.name?.trim() ?? '',
|
|
279
|
-
email: raw.email?.trim() ?? '',
|
|
280
|
-
phone: raw.phone?.trim() ?? '',
|
|
281
|
-
groups: Array.isArray(raw.groups) ? raw.groups : [],
|
|
282
|
-
notes: raw.notes?.trim() ?? '',
|
|
283
|
-
favourite: raw.favourite === true,
|
|
284
|
-
userId: uid ?? null
|
|
285
|
-
}, {source: 'import'});
|
|
286
|
-
imported++;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return reply.send({ imported });
|
|
290
|
-
});
|
|
291
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "contacts",
|
|
3
|
-
"displayName": "Contacts",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"description": "Contact manager with groups, favourites, search, and import/export.",
|
|
6
|
-
"author": "Darryl Waterhouse",
|
|
7
|
-
"date": "2026-03-24",
|
|
8
|
-
"icon": "users",
|
|
9
|
-
"admin": {
|
|
10
|
-
"sidebar": [
|
|
11
|
-
{
|
|
12
|
-
"id": "contacts",
|
|
13
|
-
"text": "Contacts",
|
|
14
|
-
"icon": "users",
|
|
15
|
-
"url": "#/plugins/contacts",
|
|
16
|
-
"section": "#/plugins/contacts"
|
|
17
|
-
}
|
|
18
|
-
],
|
|
19
|
-
"routes": [
|
|
20
|
-
{
|
|
21
|
-
"path": "/plugins/contacts",
|
|
22
|
-
"view": "plugin-contacts",
|
|
23
|
-
"title": "Contacts - Domma CMS"
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"views": {
|
|
27
|
-
"plugin-contacts": {
|
|
28
|
-
"entry": "contacts/admin/views/contacts.js?v=b7ed0c5c",
|
|
29
|
-
"exportName": "contactsView"
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Demo Viewer Plugin
|
|
3
|
-
*
|
|
4
|
-
* Registers a [demo] shortcode that embeds interactive Domma JS demos via iframe.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* [demo src="elements/accordion" /]
|
|
8
|
-
* [demo src="effects/ambient" height="600px" title="Ambient Effect" /]
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export default async function demoViewerPlugin(fastify, opts) {
|
|
12
|
-
const {hooks, settings} = opts;
|
|
13
|
-
const basePath = settings.basePath || '/public/demos';
|
|
14
|
-
const defaultHeight = settings.defaultHeight || '500px';
|
|
15
|
-
|
|
16
|
-
hooks.registerShortcode('demo', (attrStr, body, context) => {
|
|
17
|
-
const attrs = context.parseShortcodeAttrs(attrStr);
|
|
18
|
-
const src = attrs.src || '';
|
|
19
|
-
const height = attrs.height || defaultHeight;
|
|
20
|
-
const title = attrs.title || '';
|
|
21
|
-
const iframeSrc = `${basePath}/${src}.html`;
|
|
22
|
-
|
|
23
|
-
const titleBar = title
|
|
24
|
-
? `<div class="demo-viewer-title">${context.escapeAttr(title)}</div>`
|
|
25
|
-
: '';
|
|
26
|
-
|
|
27
|
-
return `<div class="demo-viewer">${titleBar}<iframe src="${context.escapeAttr(iframeSrc)}" style="width:100%;height:${context.escapeAttr(height)};border:none;border-radius:var(--dm-radius, 8px);background:var(--dm-surface, #1a1a2e);" loading="lazy" allowfullscreen></iframe></div>`;
|
|
28
|
-
}, {priority: 5});
|
|
29
|
-
|
|
30
|
-
hooks.registerSanitizeRules({
|
|
31
|
-
tags: ['iframe'],
|
|
32
|
-
attributes: {
|
|
33
|
-
iframe: ['src', 'height', 'width', 'style', 'loading', 'allowfullscreen']
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
<div class="view-header d-flex align-items-center justify-content-between mb-4">
|
|
2
|
-
<h1 class="h3 mb-0" id="editor-title">New Invoice</h1>
|
|
3
|
-
<div class="d-flex gap-2">
|
|
4
|
-
<a id="btn-print" class="btn btn-secondary" href="#" target="_blank" style="display:none;">
|
|
5
|
-
<span data-icon="printer"></span> Print / PDF
|
|
6
|
-
</a>
|
|
7
|
-
<button id="btn-save" class="btn btn-primary">
|
|
8
|
-
<span data-icon="save"></span> Save
|
|
9
|
-
</button>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
|
-
<div class="d-flex gap-4" style="align-items:flex-start;">
|
|
14
|
-
|
|
15
|
-
<div style="flex:1; min-width:0;">
|
|
16
|
-
|
|
17
|
-
<div class="card mb-3">
|
|
18
|
-
<div class="card-body">
|
|
19
|
-
<div class="grid grid-cols-2 gap-3">
|
|
20
|
-
<div>
|
|
21
|
-
<label class="form-label">Invoice Number</label>
|
|
22
|
-
<input id="f-number" type="text" class="form-control font-monospace" placeholder="auto-generated">
|
|
23
|
-
<div class="text-muted small">Leave blank to auto-number</div>
|
|
24
|
-
</div>
|
|
25
|
-
<div>
|
|
26
|
-
<label class="form-label">Status</label>
|
|
27
|
-
<select id="f-status" class="form-select">
|
|
28
|
-
<option value="draft">Draft</option>
|
|
29
|
-
<option value="sent">Sent</option>
|
|
30
|
-
<option value="paid">Paid</option>
|
|
31
|
-
<option value="overdue">Overdue</option>
|
|
32
|
-
<option value="cancelled">Cancelled</option>
|
|
33
|
-
</select>
|
|
34
|
-
</div>
|
|
35
|
-
<div>
|
|
36
|
-
<label class="form-label">Issue Date</label>
|
|
37
|
-
<input id="f-issue-date" type="date" class="form-control">
|
|
38
|
-
</div>
|
|
39
|
-
<div>
|
|
40
|
-
<label class="form-label">Due Date</label>
|
|
41
|
-
<input id="f-due-date" type="date" class="form-control">
|
|
42
|
-
</div>
|
|
43
|
-
<div>
|
|
44
|
-
<label class="form-label">Issuer</label>
|
|
45
|
-
<select id="f-issuer" class="form-select"></select>
|
|
46
|
-
</div>
|
|
47
|
-
<div>
|
|
48
|
-
<label class="form-label">Receiver</label>
|
|
49
|
-
<select id="f-receiver" class="form-select"></select>
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<div class="card mb-3">
|
|
56
|
-
<div class="card-header d-flex justify-content-between align-items-center">
|
|
57
|
-
<strong>Line Items</strong>
|
|
58
|
-
<button id="btn-add-line" type="button" class="btn btn-sm btn-outline">
|
|
59
|
-
<span data-icon="plus"></span> Add Line
|
|
60
|
-
</button>
|
|
61
|
-
</div>
|
|
62
|
-
<div class="card-body">
|
|
63
|
-
<table class="table mb-0" id="line-items-table">
|
|
64
|
-
<thead>
|
|
65
|
-
<tr>
|
|
66
|
-
<th style="width:55%;">Description</th>
|
|
67
|
-
<th style="width:10%;">Qty</th>
|
|
68
|
-
<th style="width:15%;">Unit (net)</th>
|
|
69
|
-
<th style="width:15%; text-align:right;">Total</th>
|
|
70
|
-
<th style="width:5%;"></th>
|
|
71
|
-
</tr>
|
|
72
|
-
</thead>
|
|
73
|
-
<tbody id="line-items-body"></tbody>
|
|
74
|
-
</table>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<div class="card mb-3">
|
|
79
|
-
<div class="card-header"><strong>Notes</strong></div>
|
|
80
|
-
<div class="card-body">
|
|
81
|
-
<textarea id="f-notes" class="form-control" rows="3" placeholder="Optional notes printed on the invoice"></textarea>
|
|
82
|
-
</div>
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
<div style="width:300px; flex-shrink:0;">
|
|
87
|
-
|
|
88
|
-
<div class="card mb-3">
|
|
89
|
-
<div class="card-header"><strong>Totals</strong></div>
|
|
90
|
-
<div class="card-body">
|
|
91
|
-
<div class="d-flex justify-content-between mb-2">
|
|
92
|
-
<span class="text-muted">Subtotal</span>
|
|
93
|
-
<span id="t-subtotal" style="font-variant-numeric:tabular-nums;">-</span>
|
|
94
|
-
</div>
|
|
95
|
-
<div class="d-flex justify-content-between mb-2" id="vat-row">
|
|
96
|
-
<span class="text-muted">VAT (<span id="t-vat-rate">20</span>%)</span>
|
|
97
|
-
<span id="t-vat" style="font-variant-numeric:tabular-nums;">-</span>
|
|
98
|
-
</div>
|
|
99
|
-
<hr>
|
|
100
|
-
<div class="d-flex justify-content-between">
|
|
101
|
-
<strong>Total</strong>
|
|
102
|
-
<strong id="t-total" style="font-variant-numeric:tabular-nums;">-</strong>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
<div class="card mb-3">
|
|
108
|
-
<div class="card-header"><strong>VAT</strong></div>
|
|
109
|
-
<div class="card-body">
|
|
110
|
-
<label class="d-flex align-items-center gap-2 mb-2" style="cursor:pointer;">
|
|
111
|
-
<input id="f-vat-enabled" type="checkbox" checked> Apply VAT
|
|
112
|
-
</label>
|
|
113
|
-
<label class="form-label">Rate %</label>
|
|
114
|
-
<input id="f-vat-rate" type="number" min="0" max="100" step="0.01" class="form-control" value="20">
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<div class="card mb-3">
|
|
119
|
-
<div class="card-header"><strong>Currency</strong></div>
|
|
120
|
-
<div class="card-body">
|
|
121
|
-
<select id="f-currency" class="form-select">
|
|
122
|
-
<option value="GBP">GBP - British Pound</option>
|
|
123
|
-
<option value="EUR">EUR - Euro</option>
|
|
124
|
-
<option value="USD">USD - US Dollar</option>
|
|
125
|
-
</select>
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|