@crombolo/backoffice-host 0.13.2 → 0.14.4
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/assets/device.js +180 -0
- package/assets/login.js +89 -2
- package/assets/remote-approval.js +134 -0
- package/package.json +2 -2
- package/plugin/assistant/umbraco-package.json +1 -1
- package/plugin/branding/branding.js +9 -14
- package/plugin/branding/localization-en.js +11 -0
- package/plugin/branding/theme.css +2 -0
- package/plugin/localizations/ar.js +3 -39
- package/plugin/localizations/bs.js +3 -29
- package/plugin/localizations/cs.js +3 -24
- package/plugin/localizations/cy.js +3 -32
- package/plugin/localizations/da.js +3 -41
- package/plugin/localizations/de.js +3 -27
- package/plugin/localizations/es.js +3 -19
- package/plugin/localizations/fr.js +3 -26
- package/plugin/localizations/hr.js +3 -29
- package/plugin/localizations/it.js +3 -29
- package/plugin/localizations/ja.js +3 -11
- package/plugin/localizations/ko.js +3 -11
- package/plugin/localizations/nb.js +3 -18
- package/plugin/localizations/nl.js +3 -29
- package/plugin/localizations/pl.js +3 -11
- package/plugin/localizations/pt-br.js +0 -10
- package/plugin/localizations/pt.js +3 -39
- package/plugin/localizations/ru.js +3 -9
- package/plugin/localizations/sv.js +3 -20
- package/plugin/localizations/tr.js +3 -26
- package/plugin/localizations/uk.js +3 -9
- package/plugin/localizations/umbraco-package.json +22 -2
- package/plugin/localizations/vi.js +3 -42
- package/plugin/localizations/zh-tw.js +0 -6
- package/plugin/localizations/zh.js +0 -6
- package/plugin/seo/umbraco-package.json +1 -1
- package/plugin/sign-in/entry.js +20 -0
- package/plugin/sign-in/sign-in-dialog.js +127 -0
- package/plugin/sign-in/umbraco-package.json +15 -0
- package/plugin/signed-in-devices.js +90 -0
- package/plugin/two-factor-setup.js +147 -0
- package/plugin/umbraco-package.json +79 -27
- package/plugin/welcome-dashboard.js +2 -5
- package/plugin/workflow-designer.js +245 -170
- package/src/login-assets.ts +5 -2
- package/src/shell.ts +65 -1
- package/plugin/branding/sysinfo.js +0 -89
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The workflow designer (story 028): stages drawn on a canvas and connected by
|
|
3
3
|
* arrows, each stage's approvers and send-back beside it, and the workflow's
|
|
4
|
-
* own settings
|
|
4
|
+
* own settings on a tab of their own. Saving writes `schema/workflows/<alias>.toml`; before it
|
|
5
5
|
* does, the requests in flight that the change affects are named.
|
|
6
6
|
*
|
|
7
7
|
* An arrow that would close a loop is refused as it is drawn. The server holds
|
|
@@ -60,13 +60,24 @@ const blank = () => ({
|
|
|
60
60
|
],
|
|
61
61
|
})
|
|
62
62
|
|
|
63
|
-
const select = (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
63
|
+
const select = (label, value, options, onChange, disabled) => html`<uui-select
|
|
64
|
+
label=${label}
|
|
65
|
+
?disabled=${disabled}
|
|
66
|
+
.options=${Object.entries(options).map(([key, name]) => ({ name, value: key, selected: key === value }))}
|
|
67
|
+
@change=${(e) => onChange(e.target.value)}
|
|
68
|
+
></uui-select>`
|
|
69
|
+
|
|
70
|
+
/** A labelled row, as every backoffice editor lays out a field. */
|
|
71
|
+
const property = (
|
|
72
|
+
label,
|
|
73
|
+
description,
|
|
74
|
+
editor,
|
|
75
|
+
orientation = 'horizontal',
|
|
76
|
+
) => html`<umb-property-layout
|
|
77
|
+
orientation=${orientation}
|
|
78
|
+
label=${label}
|
|
79
|
+
description=${description ?? ''}
|
|
80
|
+
><div slot="editor">${editor}</div></umb-property-layout>`
|
|
70
81
|
|
|
71
82
|
export default class CromboloWorkflowDesignerElement extends UmbLitElement {
|
|
72
83
|
static properties = {
|
|
@@ -78,6 +89,7 @@ export default class CromboloWorkflowDesignerElement extends UmbLitElement {
|
|
|
78
89
|
_problems: { state: true },
|
|
79
90
|
_affected: { state: true },
|
|
80
91
|
_message: { state: true },
|
|
92
|
+
_tab: { state: true },
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
#drag
|
|
@@ -115,6 +127,7 @@ export default class CromboloWorkflowDesignerElement extends UmbLitElement {
|
|
|
115
127
|
this._problems = undefined
|
|
116
128
|
this._affected = undefined
|
|
117
129
|
this._message = undefined
|
|
130
|
+
this._tab = 'settings'
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
#touch() {
|
|
@@ -309,7 +322,7 @@ export default class CromboloWorkflowDesignerElement extends UmbLitElement {
|
|
|
309
322
|
<uui-table-head-cell>Workflow</uui-table-head-cell>
|
|
310
323
|
<uui-table-head-cell>Covers</uui-table-head-cell>
|
|
311
324
|
<uui-table-head-cell>Stages</uui-table-head-cell>
|
|
312
|
-
<uui-table-head-cell></uui-table-head-cell>
|
|
325
|
+
<uui-table-head-cell class="actions"></uui-table-head-cell>
|
|
313
326
|
</uui-table-head>
|
|
314
327
|
${items.map(
|
|
315
328
|
(w) => html`<uui-table-row>
|
|
@@ -321,7 +334,7 @@ export default class CromboloWorkflowDesignerElement extends UmbLitElement {
|
|
|
321
334
|
.filter(Boolean)
|
|
322
335
|
.join(', ')}</uui-table-cell>
|
|
323
336
|
<uui-table-cell>${w.stages.length}</uui-table-cell>
|
|
324
|
-
<uui-table-cell>
|
|
337
|
+
<uui-table-cell class="actions">
|
|
325
338
|
<uui-button look="secondary" label=${editable ? 'Edit' : 'View'} @click=${() => this.#edit(w)}></uui-button>
|
|
326
339
|
${
|
|
327
340
|
editable
|
|
@@ -340,53 +353,41 @@ export default class CromboloWorkflowDesignerElement extends UmbLitElement {
|
|
|
340
353
|
#renderEditor() {
|
|
341
354
|
const w = this._editing
|
|
342
355
|
const editable = this._list.editable
|
|
343
|
-
const stage = this.#stage(this._selected)
|
|
344
356
|
const leave = () => {
|
|
345
357
|
this._editing = undefined
|
|
346
358
|
this._message = undefined
|
|
347
359
|
this._problems = undefined
|
|
348
360
|
}
|
|
361
|
+
const tab = (id, label, icon) => html`<uui-tab
|
|
362
|
+
label=${label}
|
|
363
|
+
?active=${this._tab === id}
|
|
364
|
+
@click=${() => {
|
|
365
|
+
this._tab = id
|
|
366
|
+
}}
|
|
367
|
+
><umb-icon slot="icon" name=${icon}></umb-icon>${label}</uui-tab>`
|
|
349
368
|
// Umbraco's own layout, so Save sits bottom right in the footer as it does
|
|
350
|
-
// in every workspace, with Back beside it.
|
|
369
|
+
// in every workspace, with Back beside it, and the tabs top right.
|
|
351
370
|
return html`<umb-body-layout headline=${w.name || 'Workflow'}>
|
|
352
|
-
<uui-
|
|
353
|
-
${
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
</div>`
|
|
363
|
-
: nothing
|
|
364
|
-
}
|
|
365
|
-
${this._message ? html`<p class="message">${this._message}</p>` : nothing}
|
|
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>
|
|
367
|
-
<div class="canvas" @pointermove=${(e) => editable && this.#pointerMove(e)} @pointerup=${() => this.#pointerUp()} @pointerleave=${() => this.#pointerUp()}>
|
|
368
|
-
${drawGraph(w.stages, {
|
|
369
|
-
selected: this._selected,
|
|
370
|
-
onStage: (alias) => this.#onStage(alias),
|
|
371
|
-
onArrow: (from, to) => editable && this.#onArrow(from, to),
|
|
372
|
-
onPointerDown: (alias, event) => editable && this.#pointerDown(alias, event),
|
|
373
|
-
})}
|
|
374
|
-
</div>
|
|
375
|
-
</uui-box>
|
|
376
|
-
<div class="panels">
|
|
377
|
-
${stage ? this.#renderStage(stage, editable) : nothing}
|
|
378
|
-
${this.#renderSettings(w, editable)}
|
|
379
|
-
</div>
|
|
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}
|
|
371
|
+
<uui-tab-group slot="navigation">
|
|
372
|
+
${tab('settings', 'Settings', 'icon-settings')}
|
|
373
|
+
${tab('stages', 'Stages', 'icon-mindmap')}
|
|
374
|
+
</uui-tab-group>
|
|
375
|
+
${this._message ? html`<uui-box class="notice"><p class="message">${this._message}</p></uui-box>` : nothing}
|
|
376
|
+
${
|
|
377
|
+
this._problems
|
|
378
|
+
? html`<uui-box class="notice" headline="Not saved"><ul class="problems">${this._problems.map((p) => html`<li>${p.path ? html`<code>${p.path}</code> ` : nothing}${p.message}</li>`)}</ul></uui-box>`
|
|
379
|
+
: nothing
|
|
380
|
+
}
|
|
381
381
|
${
|
|
382
382
|
this._affected
|
|
383
|
-
? html`<
|
|
383
|
+
? html`<uui-box class="notice affected" headline="Pages waiting for approval">
|
|
384
384
|
<p>Saving changes the rules for ${this._affected.length} page(s) waiting for approval:</p>
|
|
385
385
|
<ul>${this._affected.map((a) => html`<li>${a.page}: ${ON_CHANGE[a.outcome]}</li>`)}</ul>
|
|
386
386
|
<uui-button look="primary" label="Save anyway" @click=${() => this.#save(true)}>Save anyway</uui-button>
|
|
387
|
-
</
|
|
387
|
+
</uui-box>`
|
|
388
388
|
: nothing
|
|
389
389
|
}
|
|
390
|
+
${this._tab === 'stages' ? this.#renderStages(w, editable) : this.#renderSettings(w, editable)}
|
|
390
391
|
${
|
|
391
392
|
this._problems
|
|
392
393
|
? html`<span slot="footer-info" class="not-saved">Not saved: ${this._problems.length} problem(s), listed above</span>`
|
|
@@ -401,163 +402,237 @@ export default class CromboloWorkflowDesignerElement extends UmbLitElement {
|
|
|
401
402
|
</umb-body-layout>`
|
|
402
403
|
}
|
|
403
404
|
|
|
405
|
+
#renderStages(w, editable) {
|
|
406
|
+
const stage = this.#stage(this._selected)
|
|
407
|
+
// One box, two columns: the graph, and the stage selected in it.
|
|
408
|
+
return html`<uui-box headline="Stages">
|
|
409
|
+
${
|
|
410
|
+
editable
|
|
411
|
+
? html`<div slot="header-actions" class="toolbar">
|
|
412
|
+
<uui-button look="secondary" compact label="Add a stage" @click=${() => this.#addStage()}>Add a stage</uui-button>
|
|
413
|
+
<uui-button look=${this._connecting ? 'primary' : 'secondary'} compact label="Connect stages"
|
|
414
|
+
@click=${() => {
|
|
415
|
+
this._connecting = this._connecting ? undefined : true
|
|
416
|
+
}}
|
|
417
|
+
>${this._connecting === true ? 'Click the stage to draw from' : this._connecting ? 'Now click the stage it leads to' : 'Connect stages'}</uui-button>
|
|
418
|
+
</div>`
|
|
419
|
+
: nothing
|
|
420
|
+
}
|
|
421
|
+
<div class="stages">
|
|
422
|
+
<div class="graph">
|
|
423
|
+
<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>
|
|
424
|
+
<div class="canvas" @pointermove=${(e) => editable && this.#pointerMove(e)} @pointerup=${() => this.#pointerUp()} @pointerleave=${() => this.#pointerUp()}>
|
|
425
|
+
${drawGraph(w.stages, {
|
|
426
|
+
selected: this._selected,
|
|
427
|
+
onStage: (alias) => this.#onStage(alias),
|
|
428
|
+
onArrow: (from, to) => editable && this.#onArrow(from, to),
|
|
429
|
+
onPointerDown: (alias, event) => editable && this.#pointerDown(alias, event),
|
|
430
|
+
})}
|
|
431
|
+
</div>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="stage">
|
|
434
|
+
${stage ? this.#renderStage(stage, editable) : html`<p class="hint">Select a stage to change it.</p>`}
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</uui-box>`
|
|
438
|
+
}
|
|
439
|
+
|
|
404
440
|
#renderStage(stage, editable) {
|
|
405
441
|
const earlier = [...ancestors(this._editing.stages, stage.alias)]
|
|
406
442
|
const requiredKind = typeof stage.required === 'number' ? 'number' : stage.required
|
|
407
|
-
return html`<
|
|
408
|
-
|
|
409
|
-
|
|
443
|
+
return html`<div class="stage-header">
|
|
444
|
+
<h4>${stage.name || stage.alias}</h4>
|
|
445
|
+
${
|
|
446
|
+
editable && this._editing.stages.length > 1
|
|
447
|
+
? html`<uui-button look="secondary" color="danger" compact label="Remove this stage" @click=${() => this.#removeStage(stage.alias)}>Remove</uui-button>`
|
|
448
|
+
: nothing
|
|
449
|
+
}
|
|
450
|
+
</div>
|
|
451
|
+
${property(
|
|
452
|
+
'Name',
|
|
453
|
+
undefined,
|
|
454
|
+
html`<uui-input label="Name" ?readonly=${!editable} .value=${stage.name ?? ''} @change=${(
|
|
455
|
+
e,
|
|
456
|
+
) => {
|
|
410
457
|
stage.name = e.target.value
|
|
411
458
|
this.#touch()
|
|
412
|
-
}}></uui-input
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
459
|
+
}}></uui-input>`,
|
|
460
|
+
'vertical',
|
|
461
|
+
)}
|
|
462
|
+
${property(
|
|
463
|
+
'Alias',
|
|
464
|
+
'How the stage is named in the file and in other stages',
|
|
465
|
+
html`<uui-input label="Alias" ?readonly=${!editable} .value=${stage.alias} @change=${(e) => this.#renameStage(stage, e.target.value)}></uui-input>`,
|
|
466
|
+
'vertical',
|
|
467
|
+
)}
|
|
468
|
+
${property(
|
|
469
|
+
'Approved by',
|
|
470
|
+
'The user groups whose members decide this stage',
|
|
471
|
+
html`<div class="checks">${this._list.groups.map(
|
|
472
|
+
(g) =>
|
|
473
|
+
html`<uui-checkbox label=${g.name} ?disabled=${!editable} ?checked=${stage.approvers.includes(g.alias)} @change=${(
|
|
474
|
+
e,
|
|
475
|
+
) => {
|
|
476
|
+
stage.approvers = e.target.checked
|
|
477
|
+
? [...stage.approvers, g.alias]
|
|
478
|
+
: stage.approvers.filter((a) => a !== g.alias)
|
|
479
|
+
this.#touch()
|
|
480
|
+
}}>${g.name}</uui-checkbox>`,
|
|
481
|
+
)}</div>`,
|
|
482
|
+
'vertical',
|
|
483
|
+
)}
|
|
484
|
+
${property(
|
|
485
|
+
'How many must approve',
|
|
486
|
+
undefined,
|
|
487
|
+
html`${select(
|
|
488
|
+
'How many must approve',
|
|
489
|
+
requiredKind,
|
|
490
|
+
{
|
|
491
|
+
one: 'Any one of them',
|
|
492
|
+
majority: 'A majority',
|
|
493
|
+
all: 'All of them',
|
|
494
|
+
number: 'A number of them',
|
|
495
|
+
},
|
|
496
|
+
(value) => {
|
|
497
|
+
stage.required = value === 'number' ? 2 : value
|
|
498
|
+
this.#touch()
|
|
499
|
+
},
|
|
500
|
+
!editable,
|
|
501
|
+
)}
|
|
502
|
+
${
|
|
503
|
+
typeof stage.required === 'number'
|
|
504
|
+
? html`<uui-input class="count" type="number" min="1" label="How many" ?readonly=${!editable} .value=${String(stage.required)} @change=${(
|
|
418
505
|
e,
|
|
419
506
|
) => {
|
|
420
|
-
stage.
|
|
421
|
-
? [...stage.approvers, g.alias]
|
|
422
|
-
: stage.approvers.filter((a) => a !== g.alias)
|
|
507
|
+
stage.required = Math.max(1, Number(e.target.value) || 1)
|
|
423
508
|
this.#touch()
|
|
424
|
-
}}
|
|
425
|
-
)}
|
|
426
|
-
</div>
|
|
427
|
-
<label>How many must approve
|
|
428
|
-
${select(
|
|
429
|
-
requiredKind,
|
|
430
|
-
{
|
|
431
|
-
one: 'Any one of them',
|
|
432
|
-
majority: 'A majority',
|
|
433
|
-
all: 'All of them',
|
|
434
|
-
number: 'A number of them',
|
|
435
|
-
},
|
|
436
|
-
(value) => {
|
|
437
|
-
stage.required = value === 'number' ? 2 : value
|
|
438
|
-
this.#touch()
|
|
439
|
-
},
|
|
440
|
-
)}
|
|
441
|
-
${
|
|
442
|
-
typeof stage.required === 'number'
|
|
443
|
-
? html`<input type="number" min="1" .value=${String(stage.required)} @change=${(
|
|
444
|
-
e,
|
|
445
|
-
) => {
|
|
446
|
-
stage.required = Math.max(1, Number(e.target.value) || 1)
|
|
447
|
-
this.#touch()
|
|
448
|
-
}} />`
|
|
449
|
-
: nothing
|
|
450
|
-
}
|
|
451
|
-
</label>
|
|
452
|
-
<label>Sending back goes to
|
|
453
|
-
${select(
|
|
454
|
-
stage.sendBack,
|
|
455
|
-
{
|
|
456
|
-
editor: 'The editor (the request ends)',
|
|
457
|
-
...Object.fromEntries(earlier.map((a) => [a, `The ${a} stage`])),
|
|
458
|
-
},
|
|
459
|
-
(value) => {
|
|
460
|
-
stage.sendBack = value
|
|
461
|
-
this.#touch()
|
|
462
|
-
},
|
|
463
|
-
)}
|
|
464
|
-
</label>
|
|
465
|
-
${
|
|
466
|
-
editable && this._editing.stages.length > 1
|
|
467
|
-
? html`<uui-button look="secondary" color="danger" label="Remove stage" @click=${() => this.#removeStage(stage.alias)}>Remove this stage</uui-button>`
|
|
509
|
+
}}></uui-input>`
|
|
468
510
|
: nothing
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
|
|
511
|
+
}`,
|
|
512
|
+
'vertical',
|
|
513
|
+
)}
|
|
514
|
+
${property(
|
|
515
|
+
'Sending back goes to',
|
|
516
|
+
undefined,
|
|
517
|
+
select(
|
|
518
|
+
'Sending back goes to',
|
|
519
|
+
stage.sendBack,
|
|
520
|
+
{
|
|
521
|
+
editor: 'The editor (the request ends)',
|
|
522
|
+
...Object.fromEntries(
|
|
523
|
+
earlier.map((a) => [a, `The ${this.#stage(a)?.name || a} stage`]),
|
|
524
|
+
),
|
|
525
|
+
},
|
|
526
|
+
(value) => {
|
|
527
|
+
stage.sendBack = value
|
|
528
|
+
this.#touch()
|
|
529
|
+
},
|
|
530
|
+
!editable,
|
|
531
|
+
),
|
|
532
|
+
'vertical',
|
|
533
|
+
)}`
|
|
472
534
|
}
|
|
473
535
|
|
|
474
536
|
#renderSettings(w, editable) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
537
|
+
const set = (key) => (value) => {
|
|
538
|
+
w[key] = value
|
|
539
|
+
this.#touch()
|
|
540
|
+
}
|
|
541
|
+
return html`<div class="settings">
|
|
542
|
+
${editable ? nothing : html`<uui-box class="notice"><p>Workflows are kept with the site's files, which cannot be changed on this server.</p></uui-box>`}
|
|
543
|
+
<uui-box headline="Workflow">
|
|
544
|
+
${property(
|
|
545
|
+
'Name',
|
|
546
|
+
undefined,
|
|
547
|
+
html`<uui-input label="Name" ?readonly=${!editable} .value=${w.name} @change=${(e) => set('name')(e.target.value)}></uui-input>`,
|
|
548
|
+
)}
|
|
549
|
+
${property(
|
|
550
|
+
'Alias',
|
|
551
|
+
'The name of its file',
|
|
552
|
+
html`<uui-input label="Alias" ?readonly=${!editable} .value=${w.alias} @change=${(e) => set('alias')(e.target.value.trim())}></uui-input>`,
|
|
553
|
+
)}
|
|
554
|
+
${property(
|
|
555
|
+
'Covers the pages under',
|
|
556
|
+
'Each page and everything below it',
|
|
557
|
+
html`<umb-input-document
|
|
558
|
+
?readonly=${!editable}
|
|
487
559
|
.selection=${w.appliesTo.branches}
|
|
488
560
|
@change=${(e) => {
|
|
489
561
|
w.appliesTo.branches = [...(e.target.selection ?? [])]
|
|
490
562
|
this.#touch()
|
|
491
563
|
}}
|
|
492
|
-
></umb-input-document
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
564
|
+
></umb-input-document>`,
|
|
565
|
+
)}
|
|
566
|
+
${property(
|
|
567
|
+
'And every page of these types',
|
|
568
|
+
'Wherever they are, unless a branch above is covered by another workflow',
|
|
569
|
+
html`<div class="checks">${this._list.contentTypes.map(
|
|
496
570
|
(t) =>
|
|
497
|
-
html`<label
|
|
571
|
+
html`<uui-checkbox label=${t.name} ?disabled=${!editable} ?checked=${w.appliesTo.contentTypes.includes(t.alias)} @change=${(
|
|
498
572
|
e,
|
|
499
573
|
) => {
|
|
500
574
|
w.appliesTo.contentTypes = e.target.checked
|
|
501
575
|
? [...w.appliesTo.contentTypes, t.alias]
|
|
502
576
|
: w.appliesTo.contentTypes.filter((a) => a !== t.alias)
|
|
503
577
|
this.#touch()
|
|
504
|
-
}}
|
|
505
|
-
)}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
SELF,
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
<label>When this workflow changes, pages waiting on it ${select(
|
|
534
|
-
w.onChange,
|
|
535
|
-
ON_CHANGE,
|
|
536
|
-
(v) => {
|
|
537
|
-
w.onChange = v
|
|
538
|
-
this.#touch()
|
|
539
|
-
},
|
|
540
|
-
)}</label>
|
|
541
|
-
</fieldset>
|
|
542
|
-
</uui-box>`
|
|
578
|
+
}}>${t.name}</uui-checkbox>`,
|
|
579
|
+
)}</div>`,
|
|
580
|
+
)}
|
|
581
|
+
</uui-box>
|
|
582
|
+
<uui-box headline="Rules">
|
|
583
|
+
${property(
|
|
584
|
+
'Publishers may skip it',
|
|
585
|
+
'People who may publish a page publish it directly',
|
|
586
|
+
html`<uui-toggle label="Publishers may skip it" ?disabled=${!editable} ?checked=${w.publishersMaySkip} @change=${(e) => set('publishersMaySkip')(e.target.checked)}></uui-toggle>`,
|
|
587
|
+
)}
|
|
588
|
+
${property(
|
|
589
|
+
'Unpublishing needs approval',
|
|
590
|
+
'Taking a page down goes through the workflow too',
|
|
591
|
+
html`<uui-toggle label="Unpublishing needs approval" ?disabled=${!editable} ?checked=${w.unpublish} @change=${(e) => set('unpublish')(e.target.checked)}></uui-toggle>`,
|
|
592
|
+
)}
|
|
593
|
+
${property('When a waiting page is changed', undefined, select('When a waiting page is changed', w.onEdit, ON_EDIT, set('onEdit'), !editable))}
|
|
594
|
+
${property(
|
|
595
|
+
'When the person who made the change could approve it',
|
|
596
|
+
undefined,
|
|
597
|
+
select('Self-approval', w.selfApproval, SELF, set('selfApproval'), !editable),
|
|
598
|
+
)}
|
|
599
|
+
${property('Comments on a decision', undefined, select('Comments', w.comments, COMMENTS, set('comments'), !editable))}
|
|
600
|
+
${property(
|
|
601
|
+
'When this workflow changes',
|
|
602
|
+
'What happens to pages already waiting on it',
|
|
603
|
+
select('When this workflow changes', w.onChange, ON_CHANGE, set('onChange'), !editable),
|
|
604
|
+
)}
|
|
605
|
+
</uui-box>
|
|
606
|
+
</div>`
|
|
543
607
|
}
|
|
544
608
|
|
|
545
609
|
static styles = css`
|
|
546
|
-
:host { display: block; height: 100%; }
|
|
610
|
+
:host { display: block; height: 100%; container-type: inline-size; }
|
|
547
611
|
.list { padding: var(--uui-size-layout-1); }
|
|
548
612
|
.create { margin-bottom: var(--uui-size-space-5); }
|
|
549
|
-
.
|
|
550
|
-
|
|
551
|
-
.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
.
|
|
556
|
-
.
|
|
557
|
-
|
|
558
|
-
|
|
613
|
+
.actions { text-align: right; white-space: nowrap; }
|
|
614
|
+
uui-tab-group { --uui-tab-divider: var(--uui-color-border); border-left: 1px solid var(--uui-color-border); border-right: 1px solid var(--uui-color-border); }
|
|
615
|
+
.notice { margin-bottom: var(--uui-size-layout-1); }
|
|
616
|
+
.toolbar { display: flex; flex-wrap: wrap; gap: var(--uui-size-space-3); }
|
|
617
|
+
.stages { display: grid; grid-template-columns: minmax(0, 1fr) minmax(280px, 380px); gap: var(--uui-size-layout-1); }
|
|
618
|
+
.stage { border-left: 1px solid var(--uui-color-border); padding-left: var(--uui-size-layout-1); }
|
|
619
|
+
.stage-header { display: flex; align-items: center; justify-content: space-between; gap: var(--uui-size-space-3); margin-bottom: var(--uui-size-space-3); }
|
|
620
|
+
.stage-header h4 { margin: 0; }
|
|
621
|
+
@container (max-width: 640px) {
|
|
622
|
+
.stages { grid-template-columns: minmax(0, 1fr); }
|
|
623
|
+
.stage { border-left: 0; padding-left: 0; border-top: 1px solid var(--uui-color-border); padding-top: var(--uui-size-layout-1); }
|
|
624
|
+
}
|
|
625
|
+
.settings { display: flex; flex-direction: column; gap: var(--uui-size-layout-1); }
|
|
626
|
+
.canvas { overflow: auto; border: 1px dashed var(--uui-color-border); border-radius: var(--uui-border-radius); padding: var(--uui-size-space-5); min-height: 240px; touch-action: none; }
|
|
627
|
+
umb-property-layout:first-of-type { padding-top: 0; }
|
|
628
|
+
uui-input, uui-select { width: 100%; }
|
|
629
|
+
uui-input.count { margin-top: var(--uui-size-space-3); max-width: 8em; }
|
|
630
|
+
.checks { display: flex; flex-direction: column; gap: var(--uui-size-space-2); }
|
|
631
|
+
.hint { margin-top: 0; color: var(--uui-color-text-alt); }
|
|
632
|
+
.message { margin: 0; color: var(--uui-color-warning-emphasis, #9e7300); }
|
|
633
|
+
.problems { color: var(--uui-color-danger); margin: 0; }
|
|
559
634
|
.not-saved { color: var(--uui-color-danger); }
|
|
560
|
-
.affected {
|
|
635
|
+
.affected { border: 1px solid var(--uui-color-warning); }
|
|
561
636
|
${unsafeCSS(GRAPH_STYLES)}
|
|
562
637
|
`
|
|
563
638
|
}
|
package/src/login-assets.ts
CHANGED
|
@@ -13,6 +13,9 @@ export function resolveLoginAsset(backOfficePath: string, pathname: string): str
|
|
|
13
13
|
const prefix = `${backOfficePath}/login/`
|
|
14
14
|
if (!pathname.startsWith(prefix)) return undefined
|
|
15
15
|
const name = pathname.slice(prefix.length)
|
|
16
|
-
// Only the files we ship; no traversal, no directory listing.
|
|
17
|
-
|
|
16
|
+
// Only the files we ship; no traversal, no directory listing. `device.js` and
|
|
17
|
+
// `remote-approval.js` are the pages a sign-in and a push are approved on (story 107).
|
|
18
|
+
return ['login.js', 'device.js', 'remote-approval.js'].includes(name)
|
|
19
|
+
? join(LOGIN_ASSET_DIR, name)
|
|
20
|
+
: undefined
|
|
18
21
|
}
|
package/src/shell.ts
CHANGED
|
@@ -34,6 +34,8 @@ export interface LoginShellSettings {
|
|
|
34
34
|
* the browser resumes the OAuth flow rather than landing on a blank editor.
|
|
35
35
|
*/
|
|
36
36
|
returnUrl?: string
|
|
37
|
+
/** "Sign in with …" buttons, one per backoffice identity provider (story 031). */
|
|
38
|
+
externalProviders?: ReadonlyArray<{ name: string; url: string }>
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const escapeHtml = (value: string): string =>
|
|
@@ -131,9 +133,71 @@ ${noscript(`${graphics}/login-logo-alternative`)}
|
|
|
131
133
|
${settings.usernameIsEmail ? 'username-is-email' : ''}
|
|
132
134
|
${settings.allowUserInvite ? 'allow-user-invite' : ''}
|
|
133
135
|
${settings.allowPasswordReset ? 'allow-password-reset' : ''}
|
|
134
|
-
${settings.disableLocalLogin ? 'disable-local-login' : ''}
|
|
136
|
+
${settings.disableLocalLogin ? 'disable-local-login' : ''}
|
|
137
|
+
external-providers="${escapeHtml(JSON.stringify(settings.externalProviders ?? []))}">
|
|
135
138
|
</crombolo-login>
|
|
136
139
|
</body>
|
|
137
140
|
</html>
|
|
138
141
|
`
|
|
139
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A page of its own beside the backoffice, for a signed-in person: approving a
|
|
146
|
+
* command-line sign-in or a protected push (story 107). `element` is the custom
|
|
147
|
+
* element's opening and closing tags, attributes already escaped.
|
|
148
|
+
*/
|
|
149
|
+
export function renderStandaloneShell(
|
|
150
|
+
paths: BackOfficePaths,
|
|
151
|
+
importmap: PackageManifestImportmap,
|
|
152
|
+
settings: { title: string; script: string; element: string },
|
|
153
|
+
): string {
|
|
154
|
+
const assets = paths.assetsPath
|
|
155
|
+
return `<!doctype html>
|
|
156
|
+
<html lang="en">
|
|
157
|
+
<head>
|
|
158
|
+
<meta charset="UTF-8" />
|
|
159
|
+
<base href="${paths.backOfficePath}/" />
|
|
160
|
+
<link rel="icon" type="image/svg+xml" href="${assets}/assets/favicon.svg" />
|
|
161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
162
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
163
|
+
<title>${escapeHtml(settings.title)}</title>
|
|
164
|
+
<link rel="stylesheet" href="${assets}/css/light.css" />
|
|
165
|
+
<link rel="stylesheet" href="${paths.pluginPath}/branding/theme.css" />
|
|
166
|
+
<style>body { margin: 0; padding: 0; background-color: #fff7ea; }</style>
|
|
167
|
+
<script type="importmap">
|
|
168
|
+
${JSON.stringify(importmap, null, 2)}
|
|
169
|
+
</script>
|
|
170
|
+
<script type="module" src="${paths.backOfficePath}/login/${settings.script}"></script>
|
|
171
|
+
</head>
|
|
172
|
+
<body class="uui-font uui-text" style="margin: 0; padding: 0; overflow: hidden">
|
|
173
|
+
${settings.element}
|
|
174
|
+
</body>
|
|
175
|
+
</html>
|
|
176
|
+
`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The page a command-line sign-in is approved on: the person is signed in already. */
|
|
180
|
+
export function renderDeviceShell(
|
|
181
|
+
paths: BackOfficePaths,
|
|
182
|
+
importmap: PackageManifestImportmap,
|
|
183
|
+
settings: { title: string; userCode?: string },
|
|
184
|
+
): string {
|
|
185
|
+
return renderStandaloneShell(paths, importmap, {
|
|
186
|
+
title: settings.title,
|
|
187
|
+
script: 'device.js',
|
|
188
|
+
element: `<crombolo-device user-code="${escapeHtml(settings.userCode ?? '')}" logo-image="${MANAGEMENT_API_PATH}/security/back-office/graphics/login-logo-alternative"></crombolo-device>`,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** The page a push to a protected environment is approved or declined on. */
|
|
193
|
+
export function renderRemoteApprovalShell(
|
|
194
|
+
paths: BackOfficePaths,
|
|
195
|
+
importmap: PackageManifestImportmap,
|
|
196
|
+
settings: { title: string; job: string },
|
|
197
|
+
): string {
|
|
198
|
+
return renderStandaloneShell(paths, importmap, {
|
|
199
|
+
title: settings.title,
|
|
200
|
+
script: 'remote-approval.js',
|
|
201
|
+
element: `<crombolo-remote-approval job="${escapeHtml(settings.job)}" logo-image="${MANAGEMENT_API_PATH}/security/back-office/graphics/login-logo-alternative"></crombolo-remote-approval>`,
|
|
202
|
+
})
|
|
203
|
+
}
|