@crombolo/backoffice-host 0.13.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 +127 -61
- package/plugin/bundles-marketplace.js +1 -1
- 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 +4 -4
- package/plugin/workflow-designer.js +88 -52
- package/plugin/workflow-requests.js +19 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/backoffice-host",
|
|
3
|
-
"version": "0.13.
|
|
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.13.
|
|
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'
|
|
@@ -41,6 +44,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
41
44
|
_busy: { state: true },
|
|
42
45
|
_notice: { state: true },
|
|
43
46
|
_installs: { state: true },
|
|
47
|
+
_filter: { state: true },
|
|
48
|
+
_selected: { state: true },
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
constructor() {
|
|
@@ -50,6 +55,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
50
55
|
this._busy = ''
|
|
51
56
|
this._notice = ''
|
|
52
57
|
this._installs = true
|
|
58
|
+
this._filter = ''
|
|
59
|
+
this._selected = ''
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
connectedCallback() {
|
|
@@ -63,6 +70,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
63
70
|
const result = await listInstalled()
|
|
64
71
|
this._items = result.items ?? []
|
|
65
72
|
this._installs = result.installs !== false
|
|
73
|
+
if (!this._items.some((item) => item.packageName === this._selected))
|
|
74
|
+
this._selected = this._items[0]?.packageName ?? ''
|
|
66
75
|
} catch (error) {
|
|
67
76
|
this._notice = error instanceof Error ? error.message : String(error)
|
|
68
77
|
} finally {
|
|
@@ -83,72 +92,98 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
83
92
|
}
|
|
84
93
|
}
|
|
85
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
|
+
|
|
86
110
|
render() {
|
|
87
111
|
if (this._loading) return html`<div id="loader"><uui-loader></uui-loader></div>`
|
|
88
|
-
|
|
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
|
|
89
121
|
return html`
|
|
90
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>
|
|
91
176
|
${
|
|
92
|
-
this.
|
|
93
|
-
? html
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
</uui-box>
|
|
101
|
-
`
|
|
102
|
-
: html`
|
|
103
|
-
<uui-box headline="Installed bundles">
|
|
104
|
-
<uui-table>
|
|
105
|
-
<uui-table-head>
|
|
106
|
-
<uui-table-head-cell>Name</uui-table-head-cell>
|
|
107
|
-
<uui-table-head-cell>npm package</uui-table-head-cell>
|
|
108
|
-
<uui-table-head-cell>Version</uui-table-head-cell>
|
|
109
|
-
<uui-table-head-cell>Adds</uui-table-head-cell>
|
|
110
|
-
<uui-table-head-cell>On this site</uui-table-head-cell>
|
|
111
|
-
<uui-table-head-cell></uui-table-head-cell>
|
|
112
|
-
</uui-table-head>
|
|
113
|
-
${repeat(
|
|
114
|
-
this._items,
|
|
115
|
-
(item) => item.packageName,
|
|
116
|
-
(item) => html`
|
|
117
|
-
<uui-table-row>
|
|
118
|
-
<uui-table-cell>${item.name}</uui-table-cell>
|
|
119
|
-
<uui-table-cell><code>${item.packageName}</code></uui-table-cell>
|
|
120
|
-
<uui-table-cell>${item.version}</uui-table-cell>
|
|
121
|
-
<uui-table-cell class="carries">${carriesTags(item.carries)}</uui-table-cell>
|
|
122
|
-
<uui-table-cell class="status">${status(item)}</uui-table-cell>
|
|
123
|
-
<uui-table-cell>
|
|
124
|
-
${
|
|
125
|
-
this._installs
|
|
126
|
-
? html`<uui-button
|
|
127
|
-
look="secondary"
|
|
128
|
-
color="danger"
|
|
129
|
-
label="Uninstall"
|
|
130
|
-
.state=${this._busy === item.packageName ? 'waiting' : undefined}
|
|
131
|
-
@click=${() => this.#uninstall(item)}></uui-button>`
|
|
132
|
-
: nothing
|
|
133
|
-
}
|
|
134
|
-
</uui-table-cell>
|
|
135
|
-
</uui-table-row>
|
|
136
|
-
`,
|
|
137
|
-
)}
|
|
138
|
-
</uui-table>
|
|
139
|
-
<p class="note">
|
|
140
|
-
${
|
|
141
|
-
this._installs
|
|
142
|
-
? html`An install writes this site's <code>package.json</code> and
|
|
143
|
-
<code>bun.lock</code>. Commit both to keep it across deployments.`
|
|
144
|
-
: html`Packages are added and removed by changing this site's
|
|
145
|
-
<code>package.json</code> and deploying, so every server has the same ones.`
|
|
146
|
-
}
|
|
147
|
-
</p>
|
|
148
|
-
</uui-box>
|
|
149
|
-
`
|
|
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>`
|
|
150
185
|
}
|
|
151
|
-
|
|
186
|
+
</uui-box>`
|
|
152
187
|
}
|
|
153
188
|
|
|
154
189
|
static styles = css`
|
|
@@ -167,10 +202,42 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
167
202
|
border-left: 3px solid var(--uui-color-positive, #2bc37c);
|
|
168
203
|
background: var(--uui-color-surface-alt, #f3f3f5);
|
|
169
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
|
+
}
|
|
170
225
|
p {
|
|
171
226
|
max-width: 60ch;
|
|
172
227
|
line-height: 1.5;
|
|
173
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
|
+
}
|
|
174
241
|
.carries {
|
|
175
242
|
display: flex;
|
|
176
243
|
gap: var(--uui-size-space-2, 6px);
|
|
@@ -179,7 +246,6 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
179
246
|
display: block;
|
|
180
247
|
}
|
|
181
248
|
.note {
|
|
182
|
-
margin-top: var(--uui-size-space-5, 18px);
|
|
183
249
|
opacity: 0.8;
|
|
184
250
|
font-size: 0.9em;
|
|
185
251
|
}
|
|
@@ -141,7 +141,7 @@ export default class CromboloBundlesMarketplaceElement extends UmbLitElement {
|
|
|
141
141
|
${
|
|
142
142
|
this._installs
|
|
143
143
|
? nothing
|
|
144
|
-
: html`<div id="notice">
|
|
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
145
|
}
|
|
146
146
|
${
|
|
147
147
|
this._items.length === 0 && !this._error
|
|
@@ -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}
|
package/plugin/forms-overview.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import '@umbraco-cms/backoffice/document'
|
|
15
15
|
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
16
16
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
17
|
+
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'
|
|
17
18
|
import {
|
|
18
19
|
deleteForm,
|
|
19
20
|
listForms,
|
|
@@ -61,8 +62,13 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
61
62
|
_types: { state: true },
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
#notifications
|
|
66
|
+
|
|
64
67
|
constructor() {
|
|
65
68
|
super()
|
|
69
|
+
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => {
|
|
70
|
+
this.#notifications = context
|
|
71
|
+
})
|
|
66
72
|
this._forms = undefined
|
|
67
73
|
this._editing = undefined
|
|
68
74
|
this._problems = []
|
|
@@ -490,16 +496,19 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
490
496
|
try {
|
|
491
497
|
const result = await saveForm(this._editing)
|
|
492
498
|
if (result.ok) {
|
|
493
|
-
|
|
499
|
+
// The green toast every workspace shows on a save.
|
|
500
|
+
this.#notifications?.peek('positive', { data: { message: 'Form saved' } })
|
|
494
501
|
this._editing = undefined
|
|
495
502
|
await this.#load()
|
|
496
503
|
} else {
|
|
497
504
|
this._problems = result.body.problems ?? []
|
|
498
|
-
if (result.body.status === 'readOnly')
|
|
499
|
-
this.
|
|
505
|
+
if (result.body.status === 'readOnly' && this._problems.length === 0)
|
|
506
|
+
this._problems = [
|
|
507
|
+
{ path: '', message: 'The schema directory is read-only on this node.' },
|
|
508
|
+
]
|
|
500
509
|
}
|
|
501
510
|
} catch (error) {
|
|
502
|
-
this.
|
|
511
|
+
this._problems = [{ path: '', message: error.message }]
|
|
503
512
|
} finally {
|
|
504
513
|
this._busy = false
|
|
505
514
|
}
|
|
@@ -517,20 +526,19 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
517
526
|
}
|
|
518
527
|
|
|
519
528
|
render() {
|
|
520
|
-
if (this._forms === undefined) return html`<uui-loader></uui-loader>`
|
|
521
|
-
return
|
|
529
|
+
if (this._forms === undefined) return html`<div class="list"><uui-loader></uui-loader></div>`
|
|
530
|
+
if (this._editing) return this.#editor()
|
|
531
|
+
return html`<div class="list">
|
|
522
532
|
${this._notice ? html`<uui-box><p>${this._notice}</p></uui-box>` : nothing}
|
|
523
|
-
${this
|
|
524
|
-
|
|
533
|
+
${this.#list()}
|
|
534
|
+
</div>`
|
|
525
535
|
}
|
|
526
536
|
|
|
527
537
|
#list() {
|
|
538
|
+
// The create button above the list, as the workflows list has it.
|
|
528
539
|
return html`
|
|
540
|
+
<uui-button class="create" color="default" look="outline" label="Create" @click=${() => this.#new()}></uui-button>
|
|
529
541
|
<uui-box headline="Forms">
|
|
530
|
-
<p>
|
|
531
|
-
A form is a file in <code>schema/forms/</code>. Editing one here writes that
|
|
532
|
-
file, so the change can be reviewed in a pull request like any other type.
|
|
533
|
-
</p>
|
|
534
542
|
${
|
|
535
543
|
this._forms.length === 0
|
|
536
544
|
? html`<p>There are no forms yet.</p>`
|
|
@@ -562,9 +570,6 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
562
570
|
</uui-table>
|
|
563
571
|
`
|
|
564
572
|
}
|
|
565
|
-
<div class="actions">
|
|
566
|
-
<uui-button look="primary" label="Add a form" @click=${() => this.#new()}></uui-button>
|
|
567
|
-
</div>
|
|
568
573
|
</uui-box>
|
|
569
574
|
`
|
|
570
575
|
}
|
|
@@ -584,18 +589,14 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
584
589
|
></uui-input>
|
|
585
590
|
</uui-form-layout-item>
|
|
586
591
|
`
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
</ul>
|
|
596
|
-
</div>`
|
|
597
|
-
: nothing
|
|
598
|
-
}
|
|
592
|
+
const leave = () => {
|
|
593
|
+
this._editing = undefined
|
|
594
|
+
this._problems = []
|
|
595
|
+
}
|
|
596
|
+
// Umbraco's own layout, as the workflow designer has it: Save bottom right,
|
|
597
|
+
// and what stopped a save listed just above it.
|
|
598
|
+
return html`<umb-body-layout headline=${form.alias ? `Form: ${form.name || form.alias}` : 'A new form'}>
|
|
599
|
+
<uui-box>
|
|
599
600
|
${text('name', 'Name')}
|
|
600
601
|
${text('alias', 'Alias', 'Letters and numbers, starting with a letter. The file is named after it.')}
|
|
601
602
|
${text('messageOnSubmit', 'Message on submit')}
|
|
@@ -633,14 +634,26 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
633
634
|
${this._editing.pages.map((page, p) => this.#page(page, p))}
|
|
634
635
|
<div class="actions">
|
|
635
636
|
<uui-button look="secondary" label="Add a page" @click=${() => this.#apply(addPage)}></uui-button>
|
|
636
|
-
<uui-button look="primary" label="Save" ?disabled=${this._busy} @click=${() => this.#save()}></uui-button>
|
|
637
|
-
<uui-button look="secondary" label="Cancel" @click=${() => {
|
|
638
|
-
this._editing = undefined
|
|
639
|
-
this._problems = []
|
|
640
|
-
}}></uui-button>
|
|
641
637
|
</div>
|
|
642
638
|
</uui-box>
|
|
643
|
-
|
|
639
|
+
${
|
|
640
|
+
this._problems.length > 0
|
|
641
|
+
? html`<div class="problems" role="alert">
|
|
642
|
+
<p>This form was not saved:</p>
|
|
643
|
+
<ul>
|
|
644
|
+
${this._problems.map((problem) => html`<li>${problem.path ? html`<code>${problem.path}</code> ` : nothing}${problem.message}</li>`)}
|
|
645
|
+
</ul>
|
|
646
|
+
</div>`
|
|
647
|
+
: nothing
|
|
648
|
+
}
|
|
649
|
+
${
|
|
650
|
+
this._problems.length > 0
|
|
651
|
+
? html`<span slot="footer-info" class="not-saved">Not saved: ${this._problems.length} problem(s), listed above</span>`
|
|
652
|
+
: nothing
|
|
653
|
+
}
|
|
654
|
+
<uui-button slot="actions" look="secondary" label="Back to the list" @click=${leave}>Back to the list</uui-button>
|
|
655
|
+
<uui-button slot="actions" look="primary" color="positive" label="Save" ?disabled=${this._busy} @click=${() => this.#save()}>Save</uui-button>
|
|
656
|
+
</umb-body-layout>`
|
|
644
657
|
}
|
|
645
658
|
|
|
646
659
|
static styles = css`
|
|
@@ -701,8 +714,17 @@ export default class CromboloFormsOverviewElement extends UmbLitElement {
|
|
|
701
714
|
}
|
|
702
715
|
:host {
|
|
703
716
|
display: block;
|
|
717
|
+
height: 100%;
|
|
718
|
+
}
|
|
719
|
+
.list {
|
|
704
720
|
padding: var(--uui-size-layout-1, 24px);
|
|
705
721
|
}
|
|
722
|
+
.create {
|
|
723
|
+
margin-bottom: var(--uui-size-space-5, 18px);
|
|
724
|
+
}
|
|
725
|
+
.not-saved {
|
|
726
|
+
color: var(--uui-color-danger, #d42054);
|
|
727
|
+
}
|
|
706
728
|
p,
|
|
707
729
|
ul {
|
|
708
730
|
max-width: 70ch;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "../../backoffice-dist/dist/umbraco-package-schema.json",
|
|
3
3
|
"name": "Crombolo",
|
|
4
4
|
"id": "Crombolo.Core",
|
|
5
|
-
"version": "0.13.
|
|
5
|
+
"version": "0.13.2",
|
|
6
6
|
"allowPublicAccess": false,
|
|
7
7
|
"extensions": [
|
|
8
8
|
{
|
|
@@ -246,7 +246,7 @@
|
|
|
246
246
|
"alias": "Crombolo.SectionView.Forms.Overview",
|
|
247
247
|
"name": "Crombolo Forms Overview",
|
|
248
248
|
"element": "/crombolo-plugin/forms-overview.js",
|
|
249
|
-
"weight":
|
|
249
|
+
"weight": 100,
|
|
250
250
|
"meta": {
|
|
251
251
|
"label": "Forms",
|
|
252
252
|
"pathname": "forms",
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
"alias": "Crombolo.SectionView.Forms.Entries",
|
|
265
265
|
"name": "Crombolo Forms Entries",
|
|
266
266
|
"element": "/crombolo-plugin/forms-entries.js",
|
|
267
|
-
"weight":
|
|
267
|
+
"weight": 200,
|
|
268
268
|
"meta": {
|
|
269
269
|
"label": "Entries",
|
|
270
270
|
"pathname": "entries",
|
|
@@ -352,7 +352,7 @@
|
|
|
352
352
|
"name": "Crombolo Workflow Section",
|
|
353
353
|
"weight": 40,
|
|
354
354
|
"meta": {
|
|
355
|
-
"label": "
|
|
355
|
+
"label": "Workflows",
|
|
356
356
|
"pathname": "workflow"
|
|
357
357
|
}
|
|
358
358
|
},
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
// For its side effect: registers `umb-input-document`, which the shell loads lazily.
|
|
11
11
|
import '@umbraco-cms/backoffice/document'
|
|
12
|
-
import { css, html,
|
|
12
|
+
import { css, html, nothing, unsafeCSS } from '@umbraco-cms/backoffice/external/lit'
|
|
13
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
14
|
+
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'
|
|
13
15
|
import { deleteWorkflow, listWorkflows, previewWorkflow, saveWorkflow } from './workflow-client.js'
|
|
14
16
|
import { ancestors, BOX, drawGraph, GRAPH_STYLES, layout, wouldLoop } from './workflow-graph.js'
|
|
15
17
|
|
|
@@ -66,7 +68,7 @@ const select = (
|
|
|
66
68
|
${Object.entries(options).map(([key, label]) => html`<option value=${key} ?selected=${key === value}>${label}</option>`)}
|
|
67
69
|
</select>`
|
|
68
70
|
|
|
69
|
-
export default class CromboloWorkflowDesignerElement extends
|
|
71
|
+
export default class CromboloWorkflowDesignerElement extends UmbLitElement {
|
|
70
72
|
static properties = {
|
|
71
73
|
_list: { state: true },
|
|
72
74
|
_editing: { state: true },
|
|
@@ -79,6 +81,14 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
|
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
#drag
|
|
84
|
+
#notifications
|
|
85
|
+
|
|
86
|
+
constructor() {
|
|
87
|
+
super()
|
|
88
|
+
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => {
|
|
89
|
+
this.#notifications = context
|
|
90
|
+
})
|
|
91
|
+
}
|
|
82
92
|
|
|
83
93
|
connectedCallback() {
|
|
84
94
|
super.connectedCallback()
|
|
@@ -247,19 +257,21 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
|
|
|
247
257
|
return
|
|
248
258
|
}
|
|
249
259
|
this._affected = undefined
|
|
250
|
-
|
|
260
|
+
// The green toast every workspace shows on a save.
|
|
261
|
+
this.#notifications?.peek('positive', { data: { message: 'Workflow saved' } })
|
|
251
262
|
this._originalAlias = this._editing.alias
|
|
252
263
|
await this.#load()
|
|
253
264
|
}
|
|
254
265
|
|
|
255
|
-
|
|
266
|
+
/** From the list or the editor: the workflow by its name and the alias its file has. */
|
|
267
|
+
async #delete(name, alias) {
|
|
256
268
|
if (
|
|
257
269
|
!window.confirm(
|
|
258
|
-
`Delete the workflow "${
|
|
270
|
+
`Delete the workflow "${name}"? Pages it covers will publish without approval.`,
|
|
259
271
|
)
|
|
260
272
|
)
|
|
261
273
|
return
|
|
262
|
-
const result = await deleteWorkflow(
|
|
274
|
+
const result = await deleteWorkflow(alias)
|
|
263
275
|
if (!result.ok) {
|
|
264
276
|
this._message = result.message
|
|
265
277
|
return
|
|
@@ -270,15 +282,24 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
|
|
|
270
282
|
|
|
271
283
|
render() {
|
|
272
284
|
if (this._message && !this._list)
|
|
273
|
-
return html`<uui-box headline="Workflows">${this._message}</uui-box>`
|
|
274
|
-
if (!this._list)
|
|
285
|
+
return html`<div class="list"><uui-box headline="Workflows">${this._message}</uui-box></div>`
|
|
286
|
+
if (!this._list)
|
|
287
|
+
return html`<div class="list"><uui-box headline="Workflows"><uui-loader></uui-loader></uui-box></div>`
|
|
275
288
|
if (!this._editing) return this.#renderList()
|
|
276
289
|
return this.#renderEditor()
|
|
277
290
|
}
|
|
278
291
|
|
|
279
292
|
#renderList() {
|
|
280
293
|
const { items, editable } = this._list
|
|
281
|
-
|
|
294
|
+
// The create button above the list, as the Created bundles view has it.
|
|
295
|
+
return html`<div class="list">
|
|
296
|
+
${
|
|
297
|
+
editable
|
|
298
|
+
? html`<uui-button class="create" color="default" look="outline" label="Create" @click=${() => this.#edit(blank())}>Create</uui-button>`
|
|
299
|
+
: nothing
|
|
300
|
+
}
|
|
301
|
+
${this._message ? html`<uui-box><p class="message">${this._message}</p></uui-box>` : nothing}
|
|
302
|
+
<uui-box headline="Workflows">
|
|
282
303
|
${editable ? nothing : html`<p>Workflows are kept with the site's files, which cannot be changed on this server. Change them in the repository and deploy.</p>`}
|
|
283
304
|
${
|
|
284
305
|
items.length === 0
|
|
@@ -288,13 +309,11 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
|
|
|
288
309
|
<uui-table-head-cell>Workflow</uui-table-head-cell>
|
|
289
310
|
<uui-table-head-cell>Covers</uui-table-head-cell>
|
|
290
311
|
<uui-table-head-cell>Stages</uui-table-head-cell>
|
|
312
|
+
<uui-table-head-cell></uui-table-head-cell>
|
|
291
313
|
</uui-table-head>
|
|
292
314
|
${items.map(
|
|
293
315
|
(w) => html`<uui-table-row>
|
|
294
|
-
<uui-table-cell
|
|
295
|
-
e.preventDefault()
|
|
296
|
-
this.#edit(w)
|
|
297
|
-
}}>${w.name}</a></uui-table-cell>
|
|
316
|
+
<uui-table-cell>${w.name}</uui-table-cell>
|
|
298
317
|
<uui-table-cell>${[
|
|
299
318
|
w.appliesTo.branches.length ? `${w.appliesTo.branches.length} branch(es)` : '',
|
|
300
319
|
...w.appliesTo.contentTypes,
|
|
@@ -302,55 +321,48 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
|
|
|
302
321
|
.filter(Boolean)
|
|
303
322
|
.join(', ')}</uui-table-cell>
|
|
304
323
|
<uui-table-cell>${w.stages.length}</uui-table-cell>
|
|
324
|
+
<uui-table-cell>
|
|
325
|
+
<uui-button look="secondary" label=${editable ? 'Edit' : 'View'} @click=${() => this.#edit(w)}></uui-button>
|
|
326
|
+
${
|
|
327
|
+
editable
|
|
328
|
+
? html`<uui-button look="secondary" color="danger" label="Delete" @click=${() => this.#delete(w.name, w.alias)}></uui-button>`
|
|
329
|
+
: nothing
|
|
330
|
+
}
|
|
331
|
+
</uui-table-cell>
|
|
305
332
|
</uui-table-row>`,
|
|
306
333
|
)}
|
|
307
334
|
</uui-table>`
|
|
308
335
|
}
|
|
309
|
-
|
|
310
|
-
</
|
|
336
|
+
</uui-box>
|
|
337
|
+
</div>`
|
|
311
338
|
}
|
|
312
339
|
|
|
313
340
|
#renderEditor() {
|
|
314
341
|
const w = this._editing
|
|
315
342
|
const editable = this._list.editable
|
|
316
343
|
const stage = this.#stage(this._selected)
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
<uui-button look="secondary" label="Add a stage" @click=${() => this.#addStage()}>Add a stage</uui-button>
|
|
327
|
-
<uui-button look=${this._connecting ? 'primary' : 'secondary'} label="Connect stages"
|
|
328
|
-
@click=${() => {
|
|
329
|
-
this._connecting = this._connecting ? undefined : true
|
|
330
|
-
}}
|
|
331
|
-
>${this._connecting === true ? 'Click the stage to draw from' : this._connecting ? 'Now click the stage it leads to' : 'Connect stages'}</uui-button>
|
|
332
|
-
<uui-button look="primary" label="Save" @click=${() => this.#save()}>Save</uui-button>
|
|
333
|
-
${
|
|
334
|
-
this._originalAlias &&
|
|
335
|
-
this._list.items.some((i) => i.alias === this._originalAlias)
|
|
336
|
-
? html`<uui-button look="secondary" color="danger" label="Delete" @click=${() => this.#delete()}>Delete</uui-button>`
|
|
337
|
-
: nothing
|
|
338
|
-
}
|
|
339
|
-
`
|
|
340
|
-
: nothing
|
|
341
|
-
}
|
|
342
|
-
</div>
|
|
343
|
-
${this._message ? html`<p class="message">${this._message}</p>` : nothing}
|
|
344
|
-
${this._problems ? html`<ul class="problems">${this._problems.map((p) => html`<li>${p.path ? html`<code>${p.path}</code> ` : nothing}${p.message}</li>`)}</ul>` : nothing}
|
|
344
|
+
const leave = () => {
|
|
345
|
+
this._editing = undefined
|
|
346
|
+
this._message = undefined
|
|
347
|
+
this._problems = undefined
|
|
348
|
+
}
|
|
349
|
+
// Umbraco's own layout, so Save sits bottom right in the footer as it does
|
|
350
|
+
// in every workspace, with Back beside it.
|
|
351
|
+
return html`<umb-body-layout headline=${w.name || 'Workflow'}>
|
|
352
|
+
<uui-box>
|
|
345
353
|
${
|
|
346
|
-
|
|
347
|
-
? html`<div class="
|
|
348
|
-
<
|
|
349
|
-
<
|
|
350
|
-
|
|
354
|
+
editable
|
|
355
|
+
? html`<div class="toolbar">
|
|
356
|
+
<uui-button look="secondary" label="Add a stage" @click=${() => this.#addStage()}>Add a stage</uui-button>
|
|
357
|
+
<uui-button look=${this._connecting ? 'primary' : 'secondary'} label="Connect stages"
|
|
358
|
+
@click=${() => {
|
|
359
|
+
this._connecting = this._connecting ? undefined : true
|
|
360
|
+
}}
|
|
361
|
+
>${this._connecting === true ? 'Click the stage to draw from' : this._connecting ? 'Now click the stage it leads to' : 'Connect stages'}</uui-button>
|
|
351
362
|
</div>`
|
|
352
363
|
: nothing
|
|
353
364
|
}
|
|
365
|
+
${this._message ? html`<p class="message">${this._message}</p>` : nothing}
|
|
354
366
|
<p class="hint">Arrows run left to right: the stages an arrow leads to wait for every stage with an arrow into them, and run side by side. Click an arrow to remove it.</p>
|
|
355
367
|
<div class="canvas" @pointermove=${(e) => editable && this.#pointerMove(e)} @pointerup=${() => this.#pointerUp()} @pointerleave=${() => this.#pointerUp()}>
|
|
356
368
|
${drawGraph(w.stages, {
|
|
@@ -365,7 +377,28 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
|
|
|
365
377
|
${stage ? this.#renderStage(stage, editable) : nothing}
|
|
366
378
|
${this.#renderSettings(w, editable)}
|
|
367
379
|
</div>
|
|
368
|
-
|
|
380
|
+
${this._problems ? html`<ul class="problems">${this._problems.map((p) => html`<li>${p.path ? html`<code>${p.path}</code> ` : nothing}${p.message}</li>`)}</ul>` : nothing}
|
|
381
|
+
${
|
|
382
|
+
this._affected
|
|
383
|
+
? html`<div class="affected">
|
|
384
|
+
<p>Saving changes the rules for ${this._affected.length} page(s) waiting for approval:</p>
|
|
385
|
+
<ul>${this._affected.map((a) => html`<li>${a.page}: ${ON_CHANGE[a.outcome]}</li>`)}</ul>
|
|
386
|
+
<uui-button look="primary" label="Save anyway" @click=${() => this.#save(true)}>Save anyway</uui-button>
|
|
387
|
+
</div>`
|
|
388
|
+
: nothing
|
|
389
|
+
}
|
|
390
|
+
${
|
|
391
|
+
this._problems
|
|
392
|
+
? html`<span slot="footer-info" class="not-saved">Not saved: ${this._problems.length} problem(s), listed above</span>`
|
|
393
|
+
: nothing
|
|
394
|
+
}
|
|
395
|
+
<uui-button slot="actions" look="secondary" label="Back to the list" @click=${leave}>Back to the list</uui-button>
|
|
396
|
+
${
|
|
397
|
+
editable
|
|
398
|
+
? html`<uui-button slot="actions" look="primary" color="positive" label="Save" @click=${() => this.#save()}>Save</uui-button>`
|
|
399
|
+
: nothing
|
|
400
|
+
}
|
|
401
|
+
</umb-body-layout>`
|
|
369
402
|
}
|
|
370
403
|
|
|
371
404
|
#renderStage(stage, editable) {
|
|
@@ -510,7 +543,9 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
|
|
|
510
543
|
}
|
|
511
544
|
|
|
512
545
|
static styles = css`
|
|
513
|
-
:host { display: block;
|
|
546
|
+
:host { display: block; height: 100%; }
|
|
547
|
+
.list { padding: var(--uui-size-layout-1); }
|
|
548
|
+
.create { margin-bottom: var(--uui-size-space-5); }
|
|
514
549
|
.toolbar { display: flex; flex-wrap: wrap; gap: var(--uui-size-space-3); margin-bottom: var(--uui-size-space-4); }
|
|
515
550
|
.canvas { overflow: auto; border: 1px dashed var(--uui-color-border); border-radius: 6px; padding: var(--uui-size-space-5); min-height: 200px; touch-action: none; }
|
|
516
551
|
.panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--uui-size-space-5); margin-top: var(--uui-size-space-5); }
|
|
@@ -520,8 +555,9 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
|
|
|
520
555
|
.field > span { font-weight: 700; }
|
|
521
556
|
.hint { opacity: 0.8; font-size: 0.9em; }
|
|
522
557
|
.message { color: var(--uui-color-warning-emphasis, #9e7300); }
|
|
523
|
-
.problems { color: var(--uui-color-danger); }
|
|
524
|
-
.
|
|
558
|
+
.problems { color: var(--uui-color-danger); margin-top: var(--uui-size-space-5); }
|
|
559
|
+
.not-saved { color: var(--uui-color-danger); }
|
|
560
|
+
.affected { margin-top: var(--uui-size-space-5); border: 1px solid var(--uui-color-warning); padding: var(--uui-size-space-4); border-radius: 6px; margin-bottom: var(--uui-size-space-4); }
|
|
525
561
|
${unsafeCSS(GRAPH_STYLES)}
|
|
526
562
|
`
|
|
527
563
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The Workflows section's lists (story 028): what waits for the reader's
|
|
3
3
|
* approval, every request in flight, and those decided. Each row links to the
|
|
4
4
|
* page, whose Workflow tab is where a request is acted on.
|
|
5
5
|
*/
|
|
6
|
-
import { css, html,
|
|
6
|
+
import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
7
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
7
8
|
import { finished, inFlight, waitingForMe } from './workflow-client.js'
|
|
8
9
|
|
|
9
10
|
const STATE = {
|
|
@@ -13,15 +14,27 @@ const STATE = {
|
|
|
13
14
|
withdrawn: ['default', 'Withdrawn'],
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
/** The page's Workflow tab: the editor's route is `edit/<key>/<variant>/view/<tab>`. */
|
|
18
|
+
const pageLink = (item) => {
|
|
17
19
|
const base = (document.querySelector('base')?.getAttribute('href') ?? '/umbraco/').replace(
|
|
18
20
|
/\/$/,
|
|
19
21
|
'',
|
|
20
22
|
)
|
|
21
|
-
return `${base}/section/content/workspace/document/edit/${
|
|
23
|
+
return `${base}/section/content/workspace/document/edit/${item.nodeKey}/${encodeURIComponent(item.variant ?? 'invariant')}/view/workflow`
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Navigates within the backoffice, as Umbraco's own code does, so the page
|
|
28
|
+
* opens on its Workflow tab without reloading the app. A modified click still
|
|
29
|
+
* opens a new tab.
|
|
30
|
+
*/
|
|
31
|
+
function openInBackoffice(event, href) {
|
|
32
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) return
|
|
33
|
+
event.preventDefault()
|
|
34
|
+
history.pushState(null, '', href)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class CromboloWorkflowListElement extends UmbLitElement {
|
|
25
38
|
static properties = {
|
|
26
39
|
_items: { state: true },
|
|
27
40
|
_message: { state: true },
|
|
@@ -76,7 +89,7 @@ class CromboloWorkflowListElement extends LitElement {
|
|
|
76
89
|
const [color, label] = STATE[item.state] ?? ['default', item.state]
|
|
77
90
|
const waiting = item.stages.filter((s) => s.state === 'waiting').map((s) => s.alias)
|
|
78
91
|
return html`<uui-table-row>
|
|
79
|
-
<uui-table-cell><a href=${pageLink(item
|
|
92
|
+
<uui-table-cell><a href=${pageLink(item)} @click=${(e) => openInBackoffice(e, pageLink(item))}>${item.nodeName}</a></uui-table-cell>
|
|
80
93
|
<uui-table-cell>${item.workflow.name}</uui-table-cell>
|
|
81
94
|
<uui-table-cell>${item.action === 'unpublish' ? 'Taking it down' : 'Publishing'}</uui-table-cell>
|
|
82
95
|
<uui-table-cell>
|