@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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Workflows section's lists (story 028): what waits for the reader's
|
|
3
|
+
* approval, every request in flight, and those decided. Each row links to the
|
|
4
|
+
* page, whose Workflow tab is where a request is acted on.
|
|
5
|
+
*/
|
|
6
|
+
import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
7
|
+
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
8
|
+
import { finished, inFlight, waitingForMe } from './workflow-client.js'
|
|
9
|
+
|
|
10
|
+
const STATE = {
|
|
11
|
+
waiting: ['warning', 'Waiting'],
|
|
12
|
+
approved: ['positive', 'Approved'],
|
|
13
|
+
'sent-back': ['danger', 'Sent back'],
|
|
14
|
+
withdrawn: ['default', 'Withdrawn'],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The page's Workflow tab: the editor's route is `edit/<key>/<variant>/view/<tab>`. */
|
|
18
|
+
const pageLink = (item) => {
|
|
19
|
+
const base = (document.querySelector('base')?.getAttribute('href') ?? '/umbraco/').replace(
|
|
20
|
+
/\/$/,
|
|
21
|
+
'',
|
|
22
|
+
)
|
|
23
|
+
return `${base}/section/content/workspace/document/edit/${item.nodeKey}/${encodeURIComponent(item.variant ?? 'invariant')}/view/workflow`
|
|
24
|
+
}
|
|
25
|
+
|
|
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 {
|
|
38
|
+
static properties = {
|
|
39
|
+
_items: { state: true },
|
|
40
|
+
_message: { state: true },
|
|
41
|
+
_total: { state: true },
|
|
42
|
+
_skip: { state: true },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Overridden: what this list is, and how it loads. */
|
|
46
|
+
get headline() {
|
|
47
|
+
return ''
|
|
48
|
+
}
|
|
49
|
+
get empty() {
|
|
50
|
+
return ''
|
|
51
|
+
}
|
|
52
|
+
async fetch(_skip) {
|
|
53
|
+
return { ok: true, items: [] }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
connectedCallback() {
|
|
57
|
+
super.connectedCallback()
|
|
58
|
+
this._skip = 0
|
|
59
|
+
this.#load()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async #load() {
|
|
63
|
+
const result = await this.fetch(this._skip)
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
this._message = result.message
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
this._items = result.items
|
|
69
|
+
this._total = result.total
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
render() {
|
|
73
|
+
if (this._message) return html`<uui-box headline=${this.headline}>${this._message}</uui-box>`
|
|
74
|
+
if (!this._items)
|
|
75
|
+
return html`<uui-box headline=${this.headline}><uui-loader></uui-loader></uui-box>`
|
|
76
|
+
return html`<uui-box headline=${this.headline}>
|
|
77
|
+
${
|
|
78
|
+
this._items.length === 0
|
|
79
|
+
? html`<p>${this.empty}</p>`
|
|
80
|
+
: html`<uui-table>
|
|
81
|
+
<uui-table-head>
|
|
82
|
+
<uui-table-head-cell>Page</uui-table-head-cell>
|
|
83
|
+
<uui-table-head-cell>Workflow</uui-table-head-cell>
|
|
84
|
+
<uui-table-head-cell>Asked for</uui-table-head-cell>
|
|
85
|
+
<uui-table-head-cell>Where it stands</uui-table-head-cell>
|
|
86
|
+
<uui-table-head-cell>Sent</uui-table-head-cell>
|
|
87
|
+
</uui-table-head>
|
|
88
|
+
${this._items.map((item) => {
|
|
89
|
+
const [color, label] = STATE[item.state] ?? ['default', item.state]
|
|
90
|
+
const waiting = item.stages.filter((s) => s.state === 'waiting').map((s) => s.alias)
|
|
91
|
+
return html`<uui-table-row>
|
|
92
|
+
<uui-table-cell><a href=${pageLink(item)} @click=${(e) => openInBackoffice(e, pageLink(item))}>${item.nodeName}</a></uui-table-cell>
|
|
93
|
+
<uui-table-cell>${item.workflow.name}</uui-table-cell>
|
|
94
|
+
<uui-table-cell>${item.action === 'unpublish' ? 'Taking it down' : 'Publishing'}</uui-table-cell>
|
|
95
|
+
<uui-table-cell>
|
|
96
|
+
<uui-tag color=${color}>${label}</uui-tag>
|
|
97
|
+
${waiting.length > 0 ? html`<small>at ${waiting.join(', ')}</small>` : nothing}
|
|
98
|
+
</uui-table-cell>
|
|
99
|
+
<uui-table-cell>${new Date(item.requestedAt).toLocaleString()}</uui-table-cell>
|
|
100
|
+
</uui-table-row>`
|
|
101
|
+
})}
|
|
102
|
+
</uui-table>`
|
|
103
|
+
}
|
|
104
|
+
${
|
|
105
|
+
this._total !== undefined && this._total > this._items.length + this._skip
|
|
106
|
+
? html`<uui-button look="secondary" label="Older" @click=${() => {
|
|
107
|
+
this._skip += this._items.length
|
|
108
|
+
this.#load()
|
|
109
|
+
}}>Older</uui-button>`
|
|
110
|
+
: nothing
|
|
111
|
+
}
|
|
112
|
+
</uui-box>`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static styles = css`
|
|
116
|
+
:host { display: block; padding: var(--uui-size-layout-1); }
|
|
117
|
+
small { margin-left: var(--uui-size-space-2); opacity: 0.8; }
|
|
118
|
+
`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
customElements.define(
|
|
122
|
+
'crombolo-workflow-waiting',
|
|
123
|
+
class extends CromboloWorkflowListElement {
|
|
124
|
+
get headline() {
|
|
125
|
+
return 'Waiting for my approval'
|
|
126
|
+
}
|
|
127
|
+
get empty() {
|
|
128
|
+
return 'Nothing is waiting for your approval.'
|
|
129
|
+
}
|
|
130
|
+
fetch() {
|
|
131
|
+
return waitingForMe()
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
customElements.define(
|
|
137
|
+
'crombolo-workflow-in-flight',
|
|
138
|
+
class extends CromboloWorkflowListElement {
|
|
139
|
+
get headline() {
|
|
140
|
+
return 'In flight'
|
|
141
|
+
}
|
|
142
|
+
get empty() {
|
|
143
|
+
return 'No page is waiting for approval.'
|
|
144
|
+
}
|
|
145
|
+
fetch() {
|
|
146
|
+
return inFlight()
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
customElements.define(
|
|
152
|
+
'crombolo-workflow-history',
|
|
153
|
+
class extends CromboloWorkflowListElement {
|
|
154
|
+
get headline() {
|
|
155
|
+
return 'Decided'
|
|
156
|
+
}
|
|
157
|
+
get empty() {
|
|
158
|
+
return 'No request has been decided yet.'
|
|
159
|
+
}
|
|
160
|
+
fetch(skip) {
|
|
161
|
+
return finished(skip, 25)
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A page's Workflow tab (story 028): which workflow covers it, where a request
|
|
3
|
+
* stands in its graph, what the reader may do about it, and its history.
|
|
4
|
+
*
|
|
5
|
+
* Sending for approval sends what is saved: the editor saves first, as they
|
|
6
|
+
* would before publishing.
|
|
7
|
+
*/
|
|
8
|
+
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'
|
|
9
|
+
import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'
|
|
10
|
+
import { css, html, LitElement, nothing, unsafeCSS } from '@umbraco-cms/backoffice/external/lit'
|
|
11
|
+
import { decide, pageApproval, sendForApproval } from './workflow-client.js'
|
|
12
|
+
import { drawGraph, GRAPH_STYLES } from './workflow-graph.js'
|
|
13
|
+
|
|
14
|
+
const KIND = {
|
|
15
|
+
sent: 'Sent for approval',
|
|
16
|
+
approved: 'Approved',
|
|
17
|
+
'sent-back': 'Sent back',
|
|
18
|
+
withdrawn: 'Withdrawn',
|
|
19
|
+
edited: 'Changed while waiting',
|
|
20
|
+
restarted: 'Started again',
|
|
21
|
+
published: 'Published',
|
|
22
|
+
unpublished: 'Taken down',
|
|
23
|
+
}
|
|
24
|
+
const STATE = {
|
|
25
|
+
waiting: 'Waiting for approval',
|
|
26
|
+
approved: 'Approved',
|
|
27
|
+
'sent-back': 'Sent back',
|
|
28
|
+
withdrawn: 'Withdrawn',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default class CromboloWorkflowTabElement extends UmbElementMixin(LitElement) {
|
|
32
|
+
static properties = {
|
|
33
|
+
_key: { state: true },
|
|
34
|
+
_data: { state: true },
|
|
35
|
+
_message: { state: true },
|
|
36
|
+
_busy: { state: true },
|
|
37
|
+
_comment: { state: true },
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
constructor() {
|
|
41
|
+
super()
|
|
42
|
+
this._comment = ''
|
|
43
|
+
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => {
|
|
44
|
+
if (!context) return
|
|
45
|
+
this.observe(context.unique, (unique) => {
|
|
46
|
+
if (!unique || unique === this._key) return
|
|
47
|
+
this._key = unique
|
|
48
|
+
this.#load()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async #load() {
|
|
54
|
+
const result = await pageApproval(this._key)
|
|
55
|
+
this._data = result.ok ? result : undefined
|
|
56
|
+
if (!result.ok) this._message = result.message
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async #act(run) {
|
|
60
|
+
this._busy = true
|
|
61
|
+
this._message = undefined
|
|
62
|
+
const result = await run()
|
|
63
|
+
this._busy = false
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
this._message = result.message ?? 'That did not work.'
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
this._comment = ''
|
|
69
|
+
await this.#load()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
render() {
|
|
73
|
+
if (!this._data)
|
|
74
|
+
return html`<uui-box headline="Workflow">${this._message ?? html`<uui-loader></uui-loader>`}</uui-box>`
|
|
75
|
+
const { workflow, request, past, can } = this._data
|
|
76
|
+
return html`
|
|
77
|
+
<uui-box headline=${workflow ? `Workflow: ${workflow.name}` : 'Workflow'}>
|
|
78
|
+
${
|
|
79
|
+
workflow
|
|
80
|
+
? html`<p class="lead">
|
|
81
|
+
${
|
|
82
|
+
can.publishDirectly
|
|
83
|
+
? 'This page is covered by this workflow, and you may publish it directly.'
|
|
84
|
+
: 'Publishing this page goes through this workflow.'
|
|
85
|
+
}
|
|
86
|
+
</p>`
|
|
87
|
+
: html`<p class="lead">No workflow covers this page: it publishes as soon as someone who may publish does.</p>`
|
|
88
|
+
}
|
|
89
|
+
${this._message ? html`<p class="message">${this._message}</p>` : nothing}
|
|
90
|
+
${request ? this.#renderRequest(request) : workflow ? this.#renderSend(workflow, can) : nothing}
|
|
91
|
+
</uui-box>
|
|
92
|
+
${past.length > 0 ? html`<uui-box headline="Earlier requests">${past.map((r) => this.#renderPast(r))}</uui-box>` : nothing}
|
|
93
|
+
`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#renderSend(workflow, can) {
|
|
97
|
+
if (!can.send && !can.requestUnpublish) return nothing
|
|
98
|
+
return html`
|
|
99
|
+
<div class="graph-wrap">${drawGraph(workflow.stages)}</div>
|
|
100
|
+
<uui-textarea
|
|
101
|
+
label="A note for the approvers"
|
|
102
|
+
placeholder="A note for the approvers (optional)"
|
|
103
|
+
.value=${this._comment}
|
|
104
|
+
@input=${(e) => {
|
|
105
|
+
this._comment = e.target.value
|
|
106
|
+
}}
|
|
107
|
+
></uui-textarea>
|
|
108
|
+
<div class="actions">
|
|
109
|
+
${
|
|
110
|
+
can.send
|
|
111
|
+
? html`<uui-button look="primary" ?disabled=${this._busy} label="Send for approval"
|
|
112
|
+
@click=${() => this.#act(() => sendForApproval(this._key, 'publish', this._comment))}
|
|
113
|
+
>Send for approval</uui-button>`
|
|
114
|
+
: nothing
|
|
115
|
+
}
|
|
116
|
+
${
|
|
117
|
+
can.requestUnpublish
|
|
118
|
+
? html`<uui-button look="secondary" ?disabled=${this._busy} label="Request unpublish"
|
|
119
|
+
@click=${() => this.#act(() => sendForApproval(this._key, 'unpublish', this._comment))}
|
|
120
|
+
>Request unpublish</uui-button>`
|
|
121
|
+
: nothing
|
|
122
|
+
}
|
|
123
|
+
</div>
|
|
124
|
+
`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#renderRequest(request) {
|
|
128
|
+
const states = Object.fromEntries(request.stages.map((s) => [s.alias, s.state]))
|
|
129
|
+
const progress = request.stages
|
|
130
|
+
.filter((s) => s.state === 'waiting')
|
|
131
|
+
.map((s) => `${s.alias}: ${s.approvals} of ${s.needed}`)
|
|
132
|
+
.join(' · ')
|
|
133
|
+
return html`
|
|
134
|
+
<p>
|
|
135
|
+
<uui-tag color="warning">${STATE[request.state]}</uui-tag>
|
|
136
|
+
${request.action === 'unpublish' ? 'To take this page down.' : 'To publish this page.'}
|
|
137
|
+
${progress ? html`<small>${progress}</small>` : nothing}
|
|
138
|
+
</p>
|
|
139
|
+
<div class="graph-wrap">${drawGraph(request.workflow.stages, { states })}</div>
|
|
140
|
+
${
|
|
141
|
+
request.can.approve.length > 0 || request.can.sendBack.length > 0 || request.can.withdraw
|
|
142
|
+
? html`
|
|
143
|
+
<uui-textarea
|
|
144
|
+
label="Comment"
|
|
145
|
+
placeholder=${request.workflow.comments === 'optional' ? 'A comment (optional)' : 'A comment'}
|
|
146
|
+
.value=${this._comment}
|
|
147
|
+
@input=${(e) => {
|
|
148
|
+
this._comment = e.target.value
|
|
149
|
+
}}
|
|
150
|
+
></uui-textarea>
|
|
151
|
+
<div class="actions">
|
|
152
|
+
${request.can.approve.map(
|
|
153
|
+
(
|
|
154
|
+
stage,
|
|
155
|
+
) => html`<uui-button look="primary" color="positive" ?disabled=${this._busy}
|
|
156
|
+
label="Approve"
|
|
157
|
+
@click=${() => this.#act(() => decide(request.key, 'approve', { stage, comment: this._comment }))}
|
|
158
|
+
>Approve${request.can.approve.length > 1 ? ` (${stage})` : ''}</uui-button>`,
|
|
159
|
+
)}
|
|
160
|
+
${request.can.sendBack.map(
|
|
161
|
+
(
|
|
162
|
+
stage,
|
|
163
|
+
) => html`<uui-button look="secondary" color="danger" ?disabled=${this._busy}
|
|
164
|
+
label="Send back"
|
|
165
|
+
@click=${() => this.#act(() => decide(request.key, 'send-back', { stage, comment: this._comment }))}
|
|
166
|
+
>Send back${request.can.sendBack.length > 1 ? ` (${stage})` : ''}</uui-button>`,
|
|
167
|
+
)}
|
|
168
|
+
${
|
|
169
|
+
request.can.withdraw
|
|
170
|
+
? html`<uui-button look="secondary" ?disabled=${this._busy} label="Withdraw"
|
|
171
|
+
@click=${() => this.#act(() => decide(request.key, 'withdraw', {}))}
|
|
172
|
+
>Withdraw</uui-button>`
|
|
173
|
+
: nothing
|
|
174
|
+
}
|
|
175
|
+
</div>
|
|
176
|
+
`
|
|
177
|
+
: nothing
|
|
178
|
+
}
|
|
179
|
+
${this.#renderHistory(request)}
|
|
180
|
+
`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#renderPast(request) {
|
|
184
|
+
return html`<details>
|
|
185
|
+
<summary>${STATE[request.state]} — ${new Date(request.requestedAt).toLocaleString()}</summary>
|
|
186
|
+
${this.#renderHistory(request)}
|
|
187
|
+
</details>`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#renderHistory(request) {
|
|
191
|
+
return html`<ol class="history">
|
|
192
|
+
${request.history.map(
|
|
193
|
+
(h) => html`<li>
|
|
194
|
+
<strong>${KIND[h.kind] ?? h.kind}</strong>
|
|
195
|
+
${h.stage ? html`<span>at ${h.stage}</span>` : nothing}
|
|
196
|
+
<small>${new Date(h.at).toLocaleString()}</small>
|
|
197
|
+
${h.comment ? html`<blockquote>${h.comment}</blockquote>` : nothing}
|
|
198
|
+
</li>`,
|
|
199
|
+
)}
|
|
200
|
+
</ol>`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static styles = css`
|
|
204
|
+
:host { display: block; padding: var(--uui-size-layout-1); }
|
|
205
|
+
uui-box + uui-box { margin-top: var(--uui-size-space-5); }
|
|
206
|
+
.lead { margin-top: 0; }
|
|
207
|
+
.message { color: var(--uui-color-danger); }
|
|
208
|
+
.graph-wrap { overflow-x: auto; margin: var(--uui-size-space-4) 0; }
|
|
209
|
+
.actions { display: flex; gap: var(--uui-size-space-3); margin: var(--uui-size-space-3) 0; }
|
|
210
|
+
uui-textarea { width: 100%; }
|
|
211
|
+
.history { padding-left: var(--uui-size-space-5); }
|
|
212
|
+
.history li { margin-bottom: var(--uui-size-space-2); }
|
|
213
|
+
.history small { opacity: 0.7; margin-left: var(--uui-size-space-2); }
|
|
214
|
+
blockquote { margin: var(--uui-size-space-1) 0 0; padding-left: var(--uui-size-space-3); border-left: 3px solid var(--uui-color-border); }
|
|
215
|
+
${unsafeCSS(GRAPH_STYLES)}
|
|
216
|
+
`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
customElements.define('crombolo-workflow-tab', CromboloWorkflowTabElement)
|