@crombolo/backoffice-host 0.0.0-stage → 0.9.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +69 -0
  3. package/assets/login.js +352 -0
  4. package/package.json +35 -4
  5. package/plugin/assistant/client.js +53 -0
  6. package/plugin/assistant/drawer.js +865 -0
  7. package/plugin/assistant/umbraco-package.json +16 -0
  8. package/plugin/branding/backdrop.svg +1 -0
  9. package/plugin/branding/branding.js +100 -0
  10. package/plugin/branding/bundle-builder-labels.js +63 -0
  11. package/plugin/branding/client-credentials.js +49 -0
  12. package/plugin/branding/components-tree.js +40 -0
  13. package/plugin/branding/content-type-labels.js +27 -0
  14. package/plugin/branding/favicon.svg +1 -0
  15. package/plugin/branding/help-menu.js +46 -0
  16. package/plugin/branding/icon-mark.js +15 -0
  17. package/plugin/branding/icon-registry.js +40 -0
  18. package/plugin/branding/localization-en.js +115 -0
  19. package/plugin/branding/log-viewer-menu.js +70 -0
  20. package/plugin/branding/logo-header.svg +1 -0
  21. package/plugin/branding/logo-surface.svg +1 -0
  22. package/plugin/branding/query-builder.js +28 -0
  23. package/plugin/branding/sign-in.js +80 -0
  24. package/plugin/branding/sysinfo.js +89 -0
  25. package/plugin/branding/theme.css +102 -0
  26. package/plugin/bundles-client.js +34 -0
  27. package/plugin/bundles-created-overview.js +150 -0
  28. package/plugin/bundles-created.js +70 -0
  29. package/plugin/bundles-installed.js +147 -0
  30. package/plugin/bundles-marketplace.js +199 -0
  31. package/plugin/changes-dashboard.js +132 -0
  32. package/plugin/element-type-hint.js +58 -0
  33. package/plugin/form-picker.js +116 -0
  34. package/plugin/forms-client.js +52 -0
  35. package/plugin/forms-entries.js +244 -0
  36. package/plugin/forms-overview.js +380 -0
  37. package/plugin/localizations/ar.js +97 -0
  38. package/plugin/localizations/bs.js +86 -0
  39. package/plugin/localizations/cs.js +73 -0
  40. package/plugin/localizations/cy.js +92 -0
  41. package/plugin/localizations/da.js +101 -0
  42. package/plugin/localizations/de.js +79 -0
  43. package/plugin/localizations/en.js +71 -0
  44. package/plugin/localizations/es.js +52 -0
  45. package/plugin/localizations/fr.js +78 -0
  46. package/plugin/localizations/he.js +24 -0
  47. package/plugin/localizations/hr.js +89 -0
  48. package/plugin/localizations/it.js +86 -0
  49. package/plugin/localizations/ja.js +43 -0
  50. package/plugin/localizations/ko.js +24 -0
  51. package/plugin/localizations/nb.js +44 -0
  52. package/plugin/localizations/nl.js +85 -0
  53. package/plugin/localizations/pl.js +44 -0
  54. package/plugin/localizations/pt-br.js +25 -0
  55. package/plugin/localizations/pt.js +100 -0
  56. package/plugin/localizations/ro.js +9 -0
  57. package/plugin/localizations/ru.js +46 -0
  58. package/plugin/localizations/sv.js +46 -0
  59. package/plugin/localizations/tr.js +80 -0
  60. package/plugin/localizations/uk.js +46 -0
  61. package/plugin/localizations/umbraco-package.json +279 -0
  62. package/plugin/localizations/vi.js +104 -0
  63. package/plugin/localizations/zh-tw.js +38 -0
  64. package/plugin/localizations/zh.js +38 -0
  65. package/plugin/read-only-banner.js +48 -0
  66. package/plugin/report-client.js +13 -0
  67. package/plugin/seo/client.js +35 -0
  68. package/plugin/seo/dashboard.js +242 -0
  69. package/plugin/seo/document-view.js +161 -0
  70. package/plugin/seo/type-hint.js +45 -0
  71. package/plugin/seo/umbraco-package.json +61 -0
  72. package/plugin/settings-dashboard.js +123 -0
  73. package/plugin/tsx-editors.js +337 -0
  74. package/plugin/tsx-snippets.js +63 -0
  75. package/plugin/umbraco-package.json +298 -0
  76. package/plugin/welcome-dashboard.js +106 -0
  77. package/src/branding.ts +59 -0
  78. package/src/extensions.ts +228 -0
  79. package/src/graphics.ts +87 -0
  80. package/src/index.ts +7 -0
  81. package/src/login-assets.ts +18 -0
  82. package/src/manifests.ts +154 -0
  83. package/src/paths.ts +117 -0
  84. package/src/shell.ts +139 -0
  85. package/src/static.ts +107 -0
  86. package/src/text-imports.d.ts +23 -0
  87. package/README.md +0 -3
