Merge branch 'main' into feature/swap-to-popover-container

This commit is contained in:
Jesper Møller Jensen
2023-12-04 18:11:59 +13:00
24 changed files with 281 additions and 203 deletions

View File

@@ -1,2 +1 @@
@umbraco-cms:registry=https://registry.npmjs.org
legacy-peer-deps=true

View File

@@ -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",

View File

@@ -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);
}
`,
];

View File

@@ -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',

View File

@@ -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<ApiType extends UmbApi>(property: ApiLoaderProperty<ApiType> | ElementAndApiLoaderProperty<any, ApiType>): Promise<ClassConstructor<ApiType> | undefined> {
const propType = typeof property
if(propType === 'function') {
if((property as ClassConstructor).prototype) {
export async function loadManifestApi<ApiType extends UmbApi>(
property: ApiLoaderProperty<ApiType> | ElementAndApiLoaderProperty<any, ApiType>,
): Promise<ClassConstructor<ApiType> | undefined> {
const propType = typeof property;
if (propType === 'function') {
if ((property as ClassConstructor).prototype) {
// Class Constructor
return property as ClassConstructor<ApiType>;
} else {
// Promise function
const result = await (property as (Exclude<Exclude<ApiLoaderProperty<ApiType>, string>, ClassConstructor<ApiType>>))();
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<Exclude<ApiLoaderProperty<ApiType>, string>, ClassConstructor<ApiType>>
)();
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') {
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<ApiType>);
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;
}
}

View File

@@ -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<ElementType extends HTMLElement>(property: ElementLoaderProperty<ElementType> | ElementAndApiLoaderProperty<ElementType>): Promise<ClassConstructor<ElementType> | undefined> {
const propType = typeof property
if(propType === 'function') {
if((property as ClassConstructor).prototype) {
export async function loadManifestElement<ElementType extends HTMLElement>(
property: ElementLoaderProperty<ElementType> | ElementAndApiLoaderProperty<ElementType>,
): Promise<ClassConstructor<ElementType> | undefined> {
const propType = typeof property;
if (propType === 'function') {
if ((property as ClassConstructor).prototype) {
// Class Constructor
return property as ClassConstructor<ElementType>;
} else {
// Promise function
const result = await (property as (Exclude<Exclude<typeof property, string>, ClassConstructor<ElementType>>))();
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<Exclude<typeof property, string>, ClassConstructor<ElementType>>)();
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') {
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<ElementType>);
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<ElementType>);
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;
}
}

View File

@@ -1,18 +1,29 @@
import type { JsLoaderProperty } from "../types/utils.js";
import type { CssLoaderExports, CssLoaderProperty } from '../types/utils.js';
export async function loadManifestPlainCss<CssType = string>(property: JsLoaderProperty<CssType>): Promise<CssType | undefined> {
export async function loadManifestPlainCss<CssType extends string>(
property: CssLoaderProperty<CssType>,
): Promise<CssType | undefined> {
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<typeof property, string>)();
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<CssType>);
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;
}
}

View File

@@ -1,18 +1,21 @@
import type { JsLoaderProperty } from "../types/utils.js";
import type { JsLoaderProperty } from '../types/utils.js';
export async function loadManifestPlainJs<JsType extends object>(property: JsLoaderProperty<JsType>): Promise<JsType | undefined> {
export async function loadManifestPlainJs<JsType extends object>(
property: JsLoaderProperty<JsType>,
): Promise<JsType | undefined> {
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<typeof property, string>)();
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;
}
}

View File

@@ -26,12 +26,12 @@ export interface ManifestElementWithElementName extends ManifestElement {
elementName: string;
}
export interface ManifestPlainCss<CssType = unknown> extends ManifestBase {
export interface ManifestPlainCss extends ManifestBase {
/**
* The file location of the stylesheet file to load
* @TJS-type string
*/
css?: CssLoaderProperty<CssType>;
css?: CssLoaderProperty;
}
export interface ManifestPlainJs<JsType> extends ManifestBase {

View File

@@ -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<T = HTMLElement> = new (...args: any[]) => T;
@@ -7,80 +6,60 @@ export type HTMLElementConstructor<T = HTMLElement> = new (...args: any[]) => T;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ClassConstructor<T = object> = new (...args: any[]) => T;
// Module Export Types:
export type ElementLoaderExports<
ElementType extends HTMLElement = HTMLElement
> = ({default: ClassConstructor<ElementType>} | {element: ClassConstructor<ElementType>})// | Omit<Omit<object, 'element'>, 'default'>
export type CssLoaderExports<CssType extends string = string> = { default: CssType } | { css: CssType };
export type ApiLoaderExports<
ApiType extends UmbApi = UmbApi
> = ({default: ClassConstructor<ApiType>} | {api: ClassConstructor<ApiType>})//| Omit<Omit<object, 'api'>, 'default'>
export type ElementLoaderExports<ElementType extends HTMLElement = HTMLElement> =
| { default: ClassConstructor<ElementType> }
| { element: ClassConstructor<ElementType> }; // | Omit<Omit<object, 'element'>, 'default'>
export type ApiLoaderExports<ApiType extends UmbApi = UmbApi> =
| { default: ClassConstructor<ApiType> }
| { api: ClassConstructor<ApiType> }; //| Omit<Omit<object, 'api'>, 'default'>
export type ElementAndApiLoaderExports<
ElementType extends HTMLElement = HTMLElement,
ApiType extends UmbApi = UmbApi
> = ({api: ClassConstructor<ApiType>} | {element: ClassConstructor<ElementType>} | {api: ClassConstructor<ApiType>, element: ClassConstructor<ElementType>})// | Omit<Omit<Omit<object, 'element'>, 'api'>, 'default'>
ApiType extends UmbApi = UmbApi,
> =
| { api: ClassConstructor<ApiType> }
| { element: ClassConstructor<ElementType> }
| { api: ClassConstructor<ApiType>; element: ClassConstructor<ElementType> }; // | Omit<Omit<Omit<object, 'element'>, 'api'>, 'default'>
// Promise Types:
export type CssLoaderPromise<
CssType = unknown
> = (() => Promise<CssType>)
export type CssLoaderPromise<CssType extends string = string> = () => Promise<CssLoaderExports<CssType>>;
export type JsLoaderPromise<
JsType
> = (() => Promise<JsType>)
export type JsLoaderPromise<JsExportType> = () => Promise<JsExportType>;
export type ElementLoaderPromise<
ElementType extends HTMLElement = HTMLElement
> = (() => Promise<ElementLoaderExports<ElementType>>)
export type ElementLoaderPromise<ElementType extends HTMLElement = HTMLElement> = () => Promise<
ElementLoaderExports<ElementType>
>;
export type ApiLoaderPromise<
ApiType extends UmbApi = UmbApi
> = (() => Promise<ApiLoaderExports<ApiType>>)
export type ApiLoaderPromise<ApiType extends UmbApi = UmbApi> = () => Promise<ApiLoaderExports<ApiType>>;
export type ElementAndApiLoaderPromise<
ElementType extends HTMLElement = HTMLElement,
ApiType extends UmbApi = UmbApi
> = (() => Promise<ElementAndApiLoaderExports<ElementType, ApiType>>)
ApiType extends UmbApi = UmbApi,
> = () => Promise<ElementAndApiLoaderExports<ElementType, ApiType>>;
// Property Types:
export type CssLoaderProperty<CssType = string> = (
string
|
CssLoaderPromise<CssType>
);
export type JsLoaderProperty<JsType> = (
string
|
JsLoaderPromise<JsType>
);
export type ElementLoaderProperty<ElementType extends HTMLElement = HTMLElement> = (
string
|
ElementLoaderPromise<ElementType>
|
ClassConstructor<ElementType>
);
export type ApiLoaderProperty<ApiType extends UmbApi = UmbApi> = (
string
|
ApiLoaderPromise<ApiType>
|
ClassConstructor<ApiType>
);
export type ElementAndApiLoaderProperty<ElementType extends HTMLElement = HTMLElement, ApiType extends UmbApi = UmbApi> = (
string
|
ElementAndApiLoaderPromise<ElementType, ApiType>
|
ElementLoaderPromise<ElementType>
|
ApiLoaderPromise<ApiType>
);
export type CssLoaderProperty<CssType extends string = string> = string | CssLoaderPromise<CssType>;
export type JsLoaderProperty<JsExportType> = string | JsLoaderPromise<JsExportType>;
export type ElementLoaderProperty<ElementType extends HTMLElement = HTMLElement> =
| string
| ElementLoaderPromise<ElementType>
| ClassConstructor<ElementType>;
export type ApiLoaderProperty<ApiType extends UmbApi = UmbApi> =
| string
| ApiLoaderPromise<ApiType>
| ClassConstructor<ApiType>;
export type ElementAndApiLoaderProperty<
ElementType extends HTMLElement = HTMLElement,
ApiType extends UmbApi = UmbApi,
> =
| string
| ElementAndApiLoaderPromise<ElementType, ApiType>
| ElementLoaderPromise<ElementType>
| ApiLoaderPromise<ApiType>;

View File

@@ -8,7 +8,7 @@ export const data: Array<UserResponseModel & { type: string }> = [
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<UserResponseModel & { type: string }> = [
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<UserResponseModel & { type: string }> = [
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<UserResponseModel & { type: string }> = [
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<UserResponseModel & { type: string }> = [
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',

View File

@@ -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,

View File

@@ -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<string> {
export interface ManifestTheme extends ManifestPlainCss {
type: 'theme';
}

View File

@@ -0,0 +1,3 @@
import './ui-culture-input/ui-culture-input.element.js';
export { UmbUiCultureInputElement } from './ui-culture-input/ui-culture-input.element.js';

View File

@@ -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<UmbCultureInputOption> = [];
@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<ManifestLocalization>) {
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` <uui-combobox value="${this._value}" @change=${this.#onChange}>
<uui-combobox-list>
${this._options.map(
(option) => html`<uui-combobox-list-option value="${option.value}">${option.name}</uui-combobox-list-option>`,
)}
</uui-combobox-list>
</uui-combobox>`;
}
static styles = [
css`
:host {
display: block;
}
`,
];
}
export default UmbUiCultureInputElement;
declare global {
interface HTMLElementTagNameMap {
'umb-ui-culture-input': UmbUiCultureInputElement;
}
}

View File

@@ -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';

View File

@@ -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`
<uui-tab-group slot="header" id="dashboards">
${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`
<uui-tab-group slot="navigation" id="views">
${this._views.map((view) => {

View File

@@ -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 {

View File

@@ -17,14 +17,14 @@ export const themes: Array<ManifestTypes> = [
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,
},
];

View File

@@ -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;
}
}
}

View File

@@ -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`<uui-box>
<div slot="headline"><umb-localize key="user_profile">Profile</umb-localize></div>
@@ -112,13 +55,12 @@ export class UmbUserWorkspaceProfileSettingsElement extends UmbLitElement {
<umb-workspace-property-layout
label="${this.localize.term('user_language')}"
description=${this.localize.term('user_languageHelp')}>
<uui-select
<umb-ui-culture-input
slot="editor"
value=${ifDefined(this._user?.languageIsoCode)}
@change="${this.#onLanguageChange}"
name="language"
label="${this.localize.term('user_language')}"
.options=${this.languages}
@change="${this.#onLanguageChange}">
</uui-select>
label="${this.localize.term('user_language')}"></umb-ui-culture-input>
</umb-workspace-property-layout>
`;
}

View File

@@ -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(),