@crombolo/backoffice-host 0.13.1 → 0.14.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.
Files changed (53) hide show
  1. package/assets/login.js +89 -2
  2. package/package.json +2 -2
  3. package/plugin/assistant/umbraco-package.json +1 -1
  4. package/plugin/branding/branding.js +9 -14
  5. package/plugin/branding/bundle-builder-labels.js +30 -1
  6. package/plugin/branding/localization-en.js +8 -0
  7. package/plugin/branding/sign-in.js +31 -0
  8. package/plugin/branding/theme.css +2 -0
  9. package/plugin/bundles-created-overview.js +31 -18
  10. package/plugin/bundles-installed.js +127 -61
  11. package/plugin/bundles-marketplace.js +1 -1
  12. package/plugin/changes-dashboard.js +8 -1
  13. package/plugin/download.js +19 -0
  14. package/plugin/form-picker.js +1 -1
  15. package/plugin/forms-client.js +5 -0
  16. package/plugin/forms-entries.js +30 -16
  17. package/plugin/forms-overview.js +55 -33
  18. package/plugin/localizations/ar.js +3 -39
  19. package/plugin/localizations/bs.js +3 -29
  20. package/plugin/localizations/cs.js +3 -24
  21. package/plugin/localizations/cy.js +3 -32
  22. package/plugin/localizations/da.js +3 -41
  23. package/plugin/localizations/de.js +3 -27
  24. package/plugin/localizations/es.js +3 -19
  25. package/plugin/localizations/fr.js +3 -26
  26. package/plugin/localizations/hr.js +3 -29
  27. package/plugin/localizations/it.js +3 -29
  28. package/plugin/localizations/ja.js +3 -11
  29. package/plugin/localizations/ko.js +3 -11
  30. package/plugin/localizations/nb.js +3 -18
  31. package/plugin/localizations/nl.js +3 -29
  32. package/plugin/localizations/pl.js +3 -11
  33. package/plugin/localizations/pt-br.js +0 -10
  34. package/plugin/localizations/pt.js +3 -39
  35. package/plugin/localizations/ru.js +3 -9
  36. package/plugin/localizations/sv.js +3 -20
  37. package/plugin/localizations/tr.js +3 -26
  38. package/plugin/localizations/uk.js +3 -9
  39. package/plugin/localizations/umbraco-package.json +22 -2
  40. package/plugin/localizations/vi.js +3 -42
  41. package/plugin/localizations/zh-tw.js +0 -6
  42. package/plugin/localizations/zh.js +0 -6
  43. package/plugin/seo/umbraco-package.json +1 -1
  44. package/plugin/sign-in/entry.js +20 -0
  45. package/plugin/sign-in/sign-in-dialog.js +127 -0
  46. package/plugin/sign-in/umbraco-package.json +15 -0
  47. package/plugin/two-factor-setup.js +147 -0
  48. package/plugin/umbraco-package.json +30 -30
  49. package/plugin/welcome-dashboard.js +2 -5
  50. package/plugin/workflow-designer.js +298 -187
  51. package/plugin/workflow-requests.js +19 -6
  52. package/src/shell.ts +4 -1
  53. 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 above. Saving writes `schema/workflows/<alias>.toml`; before it
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
@@ -9,7 +9,9 @@
9
9
  */
10
10
  // For its side effect: registers `umb-input-document`, which the shell loads lazily.
11
11
  import '@umbraco-cms/backoffice/document'
12
- import { css, html, LitElement, nothing, unsafeCSS } from '@umbraco-cms/backoffice/external/lit'
12
+ import { css, html, nothing, unsafeCSS } from '@umbraco-cms/backoffice/external/lit'
13
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
14
+ import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'
13
15
  import { deleteWorkflow, listWorkflows, previewWorkflow, saveWorkflow } from './workflow-client.js'
14
16
  import { ancestors, BOX, drawGraph, GRAPH_STYLES, layout, wouldLoop } from './workflow-graph.js'
15
17
 
@@ -58,15 +60,26 @@ const blank = () => ({
58
60
  ],
59
61
  })
60
62
 