@@ -0,0 +1,104 @@
1
+ // Generated by `bun run localizations:build`. Do not edit.
2
+ // The product name in this language's own wording, for the keys
3
+ // plugin/branding/localization-en.js curates, and "content type" where
4
+ // Umbraco says "document type".
5
+ export default {
6
+ "login": {
7
+ "instruction": "Đăng nhập vào Crombolo"
8
+ },
9
+ "user": {
10
+ "changePasswordDescription": "Bạn có thể thay đổi mật khẩu của mình để truy cập vào Crombolo backoffice bằng cách điền vào mẫu dưới đây và nhấp vào nút 'Đổi mật khẩu'",
11
+ "createUserHelp": "Tạo người dùng mới để cấp quyền truy cập cho họ vào Crombolo. Khi một người dùng mới được tạo, một mật khẩu sẽ được tạo ra mà bạn có thể chia sẻ với người dùng đó.",
12
+ "inviteUserHelp": "Mời người dùng mới để cấp quyền truy cập cho họ vào Crombolo. Một email mời sẽ được gửi đến người dùng với thông tin về cách đăng nhập vào Crombolo. Thời gian hiệu lực của lời mời là 72 giờ.",
13
+ "noConsole": "Vô hiệu hóa quyền truy cập Crombolo",
14
+ "userCreatedSuccessHelp": "Người dùng mới đã được tạo thành công. Để đăng nhập vào Crombolo, hãy sử dụng mật khẩu bên dưới.",
15
+ "userInvitedSuccessHelp": "Một lời mời đã được gửi đến người dùng mới với thông tin chi tiết về cách đăng nhập vào Crombolo.",
16
+ "userinviteWelcomeMessage": "Xin chào và chào mừng bạn đến với Crombolo! Chỉ trong 1 phút, bạn sẽ sẵn sàng sử dụng. Chúng tôi chỉ cần bạn tạo mật khẩu và thêm hình đại diện cho tài khoản.",
17
+ "userinviteExpiredMessage": "Chào mừng bạn đến với Crombolo! Rất tiếc, lời mời của bạn đã hết hạn. Vui lòng liên hệ với quản trị viên của bạn và yêu cầu họ gửi lại lời mời.",
18
+ "documentType": "Loại nội dung"
19
+ },
20
+ "general": {
21
+ "umbracoInfo": "Thông tin Crombolo"
22
+ },
23
+ "buttons": {
24
+ "viewSystemDetails": "Xem thông tin hệ thống Crombolo CMS và số phiên bản"
25
+ },
26
+ "visuallyHiddenTexts": {
27
+ "searchOverlayTitle": "Tìm kiếm trong Crombolo backoffice"
28
+ },
29
+ "languages": {
30
+ "defaultLanguageHelp": "Một trang Crombolo chỉ có thể có một ngôn ngữ mặc định được thiết lập."
31
+ },
32
+ "paste": {
33
+ "errorMessage": "Văn bản bạn đang cố dán chứa các ký tự hoặc định dạng đặc biệt. Điều này có thể do việc sao chép văn bản từ Microsoft Word. Crombolo có thể tự động loại bỏ các ký tự hoặc định dạng đặc biệt, vì vậy nội dung được dán sẽ phù hợp hơn với web."
34
+ },
35
+ "dashboard": {
36
+ "nothinghappens": "Nếu Crombolo không mở, bạn có thể cần cho phép cửa sổ bật lên từ trang này"
37
+ },
38
+ "logViewer": {
39
+ "searchUmbracoSource": "Tìm kiếm mã nguồn Crombolo",
40
+ "searchWithinUmbracoSourceCodeOnGithub": "Tìm trong mã nguồn Crombolo trên GitHub",
41
+ "searchUmbracoIssues": "Tìm kiếm vấn đề của Crombolo",
42
+ "searchUmbracoIssuesOnGithub": "Tìm vấn đề của Crombolo trên GitHub"
43
+ },
44
+ "defaultdialogs": {
45
+ "linkYourConfirm": "Bạn sắp liên kết tài khoản Crombolo và {0} của mình và bạn sẽ được chuyển hướng đến {0} để xác nhận.",
46
+ "unLinkYourConfirm": "Bạn sắp hủy liên kết tài khoản Crombolo và {0} của mình và bạn sẽ bị đăng xuất."
47
+ },
48
+ "actions": {
49
+ "changeDocType": "Thay đổi loại nội dung",
50
+ "exportDocumentType": "Xuất khẩu loại nội dung",
51
+ "importdocumenttype": "Nhập khẩu loại nội dung"
52
+ },
53
+ "actionDescriptions": {
54
+ "changeDocType": "Cho phép truy cập để thay đổi loại nội dung cho một tài liệu"
55
+ },
56
+ "content": {
57
+ "documentType": "Loại Nội dung"
58
+ },
59
+ "create": {
60
+ "createContentBlueprint": "Chọn loại nội dung bạn muốn tạo bản mẫu tài liệu cho",
61
+ "noDocumentTypes": "Không có loại nội dung nào được phép để tạo nội dung ở đây. Bạn phải kích hoạt chúng trong <strong>Content Types</strong> trong phần <strong>Settings</strong>, bằng cách chỉnh sửa <strong>Allowed child node types</strong> dưới <strong>Structure</strong>.",
62
+ "noDocumentTypesAtRoot": "Không có loại nội dung nào có sẵn để tạo nội dung ở đây. Bạn phải tạo chúng trong <strong>Content Types</strong> trong phần <strong>Settings</strong>.",
63
+ "noDocumentTypesEditPermissions": "Chỉnh sửa quyền cho loại nội dung này",
64
+ "noDocumentTypesCreateNew": "Tạo một loại nội dung mới",
65
+ "noDocumentTypesAllowedAtRoot": "Không có loại nội dung nào được phép có sẵn để tạo nội dung ở đây. Bạn phải kích hoạt chúng trong <strong>Content Types</strong> trong phần <strong>Settings</strong>, bằng cách thay đổi tùy chọn <strong>Allow as root</strong> dưới <strong>Structure</strong>.",
66
+ "documentTypeWithoutTemplate": "Loại nội dung không có mẫu",
67
+ "documentTypeWithTemplate": "Loại nội dung có mẫu",
68
+ "documentType": "Loại nội dung",
69
+ "compositionDescription": "Xác định một tập hợp thuộc tính có thể tái sử dụng, được đưa vào định nghĩa của nhiều Loại nội dung khác nhau. Ví dụ: một tập hợp 'Cài đặt trang chung'."
70
+ },
71
+ "prompt": {
72
+ "doctypeChangeWarning": "Bạn có thay đổi chưa lưu. Thay đổi Loại nội dung sẽ hủy bỏ các thay đổi."
73
+ },
74
+ "settings": {
75
+ "importDocumentTypeHelp": "Để nhập một loại nội dung, hãy tìm tệp \".udt\" trên máy tính của bạn bằng cách nhấp vào nút \"Nhập\" (bạn sẽ được yêu cầu xác nhận trên màn hình tiếp theo)"
76
+ },
77
+ "speechBubbles": {
78
+ "contentTypeSavedHeader": "Loại nội dung đã được lưu",
79
+ "documentTypeExportedSuccess": "Content Type đã được xuất ra file",
80
+ "documentTypeExportedError": "Đã xảy ra lỗi khi xuất Content Type"
81
+ },
82
+ "contentTypeEditor": {
83
+ "compositionsDescription": "Kế thừa các tab và thuộc tính từ một loại nội dung hiện có. Các tab mới sẽ được thêm vào loại nội dung hiện tại hoặc được hợp nhất nếu một tab có tên giống hệt tồn tại.",
84
+ "compositionRemoveWarning": "Việc xóa một thành phần sẽ xóa tất cả dữ liệu thuộc tính liên quan. Khi bạn lưu loại nội dung, sẽ không có cách nào quay lại.",
85
+ "allDocumentTypes": "Tất cả các loại nội dung",
86
+ "usingThisDocument": "Việc sử dụng loại nội dung này sẽ bị xóa vĩnh viễn, vui lòng xác nhận bạn muốn xóa những cái này.",
87
+ "compositionUsageSpecification": "Loại nội dung này hiện đang được sử dụng trong thành phần của các loại nội dung sau:",
88
+ "elementDescription": "Một loại phần tử được sử dụng trong các loại nội dung khác, và không nằm trong cây nội dung.",
89
+ "elementCannotToggle": "Loại nội dung không thể được thay đổi thành loại phần tử một khi nó đã được sử dụng để tạo một hoặc nhiều mục nội dung."
90
+ },
91
+ "webhooks": {
92
+ "addDocumentType": "Thêm loại nội dung"
93
+ },
94
+ "treeHeaders": {
95
+ "documentTypes": "Loại nội dung"
96
+ },
97
+ "references": {
98
+ "labelUsedByDocumentTypes": "Được sử dụng trong các Loại Nội dung"
99
+ },
100
+ "contentTemplatesDashboard": {
101
+ "createDescription": "<p>Có hai cách để tạo một Mẫu tài liệu:</p><ul><li>Nhấp chuột phải vào một nút nội dung và chọn \"Tạo Mẫu tài liệu\" để tạo một Mẫu tài liệu mới.</li><li>Nhấp chuột phải vào cây Mẫu tài liệu trong phần Cài đặt và chọn Loại nội dung mà bạn muốn tạo Mẫu tài liệu cho.</li></ul><p>Sau khi được đặt tên, các biên tập viên có thể bắt đầu sử dụng Mẫu tài liệu như một nền tảng cho trang mới của họ.</p>",
102
+ "manageDescription": "Bạn có thể chỉnh sửa và xóa Mẫu tài liệu từ cây \"Mẫu tài liệu\" trong phần Cài đặt. Mở rộng Loại nội dung mà Mẫu tài liệu dựa trên đó và nhấp vào để chỉnh sửa hoặc xóa."
103
+ }
104
+ }
@@ -0,0 +1,38 @@
1
+ // Generated by `bun run localizations:build`. Do not edit.
2
+ // The product name in this language's own wording, for the keys
3
+ // plugin/branding/localization-en.js curates, and "content type" where
4
+ // Umbraco says "document type".
5
+ export default {
6
+ "paste": {
7
+ "errorMessage": "您所粘貼的文本含有特殊字元或格式,Crombolo將清除以適應網頁。"
8
+ },
9
+ "dashboard": {
10
+ "nothinghappens": "如果Crombolo沒有打開,您可能需要允許彈出式視窗。"
11
+ },
12
+ "actions": {
13
+ "changeDocType": "改變內容類型",
14
+ "exportDocumentType": "匯出內容類型",
15
+ "importdocumenttype": "導入內容類型"
16
+ },
17
+ "content": {
18
+ "documentType": "內容類型"
19
+ },
20
+ "create": {
21
+ "noDocumentTypes": "此處沒有被允許的內容類型。請至設定中的 <strong>\"內容類型\"</strong>處變更。",
22
+ "documentTypeWithoutTemplate": "內容類型沒有相關範本"
23
+ },
24
+ "settings": {
25
+ "importDocumentTypeHelp": "要導入內容類型,請點擊“流覽”按鈕,再點擊“導入”,然後在您電腦上查找 \".udt\"檔導入(下一頁中需要您再次確認)"
26
+ },
27
+ "contentTypeEditor": {
28
+ "compositionsDescription": "從已存在的內容類型中繼承選項卡以及屬性。新選項卡將被新增至目前內容類型或合併至已存在同名的選項卡中。",
29
+ "allDocumentTypes": "所有內容類型",
30
+ "usingThisDocument": "使用此內容類型的將被永久刪除,請確認您也想要將它們刪除。"
31
+ },
32
+ "treeHeaders": {
33
+ "documentTypes": "內容類型"
34
+ },
35
+ "user": {
36
+ "documentType": "內容類型"
37
+ }
38
+ }
@@ -0,0 +1,38 @@
1
+ // Generated by `bun run localizations:build`. Do not edit.
2
+ // The product name in this language's own wording, for the keys
3
+ // plugin/branding/localization-en.js curates, and "content type" where
4
+ // Umbraco says "document type".
5
+ export default {
6
+ "paste": {
7
+ "errorMessage": "您所粘贴的文本含有特殊字符或格式,Crombolo将清除以适应网页。"
8
+ },
9
+ "dashboard": {
10
+ "nothinghappens": "如果Crombolo没有打开,您可能需要允许弹出式窗口。"
11
+ },
12
+ "actions": {
13
+ "changeDocType": "改变内容类型",
14
+ "exportDocumentType": "导出内容类型",
15
+ "importdocumenttype": "导入内容类型"
16
+ },
17
+ "content": {
18
+ "documentType": "内容类型"
19
+ },
20
+ "create": {
21
+ "noDocumentTypes": "没有允许的内容类型可用。必须在 <strong> \"内容类型\" </strong> 下的 \"设置\" 部分中启用这些内容。",
22
+ "documentTypeWithoutTemplate": "没有模板的内容类型"
23
+ },
24
+ "settings": {
25
+ "importDocumentTypeHelp": "要导入内容类型,请点击“浏览”按钮,再点击“导入”,然后在您电脑上查找 \".udt\"文件导入(下一页中需要您再次确认)"
26
+ },
27
+ "contentTypeEditor": {
28
+ "compositionsDescription": "从现有内容类型继承选项卡和属性。如果存在同名的选项卡, 则新选项卡将添加到当前内容类型或合并。",
29
+ "allDocumentTypes": "所有内容类型",
30
+ "usingThisDocument": "使用此内容类型将被永久删除, 请确认您还要删除这些文件。"
31
+ },
32
+ "treeHeaders": {
33
+ "documentTypes": "内容类型"
34
+ },
35
+ "user": {
36
+ "documentType": "内容类型"
37
+ }
38
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * A header app that shows when this server has fallen behind the database:
3
+ * reads keep working, writes answer 409, and the load balancer is draining it.
4
+ */
5
+ import { css, html, LitElement, nothing } from '@umbraco-cms/backoffice/external/lit'
6
+ import { fetchReport } from './report-client.js'
7
+
8
+ export default class CromboloReadOnlyBannerElement extends LitElement {
9
+ static properties = { _health: { state: true } }
10
+
11
+ constructor() {
12
+ super()
13
+ this._health = undefined
14
+ this._timer = undefined
15
+ }
16
+
17
+ connectedCallback() {
18
+ super.connectedCallback()
19
+ this.#poll()
20
+ this._timer = setInterval(() => this.#poll(), 15_000)
21
+ }
22
+
23
+ disconnectedCallback() {
24
+ super.disconnectedCallback()
25
+ clearInterval(this._timer)
26
+ }
27
+
28
+ async #poll() {
29
+ try {
30
+ this._health = (await fetchReport()).health
31
+ } catch {
32
+ // The report is not reachable; say nothing rather than something wrong.
33
+ }
34
+ }
35
+
36
+ render() {
37
+ if (!this._health?.readOnly) return nothing
38
+ return html`<uui-tag color="warning" look="primary" title="An upgrade moved the database past this server (${this._health.version}+${this._health.revision}). Editing resumes on the new servers.">
39
+ Read-only: upgrade in progress
40
+ </uui-tag>`
41
+ }
42
+
43
+ static styles = css`
44
+ :host { display: flex; align-items: center; padding: 0 var(--uui-size-space-3); }
45
+ `
46
+ }
47
+
48
+ customElements.define('crombolo-read-only-banner', CromboloReadOnlyBannerElement)
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Reads the change report the server keeps at <backoffice>/crombolo/api/change-report.
3
+ * The cookie carries the session, so no token handling is needed here.
4
+ */
5
+ export async function fetchReport() {
6
+ const base = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
7
+ const response = await fetch(`${base.replace(/\/$/, '')}/crombolo/api/change-report`, {
8
+ credentials: 'include',
9
+ headers: { accept: 'application/json' },
10
+ })
11
+ if (!response.ok) throw new Error(`change report responded ${response.status}`)
12
+ return response.json()
13
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Reads and saves search and sharing at `<backoffice>/crombolo/api/seo/*`. The
3
+ * session cookie carries the auth, as it does for the framework's other plugin
4
+ * endpoints; the base comes from `<base href>` because the backoffice path is
5
+ * configurable.
6
+ */
7
+ const base = () => {
8
+ const href = document.querySelector('base')?.getAttribute('href') ?? '/umbraco/'
9
+ return `${href.replace(/\/$/, '')}/crombolo/api/seo`
10
+ }
11
+
12
+ const json = async (path, init) => {
13
+ const response = await fetch(`${base()}${path}`, {
14
+ credentials: 'include',
15
+ headers: {
16
+ accept: 'application/json',
17
+ ...(init?.body ? { 'content-type': 'application/json' } : {}),
18
+ },
19
+ ...init,
20
+ })
21
+ const body = await response.json().catch(() => ({}))
22
+ if (!response.ok) throw new Error(body.message ?? 'Search and sharing could not be loaded.')
23
+ return body
24
+ }
25
+
26
+ export const readSettings = () => json('/settings')
27
+
28
+ export const saveSettings = (changes) =>
29
+ json('/settings', { method: 'PUT', body: JSON.stringify(changes) })
30
+
31
+ export const readPreview = (documentKey, culture) => {
32
+ const query = new URLSearchParams({ document: documentKey })
33
+ if (culture) query.set('culture', culture)
34
+ return json(`/preview?${query}`)
35
+ }
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Search & sharing, in Settings: the site-wide values every page falls back
3
+ * to, the robots file, and the page shown for an address that does not exist.
4
+ *
5
+ * Each field starts from the site's configuration. A value saved here overrides
6
+ * it, and "Use the default" removes the override, so the configuration applies
7
+ * again; the label beside each field says which is in force.
8
+ */
9
+ // For their side effects: these register `umb-input-document` and
10
+ // `umb-input-media`, which the shell otherwise loads lazily.
11
+ import '@umbraco-cms/backoffice/document'
12
+ import '@umbraco-cms/backoffice/media'
13
+ import { css, html, nothing } from '@umbraco-cms/backoffice/external/lit'
14
+ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'
15
+ import { readSettings, saveSettings } from './client.js'
16
+
17
+ const SOURCES = {
18
+ backoffice: 'Set here',
19
+ config: 'From the site configuration',
20
+ default: 'Default',
21
+ }
22
+
23
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
24
+
25
+ export default class CromboloSeoDashboardElement extends UmbLitElement {
26
+ static properties = {
27
+ _settings: { state: true },
28
+ _draft: { state: true },
29
+ _message: { state: true },
30
+ _saving: { state: true },
31
+ }
32
+
33
+ constructor() {
34
+ super()
35
+ this._draft = {}
36
+ }
37
+
38
+ connectedCallback() {
39
+ super.connectedCallback()
40
+ this.#load()
41
+ }
42
+
43
+ async #load() {
44
+ try {
45
+ this._settings = await readSettings()
46
+ this._draft = {}
47
+ } catch (error) {
48
+ this._message = error.message
49
+ }
50
+ }
51
+
52
+ #value(field) {
53
+ return field in this._draft ? this._draft[field] : this._settings.effective[field]
54
+ }
55
+
56
+ #set(field, value) {
57
+ this._draft = { ...this._draft, [field]: value }
58
+ }
59
+
60
+ async #save(changes) {
61
+ this._saving = true
62
+ try {
63
+ this._settings = await saveSettings(changes)
64
+ this._draft = {}
65
+ this._message = 'Saved.'
66
+ } catch (error) {
67
+ this._message = error.message
68
+ } finally {
69
+ this._saving = false
70
+ }
71
+ }
72
+
73
+ #source(field) {
74
+ const source = this._settings.sources[field]
75
+ return html`
76
+ <span class="source">
77
+ <uui-tag look="secondary">${SOURCES[source]}</uui-tag>
78
+ ${
79
+ source === 'backoffice'
80
+ ? html`<uui-button
81
+ compact
82
+ look="placeholder"
83
+ label="Use the default"
84
+ @click=${() => this.#save({ [field]: null })}
85
+ ></uui-button>`
86
+ : nothing
87
+ }
88
+ </span>
89
+ `
90
+ }
91
+
92
+ #field(field, label, description, control) {
93
+ return html`
94
+ <div class="field">
95
+ <div class="label">
96
+ <strong>${label}</strong>
97
+ ${this.#source(field)}
98
+ </div>
99
+ <small>${description}</small>
100
+ ${control}
101
+ </div>
102
+ `
103
+ }
104
+
105
+ #toggle(field, label, description) {
106
+ return this.#field(
107
+ field,
108
+ label,
109
+ description,
110
+ html`<uui-toggle
111
+ .checked=${Boolean(this.#value(field))}
112
+ label=${label}
113
+ @change=${(event) => this.#set(field, event.target.checked)}
114
+ ></uui-toggle>`,
115
+ )
116
+ }
117
+
118
+ #text(field, label, description, placeholder = '') {
119
+ return this.#field(
120
+ field,
121
+ label,
122
+ description,
123
+ html`<uui-input
124
+ .value=${this.#value(field) ?? ''}
125
+ label=${label}
126
+ placeholder=${placeholder}
127
+ @input=${(event) => this.#set(field, event.target.value)}
128
+ ></uui-input>`,
129
+ )
130
+ }
131
+
132
+ render() {
133
+ if (!this._settings)
134
+ return this._message
135
+ ? html`<uui-box><p>${this._message}</p></uui-box>`
136
+ : html`<uui-loader></uui-loader>`
137
+ const image = this.#value('defaultImage')
138
+ const notFound = this.#value('notFoundPage') ?? []
139
+ const scoped = notFound.some((rule) => rule.host || rule.culture)
140
+ return html`
141
+ <uui-box headline="Search & sharing">
142
+ ${this.#toggle(
143
+ 'indexable',
144
+ 'Let search engines list this site',
145
+ 'Turn off for a site that is not live yet, such as a staging copy.',
146
+ )}
147
+ ${this.#toggle(
148
+ 'sitemap',
149
+ 'Offer a list of pages to search engines',
150
+ 'Lists every published page at /sitemap.xml, except those hidden from it.',
151
+ )}
152
+ ${this.#text('siteName', 'Site name', 'Shown when a page is shared.')}
153
+ ${this.#text(
154
+ 'titleSuffix',
155
+ 'Added to every page title',
156
+ 'For example " | Acme". Leave empty for none.',
157
+ )}
158
+ ${this.#field(
159
+ 'defaultImage',
160
+ 'Sharing image',
161
+ 'Used when a page without its own image is shared.',
162
+ html`<umb-input-media
163
+ max="1"
164
+ .selection=${UUID.test(image ?? '') ? [image] : []}
165
+ @change=${(event) => this.#set('defaultImage', event.target.selection?.[0] ?? '')}
166
+ ></umb-input-media>`,
167
+ )}
168
+ ${this.#field(
169
+ 'notFoundPage',
170
+ 'Page for addresses that do not exist',
171
+ scoped
172
+ ? 'The site configuration chooses a page per address or language. Choosing one here uses it everywhere.'
173
+ : 'Shown, with a "not found" status, when nothing else answers an address.',
174
+ html`<umb-input-document
175
+ max="1"
176
+ .selection=${notFound[0]?.document && !scoped ? [notFound[0].document] : []}
177
+ @change=${(event) => {
178
+ const key = event.target.selection?.[0]
179
+ this.#set('notFoundPage', key ? [{ document: key }] : [])
180
+ }}
181
+ ></umb-input-document>`,
182
+ )}
183
+ ${this.#field(
184
+ 'robotsTxt',
185
+ 'Instructions for search engines',
186
+ 'The rules in /robots.txt. The address of the page list is added for you.',
187
+ html`<uui-textarea
188
+ .value=${this.#value('robotsTxt') ?? ''}
189
+ label="Instructions for search engines"
190
+ @input=${(event) => this.#set('robotsTxt', event.target.value)}
191
+ ></uui-textarea>`,
192
+ )}
193
+ <div class="actions">
194
+ <uui-button
195
+ look="primary"
196
+ label="Save"
197
+ ?disabled=${this._saving || Object.keys(this._draft).length === 0}
198
+ @click=${() => this.#save(this._draft)}
199
+ ></uui-button>
200
+ ${this._message ? html`<span>${this._message}</span>` : nothing}
201
+ </div>
202
+ </uui-box>
203
+ `
204
+ }
205
+
206
+ static styles = css`
207
+ :host {
208
+ display: block;
209
+ padding: var(--uui-size-layout-1);
210
+ }
211
+ .field {
212
+ display: flex;
213
+ flex-direction: column;
214
+ gap: var(--uui-size-space-2);
215
+ margin-bottom: var(--uui-size-layout-1);
216
+ }
217
+ .label {
218
+ display: flex;
219
+ align-items: center;
220
+ gap: var(--uui-size-space-3);
221
+ }
222
+ .source {
223
+ display: inline-flex;
224
+ align-items: center;
225
+ gap: var(--uui-size-space-2);
226
+ }
227
+ small {
228
+ color: var(--uui-color-text-alt);
229
+ }
230
+ uui-textarea {
231
+ --uui-textarea-min-height: 8em;
232
+ font-family: monospace;
233
+ }
234
+ .actions {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: var(--uui-size-space-4);
238
+ }
239
+ `
240
+ }
241
+
242
+ customElements.define('crombolo-seo-dashboard', CromboloSeoDashboardElement)