From 92d511e313d5049fd859160902ed7b735e4f0f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 22 Feb 2023 12:35:22 +1300 Subject: [PATCH 01/18] added shortcut service --- .../src/backoffice/backoffice.element.ts | 2 + .../src/core/shortcuts/shortcut.service.ts | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index dae0f7ba21..51873f3759 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -55,6 +55,7 @@ import './search'; import './templating'; import './shared'; import { UmbLitElement } from '@umbraco-cms/element'; +import UmbShortcutService from 'src/core/shortcuts/shortcut.service'; @defineElement('umb-backoffice') export class UmbBackofficeElement extends UmbLitElement { @@ -107,6 +108,7 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbTemplateTreeStore(this); new UmbTemplateDetailStore(this); new UmbLanguageStore(this); + new UmbShortcutService(); this.provideContext(UMB_BACKOFFICE_CONTEXT_TOKEN, new UmbBackofficeContext()); this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); diff --git a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts new file mode 100644 index 0000000000..334506cb14 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts @@ -0,0 +1,44 @@ +export type Shortcut = { + key: string; + altKey?: boolean; + ctrlKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; + callback(): void; +}; + +export class UmbShortcutService { + #shortcuts: Array = [ + { + key: 'k', + metaKey: true, + callback: () => console.log('Open search'), + }, + ]; + + constructor() { + addEventListener('keydown', (event: KeyboardEvent) => { + if (!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) return; + if (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt' || event.key === 'Meta') return; + + const shortcut = this.#shortcuts.find((x) => { + if (x.key !== event.key) return false; + if ((x.altKey ? true : false) !== event.altKey) return false; + if ((x.ctrlKey ? true : false) !== event.ctrlKey) return false; + if ((x.shiftKey ? true : false) !== event.shiftKey) return false; + if ((x.metaKey ? true : false) !== event.metaKey) return false; + return true; + }); + + shortcut?.callback(); + }); + } +} + +export default UmbShortcutService; + +declare global { + interface HTMLElementTagNameMap { + 'umb-shortcut': UmbShortcutService; + } +} From 0be34c8bdd89a436ab4d18007335a926a26da806 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 22 Feb 2023 12:41:18 +1300 Subject: [PATCH 02/18] update shortcut service --- .../src/core/shortcuts/shortcut.service.ts | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts index 334506cb14..06f29b5a40 100644 --- a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts @@ -1,18 +1,32 @@ export type Shortcut = { - key: string; - altKey?: boolean; - ctrlKey?: boolean; - shiftKey?: boolean; - metaKey?: boolean; + name: string; + alias: string; callback(): void; + combinations: Array<{ + key: string; + altKey?: boolean; + ctrlKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; + }>; }; export class UmbShortcutService { #shortcuts: Array = [ { - key: 'k', - metaKey: true, + name: 'Open search', + alias: 'Shortcut.OpenSearch', callback: () => console.log('Open search'), + combinations: [ + { + key: 'k', + metaKey: true, + }, + { + key: 'k', + ctrlKey: true, + }, + ], }, ]; @@ -22,12 +36,14 @@ export class UmbShortcutService { if (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt' || event.key === 'Meta') return; const shortcut = this.#shortcuts.find((x) => { - if (x.key !== event.key) return false; - if ((x.altKey ? true : false) !== event.altKey) return false; - if ((x.ctrlKey ? true : false) !== event.ctrlKey) return false; - if ((x.shiftKey ? true : false) !== event.shiftKey) return false; - if ((x.metaKey ? true : false) !== event.metaKey) return false; - return true; + return x.combinations.find((y) => { + if (y.key !== event.key) return false; + if ((y.altKey ? true : false) !== event.altKey) return false; + if ((y.ctrlKey ? true : false) !== event.ctrlKey) return false; + if ((y.shiftKey ? true : false) !== event.shiftKey) return false; + if ((y.metaKey ? true : false) !== event.metaKey) return false; + return true; + }); }); shortcut?.callback(); From 2a58d5caaf23bf792354f13e733128a725df3498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 22 Feb 2023 12:43:47 +1300 Subject: [PATCH 03/18] added search service and cleanup --- .../src/backoffice/search/search.service.ts | 3 +++ .../src/core/shortcuts/shortcut.service.ts | 12 +++--------- 2 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts new file mode 100644 index 0000000000..55986041e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts @@ -0,0 +1,3 @@ +export class UmbSearchService {} + +export default UmbSearchService; diff --git a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts index 06f29b5a40..8925f2759c 100644 --- a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts @@ -1,7 +1,7 @@ export type Shortcut = { name: string; alias: string; - callback(): void; + action(): void; combinations: Array<{ key: string; altKey?: boolean; @@ -16,7 +16,7 @@ export class UmbShortcutService { { name: 'Open search', alias: 'Shortcut.OpenSearch', - callback: () => console.log('Open search'), + action: () => console.log('Open search'), combinations: [ { key: 'k', @@ -46,15 +46,9 @@ export class UmbShortcutService { }); }); - shortcut?.callback(); + shortcut?.action(); }); } } export default UmbShortcutService; - -declare global { - interface HTMLElementTagNameMap { - 'umb-shortcut': UmbShortcutService; - } -} From f26924ad81096599c20aa2123f217a6abb16c84a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:05:16 +1300 Subject: [PATCH 04/18] updated services and styling --- .../src/backoffice/backoffice.element.ts | 6 +- .../src/backoffice/search/search.element.ts | 144 ++++++++++++++++++ .../src/backoffice/search/search.service.ts | 34 ++++- .../src/core/shortcuts/shortcut.service.ts | 15 +- 4 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index 51873f3759..b9fb3abe38 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -3,6 +3,7 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../core/modal'; +import { UmbShortcutService } from '../core/shortcuts/shortcut.service'; import { UmbUserStore } from './users/users/user.store'; import { UmbUserGroupStore } from './users/user-groups/user-group.store'; import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from './users/current-user/current-user.store'; @@ -39,6 +40,7 @@ import { UmbTemplateTreeStore } from './templating/templates/tree/data/template. import { UmbTemplateDetailStore } from './templating/templates/workspace/data/template.detail.store'; import { UmbThemeContext } from './themes/theme.context'; import { UmbLanguageStore } from './settings/languages/language.store'; +import { UmbSearchService } from './search/search.service'; import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; import '@umbraco-cms/router'; @@ -55,7 +57,6 @@ import './search'; import './templating'; import './shared'; import { UmbLitElement } from '@umbraco-cms/element'; -import UmbShortcutService from 'src/core/shortcuts/shortcut.service'; @defineElement('umb-backoffice') export class UmbBackofficeElement extends UmbLitElement { @@ -108,7 +109,8 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbTemplateTreeStore(this); new UmbTemplateDetailStore(this); new UmbLanguageStore(this); - new UmbShortcutService(); + new UmbShortcutService(this); + new UmbSearchService(); this.provideContext(UMB_BACKOFFICE_CONTEXT_TOKEN, new UmbBackofficeContext()); this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts new file mode 100644 index 0000000000..ff8d522546 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts @@ -0,0 +1,144 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, LitElement } from 'lit'; +import { customElement } from 'lit/decorators.js'; + +@customElement('umb-search') +export class UmbSearchElement extends LitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + height: 100%; + background-color: var(--uui-color-surface-alt); + box-sizing: border-box; + color: var(--uui-color-text); + font-size: 1rem; + } + input { + all: unset; + height: 100%; + width: 100%; + } + #search-icon, + #close-icon { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; + height: 100%; + } + #close-icon > div { + background: var(--uui-color-surface-alt); + border: 1px solid var(--uui-color-border); + padding: 3px 6px 4px 6px; + line-height: 1; + border-radius: 3px; + color: var(--uui-color-text-alt); + font-weight: 800; + font-size: 12px; + } + #close-icon > div:hover { + border-color: var(--uui-color-focus); + color: var(--uui-color-focus); + cursor: pointer; + } + #top { + background-color: var(--uui-color-surface); + display: flex; + height: 48px; + border-bottom: 1px solid var(--uui-color-border); + } + #main { + display: flex; + flex-direction: column; + padding: 0 32px 16px 32px; + } + .group { + margin-top: var(--uui-size-space-4); + } + .group-name { + font-weight: 600; + margin-bottom: var(--uui-size-space-1); + } + .results { + display: flex; + flex-direction: column; + gap: 8px; + } + .result { + background: var(--uui-color-surface); + border: 1px solid var(--uui-color-border); + padding: var(--uui-size-space-3) var(--uui-size-space-4); + border-radius: var(--uui-border-radius); + color: var(--uui-color-interactive); + cursor: pointer; + justify-content: space-between; + display: flex; + } + .result:hover { + background-color: var(--uui-color-surface-emphasis); + color: var(--uui-color-interactive-emphasis); + } + a { + text-decoration: none; + color: inherit; + } + `, + ]; + + connectedCallback() { + super.connectedCallback(); + + requestAnimationFrame(() => { + this.shadowRoot?.querySelector('input')?.focus(); + }); + } + + render() { + return html` +
+
+ +
+ +
+
esc
+
+
+
+
+
Document Types
+ +
+
+
Media Types
+ +
+
+ `; + } +} + +export default UmbSearchElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-search': UmbSearchElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts index 55986041e7..4c5f4365f7 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts @@ -1,3 +1,33 @@ -export class UmbSearchService {} +import './search.element'; +import type { UmbSearchElement } from './search.element'; -export default UmbSearchService; +export class UmbSearchService { + public static async Open() { + const topDistance = '128px'; + const margin = '16px'; + const maxHeight = '600px'; + const maxWidth = '500px'; + const dialog = document.createElement('dialog') as HTMLDialogElement; + dialog.style.top = `min(${topDistance}, 10vh)`; + dialog.style.margin = '0 auto'; + dialog.style.height = `min(${maxHeight}, calc(100vh - ${margin}))`; + dialog.style.width = `min(${maxWidth}, calc(100vw - ${margin}))`; + dialog.style.boxSizing = 'border-box'; + dialog.style.background = 'none'; + dialog.style.border = 'none'; + dialog.style.padding = '0'; + dialog.style.boxShadow = 'var(--uui-shadow-depth-5)'; + dialog.style.borderRadius = '9px'; + const search = document.createElement('umb-search') as UmbSearchElement; + dialog.appendChild(search); + + //TODO: Yeah... This is not final + const backoffice = + document.body.children[0].shadowRoot?.children[0].shadowRoot?.children[0].children[0].shadowRoot?.querySelector( + 'umb-backoffice-modal-container' + )?.shadowRoot?.children[0].shadowRoot; + + backoffice?.appendChild(dialog); + dialog.showModal(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts index 8925f2759c..042528994d 100644 --- a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts @@ -1,3 +1,7 @@ +import { UmbContextProviderController, UmbContextToken } from '@umbraco-cms/context-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbSearchService } from 'src/backoffice/search/search.service'; + export type Shortcut = { name: string; alias: string; @@ -12,11 +16,12 @@ export type Shortcut = { }; export class UmbShortcutService { + #host: UmbControllerHostInterface; #shortcuts: Array = [ { name: 'Open search', alias: 'Shortcut.OpenSearch', - action: () => console.log('Open search'), + action: () => UmbSearchService.Open(), combinations: [ { key: 'k', @@ -30,7 +35,11 @@ export class UmbShortcutService { }, ]; - constructor() { + constructor(host: UmbControllerHostInterface) { + this.#host = host; + + new UmbContextProviderController(host, UMB_SHORTCUT_CONTEXT_TOKEN, this); + addEventListener('keydown', (event: KeyboardEvent) => { if (!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) return; if (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt' || event.key === 'Meta') return; @@ -51,4 +60,4 @@ export class UmbShortcutService { } } -export default UmbShortcutService; +export const UMB_SHORTCUT_CONTEXT_TOKEN = new UmbContextToken(UmbShortcutService.name); From ae60f2fe6cc8e8d863b6d4d92b6583365774bbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:09:53 +1300 Subject: [PATCH 05/18] styling --- .../src/backoffice/search/search.element.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts index ff8d522546..62182dce56 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts @@ -31,7 +31,7 @@ export class UmbSearchElement extends LitElement { aspect-ratio: 1; height: 100%; } - #close-icon > div { + #close-icon > button { background: var(--uui-color-surface-alt); border: 1px solid var(--uui-color-border); padding: 3px 6px 4px 6px; @@ -41,10 +41,9 @@ export class UmbSearchElement extends LitElement { font-weight: 800; font-size: 12px; } - #close-icon > div:hover { + #close-icon > button:hover { border-color: var(--uui-color-focus); color: var(--uui-color-focus); - cursor: pointer; } #top { background-color: var(--uui-color-surface); @@ -83,10 +82,18 @@ export class UmbSearchElement extends LitElement { background-color: var(--uui-color-surface-emphasis); color: var(--uui-color-interactive-emphasis); } + .result:hover span { + font-weight: unset; + opacity: unset; + } a { text-decoration: none; color: inherit; } + a span { + opacity: 0.5; + font-weight: 100; + } `, ]; @@ -106,7 +113,7 @@ export class UmbSearchElement extends LitElement {
-
esc
+
@@ -115,10 +122,11 @@ export class UmbSearchElement extends LitElement {
@@ -127,6 +135,7 @@ export class UmbSearchElement extends LitElement { From 7d4d9c2c1fa84216476f7647a11ea7cb25ef1846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:16:27 +1300 Subject: [PATCH 06/18] fixed formatting --- .../src/backoffice/search/search.element.ts | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts index 62182dce56..8d2b785b93 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts @@ -108,38 +108,41 @@ export class UmbSearchElement extends LitElement { render() { return html`
-
- -
- -
- -
+
+ +
+ +
+ +
- -
-
Media Types
- -
-
+ +
+
Media Types
+ +
+ `; } } From 3ce405d2508e9198b32a46987bf91923fd99b504 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 22 Feb 2023 16:11:16 +0100 Subject: [PATCH 07/18] provide modals under the correct context token --- .../.storybook/preview.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/.storybook/preview.js b/src/Umbraco.Web.UI.Client/.storybook/preview.js index dbc166a597..5b0150c957 100644 --- a/src/Umbraco.Web.UI.Client/.storybook/preview.js +++ b/src/Umbraco.Web.UI.Client/.storybook/preview.js @@ -11,21 +11,15 @@ import { html } from 'lit-html'; import { initialize, mswDecorator } from 'msw-storybook-addon'; import { setCustomElements } from '@storybook/web-components'; -import { - UMB_DATA_TYPE_STORE_CONTEXT_TOKEN, - UmbDataTypeStore, -} from '../src/backoffice/settings/data-types/repository/data-type.store.ts'; -import { - UMB_DOCUMENT_TYPE_STORE_CONTEXT_TOKEN, - UmbDocumentTypeStore, -} from '../src/backoffice/documents/document-types/repository/document-type.store.ts'; +import { UmbDataTypeStore } from '../src/backoffice/settings/data-types/repository/data-type.store.ts'; +import { UmbDocumentTypeStore } from '../src/backoffice/documents/document-types/repository/document-type.store.ts'; import customElementManifests from '../custom-elements.json'; import { UmbIconStore } from '../libs/store/icon/icon.store'; import { onUnhandledRequest } from '../src/core/mocks/browser'; import { handlers } from '../src/core/mocks/browser-handlers'; import { LitElement } from 'lit'; -import { UmbModalService } from '../src/core/modal'; +import { UMB_MODAL_SERVICE_CONTEXT_TOKEN, UmbModalService } from '../src/core/modal'; // TODO: Fix storybook manifest registrations. @@ -71,7 +65,10 @@ const documentTypeStoreProvider = (story) => html` `; const modalServiceProvider = (story) => html` - + ${story()} @@ -94,7 +91,7 @@ export const parameters = { storySort: { method: 'alphabetical', includeNames: true, - order: ['Guides', ['Getting started'], '*'] + order: ['Guides', ['Getting started'], '*'], }, }, actions: { argTypesRegex: '^on.*' }, From a25eed6cd35b26f52f15bf21fa740cc6dfa2c643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:10:16 +1300 Subject: [PATCH 08/18] styling --- .../src/backoffice/search/search.element.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts index 8d2b785b93..6ae9e77ab9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts @@ -13,7 +13,7 @@ export class UmbSearchElement extends LitElement { height: 100%; width: 100%; height: 100%; - background-color: var(--uui-color-surface-alt); + background-color: var(--uui-color-background); box-sizing: border-box; color: var(--uui-color-text); font-size: 1rem; @@ -121,8 +121,8 @@ export class UmbSearchElement extends LitElement {
Document Types
-
- # +
+ # Article Controls
> From 0b2a5122ea4bdf82d1bb8e48304194b924e72b32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 16:41:34 +1300 Subject: [PATCH 09/18] add search header app --- .../src/backoffice/search/manifests.ts | 2 +- .../search/umb-search-header-app.element.ts | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/search/umb-search-header-app.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/manifests.ts index c69d4d6a02..ea025667f8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/manifests.ts @@ -5,7 +5,7 @@ const headerApps: Array = [ type: 'headerApp', alias: 'Umb.HeaderApp.Search', name: 'Header App Search', - loader: () => import('src/backoffice/shared/components/header-app/header-app-button.element'), + loader: () => import('./umb-search-header-app.element'), weight: 10, meta: { label: 'Search', diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/umb-search-header-app.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/umb-search-header-app.element.ts new file mode 100644 index 0000000000..9518fb409f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/search/umb-search-header-app.element.ts @@ -0,0 +1,48 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { css, CSSResultGroup, html } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-search-header-app') +export class UmbSearchHeaderApp extends UmbLitElement { + static styles: CSSResultGroup = [ + UUITextStyles, + css` + uui-button { + font-size: 18px; + --uui-button-background-color: transparent; + } + `, + ]; + + private _modalService?: UmbModalService; + + constructor() { + super(); + + this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (_instance) => { + this._modalService = _instance; + }); + } + + #onSearchClick() { + this._modalService?.search(); + } + + render() { + return html` + + + + `; + } +} + +export default UmbSearchHeaderApp; + +declare global { + interface HTMLElementTagNameMap { + 'umb-search-header-app': UmbSearchHeaderApp; + } +} From 9a6b082cbdf12fd52e338fd138892e9d0c5d0050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 16:46:35 +1300 Subject: [PATCH 10/18] remove shortcut and search services --- .../src/backoffice/backoffice.element.ts | 4 -- .../src/backoffice/search/search.service.ts | 33 ---------- .../src/core/shortcuts/shortcut.service.ts | 63 ------------------- 3 files changed, 100 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index b9fb3abe38..dae0f7ba21 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -3,7 +3,6 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../core/modal'; -import { UmbShortcutService } from '../core/shortcuts/shortcut.service'; import { UmbUserStore } from './users/users/user.store'; import { UmbUserGroupStore } from './users/user-groups/user-group.store'; import { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT_TOKEN } from './users/current-user/current-user.store'; @@ -40,7 +39,6 @@ import { UmbTemplateTreeStore } from './templating/templates/tree/data/template. import { UmbTemplateDetailStore } from './templating/templates/workspace/data/template.detail.store'; import { UmbThemeContext } from './themes/theme.context'; import { UmbLanguageStore } from './settings/languages/language.store'; -import { UmbSearchService } from './search/search.service'; import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; import '@umbraco-cms/router'; @@ -109,8 +107,6 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbTemplateTreeStore(this); new UmbTemplateDetailStore(this); new UmbLanguageStore(this); - new UmbShortcutService(this); - new UmbSearchService(); this.provideContext(UMB_BACKOFFICE_CONTEXT_TOKEN, new UmbBackofficeContext()); this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts deleted file mode 100644 index 4c5f4365f7..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import './search.element'; -import type { UmbSearchElement } from './search.element'; - -export class UmbSearchService { - public static async Open() { - const topDistance = '128px'; - const margin = '16px'; - const maxHeight = '600px'; - const maxWidth = '500px'; - const dialog = document.createElement('dialog') as HTMLDialogElement; - dialog.style.top = `min(${topDistance}, 10vh)`; - dialog.style.margin = '0 auto'; - dialog.style.height = `min(${maxHeight}, calc(100vh - ${margin}))`; - dialog.style.width = `min(${maxWidth}, calc(100vw - ${margin}))`; - dialog.style.boxSizing = 'border-box'; - dialog.style.background = 'none'; - dialog.style.border = 'none'; - dialog.style.padding = '0'; - dialog.style.boxShadow = 'var(--uui-shadow-depth-5)'; - dialog.style.borderRadius = '9px'; - const search = document.createElement('umb-search') as UmbSearchElement; - dialog.appendChild(search); - - //TODO: Yeah... This is not final - const backoffice = - document.body.children[0].shadowRoot?.children[0].shadowRoot?.children[0].children[0].shadowRoot?.querySelector( - 'umb-backoffice-modal-container' - )?.shadowRoot?.children[0].shadowRoot; - - backoffice?.appendChild(dialog); - dialog.showModal(); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts b/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts deleted file mode 100644 index 042528994d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/core/shortcuts/shortcut.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { UmbContextProviderController, UmbContextToken } from '@umbraco-cms/context-api'; -import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -import { UmbSearchService } from 'src/backoffice/search/search.service'; - -export type Shortcut = { - name: string; - alias: string; - action(): void; - combinations: Array<{ - key: string; - altKey?: boolean; - ctrlKey?: boolean; - shiftKey?: boolean; - metaKey?: boolean; - }>; -}; - -export class UmbShortcutService { - #host: UmbControllerHostInterface; - #shortcuts: Array = [ - { - name: 'Open search', - alias: 'Shortcut.OpenSearch', - action: () => UmbSearchService.Open(), - combinations: [ - { - key: 'k', - metaKey: true, - }, - { - key: 'k', - ctrlKey: true, - }, - ], - }, - ]; - - constructor(host: UmbControllerHostInterface) { - this.#host = host; - - new UmbContextProviderController(host, UMB_SHORTCUT_CONTEXT_TOKEN, this); - - addEventListener('keydown', (event: KeyboardEvent) => { - if (!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) return; - if (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt' || event.key === 'Meta') return; - - const shortcut = this.#shortcuts.find((x) => { - return x.combinations.find((y) => { - if (y.key !== event.key) return false; - if ((y.altKey ? true : false) !== event.altKey) return false; - if ((y.ctrlKey ? true : false) !== event.ctrlKey) return false; - if ((y.shiftKey ? true : false) !== event.shiftKey) return false; - if ((y.metaKey ? true : false) !== event.metaKey) return false; - return true; - }); - }); - - shortcut?.action(); - }); - } -} - -export const UMB_SHORTCUT_CONTEXT_TOKEN = new UmbContextToken(UmbShortcutService.name); From 81bba10eeb0ae00a74796a01c919e9373448be52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 16:47:06 +1300 Subject: [PATCH 11/18] add modal layout search --- .../src/backoffice/search/search.element.ts | 156 ---------- .../search/modal-layout-search.element.ts | 292 ++++++++++++++++++ .../src/core/modal/modal.service.ts | 38 ++- 3 files changed, 329 insertions(+), 157 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts deleted file mode 100644 index 6ae9e77ab9..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/search/search.element.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { UUITextStyles } from '@umbraco-ui/uui-css'; -import { css, html, LitElement } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -@customElement('umb-search') -export class UmbSearchElement extends LitElement { - static styles = [ - UUITextStyles, - css` - :host { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - height: 100%; - background-color: var(--uui-color-background); - box-sizing: border-box; - color: var(--uui-color-text); - font-size: 1rem; - } - input { - all: unset; - height: 100%; - width: 100%; - } - #search-icon, - #close-icon { - display: flex; - align-items: center; - justify-content: center; - aspect-ratio: 1; - height: 100%; - } - #close-icon > button { - background: var(--uui-color-surface-alt); - border: 1px solid var(--uui-color-border); - padding: 3px 6px 4px 6px; - line-height: 1; - border-radius: 3px; - color: var(--uui-color-text-alt); - font-weight: 800; - font-size: 12px; - } - #close-icon > button:hover { - border-color: var(--uui-color-focus); - color: var(--uui-color-focus); - } - #top { - background-color: var(--uui-color-surface); - display: flex; - height: 48px; - border-bottom: 1px solid var(--uui-color-border); - } - #main { - display: flex; - flex-direction: column; - padding: 0 32px 16px 32px; - } - .group { - margin-top: var(--uui-size-space-4); - } - .group-name { - font-weight: 600; - margin-bottom: var(--uui-size-space-1); - } - .results { - display: flex; - flex-direction: column; - gap: 8px; - } - .result { - background: var(--uui-color-surface); - border: 1px solid var(--uui-color-border); - padding: var(--uui-size-space-3) var(--uui-size-space-4); - border-radius: var(--uui-border-radius); - color: var(--uui-color-interactive); - cursor: pointer; - justify-content: space-between; - display: flex; - } - .result:hover { - background-color: var(--uui-color-surface-emphasis); - color: var(--uui-color-interactive-emphasis); - } - .result:hover span { - font-weight: unset; - opacity: unset; - } - a { - text-decoration: none; - color: inherit; - } - a span { - opacity: 0.5; - font-weight: 100; - } - `, - ]; - - connectedCallback() { - super.connectedCallback(); - - requestAnimationFrame(() => { - this.shadowRoot?.querySelector('input')?.focus(); - }); - } - - render() { - return html` -
-
- -
- -
- -
-
-
- `; - } -} - -export default UmbSearchElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-search': UmbSearchElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts new file mode 100644 index 0000000000..ce3e8be0fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts @@ -0,0 +1,292 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, LitElement, nothing } from 'lit'; +import { repeat } from 'lit-html/directives/repeat.js'; +import { customElement, query, state } from 'lit/decorators.js'; + +export type SearchItem = { + name: string; + icon?: string; + href: string; + parent: string; +}; +export type SearchGroupItem = { + name: string; + items: Array; +}; +@customElement('umb-modal-layout-search') +export class UmbModalLayoutSearchElement extends LitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + height: 100%; + background-color: var(--uui-color-background); + box-sizing: border-box; + color: var(--uui-color-text); + font-size: 1rem; + } + input { + all: unset; + height: 100%; + width: 100%; + } + #search-icon, + #close-icon { + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; + height: 100%; + } + #close-icon { + padding: 0 var(--uui-size-space-3); + } + #close-icon > button { + background: var(--uui-color-surface-alt); + border: 1px solid var(--uui-color-border); + padding: 3px 6px 4px 6px; + line-height: 1; + border-radius: 3px; + color: var(--uui-color-text-alt); + font-weight: 800; + font-size: 12px; + cursor: pointer; + } + #close-icon > button:hover { + border-color: var(--uui-color-focus); + color: var(--uui-color-focus); + } + #top { + background-color: var(--uui-color-surface); + display: flex; + height: 48px; + border-bottom: 1px solid var(--uui-color-border); + } + #main { + display: flex; + flex-direction: column; + padding: 0px var(--uui-size-space-6) var(--uui-size-space-5) var(--uui-size-space-6); + height: 100%; + } + .group { + margin-top: var(--uui-size-space-4); + } + .group-name { + font-weight: 600; + margin-bottom: var(--uui-size-space-1); + } + .group-items { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-3); + } + .item { + background: var(--uui-color-surface); + border: 1px solid var(--uui-color-border); + padding: var(--uui-size-space-3) var(--uui-size-space-4); + border-radius: var(--uui-border-radius); + color: var(--uui-color-interactive); + display: grid; + grid-template-columns: var(--uui-size-space-6) 1fr var(--uui-size-space-5); + height: min-content; + align-items: center; + } + .item:hover { + background-color: var(--uui-color-surface-emphasis); + color: var(--uui-color-interactive-emphasis); + } + .item:hover .item-symbol { + font-weight: unset; + opacity: 1; + } + .item-icon, + .item-symbol { + opacity: 0.4; + } + .item-icon > * { + height: 1rem; + display: flex; + width: min-content; + } + .item-symbol { + font-weight: 100; + } + a { + text-decoration: none; + color: inherit; + } + #no-results { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + margin-top: var(--uui-size-space-5); + color: var(--uui-color-text-alt); + } + `, + ]; + + @query('input') + private _input!: HTMLInputElement; + + @state() + private _search = ''; + + @state() + private _groups: Array = []; + + connectedCallback() { + super.connectedCallback(); + + requestAnimationFrame(() => { + this._input.focus(); + }); + } + + #onSearchChange(event: InputEvent) { + const target = event.target as HTMLInputElement; + this._search = target.value; + + this.#updateGroups(); + } + + #onClearSearch() { + this._search = ''; + this._input.value = ''; + this._input.focus(); + this.#updateGroups(); + } + + #updateGroups() { + const filtered = this.#mockData.filter((item) => { + return item.name.toLowerCase().includes(this._search.toLowerCase()); + }); + + const grouped: Array = filtered.reduce((acc, item) => { + const group = acc.find((group) => group.name === item.parent); + if (group) { + group.items.push(item); + } else { + acc.push({ + name: item.parent, + items: [item], + }); + } + return acc; + }, [] as Array); + + this._groups = grouped; + } + + render() { + return html` +
+
+ +
+ +
+ +
+
+ ${this._search + ? html`
+ ${this._groups.length > 0 + ? repeat( + this._groups, + (group) => group.name, + (group) => this.#renderGroup(group.name, group.items) + ) + : html`
Only mock data for now Search for blog
`} +
` + : nothing} + `; + } + + #renderGroup(name: string, items: Array) { + return html` +
+
${name}
+
${repeat(items, (item) => item.name, this.#renderItem.bind(this))}
+
+ `; + } + + #renderItem(item: SearchItem) { + return html` + + + ${item.icon ? html`` : this.#renderHashTag()} + + ${item.name} + > + + `; + } + + #renderHashTag() { + return html` + + + + + `; + } + + #mockData: Array = [ + { + name: 'Blog', + href: '#', + icon: 'umb:thumbnail-list', + parent: 'Content', + }, + { + name: 'Popular blogs', + href: '#', + icon: 'umb:article', + parent: 'Content', + }, + { + name: 'Blog hero', + href: '#', + icon: 'umb:picture', + parent: 'Media', + }, + { + name: 'Contact form for blog', + href: '#', + parent: 'Document Types', + }, + { + name: 'Blog', + href: '#', + parent: 'Document Types', + }, + { + name: 'Blog link item', + href: '#', + parent: 'Document Types', + }, + ]; +} + +export default UmbModalLayoutSearchElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-modal-layout-search': UmbModalLayoutSearchElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts index f19a149ac7..51332235b3 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts @@ -7,6 +7,7 @@ import './layouts/modal-layout-current-user.element'; import './layouts/icon-picker/modal-layout-icon-picker.element'; import './layouts/link-picker/modal-layout-link-picker.element'; import './layouts/basic/modal-layout-basic.element'; +import './layouts/search/modal-layout-search.element.ts'; import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; import { BehaviorSubject } from 'rxjs'; @@ -18,8 +19,9 @@ import type { UmbModalPropertyEditorUIPickerData } from './layouts/property-edit import type { UmbModalMediaPickerData } from './layouts/media-picker/modal-layout-media-picker.element'; import type { UmbModalLinkPickerData } from './layouts/link-picker/modal-layout-link-picker.element'; import { UmbModalHandler } from './modal-handler'; +import type { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element'; +import { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog'; export type UmbModalType = 'dialog' | 'sidebar'; @@ -139,6 +141,40 @@ export class UmbModalService { }); } + public search(): UmbModalHandler { + const modalHandler = new UmbModalHandler('umb-modal-layout-search'); + + //TODO START: This is a hack to get the search modal layout to look like i want it to. + //TODO: Remove from here to END when the modal system is more flexible + const topDistance = '128px'; + const margin = '16px'; + const maxHeight = '600px'; + const maxWidth = '500px'; + const dialog = document.createElement('dialog') as HTMLDialogElement; + dialog.style.top = `min(${topDistance}, 10vh)`; + dialog.style.margin = '0 auto'; + dialog.style.maxHeight = `min(${maxHeight}, calc(100vh - ${margin}))`; + dialog.style.width = `min(${maxWidth}, calc(100vw - ${margin}))`; + dialog.style.boxSizing = 'border-box'; + dialog.style.background = 'none'; + dialog.style.border = 'none'; + dialog.style.padding = '0'; + dialog.style.boxShadow = 'var(--uui-shadow-depth-5)'; + dialog.style.borderRadius = '9px'; + const search = document.createElement('umb-modal-layout-search'); + dialog.appendChild(search); + requestAnimationFrame(() => { + dialog.showModal(); + }); + modalHandler.element = dialog as unknown as UUIModalDialogElement; + //TODO END + + modalHandler.element.addEventListener('close-end', () => this._handleCloseEnd(modalHandler)); + + this.#modals.next([...this.#modals.getValue(), modalHandler]); + return modalHandler; + } + /** * Opens a modal or sidebar modal * @public From 84923da5fc783ddc52c0f0190ab33666ae7b3892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 16:53:02 +1300 Subject: [PATCH 12/18] cleanup --- src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts index 51332235b3..83439c6a86 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts @@ -11,6 +11,7 @@ import './layouts/search/modal-layout-search.element.ts'; import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; import { BehaviorSubject } from 'rxjs'; +import type { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog'; import { UmbModalChangePasswordData } from './layouts/modal-layout-change-password.element'; import type { UmbModalIconPickerData } from './layouts/icon-picker/modal-layout-icon-picker.element'; import type { UmbModalConfirmData } from './layouts/confirm/modal-layout-confirm.element'; @@ -21,7 +22,6 @@ import type { UmbModalLinkPickerData } from './layouts/link-picker/modal-layout- import { UmbModalHandler } from './modal-handler'; import type { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { UUIModalDialogElement } from '@umbraco-ui/uui-modal-dialog'; export type UmbModalType = 'dialog' | 'sidebar'; From 515e803f909f658de316760bfa906e71fb0c3883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 17:40:55 +1300 Subject: [PATCH 13/18] added urls --- .../search/modal-layout-search.element.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts index ce3e8be0fd..a2ab29e71a 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts @@ -8,6 +8,7 @@ export type SearchItem = { icon?: string; href: string; parent: string; + url?: string; }; export type SearchGroupItem = { name: string; @@ -103,10 +104,23 @@ export class UmbModalLayoutSearchElement extends LitElement { font-weight: unset; opacity: 1; } + .item-icon { + margin-bottom: auto; + margin-top: 5px; + } .item-icon, .item-symbol { opacity: 0.4; } + .item-url { + font-size: 0.8rem; + line-height: 1.2; + font-weight: 100; + } + .item-name { + display: flex; + flex-direction: column; + } .item-icon > * { height: 1rem; display: flex; @@ -229,7 +243,9 @@ export class UmbModalLayoutSearchElement extends LitElement { ${item.icon ? html`` : this.#renderHashTag()} - ${item.name} + + ${item.name} ${item.url ? html`${item.url}` : nothing} + > `; @@ -252,12 +268,21 @@ export class UmbModalLayoutSearchElement extends LitElement { href: '#', icon: 'umb:thumbnail-list', parent: 'Content', + url: '/blog/', }, { name: 'Popular blogs', href: '#', icon: 'umb:article', parent: 'Content', + url: '/blog/popular-blogs/', + }, + { + name: 'How to write a blog', + href: '#', + icon: 'umb:article', + parent: 'Content', + url: '/blog/how-to-write-a-blog/', }, { name: 'Blog hero', From ea5cc6c3d3aa202b06f68692c2403ddd7023f85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 17:43:43 +1300 Subject: [PATCH 14/18] styling --- .../core/modal/layouts/search/modal-layout-search.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts index a2ab29e71a..957369c97d 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts @@ -44,7 +44,7 @@ export class UmbModalLayoutSearchElement extends LitElement { height: 100%; } #close-icon { - padding: 0 var(--uui-size-space-3); + padding: 0 var(--uui-size-space-4); } #close-icon > button { background: var(--uui-color-surface-alt); From 4fc70571519ad6f6c687431067b52155923f60dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 19:47:57 +1300 Subject: [PATCH 15/18] cleanup and fix position --- .../modal/layouts/search/modal-layout-search.element.ts | 2 +- src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts index 957369c97d..df4ef06d2f 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/search/modal-layout-search.element.ts @@ -65,13 +65,13 @@ export class UmbModalLayoutSearchElement extends LitElement { background-color: var(--uui-color-surface); display: flex; height: 48px; - border-bottom: 1px solid var(--uui-color-border); } #main { display: flex; flex-direction: column; padding: 0px var(--uui-size-space-6) var(--uui-size-space-5) var(--uui-size-space-6); height: 100%; + border-top: 1px solid var(--uui-color-border); } .group { margin-top: var(--uui-size-space-4); diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts index 83439c6a86..ab651390df 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts @@ -146,14 +146,15 @@ export class UmbModalService { //TODO START: This is a hack to get the search modal layout to look like i want it to. //TODO: Remove from here to END when the modal system is more flexible - const topDistance = '128px'; + const topDistance = '50%'; const margin = '16px'; const maxHeight = '600px'; const maxWidth = '500px'; const dialog = document.createElement('dialog') as HTMLDialogElement; - dialog.style.top = `min(${topDistance}, 10vh)`; + dialog.style.top = `max(${margin}, calc(${topDistance} - ${maxHeight} / 2))`; dialog.style.margin = '0 auto'; - dialog.style.maxHeight = `min(${maxHeight}, calc(100vh - ${margin}))`; + dialog.style.transform = `translateY(${-maxHeight})`; + dialog.style.maxHeight = `min(${maxHeight}, calc(100% - ${margin}px * 2))`; dialog.style.width = `min(${maxWidth}, calc(100vw - ${margin}))`; dialog.style.boxSizing = 'border-box'; dialog.style.background = 'none'; From 1f35c4457a2d0e73ed0fb1f9909983562ea99e5f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 23 Feb 2023 09:01:04 +0100 Subject: [PATCH 16/18] Feature: Languages repository (#527) * wip migrate languages to repositories * load language and subscribe to draft data * add methods to update data * rename file + use methods on context * add crud methods to repo * use new workspace action * clean up * register delete language entity action + language repository * add dropdown element to supply styles to the uui popover element * import dropdown element * use requestItems in delete action instead of treeItems * add method to language repo to request items * add todo * render entity actions in language table * clean up table header * add await to save action * remove detail from method names * let save action save or create * remove save and delete from template workspace * add isNew to workspace interface * add isNew to template workspace * clean up * fix type errors * add isNew to workspaces * add isNew * remove todo * remove space * abstract culture selection into its own component * add correct event types * don't show undefined in input * fix wrong repository alias * fix import order * wip language picker * add language picker modal layout * handle fallback language change event * set value for fallback language on language picker input * remove unused * add app language select * don't set width on dropdown * make scroll container full height * adjust padding and add border * temp move sidebar headings + set fixed height on language toggle * set fixed body header height * only show scroll bars if sidebar content is scrollable * align height * create full scaffold * align handlers with end points * align server data source with end points * add todo * remove culture warning * set the culture to readonly on saved languages * prevent having no default language + show message if changing the default language * clean up + add caret * add hover state * set active state on active language * make workspace isNew an observable * update workspace contexts * fix typescript errors --- .../repository/detail-repository.interface.ts | 6 +- ...repository-detail-data-source.interface.ts | 2 +- .../src/backoffice/backoffice.element.ts | 6 +- .../repository/document-type.repository.ts | 6 +- .../sources/document-type.server.data.ts | 22 +- .../repository/document.repository.ts | 6 +- .../sources/document.server.data.ts | 21 +- .../documents/repository/sources/index.ts | 7 + .../workspace/document-workspace.context.ts | 15 +- .../repository/media-type.repository.ts | 26 +- .../workspace/media-type-workspace.context.ts | 8 +- .../media/repository/media.repository.ts | 6 +- .../sources/media.detail.server.data.ts | 20 +- .../workspace/media-workspace.context.ts | 15 +- .../repository/member-group.repository.ts | 16 +- .../member-group-workspace.context.ts | 8 +- .../repository/member-type.repository.ts | 12 +- .../sources/member-type.detail.server.data.ts | 6 +- .../member-type-workspace.context.ts | 17 +- .../workspace/member-workspace.context.ts | 2 +- .../backoffice/settings/cultures/manifests.ts | 3 + .../cultures/repository/culture.repository.ts | 29 ++ .../settings/cultures/repository/manifests.ts | 13 + .../repository/sources/culture.server.data.ts | 32 ++ .../cultures/repository/sources/index.ts | 12 + .../repository/data-type.repository.ts | 6 +- .../workspace/data-type-workspace.context.ts | 18 +- .../src/backoffice/settings/index.ts | 2 + .../languages/app-language-select.element.ts | 149 ++++++++++ .../languages/app-language.context.ts | 50 ++++ .../languages/entity-actions/manifests.ts | 22 ++ .../language-picker-modal-layout.element.ts | 72 +++++ .../settings/languages/language.store.ts | 87 ------ .../settings/languages/manifests.ts | 4 +- .../repository/language.repository.ts | 145 +++++++++ .../languages/repository/language.store.ts | 35 +++ .../languages/repository/manifests.ts | 13 + .../languages/repository/sources/index.ts | 16 + .../sources/language.server.data.ts | 120 ++++++++ ...root-table-delete-column-layout.element.ts | 67 ++--- .../language-root-workspace.element.ts | 48 ++- .../language/language-workspace.context.ts | 108 +++---- .../language/language-workspace.element.ts | 42 +-- .../languages/workspace/language/manifests.ts | 35 ++- .../edit-language-workspace-view.element.ts | 218 ++++++++++++++ .../workspace-view-language-edit.element.ts | 276 ------------------ .../body-layout/body-layout.element.ts | 2 +- .../components/dropdown/dropdown.element.ts | 54 ++++ .../src/backoffice/shared/components/index.ts | 5 +- .../input-culture-select.element.ts | 115 ++++++++ .../input-language-picker.element.ts | 186 ++++++++++++ .../input-list-base/input-list-base.ts | 4 +- .../section-dashboards.element.ts | 16 +- .../section-sidebar.element.ts | 38 +-- .../components/section/section.element.ts | 16 +- .../workspace-context.interface.ts | 12 +- .../workspace-context/workspace-context.ts | 12 +- .../workspace-entity-context.interface.ts | 5 - .../entity-actions/delete/delete.action.ts | 4 +- .../shared/workspace-actions/save.action.ts | 27 +- .../repository/template.repository.ts | 6 +- .../workspace/template-workspace.context.ts | 12 +- ...ashboard-translation-dictionary.element.ts | 2 +- .../entity-actions/create/create.action.ts | 2 +- .../repository/dictionary.repository.ts | 6 +- .../workspace/dictionary-workspace.context.ts | 13 +- .../workspace/user-group-workspace.context.ts | 11 +- .../users/workspace/user-workspace.context.ts | 8 +- .../src/core/mocks/data/languages.data.ts | 24 +- .../core/mocks/domains/language.handlers.ts | 29 +- .../modal/layouts/modal-layout-picker-base.ts | 41 +-- .../picker-layout-section.element.ts | 10 +- .../picker-layout-user-group.element.ts | 10 +- .../picker-user/picker-layout-user.element.ts | 10 +- .../src/core/modal/modal.service.ts | 19 +- 75 files changed, 1776 insertions(+), 772 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/culture.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/sources/culture.server.data.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/sources/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language-select.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language.context.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/entity-actions/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language.server.data.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/dropdown/dropdown.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-culture-select/input-culture-select.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts diff --git a/src/Umbraco.Web.UI.Client/libs/repository/detail-repository.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/detail-repository.interface.ts index ff6e58a689..ea842ef36f 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/detail-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/detail-repository.interface.ts @@ -1,7 +1,7 @@ import type { ProblemDetailsModel } from '@umbraco-cms/backend-api'; export interface UmbDetailRepository { - createDetailsScaffold(parentKey: string | null): Promise<{ + createScaffold(parentKey: string | null): Promise<{ data?: DetailType; error?: ProblemDetailsModel; }>; @@ -11,11 +11,11 @@ export interface UmbDetailRepository { error?: ProblemDetailsModel; }>; - createDetail(data: DetailType): Promise<{ + create(data: DetailType): Promise<{ error?: ProblemDetailsModel; }>; - saveDetail(data: DetailType): Promise<{ + save(data: DetailType): Promise<{ error?: ProblemDetailsModel; }>; diff --git a/src/Umbraco.Web.UI.Client/libs/repository/repository-detail-data-source.interface.ts b/src/Umbraco.Web.UI.Client/libs/repository/repository-detail-data-source.interface.ts index 04f2894b39..7715d5a61e 100644 --- a/src/Umbraco.Web.UI.Client/libs/repository/repository-detail-data-source.interface.ts +++ b/src/Umbraco.Web.UI.Client/libs/repository/repository-detail-data-source.interface.ts @@ -5,5 +5,5 @@ export interface RepositoryDetailDataSource { get(key: string): Promise>; insert(data: DetailType): Promise>; update(data: DetailType): Promise>; - trash(key: string): Promise>; + delete(key: string): Promise>; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts index dae0f7ba21..69e02e26c2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/backoffice.element.ts @@ -38,8 +38,10 @@ import { UmbDataTypeTreeStore } from './settings/data-types/repository/data-type import { UmbTemplateTreeStore } from './templating/templates/tree/data/template.tree.store'; import { UmbTemplateDetailStore } from './templating/templates/workspace/data/template.detail.store'; import { UmbThemeContext } from './themes/theme.context'; -import { UmbLanguageStore } from './settings/languages/language.store'; +import { UmbLanguageStore } from './settings/languages/repository/language.store'; import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { UMB_APP_LANGUAGE_CONTEXT_TOKEN, UmbAppLanguageContext } from './settings/languages/app-language.context'; import '@umbraco-cms/router'; @@ -54,7 +56,6 @@ import './packages'; import './search'; import './templating'; import './shared'; -import { UmbLitElement } from '@umbraco-cms/element'; @defineElement('umb-backoffice') export class UmbBackofficeElement extends UmbLitElement { @@ -108,6 +109,7 @@ export class UmbBackofficeElement extends UmbLitElement { new UmbTemplateDetailStore(this); new UmbLanguageStore(this); + this.provideContext(UMB_APP_LANGUAGE_CONTEXT_TOKEN, new UmbAppLanguageContext(this)); this.provideContext(UMB_BACKOFFICE_CONTEXT_TOKEN, new UmbBackofficeContext()); this.provideContext(UMB_CURRENT_USER_HISTORY_STORE_CONTEXT_TOKEN, new UmbCurrentUserHistoryStore()); new UmbThemeContext(this); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts index f296ca5bdb..085cd78f5e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/document-type.repository.ts @@ -113,7 +113,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, UmbDetailRe // DETAILS: - async createDetailsScaffold(parentKey: string | null) { + async createScaffold(parentKey: string | null) { await this.#init; if (!parentKey) { @@ -148,7 +148,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, UmbDetailRe // Could potentially be general methods: - async createDetail(template: ItemType) { + async create(template: ItemType) { await this.#init; if (!template || !template.key) { @@ -170,7 +170,7 @@ export class UmbDocumentTypeRepository implements UmbTreeRepository, UmbDetailRe return { error }; } - async saveDetail(item: ItemType) { + async save(item: ItemType) { await this.#init; if (!item || !item.key) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts index 3bb0855410..f014c438ba 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/document-types/repository/sources/document-type.server.data.ts @@ -157,23 +157,33 @@ export class UmbDocumentTypeServerDataSource implements RepositoryDetailDataSour * @return {*} * @memberof UmbDocumentTypeServerDataSource */ - // TODO: Error mistake in this: async delete(key: string) { if (!key) { const error: ProblemDetailsModel = { title: 'Key is missing' }; return { error }; } - // TODO: use resources when end point is ready: - return tryExecuteAndNotify( - this.#host, - fetch('/umbraco/management/api/v1/document-type/trash', { + let problemDetails: ProblemDetailsModel | undefined = undefined; + + try { + await fetch('/umbraco/management/api/v1/document-type/trash', { method: 'POST', body: JSON.stringify([key]), headers: { 'Content-Type': 'application/json', }, - }) + }); + } catch (error) { + problemDetails = { title: 'Delete document Failed' }; + } + + return { error: problemDetails }; + + // TODO: use resources when end point is ready: + /* + return tryExecuteAndNotify( + this.#host, ); + */ } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts index 7c0149a51f..7e0188a602 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/document.repository.ts @@ -113,7 +113,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi // DETAILS: - async createDetailsScaffold(parentKey: string | null) { + async createScaffold(parentKey: string | null) { await this.#init; if (!parentKey) { @@ -143,7 +143,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi // Could potentially be general methods: - async createDetail(item: ItemType) { + async create(item: ItemType) { await this.#init; if (!item || !item.key) { @@ -165,7 +165,7 @@ export class UmbDocumentRepository implements UmbTreeRepository, UmbDetailReposi return { error }; } - async saveDetail(item: ItemType) { + async save(item: ItemType) { await this.#init; if (!item || !item.key) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts index 41480a0d0f..3807b77c1d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/repository/sources/document.server.data.ts @@ -158,23 +158,30 @@ export class UmbDocumentServerDataSource implements RepositoryDetailDataSource { + trash(key: string): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts index 8c9006ae8d..71364f6a7e 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/documents/documents/workspace/document-workspace.context.ts @@ -26,7 +26,6 @@ export class UmbDocumentWorkspaceContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface { - #isNew = false; #host: UmbControllerHostInterface; #documentRepository: UmbDocumentRepository; #documentTypeRepository: UmbDocumentTypeRepository; @@ -68,16 +67,16 @@ export class UmbDocumentWorkspaceContext async load(entityKey: string) { const { data } = await this.#documentRepository.requestByKey(entityKey); if (data) { - this.#isNew = false; + this.setIsNew(false); this.#document.next(data); this.#draft.next(data); } } async createScaffold(parentKey: string | null) { - const { data } = await this.#documentRepository.createDetailsScaffold(parentKey); + const { data } = await this.#documentRepository.createScaffold(parentKey); if (!data) return; - this.#isNew = true; + this.setIsNew(true); this.#document.next(data); this.#draft.next(data); } @@ -225,13 +224,13 @@ export class UmbDocumentWorkspaceContext async save() { if (!this.#draft.value) return; - if (this.#isNew) { - await this.#documentRepository.createDetail(this.#draft.value); + if (this.getIsNew()) { + await this.#documentRepository.create(this.#draft.value); } else { - await this.#documentRepository.saveDetail(this.#draft.value); + await this.#documentRepository.save(this.#draft.value); } // If it went well, then its not new anymore?. - this.#isNew = false; + this.setIsNew(false); } async delete(key: string) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts index 71c8a21df6..bb3e3ff038 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/repository/media-type.repository.ts @@ -1,13 +1,13 @@ -import { UmbMediaTypeTreeStore, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN } from "./media-type.tree.store"; -import { UmbMediaTypeDetailServerDataSource } from "./sources/media-type.detail.server.data"; -import { UmbMediaTypeDetailStore, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN } from "./media-type.detail.store"; -import { MediaTypeTreeServerDataSource } from "./sources/media-type.tree.server.data"; -import { ProblemDetailsModel } from "@umbraco-cms/backend-api"; -import { UmbContextConsumerController } from "@umbraco-cms/context-api"; -import { UmbControllerHostInterface } from "@umbraco-cms/controller"; -import type { MediaTypeDetails } from "@umbraco-cms/models"; -import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from "@umbraco-cms/notification"; -import { UmbTreeRepository, RepositoryTreeDataSource } from "@umbraco-cms/repository"; +import { UmbMediaTypeTreeStore, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT_TOKEN } from './media-type.tree.store'; +import { UmbMediaTypeDetailServerDataSource } from './sources/media-type.detail.server.data'; +import { UmbMediaTypeDetailStore, UMB_MEDIA_TYPE_DETAIL_STORE_CONTEXT_TOKEN } from './media-type.detail.store'; +import { MediaTypeTreeServerDataSource } from './sources/media-type.tree.server.data'; +import { ProblemDetailsModel } from '@umbraco-cms/backend-api'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import type { MediaTypeDetails } from '@umbraco-cms/models'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { UmbTreeRepository, RepositoryTreeDataSource } from '@umbraco-cms/repository'; export class UmbMediaTypeRepository implements UmbTreeRepository { #init!: Promise; @@ -103,7 +103,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { // DETAILS - async createDetailsScaffold() { + async createScaffold() { await this.#init; return this.#detailSource.createScaffold(); } @@ -130,7 +130,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { return this.#detailSource.delete(key); } - async saveDetail(mediaType: MediaTypeDetails) { + async save(mediaType: MediaTypeDetails) { await this.#init; // TODO: should we show a notification if the media type is missing? @@ -157,7 +157,7 @@ export class UmbMediaTypeRepository implements UmbTreeRepository { return { error }; } - async createDetail(mediaType: MediaTypeDetails) { + async create(mediaType: MediaTypeDetails) { await this.#init; if (!mediaType.name) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts index 4433b62d75..2445086e43 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media-types/workspace/media-type-workspace.context.ts @@ -51,16 +51,18 @@ export class UmbWorkspaceMediaTypeContext } async createScaffold() { - const { data } = await this.#repo.createDetailsScaffold(); + const { data } = await this.#repo.createScaffold(); if (!data) return; + this.setIsNew(true); this.#data.next(data); } async save() { if (!this.#data.value) return; - this.#repo.saveDetail(this.#data.value); + await this.#repo.save(this.#data.value); + this.setIsNew(false); } - + public destroy(): void { this.#data.complete(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts index 365e728589..831af8b644 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/media.repository.ts @@ -111,7 +111,7 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor // DETAILS: - async createDetailsScaffold(parentKey: string | null) { + async createScaffold(parentKey: string | null) { await this.#init; if (!parentKey) { @@ -141,7 +141,7 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor // Could potentially be general methods: - async createDetail(template: ItemDetailType) { + async create(template: ItemDetailType) { await this.#init; if (!template || !template.key) { @@ -163,7 +163,7 @@ export class UmbMediaRepository implements UmbTreeRepository, UmbDetailRepositor return { error }; } - async saveDetail(document: ItemDetailType) { + async save(document: ItemDetailType) { await this.#init; if (!document || !document.key) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts index cbed009384..0d75a206b9 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/repository/sources/media.detail.server.data.ts @@ -181,22 +181,30 @@ export class UmbMediaDetailServerDataSource implements RepositoryDetailDataSourc * @return {*} * @memberof UmbTemplateDetailServerDataSource */ - // TODO: Error mistake in this: async delete(key: string) { if (!key) { const error: ProblemDetailsModel = { title: 'Key is missing' }; return { error }; } - return tryExecuteAndNotify( - this.#host, - fetch('/umbraco/management/api/v1/media/delete', { + let problemDetails: ProblemDetailsModel | undefined = undefined; + + try { + await fetch('/umbraco/management/api/v1/media/delete', { method: 'POST', body: JSON.stringify([key]), headers: { 'Content-Type': 'application/json', }, - }) - ); + }); + } catch (error) { + problemDetails = { title: 'Delete document Failed' }; + } + + return { error: problemDetails }; + + /* TODO: use backend cli when available. + return tryExecuteAndNotify(this.#host); + */ } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts index c287034bf1..fe27ecb936 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/media/media/workspace/media-workspace.context.ts @@ -10,7 +10,6 @@ export class UmbMediaWorkspaceContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface { - #isNew = false; #host: UmbControllerHostInterface; #detailRepository: UmbMediaRepository; @@ -54,27 +53,27 @@ export class UmbMediaWorkspaceContext async load(entityKey: string) { const { data } = await this.#detailRepository.requestByKey(entityKey); if (data) { - this.#isNew = false; + this.setIsNew(false); this.#data.next(data); } } async createScaffold(parentKey: string | null) { - const { data } = await this.#detailRepository.createDetailsScaffold(parentKey); + const { data } = await this.#detailRepository.createScaffold(parentKey); if (!data) return; - this.#isNew = true; + this.setIsNew(true); this.#data.next(data); } async save() { if (!this.#data.value) return; - if (this.#isNew) { - await this.#detailRepository.createDetail(this.#data.value); + if (this.isNew) { + await this.#detailRepository.create(this.#data.value); } else { - await this.#detailRepository.saveDetail(this.#data.value); + await this.#detailRepository.save(this.#data.value); } // If it went well, then its not new anymore?. - this.#isNew = false; + this.setIsNew(false); } async delete(key: string) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts index 0084c13140..7f273071e5 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/repository/member-group.repository.ts @@ -39,7 +39,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => { this.#notificationService = instance; - }); + }); } async requestRootTreeItems() { @@ -89,9 +89,9 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep // DETAIL - async createDetailsScaffold() { + async createScaffold() { await this.#init; - return this.#detailSource.createScaffold(); + return this.#detailSource.createScaffold(); } async requestByKey(key: string) { @@ -111,7 +111,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep return { data, error }; } - async createDetail(detail: MemberGroupDetails) { + async create(detail: MemberGroupDetails) { await this.#init; if (!detail.name) { @@ -129,9 +129,9 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep return { data, error }; } - async saveDetail(memberGroup: MemberGroupDetails) { + async save(memberGroup: MemberGroupDetails) { await this.#init; - + if (!memberGroup || !memberGroup.name) { const error: ProblemDetailsModel = { title: 'Member group is missing' }; return { error }; @@ -140,7 +140,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep const { error } = await this.#detailSource.update(memberGroup); if (!error) { - const notification = { data: { message: `Member group '${memberGroup.name} saved`}}; + const notification = { data: { message: `Member group '${memberGroup.name} saved` } }; this.#notificationService?.peek('positive', notification); } @@ -149,7 +149,7 @@ export class UmbMemberGroupRepository implements UmbTreeRepository, UmbDetailRep return { error }; } - + async delete(key: string) { await this.#init; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts index 726841e634..194e50c0ae 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-groups/workspace/member-group-workspace.context.ts @@ -52,16 +52,18 @@ export class UmbWorkspaceMemberGroupContext } async createScaffold() { - const { data } = await this.#repo.createDetailsScaffold(); + const { data } = await this.#repo.createScaffold(); if (!data) return; + this.setIsNew(true); this.#data.next(data); } async save() { if (!this.#data.value) return; - this.#repo.saveDetail(this.#data.value); + await this.#repo.save(this.#data.value); + this.setIsNew(true); } - + public destroy(): void { this.#data.complete(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts index d2512d0aa0..c03d4eeb44 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/member-type.repository.ts @@ -106,9 +106,9 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo // DETAILS - async createDetailsScaffold() { + async createScaffold() { await this.#init; - return this.#detailSource.createDetailsScaffold(); + return this.#detailSource.createScaffold(); } async requestByKey(key: string) { @@ -153,7 +153,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo return { error }; } - async saveDetail(detail: ItemType) { + async save(detail: ItemType) { await this.#init; // TODO: should we show a notification if the MemberType is missing? @@ -163,7 +163,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo return { error }; } - const { error } = await this.#detailSource.saveDetail(detail); + const { error } = await this.#detailSource.save(detail); if (!error) { const notification = { data: { message: `Member type '${detail.name}' saved` } }; @@ -180,7 +180,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo return { error }; } - async createDetail(detail: MemberTypeDetails) { + async create(detail: MemberTypeDetails) { await this.#init; if (!detail.name) { @@ -188,7 +188,7 @@ export class UmbMemberTypeRepository implements UmbTreeRepository, UmbDetailRepo return { error }; } - const { data, error } = await this.#detailSource.createDetail(detail); + const { data, error } = await this.#detailSource.create(detail); if (!error) { const notification = { data: { message: `Member type '${detail.name}' created` } }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.detail.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.detail.server.data.ts index 01f2f5537a..58ec90e115 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.detail.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/repository/sources/member-type.detail.server.data.ts @@ -22,7 +22,7 @@ export class UmbMemberTypeDetailServerDataSource implements UmbDetailRepository< * @return {*} * @memberof UmbMemberTypeDetailServerDataSource */ - async createDetailsScaffold() { + async createScaffold() { const data = {} as MemberTypeDetails; return { data }; } @@ -45,7 +45,7 @@ export class UmbMemberTypeDetailServerDataSource implements UmbDetailRepository< * @return {*} * @memberof UmbMemberTypeDetailServerDataSource */ - async saveDetail(memberType: MemberTypeDetails) { + async save(memberType: MemberTypeDetails) { if (!memberType.key) { const error: ProblemDetailsModel = { title: 'MemberType key is missing' }; return { error }; @@ -73,7 +73,7 @@ export class UmbMemberTypeDetailServerDataSource implements UmbDetailRepository< * @return {*} * @memberof UmbMemberTypeDetailServerDataSource */ - async createDetail(data: MemberTypeDetails) { + async create(data: MemberTypeDetails) { const requestBody = { name: data.name, }; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/workspace/member-type-workspace.context.ts index 5f076fa40b..166d07c469 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/member-types/workspace/member-type-workspace.context.ts @@ -11,7 +11,6 @@ export class UmbWorkspaceMemberTypeContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface { - #isNew = false; #host: UmbControllerHostInterface; #dataTypeRepository: UmbMemberTypeRepository; @@ -27,22 +26,22 @@ export class UmbWorkspaceMemberTypeContext async load(entityKey: string) { const { data } = await this.#dataTypeRepository.requestByKey(entityKey); if (data) { - this.#isNew = false; + this.setIsNew(false); this.#data.next(data); } } async createScaffold() { - const { data } = await this.#dataTypeRepository.createDetailsScaffold(); + const { data } = await this.#dataTypeRepository.createScaffold(); if (!data) return; - this.#isNew = true; + this.setIsNew(true); this.#data.next(data); } getData() { return this.#data.getValue(); } - + getEntityKey() { return this.getData()?.key || ''; } @@ -61,13 +60,13 @@ export class UmbWorkspaceMemberTypeContext async save() { if (!this.#data.value) return; - if (this.#isNew) { - await this.#dataTypeRepository.createDetail(this.#data.value); + if (this.isNew) { + await this.#dataTypeRepository.create(this.#data.value); } else { - await this.#dataTypeRepository.saveDetail(this.#data.value); + await this.#dataTypeRepository.save(this.#data.value); } // If it went well, then its not new anymore?. - this.#isNew = false; + this.setIsNew(false); } async delete(key: string) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts index d010b9edd0..e3ea0a4a1a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/members/members/workspace/member-workspace.context.ts @@ -18,7 +18,7 @@ export class UmbWorkspaceMemberContext } setName(name: string) { - this.#manager.state.update({name}); + this.#manager.state.update({ name }); } getEntityType = this.#manager.getEntityType; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/manifests.ts new file mode 100644 index 0000000000..2bd802ce9c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as repositoryManifests } from './repository/manifests'; + +export const manifests = [...repositoryManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/culture.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/culture.repository.ts new file mode 100644 index 0000000000..abdd4e6e72 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/culture.repository.ts @@ -0,0 +1,29 @@ +import { UmbCultureServerDataSource } from './sources/culture.server.data'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; + +export class UmbCultureRepository { + #init!: Promise; + #host: UmbControllerHostInterface; + + #dataSource: UmbCultureServerDataSource; + + #notificationService?: UmbNotificationService; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + + this.#dataSource = new UmbCultureServerDataSource(this.#host); + + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#notificationService = instance; + }), + ]); + } + + requestCultures({ skip, take } = { skip: 0, take: 1000 }) { + return this.#dataSource.getCollection({ skip, take }); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/manifests.ts new file mode 100644 index 0000000000..0bc2ae18bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/manifests.ts @@ -0,0 +1,13 @@ +import { UmbCultureRepository } from '../repository/culture.repository'; +import { ManifestRepository } from 'libs/extensions-registry/repository.models'; + +export const CULTURE_REPOSITORY_ALIAS = 'Umb.Repository.Cultures'; + +const repository: ManifestRepository = { + type: 'repository', + alias: CULTURE_REPOSITORY_ALIAS, + name: 'Cultures Repository', + class: UmbCultureRepository, +}; + +export const manifests = [repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/sources/culture.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/sources/culture.server.data.ts new file mode 100644 index 0000000000..02ac62a9bb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/sources/culture.server.data.ts @@ -0,0 +1,32 @@ +import { UmbCultureDataSource } from '.'; +import { CultureResource } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the Language that fetches data from the server + * @export + * @class UmbLanguageServerDataSource + * @implements {RepositoryDetailDataSource} + */ +export class UmbCultureServerDataSource implements UmbCultureDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of UmbLanguageServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbLanguageServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Get a list of cultures on the server + * @return {*} + * @memberof UmbLanguageServerDataSource + */ + async getCollection({ skip, take }: { skip: number; take: number }) { + return tryExecuteAndNotify(this.#host, CultureResource.getCulture({ skip, take })); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/sources/index.ts new file mode 100644 index 0000000000..8b3555ed3f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/cultures/repository/sources/index.ts @@ -0,0 +1,12 @@ +import { PagedCultureModel } from '@umbraco-cms/backend-api'; +import type { DataSourceResponse } from '@umbraco-cms/models'; + +// TODO: This is a temporary solution until we have a proper paging interface +type paging = { + skip: number; + take: number; +}; + +export interface UmbCultureDataSource { + getCollection(paging: paging): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts index 0b93de9956..0c52ff35ad 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/repository/data-type.repository.ts @@ -113,7 +113,7 @@ export class UmbDataTypeRepository implements UmbTreeRepository, UmbDetailReposi // DETAILS: - async createDetailsScaffold(parentKey: string | null) { + async createScaffold(parentKey: string | null) { await this.#init; if (!parentKey) { @@ -148,7 +148,7 @@ export class UmbDataTypeRepository implements UmbTreeRepository, UmbDetailReposi // Could potentially be general methods: - async createDetail(template: ItemType) { + async create(template: ItemType) { await this.#init; if (!template || !template.key) { @@ -170,7 +170,7 @@ export class UmbDataTypeRepository implements UmbTreeRepository, UmbDetailReposi return { error }; } - async saveDetail(item: ItemType) { + async save(item: ItemType) { await this.#init; if (!item || !item.key) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts index ace8a934a8..e07767d67a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/data-types/workspace/data-type-workspace.context.ts @@ -1,4 +1,3 @@ -import { BehaviorSubject } from 'rxjs'; import { UmbWorkspaceContext } from '../../../shared/components/workspace/workspace-context/workspace-context'; import { UmbWorkspaceEntityContextInterface } from '../../../shared/components/workspace/workspace-context/workspace-entity-context.interface'; import { UmbDataTypeRepository } from '../repository/data-type.repository'; @@ -10,7 +9,6 @@ export class UmbDataTypeWorkspaceContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface { - #isNew = false; #host: UmbControllerHostInterface; #dataTypeRepository: UmbDataTypeRepository; @@ -28,24 +26,26 @@ export class UmbDataTypeWorkspaceContext async load(key: string) { const { data } = await this.#dataTypeRepository.requestByKey(key); if (data) { - this.#isNew = false; + this.setIsNew(false); this.#data.update(data); } } async createScaffold(parentKey: string | null) { - const { data } = await this.#dataTypeRepository.createDetailsScaffold(parentKey); + const { data } = await this.#dataTypeRepository.createScaffold(parentKey); if (!data) return; - this.#isNew = true; + this.setIsNew(true); this.#data.next(data); } getData() { return this.#data.getValue(); } + getEntityKey() { return this.getData()?.key || ''; } + getEntityType() { return 'data-type'; } @@ -75,13 +75,13 @@ export class UmbDataTypeWorkspaceContext async save() { if (!this.#data.value) return; - if (this.#isNew) { - await this.#dataTypeRepository.createDetail(this.#data.value); + if (this.isNew) { + await this.#dataTypeRepository.create(this.#data.value); } else { - await this.#dataTypeRepository.saveDetail(this.#data.value); + await this.#dataTypeRepository.save(this.#data.value); } // If it went well, then its not new anymore?. - this.#isNew = false; + this.setIsNew(false); } async delete(key: string) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts index de60d18c58..43ef6bfa40 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/index.ts @@ -2,6 +2,7 @@ import { manifests as settingsSectionManifests } from './section.manifests'; import { manifests as dashboardManifests } from './dashboards/manifests'; import { manifests as dataTypeManifests } from './data-types/manifests'; import { manifests as extensionManifests } from './extensions/manifests'; +import { manifests as cultureManifests } from './cultures/manifests'; import { manifests as languageManifests } from './languages/manifests'; import { manifests as logviewerManifests } from './logviewer/manifests'; @@ -20,6 +21,7 @@ registerExtensions([ ...dashboardManifests, ...dataTypeManifests, ...extensionManifests, + ...cultureManifests, ...languageManifests, ...logviewerManifests, ]); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language-select.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language-select.element.ts new file mode 100644 index 0000000000..1415471f37 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language-select.element.ts @@ -0,0 +1,149 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { UUIMenuItemEvent } from '@umbraco-ui/uui'; +import { UmbLanguageRepository } from './repository/language.repository'; +import { UMB_APP_LANGUAGE_CONTEXT_TOKEN, UmbAppLanguageContext } from './app-language.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { LanguageModel } from '@umbraco-cms/backend-api'; + +@customElement('umb-app-language-select') +export class UmbAppLanguageSelectElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + position: relative; + z-index: 10; + } + + #toggle { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + height: 70px; + padding: 0 var(--uui-size-8); + border-bottom: 1px solid var(--uui-color-border); + font-size: 14px; + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + } + + #toggle:hover { + background-color: var(--uui-color-surface-emphasis); + } + `, + ]; + + @state() + private _languages: Array = []; + + @state() + private _appLanguage?: LanguageModel; + + @state() + private _isOpen = false; + + #repository = new UmbLanguageRepository(this); + #appLanguageContext?: UmbAppLanguageContext; + #languagesObserver?: any; + + constructor() { + super(); + + this.consumeContext(UMB_APP_LANGUAGE_CONTEXT_TOKEN, (instance) => { + this.#appLanguageContext = instance; + this.#observeAppLanguage(); + }); + } + + async #observeAppLanguage() { + if (!this.#appLanguageContext) return; + + this.observe(this.#appLanguageContext.appLanguage, (isoCode) => { + this._appLanguage = isoCode; + }); + } + + async #observeLanguages() { + const { asObservable } = await this.#repository.requestLanguages(); + + this.#languagesObserver = this.observe(asObservable(), (languages) => { + this._languages = languages; + }); + } + + #onClick() { + this.#toggleDropdown(); + } + + #onClose() { + this.#closeDropdown(); + } + + #toggleDropdown() { + this._isOpen = !this._isOpen; + + // first start observing the languages when the dropdown is opened + if (this._isOpen && !this.#languagesObserver) { + this.#observeLanguages(); + } + } + + #closeDropdown() { + this._isOpen = false; + } + + #onLabelClick(event: UUIMenuItemEvent) { + const menuItem = event.target; + const isoCode = menuItem.dataset.isoCode; + + // TODO: handle error + if (!isoCode) return; + + this.#appLanguageContext?.setLanguage(isoCode); + this._isOpen = false; + } + + render() { + return html` + ${this.#renderTrigger()} ${this.#renderContent()} + `; + } + + #renderTrigger() { + return html``; + } + + #renderContent() { + return html`
+ ${repeat( + this._languages, + (language) => language.isoCode, + (language) => + html` + + ` + )} +
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-app-language-select': UmbAppLanguageSelectElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language.context.ts new file mode 100644 index 0000000000..603e2e4955 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/app-language.context.ts @@ -0,0 +1,50 @@ +import { UmbLanguageRepository } from './repository/language.repository'; +import { ObjectState, UmbObserverController } from '@umbraco-cms/observable-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { LanguageModel } from '@umbraco-cms/backend-api'; + +export class UmbAppLanguageContext { + #host: UmbControllerHostInterface; + #languageRepository: UmbLanguageRepository; + + #languages: Array = []; + + #appLanguage = new ObjectState(undefined); + appLanguage = this.#appLanguage.asObservable(); + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + this.#languageRepository = new UmbLanguageRepository(this.#host); + this.#observeLanguages(); + } + + setLanguage(isoCode: string) { + const language = this.#languages.find((x) => x.isoCode === isoCode); + this.#appLanguage.update(language); + } + + async #observeLanguages() { + const { asObservable } = await this.#languageRepository.requestLanguages(); + + new UmbObserverController(this.#host, asObservable(), (languages) => { + this.#languages = languages; + + // If the app language is not set, set it to the default language + if (!this.#appLanguage.getValue()) { + this.#initAppLanguage(); + } + }); + } + + #initAppLanguage() { + const defaultLanguage = this.#languages.find((x) => x.isDefault); + // TODO: do we always have a default language? + // do we always get the default language on the first request, or could it be on page 2? + // in that case do we then need an endpoint to get the default language? + if (!defaultLanguage?.isoCode) return; + this.setLanguage(defaultLanguage.isoCode); + } +} + +export const UMB_APP_LANGUAGE_CONTEXT_TOKEN = new UmbContextToken(UmbAppLanguageContext.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/entity-actions/manifests.ts new file mode 100644 index 0000000000..84b57075b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/entity-actions/manifests.ts @@ -0,0 +1,22 @@ +import { UmbDeleteEntityAction } from '../../../shared/entity-actions/delete/delete.action'; +import { ManifestEntityAction } from '@umbraco-cms/extensions-registry'; + +const entityType = 'language'; +const repositoryAlias = 'Umb.Repository.Languages'; + +const entityActions: Array = [ + { + type: 'entityAction', + alias: 'Umb.EntityAction.Language.Delete', + name: 'Delete Language Entity Action', + meta: { + entityType, + repositoryAlias, + icon: 'umb:trash', + label: 'Delete', + api: UmbDeleteEntityAction, + }, + }, +]; + +export const manifests = [...entityActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts new file mode 100644 index 0000000000..6fa7c9ace8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language-picker/language-picker-modal-layout.element.ts @@ -0,0 +1,72 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { repeat } from 'lit-html/directives/repeat.js'; +import { UUIMenuItemElement, UUIMenuItemEvent } from '@umbraco-ui/uui'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { UmbModalLayoutPickerBase } from '../../../../core/modal/layouts/modal-layout-picker-base'; +import { UmbLanguageRepository } from '../repository/language.repository'; +import { LanguageModel } from '@umbraco-cms/backend-api'; + +@customElement('umb-language-picker-modal-layout') +export class UmbLanguagePickerModalLayoutElement extends UmbModalLayoutPickerBase { + static styles = [UUITextStyles, css``]; + + @state() + private _languages: Array = []; + + private _languageRepository = new UmbLanguageRepository(this); + + async firstUpdated() { + const { data } = await this._languageRepository.requestLanguages(); + this._languages = data?.items ?? []; + } + + #onSelection(event: UUIMenuItemEvent) { + event?.stopPropagation(); + const language = event?.target as UUIMenuItemElement; + const isoCode = language.dataset.isoCode; + if (!isoCode) return; + this.handleSelection(isoCode); + } + + get #filteredLanguages() { + if (this.data?.filter) { + return this._languages.filter(this.data.filter); + } else { + return this._languages; + } + } + + render() { + return html` + + ${repeat( + this.#filteredLanguages, + (item) => item.isoCode, + (item) => html` + + + + ` + )} + +
+ + +
+
`; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-language-picker-modal-layout': UmbLanguagePickerModalLayoutElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts deleted file mode 100644 index ffc3f178ba..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/language.store.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Observable } from 'rxjs'; -import { CultureModel, CultureResource, LanguageModel, LanguageResource } from '@umbraco-cms/backend-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/resources'; -import { UmbContextToken } from '@umbraco-cms/context-api'; -import { UmbStoreBase } from '@umbraco-cms/store'; -import { ArrayState } from '@umbraco-cms/observable-api'; -import { UmbControllerHostInterface } from '@umbraco-cms/controller'; - -export type UmbLanguageStoreItemType = LanguageModel; -export const UMB_LANGUAGE_STORE_CONTEXT_TOKEN = new UmbContextToken('umbLanguageStore'); - -/** - * @export - * @class UmbLanguageStore - * @extends {UmbStoreBase} - * @description - Data Store for languages - */ -export class UmbLanguageStore extends UmbStoreBase { - #data = new ArrayState([], (x) => x.isoCode); - #availableLanguages = new ArrayState([], (x) => x.name); - - public readonly availableLanguages = this.#availableLanguages.asObservable(); - - constructor(host: UmbControllerHostInterface) { - super(host, UMB_LANGUAGE_STORE_CONTEXT_TOKEN.toString()); - } - - getByIsoCode(isoCode: string) { - tryExecuteAndNotify(this._host, LanguageResource.getLanguageByIsoCode({ isoCode })).then(({ data }) => { - if (data) { - this.#data.appendOne(data); - } - }); - - return this.#data.getObservablePart((items) => items.find((item) => item.isoCode === isoCode)); - } - - getAll(): Observable> { - tryExecuteAndNotify(this._host, LanguageResource.getLanguage({ skip: 0, take: 1000 })).then(({ data }) => { - this.#data.append(data?.items ?? []); - }); - - return this.#data; - } - - getAvailableCultures() { - tryExecuteAndNotify(this._host, CultureResource.getCulture({ skip: 0, take: 1000 })).then(({ data }) => { - if (!data) return; - this.#availableLanguages.append(data.items); - }); - - return this.availableLanguages; - } - - async save(language: UmbLanguageStoreItemType): Promise { - if (language.isoCode) { - const { data: updatedLanguage } = await tryExecuteAndNotify( - this._host, - LanguageResource.putLanguageByIsoCode({ isoCode: language.isoCode, requestBody: language }) - ); - if (updatedLanguage) { - this.#data.appendOne(updatedLanguage); - } - } else { - const { data: newLanguage } = await tryExecuteAndNotify( - this._host, - LanguageResource.postLanguage({ requestBody: language }) - ); - if (newLanguage) { - this.#data.appendOne(newLanguage); - } - } - } - - async delete(isoCodes: Array) { - // TODO: revisit this. It looks a bit weird with the nested tryExecuteAndNotify - const queue = isoCodes.map((isoCode) => - tryExecuteAndNotify( - this._host, - tryExecuteAndNotify(this._host, LanguageResource.deleteLanguageByIsoCode({ isoCode })).then(() => isoCode) - ) - ); - const results = await Promise.all(queue); - const filtered = results.filter((x) => !!x).map((result) => result.data); - this.#data.remove(filtered); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/manifests.ts index 450deac4e9..c7e92f8c72 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/manifests.ts @@ -1,4 +1,6 @@ +import { manifests as repositoryManifests } from './repository/manifests'; import { manifests as treeManifests } from './sidebar-menu-item/manifests'; +import { manifests as entityActions } from './entity-actions/manifests'; import { manifests as workspaceManifests } from './workspace/manifests'; -export const manifests = [...treeManifests, ...workspaceManifests]; +export const manifests = [...repositoryManifests, ...entityActions, ...treeManifests, ...workspaceManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts new file mode 100644 index 0000000000..9fa8683457 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.repository.ts @@ -0,0 +1,145 @@ +import { UmbLanguageServerDataSource } from './sources/language.server.data'; +import { UmbLanguageStore, UMB_LANGUAGE_STORE_CONTEXT_TOKEN } from './language.store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { UmbContextConsumerController } from '@umbraco-cms/context-api'; +import { UmbNotificationService, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/notification'; +import { LanguageModel, ProblemDetailsModel } from '@umbraco-cms/backend-api'; + +export class UmbLanguageRepository { + #init!: Promise; + + #host: UmbControllerHostInterface; + + #dataSource: UmbLanguageServerDataSource; + #languageStore?: UmbLanguageStore; + + #notificationService?: UmbNotificationService; + + constructor(host: UmbControllerHostInterface) { + this.#host = host; + + this.#dataSource = new UmbLanguageServerDataSource(this.#host); + + this.#init = Promise.all([ + new UmbContextConsumerController(this.#host, UMB_NOTIFICATION_SERVICE_CONTEXT_TOKEN, (instance) => { + this.#notificationService = instance; + }), + + new UmbContextConsumerController(this.#host, UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (instance) => { + this.#languageStore = instance; + }), + ]); + } + + // TODO: maybe this should be renamed to something more generic? + async requestByIsoCode(isoCode: string) { + await this.#init; + + if (!isoCode) { + const error: ProblemDetailsModel = { title: 'Iso code is missing' }; + return { error }; + } + + return this.#dataSource.get(isoCode); + } + + // TODO: maybe this should be renamed to something more generic. + // Revisit when collection are in place + async requestLanguages({ skip, take } = { skip: 0, take: 1000 }) { + await this.#init; + + const { data, error } = await this.#dataSource.getCollection({ skip, take }); + + if (data) { + // TODO: allow to append an array of items to the store + data.items.forEach((x) => this.#languageStore?.append(x)); + } + + return { data, error, asObservable: () => this.#languageStore!.data }; + } + + async requestItems(isoCodes: Array) { + // HACK: filter client side until we have a proper server side endpoint + // TODO: we will get a different size model here, how do we handle that in the store? + const { data, error } = await this.requestLanguages(); + + let items = undefined; + + if (data) { + // TODO: how do we best handle this? They might have a smaller data set than the details + items = data.items = data.items.filter((x) => isoCodes.includes(x.isoCode!)); + data.items.forEach((x) => this.#languageStore?.append(x)); + } + + return { data: items, error, asObservable: () => this.#languageStore!.items(isoCodes) }; + } + + /** + * Creates a new Language scaffold + * @param + * @return {*} + * @memberof UmbLanguageRepository + */ + async createScaffold() { + return this.#dataSource.createScaffold(); + } + + async create(language: LanguageModel) { + await this.#init; + + const { error } = await this.#dataSource.insert(language); + + if (!error) { + this.#languageStore?.append(language); + const notification = { data: { message: `Language created` } }; + this.#notificationService?.peek('positive', notification); + } + + return { error }; + } + + /** + * Saves a language + * @param {LanguageModel} language + * @return {*} + * @memberof UmbLanguageRepository + */ + async save(language: LanguageModel) { + await this.#init; + + const { error } = await this.#dataSource.update(language); + + if (!error) { + const notification = { data: { message: `Language saved` } }; + this.#notificationService?.peek('positive', notification); + this.#languageStore?.append(language); + } + + return { error }; + } + + /** + * Deletes a language + * @param {string} isoCode + * @return {*} + * @memberof UmbLanguageRepository + */ + async delete(isoCode: string) { + await this.#init; + + if (!isoCode) { + const error: ProblemDetailsModel = { title: 'Language iso code is missing' }; + return { error }; + } + + const { error } = await this.#dataSource.delete(isoCode); + + if (!error) { + this.#languageStore?.remove([isoCode]); + const notification = { data: { message: `Language deleted` } }; + this.#notificationService?.peek('positive', notification); + } + + return { error }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts new file mode 100644 index 0000000000..7f13331aca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/language.store.ts @@ -0,0 +1,35 @@ +import { UmbContextToken } from '@umbraco-cms/context-api'; +import { UmbStoreBase } from '@umbraco-cms/store'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { ArrayState } from '@umbraco-cms/observable-api'; +import { LanguageModel } from '@umbraco-cms/backend-api'; + +/** + * @export + * @class UmbLanguageStore + * @extends {UmbStoreBase} + * @description - Details Data Store for Languages + */ +export class UmbLanguageStore extends UmbStoreBase { + #data = new ArrayState([], (x) => x.isoCode); + data = this.#data.asObservable(); + + constructor(host: UmbControllerHostInterface) { + super(host, UmbLanguageStore.name); + } + + append(language: LanguageModel) { + this.#data.append([language]); + } + + remove(uniques: string[]) { + this.#data.remove(uniques); + } + + // TODO: how do we best handle this? They might have a smaller data set than the details + items(isoCodes: Array) { + return this.#data.getObservablePart((items) => items.filter((item) => isoCodes.includes(item.isoCode ?? ''))); + } +} + +export const UMB_LANGUAGE_STORE_CONTEXT_TOKEN = new UmbContextToken(UmbLanguageStore.name); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts new file mode 100644 index 0000000000..7020f13c22 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/manifests.ts @@ -0,0 +1,13 @@ +import { UmbLanguageRepository } from '../repository/language.repository'; +import { ManifestRepository } from 'libs/extensions-registry/repository.models'; + +export const LANGUAGE_REPOSITORY_ALIAS = 'Umb.Repository.Languages'; + +const repository: ManifestRepository = { + type: 'repository', + alias: LANGUAGE_REPOSITORY_ALIAS, + name: 'Languages Repository', + class: UmbLanguageRepository, +}; + +export const manifests = [repository]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts new file mode 100644 index 0000000000..81efc0303f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/index.ts @@ -0,0 +1,16 @@ +import { LanguageModel, PagedLanguageModel } from '@umbraco-cms/backend-api'; +import type { DataSourceResponse } from '@umbraco-cms/models'; +import { RepositoryDetailDataSource } from '@umbraco-cms/repository'; + +// TODO: This is a temporary solution until we have a proper paging interface +type paging = { + skip: number; + take: number; +}; + +export interface UmbLanguageDataSource extends RepositoryDetailDataSource { + createScaffold(): Promise>; + get(isoCode: string): Promise>; + delete(isoCode: string): Promise>; + getCollection(paging: paging): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language.server.data.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language.server.data.ts new file mode 100644 index 0000000000..cca5af2960 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/repository/sources/language.server.data.ts @@ -0,0 +1,120 @@ +import { ProblemDetailsModel, LanguageResource, LanguageModel } from '@umbraco-cms/backend-api'; +import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { tryExecuteAndNotify } from '@umbraco-cms/resources'; + +/** + * A data source for the Language that fetches data from the server + * @export + * @class UmbLanguageServerDataSource + * @implements {RepositoryDetailDataSource} + */ +export class UmbLanguageServerDataSource implements UmbLanguageServerDataSource { + #host: UmbControllerHostInterface; + + /** + * Creates an instance of UmbLanguageServerDataSource. + * @param {UmbControllerHostInterface} host + * @memberof UmbLanguageServerDataSource + */ + constructor(host: UmbControllerHostInterface) { + this.#host = host; + } + + /** + * Fetches a Language with the given iso code from the server + * @param {string} isoCode + * @return {*} + * @memberof UmbLanguageServerDataSource + */ + async get(isoCode: string) { + if (!isoCode) { + const error: ProblemDetailsModel = { title: 'Iso Code is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + LanguageResource.getLanguageByIsoCode({ + isoCode, + }) + ); + } + + /** + * Creates a new Language scaffold + * @param + * @return {*} + * @memberof UmbLanguageServerDataSource + */ + async createScaffold() { + const data: LanguageModel = { + name: '', + isDefault: false, + isMandatory: false, + fallbackIsoCode: '', + isoCode: '', + }; + + return { data }; + } + + /** + * Inserts a new Language on the server + * @param {LanguageModel} language + * @return {*} + * @memberof UmbLanguageServerDataSource + */ + async insert(language: LanguageModel) { + if (!language.isoCode) { + const error: ProblemDetailsModel = { title: 'Language iso code is missing' }; + return { error }; + } + + return tryExecuteAndNotify(this.#host, LanguageResource.postLanguage({ requestBody: language })); + } + + /** + * Updates a Language on the server + * @param {LanguageModel} language + * @return {*} + * @memberof UmbLanguageServerDataSource + */ + async update(language: LanguageModel) { + if (!language.isoCode) { + const error: ProblemDetailsModel = { title: 'Language iso code is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + LanguageResource.putLanguageByIsoCode({ isoCode: language.isoCode, requestBody: language }) + ); + } + + /** + * Deletes a Language on the server + * @param {string} isoCode + * @return {*} + * @memberof UmbLanguageServerDataSource + */ + async delete(isoCode: string) { + if (!isoCode) { + const error: ProblemDetailsModel = { title: 'Iso code is missing' }; + return { error }; + } + + return tryExecuteAndNotify( + this.#host, + tryExecuteAndNotify(this.#host, LanguageResource.deleteLanguageByIsoCode({ isoCode })).then(() => isoCode) + ); + } + + /** + * Get a list of Languages on the server + * @return {*} + * @memberof UmbLanguageServerDataSource + */ + async getCollection({ skip, take }: { skip: number; take: number }) { + return tryExecuteAndNotify(this.#host, LanguageResource.getLanguage({ skip, take })); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-table-delete-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-table-delete-column-layout.element.ts index ac3b22a1f4..d318a967fa 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-table-delete-column-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-table-delete-column-layout.element.ts @@ -1,64 +1,47 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html, nothing } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; -import { UmbLanguageStore, UmbLanguageStoreItemType, UMB_LANGUAGE_STORE_CONTEXT_TOKEN } from '../../language.store'; -import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../../core/modal'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; import { UmbLitElement } from '@umbraco-cms/element'; +import { LanguageModel } from '@umbraco-cms/backend-api'; @customElement('umb-language-root-table-delete-column-layout') export class UmbLanguageRootTableDeleteColumnLayoutElement extends UmbLitElement { static styles = [UUITextStyles, css``]; @property({ attribute: false }) - value!: UmbLanguageStoreItemType; + value!: LanguageModel; - #languageStore?: UmbLanguageStore; - #modalService?: UmbModalService; + @state() + _isOpen = false; - constructor() { - super(); - this.consumeContext(UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (instance) => { - this.#languageStore = instance; - }); - - this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => { - this.#modalService = instance; - }); + #onActionExecuted() { + this._isOpen = false; } - #handleDelete(event: MouseEvent) { - event.stopImmediatePropagation(); - if (!this.#languageStore) return; + #onClick() { + this._isOpen = !this._isOpen; + } - const modalHandler = this.#modalService?.confirm({ - headline: 'Delete language', - content: html` -
- This will delete language ${this.value.name}. -
- Are you sure you want to delete? - `, - color: 'danger', - confirmLabel: 'Delete', - }); - - modalHandler?.onClose().then(({ confirmed }) => { - if (confirmed) { - this.#languageStore?.delete([this.value.isoCode!]); - } - }); + #onClose() { + this._isOpen = false; } render() { + // TODO: we need to use conditionals on each action here. But until we have that in place + // we'll just remove all actions on the default language. if (this.value.isDefault) return nothing; - return html``; + return html` + + + + + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-workspace.element.ts index 28e918ee79..aba3ffb008 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language-root/language-root-workspace.element.ts @@ -1,17 +1,16 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html } from 'lit'; import { customElement, state } from 'lit/decorators.js'; -import { UmbLanguageStore, UmbLanguageStoreItemType, UMB_LANGUAGE_STORE_CONTEXT_TOKEN } from '../../language.store'; import { UmbTableColumn, UmbTableConfig, UmbTableItem } from '../../../../shared/components/table'; -import { UmbWorkspaceEntityElement } from '../../../../shared/components/workspace/workspace-entity-element.interface'; +import { UmbLanguageRepository } from '../../repository/language.repository'; import { UmbLitElement } from '@umbraco-cms/element'; +import { LanguageModel } from '@umbraco-cms/backend-api'; -import '../language/language-workspace.element'; import './language-root-table-delete-column-layout.element'; import './language-root-table-name-column-layout.element'; @customElement('umb-language-root-workspace') -export class UmbLanguageRootWorkspaceElement extends UmbLitElement implements UmbWorkspaceEntityElement { +export class UmbLanguageRootWorkspaceElement extends UmbLitElement { static styles = [ UUITextStyles, css` @@ -35,7 +34,7 @@ export class UmbLanguageRootWorkspaceElement extends UmbLitElement implements Um @state() private _tableColumns: Array = [ { - name: 'Language', + name: 'Name', alias: 'languageName', elementName: 'umb-language-root-table-name-column-layout', }, @@ -44,15 +43,15 @@ export class UmbLanguageRootWorkspaceElement extends UmbLitElement implements Um alias: 'isoCode', }, { - name: 'Default language', + name: 'Default', alias: 'defaultLanguage', }, { - name: 'Mandatory language', + name: 'Mandatory', alias: 'mandatoryLanguage', }, { - name: 'Fall back language', + name: 'Fallback', alias: 'fallBackLanguage', }, { @@ -65,32 +64,22 @@ export class UmbLanguageRootWorkspaceElement extends UmbLitElement implements Um @state() private _tableItems: Array = []; - #languageStore?: UmbLanguageStore; + #languageRepository = new UmbLanguageRepository(this); - constructor() { - super(); - - this.consumeContext(UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (instance) => { - this.#languageStore = instance; - this.#observeLanguages(); - }); + connectedCallback() { + super.connectedCallback(); + this.#observeLanguages(); } - load(): void { - // Not relevant for this workspace + async #observeLanguages() { + const { asObservable } = await this.#languageRepository.requestLanguages(); + + if (asObservable) { + this.observe(asObservable(), (languages) => this.#createTableItems(languages)); + } } - create(): void { - // Not relevant for this workspace - } - - #observeLanguages() { - this.#languageStore?.getAll().subscribe((languages) => { - this.#createTableItems(languages); - }); - } - - #createTableItems(languages: Array) { + #createTableItems(languages: Array) { this._tableItems = languages.map((language) => { return { key: language.isoCode ?? '', @@ -139,6 +128,7 @@ export class UmbLanguageRootWorkspaceElement extends UmbLitElement implements Um color="default" href="section/settings/language/create/root">
+
diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts index abd98b217b..8f1fd0b0f4 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.context.ts @@ -1,70 +1,76 @@ -import { UmbLanguageStore, UmbLanguageStoreItemType, UMB_LANGUAGE_STORE_CONTEXT_TOKEN } from '../../language.store'; +import { UmbLanguageRepository } from '../../repository/language.repository'; +import { UmbWorkspaceContext } from '../../../../shared/components/workspace/workspace-context/workspace-context'; +import type { LanguageModel } from '@umbraco-cms/backend-api'; +import { ObjectState } from '@umbraco-cms/observable-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; -import { ObjectState, UmbObserverController } from '@umbraco-cms/observable-api'; -import { UmbContextConsumerController } from '@umbraco-cms/context-api'; -const DefaultLanguageData: UmbLanguageStoreItemType = { - name: '', - isoCode: '', - isDefault: false, - isMandatory: false, -}; +export class UmbLanguageWorkspaceContext extends UmbWorkspaceContext { + #host: UmbControllerHostInterface; + #languageRepository: UmbLanguageRepository; -export class UmbWorkspaceLanguageContext { - public host: UmbControllerHostInterface; + #data = new ObjectState(undefined); + data = this.#data.asObservable(); - #entityKey: string | null; + // TODO: this is a temp solution to bubble validation errors to the UI + #validationErrors = new ObjectState(undefined); + validationErrors = this.#validationErrors.asObservable(); - #data; - public readonly data; - - #store: UmbLanguageStore | null = null; - protected _storeObserver?: UmbObserverController; - - constructor(host: UmbControllerHostInterface, entityKey: string | null) { - this.host = host; - this.#entityKey = entityKey; - - this.#data = new ObjectState(DefaultLanguageData); - this.data = this.#data.asObservable(); - - new UmbContextConsumerController(host, UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (_instance: UmbLanguageStore) => { - this.#store = _instance; - this.#observeStore(); - }); + constructor(host: UmbControllerHostInterface) { + super(host); + this.#host = host; + this.#languageRepository = new UmbLanguageRepository(this.#host); } - #observeStore(): void { - if (!this.#store || this.#entityKey === null) { - return; + async load(isoCode: string) { + const { data } = await this.#languageRepository.requestByIsoCode(isoCode); + if (data) { + this.setIsNew(false); + this.#data.update(data); } - - this._storeObserver?.destroy(); - this._storeObserver = new UmbObserverController(this.host, this.#store.getByIsoCode(this.#entityKey), (content) => { - if (!content) return; // TODO: Handle nicely if there is no content data. - this.update(content); - }); } - public getData() { + async createScaffold() { + const { data } = await this.#languageRepository.createScaffold(); + if (!data) return; + this.setIsNew(true); + this.#data.update(data); + } + + getData() { return this.#data.getValue(); } - public getAvailableCultures() { - //TODO: Don't use !, however this will be changed with the introduction of repositories. - return this.#store!.getAvailableCultures(); + getEntityType() { + return 'language'; } - public update(data: Partial) { - this.#data.next({ ...this.getData(), ...data }); + setName(name: string) { + this.#data.update({ name }); } - public save(): Promise { - if (!this.#store) { - // TODO: more beautiful error: - console.error('Could not save cause workspace context has no store.'); - return Promise.resolve(); - } - return this.#store.save(this.getData()); + setCulture(isoCode: string) { + this.#data.update({ isoCode }); + } + + setMandatory(isMandatory: boolean) { + this.#data.update({ isMandatory }); + } + + setDefault(isDefault: boolean) { + this.#data.update({ isDefault }); + } + + setFallbackCulture(isoCode: string) { + this.#data.update({ fallbackIsoCode: isoCode }); + } + + // TODO: this is a temp solution to bubble validation errors to the UI + setValidationErrors(errorMap: any) { + // TODO: I can't use the update method to set the value to undefined + this.#validationErrors.next(errorMap); + } + + destroy(): void { + this.#data.complete(); } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts index 7bf01a12ef..409d475777 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/language-workspace.element.ts @@ -1,12 +1,12 @@ import { UUITextStyles } from '@umbraco-ui/uui-css'; import { css, html, nothing } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; +import { customElement, state } from 'lit/decorators.js'; import { UUIInputElement, UUIInputEvent } from '@umbraco-ui/uui'; -import { UmbLanguageStoreItemType } from '../../language.store'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; import { UmbWorkspaceEntityElement } from '../../../../shared/components/workspace/workspace-entity-element.interface'; -import { UmbWorkspaceLanguageContext } from './language-workspace.context'; +import { UmbLanguageWorkspaceContext } from './language-workspace.context'; import { UmbLitElement } from '@umbraco-cms/element'; -import '../../../../shared/components/workspace/workspace-action/save/workspace-action-node-save.element.ts'; +import { LanguageModel } from '@umbraco-cms/backend-api'; @customElement('umb-language-workspace') export class UmbLanguageWorkspaceElement extends UmbLitElement implements UmbWorkspaceEntityElement { @@ -25,25 +25,25 @@ export class UmbLanguageWorkspaceElement extends UmbLitElement implements UmbWor `, ]; - @property() - language?: UmbLanguageStoreItemType; + @state() + _language?: LanguageModel; - #languageWorkspaceContext?: UmbWorkspaceLanguageContext; + #languageWorkspaceContext = new UmbLanguageWorkspaceContext(this); + + constructor() { + super(); + + this.observe(this.#languageWorkspaceContext.data, (data) => { + this._language = data; + }); + } load(key: string): void { - this.provideLanguageWorkspaceContext(key); + this.#languageWorkspaceContext.load(key); } - create(parentKey: string | null): void { - this.provideLanguageWorkspaceContext(parentKey); - } - - public provideLanguageWorkspaceContext(entityKey: string | null) { - this.#languageWorkspaceContext = new UmbWorkspaceLanguageContext(this, entityKey); - this.provideContext('umbWorkspaceContext', this.#languageWorkspaceContext); - this.#languageWorkspaceContext.data.subscribe((language) => { - this.language = language; - }); + create(): void { + this.#languageWorkspaceContext.createScaffold(); } #handleInput(event: UUIInputEvent) { @@ -51,13 +51,13 @@ export class UmbLanguageWorkspaceElement extends UmbLitElement implements UmbWor const target = event.composedPath()[0] as UUIInputElement; if (typeof target?.value === 'string') { - this.#languageWorkspaceContext?.update({ name: target.value }); + this.#languageWorkspaceContext?.setName(target.value); } } } render() { - if (!this.language) return nothing; + if (!this._language) return nothing; return html` @@ -65,7 +65,7 @@ export class UmbLanguageWorkspaceElement extends UmbLitElement implements UmbWor - + `; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/manifests.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/manifests.ts index 1c686ba026..b2581f133d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/manifests.ts @@ -1,3 +1,5 @@ +import { UmbSaveWorkspaceAction } from '../../../../shared/workspace-actions/save.action'; +import { LANGUAGE_REPOSITORY_ALIAS } from '../../repository/manifests'; import type { ManifestWorkspace, ManifestWorkspaceAction, ManifestWorkspaceView } from '@umbraco-cms/models'; const workspace: ManifestWorkspace = { @@ -10,27 +12,12 @@ const workspace: ManifestWorkspace = { }, }; -const workspaceActions: Array = [ - { - type: 'workspaceAction', - alias: 'Umb.WorkspaceAction.Language.Save', - name: 'Save Language Workspace Action', - loader: () => - import('../../../../shared/components/workspace/workspace-action/save/workspace-action-node-save.element'), - meta: { - workspaces: ['Umb.Workspace.Language'], - look: 'primary', - color: 'positive', - }, - }, -]; - const workspaceViews: Array = [ { type: 'workspaceView', alias: 'Umb.WorkspaceView.Language.Edit', name: 'Language Workspace Edit View', - loader: () => import('./views/edit/workspace-view-language-edit.element'), + loader: () => import('./views/edit/edit-language-workspace-view.element'), weight: 90, meta: { workspaces: ['Umb.Workspace.Language'], @@ -41,4 +28,20 @@ const workspaceViews: Array = [ }, ]; +const workspaceActions: Array = [ + { + type: 'workspaceAction', + alias: 'Umb.WorkspaceAction.Language.Save', + name: 'Save Language Workspace Action', + meta: { + workspaces: ['Umb.Workspace.Language'], + look: 'primary', + color: 'positive', + label: 'Save', + repositoryAlias: LANGUAGE_REPOSITORY_ALIAS, + api: UmbSaveWorkspaceAction, + }, + }, +]; + export const manifests = [workspace, ...workspaceViews, ...workspaceActions]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts new file mode 100644 index 0000000000..458916f8e2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/edit-language-workspace-view.element.ts @@ -0,0 +1,218 @@ +import { UUIBooleanInputEvent, UUIToggleElement } from '@umbraco-ui/uui'; +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, nothing } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit/directives/if-defined.js'; +import { UmbChangeEvent } from '@umbraco-cms/events'; +import { UmbLanguageWorkspaceContext } from '../../language-workspace.context'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { LanguageModel } from '@umbraco-cms/backend-api'; +import UmbInputCultureSelectElement from 'src/backoffice/shared/components/input-culture-select/input-culture-select.element'; +import UmbInputLanguagePickerElement from 'src/backoffice/shared/components/input-language-picker/input-language-picker.element'; + +@customElement('umb-edit-language-workspace-view') +export class UmbEditLanguageWorkspaceViewElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + :host { + display: block; + padding: var(--uui-size-space-6); + } + + uui-combobox { + width: 100%; + } + + hr { + border: none; + border-bottom: 1px solid var(--uui-color-divider); + } + + #default-language-warning { + background-color: var(--uui-color-warning); + color: var(--uui-color-warning-contrast); + padding: var(--uui-size-space-4) var(--uui-size-space-5); + border: 1px solid var(--uui-color-warning-standalone); + margin-top: var(--uui-size-space-4); + border-radius: var(--uui-border-radius); + } + + .validation-message { + color: var(--uui-color-danger); + } + `, + ]; + + @state() + _language?: LanguageModel; + + @state() + _isDefaultLanguage = false; + + @state() + _isNew = false; + + @state() + _validationErrors?: { [key: string]: Array }; + + #languageWorkspaceContext?: UmbLanguageWorkspaceContext; + + constructor() { + super(); + + /* TODO: we will need some system to notify about an action has been executed. + In the language workspace we want to clear a default language change warning and reset the initial state after a save action has been executed. */ + let initialStateSet = false; + + this.consumeContext('umbWorkspaceContext', (instance) => { + this.#languageWorkspaceContext = instance; + + this.observe(this.#languageWorkspaceContext.data, (language) => { + this._language = language; + + /* Store the initial value of the default language. + When we change the default language we get notified of the change, + and we need the initial value to compare against */ + if (initialStateSet === false) { + this._isDefaultLanguage = language?.isDefault ?? false; + initialStateSet = true; + } + }); + + this.observe(this.#languageWorkspaceContext.isNew, (value) => { + this._isNew = value; + }); + + this.observe(this.#languageWorkspaceContext.validationErrors, (value) => { + this._validationErrors = value; + this.requestUpdate('_validationErrors'); + }); + }); + } + + #handleCultureChange(event: Event) { + if (event instanceof UmbChangeEvent) { + const target = event.target as UmbInputCultureSelectElement; + const isoCode = target.value.toString(); + const cultureName = target.selectedCultureName; + + if (!isoCode) { + // If the isoCode is empty, we reset the value to the original value. + // Provides a way better UX + //TODO: Maybe the combobox should implement something similar? + const resetFunction = () => { + target.value = this._language?.isoCode ?? ''; + }; + + target.addEventListener('close', resetFunction, { once: true }); + target.addEventListener('blur', resetFunction, { once: true }); + return; + } + + this.#languageWorkspaceContext?.setCulture(isoCode); + + // to improve UX, we set the name to the culture name if it's a new language + if (this._isNew && cultureName) { + this.#languageWorkspaceContext?.setName(cultureName); + } + } + } + + #handleDefaultChange(event: UUIBooleanInputEvent) { + if (event instanceof UUIBooleanInputEvent) { + const target = event.composedPath()[0] as UUIToggleElement; + this.#languageWorkspaceContext?.setDefault(target.checked); + } + } + + #handleMandatoryChange(event: UUIBooleanInputEvent) { + if (event instanceof UUIBooleanInputEvent) { + const target = event.composedPath()[0] as UUIToggleElement; + this.#languageWorkspaceContext?.setMandatory(target.checked); + } + } + + #handleFallbackChange(event: UmbChangeEvent) { + if (event instanceof UmbChangeEvent) { + const target = event.target as UmbInputLanguagePickerElement; + const selectedLanguageIsoCode = target.selectedIsoCodes?.[0]; + this.#languageWorkspaceContext?.setFallbackCulture(selectedLanguageIsoCode); + } + } + + render() { + if (!this._language) return nothing; + + return html` + + +
+ + + + + ${this._validationErrors?.isoCode.map( + (isoCodeError) => html`
${isoCodeError}
` + )} +
+
+ + +
${this._language.isoCode}
+
+ + +
+ +
+ Default language +
An Umbraco site can only have one default language set.
+
+
+ + ${this._language.isDefault !== this._isDefaultLanguage + ? html`
+ Switching default language may result in default content missing. +
` + : nothing} + +
+ +
+ Mandatory language +
Properties on this language have to be filled out before the node can be published.
+
+
+
+
+ + + + language.isoCode !== this._language?.isoCode}> + +
+ `; + } +} + +export default UmbEditLanguageWorkspaceViewElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-edit-language-workspace-view': UmbEditLanguageWorkspaceViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts deleted file mode 100644 index 831f403ca0..0000000000 --- a/src/Umbraco.Web.UI.Client/src/backoffice/settings/languages/workspace/language/views/edit/workspace-view-language-edit.element.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { UUIBooleanInputEvent, UUIComboboxElement, UUIComboboxEvent, UUIToggleElement } from '@umbraco-ui/uui'; -import { UUITextStyles } from '@umbraco-ui/uui-css'; -import { css, html, nothing } from 'lit'; -import { repeat } from 'lit/directives/repeat.js'; -import { customElement, property, state } from 'lit/decorators.js'; -import { ifDefined } from 'lit/directives/if-defined.js'; -import { UmbWorkspaceLanguageContext } from '../../language-workspace.context'; -import { - UmbLanguageStore, - UmbLanguageStoreItemType, - UMB_LANGUAGE_STORE_CONTEXT_TOKEN, -} from '../../../../language.store'; -import { UmbLitElement } from '@umbraco-cms/element'; -import { CultureModel, LanguageModel } from '@umbraco-cms/backend-api'; - -@customElement('umb-workspace-view-language-edit') -export class UmbWorkspaceViewLanguageEditElement extends UmbLitElement { - static styles = [ - UUITextStyles, - css` - :host { - display: block; - padding: var(--uui-size-space-6); - } - uui-combobox { - width: 100%; - } - hr { - border: none; - border-bottom: 1px solid var(--uui-color-divider); - } - #culture-warning, - #default-language-warning { - padding: var(--uui-size-space-4) var(--uui-size-space-5); - border: 1px solid; - margin-top: var(--uui-size-space-4); - border-radius: var(--uui-border-radius); - } - #culture-warning { - background-color: var(--uui-color-danger); - color: var(--uui-color-danger-contrast); - border-color: var(--uui-color-danger-standalone); - } - #default-language-warning { - background-color: var(--uui-color-warning); - color: var(--uui-color-warning-contrast); - border-color: var(--uui-color-warning-standalone); - } - `, - ]; - - @property() - language?: UmbLanguageStoreItemType; - - @state() - private _languages: UmbLanguageStoreItemType[] = []; - - @state() - private _availableCultures: CultureModel[] = []; - - @state() - private _search = ''; - - @state() - private _startData: LanguageModel | null = null; - - #languageWorkspaceContext?: UmbWorkspaceLanguageContext; - - constructor() { - super(); - - this.consumeContext('umbWorkspaceContext', (instance) => { - this.#languageWorkspaceContext = instance; - - if (!this.#languageWorkspaceContext) return; - - this.observe(this.#languageWorkspaceContext.data, (language) => { - this.language = language; - - if (this._startData === null) { - this._startData = language; - } - }); - this.observe(this.#languageWorkspaceContext.getAvailableCultures(), (cultures) => { - this._availableCultures = cultures; - }); - }); - - this.consumeContext(UMB_LANGUAGE_STORE_CONTEXT_TOKEN, (instance: UmbLanguageStore) => { - if (!instance) return; - - instance.getAll().subscribe((languages: Array) => { - this._languages = languages; - }); - }); - } - - #handleLanguageChange(event: Event) { - if (event instanceof UUIComboboxEvent) { - const target = event.composedPath()[0] as UUIComboboxElement; - const isoCode = target.value.toString(); - - if (isoCode) { - this.#languageWorkspaceContext?.update({ isoCode }); - - // If the language name is not set, we set it to the name of the selected language. - if (!this.language?.name) { - const language = this._availableCultures.find((culture) => culture.name === isoCode); - if (language) { - this.#languageWorkspaceContext?.update({ name: language.name }); - } - } - } else { - // If the isoCode is empty, we reset the value to the original value. - // Provides a way better UX - //TODO: Maybe the combobox should implement something similar? - const resetFunction = () => { - target.value = this.language?.isoCode ?? ''; - }; - - target.addEventListener('close', resetFunction, { once: true }); - target.addEventListener('blur', resetFunction, { once: true }); - } - } - } - - // TODO: move some of these methods to the context - #handleSearchChange(event: Event) { - const target = event.composedPath()[0] as UUIComboboxElement; - this._search = target.search; - } - - #handleDefaultChange(event: UUIBooleanInputEvent) { - if (event instanceof UUIBooleanInputEvent) { - const target = event.composedPath()[0] as UUIToggleElement; - - this.#languageWorkspaceContext?.update({ isDefault: target.checked }); - } - } - - #handleMandatoryChange(event: UUIBooleanInputEvent) { - if (event instanceof UUIBooleanInputEvent) { - const target = event.composedPath()[0] as UUIToggleElement; - - this.#languageWorkspaceContext?.update({ isMandatory: target.checked }); - } - } - - #handleFallbackChange(event: UUIComboboxEvent) { - if (event instanceof UUIComboboxEvent) { - const target = event.composedPath()[0] as UUIComboboxElement; - this.#languageWorkspaceContext?.update({ fallbackIsoCode: target.value.toString() }); - } - } - - get #filteredCultures(): Array { - return this._availableCultures.filter((culture) => { - return culture.englishName?.toLowerCase().includes(this._search.toLowerCase()); - }); - } - - get #fallbackLanguages() { - return this._languages.filter((language) => { - return language.isoCode !== this.language?.isoCode; - }); - } - - get #fallbackLanguage() { - return this.#fallbackLanguages.find((language) => language.isoCode === this.language?.fallbackIsoCode); - } - - get #fromAvailableCultures() { - return this._availableCultures.find((culture) => culture.name === this.language?.isoCode); - } - - #renderCultureWarning() { - if (!this._startData?.isoCode || this._startData?.isoCode === this.language?.isoCode) return nothing; - - return html`
- Changing the culture for a language may be an expensive operation and will result in the content cache and indexes - being rebuilt. -
`; - } - - #renderDefaultLanguageWarning() { - if (this._startData?.isDefault || this.language?.isDefault !== true) return nothing; - - return html`
- Switching default language may result in default content missing. -
`; - } - - render() { - if (!this.language) return nothing; - - return html` - - -
- - - ${repeat( - this.#filteredCultures, - (language) => language.name, - (language) => - html` - ${language.englishName} - ` - )} - - - ${this.#renderCultureWarning()} -
-
- -
${this.language.isoCode}
-
- -
- -
- Default language -
An Umbraco site can only have one default language set.
-
-
- ${this.#renderDefaultLanguageWarning()} -
- -
- Mandatory language -
Properties on this language have to be filled out before the node can be published.
-
-
-
-
- - - - ${repeat( - this.#fallbackLanguages, - (language) => language.isoCode, - (language) => - html` - ${language.name} - ` - )} - - - -
- `; - } -} - -export default UmbWorkspaceViewLanguageEditElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-workspace-view-language-edit': UmbWorkspaceViewLanguageEditElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts index 1f12bd5873..0770f98a60 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/body-layout/body-layout.element.ts @@ -20,7 +20,7 @@ export class UmbBodyLayout extends LitElement { align-items: center; justify-content: space-between; width: 100%; - min-height: 60px; + height: 70px; background-color: var(--uui-color-surface); border-bottom: 1px solid var(--uui-color-border); box-sizing: border-box; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/dropdown/dropdown.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/dropdown/dropdown.element.ts new file mode 100644 index 0000000000..ab21ee4f5e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/dropdown/dropdown.element.ts @@ -0,0 +1,54 @@ +import { UUITextStyles } from '@umbraco-ui/uui-css'; +import { css, html, nothing } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import { UmbLitElement } from '@umbraco-cms/element'; + +@customElement('umb-dropdown') +export class UmbDropdownElement extends UmbLitElement { + static styles = [ + UUITextStyles, + css` + #container { + display: inline-block; + } + + #dropdown { + overflow: hidden; + z-index: -1; + background-color: var(--uui-combobox-popover-background-color, var(--uui-color-surface)); + border: 1px solid var(--uui-color-border); + border-radius: var(--uui-border-radius); + width: 100%; + height: 100%; + box-sizing: border-box; + box-shadow: var(--uui-shadow-depth-3); + } + `, + ]; + + @property({ type: Boolean, reflect: true }) + open = false; + + render() { + return html` + + + ${this.open ? this.#renderDropdown() : nothing} + + `; + } + + #renderDropdown() { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-dropdown': UmbDropdownElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts index fee735ecf4..a249ba7493 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/index.ts @@ -1,4 +1,5 @@ //TODO: we need to figure out what components should be available for extensions and load them upfront +// TODO: we need to move these files into their respective folders/silos. We then need a way for a silo to globally register a component import './backoffice-frame/backoffice-header.element'; import './backoffice-frame/backoffice-main.element'; import './backoffice-frame/backoffice-modal-container.element'; @@ -8,6 +9,7 @@ import './ref-property-editor-ui/ref-property-editor-ui.element'; import './content-property/content-property.element'; import './table/table.element'; import './code-block/code-block.element'; +import './dropdown/dropdown.element'; import './extension-slot/extension-slot.element'; import './workspace/workspace-layout/workspace-layout.element'; @@ -27,6 +29,8 @@ import '../entity-actions/entity-action-list.element'; import './input-media-picker/input-media-picker.element'; import './input-document-picker/input-document-picker.element'; +import './input-language-picker/input-language-picker.element'; +import './input-culture-select/input-culture-select.element'; import './input-color-picker/input-color-picker.element'; import './input-eye-dropper/input-eye-dropper.element'; import './input-checkbox-list/input-checkbox-list.element'; @@ -35,4 +39,3 @@ import './input-multi-url-picker/input-multi-url-picker.element'; import './empty-state/empty-state.element'; import './debug/debug.element'; - diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-culture-select/input-culture-select.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-culture-select/input-culture-select.element.ts new file mode 100644 index 0000000000..3e4d61e854 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-culture-select/input-culture-select.element.ts @@ -0,0 +1,115 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { repeat } from 'lit/directives/repeat.js'; +import { UUIComboboxElement, UUIComboboxEvent } from '@umbraco-ui/uui'; +import { UmbChangeEvent } from '@umbraco-cms/events'; +import { UmbCultureRepository } from '../../../settings/cultures/repository/culture.repository'; +import { UmbLitElement } from '@umbraco-cms/element'; +import { CultureModel } from '@umbraco-cms/backend-api'; + +@customElement('umb-input-culture-select') +export class UmbInputCultureSelectElement extends FormControlMixin(UmbLitElement) { + static styles = [UUITextStyles, css``]; + + /** + * Disables the input + * @type {boolean} + * @attr + * @default false + */ + @property({ type: Boolean, reflect: true }) + disabled = false; + + /** + * Disables the input + * @type {boolean} + * @attr + * @default false + */ + @property({ type: Boolean, reflect: true }) + readonly = false; + + @state() + private _cultures: CultureModel[] = []; + + @state() + private _search = ''; + + public selectedCultureName?: string; + + #cultureRepository = new UmbCultureRepository(this); + + protected getFormElement() { + return undefined; + } + + protected async firstUpdated() { + const { data } = await this.#cultureRepository.requestCultures(); + if (data) { + this._cultures = data.items; + } + } + + #onSearchChange(event: UUIComboboxEvent) { + event.stopPropagation(); + const target = event.composedPath()[0] as UUIComboboxElement; + this._search = target.search; + } + + #onCultureChange(event: UUIComboboxEvent) { + event.stopPropagation(); + const target = event.composedPath()[0] as UUIComboboxElement; + this._value = target.value; + const culture = this._cultures.find((culture) => culture.name === this._value); + this.selectedCultureName = culture?.englishName; + this.dispatchEvent(new UmbChangeEvent()); + } + + get #filteredCultures() { + return this._cultures.filter((culture) => { + return culture.englishName?.toLowerCase().includes(this._search.toLowerCase()); + }); + } + + get #fromAvailableCultures() { + return this._cultures.find((culture) => culture.name === this.value); + } + + render() { + return html` + + ${this.disabled || this.readonly + ? html`${this.#fromAvailableCultures?.englishName}` + : html` + + + ${repeat( + this.#filteredCultures, + (culture) => culture.name, + (culture) => + html` + ${culture.englishName} + ` + )} + + + `} + `; + } +} + +export default UmbInputCultureSelectElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-culture-select': UmbInputCultureSelectElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts new file mode 100644 index 0000000000..e80efad01a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-language-picker/input-language-picker.element.ts @@ -0,0 +1,186 @@ +import { css, html } from 'lit'; +import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; +import { customElement, property, state } from 'lit/decorators.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins'; +import { UmbChangeEvent } from '@umbraco-cms/events'; +import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal'; +import { UmbLitElement } from '@umbraco-cms/element'; +import type { LanguageModel } from '@umbraco-cms/backend-api'; +import type { UmbObserverController } from '@umbraco-cms/observable-api'; +import { UmbLanguageRepository } from 'src/backoffice/settings/languages/repository/language.repository'; + +@customElement('umb-input-language-picker') +export class UmbInputLanguagePickerElement extends FormControlMixin(UmbLitElement) { + static styles = [ + UUITextStyles, + css` + #add-button { + width: 100%; + } + `, + ]; + /** + * This is a minimum amount of selected items in this input. + * @type {number} + * @attr + * @default undefined + */ + @property({ type: Number }) + min?: number; + + /** + * Min validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + minMessage = 'This field need more items'; + + /** + * This is a maximum amount of selected items in this input. + * @type {number} + * @attr + * @default undefined + */ + @property({ type: Number }) + max?: number; + + /** + * Max validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + maxMessage = 'This field exceeds the allowed amount of items'; + + @property({ type: Object, attribute: false }) + public filter: (language: LanguageModel) => boolean = () => true; + + private _selectedIsoCodes: Array = []; + public get selectedIsoCodes(): Array { + return this._selectedIsoCodes; + } + public set selectedIsoCodes(keys: Array) { + this._selectedIsoCodes = keys; + super.value = keys.join(','); + this._observePickedItems(); + } + + @property() + public set value(keysString: string) { + if (keysString !== this._value) { + this.selectedIsoCodes = keysString.split(/[ ,]+/); + } + } + + @state() + private _items?: Array; + + private _modalService?: UmbModalService; + private _repository = new UmbLanguageRepository(this); + private _pickedItemsObserver?: UmbObserverController; + + constructor() { + super(); + + this.addValidator( + 'rangeUnderflow', + () => this.minMessage, + () => !!this.min && this._selectedIsoCodes.length < this.min + ); + + this.addValidator( + 'rangeOverflow', + () => this.maxMessage, + () => !!this.max && this._selectedIsoCodes.length > this.max + ); + + this.consumeContext(UMB_MODAL_SERVICE_CONTEXT_TOKEN, (instance) => { + this._modalService = instance; + }); + } + + protected getFormElement() { + return undefined; + } + + private async _observePickedItems() { + this._pickedItemsObserver?.destroy(); + if (!this._repository) return; + + const { asObservable } = await this._repository.requestItems(this._selectedIsoCodes); + + this._pickedItemsObserver = this.observe(asObservable(), (items) => { + this._items = items; + }); + } + + private _openPicker() { + const modalHandler = this._modalService?.languagePicker({ + multiple: this.max === 1 ? false : true, + selection: [...this._selectedIsoCodes], + filter: this.filter, + }); + + modalHandler?.onClose().then(({ selection }: any) => { + this._setSelection(selection); + }); + } + + private _removeItem(item: LanguageModel) { + const modalHandler = this._modalService?.confirm({ + color: 'danger', + headline: `Remove ${item.name}?`, + content: 'Are you sure you want to remove this item', + confirmLabel: 'Remove', + }); + + modalHandler?.onClose().then(({ confirmed }) => { + if (confirmed) { + const newSelection = this._selectedIsoCodes.filter((value) => value !== item.isoCode); + this._setSelection(newSelection); + } + }); + } + + private _setSelection(newSelection: Array) { + this.selectedIsoCodes = newSelection; + this.dispatchEvent(new UmbChangeEvent()); + } + + render() { + return html` + ${this._items?.map((item) => this._renderItem(item))} + Add + `; + } + + private _renderItem(item: LanguageModel) { + return html` + + + + this._removeItem(item)} label="Remove ${item.name}">Remove + + + `; + } +} + +export default UmbInputLanguagePickerElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-language-picker': UmbInputLanguagePickerElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-list-base/input-list-base.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-list-base/input-list-base.ts index 847b7b9155..78f2d3d54a 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-list-base/input-list-base.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/input-list-base/input-list-base.ts @@ -1,7 +1,7 @@ import { html } from 'lit'; import { property } from 'lit/decorators.js'; import { UUIModalSidebarSize } from '@umbraco-ui/uui-modal-sidebar'; -import { UmbPickerData } from '../../../../core/modal/layouts/modal-layout-picker-base'; +import { UmbPickerModalData } from '../../../../core/modal/layouts/modal-layout-picker-base'; import { UmbModalService, UmbModalType, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '../../../../core/modal'; //TODO: These should probably be imported dynamically. @@ -45,7 +45,7 @@ export class UmbInputListBase extends UmbLitElement { selection: this.value, }, }); - modalHandler?.onClose().then((data: UmbPickerData) => { + modalHandler?.onClose().then((data: UmbPickerModalData) => { if (data) { this.value = data.selection; this.selectionUpdated(); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts index a3d8f6a3d2..61f4bf1fd3 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-dashboards/section-dashboards.element.ts @@ -25,11 +25,12 @@ export class UmbSectionDashboardsElement extends UmbLitElement { background-color: var(--uui-color-surface); height: 70px; border-bottom: 1px solid var(--uui-color-border); + box-sizing: border-box; } #scroll-container { - flex:1; - position:relative; + flex: 1; + position: relative; } #router-slot { @@ -45,10 +46,10 @@ export class UmbSectionDashboardsElement extends UmbLitElement { width: 100%; min-height: 60px; box-sizing: border-box; - margin:0; - padding:0 var(--uui-size-5); - background-color:var(--uui-color-surface); - border-bottom:1px solid var(--uui-color-border); + margin: 0; + padding: 0 var(--uui-size-5); + background-color: var(--uui-color-surface); + border-bottom: 1px solid var(--uui-color-border); } `, ]; @@ -171,8 +172,7 @@ export class UmbSectionDashboardsElement extends UmbLitElement { }} @change=${(event: UmbRouterSlotChangeEvent) => { this._activePath = event.target.localActiveViewPath; - }} - > + }}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.element.ts index 3925b34a71..d388e8bfc2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section-sidebar/section-sidebar.element.ts @@ -1,12 +1,11 @@ import { UUITextStyles } from '@umbraco-ui/uui-css/lib'; import { css, html } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import { UmbSectionContext, UMB_SECTION_CONTEXT_TOKEN } from '../section.context'; +import { customElement } from 'lit/decorators.js'; +import { UmbSectionSidebarContext, UMB_SECTION_SIDEBAR_CONTEXT_TOKEN } from './section-sidebar.context'; +import { UmbLitElement } from '@umbraco-cms/element'; import '../../tree/context-menu/tree-context-menu.service'; import '../section-sidebar-context-menu/section-sidebar-context-menu.element'; -import { UmbSectionSidebarContext, UMB_SECTION_SIDEBAR_CONTEXT_TOKEN } from './section-sidebar.context'; -import { UmbLitElement } from '@umbraco-cms/element'; @customElement('umb-section-sidebar') export class UmbSectionSidebarElement extends UmbLitElement { @@ -23,44 +22,25 @@ export class UmbSectionSidebarElement extends UmbLitElement { flex-direction: column; z-index: 10; } + + #scroll-container { + height: 100%; + overflow-y: auto; + } `, ]; - @state() - private _sectionLabel = ''; - - @state() - private _sectionPathname = ''; - - private _sectionContext?: UmbSectionContext; #sectionSidebarContext = new UmbSectionSidebarContext(this); constructor() { super(); - - this.consumeContext(UMB_SECTION_CONTEXT_TOKEN, (sectionContext) => { - this._sectionContext = sectionContext; - this._observeSectionContext(); - }); - this.provideContext(UMB_SECTION_SIDEBAR_CONTEXT_TOKEN, this.#sectionSidebarContext); } - private _observeSectionContext() { - if (!this._sectionContext) return; - - this.observe(this._sectionContext.pathname, (pathname) => { - this._sectionPathname = pathname || ''; - }); - this.observe(this._sectionContext.label, (label) => { - this._sectionLabel = label || ''; - }); - } - render() { return html` - + diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts index ebfd711528..7120cd3dab 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/section/section.element.ts @@ -12,6 +12,7 @@ import { UmbRouterSlotChangeEvent } from '@umbraco-cms/router'; import './section-sidebar-menu/section-sidebar-menu.element.ts'; import './section-views/section-views.element.ts'; +import '../../../settings/languages/app-language-select.element.ts'; @customElement('umb-section') export class UmbSectionElement extends UmbLitElement { @@ -28,6 +29,10 @@ export class UmbSectionElement extends UmbLitElement { overflow: auto; height: 100%; } + + h3 { + padding: var(--uui-size-4) var(--uui-size-8); + } `, ]; @@ -37,11 +42,16 @@ export class UmbSectionElement extends UmbLitElement { @state() private _menus?: Array; - private _workspaces?: Array; - @state() private _views?: Array; + @state() + private _sectionLabel = ''; + + @state() + private _sectionPathname = ''; + + private _workspaces?: Array; private _sectionContext?: UmbSectionContext; private _sectionAlias?: string; @@ -190,6 +200,8 @@ export class UmbSectionElement extends UmbLitElement { ${this._menus && this._menus.length > 0 ? html` + + items.meta.sections.includes(this._sectionAlias || '')} diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface.ts index 65dfa62416..e372dfc228 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.interface.ts @@ -1,10 +1,12 @@ +import { Observable } from 'rxjs'; + export interface UmbWorkspaceContextInterface { - //readonly data: Observable; - //getUnique(): string | undefined; - + isNew: Observable; + getIsNew(): boolean; + setIsNew(value: boolean): void; getEntityType(): string; - getData(): T; - destroy(): void; + // TODO: temp solution to bubble validation errors to the UI + setValidationErrors?(errorMap: any): void; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.ts index 1ee9462a36..ca0407059d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-context.ts @@ -1,5 +1,6 @@ import { UmbContextProviderController } from '@umbraco-cms/context-api'; import { UmbControllerHostInterface } from '@umbraco-cms/controller'; +import { DeepState } from '@umbraco-cms/observable-api'; /* @@ -7,12 +8,21 @@ TODO: We need to figure out if we like to keep using same alias for all workspac If so we need to align on a interface that all of these implements. otherwise consumers cant trust the workspace-context. */ export abstract class UmbWorkspaceContext { - protected _host: UmbControllerHostInterface; + #isNew = new DeepState(false); + isNew = this.#isNew.asObservable(); + constructor(host: UmbControllerHostInterface) { this._host = host; new UmbContextProviderController(host, 'umbWorkspaceContext', this); } + getIsNew() { + return this.#isNew.getValue(); + } + + setIsNew(isNew: boolean) { + this.#isNew.next(isNew); + } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts index 87fd3e526e..6d15dead5d 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/components/workspace/workspace-context/workspace-entity-context.interface.ts @@ -1,14 +1,9 @@ import { UmbWorkspaceContextInterface } from './workspace-context.interface'; export interface UmbWorkspaceEntityContextInterface extends UmbWorkspaceContextInterface { - //readonly name: Observable; - getEntityKey(): string | undefined; // COnsider if this should go away now that we have getUnique() getEntityType(): string; - getData(): T; - setPropertyValue(alias: string, value: unknown): void; - save(): Promise; } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/entity-actions/delete/delete.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/entity-actions/delete/delete.action.ts index 58e9d919ae..3521026991 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/entity-actions/delete/delete.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/entity-actions/delete/delete.action.ts @@ -4,7 +4,7 @@ import { UmbControllerHostInterface } from '@umbraco-cms/controller'; import { UmbModalService, UMB_MODAL_SERVICE_CONTEXT_TOKEN } from '@umbraco-cms/modal'; export class UmbDeleteEntityAction< - T extends { delete(unique: string): Promise; requestTreeItems(uniques: Array): any } + T extends { delete(unique: string): Promise; requestItems(uniques: Array): any } > extends UmbEntityActionBase { #modalService?: UmbModalService; @@ -19,7 +19,7 @@ export class UmbDeleteEntityAction< async execute() { if (!this.repository || !this.#modalService) return; - const { data } = await this.repository.requestTreeItems([this.unique]); + const { data } = await this.repository.requestItems([this.unique]); if (data) { const item = data[0]; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/shared/workspace-actions/save.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/shared/workspace-actions/save.action.ts index 26476e76f1..55d0925d61 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/shared/workspace-actions/save.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/shared/workspace-actions/save.action.ts @@ -8,12 +8,37 @@ export class UmbSaveWorkspaceAction extends UmbWorkspaceAction data?.name); content = createObservablePart(this.#data, (data) => data?.content); + isNew = false; + constructor(host: UmbControllerHostInterface) { this.#host = host; this.#templateDetailRepo = new UmbTemplateDetailRepository(this.#host); @@ -39,17 +41,9 @@ export class UmbTemplateWorkspaceContext { } async createScaffold(parentKey: string | null) { + this.isNew = true; const { data } = await this.#templateDetailRepo.createScaffold(parentKey); if (!data) return; this.#data.next(data); } - - async save(isNew: boolean) { - if (!this.#data.value) return; - isNew ? this.#templateDetailRepo.insert(this.#data.value) : this.#templateDetailRepo.update(this.#data.value); - } - - async delete(key: string) { - await this.#templateDetailRepo.delete(key); - } } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts index 6ff6446f51..c48a2ae248 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dashboards/dictionary/dashboard-translation-dictionary.element.ts @@ -165,7 +165,7 @@ export class UmbDashboardTranslationDictionaryElement extends UmbLitElement { const { name }: UmbCreateDictionaryModalResultData = await modalHandler.onClose(); if (!name) return; - const result = await this.#repo?.createDetail({ name, parentKey: null, translations: [], key: '' }); + const result = await this.#repo?.create({ name, parentKey: null, translations: [], key: '' }); // TODO => get location header to route to new item console.log(result); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create.action.ts index 53b6d4117f..aeb8b448a2 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create.action.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/entity-actions/create/create.action.ts @@ -47,7 +47,7 @@ export default class UmbCreateDictionaryEntityAction extends UmbEntityActionBase const { name }: UmbCreateDictionaryModalResultData = await modalHandler.onClose(); if (!name) return; - const result = await this.repository?.createDetail({ name, parentKey: this.unique, translations: [], key: ''}); + const result = await this.repository?.create({ name, parentKey: this.unique, translations: [], key: '' }); // TODO => get location header to route to new item console.log(result); diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts index dd9c5e23dd..0ed7a54092 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/repository/dictionary.repository.ts @@ -103,7 +103,7 @@ export class UmbDictionaryRepository implements UmbTreeRepository, UmbDetailRepo // DETAILS - async createDetailsScaffold(parentKey: string | null) { + async createScaffold(parentKey: string | null) { await this.#init; if (!parentKey) { @@ -141,7 +141,7 @@ export class UmbDictionaryRepository implements UmbTreeRepository, UmbDetailRepo return this.#detailSource.delete(key); } - async saveDetail(dictionary: DictionaryDetails) { + async save(dictionary: DictionaryDetails) { await this.#init; // TODO: should we show a notification if the dictionary is missing? @@ -168,7 +168,7 @@ export class UmbDictionaryRepository implements UmbTreeRepository, UmbDetailRepo return { error }; } - async createDetail(detail: DictionaryDetails) { + async create(detail: DictionaryDetails) { await this.#init; if (!detail.name) { diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts index 723af850f5..d8d1080ead 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/translation/dictionary/workspace/dictionary-workspace.context.ts @@ -47,7 +47,7 @@ export class UmbWorkspaceDictionaryContext const updatedValue = this.#data.value.translations?.map((translationItem) => { if (translationItem.isoCode === isoCode) { - return { ...translationItem, translation}; + return { ...translationItem, translation }; } return translationItem; }) ?? []; @@ -57,27 +57,30 @@ export class UmbWorkspaceDictionaryContext updatedValue?.push({ isoCode, translation }); } - this.#data.next({ ...this.#data.value, translations: updatedValue }); + this.#data.next({ ...this.#data.value, translations: updatedValue }); } async load(entityKey: string) { const { data } = await this.#repo.requestByKey(entityKey); if (data) { + this.setIsNew(false); this.#data.next(data); } } async createScaffold(parentKey: string | null) { - const { data } = await this.#repo.createDetailsScaffold(parentKey); + const { data } = await this.#repo.createScaffold(parentKey); if (!data) return; + this.setIsNew(true); this.#data.next(data); } async save() { if (!this.#data.value) return; - this.#repo.saveDetail(this.#data.value); + await this.#repo.save(this.#data.value); + this.setIsNew(false); } - + public destroy(): void { this.#data.complete(); } diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.context.ts index 470163bc3c..8f7d07ef24 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/user-groups/workspace/user-group-workspace.context.ts @@ -4,18 +4,17 @@ import { UmbWorkspaceEntityContextInterface } from '../../../shared/components/w import { UMB_USER_GROUP_STORE_CONTEXT_TOKEN } from '../user-group.store'; import type { UserGroupDetails } from '@umbraco-cms/models'; - -export class UmbWorkspaceUserGroupContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface { - - - +export class UmbWorkspaceUserGroupContext + extends UmbWorkspaceContext + implements UmbWorkspaceEntityContextInterface +{ #manager = new UmbEntityWorkspaceManager(this._host, 'user-group', UMB_USER_GROUP_STORE_CONTEXT_TOKEN); public readonly data = this.#manager.state.asObservable(); public readonly name = this.#manager.state.getObservablePart((state) => state?.name); setName(name: string) { - this.#manager.state.update({name: name}) + this.#manager.state.update({ name: name }); } getEntityType = this.#manager.getEntityType; getUnique = this.#manager.getEntityKey; diff --git a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts index eb22cff826..5552864df8 100644 --- a/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/backoffice/users/users/workspace/user-workspace.context.ts @@ -4,8 +4,10 @@ import { UmbWorkspaceEntityContextInterface } from '../../../shared/components/w import { UmbEntityWorkspaceManager } from '../../../shared/components/workspace/workspace-context/entity-manager-controller'; import type { UserDetails } from '@umbraco-cms/models'; -export class UmbWorkspaceUserContext extends UmbWorkspaceContext implements UmbWorkspaceEntityContextInterface { - +export class UmbWorkspaceUserContext + extends UmbWorkspaceContext + implements UmbWorkspaceEntityContextInterface +{ #manager = new UmbEntityWorkspaceManager(this._host, 'user', UMB_USER_STORE_CONTEXT_TOKEN); public readonly data = this.#manager.state.asObservable(); @@ -15,7 +17,7 @@ export class UmbWorkspaceUserContext extends UmbWorkspaceContext implements UmbW update = this.#manager.state.update; setName(name: string) { - this.#manager.state.update({name: name}) + this.#manager.state.update({ name: name }); } getEntityType = this.#manager.getEntityType; getUnique = this.#manager.getEntityKey; diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts index f693df0fee..c795fafd57 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/data/languages.data.ts @@ -1,14 +1,14 @@ -import { UmbLanguageStoreItemType } from '../../../backoffice/settings/languages/language.store'; import { UmbData } from './data'; +import { LanguageModel } from '@umbraco-cms/backend-api'; // Temp mocked database -class UmbLanguagesData extends UmbData { - constructor(data: UmbLanguageStoreItemType[]) { +class UmbLanguagesData extends UmbData { + constructor(data: LanguageModel[]) { super(data); } // skip can be number or null - getAll(skip = 0, take = this.data.length): Array { + getAll(skip = 0, take = this.data.length): Array { return this.data.slice(skip, take); } @@ -16,7 +16,17 @@ class UmbLanguagesData extends UmbData { return this.data.find((item) => item.isoCode === key); } - save(saveItems: Array) { + insert(language: LanguageModel) { + const foundIndex = this.data.findIndex((item) => item.isoCode === language.isoCode); + + if (foundIndex !== -1) { + throw new Error('Language with same iso code already exists'); + } + + this.data.push(language); + } + + save(saveItems: Array) { saveItems.forEach((saveItem) => { const foundIndex = this.data.findIndex((item) => item.isoCode === saveItem.isoCode); if (foundIndex !== -1) { @@ -50,7 +60,7 @@ class UmbLanguagesData extends UmbData { return keys; } - updateData(updateItem: UmbLanguageStoreItemType) { + updateData(updateItem: LanguageModel) { const itemIndex = this.data.findIndex((item) => item.isoCode === updateItem.isoCode); const item = this.data[itemIndex]; if (!item) return; @@ -81,7 +91,7 @@ class UmbLanguagesData extends UmbData { } } -export const MockData: Array = [ +export const MockData: Array = [ { name: 'English', isoCode: 'en', diff --git a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts index 0744df0db5..153894b174 100644 --- a/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/core/mocks/domains/language.handlers.ts @@ -1,7 +1,6 @@ import { rest } from 'msw'; -import { v4 as uuidv4 } from 'uuid'; import { umbLanguagesData } from '../data/languages.data'; -import { LanguageModel } from '@umbraco-cms/backend-api'; +import { LanguageModel, ProblemDetailsModel } from '@umbraco-cms/backend-api'; import { umbracoPath } from '@umbraco-cms/utils'; // TODO: add schema @@ -36,12 +35,22 @@ export const handlers = [ if (!data) return; - data.id = umbLanguagesData.getAll().length + 1; - data.key = uuidv4(); - - const saved = umbLanguagesData.save([data]); - - return res(ctx.status(200), ctx.json(saved[0])); + try { + umbLanguagesData.insert(data); + return res(ctx.status(201)); + } catch (error) { + return res( + ctx.status(400), + ctx.json({ + status: 400, + type: 'validation', + detail: 'Something went wrong', + errors: { + isoCode: ['Language with same iso code already exists'], + }, + }) + ); + } }), rest.put(umbracoPath('/language/:key'), async (req, res, ctx) => { @@ -49,9 +58,9 @@ export const handlers = [ if (!data) return; - const saved = umbLanguagesData.save([data]); + umbLanguagesData.save([data]); - return res(ctx.status(200), ctx.json(saved[0])); + return res(ctx.status(200)); }), rest.delete(umbracoPath('/language/:key'), async (req, res, ctx) => { diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts index a8ad224698..7c84bd0a43 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/modal-layout-picker-base.ts @@ -1,50 +1,53 @@ -import { state } from 'lit/decorators.js'; +import { property } from 'lit/decorators.js'; import { UmbModalLayoutElement } from '..'; -export interface UmbPickerData { +export interface UmbPickerModalData { multiple: boolean; - selection: Array; + selection: Array; + filter?: (language: T) => boolean; } -export class UmbModalLayoutPickerBase extends UmbModalLayoutElement> { - @state() - private _selection: Array = []; +// TODO: we should consider moving this into a class/context instead of an element. +// So we don't have to extend an element to get basic picker/selection logic +export class UmbModalLayoutPickerBase extends UmbModalLayoutElement> { + @property() + selection: Array = []; connectedCallback(): void { super.connectedCallback(); - this._selection = this.data?.selection || []; + this.selection = this.data?.selection || []; } - protected _submit() { - this.modalHandler?.close({ selection: this._selection }); + submit() { + this.modalHandler?.close({ selection: this.selection }); } - protected _close() { + close() { this.modalHandler?.close(); } - protected _handleKeydown(e: KeyboardEvent, key: selectType) { + protected _handleKeydown(e: KeyboardEvent, key: string) { if (e.key === 'Enter') { - this._handleItemClick(key); + this.handleSelection(key); } } /* TODO: Write test for this select/deselect method. */ - protected _handleItemClick(key: selectType) { + handleSelection(key: string) { if (this.data?.multiple) { - if (this._isSelected(key)) { - this._selection = this._selection.filter((selectedKey) => selectedKey !== key); + if (this.isSelected(key)) { + this.selection = this.selection.filter((selectedKey) => selectedKey !== key); } else { - this._selection.push(key); + this.selection.push(key); } } else { - this._selection = [key]; + this.selection = [key]; } this.requestUpdate('_selection'); } - protected _isSelected(key: selectType): boolean { - return this._selection.includes(key); + isSelected(key: string): boolean { + return this.selection.includes(key); } } diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts index 21b0608262..ff0470d6c7 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-section/picker-layout-section.element.ts @@ -6,7 +6,7 @@ import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api'; import type { ManifestSection } from '@umbraco-cms/models'; @customElement('umb-picker-layout-section') -export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase { +export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase { static styles = [ UUITextStyles, css` @@ -73,9 +73,9 @@ export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase { ${this._sections.map( (item) => html`
this._handleItemClick(item.alias)} + @click=${() => this.handleSelection(item.alias)} @keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.alias)} - class=${this._isSelected(item.alias) ? 'item selected' : 'item'}> + class=${this.isSelected(item.alias) ? 'item selected' : 'item'}> ${item.meta.label}
` @@ -83,8 +83,8 @@ export class UmbPickerLayoutSectionElement extends UmbModalLayoutPickerBase {
- - + +
`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts index d05474cf87..9d103d4d19 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user-group/picker-layout-user-group.element.ts @@ -7,7 +7,7 @@ import type { UmbUserGroupStore } from '../../../../backoffice/users/user-groups import type { UserGroupDetails } from '@umbraco-cms/models'; @customElement('umb-picker-layout-user-group') -export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase { +export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase { static styles = [ UUITextStyles, css` @@ -82,9 +82,9 @@ export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase { ${this._userGroups.map( (item) => html`
this._handleItemClick(item.key)} + @click=${() => this.handleSelection(item.key)} @keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.key)} - class=${this._isSelected(item.key) ? 'item selected' : 'item'}> + class=${this.isSelected(item.key) ? 'item selected' : 'item'}> ${item.name}
@@ -93,8 +93,8 @@ export class UmbPickerLayoutUserGroupElement extends UmbModalLayoutPickerBase {
- - + +
`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts index 452d26ca4f..b2c0ddbb56 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/layouts/picker-user/picker-layout-user.element.ts @@ -6,7 +6,7 @@ import { UmbUserStore, UMB_USER_STORE_CONTEXT_TOKEN } from '../../../../backoffi import type { UserDetails } from '@umbraco-cms/models'; @customElement('umb-picker-layout-user') -export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase { +export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase { static styles = [ UUITextStyles, css` @@ -86,9 +86,9 @@ export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase { ${this._users.map( (item) => html`
this._handleItemClick(item.key)} + @click=${() => this.handleSelection(item.key)} @keydown=${(e: KeyboardEvent) => this._handleKeydown(e, item.key)} - class=${this._isSelected(item.key) ? 'item selected' : 'item'}> + class=${this.isSelected(item.key) ? 'item selected' : 'item'}> ${item.name}
@@ -97,8 +97,8 @@ export class UmbPickerLayoutUserElement extends UmbModalLayoutPickerBase {
- - + +
`; diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts index f19a149ac7..4eb2e8ea60 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts @@ -5,6 +5,7 @@ import './layouts/media-picker/modal-layout-media-picker.element'; import './layouts/property-editor-ui-picker/modal-layout-property-editor-ui-picker.element'; import './layouts/modal-layout-current-user.element'; import './layouts/icon-picker/modal-layout-icon-picker.element'; +import '../../backoffice/settings/languages/language-picker/language-picker-modal-layout.element'; import './layouts/link-picker/modal-layout-link-picker.element'; import './layouts/basic/modal-layout-basic.element'; @@ -18,8 +19,10 @@ import type { UmbModalPropertyEditorUIPickerData } from './layouts/property-edit import type { UmbModalMediaPickerData } from './layouts/media-picker/modal-layout-media-picker.element'; import type { UmbModalLinkPickerData } from './layouts/link-picker/modal-layout-link-picker.element'; import { UmbModalHandler } from './modal-handler'; +import type { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element'; import { UmbContextToken } from '@umbraco-cms/context-api'; -import { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element'; +import { UmbPickerModalData } from './layouts/modal-layout-picker-base'; +import { LanguageModel } from '@umbraco-cms/backend-api'; export type UmbModalType = 'dialog' | 'sidebar'; @@ -29,7 +32,9 @@ export interface UmbModalOptions { data?: UmbModalData; } -// TODO: Should this be called UmbModalContext ? as we don't have 'services' as a term. +// TODO: rename to UmbModalContext +// TODO: we should find a way to easily open a modal without adding custom methods to this context. It would result in a better separation of concerns. +// TODO: move all layouts into their correct "silo" folders. User picker should live with users etc. export class UmbModalService { // TODO: Investigate if we can get rid of HTML elements in our store, so we can use one of our states. #modals = new BehaviorSubject(>[]); @@ -125,6 +130,16 @@ export class UmbModalService { return this.open('umb-modal-layout-change-password', { data, type: 'dialog' }); } + /** + * Opens a language picker sidebar modal + * @public + * @return {*} {UmbModalHandler} + * @memberof UmbModalService + */ + public languagePicker(data: UmbPickerModalData): UmbModalHandler { + return this.open('umb-language-picker-modal-layout', { data, type: 'sidebar' }); + } + /** * Opens a basic sidebar modal to display readonly information * @public From 4d4c30b5f95e6111eeb88798e21f759bf2dc5563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Feb 2023 09:21:14 +0100 Subject: [PATCH 17/18] fix imports --- src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts index ff519573a8..86b768f440 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts @@ -22,7 +22,9 @@ import type { UmbModalMediaPickerData } from './layouts/media-picker/modal-layou import type { UmbModalLinkPickerData } from './layouts/link-picker/modal-layout-link-picker.element'; import { UmbModalHandler } from './modal-handler'; import type { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element'; +import { UmbPickerModalData } from './layouts/modal-layout-picker-base'; import { UmbContextToken } from '@umbraco-cms/context-api'; +import { LanguageModel } from '@umbraco-cms/backend-api'; export type UmbModalType = 'dialog' | 'sidebar'; From b9f8805a405da992ed2f7eddec6c67bb102d4a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesper=20M=C3=B8ller=20Jensen?= <26099018+JesmoDev@users.noreply.github.com> Date: Thu, 23 Feb 2023 21:25:12 +1300 Subject: [PATCH 18/18] import fix --- src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts index ff519573a8..86b768f440 100644 --- a/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts +++ b/src/Umbraco.Web.UI.Client/src/core/modal/modal.service.ts @@ -22,7 +22,9 @@ import type { UmbModalMediaPickerData } from './layouts/media-picker/modal-layou import type { UmbModalLinkPickerData } from './layouts/link-picker/modal-layout-link-picker.element'; import { UmbModalHandler } from './modal-handler'; import type { UmbBasicModalData } from './layouts/basic/modal-layout-basic.element'; +import { UmbPickerModalData } from './layouts/modal-layout-picker-base'; import { UmbContextToken } from '@umbraco-cms/context-api'; +import { LanguageModel } from '@umbraco-cms/backend-api'; export type UmbModalType = 'dialog' | 'sidebar';