61
- const select = (
62
- value,
63
- options,
64
- onChange,
65
- ) => html`<select @change=${(e) => onChange(e.target.value)}>
66
- ${Object.entries(options).map(([key, label]) => html`<option value=${key} ?selected=${key === value}>${label}</option>`)}
67
- </select>`
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>`
68
69
 
69
- export default class CromboloWorkflowDesignerElement extends LitElement {
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>`
81
+
82
+ export default class CromboloWorkflowDesignerElement extends UmbLitElement {
70
83
  static properties = {
71
84
  _list: { state: true },
72
85
  _editing: { state: true },
@@ -76,9 +89,18 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
76
89
  _problems: { state: true },
77
90
  _affected: { state: true },
78
91
  _message: { state: true },
92
+ _tab: { state: true },
79
93
  }
80
94
 
81
95
  #drag
96
+ #notifications
97
+
98
+ constructor() {
99
+ super()
100
+ this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => {
101
+ this.#notifications = context
102
+ })
103
+ }
82
104
 
83
105
  connectedCallback() {
84
106
  super.connectedCallback()
@@ -105,6 +127,7 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
105
127
  this._problems = undefined
106
128
  this._affected = undefined
107
129
  this._message = undefined
130
+ this._tab = 'settings'
108
131
  }
109
132
 
110
133
  #touch() {
@@ -247,19 +270,21 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
247
270
  return
248
271
  }
249
272
  this._affected = undefined
250
- this._message = 'Saved.'
273
+ // The green toast every workspace shows on a save.
274
+ this.#notifications?.peek('positive', { data: { message: 'Workflow saved' } })
251
275
  this._originalAlias = this._editing.alias
252
276
  await this.#load()
253
277
  }
254
278
 
255
- async #delete() {
279
+ /** From the list or the editor: the workflow by its name and the alias its file has. */
280
+ async #delete(name, alias) {
256
281
  if (
257
282
  !window.confirm(
258
- `Delete the workflow "${this._editing.name}"? Pages it covers will publish without approval.`,
283
+ `Delete the workflow "${name}"? Pages it covers will publish without approval.`,
259
284
  )
260
285
  )
261
286
  return
