@crombolo/backoffice-host 0.0.0-stage → 0.10.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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/assets/login.js +352 -0
  4. package/package.json +35 -4
  5. package/plugin/assistant/client.js +53 -0
  6. package/plugin/assistant/drawer.js +865 -0
  7. package/plugin/assistant/umbraco-package.json +16 -0
  8. package/plugin/branding/backdrop.svg +1 -0
  9. package/plugin/branding/branding.js +100 -0
  10. package/plugin/branding/bundle-builder-labels.js +63 -0
  11. package/plugin/branding/client-credentials.js +49 -0
  12. package/plugin/branding/components-tree.js +40 -0
  13. package/plugin/branding/content-type-labels.js +27 -0
  14. package/plugin/branding/favicon.svg +1 -0
  15. package/plugin/branding/help-menu.js +46 -0
  16. package/plugin/branding/icon-mark.js +15 -0
  17. package/plugin/branding/icon-registry.js +40 -0
  18. package/plugin/branding/localization-en.js +115 -0
  19. package/plugin/branding/log-viewer-menu.js +70 -0
  20. package/plugin/branding/logo-header.svg +1 -0
  21. package/plugin/branding/logo-surface.svg +1 -0
  22. package/plugin/branding/query-builder.js +28 -0
  23. package/plugin/branding/sign-in.js +80 -0
  24. package/plugin/branding/sysinfo.js +89 -0
  25. package/plugin/branding/theme.css +102 -0
  26. package/plugin/bundles-carries.js +16 -0
  27. package/plugin/bundles-client.js +40 -0
  28. package/plugin/bundles-created-overview.js +244 -0
  29. package/plugin/bundles-created.js +70 -0
  30. package/plugin/bundles-installed.js +176 -0
  31. package/plugin/bundles-marketplace.js +236 -0
  32. package/plugin/changes-dashboard.js +169 -0
  33. package/plugin/element-type-hint.js +58 -0
  34. package/plugin/form-picker.js +116 -0
  35. package/plugin/forms-client.js +52 -0
  36. package/plugin/forms-entries.js +244 -0
  37. package/plugin/forms-overview.js +380 -0
  38. package/plugin/localizations/ar.js +97 -0
  39. package/plugin/localizations/bs.js +86 -0
  40. package/plugin/localizations/cs.js +73 -0
  41. package/plugin/localizations/cy.js +92 -0
  42. package/plugin/localizations/da.js +101 -0
  43. package/plugin/localizations/de.js +79 -0
  44. package/plugin/localizations/en.js +71 -0
  45. package/plugin/localizations/es.js +52 -0
  46. package/plugin/localizations/fr.js +78 -0
  47. package/plugin/localizations/he.js +24 -0
  48. package/plugin/localizations/hr.js +89 -0
  49. package/plugin/localizations/it.js +86 -0
  50. package/plugin/localizations/ja.js +43 -0
  51. package/plugin/localizations/ko.js +24 -0
  52. package/plugin/localizations/nb.js +44 -0
  53. package/plugin/localizations/nl.js +85 -0
  54. package/plugin/localizations/pl.js +44 -0
  55. package/plugin/localizations/pt-br.js +25 -0
  56. package/plugin/localizations/pt.js +100 -0
  57. package/plugin/localizations/ro.js +9 -0
  58. package/plugin/localizations/ru.js +46 -0
  59. package/plugin/localizations/sv.js +46 -0
  60. package/plugin/localizations/tr.js +80 -0
  61. package/plugin/localizations/uk.js +46 -0
  62. package/plugin/localizations/umbraco-package.json +279 -0
  63. package/plugin/localizations/vi.js +104 -0
  64. package/plugin/localizations/zh-tw.js +38 -0
  65. package/plugin/localizations/zh.js +38 -0
  66. package/plugin/read-only-banner.js +48 -0
  67. package/plugin/report-client.js +13 -0
  68. package/plugin/seo/client.js +35 -0
  69. package/plugin/seo/dashboard.js +242 -0
  70. package/plugin/seo/document-view.js +161 -0
  71. package/plugin/seo/type-hint.js +45 -0
  72. package/plugin/seo/umbraco-package.json +61 -0
  73. package/plugin/settings-dashboard.js +123 -0
  74. package/plugin/tsx-editors.js +337 -0
  75. package/plugin/tsx-snippets.js +63 -0
  76. package/plugin/umbraco-package.json +298 -0
  77. package/plugin/welcome-dashboard.js +106 -0
  78. package/src/branding.ts +59 -0
  79. package/src/bundle-package.ts +266 -0
  80. package/src/extensions.ts +189 -0
  81. package/src/graphics.ts +87 -0
  82. package/src/index.ts +8 -0
  83. package/src/login-assets.ts +18 -0
  84. package/src/manifests.ts +154 -0
  85. package/src/paths.ts +117 -0
  86. package/src/shell.ts +139 -0
  87. package/src/static.ts +107 -0
  88. package/src/text-imports.d.ts +23 -0
  89. package/README.md +0 -3
