@crombolo/backoffice-host 0.9.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/plugin/assistant/umbraco-package.json +1 -1
- package/plugin/audit-dashboard.js +369 -0
- package/plugin/branding/components-tree.js +1 -1
- package/plugin/branding/localization-en.js +1 -1
- package/plugin/bundles-carries.js +16 -0
- package/plugin/bundles-client.js +8 -2
- package/plugin/bundles-created-overview.js +96 -2
- package/plugin/bundles-installed.js +36 -7
- package/plugin/bundles-marketplace.js +41 -4
- package/plugin/changes-dashboard.js +38 -1
- package/plugin/localizations/umbraco-package.json +1 -1
- package/plugin/seo/umbraco-package.json +1 -1
- package/plugin/umbraco-package.json +18 -1
- package/src/bundle-package.ts +266 -0
- package/src/extensions.ts +42 -81
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crombolo/backoffice-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.11.0",
|
|
35
35
|
"@crombolo/backoffice-dist": "18.2.0"
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Audit log dashboard in Settings: who did what, when and from where,
|
|
3
|
+
* newest first, filtered by person, kind of change, words and dates.
|
|
4
|
+
* docs/audit-log.md.
|
|
5
|
+
*/
|
|
6
|
+
import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
|
|
7
|
+
|
|
8
|
+
const PAGE_SIZE = 50
|
|
9
|
+
|
|
10
|
+
const CATEGORIES = [
|
|
11
|
+
['', 'Everything'],
|
|
12
|
+
['content', 'Content'],
|
|
13
|
+
['media', 'Media'],
|
|
14
|
+
['member', 'Members'],
|
|
15
|
+
['schema', 'Types and templates'],
|
|
16
|
+
['settings', 'Settings'],
|
|
17
|
+
['user', 'Users'],
|
|
18
|
+
['permission', 'Permissions'],
|
|
19
|
+
['sign-in', 'Sign-ins'],
|
|
20
|
+
['system', 'System'],
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const ACTIONS = {
|
|
24
|
+
Create: 'Created',
|
|
25
|
+
CreateAndPublish: 'Created and published',
|
|
26
|
+
Save: 'Saved',
|
|
27
|
+
SaveAndPublish: 'Saved and published',
|
|
28
|
+
Publish: 'Published',
|
|
29
|
+
PublishWithDescendants: 'Published with everything below it',
|
|
30
|
+
Unpublish: 'Unpublished',
|
|
31
|
+
Delete: 'Deleted',
|
|
32
|
+
Move: 'Moved',
|
|
33
|
+
Trash: 'Moved to the recycle bin',
|
|
34
|
+
Restore: 'Restored from the recycle bin',
|
|
35
|
+
EmptyRecycleBin: 'Emptied the recycle bin',
|
|
36
|
+
Copy: 'Copied',
|
|
37
|
+
Sort: 'Sorted',
|
|
38
|
+
Rename: 'Renamed',
|
|
39
|
+
Rollback: 'Rolled back',
|
|
40
|
+
Revert: 'Reverted',
|
|
41
|
+
PreventCleanup: 'Kept a version',
|
|
42
|
+
AssignDomains: 'Changed domains',
|
|
43
|
+
SetPublicAccess: 'Restricted access',
|
|
44
|
+
AddPublicAccess: 'Restricted access',
|
|
45
|
+
RemovePublicAccess: 'Removed access restrictions',
|
|
46
|
+
SetNotifications: 'Changed notifications',
|
|
47
|
+
Import: 'Imported',
|
|
48
|
+
CreateTemplate: 'Created a template',
|
|
49
|
+
ChangePassword: 'Changed password',
|
|
50
|
+
ResetPassword: 'Reset password',
|
|
51
|
+
ResetPasswordByLink: 'Reset password from an e-mailed link',
|
|
52
|
+
Invite: 'Invited',
|
|
53
|
+
ResendInvite: 'Resent the invitation',
|
|
54
|
+
AcceptInvite: 'Accepted the invitation',
|
|
55
|
+
Disable: 'Disabled',
|
|
56
|
+
Enable: 'Enabled',
|
|
57
|
+
Unlock: 'Unlocked',
|
|
58
|
+
SetUserGroups: 'Changed groups',
|
|
59
|
+
AddUsers: 'Added people',
|
|
60
|
+
RemoveUsers: 'Removed people',
|
|
61
|
+
SaveProfile: 'Updated their profile',
|
|
62
|
+
SetAvatar: 'Changed the picture',
|
|
63
|
+
RemoveAvatar: 'Removed the picture',
|
|
64
|
+
EnableTwoFactor: 'Turned on two-factor sign-in',
|
|
65
|
+
DisableTwoFactor: 'Turned off two-factor sign-in',
|
|
66
|
+
AddClientCredentials: 'Added an API key',
|
|
67
|
+
RemoveClientCredentials: 'Removed an API key',
|
|
68
|
+
SignIn: 'Signed in',
|
|
69
|
+
SignInFailed: 'Failed to sign in',
|
|
70
|
+
LockedOut: 'Locked out',
|
|
71
|
+
SignOut: 'Signed out',
|
|
72
|
+
Rebuild: 'Rebuilt',
|
|
73
|
+
Reload: 'Reloaded',
|
|
74
|
+
Build: 'Built',
|
|
75
|
+
Install: 'Installed',
|
|
76
|
+
Uninstall: 'Uninstalled',
|
|
77
|
+
Commit: 'Sent to source control',
|
|
78
|
+
Sync: 'Applied the schema files',
|
|
79
|
+
Purge: 'Purged',
|
|
80
|
+
Upgrade: 'Upgraded the site',
|
|
81
|
+
UpgradeFix: 'Prepared an upgrade',
|
|
82
|
+
PauseEditing: 'Paused editing',
|
|
83
|
+
ResumeEditing: 'Resumed editing',
|
|
84
|
+
RunMigration: 'Ran a package update',
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const ITEM_TYPES = {
|
|
88
|
+
Document: 'Page',
|
|
89
|
+
DocumentBlueprint: 'Document blueprint',
|
|
90
|
+
DocumentType: 'Document type',
|
|
91
|
+
DictionaryItem: 'Dictionary item',
|
|
92
|
+
MediaType: 'Media type',
|
|
93
|
+
MemberType: 'Member type',
|
|
94
|
+
MemberGroup: 'Member group',
|
|
95
|
+
DataType: 'Data type',
|
|
96
|
+
PartialView: 'Partial view',
|
|
97
|
+
UserGroup: 'User group',
|
|
98
|
+
SeoSettings: 'Search and sharing settings',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const WORKSPACE = {
|
|
102
|
+
Document: (key) => `section/content/workspace/document/edit/${key}`,
|
|
103
|
+
Media: (key) => `section/media/workspace/media/edit/${key}`,
|
|
104
|
+
Member: (key) => `section/member-management/workspace/member/edit/${key}`,
|
|
105
|
+
User: (key) => `section/user-management/workspace/user/edit/${key}`,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const words = (name) =>
|
|
109
|
+
name.replace(/([a-z])([A-Z])/g, '$1 $2').replace(/^./, (c) => c.toUpperCase())
|
|
110
|
+
const actionLabel = (action) => ACTIONS[action] ?? words(action)
|
|
111
|
+
const itemType = (type) => ITEM_TYPES[type] ?? words(type.replace(/Folder$/, ' folder'))
|
|
112
|
+
|
|
113
|
+
function apiBase() {
|
|
114
|
+
const base = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
115
|
+
return `${base.replace(/\/$/, '')}/crombolo/api/audit`
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function get(path, params) {
|
|
119
|
+
const query = params ? `?${new URLSearchParams(params)}` : ''
|
|
120
|
+
const response = await fetch(`${apiBase()}${path}${query}`, {
|
|
121
|
+
credentials: 'include',
|
|
122
|
+
headers: { accept: 'application/json' },
|
|
123
|
+
})
|
|
124
|
+
if (!response.ok) throw new Error(`The audit log could not be read (${response.status}).`)
|
|
125
|
+
return response.json()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function fromWhere(entry) {
|
|
129
|
+
if (entry.source === 'cli') return 'Command line'
|
|
130
|
+
if (entry.source === 'job') return 'Scheduled'
|
|
131
|
+
const forwarded = entry.forwardedFor?.split(',')[0]?.trim()
|
|
132
|
+
return forwarded || entry.ipAddress || '—'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default class CromboloAuditDashboardElement extends LitElement {
|
|
136
|
+
static properties = {
|
|
137
|
+
_page: { state: true },
|
|
138
|
+
_users: { state: true },
|
|
139
|
+
_error: { state: true },
|
|
140
|
+
_filter: { state: true },
|
|
141
|
+
_skip: { state: true },
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
constructor() {
|
|
145
|
+
super()
|
|
146
|
+
this._page = undefined
|
|
147
|
+
this._users = []
|
|
148
|
+
this._error = undefined
|
|
149
|
+
this._filter = { user: '', category: '', text: '', since: '', until: '' }
|
|
150
|
+
this._skip = 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
connectedCallback() {
|
|
154
|
+
super.connectedCallback()
|
|
155
|
+
get('/users')
|
|
156
|
+
.then((users) => {
|
|
157
|
+
this._users = users.items
|
|
158
|
+
})
|
|
159
|
+
.catch(() => {})
|
|
160
|
+
this.#load()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async #load() {
|
|
164
|
+
const params = { skip: String(this._skip), take: String(PAGE_SIZE) }
|
|
165
|
+
const { user, category, text, since, until } = this._filter
|
|
166
|
+
if (user) params.user = user
|
|
167
|
+
if (category) params.category = category
|
|
168
|
+
if (text.trim()) params.text = text.trim()
|
|
169
|
+
// Whole local days: "To" includes the day it names.
|
|
170
|
+
if (since) params.since = new Date(`${since}T00:00:00`).toISOString()
|
|
171
|
+
if (until)
|
|
172
|
+
params.until = new Date(new Date(`${until}T00:00:00`).getTime() + 86_400_000).toISOString()
|
|
173
|
+
try {
|
|
174
|
+
this._page = await get('', params)
|
|
175
|
+
this._error = undefined
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this._error = error.message
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#set(field, value) {
|
|
182
|
+
this._filter = { ...this._filter, [field]: value }
|
|
183
|
+
this._skip = 0
|
|
184
|
+
this.#load()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#go(skip) {
|
|
188
|
+
this._skip = Math.max(0, skip)
|
|
189
|
+
this.#load()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
render() {
|
|
193
|
+
return html`
|
|
194
|
+
<uui-box headline="Audit log">
|
|
195
|
+
<p class="intro">
|
|
196
|
+
Every change made in the backoffice, every sign-in, and what the command line and
|
|
197
|
+
scheduled publishing changed.
|
|
198
|
+
${
|
|
199
|
+
this._page?.retentionDays
|
|
200
|
+
? html`Entries are kept for ${this._page.retentionDays} days.`
|
|
201
|
+
: this._page
|
|
202
|
+
? html`Entries are kept until they are removed.`
|
|
203
|
+
: nothing
|
|
204
|
+
}
|
|
205
|
+
</p>
|
|
206
|
+
${this.#renderFilters()}
|
|
207
|
+
${this.#renderBody()}
|
|
208
|
+
</uui-box>
|
|
209
|
+
`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#renderFilters() {
|
|
213
|
+
const f = this._filter
|
|
214
|
+
return html`
|
|
215
|
+
<div class="filters">
|
|
216
|
+
<label>
|
|
217
|
+
Who
|
|
218
|
+
<select .value=${f.user} @change=${(e) => this.#set('user', e.target.value)}>
|
|
219
|
+
<option value="">Anyone</option>
|
|
220
|
+
${this._users.map((u) => html`<option value=${u.key}>${u.name || 'Unnamed'}</option>`)}
|
|
221
|
+
</select>
|
|
222
|
+
</label>
|
|
223
|
+
<label>
|
|
224
|
+
What
|
|
225
|
+
<select .value=${f.category} @change=${(e) => this.#set('category', e.target.value)}>
|
|
226
|
+
${CATEGORIES.map(([value, label]) => html`<option value=${value}>${label}</option>`)}
|
|
227
|
+
</select>
|
|
228
|
+
</label>
|
|
229
|
+
<label>
|
|
230
|
+
Containing
|
|
231
|
+
<uui-input
|
|
232
|
+
label="Containing"
|
|
233
|
+
placeholder="A name"
|
|
234
|
+
.value=${f.text}
|
|
235
|
+
@change=${(e) => this.#set('text', e.target.value)}
|
|
236
|
+
></uui-input>
|
|
237
|
+
</label>
|
|
238
|
+
<label>
|
|
239
|
+
From
|
|
240
|
+
<input type="date" .value=${f.since} @change=${(e) => this.#set('since', e.target.value)} />
|
|
241
|
+
</label>
|
|
242
|
+
<label>
|
|
243
|
+
To
|
|
244
|
+
<input type="date" .value=${f.until} @change=${(e) => this.#set('until', e.target.value)} />
|
|
245
|
+
</label>
|
|
246
|
+
<uui-button look="secondary" compact label="Refresh" @click=${() => this.#load()}>Refresh</uui-button>
|
|
247
|
+
</div>
|
|
248
|
+
`
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#renderBody() {
|
|
252
|
+
if (this._error) return html`<p class="error">${this._error}</p>`
|
|
253
|
+
if (!this._page) return html`<uui-loader></uui-loader>`
|
|
254
|
+
const { items, total } = this._page
|
|
255
|
+
if (items.length === 0) return html`<p class="empty">Nothing matches.</p>`
|
|
256
|
+
const last = Math.min(this._skip + items.length, total)
|
|
257
|
+
return html`
|
|
258
|
+
<uui-table>
|
|
259
|
+
<uui-table-head>
|
|
260
|
+
<uui-table-head-cell>When</uui-table-head-cell>
|
|
261
|
+
<uui-table-head-cell>Who</uui-table-head-cell>
|
|
262
|
+
<uui-table-head-cell>What</uui-table-head-cell>
|
|
263
|
+
<uui-table-head-cell>Item</uui-table-head-cell>
|
|
264
|
+
<uui-table-head-cell>From</uui-table-head-cell>
|
|
265
|
+
</uui-table-head>
|
|
266
|
+
${items.map((entry) => this.#renderRow(entry))}
|
|
267
|
+
</uui-table>
|
|
268
|
+
<div class="pager">
|
|
269
|
+
<span>${this._skip + 1}–${last} of ${total}</span>
|
|
270
|
+
<uui-button
|
|
271
|
+
compact
|
|
272
|
+
look="secondary"
|
|
273
|
+
label="Newer"
|
|
274
|
+
?disabled=${this._skip === 0}
|
|
275
|
+
@click=${() => this.#go(this._skip - PAGE_SIZE)}
|
|
276
|
+
>Newer</uui-button>
|
|
277
|
+
<uui-button
|
|
278
|
+
compact
|
|
279
|
+
look="secondary"
|
|
280
|
+
label="Older"
|
|
281
|
+
?disabled=${last >= total}
|
|
282
|
+
@click=${() => this.#go(this._skip + PAGE_SIZE)}
|
|
283
|
+
>Older</uui-button>
|
|
284
|
+
</div>
|
|
285
|
+
`
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
#renderRow(entry) {
|
|
289
|
+
const when = new Date(entry.occurredAt)
|
|
290
|
+
const failed = entry.action === 'SignInFailed' || entry.action === 'LockedOut'
|
|
291
|
+
return html`<uui-table-row>
|
|
292
|
+
<uui-table-cell title=${when.toISOString()}>${when.toLocaleString()}</uui-table-cell>
|
|
293
|
+
<uui-table-cell>${entry.user?.name ?? (entry.source === 'job' ? 'Scheduled publishing' : 'Unknown')}</uui-table-cell>
|
|
294
|
+
<uui-table-cell>
|
|
295
|
+
${failed ? html`<uui-tag color="danger">${actionLabel(entry.action)}</uui-tag>` : actionLabel(entry.action)}
|
|
296
|
+
${entry.detail && entry.source !== 'cli' ? html`<small>${entry.detail}</small>` : nothing}
|
|
297
|
+
</uui-table-cell>
|
|
298
|
+
<uui-table-cell>${this.#renderItem(entry)}</uui-table-cell>
|
|
299
|
+
<uui-table-cell title=${entry.forwardedFor ? `Connected from ${entry.ipAddress ?? 'unknown'}` : ''}>
|
|
300
|
+
${fromWhere(entry)}
|
|
301
|
+
</uui-table-cell>
|
|
302
|
+
</uui-table-row>`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#renderItem(entry) {
|
|
306
|
+
const item = entry.item
|
|
307
|
+
if (!item) return nothing
|
|
308
|
+
const label = item.name ?? item.key ?? ''
|
|
309
|
+
const link = item.key && WORKSPACE[item.type]?.(item.key)
|
|
310
|
+
const base = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
|
|
311
|
+
return html`
|
|
312
|
+
<small>${itemType(item.type)}</small>
|
|
313
|
+
${link ? html`<a href=${`${base.replace(/\/$/, '')}/${link}`}>${label}</a>` : label}
|
|
314
|
+
`
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
static styles = css`
|
|
318
|
+
:host {
|
|
319
|
+
display: block;
|
|
320
|
+
padding: var(--uui-size-layout-1);
|
|
321
|
+
}
|
|
322
|
+
.intro {
|
|
323
|
+
margin-top: 0;
|
|
324
|
+
color: var(--uui-color-text-alt);
|
|
325
|
+
}
|
|
326
|
+
.filters {
|
|
327
|
+
display: flex;
|
|
328
|
+
flex-wrap: wrap;
|
|
329
|
+
gap: var(--uui-size-space-4);
|
|
330
|
+
align-items: flex-end;
|
|
331
|
+
margin-bottom: var(--uui-size-space-5);
|
|
332
|
+
}
|
|
333
|
+
.filters label {
|
|
334
|
+
display: flex;
|
|
335
|
+
flex-direction: column;
|
|
336
|
+
gap: var(--uui-size-space-1);
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
}
|
|
339
|
+
select,
|
|
340
|
+
input[type='date'] {
|
|
341
|
+
font: inherit;
|
|
342
|
+
padding: var(--uui-size-space-2);
|
|
343
|
+
border: 1px solid var(--uui-color-border);
|
|
344
|
+
border-radius: var(--uui-border-radius);
|
|
345
|
+
background: var(--uui-color-surface);
|
|
346
|
+
color: var(--uui-color-text);
|
|
347
|
+
}
|
|
348
|
+
uui-table-cell small {
|
|
349
|
+
display: block;
|
|
350
|
+
color: var(--uui-color-text-alt);
|
|
351
|
+
}
|
|
352
|
+
.pager {
|
|
353
|
+
display: flex;
|
|
354
|
+
gap: var(--uui-size-space-3);
|
|
355
|
+
align-items: center;
|
|
356
|
+
justify-content: flex-end;
|
|
357
|
+
margin-top: var(--uui-size-space-4);
|
|
358
|
+
}
|
|
359
|
+
.empty,
|
|
360
|
+
.error {
|
|
361
|
+
color: var(--uui-color-text-alt);
|
|
362
|
+
}
|
|
363
|
+
.error {
|
|
364
|
+
color: var(--uui-color-danger);
|
|
365
|
+
}
|
|
366
|
+
`
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
customElements.define('crombolo-audit-dashboard', CromboloAuditDashboardElement)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Umbraco splits the two because Razor needed the distinction for
|
|
5
5
|
* master-template inheritance; a TSX layout is an import, so here a template is
|
|
6
|
-
* just a component a content type names (`docs/
|
|
6
|
+
* just a component a content type names (`docs/rendering.md`). The tree that
|
|
7
7
|
* survives is the partial-view one, because it is path-addressed and understands
|
|
8
8
|
* folders — the template tree is id-addressed and cannot show one.
|
|
9
9
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* `sections` and `packager` are the other kind of rename: not the product's name
|
|
19
19
|
* but a feature's. Umbraco's Packages section builds a `.umb` file; the artifact
|
|
20
|
-
* here is a bundle (`docs/
|
|
20
|
+
* here is a bundle (`docs/bundles.md`), so the section and the flow that
|
|
21
21
|
* builds one say so. `build-localizations.ts` reads this file for its key set and
|
|
22
22
|
* rewrites only values that name the product, so these pass through untouched and
|
|
23
23
|
* every other language keeps its own word.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a bundle adds, as the Marketplace and Installed views both show it.
|
|
3
|
+
*/
|
|
4
|
+
import { html } from '@umbraco-cms/backoffice/external/lit'
|
|
5
|
+
|
|
6
|
+
/** What a bundle adds, in the words of someone choosing one. */
|
|
7
|
+
export const CARRIES_LABELS = {
|
|
8
|
+
extensions: 'Screens',
|
|
9
|
+
content: 'Content',
|
|
10
|
+
server: 'Server features',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const carriesTags = (carries) =>
|
|
14
|
+
Object.entries(CARRIES_LABELS)
|
|
15
|
+
.filter(([key]) => carries?.[key])
|
|
16
|
+
.map(([, label]) => html`<uui-tag look="secondary">${label}</uui-tag>`)
|
package/plugin/bundles-client.js
CHANGED
|
@@ -27,8 +27,14 @@ export const searchMarketplace = (query) =>
|
|
|
27
27
|
|
|
28
28
|
export const listInstalled = () => json('/installed')
|
|
29
29
|
|
|
30
|
-
export const installBundle = (name, version) =>
|
|
31
|
-
json('/install', { method: 'POST', body: JSON.stringify({ name, version }) })
|
|
30
|
+
export const installBundle = (name, version, applyContent = true) =>
|
|
31
|
+
json('/install', { method: 'POST', body: JSON.stringify({ name, version, applyContent }) })
|
|
32
32
|
|
|
33
33
|
export const uninstallBundle = (name) =>
|
|
34
34
|
json('/uninstall', { method: 'POST', body: JSON.stringify({ name }) })
|
|
35
|
+
|
|
36
|
+
/** The npm package a created bundle downloads as. */
|
|
37
|
+
export const readCreatedPackage = (id) => json(`/created/${encodeURIComponent(id)}/package`)
|
|
38
|
+
|
|
39
|
+
export const saveCreatedPackage = (id, pkg) =>
|
|
40
|
+
json(`/created/${encodeURIComponent(id)}/package`, { method: 'PUT', body: JSON.stringify(pkg) })
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* the definitions are the Management API's nine `package` operations, which are
|
|
6
6
|
* contract-fixed and bearer-authenticated, and the repository already carries
|
|
7
7
|
* the client's token. The plugin's `/crombolo/api/bundles` routes are a
|
|
8
|
-
* different thing — installed npm bundles, on the session cookie
|
|
8
|
+
* different thing — installed npm bundles, on the session cookie — and one of
|
|
9
|
+
* them is used here too: every bundle downloads as an npm package, and the name
|
|
10
|
+
* and version it is packed under are not part of Umbraco's definition.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
12
14
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
13
15
|
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'
|
|
14
16
|
import { UmbPackageRepository } from '@umbraco-cms/backoffice/package'
|
|
17
|
+
import { readCreatedPackage, saveCreatedPackage } from './bundles-client.js'
|
|
15
18
|
|
|
16
19
|
const TAKE = 10
|
|
17
20
|
|
|
@@ -24,6 +27,10 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
24
27
|
_loading: { state: true },
|
|
25
28
|
_total: { state: true },
|
|
26
29
|
_page: { state: true },
|
|
30
|
+
_packages: { state: true },
|
|
31
|
+
_editing: { state: true },
|
|
32
|
+
_draft: { state: true },
|
|
33
|
+
_problem: { state: true },
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
#repository = new UmbPackageRepository(this)
|
|
@@ -34,6 +41,10 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
34
41
|
this._loading = true
|
|
35
42
|
this._total = 0
|
|
36
43
|
this._page = 1
|
|
44
|
+
this._packages = {}
|
|
45
|
+
this._editing = ''
|
|
46
|
+
this._draft = { name: '', version: '' }
|
|
47
|
+
this._problem = ''
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
connectedCallback() {
|
|
@@ -48,6 +59,67 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
48
59
|
this._items = data?.items ?? []
|
|
49
60
|
this._total = data?.total ?? 0
|
|
50
61
|
this._loading = false
|
|
62
|
+
const packages = {}
|
|
63
|
+
await Promise.all(
|
|
64
|
+
this._items
|
|
65
|
+
.filter((item) => item.unique)
|
|
66
|
+
.map(async (item) => {
|
|
67
|
+
packages[item.unique] = await readCreatedPackage(item.unique).catch(() => undefined)
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
this._packages = packages
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#edit(item) {
|
|
74
|
+
const pkg = this._packages[item.unique]
|
|
75
|
+
this._editing = item.unique
|
|
76
|
+
this._problem = ''
|
|
77
|
+
this._draft = {
|
|
78
|
+
name: pkg?.set?.name ? pkg.name : '',
|
|
79
|
+
version: pkg?.set?.version ? pkg.version : '',
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async #save() {
|
|
84
|
+
const outcome = await saveCreatedPackage(this._editing, this._draft)
|
|
85
|
+
if (!outcome.ok) {
|
|
86
|
+
this._problem = outcome.message ?? 'Those details could not be saved.'
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
this._packages = { ...this._packages, [this._editing]: outcome.package }
|
|
90
|
+
this._editing = ''
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#renderEditor(item) {
|
|
94
|
+
if (this._editing !== item.unique) return nothing
|
|
95
|
+
const pkg = this._packages[item.unique]
|
|
96
|
+
return html`
|
|
97
|
+
<div class="package-editor">
|
|
98
|
+
<p>
|
|
99
|
+
This bundle downloads as the npm package <code>${pkg?.name}@${pkg?.version}</code>.
|
|
100
|
+
Leave a field empty to go back to the name taken from the bundle's own name, and
|
|
101
|
+
version 1.0.0.
|
|
102
|
+
</p>
|
|
103
|
+
<uui-input
|
|
104
|
+
label="Package name"
|
|
105
|
+
placeholder=${pkg?.name ?? ''}
|
|
106
|
+
.value=${this._draft.name}
|
|
107
|
+
@input=${(event) => (this._draft = { ...this._draft, name: event.target.value })}></uui-input>
|
|
108
|
+
<uui-input
|
|
109
|
+
label="Version"
|
|
110
|
+
placeholder="1.0.0"
|
|
111
|
+
.value=${this._draft.version}
|
|
112
|
+
@input=${(event) => (this._draft = { ...this._draft, version: event.target.value })}></uui-input>
|
|
113
|
+
${this._problem ? html`<p class="problem">${this._problem}</p>` : nothing}
|
|
114
|
+
<uui-button look="primary" label="Save" @click=${() => this.#save()}></uui-button>
|
|
115
|
+
<uui-button label="Cancel" @click=${() => (this._editing = '')}></uui-button>
|
|
116
|
+
</div>
|
|
117
|
+
`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#detail(item) {
|
|
121
|
+
const pkg = this._packages[item.unique]
|
|
122
|
+
return pkg ? `${pkg.name}@${pkg.version}` : ''
|
|
51
123
|
}
|
|
52
124
|
|
|
53
125
|
#onPageChange(event) {
|
|
@@ -109,13 +181,20 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
109
181
|
this._items,
|
|
110
182
|
(item) => item.unique,
|
|
111
183
|
(item) => html`
|
|
112
|
-
<uui-ref-node
|
|
184
|
+
<uui-ref-node
|
|
185
|
+
name=${item.name}
|
|
186
|
+
detail=${this.#detail(item)}
|
|
187
|
+
@open=${() => this.#open(item)}>
|
|
113
188
|
<uui-action-bar slot="actions">
|
|
189
|
+
<uui-button
|
|
190
|
+
@click=${() => this.#edit(item)}
|
|
191
|
+
label="Package details"></uui-button>
|
|
114
192
|
<uui-button
|
|
115
193
|
@click=${() => this.#remove(item)}
|
|
116
194
|
label=${this.localize.term('general_delete')}></uui-button>
|
|
117
195
|
</uui-action-bar>
|
|
118
196
|
</uui-ref-node>
|
|
197
|
+
${this.#renderEditor(item)}
|
|
119
198
|
`,
|
|
120
199
|
)}
|
|
121
200
|
</uui-ref-list>
|
|
@@ -144,6 +223,21 @@ export default class CromboloBundlesCreatedOverviewElement extends UmbLitElement
|
|
|
144
223
|
text-align: center;
|
|
145
224
|
opacity: 0.6;
|
|
146
225
|
}
|
|
226
|
+
.package-editor {
|
|
227
|
+
display: flex;
|
|
228
|
+
flex-wrap: wrap;
|
|
229
|
+
align-items: center;
|
|
230
|
+
gap: var(--uui-size-space-3);
|
|
231
|
+
padding: var(--uui-size-space-4) var(--uui-size-space-5);
|
|
232
|
+
background: var(--uui-color-surface-alt);
|
|
233
|
+
}
|
|
234
|
+
.package-editor p {
|
|
235
|
+
flex-basis: 100%;
|
|
236
|
+
margin: 0;
|
|
237
|
+
}
|
|
238
|
+
.problem {
|
|
239
|
+
color: var(--uui-color-danger);
|
|
240
|
+
}
|
|
147
241
|
`
|
|
148
242
|
}
|
|
149
243
|
|
|
@@ -4,16 +4,36 @@
|
|
|
4
4
|
* Its manifest declares `overwrites: ['Umb.SectionView.Packages.Installed']`.
|
|
5
5
|
* Umbraco's view is built around package migrations — it lists installed
|
|
6
6
|
* packages so that pending C# migrations can be run. Nothing carries those here
|
|
7
|
-
* (`docs/
|
|
7
|
+
* (`docs/bundles.md`), so the view would be a list of nothing with a button
|
|
8
8
|
* that cannot apply.
|
|
9
9
|
*
|
|
10
|
-
* What it lists instead is the site's
|
|
11
|
-
*
|
|
10
|
+
* What it lists instead is the site's bundle dependencies, which is what
|
|
11
|
+
* "installed" means here: what each one adds, whether its content has been
|
|
12
|
+
* added to this site, and whether its server features are switched on.
|
|
12
13
|
*/
|
|
13
14
|
import { css, html, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'
|
|
14
15
|
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
|
|
16
|
+
import { carriesTags } from './bundles-carries.js'
|
|
15
17
|
import { listInstalled, uninstallBundle } from './bundles-client.js'
|
|
16
18
|
|
|
19
|
+
/** Where each part of a bundle stands on this site, in the words of someone running it. */
|
|
20
|
+
function status(item) {
|
|
21
|
+
const parts = []
|
|
22
|
+
if (item.carries?.content)
|
|
23
|
+
parts.push(
|
|
24
|
+
item.contentApplied
|
|
25
|
+
? html`<span>Content added (${item.contentApplied})</span>`
|
|
26
|
+
: html`<span>Content not added yet</span>`,
|
|
27
|
+
)
|
|
28
|
+
if (item.carries?.server)
|
|
29
|
+
parts.push(
|
|
30
|
+
item.wired
|
|
31
|
+
? html`<span>Server features on</span>`
|
|
32
|
+
: html`<span>Server features off until the site's configuration includes it</span>`,
|
|
33
|
+
)
|
|
34
|
+
return parts.length > 0 ? parts : html`<span>Ready</span>`
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
18
38
|
static properties = {
|
|
19
39
|
_items: { state: true },
|
|
@@ -70,8 +90,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
70
90
|
<uui-box headline="No bundles installed">
|
|
71
91
|
<p>
|
|
72
92
|
A bundle is one of this site's npm dependencies. Install one from the
|
|
73
|
-
Marketplace tab, or with <code>bun add</code> in the site directory
|
|
74
|
-
|
|
93
|
+
Marketplace tab, or with <code>bun add</code> in the site directory, and it
|
|
94
|
+
appears here.
|
|
75
95
|
</p>
|
|
76
96
|
</uui-box>
|
|
77
97
|
`
|
|
@@ -82,7 +102,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
82
102
|
<uui-table-head-cell>Name</uui-table-head-cell>
|
|
83
103
|
<uui-table-head-cell>npm package</uui-table-head-cell>
|
|
84
104
|
<uui-table-head-cell>Version</uui-table-head-cell>
|
|
85
|
-
<uui-table-head-cell>
|
|
105
|
+
<uui-table-head-cell>Adds</uui-table-head-cell>
|
|
106
|
+
<uui-table-head-cell>On this site</uui-table-head-cell>
|
|
86
107
|
<uui-table-head-cell></uui-table-head-cell>
|
|
87
108
|
</uui-table-head>
|
|
88
109
|
${repeat(
|
|
@@ -93,7 +114,8 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
93
114
|
<uui-table-cell>${item.name}</uui-table-cell>
|
|
94
115
|
<uui-table-cell><code>${item.packageName}</code></uui-table-cell>
|
|
95
116
|
<uui-table-cell>${item.version}</uui-table-cell>
|
|
96
|
-
<uui-table-cell>${item.
|
|
117
|
+
<uui-table-cell class="carries">${carriesTags(item.carries)}</uui-table-cell>
|
|
118
|
+
<uui-table-cell class="status">${status(item)}</uui-table-cell>
|
|
97
119
|
<uui-table-cell>
|
|
98
120
|
<uui-button
|
|
99
121
|
look="secondary"
|
|
@@ -136,6 +158,13 @@ export default class CromboloBundlesInstalledElement extends UmbLitElement {
|
|
|
136
158
|
max-width: 60ch;
|
|
137
159
|
line-height: 1.5;
|
|
138
160
|
}
|
|
161
|
+
.carries {
|
|
162
|
+
display: flex;
|
|
163
|
+
gap: var(--uui-size-space-2, 6px);
|
|
164
|
+
}
|
|
165
|
+
.status span {
|
|
166
|
+
display: block;
|
|
167
|
+
}
|
|
139
168
|
.note {
|
|
140
169
|
margin-top: var(--uui-size-space-5, 18px);
|
|
141
170
|
opacity: 0.8;
|