262
- const result = await deleteWorkflow(this._originalAlias)
287
+ const result = await deleteWorkflow(alias)
263
288
  if (!result.ok) {
264
289
  this._message = result.message
265
290
  return
@@ -270,15 +295,24 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
270
295
 
271
296
  render() {
272
297
  if (this._message && !this._list)
273
- return html`<uui-box headline="Workflows">${this._message}</uui-box>`
274
- if (!this._list) return html`<uui-box headline="Workflows"><uui-loader></uui-loader></uui-box>`
298
+ return html`<div class="list"><uui-box headline="Workflows">${this._message}</uui-box></div>`
299
+ if (!this._list)
300
+ return html`<div class="list"><uui-box headline="Workflows"><uui-loader></uui-loader></uui-box></div>`
275
301
  if (!this._editing) return this.#renderList()
276
302
  return this.#renderEditor()
277
303
  }
278
304
 
279
305
  #renderList() {
280
306
  const { items, editable } = this._list
281
- return html`<uui-box headline="Workflows">
307
+ // The create button above the list, as the Created bundles view has it.
308
+ return html`<div class="list">
309
+ ${
310
+ editable
311
+ ? html`<uui-button class="create" color="default" look="outline" label="Create" @click=${() => this.#edit(blank())}>Create</uui-button>`
312
+ : nothing
313
+ }
314
+ ${this._message ? html`<uui-box><p class="message">${this._message}</p></uui-box>` : nothing}
315
+ <uui-box headline="Workflows">
282
316
  ${editable ? nothing : html`<p>Workflows are kept with the site's files, which cannot be changed on this server. Change them in the repository and deploy.</p>`}
283
317
  ${
284
318
  items.length === 0
@@ -288,13 +322,11 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
288
322
  <uui-table-head-cell>Workflow</uui-table-head-cell>
289
323
  <uui-table-head-cell>Covers</uui-table-head-cell>
290
324
  <uui-table-head-cell>Stages</uui-table-head-cell>
325
+ <uui-table-head-cell class="actions"></uui-table-head-cell>
291
326
  </uui-table-head>
292
327
  ${items.map(
293
328
  (w) => html`<uui-table-row>
294
- <uui-table-cell><a href="#" @click=${(e) => {
295
- e.preventDefault()
296
- this.#edit(w)
297
- }}>${w.name}</a></uui-table-cell>
329
+ <uui-table-cell>${w.name}</uui-table-cell>
298
330
  <uui-table-cell>${[
299
331
  w.appliesTo.branches.length ? `${w.appliesTo.branches.length} branch(es)` : '',
300
332
  ...w.appliesTo.contentTypes,
@@ -302,55 +334,92 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
302
334
  .filter(Boolean)
303
335
  .join(', ')}</uui-table-cell>
304
336
  <uui-table-cell>${w.stages.length}</uui-table-cell>
337
+ <uui-table-cell class="actions">
338
+ <uui-button look="secondary" label=${editable ? 'Edit' : 'View'} @click=${() => this.#edit(w)}></uui-button>
339
+ ${
340
+ editable
341
+ ? html`<uui-button look="secondary" color="danger" label="Delete" @click=${() => this.#delete(w.name, w.alias)}></uui-button>`
342
+ : nothing
343
+ }
344
+ </uui-table-cell>
305
345
  </uui-table-row>`,
306
346
  )}
307
347
  </uui-table>`
308
348
  }
309
- ${editable ? html`<uui-button look="primary" label="New workflow" @click=${() => this.#edit(blank())}>New workflow</uui-button>` : nothing}
310
- </uui-box>`
349
+ </uui-box>
350
+ </div>`
311
351
  }
312
352
 
313
353
  #renderEditor() {
314
354
  const w = this._editing
315
355
  const editable = this._list.editable
316
- const stage = this.#stage(this._selected)
317
- return html`
318
- <uui-box headline=${w.name || 'Workflow'}>
319
- <div class="toolbar">
320
- <uui-button look="secondary" label="Back" @click=${() => {
321
- this._editing = undefined
322
- }}>Back to the list</uui-button>
323
- ${
324
- editable
325
- ? html`
326
- <uui-button look="secondary" label="Add a stage" @click=${() => this.#addStage()}>Add a stage</uui-button>
327
- <uui-button look=${this._connecting ? 'primary' : 'secondary'} label="Connect stages"
328
- @click=${() => {
329
- this._connecting = this._connecting ? undefined : true
330
- }}
331
- >${this._connecting === true ? 'Click the stage to draw from' : this._connecting ? 'Now click the stage it leads to' : 'Connect stages'}</uui-button>
332
- <uui-button look="primary" label="Save" @click=${() => this.#save()}>Save</uui-button>
333
- ${
334
- this._originalAlias &&
335
- this._list.items.some((i) => i.alias === this._originalAlias)
336
- ? html`<uui-button look="secondary" color="danger" label="Delete" @click=${() => this.#delete()}>Delete</uui-button>`
337
- : nothing
338
- }
339
- `
340
- : nothing
341
- }
342
- </div>
343
- ${this._message ? html`<p class="message">${this._message}</p>` : nothing}
344
- ${this._problems ? html`<ul class="problems">${this._problems.map((p) => html`<li>${p.path ? html`<code>${p.path}</code> ` : nothing}${p.message}</li>`)}</ul>` : nothing}
345
- ${
346
- this._affected
347
- ? html`<div class="affected">
356
+ const leave = () => {
357
+ this._editing = undefined
358
+ this._message = undefined
359
+ this._problems = undefined
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>`
368
+ // Umbraco's own layout, so Save sits bottom right in the footer as it does
369
+ // in every workspace, with Back beside it, and the tabs top right.
370
+ return html`<umb-body-layout headline=${w.name || 'Workflow'}>
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
+ ${
382
+ this._affected
383
+ ? html`<uui-box class="notice affected" headline="Pages waiting for approval">
348
384
  <p>Saving changes the rules for ${this._affected.length} page(s) waiting for approval:</p>
349
385
  <ul>${this._affected.map((a) => html`<li>${a.page}: ${ON_CHANGE[a.outcome]}</li>`)}</ul>
350
386
  <uui-button look="primary" label="Save anyway" @click=${() => this.#save(true)}>Save anyway</uui-button>
387
+ </uui-box>`
388
+ : nothing
389
+ }
390
+ ${this._tab === 'stages' ? this.#renderStages(w, editable) : this.#renderSettings(w, editable)}
391
+ ${
392
+ this._problems
393
+ ? html`<span slot="footer-info" class="not-saved">Not saved: ${this._problems.length} problem(s), listed above</span>`
394
+ : nothing
395
+ }
396
+ <uui-button slot="actions" look="secondary" label="Back to the list" @click=${leave}>Back to the list</uui-button>
397
+ ${
398
+ editable
399
+ ? html`<uui-button slot="actions" look="primary" color="positive" label="Save" @click=${() => this.#save()}>Save</uui-button>`
400
+ : nothing
401
+ }
402
+ </umb-body-layout>`
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>
351
418
  </div>`
352
419
  : nothing
353
420
  }
421
+ <div class="stages">
422
+ <div class="graph">
354
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>
355
424
  <div class="canvas" @pointermove=${(e) => editable && this.#pointerMove(e)} @pointerup=${() => this.#pointerUp()} @pointerleave=${() => this.#pointerUp()}>
356
425
  ${drawGraph(w.stages, {
@@ -360,168 +429,210 @@ export default class CromboloWorkflowDesignerElement extends LitElement {
360
429
  onPointerDown: (alias, event) => editable && this.#pointerDown(alias, event),
361
430
  })}
362
431
  </div>
363
- </uui-box>
364
- <div class="panels">
365
- ${stage ? this.#renderStage(stage, editable) : nothing}
366
- ${this.#renderSettings(w, editable)}
367
432
  </div>
368
- `
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>`
369
438
  }
370
439
 
371
440
  #renderStage(stage, editable) {
372
441
  const earlier = [...ancestors(this._editing.stages, stage.alias)]
373
442
  const requiredKind = typeof stage.required === 'number' ? 'number' : stage.required
374
- return html`<uui-box headline=${`Stage: ${stage.name || stage.alias}`}>
375
- <fieldset ?disabled=${!editable}>
376
- <label>Name <uui-input label="Name" .value=${stage.name ?? ''} @change=${(e) => {
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
+ ) => {
377
457
  stage.name = e.target.value
378
458
  this.#touch()
379
- }}></uui-input></label>
380
- <label>Alias <uui-input label="Alias" .value=${stage.alias} @change=${(e) => this.#renameStage(stage, e.target.value)}></uui-input></label>
381
- <div class="field"><span>Approved by</span>
382
- ${this._list.groups.map(
383
- (g) =>
384
- html`<label class="check"><input type="checkbox" .checked=${stage.approvers.includes(g.alias)} @change=${(
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=${(
385
505
  e,
386
506
  ) => {
387
- stage.approvers = e.target.checked
388
- ? [...stage.approvers, g.alias]
389
- : stage.approvers.filter((a) => a !== g.alias)
507
+ stage.required = Math.max(1, Number(e.target.value) || 1)
390
508
  this.#touch()
391
- }} /> ${g.name}</label>`,
392
- )}
393
- </div>
394
- <label>How many must approve
395
- ${select(
396
- requiredKind,
397
- {
398
- one: 'Any one of them',
399
- majority: 'A majority',
400
- all: 'All of them',
401
- number: 'A number of them',
402
- },
403
- (value) => {
404
- stage.required = value === 'number' ? 2 : value
405
- this.#touch()
406
- },
407
- )}
408
- ${
409
- typeof stage.required === 'number'
410
- ? html`<input type="number" min="1" .value=${String(stage.required)} @change=${(
411
- e,
412
- ) => {
413
- stage.required = Math.max(1, Number(e.target.value) || 1)
414
- this.#touch()
415
- }} />`
416
- : nothing
417
- }
418
- </label>
419
- <label>Sending back goes to
420
- ${select(
421
- stage.sendBack,
422
- {
423
- editor: 'The editor (the request ends)',
424
- ...Object.fromEntries(earlier.map((a) => [a, `The ${a} stage`])),
425
- },
426
- (value) => {
427
- stage.sendBack = value
428
- this.#touch()
429
- },
430
- )}
431
- </label>
432
- ${
433
- editable && this._editing.stages.length > 1
434
- ? html`<uui-button look="secondary" color="danger" label="Remove stage" @click=${() => this.#removeStage(stage.alias)}>Remove this stage</uui-button>`
509
+ }}></uui-input>`
435
510
  : nothing
436
- }
437
- </fieldset>
438
- </uui-box>`
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
+ )}`
439
534
  }
440
535
 
441
536
  #renderSettings(w, editable) {
442
- return html`<uui-box headline="Settings">
443
- <fieldset ?disabled=${!editable}>
444
- <label>Name <uui-input label="Name" .value=${w.name} @change=${(e) => {
445
- w.name = e.target.value
446
- this.#touch()
447
- }}></uui-input></label>
448
- <label>Alias <uui-input label="Alias" .value=${w.alias} @change=${(e) => {
449
- w.alias = e.target.value.trim()
450
- this.#touch()
451
- }}></uui-input></label>
452
- <div class="field"><span>Covers the pages under</span>
453
- <umb-input-document
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}
454
559
  .selection=${w.appliesTo.branches}
455
560
  @change=${(e) => {
456
561
  w.appliesTo.branches = [...(e.target.selection ?? [])]
457
562
  this.#touch()
458
563
  }}
459
- ></umb-input-document>
460
- </div>
461
- <div class="field"><span>And every page of these types</span>
462
- ${this._list.contentTypes.map(
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(
463
570
  (t) =>
464
- html`<label class="check"><input type="checkbox" .checked=${w.appliesTo.contentTypes.includes(t.alias)} @change=${(
571
+ html`<uui-checkbox label=${t.name} ?disabled=${!editable} ?checked=${w.appliesTo.contentTypes.includes(t.alias)} @change=${(
465
572
  e,
466
573
  ) => {
467
574
  w.appliesTo.contentTypes = e.target.checked
468
575
  ? [...w.appliesTo.contentTypes, t.alias]
469
576
  : w.appliesTo.contentTypes.filter((a) => a !== t.alias)
470
577
  this.#touch()
471
- }} /> ${t.name}</label>`,
472
- )}
473
- </div>
474
- <label class="check"><input type="checkbox" .checked=${w.publishersMaySkip} @change=${(
475
- e,
476
- ) => {
477
- w.publishersMaySkip = e.target.checked
478
- this.#touch()
479
- }} /> People who may publish can publish directly</label>
480
- <label class="check"><input type="checkbox" .checked=${w.unpublish} @change=${(e) => {
481
- w.unpublish = e.target.checked
482
- this.#touch()
483
- }} /> Taking a page down needs approval too</label>
484
- <label>When a waiting page is changed ${select(w.onEdit, ON_EDIT, (v) => {
485
- w.onEdit = v
486
- this.#touch()
487
- })}</label>
488
- <label>When the person who made the change could approve it ${select(
489
- w.selfApproval,
490
- SELF,
491
- (v) => {
492
- w.selfApproval = v
493
- this.#touch()
494
- },
495
- )}</label>
496
- <label>Comments ${select(w.comments, COMMENTS, (v) => {
497
- w.comments = v
498
- this.#touch()
499
- })}</label>
500
- <label>When this workflow changes, pages waiting on it ${select(
501
- w.onChange,
502
- ON_CHANGE,
503
- (v) => {
504
- w.onChange = v
505
- this.#touch()
506
- },
507
- )}</label>
508
- </fieldset>
509
- </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>`
510
607
  }
511
608
 
512
609
  static styles = css`
513
- :host { display: block; padding: var(--uui-size-layout-1); }
514
- .toolbar { display: flex; flex-wrap: wrap; gap: var(--uui-size-space-3); margin-bottom: var(--uui-size-space-4); }
515
- .canvas { overflow: auto; border: 1px dashed var(--uui-color-border); border-radius: 6px; padding: var(--uui-size-space-5); min-height: 200px; touch-action: none; }
516
- .panels { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: var(--uui-size-space-5); margin-top: var(--uui-size-space-5); }
517
- fieldset { border: 0; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--uui-size-space-4); }
518
- label, .field { display: flex; flex-direction: column; gap: var(--uui-size-space-2); }
519
- label.check { flex-direction: row; align-items: center; }
520
- .field > span { font-weight: 700; }
521
- .hint { opacity: 0.8; font-size: 0.9em; }
522
- .message { color: var(--uui-color-warning-emphasis, #9e7300); }
523
- .problems { color: var(--uui-color-danger); }
524
- .affected { border: 1px solid var(--uui-color-warning); padding: var(--uui-size-space-4); border-radius: 6px; margin-bottom: var(--uui-size-space-4); }
610
+ :host { display: block; height: 100%; container-type: inline-size; }
611
+ .list { padding: var(--uui-size-layout-1); }
612
+ .create { margin-bottom: var(--uui-size-space-5); }
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; }
634
+ .not-saved { color: var(--uui-color-danger); }
635
+ .affected { border: 1px solid var(--uui-color-warning); }
525
636
  ${unsafeCSS(GRAPH_STYLES)}
526
637
  `
527
638
  }