@crombolo/backoffice-host 0.16.0 → 0.18.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/package.json +2 -2
- package/plugin/assistant/umbraco-package.json +1 -1
- package/plugin/branding/branding.js +93 -0
- package/plugin/branding/logo-header-dark.svg +1 -0
- package/plugin/branding/logo-surface-dark.svg +1 -0
- package/plugin/forms-client.js +3 -0
- package/plugin/forms-overview.js +187 -2
- package/plugin/forms-workflows.js +147 -0
- package/plugin/localizations/umbraco-package.json +1 -1
- package/plugin/notifications-dialog.js +62 -0
- package/plugin/seo/umbraco-package.json +1 -1
- package/plugin/sign-in/umbraco-package.json +1 -1
- package/plugin/umbraco-package.json +7 -1
- package/src/graphics.ts +7 -0
- package/src/theme-early.ts +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/backoffice-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Serves the Umbraco backoffice SPA: shell HTML, import map and package manifests.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Crombolo",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@crombolo/core": "0.
|
|
34
|
+
"@crombolo/core": "0.18.0",
|
|
35
35
|
"@crombolo/backoffice-dist": "18.2.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -70,8 +70,101 @@ function correctLogoLink() {
|
|
|
70
70
|
return true
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/** Where Umbraco's theme context keeps the choice, as the shells' early script reads it. */
|
|
74
|
+
const THEME_KEY = 'umb-theme-alias'
|
|
75
|
+
|
|
76
|
+
/** Whether a theme choice paints the back office dark: Dark, or Match my system on a dark computer. */
|
|
77
|
+
export function isDarkTheme(alias, prefersDark) {
|
|
78
|
+
return (
|
|
79
|
+
alias === 'Crombolo.Theme.Dark' ||
|
|
80
|
+
alias === 'umb-dark-theme' ||
|
|
81
|
+
(alias === 'Crombolo.Theme.System' && Boolean(prefersDark))
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const GRAPHIC =
|
|
86
|
+
/(\/security\/back-office\/graphics\/)(logo|logo-alternative|login-logo|login-logo-alternative)(-dark)?$/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A logo's address for the theme: the cream-lettered `-dark` variant on a dark
|
|
90
|
+
* theme, the plain one otherwise. Anything that is not one of the logos comes
|
|
91
|
+
* back as it was.
|
|
92
|
+
*/
|
|
93
|
+
export function themedLogoUrl(src, dark) {
|
|
94
|
+
const match = GRAPHIC.exec(src ?? '')
|
|
95
|
+
if (!match) return src
|
|
96
|
+
return `${src.slice(0, match.index)}${match[1]}${match[2]}${dark ? '-dark' : ''}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const prefersDark = () => {
|
|
100
|
+
try {
|
|
101
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
102
|
+
} catch {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const storedTheme = () => {
|
|
108
|
+
try {
|
|
109
|
+
return localStorage.getItem(THEME_KEY)
|
|
110
|
+
} catch {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The header's logo, and the wordmark in its popover, in the theme's colours.
|
|
117
|
+
*
|
|
118
|
+
* `umb-app-logo` picks its graphic from Umbraco's own theme names, and the
|
|
119
|
+
* header pins it to the one drawn for an orange bar, so its cocoa lettering
|
|
120
|
+
* vanished on the dark header. Like the link above, this reaches into the
|
|
121
|
+
* vendored element and does nothing if it is not what it was.
|
|
122
|
+
*/
|
|
123
|
+
function applyThemedLogos() {
|
|
124
|
+
const dark = isDarkTheme(storedTheme(), prefersDark())
|
|
125
|
+
const header = deepQuery('umb-backoffice-header-logo')?.shadowRoot
|
|
126
|
+
const mark = header?.querySelector('umb-app-logo')?.shadowRoot?.querySelector('img')
|
|
127
|
+
if (!mark) return false
|
|
128
|
+
const src = mark.getAttribute('src')
|
|
129
|
+
const next = themedLogoUrl(src, dark)
|
|
130
|
+
if (next !== src) mark.setAttribute('src', next)
|
|
131
|
+
const popover = header.querySelector('img#logo')
|
|
132
|
+
if (popover) {
|
|
133
|
+
popover.dataset.lightSrc ??= popover.getAttribute('src') ?? ''
|
|
134
|
+
const wanted = dark
|
|
135
|
+
? next.replace(/graphics\/logo-dark$/, 'graphics/logo-alternative-dark')
|
|
136
|
+
: popover.dataset.lightSrc
|
|
137
|
+
if (popover.getAttribute('src') !== wanted) popover.setAttribute('src', wanted)
|
|
138
|
+
}
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Applies the logos now and on every change of theme: the menu, another tab, or the computer's. */
|
|
143
|
+
function followTheme() {
|
|
144
|
+
const apply = () => setTimeout(applyThemedLogos, 0)
|
|
145
|
+
// The theme context swaps the theme's stylesheet in the head when the choice changes.
|
|
146
|
+
new MutationObserver(apply).observe(document.head, { childList: true })
|
|
147
|
+
window.addEventListener('storage', (event) => {
|
|
148
|
+
if (event.key === THEME_KEY) apply()
|
|
149
|
+
})
|
|
150
|
+
try {
|
|
151
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', apply)
|
|
152
|
+
} catch {
|
|
153
|
+
// Without media queries, Match my system stays light.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
73
157
|
export const onInit = () => {
|
|
74
158
|
const brand = brandName()
|
|
159
|
+
let themed = false
|
|
160
|
+
const logos = setInterval(() => {
|
|
161
|
+
if (applyThemedLogos() && !themed) {
|
|
162
|
+
themed = true
|
|
163
|
+
clearInterval(logos)
|
|
164
|
+
followTheme()
|
|
165
|
+
}
|
|
166
|
+
}, 250)
|
|
167
|
+
setTimeout(() => clearInterval(logos), 30_000)
|
|
75
168
|
const correct = () => {
|
|
76
169
|
const corrected = brandedTitle(document.title, brand)
|
|
77
170
|
if (corrected !== document.title) document.title = corrected
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="81.25 269.75 1022.75 237.5" role="img" aria-label="crombolo"><circle cx="200" cy="388.5" r="116.75" fill="#cb6f48"/><path transform="matrix(0.025 0 0 -0.025 75 516)" d="M4285 8205 c-459 -94 -647 -159 -1014 -349 -249 -130 -544 -347 -669 -494 -26 -30 -82 -96 -125 -145 -218 -254 -207 -378 24 -266 295 143 924 329 1113 329 19 0 78 9 131 20 128 26 792 30 910 5 39 -8 108 -20 155 -25 378 -43 1093 -273 1364 -438 45 -27 114 -66 153 -86 91 -45 440 -278 525 -349 35 -29 91 -74 124 -100 651 -496 1244 -1578 1303 -2379 13 -165 47 -197 125 -115 86 90 236 513 236 663 0 23 9 86 20 139 25 122 27 345 4 450 -8 39 -19 111 -24 162 -50 500 -394 1231 -774 1648 -50 55 -116 130 -145 167 -30 37 -81 89 -115 115 -33 26 -114 96 -178 155 -117 106 -442 334 -560 394 -29 14 -100 55 -158 89 -97 59 -240 123 -516 231 -122 47 -541 156 -671 174 -45 6 -112 17 -150 25 -152 32 -900 19 -1088 -20z M3300 6458 c-58 -11 -142 -26 -186 -34 -80 -14 -243 -58 -414 -111 -742 -234 -1299 -909 -1300 -1575 0 -312 82 -391 218 -208 298 402 1010 790 1450 790 17 0 74 9 127 20 79 16 136 20 305 20 168 0 228 -4 310 -20 56 -11 124 -20 152 -20 406 0 1286 -389 1680 -742 56 -52 133 -117 171 -147 37 -29 88 -81 114 -115 26 -33 75 -92 108 -131 303 -346 645 -1054 645 -1336 0 -14 9 -68 20 -120 23 -111 27 -371 8 -505 -22 -155 37 -185 183 -93 657 414 699 1621 89 2594 -137 218 -431 585 -518 647 -11 7 -75 66 -144 130 -138 130 -157 146 -338 273 -329 233 -595 368 -985 501 -208 72 -585 164 -670 164 -18 0 -79 9 -135 20 -147 29 -730 28 -890 -2z M2909 4331 c-921 -186 -1424 -895 -1087 -1533 105 -199 210 -222 296 -65 284 517 979 758 1655 572 241 -66 501 -192 647 -314 25 -21 78 -64 119 -97 262 -210 502 -618 586 -998 64 -293 346 -107 440 291 45 189 45 192 -20 531 -50 262 -304 691 -560 947 -304 304 -848 601 -1190 650 -28 4 -90 16 -140 26 -153 32 -566 26 -746 -10z" fill="#fdebd4"/><path transform="matrix(0.025 0 0 -0.025 340 470)" d="M17231 5945 c-168 -47 -253 -129 -320 -310 -32 -87 -48 -4483 -16 -4609 116 -459 691 -531 916 -115 64 119 84 124 184 38 765 -661 1980 -322 2461 686 211 443 246 966 103 1527 -156 616 -774 1208 -1325 1273 -33 4 -103 16 -157 26 -288 56 -760 -83 -1009 -295 -193 -166 -188 -183 -188 699 0 837 0 835 -144 974 -117 112 -326 156 -505 106z m1804 -2497 c449 -160 700 -668 575 -1164 -177 -708 -937 -952 -1444 -465 -535 516 -283 1523 414 1655 84 16 386 -2 455 -26z M25519 5946 c-139 -37 -259 -165 -328 -351 -41 -109 -46 -4365 -6 -4534 132 -554 862 -581 1000 -36 23 90 23 4460 0 4550 -76 297 -358 454 -666 371z M2125 4445 c-485 -100 -723 -223 -1037 -540 -245 -245 -338 -398 -452 -740 -219 -652 -94 -1396 311 -1852 46 -52 91 -105 99 -119 9 -13 66 -65 128 -116 295 -246 613 -393 943 -438 45 -6 112 -17 150 -25 103 -22 324 -19 463 5 63 12 153 28 200 36 766 137 1266 828 868 1200 -185 173 -405 157 -681 -48 -184 -137 -210 -151 -367 -204 -503 -170 -1103 189 -1182 706 -5 36 -16 102 -24 148 -41 249 136 680 339 823 406 286 884 272 1252 -37 290 -244 676 -128 770 231 147 567 -919 1148 -1780 970z M8460 4464 c-36 -7 -103 -19 -150 -25 -91 -12 -407 -112 -494 -156 -129 -66 -499 -343 -532 -399 -6 -11 -42 -55 -79 -99 -429 -503 -572 -1318 -336 -1911 86 -219 195 -419 289 -532 31 -38 78 -96 104 -130 27 -34 76 -83 110 -110 33 -26 95 -75 137 -110 521 -432 1453 -504 2071 -160 1479 825 1219 3159 -397 3572 -283 73 -560 96 -723 60z m423 -999 c75 -19 211 -88 326 -165 370 -248 476 -916 208 -1315 -457 -679 -1424 -500 -1628 302 -176 689 438 1349 1094 1178z M14940 4464 c-36 -7 -103 -19 -150 -25 -248 -33 -455 -137 -665 -334 -169 -159 -170 -159 -345 12 -176 172 -279 233 -485 290 -166 46 -660 43 -790 -6 -164 -60 -274 -124 -384 -221 -145 -128 -166 -128 -235 -1 -213 388 -797 302 -911 -134 -23 -89 -23 -2930 0 -3020 95 -368 575 -518 841 -264 148 142 144 114 144 1134 0 920 5 998 81 1200 165 442 803 510 1016 109 102 -194 97 -136 103 -1204 5 -965 5 -965 27 -1024 183 -481 916 -433 973 64 5 47 15 101 22 120 9 26 14 263 18 940 5 905 5 905 31 975 152 410 629 539 935 252 186 -173 188 -189 194 -1342 5 -980 5 -980 28 -1037 169 -418 775 -418 944 0 39 97 41 2271 2 2435 -92 387 -272 671 -547 865 -212 150 -645 260 -847 216z M22565 4445 c-282 -58 -363 -84 -555 -180 -506 -253 -953 -845 -1009 -1339 -6 -44 -17 -110 -25 -146 -60 -251 31 -828 177 -1115 199 -393 460 -651 865 -854 141 -71 383 -156 468 -166 33 -3 103 -15 156 -26 302 -60 757 21 1108 196 1042 522 1348 1936 628 2907 -416 563 -1157 859 -1813 723z m590 -997 c396 -142 605 -458 605 -913 0 -803 -753 -1231 -1403 -798 -576 383 -505 1340 123 1662 174 90 496 113 675 49z M28340 4464 c-35 -7 -101 -19 -145 -24 -484 -66 -963 -375 -1258 -815 -156 -233 -278 -547 -298 -765 -5 -58 -16 -132 -24 -165 -21 -87 -19 -270 5 -375 11 -47 20 -101 20 -120 2 -368 398 -997 793 -1261 386 -258 945 -393 1324 -320 54 10 124 22 157 26 709 84 1433 881 1486 1640 5 66 14 136 19 155 6 19 11 64 11 100 0 36 -5 81 -11 100 -5 19 -14 89 -19 155 -33 469 -342 1002 -764 1317 -216 162 -535 294 -793 328 -45 6 -112 17 -150 25 -85 18 -267 18 -353 -1z m455 -1016 c875 -312 832 -1609 -60 -1855 -748 -206 -1382 660 -1017 1392 121 243 374 442 622 489 84 16 386 -2 455 -26z M6060 4425 c-214 -48 -298 -92 -455 -240 -164 -154 -183 -157 -249 -36 -238 434 -814 353 -931 -130 -41 -171 -35 -2904 7 -3017 184 -495 793 -495 976 0 27 73 27 73 32 918 5 845 5 845 49 970 126 362 303 489 706 509 311 16 439 111 521 388 118 400 -226 735 -656 638z" fill="#f6ede4"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="81.25 269.75 1022.75 237.5" role="img" aria-label="crombolo"><circle cx="200" cy="388.5" r="116.75" fill="#cb6f48"/><path transform="matrix(0.025 0 0 -0.025 75 516)" d="M4285 8205 c-459 -94 -647 -159 -1014 -349 -249 -130 -544 -347 -669 -494 -26 -30 -82 -96 -125 -145 -218 -254 -207 -378 24 -266 295 143 924 329 1113 329 19 0 78 9 131 20 128 26 792 30 910 5 39 -8 108 -20 155 -25 378 -43 1093 -273 1364 -438 45 -27 114 -66 153 -86 91 -45 440 -278 525 -349 35 -29 91 -74 124 -100 651 -496 1244 -1578 1303 -2379 13 -165 47 -197 125 -115 86 90 236 513 236 663 0 23 9 86 20 139 25 122 27 345 4 450 -8 39 -19 111 -24 162 -50 500 -394 1231 -774 1648 -50 55 -116 130 -145 167 -30 37 -81 89 -115 115 -33 26 -114 96 -178 155 -117 106 -442 334 -560 394 -29 14 -100 55 -158 89 -97 59 -240 123 -516 231 -122 47 -541 156 -671 174 -45 6 -112 17 -150 25 -152 32 -900 19 -1088 -20z M3300 6458 c-58 -11 -142 -26 -186 -34 -80 -14 -243 -58 -414 -111 -742 -234 -1299 -909 -1300 -1575 0 -312 82 -391 218 -208 298 402 1010 790 1450 790 17 0 74 9 127 20 79 16 136 20 305 20 168 0 228 -4 310 -20 56 -11 124 -20 152 -20 406 0 1286 -389 1680 -742 56 -52 133 -117 171 -147 37 -29 88 -81 114 -115 26 -33 75 -92 108 -131 303 -346 645 -1054 645 -1336 0 -14 9 -68 20 -120 23 -111 27 -371 8 -505 -22 -155 37 -185 183 -93 657 414 699 1621 89 2594 -137 218 -431 585 -518 647 -11 7 -75 66 -144 130 -138 130 -157 146 -338 273 -329 233 -595 368 -985 501 -208 72 -585 164 -670 164 -18 0 -79 9 -135 20 -147 29 -730 28 -890 -2z M2909 4331 c-921 -186 -1424 -895 -1087 -1533 105 -199 210 -222 296 -65 284 517 979 758 1655 572 241 -66 501 -192 647 -314 25 -21 78 -64 119 -97 262 -210 502 -618 586 -998 64 -293 346 -107 440 291 45 189 45 192 -20 531 -50 262 -304 691 -560 947 -304 304 -848 601 -1190 650 -28 4 -90 16 -140 26 -153 32 -566 26 -746 -10z" fill="#fdebd4"/><path transform="matrix(0.025 0 0 -0.025 340 470)" d="M17231 5945 c-168 -47 -253 -129 -320 -310 -32 -87 -48 -4483 -16 -4609 116 -459 691 -531 916 -115 64 119 84 124 184 38 765 -661 1980 -322 2461 686 211 443 246 966 103 1527 -156 616 -774 1208 -1325 1273 -33 4 -103 16 -157 26 -288 56 -760 -83 -1009 -295 -193 -166 -188 -183 -188 699 0 837 0 835 -144 974 -117 112 -326 156 -505 106z m1804 -2497 c449 -160 700 -668 575 -1164 -177 -708 -937 -952 -1444 -465 -535 516 -283 1523 414 1655 84 16 386 -2 455 -26z M25519 5946 c-139 -37 -259 -165 -328 -351 -41 -109 -46 -4365 -6 -4534 132 -554 862 -581 1000 -36 23 90 23 4460 0 4550 -76 297 -358 454 -666 371z M2125 4445 c-485 -100 -723 -223 -1037 -540 -245 -245 -338 -398 -452 -740 -219 -652 -94 -1396 311 -1852 46 -52 91 -105 99 -119 9 -13 66 -65 128 -116 295 -246 613 -393 943 -438 45 -6 112 -17 150 -25 103 -22 324 -19 463 5 63 12 153 28 200 36 766 137 1266 828 868 1200 -185 173 -405 157 -681 -48 -184 -137 -210 -151 -367 -204 -503 -170 -1103 189 -1182 706 -5 36 -16 102 -24 148 -41 249 136 680 339 823 406 286 884 272 1252 -37 290 -244 676 -128 770 231 147 567 -919 1148 -1780 970z M8460 4464 c-36 -7 -103 -19 -150 -25 -91 -12 -407 -112 -494 -156 -129 -66 -499 -343 -532 -399 -6 -11 -42 -55 -79 -99 -429 -503 -572 -1318 -336 -1911 86 -219 195 -419 289 -532 31 -38 78 -96 104 -130 27 -34 76 -83 110 -110 33 -26 95 -75 137 -110 521 -432 1453 -504 2071 -160 1479 825 1219 3159 -397 3572 -283 73 -560 96 -723 60z m423 -999 c75 -19 211 -88 326 -165 370 -248 476 -916 208 -1315 -457 -679 -1424 -500 -1628 302 -176 689 438 1349 1094 1178z M14940 4464 c-36 -7 -103 -19 -150 -25 -248 -33 -455 -137 -665 -334 -169 -159 -170 -159 -345 12 -176 172 -279 233 -485 290 -166 46 -660 43 -790 -6 -164 -60 -274 -124 -384 -221 -145 -128 -166 -128 -235 -1 -213 388 -797 302 -911 -134 -23 -89 -23 -2930 0 -3020 95 -368 575 -518 841 -264 148 142 144 114 144 1134 0 920 5 998 81 1200 165 442 803 510 1016 109 102 -194 97 -136 103 -1204 5 -965 5 -965 27 -1024 183 -481 916 -433 973 64 5 47 15 101 22 120 9 26 14 263 18 940 5 905 5 905 31 975 152 410 629 539 935 252 186 -173 188 -189 194 -1342 5 -980 5 -980 28 -1037 169 -418 775 -418 944 0 39 97 41 2271 2 2435 -92 387 -272 671 -547 865 -212 150 -645 260 -847 216z M22565 4445 c-282 -58 -363 -84 -555 -180 -506 -253 -953 -845 -1009 -1339 -6 -44 -17 -110 -25 -146 -60 -251 31 -828 177 -1115 199 -393 460 -651 865 -854 141 -71 383 -156 468 -166 33 -3 103 -15 156 -26 302 -60 757 21 1108 196 1042 522 1348 1936 628 2907 -416 563 -1157 859 -1813 723z m590 -997 c396 -142 605 -458 605 -913 0 -803 -753 -1231 -1403 -798 -576 383 -505 1340 123 1662 174 90 496 113 675 49z M28340 4464 c-35 -7 -101 -19 -145 -24 -484 -66 -963 -375 -1258 -815 -156 -233 -278 -547 -298 -765 -5 -58 -16 -132 -24 -165 -21 -87 -19 -270 5 -375 11 -47 20 -101 20 -120 2 -368 398 -997 793 -1261 386 -258 945 -393 1324 -320 54 10 124 22 157 26 709 84 1433 881 1486 1640 5 66 14 136 19 155 6 19 11 64 11 100 0 36 -5 81 -11 100 -5 19 -14 89 -19 155 -33 469 -342 1002 -764 1317 -216 162 -535 294 -793 328 -45 6 -112 17 -150 25 -85 18 -267 18 -353 -1z m455 -1016 c875 -312 832 -1609 -60 -1855 -748 -206 -1382 660 -1017 1392 121 243 374 442 622 489 84 16 386 -2 455 -26z M6060 4425 c-214 -48 -298 -92 -455 -240 -164 -154 -183 -157 -249 -36 -238 434 -814 353 -931 -130 -41 -171 -35 -2904 7 -3017 184 -495 793 -495 976 0 27 73 27 73 32 918 5 845 5 845 49 970 126 362 303 489 706 509 311 16 439 111 521 388 118 400 -226 735 -656 638z" fill="#f6ede4"/></svg>
|
package/plugin/forms-client.js
CHANGED
|
@@ -30,6 +30,9 @@ export const listForms = async () => (await call('')).body.items ?? []
|
|
|
30
30
|
/** The field and workflow types this site has, built in and from its bundles. */
|
|
31
31
|
export const listFormTypes = async () => (await call('/available-types')).body
|
|
32
32
|
|
|
33
|
+
/** The site's e-mail templates, for a Send email workflow's "E-mail design". */
|
|
34
|
+
export const listEmailTemplates = async () => (await call('/email-templates')).body.items ?? []
|
|
35
|
+
|
|
33
36
|
export const readForm = async (key) => (await call(`/${encodeURIComponent(key)}`)).body
|
|
34
37
|
|
|
35
38
|
export const saveForm = (definition) =>
|
package/plugin/forms-overview.js
CHANGED
|
@@ -17,6 +17,7 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
|
17
17
|
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'
|
|
18
18
|
import {
|
|
19
19
|
deleteForm,
|
|
20
|
+
listEmailTemplates,
|
|
20
21
|
listForms,
|
|
21
22
|
listFormTypes,
|
|
22
23
|
previewChoices,
|
|
@@ -40,6 +41,17 @@ import {
|
|
|
40
41
|
setPageCaption,
|
|
41
42
|
withLayout,
|
|
42
43
|
} from './forms-layout.js'
|
|
44
|
+
import {
|
|
45
|
+
addWorkflow,
|
|
46
|
+
describeProblemPath,
|
|
47
|
+
removeWorkflow,
|
|
48
|
+
setWorkflowName,
|
|
49
|
+
setWorkflowSetting,
|
|
50
|
+
setWorkflowTrigger,
|
|
51
|
+
setWorkflowType,
|
|
52
|
+
switchValue,
|
|
53
|
+
workflowSettings,
|
|
54
|
+
} from './forms-workflows.js'
|
|
43
55
|
|
|
44
56
|
/** What a preview depends on, so it survives a layout change that copies the field. */
|
|
45
57
|
const sourceKey = (source) => `${source.kind}|${source.parent}`
|
|
@@ -60,6 +72,8 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
60
72
|
_busy: { state: true },
|
|
61
73
|
_choices: { state: true },
|
|
62
74
|
_types: { state: true },
|
|
75
|
+
_workflowTypes: { state: true },
|
|
76
|
+
_designs: { state: true },
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
#notifications
|
|
@@ -78,6 +92,10 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
78
92
|
this._choices = new Map()
|
|
79
93
|
/** Field types by name, as the server has them: built in and from bundles. */
|
|
80
94
|
this._types = new Map()
|
|
95
|
+
/** Workflow types by name, likewise. */
|
|
96
|
+
this._workflowTypes = new Map()
|
|
97
|
+
/** The site's e-mail templates, for a Send email workflow's design. */
|
|
98
|
+
this._designs = []
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
connectedCallback() {
|
|
@@ -87,9 +105,16 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
87
105
|
|
|
88
106
|
async #load() {
|
|
89
107
|
try {
|
|
90
|
-
const [forms, types] = await Promise.all([
|
|
108
|
+
const [forms, types, designs] = await Promise.all([
|
|
109
|
+
listForms(),
|
|
110
|
+
listFormTypes(),
|
|
111
|
+
// The picker can still offer Standard without them.
|
|
112
|
+
listEmailTemplates().catch(() => []),
|
|
113
|
+
])
|
|
91
114
|
this._forms = forms
|
|
92
115
|
this._types = new Map(types.fields.map((info) => [info.type, info]))
|
|
116
|
+
this._workflowTypes = new Map(types.workflows.map((info) => [info.type, info]))
|
|
117
|
+
this._designs = designs
|
|
93
118
|
} catch (error) {
|
|
94
119
|
this._forms = []
|
|
95
120
|
this._notice = error.message
|
|
@@ -484,6 +509,137 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
484
509
|
</uui-table>`
|
|
485
510
|
}
|
|
486
511
|
|
|
512
|
+
/** What happens after someone sends the form: each workflow with its settings. */
|
|
513
|
+
#workflows() {
|
|
514
|
+
const form = this._editing
|
|
515
|
+
const workflows = form.workflows ?? []
|
|
516
|
+
const types = [...this._workflowTypes.values()]
|
|
517
|
+
return html`<uui-box headline="Workflows" class="workflows">
|
|
518
|
+
<p class="offered">What happens after someone sends this form.</p>
|
|
519
|
+
${
|
|
520
|
+
workflows.length === 0
|
|
521
|
+
? html`<p>Nothing happens yet${form.storeEntries === false ? '' : ' beyond keeping the entry'}.</p>`
|
|
522
|
+
: nothing
|
|
523
|
+
}
|
|
524
|
+
${repeat(
|
|
525
|
+
workflows,
|
|
526
|
+
(workflow, index) => workflow.key ?? index,
|
|
527
|
+
(
|
|
528
|
+
workflow,
|
|
529
|
+
index,
|
|
530
|
+
) => html`<section class="workflow" aria-label=${workflow.name || `Workflow ${index + 1}`}>
|
|
531
|
+
<div class="bar">
|
|
532
|
+
<uui-input
|
|
533
|
+
label="Workflow name"
|
|
534
|
+
placeholder="Name"
|
|
535
|
+
.value=${workflow.name ?? ''}
|
|
536
|
+
@change=${(event) => this.#apply(setWorkflowName, index, event.target.value)}
|
|
537
|
+
></uui-input>
|
|
538
|
+
<select
|
|
539
|
+
aria-label="What it does"
|
|
540
|
+
@change=${(event) => this.#apply(setWorkflowType, index, event.target.value)}
|
|
541
|
+
>
|
|
542
|
+
${types.map(
|
|
543
|
+
(info) =>
|
|
544
|
+
html`<option value=${info.type} ?selected=${info.type === workflow.type}>${info.label}</option>`,
|
|
545
|
+
)}
|
|
546
|
+
</select>
|
|
547
|
+
<select
|
|
548
|
+
aria-label="When it runs"
|
|
549
|
+
@change=${(event) => this.#apply(setWorkflowTrigger, index, event.target.value)}
|
|
550
|
+
>
|
|
551
|
+
<option value="submit" ?selected=${workflow.on !== 'approve'}>When the form is sent</option>
|
|
552
|
+
<option value="approve" ?selected=${workflow.on === 'approve'}>When an entry is approved</option>
|
|
553
|
+
</select>
|
|
554
|
+
<uui-button compact look="secondary" color="danger" label="Remove workflow"
|
|
555
|
+
@click=${() => {
|
|
556
|
+
if (!confirm(`Remove the workflow "${workflow.name || 'untitled'}"?`)) return
|
|
557
|
+
this.#apply(removeWorkflow, index)
|
|
558
|
+
}}></uui-button>
|
|
559
|
+
</div>
|
|
560
|
+
${this.#workflowSettings(workflow, index)}
|
|
561
|
+
</section>`,
|
|
562
|
+
)}
|
|
563
|
+
<uui-button
|
|
564
|
+
look="secondary"
|
|
565
|
+
label="Add a workflow"
|
|
566
|
+
@click=${() => {
|
|
567
|
+
const first = this._workflowTypes.get('sendEmail') ?? types[0]
|
|
568
|
+
if (first) this.#apply(addWorkflow, first.type, first.label)
|
|
569
|
+
}}
|
|
570
|
+
></uui-button>
|
|
571
|
+
</uui-box>`
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
#workflowSettings(workflow, index) {
|
|
575
|
+
const settings = workflowSettings(workflow.type, this._workflowTypes.get(workflow.type))
|
|
576
|
+
if (settings.length === 0) return nothing
|
|
577
|
+
const write = (key, value) => this.#apply(setWorkflowSetting, index, key, value)
|
|
578
|
+
return html`<div class="workflow-settings">
|
|
579
|
+
${settings.map((setting) => {
|
|
580
|
+
const value = workflow.settings?.[setting.key]
|
|
581
|
+
const label = `${setting.label}${setting.required ? ' *' : ''}`
|
|
582
|
+
const hint = setting.description
|
|
583
|
+
? html`<small class="offered">${setting.description}</small>`
|
|
584
|
+
: nothing
|
|
585
|
+
if (setting.kind === 'boolean')
|
|
586
|
+
return html`<div class="switch">
|
|
587
|
+
<uui-toggle
|
|
588
|
+
label=${label}
|
|
589
|
+
.checked=${value === undefined ? setting.defaultValue === true : value === true}
|
|
590
|
+
@change=${(event) => write(setting.key, switchValue(setting, event.target.checked))}
|
|
591
|
+
></uui-toggle>
|
|
592
|
+
${hint}
|
|
593
|
+
</div>`
|
|
594
|
+
if (setting.kind === 'design') {
|
|
595
|
+
const known = this._designs.some((design) => design.alias === value)
|
|
596
|
+
return html`<label>
|
|
597
|
+
<span>${label}</span>
|
|
598
|
+
<select
|
|
599
|
+
aria-label=${setting.label}
|
|
600
|
+
@change=${(event) => write(setting.key, event.target.value || undefined)}
|
|
601
|
+
>
|
|
602
|
+
<option value="" ?selected=${!value}>Standard</option>
|
|
603
|
+
${this._designs.map(
|
|
604
|
+
(design) =>
|
|
605
|
+
html`<option value=${design.alias} ?selected=${design.alias === value}>${design.name}</option>`,
|
|
606
|
+
)}
|
|
607
|
+
${
|
|
608
|
+
value && !known
|
|
609
|
+
? html`<option value=${value} selected>${value} (not found)</option>`
|
|
610
|
+
: nothing
|
|
611
|
+
}
|
|
612
|
+
</select>
|
|
613
|
+
${hint}
|
|
614
|
+
</label>`
|
|
615
|
+
}
|
|
616
|
+
return html`<label>
|
|
617
|
+
<span>${label}</span>
|
|
618
|
+
<uui-input
|
|
619
|
+
label=${setting.label}
|
|
620
|
+
type=${setting.kind === 'number' ? 'number' : 'text'}
|
|
621
|
+
.value=${value === undefined ? '' : Array.isArray(value) ? value.join(', ') : String(value)}
|
|
622
|
+
@change=${(event) => {
|
|
623
|
+
const raw = event.target.value.trim()
|
|
624
|
+
if (raw === '') return write(setting.key, undefined)
|
|
625
|
+
if (setting.kind === 'number') return write(setting.key, Number(raw))
|
|
626
|
+
if (setting.kind === 'list')
|
|
627
|
+
return write(
|
|
628
|
+
setting.key,
|
|
629
|
+
raw
|
|
630
|
+
.split(',')
|
|
631
|
+
.map((item) => item.trim())
|
|
632
|
+
.filter(Boolean),
|
|
633
|
+
)
|
|
634
|
+
write(setting.key, raw)
|
|
635
|
+
}}
|
|
636
|
+
></uui-input>
|
|
637
|
+
${hint}
|
|
638
|
+
</label>`
|
|
639
|
+
})}
|
|
640
|
+
</div>`
|
|
641
|
+
}
|
|
642
|
+
|
|
487
643
|
#touch() {
|
|
488
644
|
// Lit tracks the reference, not what is inside it.
|
|
489
645
|
this._editing = { ...this._editing }
|
|
@@ -636,12 +792,22 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
636
792
|
<uui-button look="secondary" label="Add a page" @click=${() => this.#apply(addPage)}></uui-button>
|
|
637
793
|
</div>
|
|
638
794
|
</uui-box>
|
|
795
|
+
${this.#workflows()}
|
|
639
796
|
${
|
|
640
797
|
this._problems.length > 0
|
|
641
798
|
? html`<div class="problems" role="alert">
|
|
642
799
|
<p>This form was not saved:</p>
|
|
643
800
|
<ul>
|
|
644
|
-
${this._problems.map((problem) =>
|
|
801
|
+
${this._problems.map((problem) => {
|
|
802
|
+
const where = describeProblemPath(form, problem.path, this._workflowTypes)
|
|
803
|
+
return html`<li>${
|
|
804
|
+
!where
|
|
805
|
+
? nothing
|
|
806
|
+
: where === problem.path
|
|
807
|
+
? html`<code>${where}</code> `
|
|
808
|
+
: html`<strong>${where}</strong>: `
|
|
809
|
+
}${problem.message}</li>`
|
|
810
|
+
})}
|
|
645
811
|
</ul>
|
|
646
812
|
</div>`
|
|
647
813
|
: nothing
|
|
@@ -712,6 +878,25 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
712
878
|
color: var(--uui-color-text-alt, #555);
|
|
713
879
|
font-size: 0.9em;
|
|
714
880
|
}
|
|
881
|
+
.workflows {
|
|
882
|
+
margin-top: var(--uui-size-layout-1, 24px);
|
|
883
|
+
}
|
|
884
|
+
.workflow {
|
|
885
|
+
border: 1px solid var(--uui-color-border, #ddd);
|
|
886
|
+
border-radius: var(--uui-border-radius, 3px);
|
|
887
|
+
padding: var(--uui-size-space-4, 12px);
|
|
888
|
+
margin: var(--uui-size-space-4, 12px) 0;
|
|
889
|
+
}
|
|
890
|
+
.workflow-settings {
|
|
891
|
+
display: grid;
|
|
892
|
+
gap: var(--uui-size-space-3, 9px);
|
|
893
|
+
max-width: 60ch;
|
|
894
|
+
}
|
|
895
|
+
.workflow-settings label,
|
|
896
|
+
.workflow-settings .switch {
|
|
897
|
+
display: grid;
|
|
898
|
+
gap: 2px;
|
|
899
|
+
}
|
|
715
900
|
:host {
|
|
716
901
|
display: block;
|
|
717
902
|
height: 100%;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The forms designer's workflow operations: adding, retyping and removing a
|
|
3
|
+
* form's workflows, and what each built-in type asks for. Pure, so each is
|
|
4
|
+
* tested on its own (tests/forms-designer-workflows.test.ts).
|
|
5
|
+
*
|
|
6
|
+
* Every operation returns a new form. A setting the designer draws no field for
|
|
7
|
+
* (`cc`, `body`, `map` and the rest) is kept as the file has it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The settings the designer offers for the built-in types, in the shape a
|
|
12
|
+
* bundle's type declares them. `design` is the e-mail design picker.
|
|
13
|
+
*/
|
|
14
|
+
export const BUILT_IN_WORKFLOW_SETTINGS = {
|
|
15
|
+
sendEmail: [
|
|
16
|
+
{
|
|
17
|
+
key: 'to',
|
|
18
|
+
label: 'Send to',
|
|
19
|
+
kind: 'text',
|
|
20
|
+
required: true,
|
|
21
|
+
description:
|
|
22
|
+
'One or more addresses, separated by commas. An alias in braces, such as {email}, puts in that answer.',
|
|
23
|
+
},
|
|
24
|
+
{ key: 'subject', label: 'Subject', kind: 'text', required: true },
|
|
25
|
+
{ key: 'reply-to', label: 'Reply to', kind: 'text' },
|
|
26
|
+
{
|
|
27
|
+
key: 'template',
|
|
28
|
+
label: 'E-mail design',
|
|
29
|
+
kind: 'design',
|
|
30
|
+
description: 'Standard lists each question with its answer.',
|
|
31
|
+
},
|
|
32
|
+
{ key: 'attach-uploads', label: 'Attach uploaded files', kind: 'boolean' },
|
|
33
|
+
{
|
|
34
|
+
key: 'include-sensitive',
|
|
35
|
+
label: 'Include sensitive answers',
|
|
36
|
+
kind: 'boolean',
|
|
37
|
+
description: 'Left off, answers to fields marked sensitive are not sent.',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
saveAsContent: [
|
|
41
|
+
{ key: 'content-type', label: 'Document type alias', kind: 'text', required: true },
|
|
42
|
+
{
|
|
43
|
+
key: 'parent',
|
|
44
|
+
label: 'Create under',
|
|
45
|
+
kind: 'text',
|
|
46
|
+
required: true,
|
|
47
|
+
description: "The parent page's key, or root for the top of the tree.",
|
|
48
|
+
},
|
|
49
|
+
{ key: 'name-field', label: 'Name it from the field', kind: 'text' },
|
|
50
|
+
{ key: 'publish', label: 'Publish straight away', kind: 'boolean' },
|
|
51
|
+
],
|
|
52
|
+
sendToUrl: [
|
|
53
|
+
{ key: 'url', label: 'Address', kind: 'text', required: true },
|
|
54
|
+
{
|
|
55
|
+
key: 'include-standard-fields',
|
|
56
|
+
label: "Include the form's name and when it was sent",
|
|
57
|
+
kind: 'boolean',
|
|
58
|
+
defaultValue: true,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* What a switch writes: nothing when it matches the default, so a flag that is
|
|
65
|
+
* off by default is absent rather than `false`, and one on by default the reverse.
|
|
66
|
+
*/
|
|
67
|
+
export const switchValue = (setting, checked) =>
|
|
68
|
+
checked === (setting.defaultValue === true) ? undefined : checked
|
|
69
|
+
|
|
70
|
+
/** The fields to draw for a workflow type: the built-in table, or what a bundle declares. */
|
|
71
|
+
export const workflowSettings = (type, info) =>
|
|
72
|
+
BUILT_IN_WORKFLOW_SETTINGS[type] ?? info?.settings ?? []
|
|
73
|
+
|
|
74
|
+
/** A name no other workflow on the form has, from the type's label. */
|
|
75
|
+
export function uniqueWorkflowName(form, label) {
|
|
76
|
+
const taken = new Set((form.workflows ?? []).map((workflow) => workflow.name.toLowerCase()))
|
|
77
|
+
if (!taken.has(label.toLowerCase())) return label
|
|
78
|
+
for (let n = 2; ; n++) if (!taken.has(`${label} ${n}`.toLowerCase())) return `${label} ${n}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function addWorkflow(form, type, label) {
|
|
82
|
+
const workflow = {
|
|
83
|
+
key: crypto.randomUUID(),
|
|
84
|
+
type,
|
|
85
|
+
name: uniqueWorkflowName(form, label),
|
|
86
|
+
on: 'submit',
|
|
87
|
+
settings: {},
|
|
88
|
+
}
|
|
89
|
+
return { ...form, workflows: [...(form.workflows ?? []), workflow] }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function removeWorkflow(form, index) {
|
|
93
|
+
return { ...form, workflows: form.workflows.filter((_, at) => at !== index) }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const replace = (form, index, change) => ({
|
|
97
|
+
...form,
|
|
98
|
+
workflows: form.workflows.map((workflow, at) =>
|
|
99
|
+
at === index ? { ...workflow, ...change(workflow) } : workflow,
|
|
100
|
+
),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
/** A new type takes none of the old one's settings: they would be refused as unknown. */
|
|
104
|
+
export const setWorkflowType = (form, index, type) =>
|
|
105
|
+
replace(form, index, () => ({ type, settings: {} }))
|
|
106
|
+
|
|
107
|
+
export const setWorkflowName = (form, index, name) => replace(form, index, () => ({ name }))
|
|
108
|
+
|
|
109
|
+
export const setWorkflowTrigger = (form, index, on) =>
|
|
110
|
+
replace(form, index, () => ({ on: on === 'approve' ? 'approve' : 'submit' }))
|
|
111
|
+
|
|
112
|
+
/** An empty text or `undefined` removes the setting, so the file stays as short as it can. */
|
|
113
|
+
export const setWorkflowSetting = (form, index, key, value) =>
|
|
114
|
+
replace(form, index, (workflow) => {
|
|
115
|
+
const settings = { ...workflow.settings }
|
|
116
|
+
if (value === undefined || value === '') delete settings[key]
|
|
117
|
+
else settings[key] = value
|
|
118
|
+
return { settings }
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
/** Settings only the file sets, named as a person would say them when one is refused. */
|
|
122
|
+
const FILE_ONLY_LABELS = {
|
|
123
|
+
body: 'Message text',
|
|
124
|
+
cc: 'Copy to',
|
|
125
|
+
bcc: 'Blind copy to',
|
|
126
|
+
from: 'Send from',
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Where a problem the server found is, in words: "Tell the team: E-mail design"
|
|
131
|
+
* for `workflow[0].settings.template`. A path elsewhere in the form is left as
|
|
132
|
+
* it is.
|
|
133
|
+
*/
|
|
134
|
+
export function describeProblemPath(form, path, types = new Map()) {
|
|
135
|
+
const found = /^workflow\[(\d+)\](?:\.settings(?:\.([\w-]+))?|\.(name|on))?/.exec(path ?? '')
|
|
136
|
+
if (!found) return path
|
|
137
|
+
const index = Number(found[1])
|
|
138
|
+
const workflow = form.workflows?.[index]
|
|
139
|
+
const name = workflow?.name || `Workflow ${index + 1}`
|
|
140
|
+
if (found[3] === 'name') return `${name}: Name`
|
|
141
|
+
if (found[3] === 'on') return `${name}: When it runs`
|
|
142
|
+
if (!found[2]) return name
|
|
143
|
+
const setting = workflowSettings(workflow?.type, types.get(workflow?.type)).find(
|
|
144
|
+
(candidate) => candidate.key === found[2],
|
|
145
|
+
)
|
|
146
|
+
return `${name}: ${setting?.label ?? FILE_ONLY_LABELS[found[2]] ?? found[2]}`
|
|
147
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Umbraco's notifications dialog, on a site that cannot send e-mail (story
|
|
3
|
+
* 045): a banner saying so, and every toggle disabled, rather than choices
|
|
4
|
+
* saved for e-mails that never come.
|
|
5
|
+
*
|
|
6
|
+
* The dialog is not an extension point and its class is not exported, so it is
|
|
7
|
+
* patched rather than replaced, as the log viewer's menu is: once the element
|
|
8
|
+
* is defined, each update disables its toggles and Save and puts the banner
|
|
9
|
+
* above them. `tests/notifications-dialog.test.ts` pins the vendored dialog
|
|
10
|
+
* this relies on, so a re-vendor that moves it fails the suite rather than the
|
|
11
|
+
* editor. The server refuses a new subscription regardless.
|
|
12
|
+
*/
|
|
13
|
+
const TAG = 'umb-document-notifications-modal'
|
|
14
|
+
export const BANNER_CLASS = 'crombolo-notifications-unavailable'
|
|
15
|
+
export const UNAVAILABLE =
|
|
16
|
+
"This site can't send e-mail yet, so it can't tell you when this page changes. Ask an administrator to set up e-mail, then come back to choose what to hear about."
|
|
17
|
+
|
|
18
|
+
/** Whether this site can send notification e-mails; asked once per page load. */
|
|
19
|
+
let answer
|
|
20
|
+
export function canSendNotifications(fetcher = fetch) {
|
|
21
|
+
answer ??= (async () => {
|
|
22
|
+
const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
23
|
+
const response = await fetcher(`${href.replace(/\/$/, '')}/crombolo/api/email`, {
|
|
24
|
+
credentials: 'include',
|
|
25
|
+
headers: { accept: 'application/json' },
|
|
26
|
+
})
|
|
27
|
+
// Unknown is not unavailable: Umbraco's dialog stays as it is.
|
|
28
|
+
if (!response.ok) return true
|
|
29
|
+
return (await response.json()).canSendNotifications !== false
|
|
30
|
+
})().catch(() => true)
|
|
31
|
+
return answer
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Disables the dialog's choices and says why. Safe to call on every update. */
|
|
35
|
+
export function markUnavailable(root, message = UNAVAILABLE) {
|
|
36
|
+
for (const toggle of root.querySelectorAll('uui-toggle')) toggle.disabled = true
|
|
37
|
+
for (const button of root.querySelectorAll('uui-button[color="positive"]')) button.disabled = true
|
|
38
|
+
const box = root.querySelector('uui-box')
|
|
39
|
+
if (!box || box.querySelector(`.${BANNER_CLASS}`)) return
|
|
40
|
+
const banner = box.ownerDocument.createElement('p')
|
|
41
|
+
banner.className = BANNER_CLASS
|
|
42
|
+
banner.setAttribute('role', 'status')
|
|
43
|
+
banner.setAttribute(
|
|
44
|
+
'style',
|
|
45
|
+
'margin: 0 0 var(--uui-size-space-4); color: var(--uui-color-text-alt);',
|
|
46
|
+
)
|
|
47
|
+
banner.textContent = message
|
|
48
|
+
box.prepend(banner)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const onInit = async () => {
|
|
52
|
+
// The dialog is loaded the first time someone opens it.
|
|
53
|
+
const element = await customElements.whenDefined(TAG)
|
|
54
|
+
const prototype = element.prototype
|
|
55
|
+
const original = prototype.updated
|
|
56
|
+
prototype.updated = function patched(changed) {
|
|
57
|
+
original?.call(this, changed)
|
|
58
|
+
void canSendNotifications().then((ok) => {
|
|
59
|
+
if (!ok && this.renderRoot) markUnavailable(this.renderRoot)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../../backoffice-dist/dist/umbraco-package-schema.json",
|
|
3
3
|
"name": "Crombolo",
|
|
4
4
|
"id": "Crombolo.Core",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.18.0",
|
|
6
6
|
"allowPublicAccess": false,
|
|
7
7
|
"extensions": [
|
|
8
8
|
{
|
|
@@ -418,6 +418,12 @@
|
|
|
418
418
|
"name": "Crombolo Log Viewer Search Menu",
|
|
419
419
|
"js": "/crombolo-plugin/branding/log-viewer-menu.js"
|
|
420
420
|
},
|
|
421
|
+
{
|
|
422
|
+
"type": "backofficeEntryPoint",
|
|
423
|
+
"alias": "Crombolo.EntryPoint.NotificationsDialog",
|
|
424
|
+
"name": "Crombolo Notifications Dialog Without E-mail",
|
|
425
|
+
"js": "/crombolo-plugin/notifications-dialog.js"
|
|
426
|
+
},
|
|
421
427
|
{
|
|
422
428
|
"type": "globalContext",
|
|
423
429
|
"alias": "Crombolo.GlobalContext.Icons",
|
package/src/graphics.ts
CHANGED
|
@@ -27,6 +27,13 @@ const GRAPHICS: Record<string, string> = {
|
|
|
27
27
|
'logo-alternative': 'branding/logo-surface.svg',
|
|
28
28
|
'login-logo': 'branding/logo-header.svg',
|
|
29
29
|
'login-logo-alternative': 'branding/logo-surface.svg',
|
|
30
|
+
// The same marks with cream lettering, for the dark theme. The client asks
|
|
31
|
+
// for the plain names whatever the theme, so the plugin and the shells'
|
|
32
|
+
// early script swap these in (`branding.js`, `theme-early.ts`).
|
|
33
|
+
'logo-dark': 'branding/logo-header-dark.svg',
|
|
34
|
+
'logo-alternative-dark': 'branding/logo-surface-dark.svg',
|
|
35
|
+
'login-logo-dark': 'branding/logo-header-dark.svg',
|
|
36
|
+
'login-logo-alternative-dark': 'branding/logo-surface-dark.svg',
|
|
30
37
|
'login-background': 'branding/backdrop.svg',
|
|
31
38
|
}
|
|
32
39
|
|
package/src/theme-early.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* (story 049). Umbraco's theme context keeps the choice in `localStorage` and
|
|
4
4
|
* applies it once the client has booted, so a reload painted light first; and
|
|
5
5
|
* the login shell boots no theme context at all. This inline script reads the
|
|
6
|
-
* same key and links the same stylesheet at once
|
|
6
|
+
* same key and links the same stylesheet at once, and on a dark theme points
|
|
7
|
+
* the sign-in screens' logos at their cream-lettered variants.
|
|
7
8
|
*/
|
|
8
9
|
import type { BackOfficePaths } from './paths.ts'
|
|
9
10
|
|
|
@@ -27,6 +28,9 @@ export function themeStylesheets(paths: BackOfficePaths): Record<string, string>
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/** The sign-in screens' logos, which get their cream-lettered `-dark` variant on a dark theme. */
|
|
32
|
+
const LOGO_PATTERN = String.raw`/\/graphics\/(login-logo|login-logo-alternative|logo|logo-alternative)$/`
|
|
33
|
+
|
|
30
34
|
/** The inline script, as the shells embed it. It fails quietly: light is a fine fallback. */
|
|
31
35
|
export function earlyThemeScript(paths: BackOfficePaths): string {
|
|
32
36
|
const sheets = JSON.stringify(themeStylesheets(paths))
|
|
@@ -37,6 +41,19 @@ export function earlyThemeScript(paths: BackOfficePaths): string {
|
|
|
37
41
|
var alias = localStorage.getItem(key);
|
|
38
42
|
var renamed = ${renamed};
|
|
39
43
|
if (alias && renamed[alias]) { alias = renamed[alias]; localStorage.setItem(key, alias); }
|
|
44
|
+
var dark = alias === ${JSON.stringify(THEME_DARK)} ||
|
|
45
|
+
(alias === ${JSON.stringify(THEME_SYSTEM)} && typeof matchMedia === 'function' &&
|
|
46
|
+
matchMedia('(prefers-color-scheme: dark)').matches);
|
|
47
|
+
if (dark) document.addEventListener('DOMContentLoaded', function () {
|
|
48
|
+
var named = ['logo-image', 'logo-image-alternative'];
|
|
49
|
+
var found = document.querySelectorAll('[logo-image], [logo-image-alternative]');
|
|
50
|
+
for (var i = 0; i < found.length; i++) {
|
|
51
|
+
for (var j = 0; j < named.length; j++) {
|
|
52
|
+
var value = found[i].getAttribute(named[j]);
|
|
53
|
+
if (value && ${LOGO_PATTERN}.test(value)) found[i].setAttribute(named[j], value + '-dark');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
40
57
|
var href = alias ? ${sheets}[alias] : undefined;
|
|
41
58
|
if (!href) return;
|
|
42
59
|
var link = document.createElement('link');
|