@@ -0,0 +1,176 @@
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/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 bundle dependencies, which is what
11
+ * "installed" means here: what each one adds, whether its content has been
12
+ * added to this site, and whether its server features are switched on.
13
+ */
14
+ import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
15
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
16
+ import { carriesTags } from './bundles-carries.js'
17
+ import { listInstalled, uninstallBundle } from './bundles-client.js'
18
+
19
+ /** Where each part of a bundle stands on this site, in the words of someone running it. */
20
+ function status(item) {
21
+ const parts = []
22
+ if (item.carries?.content)
23
+ parts.push(
24
+ item.contentApplied
25
+ ? html`<span>Content added (${item.contentApplied})</span>`
26
+ : html`<span>Content not added yet</span>`,
27
+ )
28
+ if (item.carries?.server)
29
+ parts.push(
30
+ item.wired
31
+ ? html`<span>Server features on</span>`
32
+ : html`<span>Server features off until the site's configuration includes it</span>`,
33
+ )
34
+ return parts.length > 0 ? parts : html`<span>Ready</span>`
35
+ }
36
+
37
+ export default class CromboloBundlesInstalledElement extends UmbLitElement {
38
+ static properties = {
39
+ _items: { state: true },
40
+ _loading: { state: true },
41
+ _busy: { state: true },
42
+ _notice: { state: true },
43
+ }
44
+
45
+ constructor() {
46
+ super()
47
+ this._items = []
48
+ this._loading = true
49
+ this._busy = ''
50
+ this._notice = ''
51
+ }
52
+
53
+ connectedCallback() {
54
+ super.connectedCallback()
55
+ this.#load()
56
+ }
57
+
58
+ async #load() {
59
+ this._loading = true
60
+ try {
61
+ this._items = (await listInstalled()).items ?? []
62
+ } catch (error) {
63
+ this._notice = error instanceof Error ? error.message : String(error)
64
+ } finally {
65
+ this._loading = false
66
+ }
67
+ }
68
+
69
+ async #uninstall(item) {
70
+ this._busy = item.packageName
71
+ try {
72
+ const outcome = await uninstallBundle(item.packageName)
73
+ this._notice = outcome.message ?? ''
74
+ if (outcome.ok) await this.#load()
75
+ } catch (error) {
76
+ this._notice = error instanceof Error ? error.message : String(error)
77
+ } finally {
78
+ this._busy = ''
79
+ }
80
+ }
81
+
82
+ render() {
83
+ if (this._loading) return html`<div id="loader"><uui-loader></uui-loader></div>`
84
+
85
+ return html`
86
+ ${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
87
+ ${
88
+ this._items.length === 0
89
+ ? html`
90
+ <uui-box headline="No bundles installed">
91
+ <p>
92
+ A bundle is one of this site's npm dependencies. Install one from the
93
+ Marketplace tab, or with <code>bun add</code> in the site directory, and it
94
+ appears here.
95
+ </p>
96
+ </uui-box>
97
+ `
98
+ : html`
99
+ <uui-box headline="Installed bundles">
100
+ <uui-table>
101
+ <uui-table-head>
102
+ <uui-table-head-cell>Name</uui-table-head-cell>
103
+ <uui-table-head-cell>npm package</uui-table-head-cell>
104
+ <uui-table-head-cell>Version</uui-table-head-cell>
105
+ <uui-table-head-cell>Adds</uui-table-head-cell>
106
+ <uui-table-head-cell>On this site</uui-table-head-cell>
107
+ <uui-table-head-cell></uui-table-head-cell>
108
+ </uui-table-head>
109
+ ${repeat(
110
+ this._items,
111
+ (item) => item.packageName,
112
+ (item) => html`
113
+ <uui-table-row>
114
+ <uui-table-cell>${item.name}</uui-table-cell>
115
+ <uui-table-cell><code>${item.packageName}</code></uui-table-cell>
116
+ <uui-table-cell>${item.version}</uui-table-cell>
117
+ <uui-table-cell class="carries">${carriesTags(item.carries)}</uui-table-cell>
118
+ <uui-table-cell class="status">${status(item)}</uui-table-cell>
119
+ <uui-table-cell>
120
+ <uui-button
121
+ look="secondary"
122
+ color="danger"
123
+ label="Uninstall"
124
+ .state=${this._busy === item.packageName ? 'waiting' : undefined}
125
+ @click=${() => this.#uninstall(item)}></uui-button>
126
+ </uui-table-cell>
127
+ </uui-table-row>
128
+ `,
129
+ )}
130
+ </uui-table>
131
+ <p class="note">
132
+ An install writes this site's <code>package.json</code> and
133
+ <code>bun.lock</code>. Commit both to keep it across deployments.
134
+ </p>
135
+ </uui-box>
136
+ `
137
+ }
138
+ `
139
+ }
140
+
141
+ static styles = css`
142
+ :host {
143
+ display: block;
144
+ padding: var(--uui-size-layout-1, 24px);
145
+ }
146
+ #loader {
147
+ display: flex;
148
+ justify-content: center;
149
+ padding: var(--uui-size-layout-3, 48px);
150
+ }
151
+ #notice {
152
+ margin-bottom: var(--uui-size-space-4, 12px);
153
+ padding: var(--uui-size-space-4, 12px);
154
+ border-left: 3px solid var(--uui-color-positive, #2bc37c);
155
+ background: var(--uui-color-surface-alt, #f3f3f5);
156
+ }
157
+ p {
158
+ max-width: 60ch;
159
+ line-height: 1.5;
160
+ }
161
+ .carries {
162
+ display: flex;
163
+ gap: var(--uui-size-space-2, 6px);
164
+ }
165
+ .status span {
166
+ display: block;
167
+ }
168
+ .note {
169
+ margin-top: var(--uui-size-space-5, 18px);
170
+ opacity: 0.8;
171
+ font-size: 0.9em;
172
+ }
173
+ `
174
+ }
175
+
176
+ customElements.define('crombolo-bundles-installed', CromboloBundlesInstalledElement)
@@ -0,0 +1,236 @@
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 whose manifest is a valid bundle.
11
+ * Installing runs `bun add` on the server, then adds the content the bundle
12
+ * carries unless the site or the person installing declines it.
13
+ */
14
+ import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
15
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
16
+ import { carriesTags } from './bundles-carries.js'
17
+ import { installBundle, searchMarketplace } from './bundles-client.js'
18
+
19
+ export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
20
+ static properties = {
21
+ _items: { state: true },
22
+ _loading: { state: true },
23
+ _error: { state: true },
24
+ _busy: { state: true },
25
+ _notice: { state: true },
26
+ _keyword: { state: true },
27
+ _url: { state: true },
28
+ _appliesContent: { state: true },
29
+ _declined: { state: true },
30
+ }
31
+
32
+ constructor() {
33
+ super()
34
+ this._items = []
35
+ this._loading = true
36
+ this._error = ''
37
+ this._busy = ''
38
+ this._notice = ''
39
+ this._keyword = 'crombolo-bundle'
40
+ this._url = ''
41
+ this._query = ''
42
+ this._appliesContent = true
43
+ this._declined = new Set()
44
+ }
45
+
46
+ connectedCallback() {
47
+ super.connectedCallback()
48
+ this.#load()
49
+ }
50
+
51
+ async #load() {
52
+ this._loading = true
53
+ this._error = ''
54
+ try {
55
+ const result = await searchMarketplace(this._query)
56
+ this._items = result.items ?? []
57
+ this._keyword = result.keyword ?? this._keyword
58
+ this._url = result.url ?? ''
59
+ this._appliesContent = result.appliesContent !== false
60
+ } catch (error) {
61
+ this._error = error instanceof Error ? error.message : String(error)
62
+ } finally {
63
+ this._loading = false
64
+ }
65
+ }
66
+
67
+ #search(event) {
68
+ this._query = event.target.value ?? ''
69
+ this.#load()
70
+ }
71
+
72
+ async #install(item) {
73
+ this._busy = item.name
74
+ this._notice = ''
75
+ try {
76
+ const outcome = await installBundle(item.name, item.version, !this._declined.has(item.name))
77
+ // The dependency line is what there is to commit, so it is shown rather
78
+ // than described: the install lives in this node's node_modules until it
79
+ // is in the site's repository.
80
+ const installed = outcome.dependency
81
+ ? `${outcome.message} — "${outcome.dependency.name}": "${outcome.dependency.range}"`
82
+ : (outcome.message ?? '')
83
+ this._notice = outcome.content ? `${installed} ${outcome.content.message}` : installed
84
+ if (outcome.ok) await this.#load()
85
+ } catch (error) {
86
+ this._notice = error instanceof Error ? error.message : String(error)
87
+ } finally {
88
+ this._busy = ''
89
+ }
90
+ }
91
+
92
+ #toggleContent(item, event) {
93
+ const declined = new Set(this._declined)
94
+ if (event.target.checked) declined.delete(item.name)
95
+ else declined.add(item.name)
96
+ this._declined = declined
97
+ }
98
+
99
+ #contentChoice(item) {
100
+ if (!item.carries?.content || item.installedVersion) return nothing
101
+ if (!this._appliesContent)
102
+ return html`<p class="meta">Its content is added separately on this site.</p>`
103
+ return html`
104
+ <uui-toggle
105
+ label="Add its content when installing"
106
+ .checked=${!this._declined.has(item.name)}
107
+ @change=${(event) => this.#toggleContent(item, event)}></uui-toggle>
108
+ `
109
+ }
110
+
111
+ #empty() {
112
+ return html`
113
+ <uui-box headline="No bundles published yet">
114
+ <p>Nothing on npm carries the <code>${this._keyword}</code> keyword yet.</p>
115
+ </uui-box>
116
+ `
117
+ }
118
+
119
+ render() {
120
+ if (this._loading && this._items.length === 0)
121
+ return html`<div id="loader"><uui-loader></uui-loader></div>`
122
+
123
+ return html`
124
+ <div id="head">
125
+ <uui-input
126
+ type="search"
127
+ label="Search bundles"
128
+ placeholder="Search npm for bundles"
129
+ @change=${this.#search}></uui-input>
130
+ ${
131
+ this._url
132
+ ? html`<a href=${this._url} target="_blank" rel="noopener">Browse on npm</a>`
133
+ : nothing
134
+ }
135
+ </div>
136
+ ${this._error ? html`<uui-box headline="The registry could not be reached"><p>${this._error}</p></uui-box>` : nothing}
137
+ ${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
138
+ ${
139
+ this._items.length === 0 && !this._error
140
+ ? this.#empty()
141
+ : html`
142
+ <div id="list">
143
+ ${repeat(
144
+ this._items,
145
+ (item) => item.name,
146
+ (item) => html`
147
+ <uui-box>
148
+ <div slot="headline">${item.name} <small>${item.version}</small></div>
149
+ <p>${item.description || 'No description.'}</p>
150
+ <p class="carries">${carriesTags(item.carries)}</p>
151
+ <p class="meta">
152
+ ${item.publisher ? html`<span>by ${item.publisher}</span>` : nothing}
153
+ <a href=${item.links.npm} target="_blank" rel="noopener">npm</a>
154
+ ${
155
+ item.links.repository
156
+ ? html`<a href=${item.links.repository} target="_blank" rel="noopener">source</a>`
157
+ : nothing
158
+ }
159
+ </p>
160
+ ${this.#contentChoice(item)}
161
+ ${
162
+ item.installedVersion
163
+ ? html`<uui-tag look="positive">Installed ${item.installedVersion}</uui-tag>`
164
+ : html`
165
+ <uui-button
166
+ look="primary"
167
+ label="Install"
168
+ .state=${this._busy === item.name ? 'waiting' : undefined}
169
+ @click=${() => this.#install(item)}></uui-button>
170
+ `
171
+ }
172
+ </uui-box>
173
+ `,
174
+ )}
175
+ </div>
176
+ `
177
+ }
178
+ `
179
+ }
180
+
181
+ static styles = css`
182
+ :host {
183
+ display: block;
184
+ padding: var(--uui-size-layout-1, 24px);
185
+ }
186
+ #loader {
187
+ display: flex;
188
+ justify-content: center;
189
+ padding: var(--uui-size-layout-3, 48px);
190
+ }
191
+ #head {
192
+ display: flex;
193
+ align-items: center;
194
+ gap: var(--uui-size-space-4, 12px);
195
+ margin-bottom: var(--uui-size-space-5, 18px);
196
+ }
197
+ #head uui-input {
198
+ flex: 1;
199
+ }
200
+ #notice {
201
+ margin-bottom: var(--uui-size-space-4, 12px);
202
+ padding: var(--uui-size-space-4, 12px);
203
+ border-left: 3px solid var(--uui-color-positive, #2bc37c);
204
+ background: var(--uui-color-surface-alt, #f3f3f5);
205
+ }
206
+ #list {
207
+ display: grid;
208
+ gap: var(--uui-size-space-5, 18px);
209
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
210
+ }
211
+ p {
212
+ max-width: 60ch;
213
+ line-height: 1.5;
214
+ }
215
+ .meta {
216
+ display: flex;
217
+ gap: var(--uui-size-space-4, 12px);
218
+ font-size: 0.9em;
219
+ opacity: 0.8;
220
+ }
221
+ small {
222
+ opacity: 0.7;
223
+ font-weight: normal;
224
+ }
225
+ .carries {
226
+ display: flex;
227
+ gap: var(--uui-size-space-2, 6px);
228
+ }
229
+ uui-toggle {
230
+ display: block;
231
+ margin-bottom: var(--uui-size-space-4, 12px);
232
+ }
233
+ `
234
+ }
235
+
236
+ customElements.define('crombolo-bundles-marketplace', CromboloBundlesMarketplaceElement)
@@ -0,0 +1,169 @@
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
+ * Below them, each installed bundle that keeps data of its own lists the
11
+ * changes it has made to its storage here, and any still to make.
12
+ */
13
+ import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
14
+ import { fetchReport } from './report-client.js'
15
+
16
+ const KIND_LABEL = { blocking: 'Blocking', person: 'Needs a person', auto: 'Automatic' }
17
+ const KIND_COLOR = { blocking: 'danger', person: 'warning', auto: 'positive' }
18
+ const SOURCE_LABEL = { upgrade: 'Schema and upgrade', transfer: 'Content transfer' }
19
+
20
+ export default class CromboloChangesDashboardElement extends LitElement {
21
+ static properties = {
22
+ _report: { state: true },
23
+ _error: { state: true },
24
+ _showResolved: { state: true },
25
+ }
26
+
27
+ constructor() {
28
+ super()
29
+ this._report = undefined
30
+ this._error = undefined
31
+ this._showResolved = false
32
+ }
33
+
34
+ connectedCallback() {
35
+ super.connectedCallback()
36
+ this.#load()
37
+ }
38
+
39
+ async #load() {
40
+ try {
41
+ this._report = await fetchReport()
42
+ this._error = undefined
43
+ } catch (error) {
44
+ this._error = error.message
45
+ }
46
+ }
47
+
48
+ #group(findings) {
49
+ const groups = new Map()
50
+ for (const f of findings) {
51
+ const source = f.source ?? 'upgrade'
52
+ const key = `${source}|${f.scope ?? ''}`
53
+ const group = groups.get(key) ?? { source, scope: f.scope ?? null, findings: [] }
54
+ group.findings.push(f)
55
+ groups.set(key, group)
56
+ }
57
+ return [...groups.values()]
58
+ }
59
+
60
+ render() {
61
+ if (this._error) return html`<uui-box headline="Changes"><p>${this._error}</p></uui-box>`
62
+ if (!this._report) return html`<uui-box headline="Changes"><uui-loader></uui-loader></uui-box>`
63
+ const { health, findings, counts, bundles = [] } = this._report
64
+ const shown = findings.filter((f) => this._showResolved || f.status === 'open')
65
+ return html`
66
+ <uui-box headline="Changes">
67
+ <p>
68
+ This server runs schema <strong>${health.version}+${health.revision}</strong>
69
+ ${
70
+ health.readOnly
71
+ ? html`<uui-tag color="warning">read-only: the database is ahead of this server</uui-tag>`
72
+ : html`<uui-tag color="positive">current</uui-tag>`
73
+ }
74
+ </p>
75
+ <div class="counts">
76
+ ${Object.entries(KIND_LABEL).map(
77
+ ([kind, label]) =>
78
+ html`<uui-tag color=${KIND_COLOR[kind]}>${label}: ${counts[kind] ?? 0}</uui-tag>`,
79
+ )}
80
+ <uui-button look="secondary" compact @click=${() => this.#load()} label="Refresh">Refresh</uui-button>
81
+ <uui-toggle
82
+ label="Show resolved"
83
+ .checked=${this._showResolved}
84
+ @change=${(e) => (this._showResolved = e.target.checked)}
85
+ ></uui-toggle>
86
+ </div>
87
+ ${
88
+ shown.length === 0
89
+ ? html`<p class="empty">Nothing outstanding. <code>crombolo upgrade check</code> and <code>crombolo content check</code> write findings here.</p>`
90
+ : this.#group(shown).map((group) => this.#renderGroup(group))
91
+ }
92
+ ${bundles.map((bundle) => this.#renderBundle(bundle))}
93
+ </uui-box>
94
+ `
95
+ }
96
+
97
+ #renderBundle(bundle) {
98
+ const steps = [
99
+ ...bundle.pending.map((step) => ({ ...step, done: false })),
100
+ ...bundle.applied.map((step) => ({ ...step, done: true })).reverse(),
101
+ ]
102
+ return html`
103
+ <h4>${bundle.name} <small>data storage</small></h4>
104
+ ${
105
+ bundle.pending.length > 0
106
+ ? html`<p><uui-tag color="warning">${bundle.pending.length} change(s) waiting for the next upgrade</uui-tag></p>`
107
+ : nothing
108
+ }
109
+ <uui-table>
110
+ <uui-table-head>
111
+ <uui-table-head-cell>Change</uui-table-head-cell>
112
+ <uui-table-head-cell>Release</uui-table-head-cell>
113
+ <uui-table-head-cell>State</uui-table-head-cell>
114
+ </uui-table-head>
115
+ ${steps.map(
116
+ (step) => html`<uui-table-row>
117
+ <uui-table-cell>${step.name}${step.kind === 'contract' ? html`<small>removes data</small>` : nothing}</uui-table-cell>
118
+ <uui-table-cell>${step.release ?? ''}</uui-table-cell>
119
+ <uui-table-cell>${
120
+ step.done
121
+ ? html`<uui-tag color="positive">done ${step.appliedAt?.slice(0, 10) ?? ''}</uui-tag>`
122
+ : html`<uui-tag color="warning">waiting</uui-tag>`
123
+ }</uui-table-cell>
124
+ </uui-table-row>`,
125
+ )}
126
+ </uui-table>
127
+ `
128
+ }
129
+
130
+ #renderGroup(group) {
131
+ return html`
132
+ <h4>
133
+ ${SOURCE_LABEL[group.source] ?? group.source}
134
+ ${group.scope ? html`<code>${group.scope}</code>` : nothing}
135
+ </h4>
136
+ <uui-table>
137
+ <uui-table-head>
138
+ <uui-table-head-cell>Kind</uui-table-head-cell>
139
+ <uui-table-head-cell>What</uui-table-head-cell>
140
+ <uui-table-head-cell>Where</uui-table-head-cell>
141
+ </uui-table-head>
142
+ ${group.findings.map(
143
+ (f) => html`<uui-table-row class=${f.status}>
144
+ <uui-table-cell><uui-tag color=${KIND_COLOR[f.kind]}>${KIND_LABEL[f.kind]}</uui-tag></uui-table-cell>
145
+ <uui-table-cell>${f.message}</uui-table-cell>
146
+ <uui-table-cell>
147
+ ${f.link ? html`<a href=${f.link}>${f.subjectName}</a>` : f.subjectName}
148
+ ${f.propertyAlias ? html`<code>${f.propertyAlias}</code>` : nothing}
149
+ ${f.culture ? html`<small>${f.culture}</small>` : nothing}
150
+ </uui-table-cell>
151
+ </uui-table-row>`,
152
+ )}
153
+ </uui-table>
154
+ `
155
+ }
156
+
157
+ static styles = css`
158
+ :host { display: block; padding: var(--uui-size-layout-1); }
159
+ .counts { display: flex; gap: var(--uui-size-space-3); align-items: center; margin-bottom: var(--uui-size-space-4); }
160
+ h4 { margin: var(--uui-size-space-5) 0 var(--uui-size-space-2); }
161
+ h4:first-of-type { margin-top: 0; }
162
+ uui-table-row.resolved { opacity: 0.5; text-decoration: line-through; }
163
+ code { margin-left: var(--uui-size-space-2); }
164
+ small { margin-left: var(--uui-size-space-2); }
165
+ .empty { color: var(--uui-color-text-alt); }
166
+ `
167
+ }
168
+
169
+ 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)