@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,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Installed view, in place of Umbraco's.
|
|
3
|
+
*
|
|
4
|
+
* Its manifest declares `overwrites: ['Umb.SectionView.Packages.Installed']`.
|
|
5
|
+
* Umbraco's view is built around package migrations — it lists installed
|
|
6
|
+
* packages so that pending C# migrations can be run. Nothing carries those here
|
|
7
|
+
* (`docs/17-bundles.md`), so the view would be a list of nothing with a button
|
|
8
|
+
* that cannot apply.
|
|
9
|
+
*
|
|
10
|
+
* What it lists instead is the site's declared `crombolo` dependencies, which is
|
|
11
|
+
* what "installed" means here, and where each one came from.
|
|
12
|
+
*/
|
|
13
|
+
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
14
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
15
|
+
import { listInstalled, uninstallBundle } from './bundles-client.js'
|
|
16
|
+
|
|
17
|
+
export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
18
|
+
static properties = {
|
|
19
|
+
_items: { state: true },
|
|
20
|
+
_loading: { state: true },
|
|
21
|
+
_busy: { state: true },
|
|
22
|
+
_notice: { state: true },
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
super()
|
|
27
|
+
this._items = []
|
|
28
|
+
this._loading = true
|
|
29
|
+
this._busy = ''
|
|
30
|
+
this._notice = ''
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
super.connectedCallback()
|
|
35
|
+
this.#load()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async #load() {
|
|
39
|
+
this._loading = true
|
|
40
|
+
try {
|
|
41
|
+
this._items = (await listInstalled()).items ?? []
|
|
42
|
+
} catch (error) {
|
|
43
|
+
this._notice = error instanceof Error ? error.message : String(error)
|
|
44
|
+
} finally {
|
|
45
|
+
this._loading = false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async #uninstall(item) {
|
|
50
|
+
this._busy = item.packageName
|
|
51
|
+
try {
|
|
52
|
+
const outcome = await uninstallBundle(item.packageName)
|
|
53
|
+
this._notice = outcome.message ?? ''
|
|
54
|
+
if (outcome.ok) await this.#load()
|
|
55
|
+
} catch (error) {
|
|
56
|
+
this._notice = error instanceof Error ? error.message : String(error)
|
|
57
|
+
} finally {
|
|
58
|
+
this._busy = ''
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
render() {
|
|
63
|
+
if (this._loading) return html`<div id="loader"><uui-loader></uui-loader></div>`
|
|
64
|
+
|
|
65
|
+
return html`
|
|
66
|
+
${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
|
|
67
|
+
${
|
|
68
|
+
this._items.length === 0
|
|
69
|
+
? html`
|
|
70
|
+
<uui-box headline="No bundles installed">
|
|
71
|
+
<p>
|
|
72
|
+
A bundle is one of this site's npm dependencies. Install one from the
|
|
73
|
+
Marketplace tab, or with <code>bun add</code> in the site directory — a dependency
|
|
74
|
+
that declares a <code>crombolo</code> field appears here.
|
|
75
|
+
</p>
|
|
76
|
+
</uui-box>
|
|
77
|
+
`
|
|
78
|
+
: html`
|
|
79
|
+
<uui-box headline="Installed bundles">
|
|
80
|
+
<uui-table>
|
|
81
|
+
<uui-table-head>
|
|
82
|
+
<uui-table-head-cell>Name</uui-table-head-cell>
|
|
83
|
+
<uui-table-head-cell>npm package</uui-table-head-cell>
|
|
84
|
+
<uui-table-head-cell>Version</uui-table-head-cell>
|
|
85
|
+
<uui-table-head-cell>Extensions</uui-table-head-cell>
|
|
86
|
+
<uui-table-head-cell></uui-table-head-cell>
|
|
87
|
+
</uui-table-head>
|
|
88
|
+
${repeat(
|
|
89
|
+
this._items,
|
|
90
|
+
(item) => item.packageName,
|
|
91
|
+
(item) => html`
|
|
92
|
+
<uui-table-row>
|
|
93
|
+
<uui-table-cell>${item.name}</uui-table-cell>
|
|
94
|
+
<uui-table-cell><code>${item.packageName}</code></uui-table-cell>
|
|
95
|
+
<uui-table-cell>${item.version}</uui-table-cell>
|
|
96
|
+
<uui-table-cell>${item.extensionCount}</uui-table-cell>
|
|
97
|
+
<uui-table-cell>
|
|
98
|
+
<uui-button
|
|
99
|
+
look="secondary"
|
|
100
|
+
color="danger"
|
|
101
|
+
label="Uninstall"
|
|
102
|
+
.state=${this._busy === item.packageName ? 'waiting' : undefined}
|
|
103
|
+
@click=${() => this.#uninstall(item)}></uui-button>
|
|
104
|
+
</uui-table-cell>
|
|
105
|
+
</uui-table-row>
|
|
106
|
+
`,
|
|
107
|
+
)}
|
|
108
|
+
</uui-table>
|
|
109
|
+
<p class="note">
|
|
110
|
+
An install writes this site's <code>package.json</code> and
|
|
111
|
+
<code>bun.lock</code>. Commit both to keep it across deployments.
|
|
112
|
+
</p>
|
|
113
|
+
</uui-box>
|
|
114
|
+
`
|
|
115
|
+
}
|
|
116
|
+
`
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
static styles = css`
|
|
120
|
+
:host {
|
|
121
|
+
display: block;
|
|
122
|
+
padding: var(--uui-size-layout-1, 24px);
|
|
123
|
+
}
|
|
124
|
+
#loader {
|
|
125
|
+
display: flex;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
padding: var(--uui-size-layout-3, 48px);
|
|
128
|
+
}
|
|
129
|
+
#notice {
|
|
130
|
+
margin-bottom: var(--uui-size-space-4, 12px);
|
|
131
|
+
padding: var(--uui-size-space-4, 12px);
|
|
132
|
+
border-left: 3px solid var(--uui-color-positive, #2bc37c);
|
|
133
|
+
background: var(--uui-color-surface-alt, #f3f3f5);
|
|
134
|
+
}
|
|
135
|
+
p {
|
|
136
|
+
max-width: 60ch;
|
|
137
|
+
line-height: 1.5;
|
|
138
|
+
}
|
|
139
|
+
.note {
|
|
140
|
+
margin-top: var(--uui-size-space-5, 18px);
|
|
141
|
+
opacity: 0.8;
|
|
142
|
+
font-size: 0.9em;
|
|
143
|
+
}
|
|
144
|
+
`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
customElements.define('crombolo-bundles-installed', CromboloBundlesInstalledElement)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Marketplace view, in place of Umbraco's.
|
|
3
|
+
*
|
|
4
|
+
* Its manifest declares `overwrites: ['Umb.SectionView.Packages.Marketplace']`.
|
|
5
|
+
* What it replaces is an iframe of marketplace.umbraco.com, which cannot work
|
|
6
|
+
* here twice over: npm refuses to be framed (`x-frame-options: SAMEORIGIN`), and
|
|
7
|
+
* an Umbraco package does not install into this CMS anyway.
|
|
8
|
+
*
|
|
9
|
+
* What it shows instead is the npm registry, searched for the keyword a crombolo
|
|
10
|
+
* bundle publishes, and filtered to the ones that really declare backoffice
|
|
11
|
+
* extensions. Installing runs `bun add` on the server.
|
|
12
|
+
*/
|
|
13
|
+
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
14
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
15
|
+
import { installBundle, searchMarketplace } from './bundles-client.js'
|
|
16
|
+
|
|
17
|
+
export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
|
|
18
|
+
static properties = {
|
|
19
|
+
_items: { state: true },
|
|
20
|
+
_loading: { state: true },
|
|
21
|
+
_error: { state: true },
|
|
22
|
+
_busy: { state: true },
|
|
23
|
+
_notice: { state: true },
|
|
24
|
+
_keyword: { state: true },
|
|
25
|
+
_url: { state: true },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
constructor() {
|
|
29
|
+
super()
|
|
30
|
+
this._items = []
|
|
31
|
+
this._loading = true
|
|
32
|
+
this._error = ''
|
|
33
|
+
this._busy = ''
|
|
34
|
+
this._notice = ''
|
|
35
|
+
this._keyword = 'crombolo-bundle'
|
|
36
|
+
this._url = ''
|
|
37
|
+
this._query = ''
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
connectedCallback() {
|
|
41
|
+
super.connectedCallback()
|
|
42
|
+
this.#load()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async #load() {
|
|
46
|
+
this._loading = true
|
|
47
|
+
this._error = ''
|
|
48
|
+
try {
|
|
49
|
+
const result = await searchMarketplace(this._query)
|
|
50
|
+
this._items = result.items ?? []
|
|
51
|
+
this._keyword = result.keyword ?? this._keyword
|
|
52
|
+
this._url = result.url ?? ''
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this._error = error instanceof Error ? error.message : String(error)
|
|
55
|
+
} finally {
|
|
56
|
+
this._loading = false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#search(event) {
|
|
61
|
+
this._query = event.target.value ?? ''
|
|
62
|
+
this.#load()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async #install(item) {
|
|
66
|
+
this._busy = item.name
|
|
67
|
+
this._notice = ''
|
|
68
|
+
try {
|
|
69
|
+
const outcome = await installBundle(item.name, item.version)
|
|
70
|
+
// The dependency line is what there is to commit, so it is shown rather
|
|
71
|
+
// than described: the install lives in this node's node_modules until it
|
|
72
|
+
// is in the site's repository.
|
|
73
|
+
this._notice = outcome.dependency
|
|
74
|
+
? `${outcome.message} — "${outcome.dependency.name}": "${outcome.dependency.range}"`
|
|
75
|
+
: (outcome.message ?? '')
|
|
76
|
+
if (outcome.ok) await this.#load()
|
|
77
|
+
} catch (error) {
|
|
78
|
+
this._notice = error instanceof Error ? error.message : String(error)
|
|
79
|
+
} finally {
|
|
80
|
+
this._busy = ''
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#empty() {
|
|
85
|
+
return html`
|
|
86
|
+
<uui-box headline="No bundles published yet">
|
|
87
|
+
<p>Nothing on npm carries the <code>${this._keyword}</code> keyword yet.</p>
|
|
88
|
+
</uui-box>
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
render() {
|
|
93
|
+
if (this._loading && this._items.length === 0)
|
|
94
|
+
return html`<div id="loader"><uui-loader></uui-loader></div>`
|
|
95
|
+
|
|
96
|
+
return html`
|
|
97
|
+
<div id="head">
|
|
98
|
+
<uui-input
|
|
99
|
+
type="search"
|
|
100
|
+
label="Search bundles"
|
|
101
|
+
placeholder="Search npm for bundles"
|
|
102
|
+
@change=${this.#search}></uui-input>
|
|
103
|
+
${
|
|
104
|
+
this._url
|
|
105
|
+
? html`<a href=${this._url} target="_blank" rel="noopener">Browse on npm</a>`
|
|
106
|
+
: nothing
|
|
107
|
+
}
|
|
108
|
+
</div>
|
|
109
|
+
${this._error ? html`<uui-box headline="The registry could not be reached"><p>${this._error}</p></uui-box>` : nothing}
|
|
110
|
+
${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
|
|
111
|
+
${
|
|
112
|
+
this._items.length === 0 && !this._error
|
|
113
|
+
? this.#empty()
|
|
114
|
+
: html`
|
|
115
|
+
<div id="list">
|
|
116
|
+
${repeat(
|
|
117
|
+
this._items,
|
|
118
|
+
(item) => item.name,
|
|
119
|
+
(item) => html`
|
|
120
|
+
<uui-box>
|
|
121
|
+
<div slot="headline">${item.name} <small>${item.version}</small></div>
|
|
122
|
+
<p>${item.description || 'No description.'}</p>
|
|
123
|
+
<p class="meta">
|
|
124
|
+
${item.publisher ? html`<span>by ${item.publisher}</span>` : nothing}
|
|
125
|
+
<a href=${item.links.npm} target="_blank" rel="noopener">npm</a>
|
|
126
|
+
${
|
|
127
|
+
item.links.repository
|
|
128
|
+
? html`<a href=${item.links.repository} target="_blank" rel="noopener">source</a>`
|
|
129
|
+
: nothing
|
|
130
|
+
}
|
|
131
|
+
</p>
|
|
132
|
+
${
|
|
133
|
+
item.installedVersion
|
|
134
|
+
? html`<uui-tag look="positive">Installed ${item.installedVersion}</uui-tag>`
|
|
135
|
+
: html`
|
|
136
|
+
<uui-button
|
|
137
|
+
look="primary"
|
|
138
|
+
label="Install"
|
|
139
|
+
.state=${this._busy === item.name ? 'waiting' : undefined}
|
|
140
|
+
@click=${() => this.#install(item)}></uui-button>
|
|
141
|
+
`
|
|
142
|
+
}
|
|
143
|
+
</uui-box>
|
|
144
|
+
`,
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
`
|
|
148
|
+
}
|
|
149
|
+
`
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static styles = css`
|
|
153
|
+
:host {
|
|
154
|
+
display: block;
|
|
155
|
+
padding: var(--uui-size-layout-1, 24px);
|
|
156
|
+
}
|
|
157
|
+
#loader {
|
|
158
|
+
display: flex;
|
|
159
|
+
justify-content: center;
|
|
160
|
+
padding: var(--uui-size-layout-3, 48px);
|
|
161
|
+
}
|
|
162
|
+
#head {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
gap: var(--uui-size-space-4, 12px);
|
|
166
|
+
margin-bottom: var(--uui-size-space-5, 18px);
|
|
167
|
+
}
|
|
168
|
+
#head uui-input {
|
|
169
|
+
flex: 1;
|
|
170
|
+
}
|
|
171
|
+
#notice {
|
|
172
|
+
margin-bottom: var(--uui-size-space-4, 12px);
|
|
173
|
+
padding: var(--uui-size-space-4, 12px);
|
|
174
|
+
border-left: 3px solid var(--uui-color-positive, #2bc37c);
|
|
175
|
+
background: var(--uui-color-surface-alt, #f3f3f5);
|
|
176
|
+
}
|
|
177
|
+
#list {
|
|
178
|
+
display: grid;
|
|
179
|
+
gap: var(--uui-size-space-5, 18px);
|
|
180
|
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
181
|
+
}
|
|
182
|
+
p {
|
|
183
|
+
max-width: 60ch;
|
|
184
|
+
line-height: 1.5;
|
|
185
|
+
}
|
|
186
|
+
.meta {
|
|
187
|
+
display: flex;
|
|
188
|
+
gap: var(--uui-size-space-4, 12px);
|
|
189
|
+
font-size: 0.9em;
|
|
190
|
+
opacity: 0.8;
|
|
191
|
+
}
|
|
192
|
+
small {
|
|
193
|
+
opacity: 0.7;
|
|
194
|
+
font-weight: normal;
|
|
195
|
+
}
|
|
196
|
+
`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
customElements.define('crombolo-bundles-marketplace', CromboloBundlesMarketplaceElement)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Changes dashboard: counts by kind, each finding with its link, resolved
|
|
3
|
+
* ones greyed. Reads a stable table, so whatever version is live can show what
|
|
4
|
+
* the next version needs. docs/10, "The upgrade dashboard ships in the first release".
|
|
5
|
+
*
|
|
6
|
+
* Findings arrive from two sources — a pending upgrade and an arriving content
|
|
7
|
+
* bundle — so they are grouped rather than pooled: the counts of one say nothing
|
|
8
|
+
* about the other, and the actions that resolve them are different.
|
|
9
|
+
*/
|
|
10
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
11
|
+
import { fetchReport } from './report-client.js'
|
|
12
|
+
|
|
13
|
+
const KIND_LABEL = { blocking: 'Blocking', person: 'Needs a person', auto: 'Automatic' }
|
|
14
|
+
const KIND_COLOR = { blocking: 'danger', person: 'warning', auto: 'positive' }
|
|
15
|
+
const SOURCE_LABEL = { upgrade: 'Schema and upgrade', transfer: 'Content transfer' }
|
|
16
|
+
|
|
17
|
+
export default class CromboloChangesDashboardElement extends LitElement {
|
|
18
|
+
static properties = {
|
|
19
|
+
_report: { state: true },
|
|
20
|
+
_error: { state: true },
|
|
21
|
+
_showResolved: { state: true },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super()
|
|
26
|
+
this._report = undefined
|
|
27
|
+
this._error = undefined
|
|
28
|
+
this._showResolved = false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
connectedCallback() {
|
|
32
|
+
super.connectedCallback()
|
|
33
|
+
this.#load()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async #load() {
|
|
37
|
+
try {
|
|
38
|
+
this._report = await fetchReport()
|
|
39
|
+
this._error = undefined
|
|
40
|
+
} catch (error) {
|
|
41
|
+
this._error = error.message
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#group(findings) {
|
|
46
|
+
const groups = new Map()
|
|
47
|
+
for (const f of findings) {
|
|
48
|
+
const source = f.source ?? 'upgrade'
|
|
49
|
+
const key = `${source}|${f.scope ?? ''}`
|
|
50
|
+
const group = groups.get(key) ?? { source, scope: f.scope ?? null, findings: [] }
|
|
51
|
+
group.findings.push(f)
|
|
52
|
+
groups.set(key, group)
|
|
53
|
+
}
|
|
54
|
+
return [...groups.values()]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
render() {
|
|
58
|
+
if (this._error) return html`<uui-box headline="Changes"><p>${this._error}</p></uui-box>`
|
|
59
|
+
if (!this._report) return html`<uui-box headline="Changes"><uui-loader></uui-loader></uui-box>`
|
|
60
|
+
const { health, findings, counts } = this._report
|
|
61
|
+
const shown = findings.filter((f) => this._showResolved || f.status === 'open')
|
|
62
|
+
return html`
|
|
63
|
+
<uui-box headline="Changes">
|
|
64
|
+
<p>
|
|
65
|
+
This server runs schema <strong>${health.version}+${health.revision}</strong>
|
|
66
|
+
${
|
|
67
|
+
health.readOnly
|
|
68
|
+
? html`<uui-tag color="warning">read-only: the database is ahead of this server</uui-tag>`
|
|
69
|
+
: html`<uui-tag color="positive">current</uui-tag>`
|
|
70
|
+
}
|
|
71
|
+
</p>
|
|
72
|
+
<div class="counts">
|
|
73
|
+
${Object.entries(KIND_LABEL).map(
|
|
74
|
+
([kind, label]) =>
|
|
75
|
+
html`<uui-tag color=${KIND_COLOR[kind]}>${label}: ${counts[kind] ?? 0}</uui-tag>`,
|
|
76
|
+
)}
|
|
77
|
+
<uui-button look="secondary" compact @click=${() => this.#load()} label="Refresh">Refresh</uui-button>
|
|
78
|
+
<uui-toggle
|
|
79
|
+
label="Show resolved"
|
|
80
|
+
.checked=${this._showResolved}
|
|
81
|
+
@change=${(e) => (this._showResolved = e.target.checked)}
|
|
82
|
+
></uui-toggle>
|
|
83
|
+
</div>
|
|
84
|
+
${
|
|
85
|
+
shown.length === 0
|
|
86
|
+
? html`<p class="empty">Nothing outstanding. <code>crombolo upgrade check</code> and <code>crombolo content check</code> write findings here.</p>`
|
|
87
|
+
: this.#group(shown).map((group) => this.#renderGroup(group))
|
|
88
|
+
}
|
|
89
|
+
</uui-box>
|
|
90
|
+
`
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#renderGroup(group) {
|
|
94
|
+
return html`
|
|
95
|
+
<h4>
|
|
96
|
+
${SOURCE_LABEL[group.source] ?? group.source}
|
|
97
|
+
${group.scope ? html`<code>${group.scope}</code>` : nothing}
|
|
98
|
+
</h4>
|
|
99
|
+
<uui-table>
|
|
100
|
+
<uui-table-head>
|
|
101
|
+
<uui-table-head-cell>Kind</uui-table-head-cell>
|
|
102
|
+
<uui-table-head-cell>What</uui-table-head-cell>
|
|
103
|
+
<uui-table-head-cell>Where</uui-table-head-cell>
|
|
104
|
+
</uui-table-head>
|
|
105
|
+
${group.findings.map(
|
|
106
|
+
(f) => html`<uui-table-row class=${f.status}>
|
|
107
|
+
<uui-table-cell><uui-tag color=${KIND_COLOR[f.kind]}>${KIND_LABEL[f.kind]}</uui-tag></uui-table-cell>
|
|
108
|
+
<uui-table-cell>${f.message}</uui-table-cell>
|
|
109
|
+
<uui-table-cell>
|
|
110
|
+
${f.link ? html`<a href=${f.link}>${f.subjectName}</a>` : f.subjectName}
|
|
111
|
+
${f.propertyAlias ? html`<code>${f.propertyAlias}</code>` : nothing}
|
|
112
|
+
${f.culture ? html`<small>${f.culture}</small>` : nothing}
|
|
113
|
+
</uui-table-cell>
|
|
114
|
+
</uui-table-row>`,
|
|
115
|
+
)}
|
|
116
|
+
</uui-table>
|
|
117
|
+
`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static styles = css`
|
|
121
|
+
:host { display: block; padding: var(--uui-size-layout-1); }
|
|
122
|
+
.counts { display: flex; gap: var(--uui-size-space-3); align-items: center; margin-bottom: var(--uui-size-space-4); }
|
|
123
|
+
h4 { margin: var(--uui-size-space-5) 0 var(--uui-size-space-2); }
|
|
124
|
+
h4:first-of-type { margin-top: 0; }
|
|
125
|
+
uui-table-row.resolved { opacity: 0.5; text-decoration: line-through; }
|
|
126
|
+
code { margin-left: var(--uui-size-space-2); }
|
|
127
|
+
small { margin-left: var(--uui-size-space-2); }
|
|
128
|
+
.empty { color: var(--uui-color-text-alt); }
|
|
129
|
+
`
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
customElements.define('crombolo-changes-dashboard', CromboloChangesDashboardElement)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hint in the document-type workspace footer, for the one thing about element
|
|
3
|
+
* types that nobody finds on their own: making a type an element type does not put
|
|
4
|
+
* it in the Library. That takes a second toggle, **Allow in Library**, on a
|
|
5
|
+
* different tab — and Umbraco's own "no allowed Element Types" message in the
|
|
6
|
+
* Library's create dialog never appears, because Folder is always an option there,
|
|
7
|
+
* so the dialog is never empty enough to explain itself.
|
|
8
|
+
*
|
|
9
|
+
* Both flags are worth keeping: most element types exist for block editors and
|
|
10
|
+
* should stay out of the Library. This says so rather than changing it.
|
|
11
|
+
*
|
|
12
|
+
* Registered as a `workspaceFooterApp` conditioned on the document-type workspace,
|
|
13
|
+
* not patched into Umbraco's own view — a manifest is the supported extension
|
|
14
|
+
* point, so an upstream bump cannot silently drop it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document-type'
|
|
18
|
+
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'
|
|
19
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
20
|
+
|
|
21
|
+
export default class CromboloElementTypeHintElement extends UmbElementMixin(LitElement) {
|
|
22
|
+
static properties = { _isElement: { state: true }, _allowedInLibrary: { state: true } }
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super()
|
|
26
|
+
this._isElement = false
|
|
27
|
+
this._allowedInLibrary = false
|
|
28
|
+
this.consumeContext(UMB_DOCUMENT_TYPE_WORKSPACE_CONTEXT, (context) => {
|
|
29
|
+
if (!context) return
|
|
30
|
+
this.observe(context.isElement, (value) => {
|
|
31
|
+
this._isElement = Boolean(value)
|
|
32
|
+
})
|
|
33
|
+
this.observe(context.allowedInLibrary, (value) => {
|
|
34
|
+
this._allowedInLibrary = Boolean(value)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
render() {
|
|
40
|
+
if (!this._isElement || this._allowedInLibrary) return nothing
|
|
41
|
+
return html`<uui-tag
|
|
42
|
+
color="default"
|
|
43
|
+
look="secondary"
|
|
44
|
+
title="An element type is only offered in the Library's Create dialog when 'Allow in Library' is on, under the Structure tab. Leave it off for an element type that exists for block editors."
|
|
45
|
+
>
|
|
46
|
+
Not in the Library — see Structure
|
|
47
|
+
</uui-tag>`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static styles = css`
|
|
51
|
+
:host {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
customElements.define('crombolo-element-type-hint', CromboloElementTypeHintElement)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Form Picker property editor.
|
|
3
|
+
*
|
|
4
|
+
* A form is a file in `schema/forms/`, so the list comes from the server rather
|
|
5
|
+
* than from a tree of nodes: there is nothing in the database to browse. The
|
|
6
|
+
* stored value is the form's key, which is what a render resolves back to the
|
|
7
|
+
* definition.
|
|
8
|
+
*
|
|
9
|
+
* Shows the alias beside the name because a form is referred to by alias
|
|
10
|
+
* everywhere else — in a view, in a workflow's settings, in a diff.
|
|
11
|
+
*/
|
|
12
|
+
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
13
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
14
|
+
|
|
15
|
+
const forms = async () => {
|
|
16
|
+
const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
17
|
+
const response = await fetch(`${href.replace(/\/$/, '')}/crombolo/api/forms`, {
|
|
18
|
+
credentials: 'include',
|
|
19
|
+
headers: { accept: 'application/json' },
|
|
20
|
+
})
|
|
21
|
+
if (!response.ok) throw new Error(`forms responded ${response.status}`)
|
|
22
|
+
return (await response.json()).items ?? []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default class CromboloFormPickerElement extends UmbLitElement {
|
|
26
|
+
static properties = {
|
|
27
|
+
value: { type: String },
|
|
28
|
+
_items: { state: true },
|
|
29
|
+
_failed: { state: true },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
super()
|
|
34
|
+
this.value = ''
|
|
35
|
+
this._items = undefined
|
|
36
|
+
this._failed = false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
connectedCallback() {
|
|
40
|
+
super.connectedCallback()
|
|
41
|
+
forms().then(
|
|
42
|
+
(items) => {
|
|
43
|
+
this._items = items
|
|
44
|
+
},
|
|
45
|
+
() => {
|
|
46
|
+
this._failed = true
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A plain `change` on the host, which is what `umb-property` listens for.
|
|
53
|
+
*
|
|
54
|
+
* `UmbPropertyValueChangeEvent` is deprecated upstream and is not exported by
|
|
55
|
+
* the vendored client at all, so importing it would fail at runtime. The
|
|
56
|
+
* inner select's own event is stopped so the host dispatches exactly one.
|
|
57
|
+
*/
|
|
58
|
+
#pick(event) {
|
|
59
|
+
event.stopPropagation()
|
|
60
|
+
this.value = event.target.value || undefined
|
|
61
|
+
this.dispatchEvent(new Event('change'))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
render() {
|
|
65
|
+
if (this._failed) return html`<uui-box><p>The list of forms could not be loaded.</p></uui-box>`
|
|
66
|
+
if (this._items === undefined) return html`<uui-loader></uui-loader>`
|
|
67
|
+
if (this._items.length === 0)
|
|
68
|
+
return html`
|
|
69
|
+
<p class="empty">
|
|
70
|
+
This site has no forms. Add one as a TOML file in <code>schema/forms/</code>.
|
|
71
|
+
</p>
|
|
72
|
+
`
|
|
73
|
+
return html`
|
|
74
|
+
<uui-select
|
|
75
|
+
.value=${this.value ?? ''}
|
|
76
|
+
@change=${this.#pick}
|
|
77
|
+
label="Form"
|
|
78
|
+
placeholder="Choose a form"
|
|
79
|
+
>
|
|
80
|
+
<option value="">(none)</option>
|
|
81
|
+
${repeat(
|
|
82
|
+
this._items,
|
|
83
|
+
(form) => form.key,
|
|
84
|
+
(form) => html`
|
|
85
|
+
<option value=${form.key} ?selected=${form.key === this.value}>
|
|
86
|
+
${form.name} (${form.alias})
|
|
87
|
+
</option>
|
|
88
|
+
`,
|
|
89
|
+
)}
|
|
90
|
+
</uui-select>
|
|
91
|
+
${this.value ? html`<p class="detail">${this.#describe()}</p>` : nothing}
|
|
92
|
+
`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#describe() {
|
|
96
|
+
const form = this._items?.find((item) => item.key === this.value)
|
|
97
|
+
if (!form) return 'This form is no longer in schema/forms/.'
|
|
98
|
+
const fields = `${form.fieldCount} field${form.fieldCount === 1 ? '' : 's'}`
|
|
99
|
+
const pages = `${form.pageCount} page${form.pageCount === 1 ? '' : 's'}`
|
|
100
|
+
return `${fields} over ${pages}${form.storeEntries ? '' : '; entries are not stored'}`
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static styles = css`
|
|
104
|
+
:host {
|
|
105
|
+
display: block;
|
|
106
|
+
}
|
|
107
|
+
.empty,
|
|
108
|
+
.detail {
|
|
109
|
+
color: var(--uui-color-text-alt, #666);
|
|
110
|
+
font-size: var(--uui-type-small-size, 0.8rem);
|
|
111
|
+
margin: var(--uui-size-space-2, 6px) 0 0;
|
|
112
|
+
}
|
|
113
|
+
`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
customElements.define('crombolo-form-picker', CromboloFormPickerElement)
|