@crombolo/backoffice-host 0.11.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/plugin/addresses-dashboard.js +100 -0
- package/plugin/assistant/umbraco-package.json +1 -1
- package/plugin/bundles-installed.js +22 -9
- package/plugin/bundles-marketplace.js +12 -2
- package/plugin/changes-dashboard.js +245 -8
- package/plugin/forms-client.js +7 -0
- package/plugin/forms-entries.js +2 -1
- package/plugin/forms-layout.js +128 -0
- package/plugin/forms-overview.js +484 -134
- package/plugin/localizations/umbraco-package.json +1 -1
- package/plugin/report-client.js +21 -0
- package/plugin/seo/umbraco-package.json +1 -1
- package/plugin/tsx-editors.js +5 -5
- package/plugin/umbraco-package.json +142 -1
- package/plugin/webhook-storage.js +107 -0
- package/plugin/welcome-dashboard.js +1 -1
- package/plugin/workflow-client.js +40 -0
- package/plugin/workflow-designer.js +529 -0
- package/plugin/workflow-graph.js +132 -0
- package/plugin/workflow-requests.js +151 -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.1",
|
|
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.1",
|
|
35
35
|
"@crombolo/backoffice-dist": "18.2.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page addresses: whether the top-level page's name is part of every address
|
|
3
|
+
* below it, as Umbraco's `HideTopLevelNodeFromPath` sets it. Saving moves the
|
|
4
|
+
* addresses, and the old ones redirect to the new. docs/rendering.md, "URLs".
|
|
5
|
+
*/
|
|
6
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
7
|
+
|
|
8
|
+
const endpoint = () =>
|
|
9
|
+
`${(document.querySelector('base')?.getAttribute('href') ?? '/umbraco/').replace(/\/$/, '')}/crombolo/api/url-options`
|
|
10
|
+
|
|
11
|
+
export default class CromboloAddressesDashboardElement extends LitElement {
|
|
12
|
+
static properties = {
|
|
13
|
+
_settings: { state: true },
|
|
14
|
+
_showTopLevel: { state: true },
|
|
15
|
+
_message: { state: true },
|
|
16
|
+
_busy: { state: true },
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor() {
|
|
20
|
+
super()
|
|
21
|
+
this._settings = undefined
|
|
22
|
+
this._showTopLevel = false
|
|
23
|
+
this._message = undefined
|
|
24
|
+
this._busy = false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
connectedCallback() {
|
|
28
|
+
super.connectedCallback()
|
|
29
|
+
this.#load()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async #load() {
|
|
33
|
+
const response = await fetch(endpoint(), {
|
|
34
|
+
credentials: 'include',
|
|
35
|
+
headers: { accept: 'application/json' },
|
|
36
|
+
})
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
this._message = 'The address settings could not be read.'
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
this._settings = await response.json()
|
|
42
|
+
this._showTopLevel = !this._settings.hideTopLevel
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async #save() {
|
|
46
|
+
this._busy = true
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch(endpoint(), {
|
|
49
|
+
method: 'PUT',
|
|
50
|
+
credentials: 'include',
|
|
51
|
+
headers: { accept: 'application/json', 'content-type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ hideTopLevel: !this._showTopLevel }),
|
|
53
|
+
})
|
|
54
|
+
const result = await response.json().catch(() => ({}))
|
|
55
|
+
this._message = response.ok
|
|
56
|
+
? 'Saved. Pages that moved still answer on their old addresses, which now redirect.'
|
|
57
|
+
: (result.message ?? 'The setting was not saved.')
|
|
58
|
+
if (response.ok) this._settings = result
|
|
59
|
+
} finally {
|
|
60
|
+
this._busy = false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
render() {
|
|
65
|
+
if (!this._settings)
|
|
66
|
+
return html`<uui-box headline="Page addresses">${this._message ?? html`<uui-loader></uui-loader>`}</uui-box>`
|
|
67
|
+
const changed = this._showTopLevel === this._settings.hideTopLevel
|
|
68
|
+
return html`
|
|
69
|
+
<uui-box headline="Page addresses">
|
|
70
|
+
<p>
|
|
71
|
+
Normally the first top-level page is the home page at <code>/</code>, and the top-level
|
|
72
|
+
page's name is left out of the addresses of the pages below it.
|
|
73
|
+
</p>
|
|
74
|
+
<uui-toggle
|
|
75
|
+
label="Include the top-level page in addresses"
|
|
76
|
+
.checked=${this._showTopLevel}
|
|
77
|
+
?disabled=${!this._settings.editable || this._busy}
|
|
78
|
+
@change=${(e) => (this._showTopLevel = e.target.checked)}
|
|
79
|
+
>Include the top-level page in addresses</uui-toggle>
|
|
80
|
+
<p class="example">
|
|
81
|
+
A page called News under a top-level page called Home is at
|
|
82
|
+
<code>${this._showTopLevel ? '/home/news/' : '/news/'}</code>.
|
|
83
|
+
</p>
|
|
84
|
+
${
|
|
85
|
+
this._settings.editable
|
|
86
|
+
? html`<uui-button look="primary" label="Save" ?disabled=${!changed || this._busy} @click=${() => this.#save()}>Save</uui-button>`
|
|
87
|
+
: html`<p class="note">This site keeps this setting with its files, so it is changed there rather than here.</p>`
|
|
88
|
+
}
|
|
89
|
+
${this._message ? html`<p>${this._message}</p>` : nothing}
|
|
90
|
+
</uui-box>
|
|
91
|
+
`
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
static styles = css`
|
|
95
|
+
:host { display: block; padding: var(--uui-size-layout-1); }
|
|
96
|
+
.example, .note { color: var(--uui-color-text-alt); }
|
|
97
|
+
`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
customElements.define('crombolo-addresses-dashboard', CromboloAddressesDashboardElement)
|
|
@@ -40,6 +40,7 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
40
40
|
_loading: { state: true },
|
|
41
41
|
_busy: { state: true },
|
|
42
42
|
_notice: { state: true },
|
|
43
|
+
_installs: { state: true },
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
constructor() {
|
|
@@ -48,6 +49,7 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
48
49
|
this._loading = true
|
|
49
50
|
this._busy = ''
|
|
50
51
|
this._notice = ''
|
|
52
|
+
this._installs = true
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
connectedCallback() {
|
|
@@ -58,7 +60,9 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
58
60
|
async #load() {
|
|
59
61
|
this._loading = true
|
|
60
62
|
try {
|
|
61
|
-
|
|
63
|
+
const result = await listInstalled()
|
|
64
|
+
this._items = result.items ?? []
|
|
65
|
+
this._installs = result.installs !== false
|
|
62
66
|
} catch (error) {
|
|
63
67
|
this._notice = error instanceof Error ? error.message : String(error)
|
|
64
68
|
} finally {
|
|
@@ -117,20 +121,29 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
117
121
|
<uui-table-cell class="carries">${carriesTags(item.carries)}</uui-table-cell>
|
|
118
122
|
<uui-table-cell class="status">${status(item)}</uui-table-cell>
|
|
119
123
|
<uui-table-cell>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
+
}
|
|
126
134
|
</uui-table-cell>
|
|
127
135
|
</uui-table-row>
|
|
128
136
|
`,
|
|
129
137
|
)}
|
|
130
138
|
</uui-table>
|
|
131
139
|
<p class="note">
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
}
|
|
134
147
|
</p>
|
|
135
148
|
</uui-box>
|
|
136
149
|
`
|
|
@@ -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">Packages 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"
|
|
@@ -9,19 +9,37 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Below them, each installed bundle that keeps data of its own lists the
|
|
11
11
|
* changes it has made to its storage here, and any still to make.
|
|
12
|
+
*
|
|
13
|
+
* A content bundle waiting to be imported can be answered and imported here,
|
|
14
|
+
* by those allowed to install packages: each finding offers the answers its
|
|
15
|
+
* kind allows, and the import runs once nothing is outstanding.
|
|
12
16
|
*/
|
|
13
17
|
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
14
|
-
import { fetchReport } from './report-client.js'
|
|
18
|
+
import { answersUrl, fetchReport, transferAction } from './report-client.js'
|
|
15
19
|
|
|
16
20
|
const KIND_LABEL = { blocking: 'Blocking', person: 'Needs a person', auto: 'Automatic' }
|
|
17
21
|
const KIND_COLOR = { blocking: 'danger', person: 'warning', auto: 'positive' }
|
|
18
22
|
const SOURCE_LABEL = { upgrade: 'Schema and upgrade', transfer: 'Content transfer' }
|
|
23
|
+
const CHOICE_LABEL = {
|
|
24
|
+
'take-bundle': 'Use the incoming version',
|
|
25
|
+
'keep-local': 'Keep this site’s version',
|
|
26
|
+
skip: 'Leave this item out',
|
|
27
|
+
}
|
|
28
|
+
const ANSWERED_LABEL = {
|
|
29
|
+
'take-bundle': 'uses the incoming version',
|
|
30
|
+
'keep-local': 'keeps this site’s version',
|
|
31
|
+
skip: 'is left out',
|
|
32
|
+
}
|
|
19
33
|
|
|
20
34
|
export default class CromboloChangesDashboardElement extends LitElement {
|
|
21
35
|
static properties = {
|
|
22
36
|
_report: { state: true },
|
|
23
37
|
_error: { state: true },
|
|
24
38
|
_showResolved: { state: true },
|
|
39
|
+
_busy: { state: true },
|
|
40
|
+
_outcome: { state: true },
|
|
41
|
+
_backupTaken: { state: true },
|
|
42
|
+
_passphrase: { state: true },
|
|
25
43
|
}
|
|
26
44
|
|
|
27
45
|
constructor() {
|
|
@@ -29,6 +47,28 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
29
47
|
this._report = undefined
|
|
30
48
|
this._error = undefined
|
|
31
49
|
this._showResolved = false
|
|
50
|
+
this._busy = undefined
|
|
51
|
+
this._outcome = {}
|
|
52
|
+
this._backupTaken = {}
|
|
53
|
+
// Held in this page only, sent with each request, never stored.
|
|
54
|
+
this._passphrase = {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async #act(bundleId, verb, body) {
|
|
58
|
+
this._busy = bundleId
|
|
59
|
+
const label = (this._report?.transfers ?? []).find((t) => t.bundleId === bundleId)?.label
|
|
60
|
+
try {
|
|
61
|
+
const passphrase = this._passphrase[bundleId]
|
|
62
|
+
const result = await transferAction(
|
|
63
|
+
bundleId,
|
|
64
|
+
verb,
|
|
65
|
+
passphrase ? { ...body, passphrase } : body,
|
|
66
|
+
)
|
|
67
|
+
this._outcome = { ...this._outcome, [bundleId]: { verb, label, ...result } }
|
|
68
|
+
} finally {
|
|
69
|
+
this._busy = undefined
|
|
70
|
+
await this.#load()
|
|
71
|
+
}
|
|
32
72
|
}
|
|
33
73
|
|
|
34
74
|
connectedCallback() {
|
|
@@ -45,8 +85,15 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
45
85
|
}
|
|
46
86
|
}
|
|
47
87
|
|
|
48
|
-
#group(findings) {
|
|
88
|
+
#group(findings, transfers = []) {
|
|
49
89
|
const groups = new Map()
|
|
90
|
+
// A bundle with nothing to answer is still waiting to be imported.
|
|
91
|
+
for (const pending of transfers)
|
|
92
|
+
groups.set(`transfer|${pending.bundleId}`, {
|
|
93
|
+
source: 'transfer',
|
|
94
|
+
scope: pending.bundleId,
|
|
95
|
+
findings: [],
|
|
96
|
+
})
|
|
50
97
|
for (const f of findings) {
|
|
51
98
|
const source = f.source ?? 'upgrade'
|
|
52
99
|
const key = `${source}|${f.scope ?? ''}`
|
|
@@ -60,8 +107,9 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
60
107
|
render() {
|
|
61
108
|
if (this._error) return html`<uui-box headline="Changes"><p>${this._error}</p></uui-box>`
|
|
62
109
|
if (!this._report) return html`<uui-box headline="Changes"><uui-loader></uui-loader></uui-box>`
|
|
63
|
-
const { health, findings, counts, bundles = [] } = this._report
|
|
110
|
+
const { health, findings, counts, bundles = [], upgrades, transfers = [] } = this._report
|
|
64
111
|
const shown = findings.filter((f) => this._showResolved || f.status === 'open')
|
|
112
|
+
const groups = this.#group(shown, transfers)
|
|
65
113
|
return html`
|
|
66
114
|
<uui-box headline="Changes">
|
|
67
115
|
<p>
|
|
@@ -72,6 +120,14 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
72
120
|
: html`<uui-tag color="positive">current</uui-tag>`
|
|
73
121
|
}
|
|
74
122
|
</p>
|
|
123
|
+
${upgrades ? html`<p class="release">${this.#describeUpgrades(upgrades)}</p>` : nothing}
|
|
124
|
+
${Object.entries(this._outcome)
|
|
125
|
+
.filter(
|
|
126
|
+
([id, o]) => o.verb === 'import' && o.ok && !transfers.some((t) => t.bundleId === id),
|
|
127
|
+
)
|
|
128
|
+
.map(
|
|
129
|
+
([id, o]) => html`<h4>${o.label ?? id}</h4>${this.#renderOutcome({ bundleId: id }, o)}`,
|
|
130
|
+
)}
|
|
75
131
|
<div class="counts">
|
|
76
132
|
${Object.entries(KIND_LABEL).map(
|
|
77
133
|
([kind, label]) =>
|
|
@@ -85,15 +141,27 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
85
141
|
></uui-toggle>
|
|
86
142
|
</div>
|
|
87
143
|
${
|
|
88
|
-
|
|
144
|
+
groups.length === 0
|
|
89
145
|
? html`<p class="empty">Nothing outstanding. <code>crombolo upgrade check</code> and <code>crombolo content check</code> write findings here.</p>`
|
|
90
|
-
:
|
|
146
|
+
: groups.map((group) => this.#renderGroup(group))
|
|
91
147
|
}
|
|
92
148
|
${bundles.map((bundle) => this.#renderBundle(bundle))}
|
|
93
149
|
</uui-box>
|
|
94
150
|
`
|
|
95
151
|
}
|
|
96
152
|
|
|
153
|
+
#describeUpgrades({ version, installedAt, lastUpgrade }) {
|
|
154
|
+
const day = (at) => at?.slice(0, 10) ?? ''
|
|
155
|
+
const parts = [`Crombolo ${version}`]
|
|
156
|
+
if (installedAt) parts.push(`installed ${day(installedAt)}`)
|
|
157
|
+
if (lastUpgrade)
|
|
158
|
+
parts.push(
|
|
159
|
+
`last upgraded ${day(lastUpgrade.at)}${lastUpgrade.release ? ` to ${lastUpgrade.release}` : ''}, ` +
|
|
160
|
+
`${lastUpgrade.steps} storage change${lastUpgrade.steps === 1 ? '' : 's'}`,
|
|
161
|
+
)
|
|
162
|
+
return parts.join(' · ')
|
|
163
|
+
}
|
|
164
|
+
|
|
97
165
|
#renderBundle(bundle) {
|
|
98
166
|
const steps = [
|
|
99
167
|
...bundle.pending.map((step) => ({ ...step, done: false })),
|
|
@@ -128,16 +196,25 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
128
196
|
}
|
|
129
197
|
|
|
130
198
|
#renderGroup(group) {
|
|
199
|
+
const pending =
|
|
200
|
+
group.source === 'transfer'
|
|
201
|
+
? (this._report.transfers ?? []).find((t) => t.bundleId === group.scope)
|
|
202
|
+
: undefined
|
|
203
|
+
const answering = Boolean(pending && this._report.mayImport)
|
|
131
204
|
return html`
|
|
132
205
|
<h4>
|
|
133
206
|
${SOURCE_LABEL[group.source] ?? group.source}
|
|
134
|
-
${group.scope ? html`<code>${group.scope}</code>` : nothing}
|
|
207
|
+
${pending?.label ? html`<strong>${pending.label}</strong>` : group.scope ? html`<code>${group.scope}</code>` : nothing}
|
|
135
208
|
</h4>
|
|
136
|
-
|
|
209
|
+
${
|
|
210
|
+
group.findings.length === 0
|
|
211
|
+
? nothing
|
|
212
|
+
: html`<uui-table>
|
|
137
213
|
<uui-table-head>
|
|
138
214
|
<uui-table-head-cell>Kind</uui-table-head-cell>
|
|
139
215
|
<uui-table-head-cell>What</uui-table-head-cell>
|
|
140
216
|
<uui-table-head-cell>Where</uui-table-head-cell>
|
|
217
|
+
${answering ? html`<uui-table-head-cell>Answer</uui-table-head-cell>` : nothing}
|
|
141
218
|
</uui-table-head>
|
|
142
219
|
${group.findings.map(
|
|
143
220
|
(f) => html`<uui-table-row class=${f.status}>
|
|
@@ -148,12 +225,167 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
148
225
|
${f.propertyAlias ? html`<code>${f.propertyAlias}</code>` : nothing}
|
|
149
226
|
${f.culture ? html`<small>${f.culture}</small>` : nothing}
|
|
150
227
|
</uui-table-cell>
|
|
228
|
+
${answering ? html`<uui-table-cell>${this.#renderChoices(pending, f)}</uui-table-cell>` : nothing}
|
|
151
229
|
</uui-table-row>`,
|
|
152
230
|
)}
|
|
153
|
-
</uui-table
|
|
231
|
+
</uui-table>`
|
|
232
|
+
}
|
|
233
|
+
${answering ? this.#renderPending(pending, group.findings) : nothing}
|
|
154
234
|
`
|
|
155
235
|
}
|
|
156
236
|
|
|
237
|
+
#renderChoices(pending, finding) {
|
|
238
|
+
if (finding.status !== 'open' || !finding.choices?.length) return nothing
|
|
239
|
+
return html`<div class="choices">
|
|
240
|
+
${finding.choices.map(
|
|
241
|
+
(choice) => html`<uui-button
|
|
242
|
+
look="secondary"
|
|
243
|
+
compact
|
|
244
|
+
?disabled=${this._busy === pending.bundleId}
|
|
245
|
+
label=${CHOICE_LABEL[choice.resolution]}
|
|
246
|
+
@click=${() =>
|
|
247
|
+
this.#act(pending.bundleId, 'answer', {
|
|
248
|
+
nodeKey: finding.subjectKey,
|
|
249
|
+
handle: choice.handle,
|
|
250
|
+
resolution: choice.resolution,
|
|
251
|
+
})}
|
|
252
|
+
>${CHOICE_LABEL[choice.resolution]}</uui-button>`,
|
|
253
|
+
)}
|
|
254
|
+
</div>`
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#describeAnswer(answer) {
|
|
258
|
+
const named = (this._report.findings ?? []).find((f) => f.subjectKey === answer.nodeKey)
|
|
259
|
+
const name = named?.subjectName ?? 'An item'
|
|
260
|
+
if (answer.handle === '*') return `${name} ${ANSWERED_LABEL[answer.resolution] ?? ''}`
|
|
261
|
+
const [field, culture] = answer.handle.split('@')
|
|
262
|
+
return `${name}: its ${field}${culture ? ` (${culture})` : ''} ${ANSWERED_LABEL[answer.resolution] ?? ''}`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#renderPending(pending, findings) {
|
|
266
|
+
const open = findings.filter((f) => f.status === 'open')
|
|
267
|
+
const waiting = open.filter((f) => f.kind === 'blocking' || f.kind === 'person')
|
|
268
|
+
const blocking = waiting.filter((f) => f.kind === 'blocking').length
|
|
269
|
+
const missingFiles = pending.allowMissingBlobs || open.some((f) => f.code === 'missing-blob')
|
|
270
|
+
const busy = this._busy === pending.bundleId
|
|
271
|
+
const outcome = this._outcome[pending.bundleId]
|
|
272
|
+
const encrypted =
|
|
273
|
+
open.some((f) => f.code === 'content-sealed') ||
|
|
274
|
+
outcome?.needsPassphrase === true ||
|
|
275
|
+
this._passphrase[pending.bundleId] !== undefined
|
|
276
|
+
return html`<div class="pending">
|
|
277
|
+
${
|
|
278
|
+
encrypted
|
|
279
|
+
? html`<p class="passphrase">
|
|
280
|
+
<span>This content is encrypted. Enter the passphrase it was exported with.</span>
|
|
281
|
+
<uui-input
|
|
282
|
+
type="password"
|
|
283
|
+
label="Passphrase"
|
|
284
|
+
autocomplete="off"
|
|
285
|
+
.value=${this._passphrase[pending.bundleId] ?? ''}
|
|
286
|
+
@input=${(e) => {
|
|
287
|
+
this._passphrase = { ...this._passphrase, [pending.bundleId]: e.target.value }
|
|
288
|
+
}}
|
|
289
|
+
></uui-input>
|
|
290
|
+
<uui-button
|
|
291
|
+
look="secondary"
|
|
292
|
+
?disabled=${busy || !this._passphrase[pending.bundleId]}
|
|
293
|
+
label="Open"
|
|
294
|
+
@click=${() => this.#act(pending.bundleId, 'check')}
|
|
295
|
+
>Open</uui-button>
|
|
296
|
+
</p>`
|
|
297
|
+
: nothing
|
|
298
|
+
}
|
|
299
|
+
${
|
|
300
|
+
pending.answers.length > 0
|
|
301
|
+
? html`<p><strong>Answers given</strong></p>
|
|
302
|
+
<ul>
|
|
303
|
+
${pending.answers.map(
|
|
304
|
+
(answer) => html`<li>
|
|
305
|
+
${this.#describeAnswer(answer)}
|
|
306
|
+
<uui-button
|
|
307
|
+
look="secondary"
|
|
308
|
+
compact
|
|
309
|
+
?disabled=${busy}
|
|
310
|
+
label="Change"
|
|
311
|
+
@click=${() =>
|
|
312
|
+
this.#act(pending.bundleId, 'answer', {
|
|
313
|
+
nodeKey: answer.nodeKey,
|
|
314
|
+
handle: answer.handle,
|
|
315
|
+
resolution: null,
|
|
316
|
+
})}
|
|
317
|
+
>Change</uui-button>
|
|
318
|
+
</li>`,
|
|
319
|
+
)}
|
|
320
|
+
</ul>`
|
|
321
|
+
: nothing
|
|
322
|
+
}
|
|
323
|
+
${
|
|
324
|
+
missingFiles
|
|
325
|
+
? html`<uui-toggle
|
|
326
|
+
label="Import even where a media file is missing"
|
|
327
|
+
.checked=${pending.allowMissingBlobs}
|
|
328
|
+
?disabled=${busy}
|
|
329
|
+
@change=${(e) => this.#act(pending.bundleId, 'missing-files', { allow: e.target.checked })}
|
|
330
|
+
></uui-toggle>`
|
|
331
|
+
: nothing
|
|
332
|
+
}
|
|
333
|
+
<p class="state">
|
|
334
|
+
${
|
|
335
|
+
waiting.length === 0
|
|
336
|
+
? html`<uui-tag color="positive">Ready to import</uui-tag>`
|
|
337
|
+
: blocking > 0
|
|
338
|
+
? html`<uui-tag color="danger">${blocking} blocking: these need a change to this site before the import can run</uui-tag>`
|
|
339
|
+
: html`<uui-tag color="warning">${waiting.length} still to answer</uui-tag>`
|
|
340
|
+
}
|
|
341
|
+
${pending.publish ? html`<small>Pages live where the content came from are published too.</small>` : nothing}
|
|
342
|
+
</p>
|
|
343
|
+
<div class="actions">
|
|
344
|
+
<uui-button
|
|
345
|
+
look="primary"
|
|
346
|
+
?disabled=${busy || waiting.length > 0}
|
|
347
|
+
label="Import"
|
|
348
|
+
@click=${() =>
|
|
349
|
+
this.#act(pending.bundleId, 'import', {
|
|
350
|
+
backupTaken: this._backupTaken[pending.bundleId] === true,
|
|
351
|
+
})}
|
|
352
|
+
>Import</uui-button>
|
|
353
|
+
<uui-button look="secondary" ?disabled=${busy} label="Check again" @click=${() => this.#act(pending.bundleId, 'check')}>Check again</uui-button>
|
|
354
|
+
<uui-button look="secondary" href=${answersUrl(pending.bundleId)} label="Download answers">Download answers</uui-button>
|
|
355
|
+
</div>
|
|
356
|
+
${outcome ? this.#renderOutcome(pending, outcome) : nothing}
|
|
357
|
+
</div>`
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
#renderOutcome(pending, outcome) {
|
|
361
|
+
if (outcome.verb === 'import' && outcome.ok) {
|
|
362
|
+
const c = outcome.counts ?? {}
|
|
363
|
+
return html`<p class="outcome">
|
|
364
|
+
<uui-tag color="positive">Imported</uui-tag>
|
|
365
|
+
${c.create ?? 0} new, ${c.update ?? 0} updated, ${c.unchanged ?? 0} unchanged, ${c.skip ?? 0} left out${
|
|
366
|
+
outcome.published?.length ? html`; ${outcome.published.length} published` : nothing
|
|
367
|
+
}.
|
|
368
|
+
${
|
|
369
|
+
outcome.publishFailures?.length
|
|
370
|
+
? html`<ul>${outcome.publishFailures.map((f) => html`<li>${f.name} was not published: ${f.reason}</li>`)}</ul>`
|
|
371
|
+
: nothing
|
|
372
|
+
}
|
|
373
|
+
</p>`
|
|
374
|
+
}
|
|
375
|
+
if (outcome.needsBackup)
|
|
376
|
+
return html`<p class="outcome">
|
|
377
|
+
${outcome.message}
|
|
378
|
+
<uui-checkbox
|
|
379
|
+
label="A backup of this site has been taken"
|
|
380
|
+
.checked=${this._backupTaken[pending.bundleId] === true}
|
|
381
|
+
@change=${(e) => (this._backupTaken = { ...this._backupTaken, [pending.bundleId]: e.target.checked })}
|
|
382
|
+
>A backup of this site has been taken</uui-checkbox>
|
|
383
|
+
</p>`
|
|
384
|
+
if (outcome.ok === false)
|
|
385
|
+
return html`<p class="outcome"><uui-tag color="danger">Not done</uui-tag> ${outcome.message ?? 'Something is still outstanding.'}</p>`
|
|
386
|
+
return nothing
|
|
387
|
+
}
|
|
388
|
+
|
|
157
389
|
static styles = css`
|
|
158
390
|
:host { display: block; padding: var(--uui-size-layout-1); }
|
|
159
391
|
.counts { display: flex; gap: var(--uui-size-space-3); align-items: center; margin-bottom: var(--uui-size-space-4); }
|
|
@@ -163,6 +395,11 @@ export default class CromboloChangesDashboardElement extends LitElement {
|
|
|
163
395
|
code { margin-left: var(--uui-size-space-2); }
|
|
164
396
|
small { margin-left: var(--uui-size-space-2); }
|
|
165
397
|
.empty { color: var(--uui-color-text-alt); }
|
|
398
|
+
.choices, .actions { display: flex; flex-wrap: wrap; gap: var(--uui-size-space-2); }
|
|
399
|
+
.pending { margin: var(--uui-size-space-3) 0 var(--uui-size-space-5); }
|
|
400
|
+
.pending ul { margin: 0 0 var(--uui-size-space-3); padding-left: var(--uui-size-space-5); }
|
|
401
|
+
.state { display: flex; gap: var(--uui-size-space-3); align-items: center; }
|
|
402
|
+
.passphrase { display: flex; flex-wrap: wrap; gap: var(--uui-size-space-3); align-items: center; }
|
|
166
403
|
`
|
|
167
404
|
}
|
|
168
405
|
|
package/plugin/forms-client.js
CHANGED
|
@@ -25,11 +25,18 @@ const call = async (path, init) => {
|
|
|
25
25
|
|
|
26
26
|
export const listForms = async () => (await call('')).body.items ?? []
|
|
27
27
|
|
|
28
|
+
/** The field and workflow types this site has, built in and from its bundles. */
|
|
29
|
+
export const listFormTypes = async () => (await call('/available-types')).body
|
|
30
|
+
|
|
28
31
|
export const readForm = async (key) => (await call(`/${encodeURIComponent(key)}`)).body
|
|
29
32
|
|
|
30
33
|
export const saveForm = (definition) =>
|
|
31
34
|
call('', { method: 'PUT', body: JSON.stringify(definition) })
|
|
32
35
|
|
|
36
|
+
/** The options a choice source offers right now, as a visitor would see them. */
|
|
37
|
+
export const previewChoices = (source) =>
|
|
38
|
+
call('/choices', { method: 'POST', body: JSON.stringify({ source }) })
|
|
39
|
+
|
|
33
40
|
export const deleteForm = (key) => call(`/${encodeURIComponent(key)}`, { method: 'DELETE' })
|
|
34
41
|
|
|
35
42
|
export const listEntries = async (key, query = {}) => {
|
package/plugin/forms-entries.js
CHANGED
|
@@ -167,7 +167,8 @@ export default class CromboloFormsEntriesElement extends UmbLitElement {
|
|
|
167
167
|
${
|
|
168
168
|
value.redacted
|
|
169
169
|
? html`<em>withheld — needs sensitive-data access</em>`
|
|
170
|
-
: value.values.join(', ') ||
|
|
170
|
+
: (value.labels ?? value.values).join(', ') ||
|
|
171
|
+
html`<em>not answered</em>`
|
|
171
172
|
}
|
|
172
173
|
</dd>
|
|
173
174
|
`,
|