@crombolo/backoffice-host 0.0.0-stage → 0.9.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/LICENSE +21 -0
- package/NOTICE +69 -0
- package/assets/login.js +352 -0
- package/package.json +35 -4
- package/plugin/assistant/client.js +53 -0
- package/plugin/assistant/drawer.js +865 -0
- package/plugin/assistant/umbraco-package.json +16 -0
- package/plugin/branding/backdrop.svg +1 -0
- package/plugin/branding/branding.js +100 -0
- package/plugin/branding/bundle-builder-labels.js +63 -0
- package/plugin/branding/client-credentials.js +49 -0
- package/plugin/branding/components-tree.js +40 -0
- package/plugin/branding/content-type-labels.js +27 -0
- package/plugin/branding/favicon.svg +1 -0
- package/plugin/branding/help-menu.js +46 -0
- package/plugin/branding/icon-mark.js +15 -0
- package/plugin/branding/icon-registry.js +40 -0
- package/plugin/branding/localization-en.js +115 -0
- package/plugin/branding/log-viewer-menu.js +70 -0
- package/plugin/branding/logo-header.svg +1 -0
- package/plugin/branding/logo-surface.svg +1 -0
- package/plugin/branding/query-builder.js +28 -0
- package/plugin/branding/sign-in.js +80 -0
- package/plugin/branding/sysinfo.js +89 -0
- package/plugin/branding/theme.css +102 -0
- package/plugin/bundles-client.js +34 -0
- package/plugin/bundles-created-overview.js +150 -0
- package/plugin/bundles-created.js +70 -0
- package/plugin/bundles-installed.js +147 -0
- package/plugin/bundles-marketplace.js +199 -0
- package/plugin/changes-dashboard.js +132 -0
- package/plugin/element-type-hint.js +58 -0
- package/plugin/form-picker.js +116 -0
- package/plugin/forms-client.js +52 -0
- package/plugin/forms-entries.js +244 -0
- package/plugin/forms-overview.js +380 -0
- package/plugin/localizations/ar.js +97 -0
- package/plugin/localizations/bs.js +86 -0
- package/plugin/localizations/cs.js +73 -0
- package/plugin/localizations/cy.js +92 -0
- package/plugin/localizations/da.js +101 -0
- package/plugin/localizations/de.js +79 -0
- package/plugin/localizations/en.js +71 -0
- package/plugin/localizations/es.js +52 -0
- package/plugin/localizations/fr.js +78 -0
- package/plugin/localizations/he.js +24 -0
- package/plugin/localizations/hr.js +89 -0
- package/plugin/localizations/it.js +86 -0
- package/plugin/localizations/ja.js +43 -0
- package/plugin/localizations/ko.js +24 -0
- package/plugin/localizations/nb.js +44 -0
- package/plugin/localizations/nl.js +85 -0
- package/plugin/localizations/pl.js +44 -0
- package/plugin/localizations/pt-br.js +25 -0
- package/plugin/localizations/pt.js +100 -0
- package/plugin/localizations/ro.js +9 -0
- package/plugin/localizations/ru.js +46 -0
- package/plugin/localizations/sv.js +46 -0
- package/plugin/localizations/tr.js +80 -0
- package/plugin/localizations/uk.js +46 -0
- package/plugin/localizations/umbraco-package.json +279 -0
- package/plugin/localizations/vi.js +104 -0
- package/plugin/localizations/zh-tw.js +38 -0
- package/plugin/localizations/zh.js +38 -0
- package/plugin/read-only-banner.js +48 -0
- package/plugin/report-client.js +13 -0
- package/plugin/seo/client.js +35 -0
- package/plugin/seo/dashboard.js +242 -0
- package/plugin/seo/document-view.js +161 -0
- package/plugin/seo/type-hint.js +45 -0
- package/plugin/seo/umbraco-package.json +61 -0
- package/plugin/settings-dashboard.js +123 -0
- package/plugin/tsx-editors.js +337 -0
- package/plugin/tsx-snippets.js +63 -0
- package/plugin/umbraco-package.json +298 -0
- package/plugin/welcome-dashboard.js +106 -0
- package/src/branding.ts +59 -0
- package/src/extensions.ts +228 -0
- package/src/graphics.ts +87 -0
- package/src/index.ts +7 -0
- package/src/login-assets.ts +18 -0
- package/src/manifests.ts +154 -0
- package/src/paths.ts +117 -0
- package/src/shell.ts +139 -0
- package/src/static.ts +107 -0
- package/src/text-imports.d.ts +23 -0
- package/README.md +0 -3
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The template editor's query builder writes TypeScript, so it should not say
|
|
3
|
+
* it wrote C#.
|
|
4
|
+
*
|
|
5
|
+
* The modal hard-codes `language="C#"` on the code block it previews the
|
|
6
|
+
* snippet in (`query-builder-modal.element.js`). The element is vendored, so
|
|
7
|
+
* the label is corrected after each render rather than by replacing the modal:
|
|
8
|
+
* the rest of it — the pickers, the filters, the sample results — is exactly
|
|
9
|
+
* what we want, and a fork of it would be a fork to maintain.
|
|
10
|
+
*/
|
|
11
|
+
const TAG = 'umb-template-query-builder-modal'
|
|
12
|
+
const LANGUAGE = 'TypeScript'
|
|
13
|
+
|
|
14
|
+
export const onInit = async () => {
|
|
15
|
+
// The modal is registered lazily, so the class may not be defined until the
|
|
16
|
+
// editor opens it.
|
|
17
|
+
const element = await customElements.whenDefined(TAG)
|
|
18
|
+
const prototype = element.prototype
|
|
19
|
+
const updated = prototype.updated
|
|
20
|
+
prototype.updated = function patched(changed) {
|
|
21
|
+
updated?.call(this, changed)
|
|
22
|
+
const block = this.shadowRoot?.querySelector('umb-code-block')
|
|
23
|
+
if (block && block.getAttribute('language') !== LANGUAGE) {
|
|
24
|
+
block.setAttribute('language', LANGUAGE)
|
|
25
|
+
block.language = LANGUAGE
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The button on the client's own sign-in screen.
|
|
3
|
+
*
|
|
4
|
+
* `umb-auth-view` draws one button per registered `authProvider`, and the
|
|
5
|
+
* vendored manifest contributes `Umb.AuthProviders.Umbraco` — label `Umbraco`,
|
|
6
|
+
* icon `icon-umbraco` — which the default button renders through
|
|
7
|
+
* `login_signInWith` as "Sign in with Umbraco" under the Umbraco mark. There is
|
|
8
|
+
* one way into this backoffice, so the button names no product at all.
|
|
9
|
+
*
|
|
10
|
+
* **Re-registered rather than replaced through `overwrites`.** The registry
|
|
11
|
+
* resolves `overwrites` where an extension is *rendered*, not in `byType`, and
|
|
12
|
+
* `app-auth.controller.js` reads `byType('authProvider')` to decide whether to
|
|
13
|
+
* draw this screen at all: with exactly one provider it skips it and sends the
|
|
14
|
+
* browser straight to the login page. An `overwrites` manifest would leave two
|
|
15
|
+
* providers in that list and so introduce a screen that signing in does not
|
|
16
|
+
* currently show.
|
|
17
|
+
*
|
|
18
|
+
* That same count is why the screen below is reached by a session timing out
|
|
19
|
+
* rather than by signing in: a logged-out visitor is redirected before it
|
|
20
|
+
* renders.
|
|
21
|
+
*
|
|
22
|
+
* `forProviderName` stays `Umbraco`. It is the identity provider the server's
|
|
23
|
+
* authorize endpoint knows, and the view hides a provider by that name when
|
|
24
|
+
* local login is disabled (`umb-auth-view.element.js`). Nothing displays it.
|
|
25
|
+
*/
|
|
26
|
+
import { css, html, LitElement } from '@umbraco-cms/backoffice/external/lit'
|
|
27
|
+
import '@umbraco-cms/backoffice/external/uui'
|
|
28
|
+
|
|
29
|
+
const TAG = 'crombolo-auth-provider'
|
|
30
|
+
const VENDORED = 'Umb.AuthProviders.Umbraco'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* English, like the login page's own buttons (`assets/login.js`). The two
|
|
34
|
+
* screens are the same step and should use the same word for it, which matters
|
|
35
|
+
* more here than half-translating one of them.
|
|
36
|
+
*/
|
|
37
|
+
const LABEL = 'Sign in'
|
|
38
|
+
|
|
39
|
+
class CromboloAuthProviderElement extends LitElement {
|
|
40
|
+
static properties = {
|
|
41
|
+
manifest: { attribute: false },
|
|
42
|
+
userLoginState: { attribute: false },
|
|
43
|
+
onSubmit: { attribute: false },
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
render() {
|
|
47
|
+
return html`<uui-button
|
|
48
|
+
type="button"
|
|
49
|
+
look="primary"
|
|
50
|
+
color="positive"
|
|
51
|
+
label=${LABEL}
|
|
52
|
+
@click=${() => this.onSubmit?.(this.manifest)}>
|
|
53
|
+
${LABEL}
|
|
54
|
+
</uui-button>`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static styles = css`
|
|
58
|
+
:host {
|
|
59
|
+
display: block;
|
|
60
|
+
}
|
|
61
|
+
uui-button {
|
|
62
|
+
width: 100%;
|
|
63
|
+
}
|
|
64
|
+
`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!customElements.get(TAG)) customElements.define(TAG, CromboloAuthProviderElement)
|
|
68
|
+
|
|
69
|
+
export const onInit = (_host, extensionRegistry) => {
|
|
70
|
+
extensionRegistry.unregister(VENDORED)
|
|
71
|
+
extensionRegistry.register({
|
|
72
|
+
type: 'authProvider',
|
|
73
|
+
alias: 'Crombolo.AuthProvider.SignIn',
|
|
74
|
+
name: 'Crombolo Sign-in Provider',
|
|
75
|
+
forProviderName: 'Umbraco',
|
|
76
|
+
elementName: TAG,
|
|
77
|
+
weight: 1000,
|
|
78
|
+
meta: { label: LABEL },
|
|
79
|
+
})
|
|
80
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The System information modal, reached from the header logo popover.
|
|
3
|
+
*
|
|
4
|
+
* It names the product in four places that no localisation key covers: three
|
|
5
|
+
* row labels built as plain strings in `sysinfo.element.js`, and the heading it
|
|
6
|
+
* puts above the text it copies to the clipboard.
|
|
7
|
+
*
|
|
8
|
+
* The rows are corrected at their source rather than in the DOM. The element
|
|
9
|
+
* assembles every row into one string and assigns it to `_systemInformation`, a
|
|
10
|
+
* lit reactive property, so wrapping that property's setter corrects the rows
|
|
11
|
+
* before they are ever rendered — no observer, and no reaching into a shadow
|
|
12
|
+
* root. The clipboard heading is prepended inside a private method that nothing
|
|
13
|
+
* can reach, so that one is taken on the way out to the clipboard instead.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The exact phrases to correct. Spelled out rather than replacing the product
|
|
18
|
+
* name wherever it appears, because the same text carries the browser's user
|
|
19
|
+
* agent and the current URL — a blanket replacement would rewrite whatever a
|
|
20
|
+
* site happens to have in its address.
|
|
21
|
+
*/
|
|
22
|
+
const PHRASES = [
|
|
23
|
+
'Umbraco build version',
|
|
24
|
+
'Umbraco assembly version',
|
|
25
|
+
'Umbraco client version',
|
|
26
|
+
'Umbraco system information',
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
/** A fixed point: its own output contains none of the phrases, so re-applying it changes nothing. */
|
|
30
|
+
export function rebrandSysinfo(text) {
|
|
31
|
+
if (typeof text !== 'string') return text
|
|
32
|
+
let corrected = text
|
|
33
|
+
for (const phrase of PHRASES) {
|
|
34
|
+
corrected = corrected.replaceAll(phrase, phrase.replace('Umbraco', 'Crombolo'))
|
|
35
|
+
}
|
|
36
|
+
return corrected
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** True when the string is the modal's clipboard payload rather than someone's own copy. */
|
|
40
|
+
export function isSysinfoClipboardText(text) {
|
|
41
|
+
return typeof text === 'string' && text.includes('Umbraco system information')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wraps the reactive property's setter. Does nothing at all if the accessor is
|
|
46
|
+
* not where lit puts it, so an upstream change costs the correction and never
|
|
47
|
+
* an error.
|
|
48
|
+
*/
|
|
49
|
+
function patchRows(element) {
|
|
50
|
+
const accessor = Object.getOwnPropertyDescriptor(element?.prototype ?? {}, '_systemInformation')
|
|
51
|
+
if (!accessor?.get || !accessor?.set) return false
|
|
52
|
+
Object.defineProperty(element.prototype, '_systemInformation', {
|
|
53
|
+
...accessor,
|
|
54
|
+
get() {
|
|
55
|
+
return accessor.get.call(this)
|
|
56
|
+
},
|
|
57
|
+
set(value) {
|
|
58
|
+
accessor.set.call(this, rebrandSysinfo(value))
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Corrects the heading on its way to the clipboard. Installed only once the
|
|
66
|
+
* modal's element exists — so a session that never opens System information
|
|
67
|
+
* never patches anything — and it passes every other string through untouched.
|
|
68
|
+
*/
|
|
69
|
+
function patchClipboard() {
|
|
70
|
+
const clipboard = navigator.clipboard
|
|
71
|
+
if (typeof clipboard?.writeText !== 'function') return
|
|
72
|
+
const write = clipboard.writeText.bind(clipboard)
|
|
73
|
+
try {
|
|
74
|
+
clipboard.writeText = (text) =>
|
|
75
|
+
write(isSysinfoClipboardText(text) ? rebrandSysinfo(text) : text)
|
|
76
|
+
} catch {
|
|
77
|
+
// A read-only `writeText` leaves the copied heading stale, which is the
|
|
78
|
+
// one thing here worth less than a working copy button.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const onInit = () => {
|
|
83
|
+
customElements
|
|
84
|
+
.whenDefined('umb-sysinfo')
|
|
85
|
+
.then((element) => {
|
|
86
|
+
if (patchRows(element)) patchClipboard()
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {})
|
|
89
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Crombolo's colour scheme: white surfaces, terracotta accents taken from the logo.
|
|
3
|
+
*
|
|
4
|
+
* Loaded by both shells immediately after the vendored `light.css`, rather than
|
|
5
|
+
* registered as a `theme` extension, so it reaches the login page too — the
|
|
6
|
+
* login shell never boots the extension registry.
|
|
7
|
+
*
|
|
8
|
+
* The Dark and High contrast themes are unaffected: the theme context appends
|
|
9
|
+
* their stylesheet to `<head>` *after* this one, and both redefine every token
|
|
10
|
+
* set below, so picking one still gets a coherent palette rather than a mix.
|
|
11
|
+
*/
|
|
12
|
+
:root {
|
|
13
|
+
--crombolo-orange: #da987c;
|
|
14
|
+
/* Lighter: a hover fill, never text. */
|
|
15
|
+
--crombolo-orange-light: #e2ae98;
|
|
16
|
+
/* Deep enough for a focus ring against white. */
|
|
17
|
+
--crombolo-orange-deep: #cb6f48;
|
|
18
|
+
/* The darkest of the three, and the only one that clears AA as body text. */
|
|
19
|
+
--crombolo-orange-text: #974b2b;
|
|
20
|
+
--crombolo-cocoa: #54321f;
|
|
21
|
+
--crombolo-cream: #fff7ea;
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* `light.css` routes default, selected and interactive-emphasis through the
|
|
25
|
+
* violet-blue palette, so swapping the palette carries all of them at once.
|
|
26
|
+
*/
|
|
27
|
+
--uui-palette-violet-blue: var(--crombolo-orange);
|
|
28
|
+
--uui-palette-violet-blue-light: var(--crombolo-orange-light);
|
|
29
|
+
--uui-palette-violet-blue-dark: var(--crombolo-orange-text);
|
|
30
|
+
--uui-palette-violet-blue-dimmed: #bd8b75;
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Dark ink rather than orange. This palette is where the client's text and
|
|
34
|
+
* icon colours come from (`--uui-color-interactive`), so an orange here would
|
|
35
|
+
* tint the whole interface and fail contrast on white.
|
|
36
|
+
*/
|
|
37
|
+
--uui-palette-space-cadet: var(--crombolo-cocoa);
|
|
38
|
+
--uui-palette-space-cadet-light: #704329;
|
|
39
|
+
--uui-palette-space-cadet-dark: #25160e;
|
|
40
|
+
--uui-palette-space-cadet-dimmed: #8a6c5c;
|
|
41
|
+
|
|
42
|
+
/* The "current item" marker, warmed from pink to the same orange family. */
|
|
43
|
+
--uui-palette-spanish-pink: #fdebd4;
|
|
44
|
+
--uui-palette-spanish-pink-light: #fef4e7;
|
|
45
|
+
--uui-palette-spanish-pink-dark: #ebc7b8;
|
|
46
|
+
--uui-palette-spanish-pink-dimmed: #f3e3ce;
|
|
47
|
+
|
|
48
|
+
/* Backs the app background, the alt surface and the disabled fill. */
|
|
49
|
+
--uui-palette-sand: var(--crombolo-cream);
|
|
50
|
+
--uui-palette-sand-light: #fff;
|
|
51
|
+
--uui-palette-sand-dark: #f4e9d7;
|
|
52
|
+
--uui-palette-sand-dimmed: #f7efe3;
|
|
53
|
+
|
|
54
|
+
--uui-palette-white: #fffefc;
|
|
55
|
+
--uui-palette-white-light: #fff;
|
|
56
|
+
--uui-palette-white-dark: #fffaf2;
|
|
57
|
+
--uui-palette-white-dimmed: #f4ede1;
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* The accent is light, so what sits on it has to be dark. Stock `light.css`
|
|
61
|
+
* puts white on the accent, which against this orange is about 2.4:1; cocoa
|
|
62
|
+
* on the same fill is 4.7:1.
|
|
63
|
+
*/
|
|
64
|
+
--uui-color-default-contrast: var(--crombolo-cocoa);
|
|
65
|
+
--uui-color-selected-contrast: var(--crombolo-cocoa);
|
|
66
|
+
|
|
67
|
+
/* An orange header bar, with its own text dark for the same reason. */
|
|
68
|
+
--uui-color-header-surface: var(--crombolo-orange);
|
|
69
|
+
--uui-color-header-contrast: rgb(84 50 31 / 80%);
|
|
70
|
+
--uui-color-header-contrast-emphasis: var(--crombolo-cocoa);
|
|
71
|
+
|
|
72
|
+
/* Pure white panels on a warm off-white page, so cards still separate. */
|
|
73
|
+
--uui-color-background: var(--crombolo-cream);
|
|
74
|
+
--uui-color-surface: #fff;
|
|
75
|
+
--uui-color-surface-alt: var(--crombolo-cream);
|
|
76
|
+
--uui-color-surface-emphasis: #fffaf2;
|
|
77
|
+
|
|
78
|
+
--uui-color-focus: var(--crombolo-orange-deep);
|
|
79
|
+
|
|
80
|
+
/* Hover text on an unfilled interactive element: the palette's light orange
|
|
81
|
+
would be 2:1 here, so it takes the text-weight orange instead. */
|
|
82
|
+
--uui-color-interactive-emphasis: var(--crombolo-orange-text);
|
|
83
|
+
|
|
84
|
+
--uui-color-border: #e7dcd2;
|
|
85
|
+
--uui-color-border-standalone: #cdbcac;
|
|
86
|
+
--uui-color-border-emphasis: #ab9684;
|
|
87
|
+
--uui-color-divider: #f8f2ec;
|
|
88
|
+
--uui-color-divider-standalone: #efe5db;
|
|
89
|
+
--uui-color-divider-emphasis: #ddcdbe;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* The two pink strokes `umb-auth-view` draws across its illustration panel
|
|
94
|
+
* (`#curve-top`, `#curve-bottom`). They are Umbraco's own flourish and carry no
|
|
95
|
+
* meaning, so the panel is left to the backdrop alone. Both of these are the
|
|
96
|
+
* element's own hooks, read off `:host` and so inherited from here — which is
|
|
97
|
+
* the only way to reach them, the panel being inside a shadow root.
|
|
98
|
+
*/
|
|
99
|
+
:root {
|
|
100
|
+
--umb-login-curves-display: none;
|
|
101
|
+
--umb-login-curves-color: transparent;
|
|
102
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and drives the Bundles endpoints the server keeps at
|
|
3
|
+
* <backoffice>/crombolo/api/bundles/*. The session cookie carries the auth.
|
|
4
|
+
*/
|
|
5
|
+
const base = () => {
|
|
6
|
+
const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
7
|
+
return `${href.replace(/\/$/, '')}/crombolo/api/bundles`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const json = async (path, init) => {
|
|
11
|
+
const response = await fetch(`${base()}${path}`, {
|
|
12
|
+
credentials: 'include',
|
|
13
|
+
headers: {
|
|
14
|
+
accept: 'application/json',
|
|
15
|
+
...(init?.body ? { 'content-type': 'application/json' } : {}),
|
|
16
|
+
},
|
|
17
|
+
...init,
|
|
18
|
+
})
|
|
19
|
+
const body = await response.json().catch(() => ({}))
|
|
20
|
+
if (!response.ok && response.status !== 400)
|
|
21
|
+
throw new Error(`bundles responded ${response.status}`)
|
|
22
|
+
return body
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const searchMarketplace = (query) =>
|
|
26
|
+
json(`/marketplace${query ? `?q=${encodeURIComponent(query)}` : ''}`)
|
|
27
|
+
|
|
28
|
+
export const listInstalled = () => json('/installed')
|
|
29
|
+
|
|
30
|
+
export const installBundle = (name, version) =>
|
|
31
|
+
json('/install', { method: 'POST', body: JSON.stringify({ name, version }) })
|
|
32
|
+
|
|
33
|
+
export const uninstallBundle = (name) =>
|
|
34
|
+
json('/uninstall', { method: 'POST', body: JSON.stringify({ name }) })
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The list of bundles that have been defined, inside the Created view.
|
|
3
|
+
*
|
|
4
|
+
* Reads through `UmbPackageRepository` rather than this plugin's own endpoints:
|
|
5
|
+
* the definitions are the Management API's nine `package` operations, which are
|
|
6
|
+
* contract-fixed and bearer-authenticated, and the repository already carries
|
|
7
|
+
* the client's token. The plugin's `/crombolo/api/bundles` routes are a
|
|
8
|
+
* different thing — installed npm bundles, on the session cookie.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
12
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
13
|
+
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'
|
|
14
|
+
import { UmbPackageRepository } from '@umbraco-cms/backoffice/package'
|
|
15
|
+
|
|
16
|
+
const TAKE = 10
|
|
17
|
+
|
|
18
|
+
/** Where the builder lives, which is Umbraco's route and not ours to rename. */
|
|
19
|
+
export const BUILDER_PATH = 'section/packages/view/created/package-builder'
|
|
20
|
+
|
|
21
|
+
export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement {
|
|
22
|
+
static properties = {
|
|
23
|
+
_items: { state: true },
|
|
24
|
+
_loading: { state: true },
|
|
25
|
+
_total: { state: true },
|
|
26
|
+
_page: { state: true },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#repository = new UmbPackageRepository(this)
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
super()
|
|
33
|
+
this._items = []
|
|
34
|
+
this._loading = true
|
|
35
|
+
this._total = 0
|
|
36
|
+
this._page = 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
super.connectedCallback()
|
|
41
|
+
this.#load()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async #load() {
|
|
45
|
+
this._loading = true
|
|
46
|
+
const skip = this._page * TAKE - TAKE
|
|
47
|
+
const data = await this.#repository.getCreatedPackages({ skip, take: TAKE })
|
|
48
|
+
this._items = data?.items ?? []
|
|
49
|
+
this._total = data?.total ?? 0
|
|
50
|
+
this._loading = false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#onPageChange(event) {
|
|
54
|
+
const current = event.target.current
|
|
55
|
+
if (current === this._page) return
|
|
56
|
+
this._page = current
|
|
57
|
+
this.#load()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async #remove(item) {
|
|
61
|
+
if (!item.unique) return
|
|
62
|
+
await umbConfirmModal(this, {
|
|
63
|
+
color: 'danger',
|
|
64
|
+
headline: `Remove ${item.name}?`,
|
|
65
|
+
content: 'Are you sure you want to delete this bundle?',
|
|
66
|
+
confirmLabel: '#general_delete',
|
|
67
|
+
})
|
|
68
|
+
if (!(await this.#repository.deleteCreatedPackage(item.unique))) return
|
|
69
|
+
this._items = this._items.filter((other) => other.unique !== item.unique)
|
|
70
|
+
this._total = Math.max(0, this._total - 1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#open(item) {
|
|
74
|
+
if (!item.unique) return
|
|
75
|
+
window.history.pushState({}, '', `${BUILDER_PATH}/edit/${item.unique}`)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#renderPagination() {
|
|
79
|
+
const pages = Math.ceil(this._total / TAKE)
|
|
80
|
+
if (pages <= 1) return nothing
|
|
81
|
+
return html`
|
|
82
|
+
<div id="pages">
|
|
83
|
+
<uui-pagination
|
|
84
|
+
.total=${pages}
|
|
85
|
+
.current=${this._page}
|
|
86
|
+
@change=${this.#onPageChange}></uui-pagination>
|
|
87
|
+
</div>
|
|
88
|
+
`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
render() {
|
|
92
|
+
// The button stays put while the list loads, as it does upstream: it is the
|
|
93
|
+
// only way to reach the builder, and a loader that hides it makes the view
|
|
94
|
+
// look empty on every page change.
|
|
95
|
+
return html`
|
|
96
|
+
<uui-button
|
|
97
|
+
look="primary"
|
|
98
|
+
href=${`${BUILDER_PATH}/create`}
|
|
99
|
+
label=${this.localize.term('packager_createPackage')}></uui-button>
|
|
100
|
+
${
|
|
101
|
+
this._loading
|
|
102
|
+
? html`<div id="loader"><uui-loader></uui-loader></div>`
|
|
103
|
+
: this._items.length === 0
|
|
104
|
+
? html`<h2 id="empty">${this.localize.term('packager_noPackagesCreated')}</h2>`
|
|
105
|
+
: html`
|
|
106
|
+
<uui-box headline="Created bundles" style="--uui-box-default-padding:0;">
|
|
107
|
+
<uui-ref-list>
|
|
108
|
+
${repeat(
|
|
109
|
+
this._items,
|
|
110
|
+
(item) => item.unique,
|
|
111
|
+
(item) => html`
|
|
112
|
+
<uui-ref-node name=${item.name} @open=${() => this.#open(item)}>
|
|
113
|
+
<uui-action-bar slot="actions">
|
|
114
|
+
<uui-button
|
|
115
|
+
@click=${() => this.#remove(item)}
|
|
116
|
+
label=${this.localize.term('general_delete')}></uui-button>
|
|
117
|
+
</uui-action-bar>
|
|
118
|
+
</uui-ref-node>
|
|
119
|
+
`,
|
|
120
|
+
)}
|
|
121
|
+
</uui-ref-list>
|
|
122
|
+
</uui-box>
|
|
123
|
+
`
|
|
124
|
+
}
|
|
125
|
+
${this.#renderPagination()}
|
|
126
|
+
`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static styles = css`
|
|
130
|
+
:host {
|
|
131
|
+
display: block;
|
|
132
|
+
padding: var(--uui-size-layout-1);
|
|
133
|
+
}
|
|
134
|
+
uui-box {
|
|
135
|
+
margin: var(--uui-size-space-5) 0;
|
|
136
|
+
padding-bottom: var(--uui-size-space-1);
|
|
137
|
+
}
|
|
138
|
+
#loader,
|
|
139
|
+
#pages {
|
|
140
|
+
display: flex;
|
|
141
|
+
justify-content: center;
|
|
142
|
+
}
|
|
143
|
+
#empty {
|
|
144
|
+
text-align: center;
|
|
145
|
+
opacity: 0.6;
|
|
146
|
+
}
|
|
147
|
+
`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
customElements.define('crombolo-bundles-created-overview', CromboloBundlesCreatedOverviewElement)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Created view, in place of Umbraco's.
|
|
3
|
+
*
|
|
4
|
+
* Its manifest declares `overwrites: ['Umb.SectionView.Packages.Builder']`.
|
|
5
|
+
* Umbraco's view is a router, not a screen: it mounts its own overview at
|
|
6
|
+
* `overview` and every `package-builder` workspace at `package-builder/create`
|
|
7
|
+
* and `package-builder/edit/:unique`. Those two routes are the only way to
|
|
8
|
+
* reach the builder, so this reproduces them exactly — replacing the view
|
|
9
|
+
* without them would have left the builder unreachable rather than renamed.
|
|
10
|
+
*
|
|
11
|
+
* What changes is the overview, which is ours and says bundle.
|
|
12
|
+
*/
|
|
13
|
+
import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'
|
|
14
|
+
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'
|
|
15
|
+
import { html } from '@umbraco-cms/backoffice/external/lit'
|
|
16
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
17
|
+
|
|
18
|
+
/** The entity type Umbraco's builder workspace declares, and the route segment. */
|
|
19
|
+
export const BUILDER_ENTITY_TYPE = 'package-builder'
|
|
20
|
+
|
|
21
|
+
export default class CromboloBundlesCreatedElement extends UmbLitElement {
|
|
22
|
+
static properties = {
|
|
23
|
+
_routes: { state: true },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super()
|
|
28
|
+
this._routes = []
|
|
29
|
+
this.observe(
|
|
30
|
+
umbExtensionsRegistry.byTypeAndFilter(
|
|
31
|
+
'workspace',
|
|
32
|
+
(workspace) => workspace.meta?.entityType === BUILDER_ENTITY_TYPE,
|
|
33
|
+
),
|
|
34
|
+
(workspaces) => {
|
|
35
|
+
this._routes = routesFor(workspaces ?? [])
|
|
36
|
+
},
|
|
37
|
+
null,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
render() {
|
|
42
|
+
return html`<umb-router-slot .routes=${this._routes}></umb-router-slot>`
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Umbraco's own route table, with our overview in place of its own. */
|
|
47
|
+
export function routesFor(workspaces) {
|
|
48
|
+
const routes = [{ path: 'overview', component: () => import('./bundles-created-overview.js') }]
|
|
49
|
+
for (const workspace of workspaces) {
|
|
50
|
+
routes.push({
|
|
51
|
+
path: `${workspace.meta.entityType}/edit/:unique`,
|
|
52
|
+
component: () => createExtensionElement(workspace),
|
|
53
|
+
setup: (component, info) => {
|
|
54
|
+
if (component) component.entityUnique = info.match.params.unique
|
|
55
|
+
},
|
|
56
|
+
})
|
|
57
|
+
routes.push({
|
|
58
|
+
path: `${workspace.meta.entityType}/create`,
|
|
59
|
+
component: () => createExtensionElement(workspace),
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
routes.push({ path: '', pathMatch: 'full', redirectTo: 'overview' })
|
|
63
|
+
routes.push({
|
|
64
|
+
path: '**',
|
|
65
|
+
component: async () => (await import('@umbraco-cms/backoffice/router')).UmbRouteNotFoundElement,
|
|
66
|
+
})
|
|
67
|
+
return routes
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
customElements.define('crombolo-bundles-created', CromboloBundlesCreatedElement)
|