@crombolo/backoffice-host 0.13.1 → 0.14.1
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/assets/login.js +89 -2
- package/package.json +2 -2
- package/plugin/assistant/umbraco-package.json +1 -1
- package/plugin/branding/branding.js +9 -14
- package/plugin/branding/bundle-builder-labels.js +30 -1
- package/plugin/branding/localization-en.js +8 -0
- package/plugin/branding/sign-in.js +31 -0
- package/plugin/branding/theme.css +2 -0
- package/plugin/bundles-created-overview.js +31 -18
- package/plugin/bundles-installed.js +127 -61
- package/plugin/bundles-marketplace.js +1 -1
- package/plugin/changes-dashboard.js +8 -1
- package/plugin/download.js +19 -0
- package/plugin/form-picker.js +1 -1
- package/plugin/forms-client.js +5 -0
- package/plugin/forms-entries.js +30 -16
- package/plugin/forms-overview.js +55 -33
- package/plugin/localizations/ar.js +3 -39
- package/plugin/localizations/bs.js +3 -29
- package/plugin/localizations/cs.js +3 -24
- package/plugin/localizations/cy.js +3 -32
- package/plugin/localizations/da.js +3 -41
- package/plugin/localizations/de.js +3 -27
- package/plugin/localizations/es.js +3 -19
- package/plugin/localizations/fr.js +3 -26
- package/plugin/localizations/hr.js +3 -29
- package/plugin/localizations/it.js +3 -29
- package/plugin/localizations/ja.js +3 -11
- package/plugin/localizations/ko.js +3 -11
- package/plugin/localizations/nb.js +3 -18
- package/plugin/localizations/nl.js +3 -29
- package/plugin/localizations/pl.js +3 -11
- package/plugin/localizations/pt-br.js +0 -10
- package/plugin/localizations/pt.js +3 -39
- package/plugin/localizations/ru.js +3 -9
- package/plugin/localizations/sv.js +3 -20
- package/plugin/localizations/tr.js +3 -26
- package/plugin/localizations/uk.js +3 -9
- package/plugin/localizations/umbraco-package.json +22 -2
- package/plugin/localizations/vi.js +3 -42
- package/plugin/localizations/zh-tw.js +0 -6
- package/plugin/localizations/zh.js +0 -6
- package/plugin/seo/umbraco-package.json +1 -1
- package/plugin/sign-in/entry.js +20 -0
- package/plugin/sign-in/sign-in-dialog.js +127 -0
- package/plugin/sign-in/umbraco-package.json +15 -0
- package/plugin/two-factor-setup.js +147 -0
- package/plugin/umbraco-package.json +30 -30
- package/plugin/welcome-dashboard.js +2 -5
- package/plugin/workflow-designer.js +298 -187
- package/plugin/workflow-requests.js +19 -6
- package/src/shell.ts +4 -1
- package/plugin/branding/sysinfo.js +0 -89
package/assets/login.js
CHANGED
|
@@ -39,6 +39,8 @@ class CromboloLoginElement extends LitElement {
|
|
|
39
39
|
logoImage: { type: String, attribute: 'logo-image-alternative' },
|
|
40
40
|
usernameIsEmail: { type: Boolean, attribute: 'username-is-email' },
|
|
41
41
|
allowPasswordReset: { type: Boolean, attribute: 'allow-password-reset' },
|
|
42
|
+
disableLocalLogin: { type: Boolean, attribute: 'disable-local-login' },
|
|
43
|
+
externalProviders: { type: Array, attribute: 'external-providers' },
|
|
42
44
|
_busy: { state: true },
|
|
43
45
|
_error: { state: true },
|
|
44
46
|
_view: { state: true },
|
|
@@ -51,6 +53,8 @@ class CromboloLoginElement extends LitElement {
|
|
|
51
53
|
this.logoImage = ''
|
|
52
54
|
this.usernameIsEmail = true
|
|
53
55
|
this.allowPasswordReset = false
|
|
56
|
+
this.disableLocalLogin = false
|
|
57
|
+
this.externalProviders = []
|
|
54
58
|
this._busy = false
|
|
55
59
|
this._error = ''
|
|
56
60
|
this._notice = ''
|
|
@@ -63,6 +67,9 @@ class CromboloLoginElement extends LitElement {
|
|
|
63
67
|
? { kind: 'reset', user: params.get('userId'), code: params.get('resetCode') }
|
|
64
68
|
: undefined
|
|
65
69
|
this._view = this._link ? 'checking' : 'login'
|
|
70
|
+
// An identity provider's sign-in that came back refused says why.
|
|
71
|
+
const external = params.get('externalLoginError')
|
|
72
|
+
if (external) this._error = external
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
connectedCallback() {
|
|
@@ -182,7 +189,9 @@ class CromboloLoginElement extends LitElement {
|
|
|
182
189
|
return
|
|
183
190
|
}
|
|
184
191
|
if (response.status === 402) {
|
|
185
|
-
|
|
192
|
+
const challenge = await response.json().catch(() => ({}))
|
|
193
|
+
this._twoFactorProvider = challenge.enabledTwoFactorProviderNames?.[0]
|
|
194
|
+
this._view = 'two-factor'
|
|
186
195
|
} else if (response.status === 403) {
|
|
187
196
|
this._error = 'This account is locked or not approved.'
|
|
188
197
|
} else if (response.status === 401) {
|
|
@@ -197,6 +206,57 @@ class CromboloLoginElement extends LitElement {
|
|
|
197
206
|
}
|
|
198
207
|
}
|
|
199
208
|
|
|
209
|
+
async #verifyTwoFactor(event) {
|
|
210
|
+
event.preventDefault()
|
|
211
|
+
if (this._busy) return
|
|
212
|
+
const code = event.target.elements.code.value.trim()
|
|
213
|
+
if (!code) return
|
|
214
|
+
this._busy = true
|
|
215
|
+
this._error = ''
|
|
216
|
+
try {
|
|
217
|
+
const response = await this.#post('/security/back-office/verify-2fa', {
|
|
218
|
+
provider: this._twoFactorProvider,
|
|
219
|
+
code,
|
|
220
|
+
})
|
|
221
|
+
if (response.ok) {
|
|
222
|
+
window.location.href = localTarget(this.returnUrl, '/umbraco')
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (response.status === 401) {
|
|
226
|
+
this._view = 'login'
|
|
227
|
+
this._error = 'That took too long. Sign in again.'
|
|
228
|
+
} else if (response.status === 403) {
|
|
229
|
+
this._view = 'login'
|
|
230
|
+
this._error = 'This account is locked or not approved.'
|
|
231
|
+
} else {
|
|
232
|
+
this._error = 'That code is not right. Try the next one.'
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
this._error = 'Could not reach the server.'
|
|
236
|
+
} finally {
|
|
237
|
+
this._busy = false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#twoFactorForm() {
|
|
242
|
+
return html`
|
|
243
|
+
<h1 class="uui-h3">Enter your code</h1>
|
|
244
|
+
<p>Enter the six-digit code from your authenticator app, or one of your recovery codes.</p>
|
|
245
|
+
<form @submit=${this.#verifyTwoFactor}>
|
|
246
|
+
<uui-form-layout-item>
|
|
247
|
+
<uui-label for="code" slot="label" required>Code</uui-label>
|
|
248
|
+
<uui-input id="code" name="code" autocomplete="one-time-code" required ?disabled=${this._busy}></uui-input>
|
|
249
|
+
</uui-form-layout-item>
|
|
250
|
+
${this.#messages()}
|
|
251
|
+
<uui-button type="submit" look="primary" color="positive" .state=${this._busy ? 'waiting' : undefined} label="Verify"></uui-button>
|
|
252
|
+
<uui-button look="link" label="Back to sign in" @click=${() => {
|
|
253
|
+
this._error = ''
|
|
254
|
+
this._view = 'login'
|
|
255
|
+
}}></uui-button>
|
|
256
|
+
</form>
|
|
257
|
+
`
|
|
258
|
+
}
|
|
259
|
+
|
|
200
260
|
#messages() {
|
|
201
261
|
return html`${this._notice ? html`<p id="notice" role="status">${this._notice}</p>` : nothing}
|
|
202
262
|
${this._error ? html`<p id="error" role="alert">${this._error}</p>` : nothing}`
|
|
@@ -245,7 +305,9 @@ class CromboloLoginElement extends LitElement {
|
|
|
245
305
|
? this.#newPasswordForm()
|
|
246
306
|
: this._view === 'forgot'
|
|
247
307
|
? this.#resetRequestForm()
|
|
248
|
-
: this
|
|
308
|
+
: this._view === 'two-factor'
|
|
309
|
+
? this.#twoFactorForm()
|
|
310
|
+
: this.#loginForm()
|
|
249
311
|
return html`
|
|
250
312
|
<div id="layout">
|
|
251
313
|
<uui-box id="card">
|
|
@@ -256,7 +318,25 @@ class CromboloLoginElement extends LitElement {
|
|
|
256
318
|
`
|
|
257
319
|
}
|
|
258
320
|
|
|
321
|
+
/** A button per backoffice identity provider, each returning here signed in. */
|
|
322
|
+
#providers() {
|
|
323
|
+
if (!this.externalProviders?.length) return nothing
|
|
324
|
+
return html`<div id="providers">
|
|
325
|
+
${this.externalProviders.map(
|
|
326
|
+
(provider) =>
|
|
327
|
+
html`<uui-button look="outline" href=${provider.url} label=${`Sign in with ${provider.name}`}>Sign in with ${provider.name}</uui-button>`,
|
|
328
|
+
)}
|
|
329
|
+
</div>`
|
|
330
|
+
}
|
|
331
|
+
|
|
259
332
|
#loginForm() {
|
|
333
|
+
// With local sign-in off, the providers are the only way in.
|
|
334
|
+
if (this.disableLocalLogin)
|
|
335
|
+
return html`
|
|
336
|
+
<h1 class="uui-h3">Sign in</h1>
|
|
337
|
+
${this.#messages()}
|
|
338
|
+
${this.#providers()}
|
|
339
|
+
`
|
|
260
340
|
return html`
|
|
261
341
|
<h1 class="uui-h3">Sign in</h1>
|
|
262
342
|
<form @submit=${this.#submit}>
|
|
@@ -301,6 +381,7 @@ class CromboloLoginElement extends LitElement {
|
|
|
301
381
|
: nothing
|
|
302
382
|
}
|
|
303
383
|
</form>
|
|
384
|
+
${this.#providers()}
|
|
304
385
|
`
|
|
305
386
|
}
|
|
306
387
|
|
|
@@ -342,6 +423,12 @@ class CromboloLoginElement extends LitElement {
|
|
|
342
423
|
margin: 0;
|
|
343
424
|
color: var(--uui-color-danger, #d42054);
|
|
344
425
|
}
|
|
426
|
+
#providers {
|
|
427
|
+
display: flex;
|
|
428
|
+
flex-direction: column;
|
|
429
|
+
gap: var(--uui-size-space-3, 9px);
|
|
430
|
+
margin-top: var(--uui-size-space-5, 18px);
|
|
431
|
+
}
|
|
345
432
|
#notice {
|
|
346
433
|
margin: 0;
|
|
347
434
|
color: var(--uui-color-positive, #0b8152);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/backoffice-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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.14.1",
|
|
35
35
|
"@crombolo/backoffice-dist": "18.2.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The two places the vendored client names the product in code rather than in a
|
|
3
|
-
* string table
|
|
3
|
+
* string table.
|
|
4
4
|
*
|
|
5
|
-
* `view.controller.js` builds the page title with a hardcoded
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* So it is corrected as it is written. The suffix is the site's own name, which
|
|
11
|
-
* the shell states in `<meta name="application-name">` — so a site called
|
|
12
|
-
* something other than Crombolo gets its own name rather than a second hardcoded
|
|
13
|
-
* one.
|
|
5
|
+
* `view.controller.js` builds the page title with a hardcoded suffix, which
|
|
6
|
+
* `vendor:backoffice` rebrands to Crombolo. The suffix should be the site's own
|
|
7
|
+
* name, which the shell states in `<meta name="application-name">` — so a site
|
|
8
|
+
* called something other than Crombolo gets its own name rather than ours.
|
|
14
9
|
*
|
|
15
10
|
* Not read from `document.title`, although the shell puts the name there too.
|
|
16
11
|
* This module loads after the client has started, and by then the client may
|
|
17
|
-
* have written a title of its own. Taking `Content |
|
|
18
|
-
* name makes every correction end in ` |
|
|
12
|
+
* have written a title of its own. Taking `Content | Crombolo` for the site's
|
|
13
|
+
* name makes every correction end in ` | Crombolo` again: the observer below
|
|
19
14
|
* then answers its own change, forever, and the page never gets its main thread
|
|
20
15
|
* back.
|
|
21
16
|
*/
|
|
22
|
-
const CLIENT_SUFFIX = ' |
|
|
17
|
+
const CLIENT_SUFFIX = ' | Crombolo'
|
|
23
18
|
|
|
24
19
|
/** The site's name as the shell stated it. */
|
|
25
20
|
export function brandName(doc = document) {
|
|
@@ -70,7 +65,7 @@ function correctLogoLink() {
|
|
|
70
65
|
if (!anchor) return false
|
|
71
66
|
anchor.href = 'https://github.com/kineco-au/crombolo-cms'
|
|
72
67
|
anchor.textContent = 'Crombolo on GitHub'
|
|
73
|
-
const image = logo.shadowRoot.querySelector('img
|
|
68
|
+
const image = logo.shadowRoot.querySelector('img#logo')
|
|
74
69
|
if (image) image.alt = brandName()
|
|
75
70
|
return true
|
|
76
71
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The two labels Umbraco's bundle builder hard-codes.
|
|
2
|
+
* The two labels and two toasts Umbraco's bundle builder hard-codes.
|
|
3
3
|
*
|
|
4
4
|
* The builder workspace is vendored and its copy is English in the template
|
|
5
5
|
* rather than dictionary keys, so there is no key to override: `Name of the
|
|
@@ -21,6 +21,16 @@ export const BUILDER_LABELS = [
|
|
|
21
21
|
['Package Content', 'Bundle content'],
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The builder's toasts on saving a definition. Shown through the notification
|
|
26
|
+
* context, so they are corrected where every toast passes rather than in the
|
|
27
|
+
* element, whose `peek` call is its own.
|
|
28
|
+
*/
|
|
29
|
+
export const BUILDER_TOASTS = [
|
|
30
|
+
['Package saved', 'Bundle saved'],
|
|
31
|
+
['Package updated', 'Bundle saved'],
|
|
32
|
+
]
|
|
33
|
+
|
|
24
34
|
/** The replacement for a vendored string, or undefined when it is not one of ours. */
|
|
25
35
|
export function correctionFor(text) {
|
|
26
36
|
if (typeof text !== 'string') return undefined
|
|
@@ -55,7 +65,26 @@ function patch(element) {
|
|
|
55
65
|
return true
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
// Loaded here rather than at the top, so the module's pure parts import
|
|
69
|
+
// anywhere — the tests read `BUILDER_LABELS` and `BUILDER_TOASTS` outside a browser.
|
|
70
|
+
async function correctToasts() {
|
|
71
|
+
const { UmbNotificationContext } = await import('@umbraco-cms/backoffice/notification')
|
|
72
|
+
const proto = UmbNotificationContext.prototype
|
|
73
|
+
const peek = proto.peek
|
|
74
|
+
if (typeof peek !== 'function') return
|
|
75
|
+
proto.peek = function correctedPeek(color, options) {
|
|
76
|
+
const message = options?.data?.message
|
|
77
|
+
const replacement = BUILDER_TOASTS.find(([before]) => before === message)?.[1]
|
|
78
|
+
return peek.call(
|
|
79
|
+
this,
|
|
80
|
+
color,
|
|
81
|
+
replacement ? { ...options, data: { ...options.data, message: replacement } } : options,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
export const onInit = () => {
|
|
87
|
+
correctToasts().catch(() => {})
|
|
59
88
|
customElements
|
|
60
89
|
.whenDefined('umb-workspace-package-builder')
|
|
61
90
|
.then(patch)
|
|
@@ -106,6 +106,14 @@ export default {
|
|
|
106
106
|
searchUmbracoIssues: 'Search Crombolo issues',
|
|
107
107
|
searchUmbracoIssuesOnGithub: 'Search Crombolo issues on GitHub',
|
|
108
108
|
},
|
|
109
|
+
redirectUrls: {
|
|
110
|
+
// Followed by `CROMBOLO_TRACK_REDIRECTS`, which `vendor:backoffice` puts in
|
|
111
|
+
// place of the .NET setting; it tracks when true, the inverse of Umbraco's.
|
|
112
|
+
disableUrlTrackerInstruction:
|
|
113
|
+
"Redirect URL tracking is set in the site's environment. To turn it off, set this variable to false:",
|
|
114
|
+
enableUrlTrackerInstruction:
|
|
115
|
+
"Redirect URL tracking is set in the site's environment. To turn it on, set this variable to true, or remove it:",
|
|
116
|
+
},
|
|
109
117
|
defaultdialogs: {
|
|
110
118
|
linkYourConfirm:
|
|
111
119
|
'You are about to link your Crombolo and {0} accounts and you will be redirected to {0} to complete the process.',
|
|
@@ -66,7 +66,38 @@ class CromboloAuthProviderElement extends LitElement {
|
|
|
66
66
|
|
|
67
67
|
if (!customElements.get(TAG)) customElements.define(TAG, CromboloAuthProviderElement)
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Signing in again after a timeout closes its modal twice.
|
|
71
|
+
*
|
|
72
|
+
* `umb-app-auth-modal` submits when the session reports itself authorised, and
|
|
73
|
+
* `umb-auth-view` calls the same submit as its `onSuccess` once
|
|
74
|
+
* `makeAuthorizationRequest` returns. Whichever runs first closes the modal and
|
|
75
|
+
* destroys its value state, so the second's `value = { success: true }` throws
|
|
76
|
+
* `_subject is undefined` — and the view shows that as a sign-in error, though
|
|
77
|
+
* the editor is signed in. The second assignment has nothing left to say, so it
|
|
78
|
+
* is dropped. Patched on the element's prototype, as `tsx-editors.js` patches
|
|
79
|
+
* upstream, rather than in the vendored bundle.
|
|
80
|
+
*/
|
|
81
|
+
async function guardTimedOutSignIn() {
|
|
82
|
+
const Modal = await customElements.whenDefined('umb-app-auth-modal')
|
|
83
|
+
let descriptor
|
|
84
|
+
for (let proto = Modal.prototype; proto && !descriptor; proto = Object.getPrototypeOf(proto))
|
|
85
|
+
descriptor = Object.getOwnPropertyDescriptor(proto, 'value')
|
|
86
|
+
if (!descriptor?.set) return
|
|
87
|
+
const { get, set } = descriptor
|
|
88
|
+
Object.defineProperty(Modal.prototype, 'value', {
|
|
89
|
+
configurable: true,
|
|
90
|
+
get,
|
|
91
|
+
set(value) {
|
|
92
|
+
if (this.__cromboloSubmitted) return
|
|
93
|
+
set.call(this, value)
|
|
94
|
+
this.__cromboloSubmitted = true
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
export const onInit = (_host, extensionRegistry) => {
|
|
100
|
+
void guardTimedOutSignIn()
|
|
70
101
|
extensionRegistry.unregister(VENDORED)
|
|
71
102
|
extensionRegistry.register({
|
|
72
103
|
type: 'authProvider',
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
--uui-color-header-surface: var(--crombolo-orange);
|
|
69
69
|
--uui-color-header-contrast: rgb(84 50 31 / 80%);
|
|
70
70
|
--uui-color-header-contrast-emphasis: var(--crombolo-cocoa);
|
|
71
|
+
/* The active section's marker, dark so it stands out from the white submenu below. */
|
|
72
|
+
--crombolo-header-current: var(--crombolo-orange-text);
|
|
71
73
|
|
|
72
74
|
/* Pure white panels on a warm off-white page, so cards still separate. */
|
|
73
75
|
--uui-color-background: var(--crombolo-cream);
|
|
@@ -101,7 +101,7 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
101
101
|
version 1.0.0.
|
|
102
102
|
</p>
|
|
103
103
|
<uui-input
|
|
104
|
-
label="
|
|
104
|
+
label="Name"
|
|
105
105
|
placeholder=${pkg?.name ?? ''}
|
|
106
106
|
.value=${this._draft.name}
|
|
107
107
|
@input=${(event) => (this._draft = { ...this._draft, name: event.target.value })}></uui-input>
|
|
@@ -166,38 +166,47 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
166
166
|
// look empty on every page change.
|
|
167
167
|
return html`
|
|
168
168
|
<uui-button
|
|
169
|
-
|
|
169
|
+
color="default"
|
|
170
|
+
look="outline"
|
|
170
171
|
href=${`${BUILDER_PATH}/create`}
|
|
171
|
-
label
|
|
172
|
+
label="Create"></uui-button>
|
|
172
173
|
${
|
|
173
174
|
this._loading
|
|
174
175
|
? html`<div id="loader"><uui-loader></uui-loader></div>`
|
|
175
176
|
: this._items.length === 0
|
|
176
177
|
? html`<h2 id="empty">${this.localize.term('packager_noPackagesCreated')}</h2>`
|
|
177
178
|
: html`
|
|
178
|
-
<uui-box headline="Created bundles"
|
|
179
|
-
<uui-
|
|
179
|
+
<uui-box headline="Created bundles">
|
|
180
|
+
<uui-table>
|
|
181
|
+
<uui-table-head>
|
|
182
|
+
<uui-table-head-cell>Name</uui-table-head-cell>
|
|
183
|
+
<uui-table-head-cell>Downloads as</uui-table-head-cell>
|
|
184
|
+
<uui-table-head-cell></uui-table-head-cell>
|
|
185
|
+
</uui-table-head>
|
|
180
186
|
${repeat(
|
|
181
187
|
this._items,
|
|
182
188
|
(item) => item.unique,
|
|
183
189
|
(item) => html`
|
|
184
|
-
<uui-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
<uui-table-row>
|
|
191
|
+
<uui-table-cell>${item.name}</uui-table-cell>
|
|
192
|
+
<uui-table-cell><code>${this.#detail(item)}</code></uui-table-cell>
|
|
193
|
+
<uui-table-cell class="buttons">
|
|
194
|
+
<uui-button look="secondary" label="Edit" @click=${() => this.#open(item)}></uui-button>
|
|
189
195
|
<uui-button
|
|
190
|
-
|
|
191
|
-
label="
|
|
196
|
+
look="secondary"
|
|
197
|
+
label="Name and version"
|
|
198
|
+
@click=${() => this.#edit(item)}></uui-button>
|
|
192
199
|
<uui-button
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
look="secondary"
|
|
201
|
+
color="danger"
|
|
202
|
+
label=${this.localize.term('general_delete')}
|
|
203
|
+
@click=${() => this.#remove(item)}></uui-button>
|
|
204
|
+
</uui-table-cell>
|
|
205
|
+
</uui-table-row>
|
|
198
206
|
`,
|
|
199
207
|
)}
|
|
200
|
-
</uui-
|
|
208
|
+
</uui-table>
|
|
209
|
+
${this._items.map((item) => this.#renderEditor(item))}
|
|
201
210
|
</uui-box>
|
|
202
211
|
`
|
|
203
212
|
}
|
|
@@ -238,6 +247,10 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
238
247
|
.problem {
|
|
239
248
|
color: var(--uui-color-danger);
|
|
240
249
|
}
|
|
250
|
+
.buttons {
|
|
251
|
+
white-space: nowrap;
|
|
252
|
+
text-align: right;
|
|
253
|
+
}
|
|
241
254
|
`
|
|
242
255
|
}
|
|
243
256
|
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* What it lists instead is the site's bundle dependencies, which is what
|
|
11
11
|
* "installed" means here: what each one adds, whether its content has been
|
|
12
12
|
* added to this site, and whether its server features are switched on.
|
|
13
|
+
*
|
|
14
|
+
* Two columns: the bundles on the left, filtered as you type; the one chosen
|
|
15
|
+
* on the right, with its details and the button that uninstalls it.
|
|
13
16
|
*/
|
|
14
17
|
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
15
18
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
@@ -41,6 +44,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
41
44
|
_busy: { state: true },
|
|
42
45
|
_notice: { state: true },
|
|
43
46
|
_installs: { state: true },
|
|
47
|
+
_filter: { state: true },
|
|
48
|
+
_selected: { state: true },
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
constructor() {
|
|
@@ -50,6 +55,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
50
55
|
this._busy = ''
|
|
51
56
|
this._notice = ''
|
|
52
57
|
this._installs = true
|
|
58
|
+
this._filter = ''
|
|
59
|
+
this._selected = ''
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
connectedCallback() {
|
|
@@ -63,6 +70,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
63
70
|
const result = await listInstalled()
|
|
64
71
|
this._items = result.items ?? []
|
|
65
72
|
this._installs = result.installs !== false
|
|
73
|
+
if (!this._items.some((item) => item.packageName === this._selected))
|
|
74
|
+
this._selected = this._items[0]?.packageName ?? ''
|
|
66
75
|
} catch (error) {
|
|
67
76
|
this._notice = error instanceof Error ? error.message : String(error)
|
|
68
77
|
} finally {
|
|
@@ -83,72 +92,98 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
#pick(item) {
|
|
96
|
+
this._selected = item.packageName
|
|
97
|
+
this.requestUpdate()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get #shown() {
|
|
101
|
+
const words = this._filter.trim().toLowerCase()
|
|
102
|
+
if (!words) return this._items
|
|
103
|
+
return this._items.filter((item) =>
|
|
104
|
+
[item.name, item.packageName, item.description].some((text) =>
|
|
105
|
+
text?.toLowerCase().includes(words),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
render() {
|
|
87
111
|
if (this._loading) return html`<div id="loader"><uui-loader></uui-loader></div>`
|
|
88
|
-
|
|
112
|
+
if (this._items.length === 0)
|
|
113
|
+
return html`
|
|
114
|
+
${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
|
|
115
|
+
<uui-box headline="No bundles installed">
|
|
116
|
+
<p>Install one from the Marketplace tab, and it appears here.</p>
|
|
117
|
+
</uui-box>
|
|
118
|
+
`
|
|
119
|
+
const selected = this._items.find((item) => item.packageName === this._selected)
|
|
120
|
+
const shown = this.#shown
|
|
89
121
|
return html`
|
|
90
122
|
${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
|
|
123
|
+
<div id="columns">
|
|
124
|
+
<uui-box headline="Installed bundles" style="--uui-box-default-padding:0;">
|
|
125
|
+
<div id="filter">
|
|
126
|
+
<uui-input
|
|
127
|
+
label="Filter"
|
|
128
|
+
placeholder="Filter"
|
|
129
|
+
.value=${this._filter}
|
|
130
|
+
@input=${(event) => {
|
|
131
|
+
this._filter = event.target.value
|
|
132
|
+
}}>
|
|
133
|
+
<uui-icon name="icon-search" slot="prepend"></uui-icon>
|
|
134
|
+
</uui-input>
|
|
135
|
+
</div>
|
|
136
|
+
${
|
|
137
|
+
shown.length === 0
|
|
138
|
+
? html`<p class="empty">No bundle matches.</p>`
|
|
139
|
+
: html`<uui-ref-list>
|
|
140
|
+
${repeat(
|
|
141
|
+
shown,
|
|
142
|
+
(item) => item.packageName,
|
|
143
|
+
(item) => html`<uui-ref-node
|
|
144
|
+
name=${item.name}
|
|
145
|
+
detail=${`${item.packageName} ${item.version}`}
|
|
146
|
+
selectable
|
|
147
|
+
select-only
|
|
148
|
+
?selected=${item.packageName === this._selected}
|
|
149
|
+
@selected=${() => this.#pick(item)}
|
|
150
|
+
@deselected=${() => this.#pick(item)}
|
|
151
|
+
@open=${() => this.#pick(item)}></uui-ref-node>`,
|
|
152
|
+
)}
|
|
153
|
+
</uui-ref-list>`
|
|
154
|
+
}
|
|
155
|
+
</uui-box>
|
|
156
|
+
${selected ? this.#details(selected) : nothing}
|
|
157
|
+
</div>
|
|
158
|
+
`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#details(item) {
|
|
162
|
+
return html`<uui-box headline=${item.name}>
|
|
163
|
+
${item.description ? html`<p>${item.description}</p>` : nothing}
|
|
164
|
+
<dl>
|
|
165
|
+
<dt>npm package</dt>
|
|
166
|
+
<dd><code>${item.packageName}</code></dd>
|
|
167
|
+
<dt>Version</dt>
|
|
168
|
+
<dd>${item.version}</dd>
|
|
169
|
+
<dt>Adds</dt>
|
|
170
|
+
<dd class="carries">${carriesTags(item.carries)}</dd>
|
|
171
|
+
<dt>On this site</dt>
|
|
172
|
+
<dd class="status">${status(item)}</dd>
|
|
173
|
+
<dt>Works with</dt>
|
|
174
|
+
<dd>Crombolo ${item.peerRange}</dd>
|
|
175
|
+
</dl>
|
|
91
176
|
${
|
|
92
|
-
this.
|
|
93
|
-
? html
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</uui-box>
|
|
101
|
-
`
|
|
102
|
-
: html`
|
|
103
|
-
<uui-box headline="Installed bundles">
|
|
104
|
-
<uui-table>
|
|
105
|
-
<uui-table-head>
|
|
106
|
-
<uui-table-head-cell>Name</uui-table-head-cell>
|
|
107
|
-
<uui-table-head-cell>npm package</uui-table-head-cell>
|
|
108
|
-
<uui-table-head-cell>Version</uui-table-head-cell>
|
|
109
|
-
<uui-table-head-cell>Adds</uui-table-head-cell>
|
|
110
|
-
<uui-table-head-cell>On this site</uui-table-head-cell>
|
|
111
|
-
<uui-table-head-cell></uui-table-head-cell>
|
|
112
|
-
</uui-table-head>
|
|
113
|
-
${repeat(
|
|
114
|
-
this._items,
|
|
115
|
-
(item) => item.packageName,
|
|
116
|
-
(item) => html`
|
|
117
|
-
<uui-table-row>
|
|
118
|
-
<uui-table-cell>${item.name}</uui-table-cell>
|
|
119
|
-
<uui-table-cell><code>${item.packageName}</code></uui-table-cell>
|
|
120
|
-
<uui-table-cell>${item.version}</uui-table-cell>
|
|
121
|
-
<uui-table-cell class="carries">${carriesTags(item.carries)}</uui-table-cell>
|
|
122
|
-
<uui-table-cell class="status">${status(item)}</uui-table-cell>
|
|
123
|
-
<uui-table-cell>
|
|
124
|
-
${
|
|
125
|
-
this._installs
|
|
126
|
-
? html`<uui-button
|
|
127
|
-
look="secondary"
|
|
128
|
-
color="danger"
|
|
129
|
-
label="Uninstall"
|
|
130
|
-
.state=${this._busy === item.packageName ? 'waiting' : undefined}
|
|
131
|
-
@click=${() => this.#uninstall(item)}></uui-button>`
|
|
132
|
-
: nothing
|
|
133
|
-
}
|
|
134
|
-
</uui-table-cell>
|
|
135
|
-
</uui-table-row>
|
|
136
|
-
`,
|
|
137
|
-
)}
|
|
138
|
-
</uui-table>
|
|
139
|
-
<p class="note">
|
|
140
|
-
${
|
|
141
|
-
this._installs
|
|
142
|
-
? html`An install writes this site's <code>package.json</code> and
|
|
143
|
-
<code>bun.lock</code>. Commit both to keep it across deployments.`
|
|
144
|
-
: html`Packages are added and removed by changing this site's
|
|
145
|
-
<code>package.json</code> and deploying, so every server has the same ones.`
|
|
146
|
-
}
|
|
147
|
-
</p>
|
|
148
|
-
</uui-box>
|
|
149
|
-
`
|
|
177
|
+
this._installs
|
|
178
|
+
? html`<uui-button
|
|
179
|
+
look="primary"
|
|
180
|
+
color="danger"
|
|
181
|
+
label="Uninstall"
|
|
182
|
+
.state=${this._busy === item.packageName ? 'waiting' : undefined}
|
|
183
|
+
@click=${() => this.#uninstall(item)}></uui-button>`
|
|
184
|
+
: html`<p class="note">This site adds and removes bundles with a deploy, so every server has the same ones.</p>`
|
|
150
185
|
}
|
|
151
|
-
|
|
186
|
+
</uui-box>`
|
|
152
187
|
}
|
|
153
188
|
|
|
154
189
|
static styles = css`
|
|
@@ -167,10 +202,42 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
167
202
|
border-left: 3px solid var(--uui-color-positive, #2bc37c);
|
|
168
203
|
background: var(--uui-color-surface-alt, #f3f3f5);
|
|
169
204
|
}
|
|
205
|
+
#columns {
|
|
206
|
+
display: grid;
|
|
207
|
+
grid-template-columns: minmax(240px, 1fr) 2fr;
|
|
208
|
+
gap: var(--uui-size-layout-1, 24px);
|
|
209
|
+
align-items: start;
|
|
210
|
+
}
|
|
211
|
+
@media (max-width: 800px) {
|
|
212
|
+
#columns {
|
|
213
|
+
grid-template-columns: 1fr;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
#filter {
|
|
217
|
+
padding: var(--uui-size-space-4, 12px);
|
|
218
|
+
}
|
|
219
|
+
#filter uui-input {
|
|
220
|
+
width: 100%;
|
|
221
|
+
}
|
|
222
|
+
.empty {
|
|
223
|
+
padding: 0 var(--uui-size-space-4, 12px) var(--uui-size-space-4, 12px);
|
|
224
|
+
}
|
|
170
225
|
p {
|
|
171
226
|
max-width: 60ch;
|
|
172
227
|
line-height: 1.5;
|
|
173
228
|
}
|
|
229
|
+
dl {
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-columns: max-content 1fr;
|
|
232
|
+
gap: var(--uui-size-space-3, 9px) var(--uui-size-space-5, 18px);
|
|
233
|
+
margin: 0 0 var(--uui-size-space-5, 18px);
|
|
234
|
+
}
|
|
235
|
+
dt {
|
|
236
|
+
font-weight: 700;
|
|
237
|
+
}
|
|
238
|
+
dd {
|
|
239
|
+
margin: 0;
|
|
240
|
+
}
|
|
174
241
|
.carries {
|
|
175
242
|
display: flex;
|
|
176
243
|
gap: var(--uui-size-space-2, 6px);
|
|
@@ -179,7 +246,6 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
179
246
|
display: block;
|
|
180
247
|
}
|
|
181
248
|
.note {
|
|
182
|
-
margin-top: var(--uui-size-space-5, 18px);
|
|
183
249
|
opacity: 0.8;
|
|
184
250
|
font-size: 0.9em;
|
|
185
251
|
}
|