@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.
Files changed (87) 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-client.js +34 -0
  27. package/plugin/bundles-created-overview.js +150 -0
  28. package/plugin/bundles-created.js +70 -0
  29. package/plugin/bundles-installed.js +147 -0
  30. package/plugin/bundles-marketplace.js +199 -0
  31. package/plugin/changes-dashboard.js +132 -0
  32. package/plugin/element-type-hint.js +58 -0
  33. package/plugin/form-picker.js +116 -0
  34. package/plugin/forms-client.js +52 -0
  35. package/plugin/forms-entries.js +244 -0
  36. package/plugin/forms-overview.js +380 -0
  37. package/plugin/localizations/ar.js +97 -0
  38. package/plugin/localizations/bs.js +86 -0
  39. package/plugin/localizations/cs.js +73 -0
  40. package/plugin/localizations/cy.js +92 -0
  41. package/plugin/localizations/da.js +101 -0
  42. package/plugin/localizations/de.js +79 -0
  43. package/plugin/localizations/en.js +71 -0
  44. package/plugin/localizations/es.js +52 -0
  45. package/plugin/localizations/fr.js +78 -0
  46. package/plugin/localizations/he.js +24 -0
  47. package/plugin/localizations/hr.js +89 -0
  48. package/plugin/localizations/it.js +86 -0
  49. package/plugin/localizations/ja.js +43 -0
  50. package/plugin/localizations/ko.js +24 -0
  51. package/plugin/localizations/nb.js +44 -0
  52. package/plugin/localizations/nl.js +85 -0
  53. package/plugin/localizations/pl.js +44 -0
  54. package/plugin/localizations/pt-br.js +25 -0
  55. package/plugin/localizations/pt.js +100 -0
  56. package/plugin/localizations/ro.js +9 -0
  57. package/plugin/localizations/ru.js +46 -0
  58. package/plugin/localizations/sv.js +46 -0
  59. package/plugin/localizations/tr.js +80 -0
  60. package/plugin/localizations/uk.js +46 -0
  61. package/plugin/localizations/umbraco-package.json +279 -0
  62. package/plugin/localizations/vi.js +104 -0
  63. package/plugin/localizations/zh-tw.js +38 -0
  64. package/plugin/localizations/zh.js +38 -0
  65. package/plugin/read-only-banner.js +48 -0
  66. package/plugin/report-client.js +13 -0
  67. package/plugin/seo/client.js +35 -0
  68. package/plugin/seo/dashboard.js +242 -0
  69. package/plugin/seo/document-view.js +161 -0
  70. package/plugin/seo/type-hint.js +45 -0
  71. package/plugin/seo/umbraco-package.json +61 -0
  72. package/plugin/settings-dashboard.js +123 -0
  73. package/plugin/tsx-editors.js +337 -0
  74. package/plugin/tsx-snippets.js +63 -0
  75. package/plugin/umbraco-package.json +298 -0
  76. package/plugin/welcome-dashboard.js +106 -0
  77. package/src/branding.ts +59 -0
  78. package/src/extensions.ts +228 -0
  79. package/src/graphics.ts +87 -0
  80. package/src/index.ts +7 -0
  81. package/src/login-assets.ts +18 -0
  82. package/src/manifests.ts +154 -0
  83. package/src/paths.ts +117 -0
  84. package/src/shell.ts +139 -0
  85. package/src/static.ts +107 -0
  86. package/src/text-imports.d.ts +23 -0
  87. 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)