@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,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)
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Reads and drives the Forms endpoints the server keeps at
3
+ * <backoffice>/crombolo/api/forms/*. 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/forms`
8
+ }
9
+
10
+ const call = 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
+ if (response.status === 403) throw new Error('You do not have permission for that.')
20
+ const body = await response.json().catch(() => ({}))
21
+ if (!response.ok && response.status !== 400 && response.status !== 409)
22
+ throw new Error(`forms responded ${response.status}`)
23
+ return { ok: response.ok, status: response.status, body }
24
+ }
25
+
26
+ export const listForms = async () => (await call('')).body.items ?? []
27
+
28
+ export const readForm = async (key) => (await call(`/${encodeURIComponent(key)}`)).body
29
+
30
+ export const saveForm = (definition) =>
31
+ call('', { method: 'PUT', body: JSON.stringify(definition) })
32
+
33
+ export const deleteForm = (key) => call(`/${encodeURIComponent(key)}`, { method: 'DELETE' })
34
+
35
+ export const listEntries = async (key, query = {}) => {
36
+ const params = new URLSearchParams()
37
+ for (const [name, value] of Object.entries(query))
38
+ if (value !== undefined && value !== '') params.set(name, String(value))
39
+ const suffix = params.toString() ? `?${params}` : ''
40
+ return (await call(`/${encodeURIComponent(key)}/entries${suffix}`)).body
41
+ }
42
+
43
+ export const setEntryState = (id, state) =>
44
+ call(`/entries/${encodeURIComponent(id)}/state`, {
45
+ method: 'POST',
46
+ body: JSON.stringify({ state }),
47
+ })
48
+
49
+ export const deleteEntry = (id) => call(`/entries/${encodeURIComponent(id)}`, { method: 'DELETE' })
50
+
51
+ /** The CSV is a download, so it is a link rather than a fetch. */
52
+ export const entriesCsvUrl = (key) => `${base()}/${encodeURIComponent(key)}/entries.csv`
@@ -0,0 +1,244 @@
1
+ /**
2
+ * The Forms section's Entries view.
3
+ *
4
+ * What a person actually does here: read what was submitted, approve or reject
5
+ * it, delete it, and take the lot away as CSV.
6
+ *
7
+ * A sensitive field arrives already redacted — the server withholds it rather
8
+ * than the UI hiding it — so this shows that it is withheld rather than
9
+ * pretending the answer was blank.
10
+ */
11
+ import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
12
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
13
+ import {
14
+ deleteEntry,
15
+ entriesCsvUrl,
16
+ listEntries,
17
+ listForms,
18
+ setEntryState,
19
+ } from './forms-client.js'
20
+
21
+ export default class CromboloFormsEntriesElement extends UmbLitElement {
22
+ static properties = {
23
+ _forms: { state: true },
24
+ _selected: { state: true },
25
+ _entries: { state: true },
26
+ _total: { state: true },
27
+ _state: { state: true },
28
+ _search: { state: true },
29
+ _includeSpam: { state: true },
30
+ _notice: { state: true },
31
+ }
32
+
33
+ constructor() {
34
+ super()
35
+ this._forms = undefined
36
+ this._selected = ''
37
+ this._entries = []
38
+ this._total = 0
39
+ this._state = ''
40
+ this._search = ''
41
+ this._includeSpam = false
42
+ this._notice = ''
43
+ }
44
+
45
+ connectedCallback() {
46
+ super.connectedCallback()
47
+ listForms().then(
48
+ (forms) => {
49
+ this._forms = forms
50
+ this._selected = forms[0]?.key ?? ''
51
+ if (this._selected) this.#load()
52
+ },
53
+ (error) => {
54
+ this._forms = []
55
+ this._notice = error.message
56
+ },
57
+ )
58
+ }
59
+
60
+ async #load() {
61
+ this._notice = ''
62
+ try {
63
+ const page = await listEntries(this._selected, {
64
+ state: this._state,
65
+ search: this._search,
66
+ includeSpam: this._includeSpam ? 'true' : '',
67
+ take: 200,
68
+ })
69
+ this._entries = page.items ?? []
70
+ this._total = page.total ?? 0
71
+ } catch (error) {
72
+ this._entries = []
73
+ this._notice = error.message
74
+ }
75
+ }
76
+
77
+ async #setState(entry, state) {
78
+ try {
79
+ const result = await setEntryState(entry.id, state)
80
+ if (!result.ok) this._notice = 'That entry could not be changed.'
81
+ await this.#load()
82
+ } catch (error) {
83
+ this._notice = error.message
84
+ }
85
+ }
86
+
87
+ async #remove(entry) {
88
+ if (!confirm('Delete this entry? It cannot be undone.')) return
89
+ try {
90
+ await deleteEntry(entry.id)
91
+ await this.#load()
92
+ } catch (error) {
93
+ this._notice = error.message
94
+ }
95
+ }
96
+
97
+ render() {
98
+ if (this._forms === undefined) return html`<uui-loader></uui-loader>`
99
+ if (this._forms.length === 0)
100
+ return html`<uui-box headline="Entries"><p>There are no forms yet.</p></uui-box>`
101
+
102
+ return html`
103
+ <uui-box headline="Entries">
104
+ <div class="filters">
105
+ <select
106
+ @change=${(event) => {
107
+ this._selected = event.target.value
108
+ this.#load()
109
+ }}
110
+ >
111
+ ${this._forms.map(
112
+ (form) =>
113
+ html`<option value=${form.key} ?selected=${form.key === this._selected}>${form.name}</option>`,
114
+ )}
115
+ </select>
116
+ <select
117
+ @change=${(event) => {
118
+ this._state = event.target.value
119
+ this.#load()
120
+ }}
121
+ >
122
+ <option value="">Any state</option>
123
+ <option value="submitted">Submitted</option>
124
+ <option value="approved">Approved</option>
125
+ <option value="rejected">Rejected</option>
126
+ </select>
127
+ <uui-input
128
+ placeholder="Search the answers"
129
+ .value=${this._search}
130
+ @change=${(event) => {
131
+ this._search = event.target.value
132
+ this.#load()
133
+ }}
134
+ ></uui-input>
135
+ <uui-toggle
136
+ label="Include spam"
137
+ .checked=${this._includeSpam}
138
+ @change=${(event) => {
139
+ this._includeSpam = event.target.checked
140
+ this.#load()
141
+ }}
142
+ ></uui-toggle>
143
+ <a class="csv" href=${entriesCsvUrl(this._selected)} download>Export CSV</a>
144
+ </div>
145
+
146
+ ${this._notice ? html`<p class="notice" role="alert">${this._notice}</p>` : nothing}
147
+ <p>${this._total} entr${this._total === 1 ? 'y' : 'ies'}.</p>
148
+
149
+ ${
150
+ this._entries.length === 0
151
+ ? html`<p>Nothing to show.</p>`
152
+ : repeat(
153
+ this._entries,
154
+ (entry) => entry.id,
155
+ (entry) => html`
156
+ <div class="entry">
157
+ <div class="meta">
158
+ <strong>${new Date(entry.createDate).toLocaleString()}</strong>
159
+ <span class="state">${entry.state}</span>
160
+ ${entry.spam ? html`<span class="spam">spam</span>` : nothing}
161
+ </div>
162
+ <dl>
163
+ ${entry.values.map(
164
+ (value) => html`
165
+ <dt>${value.fieldAlias}</dt>
166
+ <dd>
167
+ ${
168
+ value.redacted
169
+ ? html`<em>withheld — needs sensitive-data access</em>`
170
+ : value.values.join(', ') || html`<em>not answered</em>`
171
+ }
172
+ </dd>
173
+ `,
174
+ )}
175
+ </dl>
176
+ <div class="actions">
177
+ <uui-button look="secondary" label="Approve" @click=${() => this.#setState(entry, 'approved')}></uui-button>
178
+ <uui-button look="secondary" label="Reject" @click=${() => this.#setState(entry, 'rejected')}></uui-button>
179
+ <uui-button look="secondary" color="danger" label="Delete" @click=${() => this.#remove(entry)}></uui-button>
180
+ </div>
181
+ </div>
182
+ `,
183
+ )
184
+ }
185
+ </uui-box>
186
+ `
187
+ }
188
+
189
+ static styles = css`
190
+ :host {
191
+ display: block;
192
+ padding: var(--uui-size-layout-1, 24px);
193
+ }
194
+ .filters {
195
+ display: flex;
196
+ gap: var(--uui-size-space-4, 12px);
197
+ align-items: center;
198
+ flex-wrap: wrap;
199
+ margin-bottom: var(--uui-size-layout-1, 24px);
200
+ }
201
+ .entry {
202
+ border-top: 1px solid var(--uui-color-border, #e5e5e5);
203
+ padding: var(--uui-size-space-4, 12px) 0;
204
+ }
205
+ .meta {
206
+ display: flex;
207
+ gap: var(--uui-size-space-4, 12px);
208
+ align-items: baseline;
209
+ }
210
+ .state,
211
+ .spam {
212
+ font-size: var(--uui-type-small-size, 0.8rem);
213
+ text-transform: uppercase;
214
+ letter-spacing: 0.04em;
215
+ }
216
+ .spam {
217
+ color: var(--uui-color-danger, #d42054);
218
+ }
219
+ dl {
220
+ display: grid;
221
+ grid-template-columns: minmax(8rem, 14rem) 1fr;
222
+ gap: var(--uui-size-space-2, 6px) var(--uui-size-space-4, 12px);
223
+ margin: var(--uui-size-space-4, 12px) 0;
224
+ }
225
+ dt {
226
+ color: var(--uui-color-text-alt, #666);
227
+ }
228
+ dd {
229
+ margin: 0;
230
+ }
231
+ .actions {
232
+ display: flex;
233
+ gap: var(--uui-size-space-4, 12px);
234
+ }
235
+ .notice {
236
+ color: var(--uui-color-danger, #d42054);
237
+ }
238
+ .csv {
239
+ margin-left: auto;
240
+ }
241
+ `
242
+ }
243
+
244
+ customElements.define('crombolo-forms-entries', CromboloFormsEntriesElement)