diff --git a/src/Umbraco.Web.UI.Client/.npmrc b/src/Umbraco.Web.UI.Client/.npmrc index d4f5d5ff86..521a9f7c07 100644 --- a/src/Umbraco.Web.UI.Client/.npmrc +++ b/src/Umbraco.Web.UI.Client/.npmrc @@ -1,2 +1 @@ -@umbraco-cms:registry=https://registry.npmjs.org legacy-peer-deps=true diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 2db0728f07..c607ab88d9 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -97,7 +97,6 @@ "backoffice:test:e2e": "npx playwright test", "build-storybook": "npm run wc-analyze && storybook build", "build:for:cms": "npm run build && node ./devops/build/copy-to-cms.js", - "build:for:npm": "npm run build && tsc-alias -f -p src/tsconfig.build.json && npm run generate:jsonschema:dist && npm run wc-analyze && npm run wc-analyze:vscode", "build:for:static": "vite build", "build:vite": "tsc && vite build --mode staging", "build": "tsc --project ./src/tsconfig.build.json && rollup -c ./src/rollup.config.js", @@ -116,7 +115,7 @@ "lint:fix": "npm run lint -- --fix", "lint": "eslint src", "new-extension": "plop --plopfile ./devops/plop/plop.js", - "prepublishOnly": "node ./devops/publish/cleanse-pkg.js", + "prepack": "tsc-alias -f -p src/tsconfig.build.json && npm run generate:jsonschema:dist && npm run wc-analyze && npm run wc-analyze:vscode && node ./devops/publish/cleanse-pkg.js", "preview": "vite preview --open", "storybook:build": "npm run wc-analyze && storybook build", "storybook": "npm run wc-analyze && storybook dev -p 6006", diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts index 04dc698593..c2c7c77ce8 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts @@ -70,14 +70,18 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement { static styles: CSSResultGroup = [ css` + :host { + display: contents; + } #tabs { - color: var(--uui-color-header-contrast); height: 60px; + flex-basis: 100%; font-size: 16px; --uui-tab-text: var(--uui-color-header-contrast); --uui-tab-text-hover: var(--uui-color-header-contrast-emphasis); --uui-tab-text-active: var(--uui-color-header-contrast-emphasis); - --uui-tab-background: var(--uui-color-header-background); + background-color: var(--uui-color-header-background); + --uui-tab-group-dropdown-background: var(--uui-color-header-surface); } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 33f798dfed..19de39092d 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -1782,8 +1782,8 @@ export default { inviteAnotherUser: 'Invite another user', inviteUserHelp: 'Invite new users to give them access to Umbraco. An invite email will be sent to the\n user with information on how to log in to Umbraco. Invites last for 72 hours.\n ', - language: 'Language', - languageHelp: 'Set the language you will see in menus and dialogs', + language: 'UI Culture', + languageHelp: 'Set the culture you will see in menus and dialogs', lastLockoutDate: 'Last lockout date', lastLogin: 'Last login', lastPasswordChangeDate: 'Password last changed', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/themes/themes/dark.theme.css b/src/Umbraco.Web.UI.Client/src/css/dark.theme.css similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/themes/themes/dark.theme.css rename to src/Umbraco.Web.UI.Client/src/css/dark.theme.css diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/themes/themes/high-contrast.theme.css b/src/Umbraco.Web.UI.Client/src/css/high-contrast.theme.css similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/themes/themes/high-contrast.theme.css rename to src/Umbraco.Web.UI.Client/src/css/high-contrast.theme.css diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-api.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-api.function.ts index ed9db89316..f153cf1e22 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-api.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-api.function.ts @@ -1,31 +1,44 @@ -import type { UmbApi } from "../models/api.interface.js"; -import type { ApiLoaderExports, ApiLoaderProperty, ClassConstructor, ElementAndApiLoaderProperty, ElementLoaderExports } from "../types/utils.js"; +import type { UmbApi } from '../models/api.interface.js'; +import type { + ApiLoaderExports, + ApiLoaderProperty, + ClassConstructor, + ElementAndApiLoaderProperty, + ElementLoaderExports, +} from '../types/utils.js'; -export async function loadManifestApi(property: ApiLoaderProperty | ElementAndApiLoaderProperty): Promise | undefined> { - const propType = typeof property - if(propType === 'function') { - if((property as ClassConstructor).prototype) { +export async function loadManifestApi( + property: ApiLoaderProperty | ElementAndApiLoaderProperty, +): Promise | undefined> { + const propType = typeof property; + if (propType === 'function') { + if ((property as ClassConstructor).prototype) { // Class Constructor return property as ClassConstructor; } else { // Promise function - const result = await (property as (Exclude, string>, ClassConstructor>))(); - if(typeof result === 'object' && result != null) { - const exportValue = 'api' in result ? result.api : undefined || 'default' in result ? (result as Exclude<(typeof result), ElementLoaderExports>).default : undefined; - if(exportValue && typeof exportValue === 'function') { + const result = await ( + property as Exclude, string>, ClassConstructor> + )(); + if (typeof result === 'object' && result != null) { + const exportValue = + ('api' in result ? result.api : undefined) || + ('default' in result ? (result as Exclude).default : undefined); + if (exportValue && typeof exportValue === 'function') { return exportValue; } } } - } else if(propType === 'string') { + } else if (propType === 'string') { // Import string const result = await (import(/* @vite-ignore */ property as string) as unknown as ApiLoaderExports); - if(typeof result === 'object' && result != null) { - const exportValue = 'api' in result ? result.api : undefined || 'default' in result ? result.default : undefined; - if(exportValue && typeof exportValue === 'function') { + if (result && typeof result === 'object') { + const exportValue = + ('api' in result ? result.api : undefined) || ('default' in result ? result.default : undefined); + if (exportValue && typeof exportValue === 'function') { return exportValue; } } } return undefined; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-element.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-element.function.ts index b1fcd616d8..89699f7fe0 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-element.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-element.function.ts @@ -1,28 +1,40 @@ -import type { ApiLoaderExports, ClassConstructor, ElementAndApiLoaderProperty, ElementLoaderExports, ElementLoaderProperty } from "../types/utils.js"; +import type { + ApiLoaderExports, + ClassConstructor, + ElementAndApiLoaderProperty, + ElementLoaderExports, + ElementLoaderProperty, +} from '../types/utils.js'; - -export async function loadManifestElement(property: ElementLoaderProperty | ElementAndApiLoaderProperty): Promise | undefined> { - const propType = typeof property - if(propType === 'function') { - if((property as ClassConstructor).prototype) { +export async function loadManifestElement( + property: ElementLoaderProperty | ElementAndApiLoaderProperty, +): Promise | undefined> { + const propType = typeof property; + if (propType === 'function') { + if ((property as ClassConstructor).prototype) { // Class Constructor return property as ClassConstructor; } else { // Promise function - const result = await (property as (Exclude, ClassConstructor>))(); - if(typeof result === 'object' && result !== null) { - const exportValue = 'element' in result ? result.element : undefined || 'default' in result ? (result as Exclude<(typeof result), ApiLoaderExports>).default : undefined; - if(exportValue && typeof exportValue === 'function') { + const result = await (property as Exclude, ClassConstructor>)(); + if (typeof result === 'object' && result !== null) { + const exportValue = + ('element' in result ? result.element : undefined) || + ('default' in result ? (result as Exclude).default : undefined); + if (exportValue && typeof exportValue === 'function') { return exportValue; } } } - } else if(propType === 'string') { + } else if (propType === 'string') { // Import string - const result = await (import(/* @vite-ignore */ property as string) as unknown as ElementLoaderExports); - if(typeof result === 'object' && result != null) { - const exportValue = 'element' in result ? result.element : undefined || 'default' in result ? result.default : undefined; - if(exportValue && typeof exportValue === 'function') { + const result = await (import( + /* @vite-ignore */ property as string + ) as unknown as ElementLoaderExports); + if (result && typeof result === 'object') { + const exportValue = + ('element' in result ? result.element : undefined) || ('default' in result ? result.default : undefined); + if (exportValue && typeof exportValue === 'function') { return exportValue; } } diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-css.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-css.function.ts index 01006b2350..861b220ba0 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-css.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-css.function.ts @@ -1,18 +1,29 @@ -import type { JsLoaderProperty } from "../types/utils.js"; +import type { CssLoaderExports, CssLoaderProperty } from '../types/utils.js'; -export async function loadManifestPlainCss(property: JsLoaderProperty): Promise { +export async function loadManifestPlainCss( + property: CssLoaderProperty, +): Promise { const propType = typeof property; - if(propType === 'function') { - const result = await (property as (Exclude<(typeof property), string>))(); - if(result != null) { - return result; + if (propType === 'function') { + // Promise function + const result = await (property as Exclude)(); + if (typeof result === 'object' && result != null) { + const exportValue = + ('css' in result ? result.css : undefined) || ('default' in result ? result.default : undefined); + if (exportValue && typeof exportValue === 'string') { + return exportValue as CssType; + } } - } else if(propType === 'string') { + } else if (propType === 'string') { // Import string - const result = await (import(/* @vite-ignore */ property as string) as unknown as CssType); - if(result != null) { - return result; + const result = await (import(/* @vite-ignore */ property as string) as unknown as CssLoaderExports); + if (typeof result === 'object' && result != null) { + const exportValue = + ('css' in result ? result.css : undefined) || ('default' in result ? result.default : undefined); + if (exportValue && typeof exportValue === 'string') { + return exportValue; + } } } return undefined; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-js.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-js.function.ts index 0611d7eb25..343a5fb952 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-js.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/load-manifest-plain-js.function.ts @@ -1,18 +1,21 @@ -import type { JsLoaderProperty } from "../types/utils.js"; +import type { JsLoaderProperty } from '../types/utils.js'; -export async function loadManifestPlainJs(property: JsLoaderProperty): Promise { +export async function loadManifestPlainJs( + property: JsLoaderProperty, +): Promise { const propType = typeof property; - if(propType === 'function') { - const result = await (property as (Exclude<(typeof property), string>))(); - if(typeof result === 'object' && result != null) { + if (propType === 'function') { + // Promise function + const result = await (property as Exclude)(); + if (typeof result === 'object' && result != null) { return result; } - } else if(propType === 'string') { + } else if (propType === 'string') { // Import string const result = await (import(/* @vite-ignore */ property as string) as unknown as JsType); - if(typeof result === 'object' && result != null) { + if (typeof result === 'object' && result != null) { return result; } } return undefined; -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/base.types.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/base.types.ts index e17bc203fc..017f59aea0 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/base.types.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/base.types.ts @@ -26,12 +26,12 @@ export interface ManifestElementWithElementName extends ManifestElement { elementName: string; } -export interface ManifestPlainCss extends ManifestBase { +export interface ManifestPlainCss extends ManifestBase { /** * The file location of the stylesheet file to load * @TJS-type string */ - css?: CssLoaderProperty; + css?: CssLoaderProperty; } export interface ManifestPlainJs extends ManifestBase { diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/utils.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/utils.ts index 9cf7996dd3..bf6ad39b25 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/utils.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/types/utils.ts @@ -1,5 +1,4 @@ -import type { UmbApi } from "../models/index.js"; - +import type { UmbApi } from '../models/index.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type HTMLElementConstructor = new (...args: any[]) => T; @@ -7,80 +6,60 @@ export type HTMLElementConstructor = new (...args: any[]) => T; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type ClassConstructor = new (...args: any[]) => T; - - // Module Export Types: -export type ElementLoaderExports< - ElementType extends HTMLElement = HTMLElement -> = ({default: ClassConstructor} | {element: ClassConstructor})// | Omit, 'default'> +export type CssLoaderExports = { default: CssType } | { css: CssType }; -export type ApiLoaderExports< - ApiType extends UmbApi = UmbApi -> = ({default: ClassConstructor} | {api: ClassConstructor})//| Omit, 'default'> +export type ElementLoaderExports = + | { default: ClassConstructor } + | { element: ClassConstructor }; // | Omit, 'default'> + +export type ApiLoaderExports = + | { default: ClassConstructor } + | { api: ClassConstructor }; //| Omit, 'default'> export type ElementAndApiLoaderExports< ElementType extends HTMLElement = HTMLElement, - ApiType extends UmbApi = UmbApi -> = ({api: ClassConstructor} | {element: ClassConstructor} | {api: ClassConstructor, element: ClassConstructor})// | Omit, 'api'>, 'default'> - + ApiType extends UmbApi = UmbApi, +> = + | { api: ClassConstructor } + | { element: ClassConstructor } + | { api: ClassConstructor; element: ClassConstructor }; // | Omit, 'api'>, 'default'> // Promise Types: -export type CssLoaderPromise< - CssType = unknown -> = (() => Promise) +export type CssLoaderPromise = () => Promise>; -export type JsLoaderPromise< - JsType -> = (() => Promise) +export type JsLoaderPromise = () => Promise; -export type ElementLoaderPromise< - ElementType extends HTMLElement = HTMLElement -> = (() => Promise>) +export type ElementLoaderPromise = () => Promise< + ElementLoaderExports +>; -export type ApiLoaderPromise< - ApiType extends UmbApi = UmbApi -> = (() => Promise>) +export type ApiLoaderPromise = () => Promise>; export type ElementAndApiLoaderPromise< ElementType extends HTMLElement = HTMLElement, - ApiType extends UmbApi = UmbApi -> = (() => Promise>) - + ApiType extends UmbApi = UmbApi, +> = () => Promise>; // Property Types: -export type CssLoaderProperty = ( - string - | - CssLoaderPromise -); -export type JsLoaderProperty = ( - string - | - JsLoaderPromise -); -export type ElementLoaderProperty = ( - string - | - ElementLoaderPromise - | - ClassConstructor -); -export type ApiLoaderProperty = ( - string - | - ApiLoaderPromise - | - ClassConstructor -); -export type ElementAndApiLoaderProperty = ( - string - | - ElementAndApiLoaderPromise - | - ElementLoaderPromise - | - ApiLoaderPromise -); \ No newline at end of file +export type CssLoaderProperty = string | CssLoaderPromise; +export type JsLoaderProperty = string | JsLoaderPromise; +export type ElementLoaderProperty = + | string + | ElementLoaderPromise + | ClassConstructor; +export type ApiLoaderProperty = + | string + | ApiLoaderPromise + | ClassConstructor; +export type ElementAndApiLoaderProperty< + ElementType extends HTMLElement = HTMLElement, + ApiType extends UmbApi = UmbApi, +> = + | string + | ElementAndApiLoaderPromise + | ElementLoaderPromise + | ApiLoaderPromise; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts index c10f52fb45..56a7ba6cb7 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user/user.data.ts @@ -8,7 +8,7 @@ export const data: Array = [ mediaStartNodeIds: [], name: 'Umbraco User', email: 'noreply@umbraco.com', - languageIsoCode: 'en-US', + languageIsoCode: 'en-us', state: UserStateModel.ACTIVE, lastLoginDate: '9/10/2022', lastLockoutDate: '11/23/2021', @@ -25,7 +25,7 @@ export const data: Array = [ mediaStartNodeIds: ['f2f81a40-c989-4b6b-84e2-057cecd3adc1'], name: 'Amelie Walker', email: 'awalker1@domain.com', - languageIsoCode: 'Japanese', + languageIsoCode: 'da-dk', state: UserStateModel.INACTIVE, lastLoginDate: '2023-10-12T18:30:32.879Z', lastLockoutDate: null, @@ -42,7 +42,7 @@ export const data: Array = [ mediaStartNodeIds: [], name: 'Oliver Kim', email: 'okim1@domain.com', - languageIsoCode: 'Russian', + languageIsoCode: 'da-dk', state: UserStateModel.ACTIVE, lastLoginDate: '2023-10-12T18:30:32.879Z', lastLockoutDate: null, @@ -59,7 +59,7 @@ export const data: Array = [ mediaStartNodeIds: [], name: 'Eliana Nieves', email: 'enieves1@domain.com', - languageIsoCode: 'Spanish', + languageIsoCode: 'en-us', state: UserStateModel.INVITED, lastLoginDate: '2023-10-12T18:30:32.879Z', lastLockoutDate: null, @@ -76,7 +76,7 @@ export const data: Array = [ mediaStartNodeIds: [], name: 'Jasmine Patel', email: 'jpatel1@domain.com', - languageIsoCode: 'Hindi', + languageIsoCode: 'en-us', state: UserStateModel.LOCKED_OUT, lastLoginDate: '2023-10-12T18:30:32.879Z', lastLockoutDate: '2023-10-12T18:30:32.879Z', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/body-layout/body-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/body-layout/body-layout.element.ts index f6dc63079c..00da0fd877 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/body-layout/body-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/body-layout/body-layout.element.ts @@ -1,4 +1,4 @@ -import { UmbTextStyles } from "@umbraco-cms/backoffice/style"; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, @@ -191,6 +191,7 @@ export class UmbBodyLayoutElement extends LitElement { height: 100%; align-items: center; box-sizing: border-box; + min-width: 0; } #navigation-slot, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/theme.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/theme.model.ts index 37cd90d2a9..2b003bc712 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/theme.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/theme.model.ts @@ -2,6 +2,6 @@ import type { ManifestPlainCss } from '@umbraco-cms/backoffice/extension-api'; /** * Theme manifest for styling the backoffice of Umbraco such as dark, high contrast etc */ -export interface ManifestTheme extends ManifestPlainCss { +export interface ManifestTheme extends ManifestPlainCss { type: 'theme'; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/components/index.ts new file mode 100644 index 0000000000..f95c0295fc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/components/index.ts @@ -0,0 +1,3 @@ +import './ui-culture-input/ui-culture-input.element.js'; + +export { UmbUiCultureInputElement } from './ui-culture-input/ui-culture-input.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/components/ui-culture-input/ui-culture-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/components/ui-culture-input/ui-culture-input.element.ts new file mode 100644 index 0000000000..03d8f5795a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/components/ui-culture-input/ui-culture-input.element.ts @@ -0,0 +1,95 @@ +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { css, html, customElement, query, state, property } from '@umbraco-cms/backoffice/external/lit'; +import { FormControlMixin, UUIComboboxElement, UUIComboboxEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; +import { ManifestLocalization, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; + +interface UmbCultureInputOption { + name: string; + value: string; +} + +@customElement('umb-ui-culture-input') +export class UmbUiCultureInputElement extends FormControlMixin(UmbLitElement) { + @state() + private _options: Array = []; + + @query('uui-combobox') + private _selectElement!: HTMLInputElement; + + @property({ type: String }) + get value() { + return this._value; + } + set value(value: FormDataEntryValue | FormData) { + if (typeof value === 'string') { + const oldValue = this._value; + this._value = value.toLowerCase(); + this.requestUpdate('value', oldValue); + } + } + + constructor() { + super(); + this.#observeTranslations(); + } + + #observeTranslations() { + this.observe( + umbExtensionsRegistry.extensionsOfType('localization'), + (localizationManifests) => { + this.#mapToOptions(localizationManifests); + }, + 'umbObserveLocalizationManifests', + ); + } + + #mapToOptions(manifests: Array) { + this._options = manifests + .filter((isoCode) => isoCode !== undefined) + .map((manifest) => ({ + name: manifest.name, + value: manifest.meta.culture.toLowerCase(), + })); + } + + protected getFormElement() { + return this._selectElement; + } + + #onChange(event: UUIComboboxEvent) { + event.stopPropagation(); + const target = event.target as UUIComboboxElement; + + if (typeof target?.value === 'string') { + this.value = target.value; + this.dispatchEvent(new UmbChangeEvent()); + } + } + + render() { + return html` + + ${this._options.map( + (option) => html`${option.name}`, + )} + + `; + } + + static styles = [ + css` + :host { + display: block; + } + `, + ]; +} + +export default UmbUiCultureInputElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-ui-culture-input': UmbUiCultureInputElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/index.ts index a87b2663fd..d6ca82fa14 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/index.ts @@ -4,3 +4,4 @@ import './localize-number.element.js'; import './localize-relative-time.element.js'; export * from './registry/localization.registry.js'; +export { UmbUiCultureInputElement } from './components/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts index c1ceca23f3..1fe1004f6e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main-views/section-main-views.element.ts @@ -102,7 +102,8 @@ export class UmbSectionMainViewElement extends UmbLitElement { } #renderDashboards() { - return this._dashboards.length > 0 + // Only show dashboards if there are more than one dashboard or if there are both dashboards and views + return (this._dashboards.length > 0 && this._views.length > 0) || this._dashboards.length > 1 ? html` ${this._dashboards.map((dashboard) => { @@ -121,7 +122,8 @@ export class UmbSectionMainViewElement extends UmbLitElement { } #renderViews() { - return this._views.length > 0 + // Only show views if there are more than one view or if there are both dashboards and views + return (this._views.length > 0 && this._dashboards.length > 0) || this._views.length > 1 ? html` ${this._views.map((view) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main/section-main.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main/section-main.element.ts index 642e0f5eac..07daf6531a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main/section-main.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-main/section-main.element.ts @@ -1,4 +1,4 @@ -import { UmbTextStyles } from "@umbraco-cms/backoffice/style"; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, LitElement, customElement } from '@umbraco-cms/backoffice/external/lit'; @customElement('umb-section-main') @@ -17,6 +17,7 @@ export class UmbSectionMainElement extends LitElement { :host { flex: 1 1 auto; height: 100%; + min-width: 0; } main { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/themes/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/themes/manifests.ts index 8754ac2767..426b20ba14 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/themes/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/themes/manifests.ts @@ -17,14 +17,14 @@ export const themes: Array = [ type: 'theme', alias: 'umb-dark-theme', name: 'Dark', - css: 'src/packages/settings/themes/themes/dark.theme.css', + css: '/umbraco/backoffice/css/dark.theme.css', weight: 200, }, { type: 'theme', alias: 'umb-high-contrast-theme', name: 'High contrast', - css: 'src/packages/settings/themes/themes/high-contrast.theme.css', + css: '/umbraco/backoffice/css/high-contrast.theme.css', weight: 100, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/themes/theme.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/themes/theme.context.ts index 333afce863..3bbbb0c81d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/themes/theme.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/themes/theme.context.ts @@ -42,14 +42,16 @@ export class UmbThemeContext extends UmbBaseController { if (themes.length > 0 && themes[0].css) { const activeTheme = themes[0]; if (typeof activeTheme.css === 'function') { - const styleEl = (this.#styleElement = document.createElement('style')); - styleEl.setAttribute('type', 'text/css'); - document.head.appendChild(styleEl); + this.#styleElement = document.createElement('style') as HTMLStyleElement; + // We store the current style element so we can check if it has been replaced by another theme in between. + const currentStyleEl = this.#styleElement; + currentStyleEl.setAttribute('type', 'text/css'); const result = await loadManifestPlainCss(activeTheme.css); // Checking that this is still our styleElement, it has not been replaced with another theme in between. - if (result && styleEl === this.#styleElement) { - styleEl.appendChild(document.createTextNode(result)); + if (result && currentStyleEl === this.#styleElement) { + currentStyleEl.appendChild(document.createTextNode(result)); + document.head.appendChild(currentStyleEl); } } else if (typeof activeTheme.css === 'string') { this.#styleElement = document.createElement('link'); @@ -58,16 +60,23 @@ export class UmbThemeContext extends UmbBaseController { document.head.appendChild(this.#styleElement); } } else { + console.log('remove style element', this.#styleElement); + // We could not load a theme for this alias, so we remove the theme. localStorage.removeItem(LOCAL_STORAGE_KEY); this.#styleElement?.childNodes.forEach((node) => node.remove()); this.#styleElement?.setAttribute('href', ''); + this.#styleElement = null; } }, ); } else { + // Super clean, we got a falsy value, so we remove the theme. + localStorage.removeItem(LOCAL_STORAGE_KEY); + this.#styleElement?.remove(); this.#styleElement?.childNodes.forEach((node) => node.remove()); this.#styleElement?.setAttribute('href', ''); + this.#styleElement = null; } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/components/user-workspace-profile-settings/user-workspace-profile-settings.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/components/user-workspace-profile-settings/user-workspace-profile-settings.element.ts index 75ec003003..b885012635 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/components/user-workspace-profile-settings/user-workspace-profile-settings.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/components/user-workspace-profile-settings/user-workspace-profile-settings.element.ts @@ -2,91 +2,34 @@ import { UMB_USER_WORKSPACE_CONTEXT } from '../../user-workspace.context.js'; import { html, customElement, state, ifDefined, css } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/internal/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UUISelectElement } from '@umbraco-cms/backoffice/external/uui'; import { UserResponseModel } from '@umbraco-cms/backoffice/backend-api'; -import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUser } from '@umbraco-cms/backoffice/current-user'; -import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UmbUiCultureInputElement } from '@umbraco-cms/backoffice/localization'; @customElement('umb-user-workspace-profile-settings') export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement { @state() private _user?: UserResponseModel; - @state() - private _currentUser?: UmbCurrentUser; - - @state() - private languages: Array<{ name: string; value: string; selected: boolean }> = []; - - #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; #userWorkspaceContext?: typeof UMB_USER_WORKSPACE_CONTEXT.TYPE; constructor() { super(); - this.consumeContext(UMB_CURRENT_USER_CONTEXT, (instance) => { - this.#currentUserContext = instance; - this.#observeCurrentUser(); - }); - this.consumeContext(UMB_USER_WORKSPACE_CONTEXT, (instance) => { this.#userWorkspaceContext = instance; this.observe(this.#userWorkspaceContext.data, (user) => (this._user = user), 'umbUserObserver'); }); } - #onLanguageChange(event: Event) { - const target = event.composedPath()[0] as UUISelectElement; + #onLanguageChange(event: UmbChangeEvent) { + const target = event.target as UmbUiCultureInputElement; if (typeof target?.value === 'string') { this.#userWorkspaceContext?.updateProperty('languageIsoCode', target.value); } } - #observeCurrentUser() { - if (!this.#currentUserContext) return; - this.observe( - this.#currentUserContext.currentUser, - async (currentUser) => { - this._currentUser = currentUser; - - if (!currentUser) { - return; - } - - // Find all translations and make a unique list of iso codes - const translations = await firstValueFrom(umbExtensionsRegistry.extensionsOfType('localization')); - - this.languages = translations - .filter((isoCode) => isoCode !== undefined) - .map((translation) => ({ - value: translation.meta.culture.toLowerCase(), - name: translation.name, - selected: false, - })); - - const currentUserLanguageCode = currentUser.languageIsoCode?.toLowerCase(); - - // Set the current user's language as selected - const currentUserLanguage = this.languages.find((language) => language.value === currentUserLanguageCode); - - if (currentUserLanguage) { - currentUserLanguage.selected = true; - } else { - // If users language code did not fit any of the options. We will create an option that fits, named unknown. - // In this way the user can keep their choice though a given language was not present at this time. - this.languages.push({ - value: currentUserLanguageCode ?? 'en-us', - name: currentUserLanguageCode ? `${currentUserLanguageCode} (unknown)` : 'Unknown', - selected: true, - }); - } - }, - 'umbUserObserver', - ); - } - render() { return html`
Profile
@@ -112,13 +55,12 @@ export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement { - - + label="${this.localize.term('user_language')}"> `; } diff --git a/src/Umbraco.Web.UI.Client/vite.config.ts b/src/Umbraco.Web.UI.Client/vite.config.ts index 93ecae5ddf..35028d98db 100644 --- a/src/Umbraco.Web.UI.Client/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/vite.config.ts @@ -17,6 +17,10 @@ export const plugins: PluginOption[] = [ src: 'public-assets/App_Plugins/custom-bundle-package/*.js', dest: 'App_Plugins/custom-bundle-package', }, + { + src: 'src/css/*.css', + dest: 'umbraco/backoffice/css', + }, { src: 'src/assets/*.svg', dest: 'umbraco/backoffice/assets', @@ -32,7 +36,7 @@ export const plugins: PluginOption[] = [ { src: 'node_modules/msw/lib/iife/**/*', dest: 'umbraco/backoffice/msw', - } + }, ], }), viteTSConfigPaths(),