@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.
@@ -11,3 +11,24 @@ export async function fetchReport() {
11
11
  if (!response.ok) throw new Error(`change report responded ${response.status}`)
12
12
  return response.json()
13
13
  }
14
+
15
+ const apiBase = () =>
16
+ `${(document.querySelector('base')?.getAttribute('href') ?? '/umbraco/').replace(/\/$/, '')}/crombolo/api`
17
+
18
+ /** Where a pending import's answers download as the file the bundle can carry. */
19
+ export const answersUrl = (bundleId) =>
20
+ `${apiBase()}/transfers/${encodeURIComponent(bundleId)}/resolutions`
21
+
22
+ /** Answers, re-checks or imports a pending content bundle; the body says how it went. */
23
+ export async function transferAction(bundleId, verb, body = {}) {
24
+ const response = await fetch(`${apiBase()}/transfers/${encodeURIComponent(bundleId)}/${verb}`, {
25
+ method: 'POST',
26
+ credentials: 'include',
27
+ headers: { accept: 'application/json', 'content-type': 'application/json' },
28
+ body: JSON.stringify(body),
29
+ })
30
+ const result = await response.json().catch(() => ({}))
31
+ if (response.status === 403)
32
+ return { ok: false, message: 'You are not allowed to import content.' }
33
+ return { ok: response.ok, ...result }
34
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo Search & sharing",
4
4
  "id": "Crombolo.Seo",
5
- "version": "0.11.0",
5
+ "version": "0.13.1",
6
6
  "allowPublicAccess": false,
7
7
  "extensions": [
8
8
  {
@@ -73,11 +73,11 @@ export function withLayout(content, alias) {
73
73
  export const TSX = 'tsx'
74
74
 
75
75
  /**
76
- * The virtual directory a view is opened in. It mirrors `Views/` on disk so that
77
- * a view's `../schema/content-types.d.ts` resolves in the editor exactly as it
78
- * resolves for `tsc`.
76
+ * The virtual directory a view is opened in. It mirrors `components/` on disk so
77
+ * that a view's `../schema/content-types.d.ts` resolves in the editor exactly as
78
+ * it resolves for `tsc`.
79
79
  */
80
- const VIEWS = 'file:///Views'
80
+ const COMPONENTS = 'file:///components'
81
81
 
82
82
  /**
83
83
  * `bundler` module resolution, which is what the site's tsconfig uses and what
@@ -226,7 +226,7 @@ function teachDialect(editors) {
226
226
  const model = monaco.editor.createModel(
227
227
  created.getValue(),
228
228
  'typescript',
229
- monaco.Uri.parse(`${VIEWS}/view-${++opened}.tsx`),
229
+ monaco.Uri.parse(`${COMPONENTS}/view-${++opened}.tsx`),
230
230
  )
231
231
  editor.setModel(model)
232
232
  created.dispose()
@@ -2,7 +2,7 @@
2
2
  "$schema": "../../backoffice-dist/dist/umbraco-package-schema.json",
3
3
  "name": "Crombolo",
4
4
  "id": "Crombolo.Core",
5
- "version": "0.11.0",
5
+ "version": "0.13.1",
6
6
  "allowPublicAccess": false,
7
7
  "extensions": [
8
8
  {
@@ -62,6 +62,41 @@
62
62
  }
63
63
  ]
64
64
  },
65
+ {
66
+ "type": "workspaceView",
67
+ "alias": "Crombolo.WorkspaceView.Webhook.Storage",
68
+ "name": "Crombolo Webhook Storage View",
69
+ "element": "/crombolo-plugin/webhook-storage.js",
70
+ "weight": 100,
71
+ "meta": {
72
+ "label": "Where it's kept",
73
+ "pathname": "where-kept",
74
+ "icon": "icon-box"
75
+ },
76
+ "conditions": [
77
+ {
78
+ "alias": "Umb.Condition.WorkspaceAlias",
79
+ "match": "Umb.Workspace.Webhook"
80
+ }
81
+ ]
82
+ },
83
+ {
84
+ "type": "dashboard",
85
+ "alias": "Crombolo.Dashboard.Addresses",
86
+ "name": "Crombolo Page Addresses Dashboard",
87
+ "element": "/crombolo-plugin/addresses-dashboard.js",
88
+ "weight": 6,
89
+ "meta": {
90
+ "label": "Page addresses",
91
+ "pathname": "addresses"
92
+ },
93
+ "conditions": [
94
+ {
95
+ "alias": "Umb.Condition.SectionAlias",
96
+ "match": "Umb.Section.Settings"
97
+ }
98
+ ]
99
+ },
65
100
  {
66
101
  "type": "dashboard",
67
102
  "alias": "Crombolo.Dashboard.Changes",
@@ -310,6 +345,112 @@
310
345
  "name": "Crombolo Icons Context",
311
346
  "overwrites": ["Umb.GlobalContext.Icons"],
312
347
  "api": "/crombolo-plugin/branding/icon-registry.js"
348
+ },
349
+ {
350
+ "type": "section",
351
+ "alias": "Crombolo.Section.Workflow",
352
+ "name": "Crombolo Workflow Section",
353
+ "weight": 40,
354
+ "meta": {
355
+ "label": "Workflow",
356
+ "pathname": "workflow"
357
+ }
358
+ },
359
+ {
360
+ "type": "sectionView",
361
+ "alias": "Crombolo.SectionView.Workflow.Waiting",
362
+ "name": "Crombolo Workflow Waiting",
363
+ "element": "/crombolo-plugin/workflow-requests.js",
364
+ "elementName": "crombolo-workflow-waiting",
365
+ "weight": 400,
366
+ "meta": {
367
+ "label": "Waiting for me",
368
+ "pathname": "waiting",
369
+ "icon": "icon-inbox"
370
+ },
371
+ "conditions": [
372
+ {
373
+ "alias": "Umb.Condition.SectionAlias",
374
+ "match": "Crombolo.Section.Workflow"
375
+ }
376
+ ]
377
+ },
378
+ {
379
+ "type": "sectionView",
380
+ "alias": "Crombolo.SectionView.Workflow.InFlight",
381
+ "name": "Crombolo Workflow In Flight",
382
+ "element": "/crombolo-plugin/workflow-requests.js",
383
+ "elementName": "crombolo-workflow-in-flight",
384
+ "weight": 300,
385
+ "meta": {
386
+ "label": "In flight",
387
+ "pathname": "in-flight",
388
+ "icon": "icon-time"
389
+ },
390
+ "conditions": [
391
+ {
392
+ "alias": "Umb.Condition.SectionAlias",
393
+ "match": "Crombolo.Section.Workflow"
394
+ }
395
+ ]
396
+ },
397
+ {
398
+ "type": "sectionView",
399
+ "alias": "Crombolo.SectionView.Workflow.History",
400
+ "name": "Crombolo Workflow History",
401
+ "element": "/crombolo-plugin/workflow-requests.js",
402
+ "elementName": "crombolo-workflow-history",
403
+ "weight": 200,
404
+ "meta": {
405
+ "label": "Decided",
406
+ "pathname": "history",
407
+ "icon": "icon-check"
408
+ },
409
+ "conditions": [
410
+ {
411
+ "alias": "Umb.Condition.SectionAlias",
412
+ "match": "Crombolo.Section.Workflow"
413
+ }
414
+ ]
415
+ },
416
+ {
417
+ "type": "sectionView",
418
+ "alias": "Crombolo.SectionView.Workflow.Designer",
419
+ "name": "Crombolo Workflow Designer",
420
+ "element": "/crombolo-plugin/workflow-designer.js",
421
+ "weight": 100,
422
+ "meta": {
423
+ "label": "Workflows",
424
+ "pathname": "workflows",
425
+ "icon": "icon-mindmap"
426
+ },
427
+ "conditions": [
428
+ {
429
+ "alias": "Umb.Condition.SectionAlias",
430
+ "match": "Crombolo.Section.Workflow"
431
+ },
432
+ {
433
+ "alias": "Umb.Condition.CurrentUser.IsAdmin"
434
+ }
435
+ ]
436
+ },
437
+ {
438
+ "type": "workspaceView",
439
+ "alias": "Crombolo.WorkspaceView.Document.Workflow",
440
+ "name": "Crombolo Document Workflow",
441
+ "element": "/crombolo-plugin/workflow-tab.js",
442
+ "weight": 50,
443
+ "meta": {
444
+ "label": "Workflow",
445
+ "pathname": "workflow",
446
+ "icon": "icon-mindmap"
447
+ },
448
+ "conditions": [
449
+ {
450
+ "alias": "Umb.Condition.WorkspaceAlias",
451
+ "match": "Umb.Workspace.Document"
452
+ }
453
+ ]
313
454
  }
314
455
  ]
315
456
  }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * "Where it's kept", on a webhook's screen: with the site's files, or in this
3
+ * environment only. Moving one is the server's decision to allow; this shows
4
+ * what it said and asks it. docs/webhooks.md.
5
+ */
6
+ import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
7
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
8
+ import { UMB_WEBHOOK_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/webhook'
9
+
10
+ const base = () =>
11
+ `${(document.querySelector('base')?.getAttribute('href') ?? '/umbraco/').replace(/\/$/, '')}/crombolo/api/webhooks`
12
+
13
+ const PLACES = {
14
+ files: {
15
+ label: "With the site's files",
16
+ detail:
17
+ 'Kept in the site’s code and deployed with it. A header that holds a secret names a server setting instead, written as env:NAME.',
18
+ },
19
+ database: {
20
+ label: 'In this environment only',
21
+ detail: 'Kept on this site alone, not in its code. A secret may be typed into a header.',
22
+ },
23
+ }
24
+
25
+ export default class CromboloWebhookStorageElement extends UmbLitElement {
26
+ static properties = {
27
+ _state: { state: true },
28
+ _busy: { state: true },
29
+ _message: { state: true },
30
+ }
31
+
32
+ #unique
33
+
34
+ constructor() {
35
+ super()
36
+ this._state = undefined
37
+ this._busy = false
38
+ this._message = ''
39
+ this.consumeContext(UMB_WEBHOOK_WORKSPACE_CONTEXT, (context) => {
40
+ this.observe(context?.unique, (unique) => {
41
+ this.#unique = unique
42
+ this.#load()
43
+ })
44
+ })
45
+ }
46
+
47
+ async #load() {
48
+ if (!this.#unique) return
49
+ const response = await fetch(`${base()}/${encodeURIComponent(this.#unique)}/store`, {
50
+ credentials: 'include',
51
+ headers: { accept: 'application/json' },
52
+ })
53
+ this._state = response.ok ? await response.json() : { missing: true }
54
+ }
55
+
56
+ async #move(store) {
57
+ this._busy = true
58
+ this._message = ''
59
+ try {
60
+ const response = await fetch(`${base()}/${encodeURIComponent(this.#unique)}/store`, {
61
+ method: 'PUT',
62
+ credentials: 'include',
63
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
64
+ body: JSON.stringify({ store }),
65
+ })
66
+ const body = await response.json().catch(() => ({}))
67
+ if (!response.ok)
68
+ this._message = [body.reason, ...(body.problems ?? [])].filter(Boolean).join(' ')
69
+ await this.#load()
70
+ } finally {
71
+ this._busy = false
72
+ }
73
+ }
74
+
75
+ render() {
76
+ const state = this._state
77
+ if (!state) return html`<uui-box><uui-loader></uui-loader></uui-box>`
78
+ if (state.missing)
79
+ return html`<uui-box><p>Save the webhook first, then choose where it is kept.</p></uui-box>`
80
+ const other = state.store === 'files' ? 'database' : 'files'
81
+ return html`<uui-box headline="Where it's kept">
82
+ <p><strong>${PLACES[state.store].label}.</strong> ${PLACES[state.store].detail}</p>
83
+ ${
84
+ state.available[other]
85
+ ? html`<uui-button
86
+ look="secondary"
87
+ ?disabled=${this._busy}
88
+ label=${`Keep it ${PLACES[other].label.toLowerCase()}`}
89
+ @click=${() => this.#move(other)}
90
+ >Keep it ${PLACES[other].label.toLowerCase()}</uui-button>`
91
+ : html`<p class="fine">${state.unavailable[other]}</p>`
92
+ }
93
+ ${this._message ? html`<p class="problem" role="alert">${this._message}</p>` : nothing}
94
+ </uui-box>`
95
+ }
96
+
97
+ static styles = css`
98
+ .fine {
99
+ color: var(--uui-color-text-alt, #555);
100
+ }
101
+ .problem {
102
+ color: var(--uui-color-danger, #b00);
103
+ }
104
+ `
105
+ }
106
+
107
+ customElements.define('crombolo-webhook-storage', CromboloWebhookStorageElement)
@@ -19,7 +19,7 @@ const STEPS = [
19
19
  {
20
20
  title: 'Write a template',
21
21
  body: html`Templates are <strong>TSX</strong>, not Razor. A template is a file in
22
- <code>Views/</code> that default-exports a component and receives the page as
22
+ <code>components/</code> that default-exports a component and receives the page as
23
23
  <code>model</code>; <code>export const layout = 'alias'</code> names its master.
24
24
  The editor under Settings → Templates writes that shape for you.`,
25
25
  },
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The Workflow screens' calls to <backoffice>/crombolo/api/approvals/ (story 028).
3
+ * The cookie carries the session. Every answer resolves; a refusal carries the
4
+ * server's own words in `message`.
5
+ */
6
+ const apiBase = () =>
7
+ `${(document.querySelector('base')?.getAttribute('href') ?? '/umbraco/').replace(/\/$/, '')}/crombolo/api/approvals`
8
+
9
+ async function call(path, init = {}) {
10
+ const response = await fetch(`${apiBase()}/${path}`, {
11
+ credentials: 'include',
12
+ ...init,
13
+ headers: { accept: 'application/json', 'content-type': 'application/json', ...init.headers },
14
+ })
15
+ const body = await response.json().catch(() => ({}))
16
+ if (response.status === 403 && !body.message)
17
+ return { ok: false, message: 'You are not allowed to do that.' }
18
+ return { ok: response.ok, status: response.status, ...body }
19
+ }
20
+
21
+ const post = (path, body = {}) => call(path, { method: 'POST', body: JSON.stringify(body) })
22
+
23
+ export const pageApproval = (key) => call(`page/${encodeURIComponent(key)}`)
24
+ export const sendForApproval = (key, action, comment) =>
25
+ post(`page/${encodeURIComponent(key)}/send`, { action, comment })
26
+ export const decide = (requestKey, verb, body) =>
27
+ post(`request/${encodeURIComponent(requestKey)}/${verb}`, body)
28
+ export const waitingForMe = () => call('waiting')
29
+ export const inFlight = () => call('in-flight')
30
+ export const finished = (skip = 0, take = 25) => call(`history?skip=${skip}&take=${take}`)
31
+ export const listWorkflows = () => call('workflows')
32
+ export const previewWorkflow = (alias, workflow) =>
33
+ post(`workflows/${encodeURIComponent(alias)}/preview`, { workflow })
34
+ export const saveWorkflow = (alias, workflow) =>
35
+ call(`workflows/${encodeURIComponent(alias)}`, {
36
+ method: 'PUT',
37
+ body: JSON.stringify({ workflow }),
38
+ })
39
+ export const deleteWorkflow = (alias) =>
40
+ call(`workflows/${encodeURIComponent(alias)}`, { method: 'DELETE' })