@crombolo/backoffice-host 0.12.1 → 0.13.2
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/package.json +2 -2
- package/plugin/assistant/umbraco-package.json +1 -1
- package/plugin/branding/bundle-builder-labels.js +30 -1
- package/plugin/branding/sign-in.js +31 -0
- package/plugin/bundles-created-overview.js +31 -18
- package/plugin/bundles-installed.js +132 -53
- package/plugin/bundles-marketplace.js +12 -2
- package/plugin/changes-dashboard.js +8 -1
- package/plugin/download.js +19 -0
- package/plugin/form-picker.js +1 -1
- package/plugin/forms-client.js +5 -0
- package/plugin/forms-entries.js +30 -16
- package/plugin/forms-overview.js +55 -33
- package/plugin/localizations/umbraco-package.json +1 -1
- package/plugin/seo/umbraco-package.json +1 -1
- package/plugin/umbraco-package.json +109 -3
- package/plugin/workflow-client.js +40 -0
- package/plugin/workflow-designer.js +565 -0
- package/plugin/workflow-graph.js +132 -0
- package/plugin/workflow-requests.js +164 -0
- package/plugin/workflow-tab.js +219 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/backoffice-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Serves the Umbraco backoffice SPA: shell HTML, import map and package manifests.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Crombolo",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"access": "public"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@crombolo/core": "0.
|
|
34
|
+
"@crombolo/core": "0.13.2",
|
|
35
35
|
"@crombolo/backoffice-dist": "18.2.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The two labels Umbraco's bundle builder hard-codes.
|
|
2
|
+
* The two labels and two toasts Umbraco's bundle builder hard-codes.
|
|
3
3
|
*
|
|
4
4
|
* The builder workspace is vendored and its copy is English in the template
|
|
5
5
|
* rather than dictionary keys, so there is no key to override: `Name of the
|
|
@@ -21,6 +21,16 @@ export const BUILDER_LABELS = [
|
|
|
21
21
|
['Package Content', 'Bundle content'],
|
|
22
22
|
]
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The builder's toasts on saving a definition. Shown through the notification
|
|
26
|
+
* context, so they are corrected where every toast passes rather than in the
|
|
27
|
+
* element, whose `peek` call is its own.
|
|
28
|
+
*/
|
|
29
|
+
export const BUILDER_TOASTS = [
|
|
30
|
+
['Package saved', 'Bundle saved'],
|
|
31
|
+
['Package updated', 'Bundle saved'],
|
|
32
|
+
]
|
|
33
|
+
|
|
24
34
|
/** The replacement for a vendored string, or undefined when it is not one of ours. */
|
|
25
35
|
export function correctionFor(text) {
|
|
26
36
|
if (typeof text !== 'string') return undefined
|
|
@@ -55,7 +65,26 @@ function patch(element) {
|
|
|
55
65
|
return true
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
// Loaded here rather than at the top, so the module's pure parts import
|
|
69
|
+
// anywhere — the tests read `BUILDER_LABELS` and `BUILDER_TOASTS` outside a browser.
|
|
70
|
+
async function correctToasts() {
|
|
71
|
+
const { UmbNotificationContext } = await import('@umbraco-cms/backoffice/notification')
|
|
72
|
+
const proto = UmbNotificationContext.prototype
|
|
73
|
+
const peek = proto.peek
|
|
74
|
+
if (typeof peek !== 'function') return
|
|
75
|
+
proto.peek = function correctedPeek(color, options) {
|
|
76
|
+
const message = options?.data?.message
|
|
77
|
+
const replacement = BUILDER_TOASTS.find(([before]) => before === message)?.[1]
|
|
78
|
+
return peek.call(
|
|
79
|
+
this,
|
|
80
|
+
color,
|
|
81
|
+
replacement ? { ...options, data: { ...options.data, message: replacement } } : options,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
58
86
|
export const onInit = () => {
|
|
87
|
+
correctToasts().catch(() => {})
|
|
59
88
|
customElements
|
|
60
89
|
.whenDefined('umb-workspace-package-builder')
|
|
61
90
|
.then(patch)
|
|
@@ -66,7 +66,38 @@ class CromboloAuthProviderElement extends LitElement {
|
|
|
66
66
|
|
|
67
67
|
if (!customElements.get(TAG)) customElements.define(TAG, CromboloAuthProviderElement)
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Signing in again after a timeout closes its modal twice.
|
|
71
|
+
*
|
|
72
|
+
* `umb-app-auth-modal` submits when the session reports itself authorised, and
|
|
73
|
+
* `umb-auth-view` calls the same submit as its `onSuccess` once
|
|
74
|
+
* `makeAuthorizationRequest` returns. Whichever runs first closes the modal and
|
|
75
|
+
* destroys its value state, so the second's `value = { success: true }` throws
|
|
76
|
+
* `_subject is undefined` — and the view shows that as a sign-in error, though
|
|
77
|
+
* the editor is signed in. The second assignment has nothing left to say, so it
|
|
78
|
+
* is dropped. Patched on the element's prototype, as `tsx-editors.js` patches
|
|
79
|
+
* upstream, rather than in the vendored bundle.
|
|
80
|
+
*/
|
|
81
|
+
async function guardTimedOutSignIn() {
|
|
82
|
+
const Modal = await customElements.whenDefined('umb-app-auth-modal')
|
|
83
|
+
let descriptor
|
|
84
|
+
for (let proto = Modal.prototype; proto && !descriptor; proto = Object.getPrototypeOf(proto))
|
|
85
|
+
descriptor = Object.getOwnPropertyDescriptor(proto, 'value')
|
|
86
|
+
if (!descriptor?.set) return
|
|
87
|
+
const { get, set } = descriptor
|
|
88
|
+
Object.defineProperty(Modal.prototype, 'value', {
|
|
89
|
+
configurable: true,
|
|
90
|
+
get,
|
|
91
|
+
set(value) {
|
|
92
|
+
if (this.__cromboloSubmitted) return
|
|
93
|
+
set.call(this, value)
|
|
94
|
+
this.__cromboloSubmitted = true
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
export const onInit = (_host, extensionRegistry) => {
|
|
100
|
+
void guardTimedOutSignIn()
|
|
70
101
|
extensionRegistry.unregister(VENDORED)
|
|
71
102
|
extensionRegistry.register({
|
|
72
103
|
type: 'authProvider',
|
|
@@ -101,7 +101,7 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
101
101
|
version 1.0.0.
|
|
102
102
|
</p>
|
|
103
103
|
<uui-input
|
|
104
|
-
label="
|
|
104
|
+
label="Name"
|
|
105
105
|
placeholder=${pkg?.name ?? ''}
|
|
106
106
|
.value=${this._draft.name}
|
|
107
107
|
@input=${(event) => (this._draft = { ...this._draft, name: event.target.value })}></uui-input>
|
|
@@ -166,38 +166,47 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
166
166
|
// look empty on every page change.
|
|
167
167
|
return html`
|
|
168
168
|
<uui-button
|
|
169
|
-
|
|
169
|
+
color="default"
|
|
170
|
+
look="outline"
|
|
170
171
|
href=${`${BUILDER_PATH}/create`}
|
|
171
|
-
label
|
|
172
|
+
label="Create"></uui-button>
|
|
172
173
|
${
|
|
173
174
|
this._loading
|
|
174
175
|
? html`<div id="loader"><uui-loader></uui-loader></div>`
|
|
175
176
|
: this._items.length === 0
|
|
176
177
|
? html`<h2 id="empty">${this.localize.term('packager_noPackagesCreated')}</h2>`
|
|
177
178
|
: html`
|
|
178
|
-
<uui-box headline="Created bundles"
|
|
179
|
-
<uui-
|
|
179
|
+
<uui-box headline="Created bundles">
|
|
180
|
+
<uui-table>
|
|
181
|
+
<uui-table-head>
|
|
182
|
+
<uui-table-head-cell>Name</uui-table-head-cell>
|
|
183
|
+
<uui-table-head-cell>Downloads as</uui-table-head-cell>
|
|
184
|
+
<uui-table-head-cell></uui-table-head-cell>
|
|
185
|
+
</uui-table-head>
|
|
180
186
|
${repeat(
|
|
181
187
|
this._items,
|
|
182
188
|
(item) => item.unique,
|
|
183
189
|
(item) => html`
|
|
184
|
-
<uui-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
<uui-table-row>
|
|
191
|
+
<uui-table-cell>${item.name}</uui-table-cell>
|
|
192
|
+
<uui-table-cell><code>${this.#detail(item)}</code></uui-table-cell>
|
|
193
|
+
<uui-table-cell class="buttons">
|
|
194
|
+
<uui-button look="secondary" label="Edit" @click=${() => this.#open(item)}></uui-button>
|
|
189
195
|
<uui-button
|
|
190
|
-
|
|
191
|
-
label="
|
|
196
|
+
look="secondary"
|
|
197
|
+
label="Name and version"
|
|
198
|
+
@click=${() => this.#edit(item)}></uui-button>
|
|
192
199
|
<uui-button
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
look="secondary"
|
|
201
|
+
color="danger"
|
|
202
|
+
label=${this.localize.term('general_delete')}
|
|
203
|
+
@click=${() => this.#remove(item)}></uui-button>
|
|
204
|
+
</uui-table-cell>
|
|
205
|
+
</uui-table-row>
|
|
198
206
|
`,
|
|
199
207
|
)}
|
|
200
|
-
</uui-
|
|
208
|
+
</uui-table>
|
|
209
|
+
${this._items.map((item) => this.#renderEditor(item))}
|
|
201
210
|
</uui-box>
|
|
202
211
|
`
|
|
203
212
|
}
|
|
@@ -238,6 +247,10 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
238
247
|
.problem {
|
|
239
248
|
color: var(--uui-color-danger);
|
|
240
249
|
}
|
|
250
|
+
.buttons {
|
|
251
|
+
white-space: nowrap;
|
|
252
|
+
text-align: right;
|
|
253
|
+
}
|
|
241
254
|
`
|
|
242
255
|
}
|
|
243
256
|
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* What it lists instead is the site's bundle dependencies, which is what
|
|
11
11
|
* "installed" means here: what each one adds, whether its content has been
|
|
12
12
|
* added to this site, and whether its server features are switched on.
|
|
13
|
+
*
|
|
14
|
+
* Two columns: the bundles on the left, filtered as you type; the one chosen
|
|
15
|
+
* on the right, with its details and the button that uninstalls it.
|
|
13
16
|
*/
|
|
14
17
|
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
15
18
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
@@ -40,6 +43,9 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
40
43
|
_loading: { state: true },
|
|
41
44
|
_busy: { state: true },
|
|
42
45
|
_notice: { state: true },
|
|
46
|
+
_installs: { state: true },
|
|
47
|
+
_filter: { state: true },
|
|
48
|
+
_selected: { state: true },
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
constructor() {
|
|
@@ -48,6 +54,9 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
48
54
|
this._loading = true
|
|
49
55
|
this._busy = ''
|
|
50
56
|
this._notice = ''
|
|
57
|
+
this._installs = true
|
|
58
|
+
this._filter = ''
|
|
59
|
+
this._selected = ''
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
connectedCallback() {
|
|
@@ -58,7 +67,11 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
58
67
|
async #load() {
|
|
59
68
|
this._loading = true
|
|
60
69
|
try {
|
|
61
|
-
|
|
70
|
+
const result = await listInstalled()
|
|
71
|
+
this._items = result.items ?? []
|
|
72
|
+
this._installs = result.installs !== false
|
|
73
|
+
if (!this._items.some((item) => item.packageName === this._selected))
|
|
74
|
+
this._selected = this._items[0]?.packageName ?? ''
|
|
62
75
|
} catch (error) {
|
|
63
76
|
this._notice = error instanceof Error ? error.message : String(error)
|
|
64
77
|
} finally {
|
|
@@ -79,63 +92,98 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
94
|
|
|
95
|
+
#pick(item) {
|
|
96
|
+
this._selected = item.packageName
|
|
97
|
+
this.requestUpdate()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get #shown() {
|
|
101
|
+
const words = this._filter.trim().toLowerCase()
|
|
102
|
+
if (!words) return this._items
|
|
103
|
+
return this._items.filter((item) =>
|
|
104
|
+
[item.name, item.packageName, item.description].some((text) =>
|
|
105
|
+
text?.toLowerCase().includes(words),
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
82
110
|
render() {
|
|
83
111
|
if (this._loading) return html`<div id="loader"><uui-loader></uui-loader></div>`
|
|
84
|
-
|
|
112
|
+
if (this._items.length === 0)
|
|
113
|
+
return html`
|
|
114
|
+
${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
|
|
115
|
+
<uui-box headline="No bundles installed">
|
|
116
|
+
<p>Install one from the Marketplace tab, and it appears here.</p>
|
|
117
|
+
</uui-box>
|
|
118
|
+
`
|
|
119
|
+
const selected = this._items.find((item) => item.packageName === this._selected)
|
|
120
|
+
const shown = this.#shown
|
|
85
121
|
return html`
|
|
86
122
|
${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
|
|
123
|
+
<div id="columns">
|
|
124
|
+
<uui-box headline="Installed bundles" style="--uui-box-default-padding:0;">
|
|
125
|
+
<div id="filter">
|
|
126
|
+
<uui-input
|
|
127
|
+
label="Filter"
|
|
128
|
+
placeholder="Filter"
|
|
129
|
+
.value=${this._filter}
|
|
130
|
+
@input=${(event) => {
|
|
131
|
+
this._filter = event.target.value
|
|
132
|
+
}}>
|
|
133
|
+
<uui-icon name="icon-search" slot="prepend"></uui-icon>
|
|
134
|
+
</uui-input>
|
|
135
|
+
</div>
|
|
136
|
+
${
|
|
137
|
+
shown.length === 0
|
|
138
|
+
? html`<p class="empty">No bundle matches.</p>`
|
|
139
|
+
: html`<uui-ref-list>
|
|
140
|
+
${repeat(
|
|
141
|
+
shown,
|
|
142
|
+
(item) => item.packageName,
|
|
143
|
+
(item) => html`<uui-ref-node
|
|
144
|
+
name=${item.name}
|
|
145
|
+
detail=${`${item.packageName} ${item.version}`}
|
|
146
|
+
selectable
|
|
147
|
+
select-only
|
|
148
|
+
?selected=${item.packageName === this._selected}
|
|
149
|
+
@selected=${() => this.#pick(item)}
|
|
150
|
+
@deselected=${() => this.#pick(item)}
|
|
151
|
+
@open=${() => this.#pick(item)}></uui-ref-node>`,
|
|
152
|
+
)}
|
|
153
|
+
</uui-ref-list>`
|
|
154
|
+
}
|
|
155
|
+
</uui-box>
|
|
156
|
+
${selected ? this.#details(selected) : nothing}
|
|
157
|
+
</div>
|
|
158
|
+
`
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#details(item) {
|
|
162
|
+
return html`<uui-box headline=${item.name}>
|
|
163
|
+
${item.description ? html`<p>${item.description}</p>` : nothing}
|
|
164
|
+
<dl>
|
|
165
|
+
<dt>npm package</dt>
|
|
166
|
+
<dd><code>${item.packageName}</code></dd>
|
|
167
|
+
<dt>Version</dt>
|
|
168
|
+
<dd>${item.version}</dd>
|
|
169
|
+
<dt>Adds</dt>
|
|
170
|
+
<dd class="carries">${carriesTags(item.carries)}</dd>
|
|
171
|
+
<dt>On this site</dt>
|
|
172
|
+
<dd class="status">${status(item)}</dd>
|
|
173
|
+
<dt>Works with</dt>
|
|
174
|
+
<dd>Crombolo ${item.peerRange}</dd>
|
|
175
|
+
</dl>
|
|
87
176
|
${
|
|
88
|
-
this.
|
|
89
|
-
? html
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
`
|
|
177
|
+
this._installs
|
|
178
|
+
? html`<uui-button
|
|
179
|
+
look="primary"
|
|
180
|
+
color="danger"
|
|
181
|
+
label="Uninstall"
|
|
182
|
+
.state=${this._busy === item.packageName ? 'waiting' : undefined}
|
|
183
|
+
@click=${() => this.#uninstall(item)}></uui-button>`
|
|
184
|
+
: html`<p class="note">This site adds and removes bundles with a deploy, so every server has the same ones.</p>`
|
|
137
185
|
}
|
|
138
|
-
|
|
186
|
+
</uui-box>`
|
|
139
187
|
}
|
|
140
188
|
|
|
141
189
|
static styles = css`
|
|
@@ -154,10 +202,42 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
154
202
|
border-left: 3px solid var(--uui-color-positive, #2bc37c);
|
|
155
203
|
background: var(--uui-color-surface-alt, #f3f3f5);
|
|
156
204
|
}
|
|
205
|
+
#columns {
|
|
206
|
+
display: grid;
|
|
207
|
+
grid-template-columns: minmax(240px, 1fr) 2fr;
|
|
208
|
+
gap: var(--uui-size-layout-1, 24px);
|
|
209
|
+
align-items: start;
|
|
210
|
+
}
|
|
211
|
+
@media (max-width: 800px) {
|
|
212
|
+
#columns {
|
|
213
|
+
grid-template-columns: 1fr;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
#filter {
|
|
217
|
+
padding: var(--uui-size-space-4, 12px);
|
|
218
|
+
}
|
|
219
|
+
#filter uui-input {
|
|
220
|
+
width: 100%;
|
|
221
|
+
}
|
|
222
|
+
.empty {
|
|
223
|
+
padding: 0 var(--uui-size-space-4, 12px) var(--uui-size-space-4, 12px);
|
|
224
|
+
}
|
|
157
225
|
p {
|
|
158
226
|
max-width: 60ch;
|
|
159
227
|
line-height: 1.5;
|
|
160
228
|
}
|
|
229
|
+
dl {
|
|
230
|
+
display: grid;
|
|
231
|
+
grid-template-columns: max-content 1fr;
|
|
232
|
+
gap: var(--uui-size-space-3, 9px) var(--uui-size-space-5, 18px);
|
|
233
|
+
margin: 0 0 var(--uui-size-space-5, 18px);
|
|
234
|
+
}
|
|
235
|
+
dt {
|
|
236
|
+
font-weight: 700;
|
|
237
|
+
}
|
|
238
|
+
dd {
|
|
239
|
+
margin: 0;
|
|
240
|
+
}
|
|
161
241
|
.carries {
|
|
162
242
|
display: flex;
|
|
163
243
|
gap: var(--uui-size-space-2, 6px);
|
|
@@ -166,7 +246,6 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
166
246
|
display: block;
|
|
167
247
|
}
|
|
168
248
|
.note {
|
|
169
|
-
margin-top: var(--uui-size-space-5, 18px);
|
|
170
249
|
opacity: 0.8;
|
|
171
250
|
font-size: 0.9em;
|
|
172
251
|
}
|
|
@@ -27,6 +27,7 @@ export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
|
|
|
27
27
|
_url: { state: true },
|
|
28
28
|
_appliesContent: { state: true },
|
|
29
29
|
_declined: { state: true },
|
|
30
|
+
_installs: { state: true },
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
constructor() {
|
|
@@ -41,6 +42,7 @@ export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
|
|
|
41
42
|
this._query = ''
|
|
42
43
|
this._appliesContent = true
|
|
43
44
|
this._declined = new Set()
|
|
45
|
+
this._installs = true
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
connectedCallback() {
|
|
@@ -57,6 +59,7 @@ export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
|
|
|
57
59
|
this._keyword = result.keyword ?? this._keyword
|
|
58
60
|
this._url = result.url ?? ''
|
|
59
61
|
this._appliesContent = result.appliesContent !== false
|
|
62
|
+
this._installs = result.installs !== false
|
|
60
63
|
} catch (error) {
|
|
61
64
|
this._error = error instanceof Error ? error.message : String(error)
|
|
62
65
|
} finally {
|
|
@@ -97,7 +100,7 @@ export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
|
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
#contentChoice(item) {
|
|
100
|
-
if (!item.carries?.content || item.installedVersion) return nothing
|
|
103
|
+
if (!item.carries?.content || item.installedVersion || !this._installs) return nothing
|
|
101
104
|
if (!this._appliesContent)
|
|
102
105
|
return html`<p class="meta">Its content is added separately on this site.</p>`
|
|
103
106
|
return html`
|
|
@@ -135,6 +138,11 @@ export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
|
|
|
135
138
|
</div>
|
|
136
139
|
${this._error ? html`<uui-box headline="The registry could not be reached"><p>${this._error}</p></uui-box>` : nothing}
|
|
137
140
|
${this._notice ? html`<div id="notice">${this._notice}</div>` : nothing}
|
|
141
|
+
${
|
|
142
|
+
this._installs
|
|
143
|
+
? nothing
|
|
144
|
+
: html`<div id="notice">Bundles are added to this site by a deploy, so that every server has them: add one to the site's <code>package.json</code> and deploy it.</div>`
|
|
145
|
+
}
|
|
138
146
|
${
|
|
139
147
|
this._items.length === 0 && !this._error
|
|
140
148
|
? this.#empty()
|
|
@@ -161,7 +169,9 @@ export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
|
|
|
161
169
|
${
|
|
162
170
|
item.installedVersion
|
|
163
171
|
? html`<uui-tag look="positive">Installed ${item.installedVersion}</uui-tag>`
|
|
164
|
-
:
|
|
172
|
+
: !this._installs
|
|
173
|
+
? html`<p class="meta">Add it to the site with <code>bun add ${item.name}</code>, then deploy.</p>`
|
|
174
|
+
: html`
|
|
165
175
|
<uui-button
|
|
166
176
|
look="primary"
|
|
167
177
|
label="Install"
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* kind allows, and the import runs once nothing is outstanding.
|
|
16
16
|
*/
|
|
17
17
|
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
18
|
+
import { downloadFile } from './download.js'
|
|
18
19
|
import { answersUrl, fetchReport, transferAction } from './report-client.js'
|
|
19
20
|
|
|
20
21
|
const KIND_LABEL = { blocking: 'Blocking', person: 'Needs a person', auto: 'Automatic' }
|
|
@@ -351,7 +352,13 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
351
352
|
})}
|
|
352
353
|
>Import</uui-button>
|
|
353
354
|
<uui-button look="secondary" ?disabled=${busy} label="Check again" @click=${() => this.#act(pending.bundleId, 'check')}>Check again</uui-button>
|
|
354
|
-
<uui-button look="secondary"
|
|
355
|
+
<uui-button look="secondary" label="Download answers" @click=${() =>
|
|
356
|
+
downloadFile(answersUrl(pending.bundleId), 'resolutions.json').catch((error) => {
|
|
357
|
+
this._outcome = {
|
|
358
|
+
...this._outcome,
|
|
359
|
+
[pending.bundleId]: { ok: false, message: error.message },
|
|
360
|
+
}
|
|
361
|
+
})}>Download answers</uui-button>
|
|
355
362
|
</div>
|
|
356
363
|
${outcome ? this.#renderOutcome(pending, outcome) : nothing}
|
|
357
364
|
</div>`
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saves a file the server serves, for a button that downloads one. Fetched
|
|
3
|
+
* rather than linked: the backoffice router takes over every link click, a
|
|
4
|
+
* `download` one included, and answers an address it has no page for with
|
|
5
|
+
* "route could not be found".
|
|
6
|
+
*/
|
|
7
|
+
export async function downloadFile(url, fallbackName) {
|
|
8
|
+
const response = await fetch(url, { credentials: 'include' })
|
|
9
|
+
if (response.status === 403) throw new Error('You do not have permission for that.')
|
|
10
|
+
if (!response.ok) throw new Error(`The download responded ${response.status}.`)
|
|
11
|
+
const named = /filename="?([^";]+)"?/.exec(response.headers.get('content-disposition') ?? '')
|
|
12
|
+
const objectUrl = URL.createObjectURL(await response.blob())
|
|
13
|
+
// Never added to the page, so its click reaches no router.
|
|
14
|
+
const link = document.createElement('a')
|
|
15
|
+
link.href = objectUrl
|
|
16
|
+
link.download = named?.[1] ?? fallbackName
|
|
17
|
+
link.click()
|
|
18
|
+
setTimeout(() => URL.revokeObjectURL(objectUrl), 0)
|
|
19
|
+
}
|
package/plugin/form-picker.js
CHANGED
|
@@ -67,7 +67,7 @@ export default class CromboloFormPickerElement extends UmbLitElement {
|
|
|
67
67
|
if (this._items.length === 0)
|
|
68
68
|
return html`
|
|
69
69
|
<p class="empty">
|
|
70
|
-
This site has no forms. Add one
|
|
70
|
+
This site has no forms yet. Add one in the Forms section.
|
|
71
71
|
</p>
|
|
72
72
|
`
|
|
73
73
|
return html`
|
package/plugin/forms-client.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Reads and drives the Forms endpoints the server keeps at
|
|
3
3
|
* <backoffice>/crombolo/api/forms/*. The session cookie carries the auth.
|
|
4
4
|
*/
|
|
5
|
+
import { downloadFile } from './download.js'
|
|
6
|
+
|
|
5
7
|
const base = () => {
|
|
6
8
|
const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
7
9
|
return `${href.replace(/\/$/, '')}/crombolo/api/forms`
|
|
@@ -57,3 +59,6 @@ export const deleteEntry = (id) => call(`/entries/${encodeURIComponent(id)}`, {
|
|
|
57
59
|
|
|
58
60
|
/** The CSV is a download, so it is a link rather than a fetch. */
|
|
59
61
|
export const entriesCsvUrl = (key) => `${base()}/${encodeURIComponent(key)}/entries.csv`
|
|
62
|
+
|
|
63
|
+
/** Downloads a form's entries as CSV. */
|
|
64
|
+
export const downloadEntriesCsv = (key) => downloadFile(entriesCsvUrl(key), 'entries.csv')
|
package/plugin/forms-entries.js
CHANGED
|
@@ -12,7 +12,7 @@ import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit
|
|
|
12
12
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
13
13
|
import {
|
|
14
14
|
deleteEntry,
|
|
15
|
-
|
|
15
|
+
downloadEntriesCsv,
|
|
16
16
|
listEntries,
|
|
17
17
|
listForms,
|
|
18
18
|
setEntryState,
|
|
@@ -102,28 +102,31 @@ export default class CromboloFormsEntriesElement extends UmbLitElement {
|
|
|
102
102
|
return html`
|
|
103
103
|
<uui-box headline="Entries">
|
|
104
104
|
<div class="filters">
|
|
105
|
-
<select
|
|
105
|
+
<uui-select
|
|
106
|
+
label="Form"
|
|
107
|
+
.options=${this._forms.map((form) => ({
|
|
108
|
+
name: form.name,
|
|
109
|
+
value: form.key,
|
|
110
|
+
selected: form.key === this._selected,
|
|
111
|
+
}))}
|
|
106
112
|
@change=${(event) => {
|
|
107
113
|
this._selected = event.target.value
|
|
108
114
|
this.#load()
|
|
109
115
|
}}
|
|
110
|
-
>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
116
|
+
></uui-select>
|
|
117
|
+
<uui-select
|
|
118
|
+
label="State"
|
|
119
|
+
.options=${[
|
|
120
|
+
['', 'Any state'],
|
|
121
|
+
['submitted', 'Submitted'],
|
|
122
|
+
['approved', 'Approved'],
|
|
123
|
+
['rejected', 'Rejected'],
|
|
124
|
+
].map(([value, name]) => ({ name, value, selected: value === (this._state ?? '') }))}
|
|
117
125
|
@change=${(event) => {
|
|
118
126
|
this._state = event.target.value
|
|
119
127
|
this.#load()
|
|
120
128
|
}}
|
|
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>
|
|
129
|
+
></uui-select>
|
|
127
130
|
<uui-input
|
|
128
131
|
placeholder="Search the answers"
|
|
129
132
|
.value=${this._search}
|
|
@@ -140,7 +143,18 @@ export default class CromboloFormsEntriesElement extends UmbLitElement {
|
|
|
140
143
|
this.#load()
|
|
141
144
|
}}
|
|
142
145
|
></uui-toggle>
|
|
143
|
-
<
|
|
146
|
+
<uui-button
|
|
147
|
+
class="csv"
|
|
148
|
+
look="secondary"
|
|
149
|
+
label="Export CSV"
|
|
150
|
+
@click=${async () => {
|
|
151
|
+
try {
|
|
152
|
+
await downloadEntriesCsv(this._selected)
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this._notice = error.message
|
|
155
|
+
}
|
|
156
|
+
}}
|
|
157
|
+
></uui-button>
|
|
144
158
|
</div>
|
|
145
159
|
|
|
146
160
|
${this._notice ? html`<p class="notice" role="alert">${this._notice}</p>` : nothing}
|