Merge branch 'main' into feature/tree-paging

This commit is contained in:
Mads Rasmussen
2024-02-27 21:51:44 +01:00
113 changed files with 1748 additions and 1422 deletions

View File

@@ -65,6 +65,16 @@ declare class UmbClassMixinDeclaration extends EventTarget implements UmbClassMi
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType>;
/**
* @description Retrieve a context. Notice this is a one time retrieving of a context, meaning if you expect this to be up to date with reality you should instead use the consumeContext method.
* @param {string} contextAlias
* @return {Promise<ContextType>} A Promise with the reference to the Context Api Instance
* @memberof UmbClassMixin
*/
getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
): Promise<ResultType>;
hasController(controller: UmbController): boolean;
getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[];
addController(controller: UmbController): void;
@@ -129,6 +139,17 @@ export const UmbClassMixin = <T extends ClassConstructor>(superClass: T) => {
return new UmbContextConsumerController(this, contextAlias, callback);
}
async getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
contextAlias: string | UmbContextToken<BaseType, ResultType>,
): Promise<ResultType> {
const controller = new UmbContextConsumerController(this, contextAlias);
const promise = controller.asPromise().then((result) => {
controller.destroy();
return result;
});
return promise;
}
public destroy(): void {
if (this._host) {
this._host.removeController(this);

View File

@@ -17,7 +17,7 @@ export class UmbContextConsumerController<BaseType = unknown, ResultType extends
constructor(
host: UmbControllerHost,
contextAlias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>,
callback?: UmbContextCallback<ResultType>,
) {
super(host.getHostElement(), contextAlias, callback);
this.#host = host;

View File

@@ -1 +1 @@
export type UmbControllerAlias = string | symbol | undefined;
export type UmbControllerAlias = string | number | symbol | undefined;

View File

@@ -6,9 +6,9 @@ import type { UmbControllerHost } from './controller-host.interface.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('test-my-controller-host')
export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
export class UmbTestControllerImplementation extends UmbControllerHostMixin(class {}) {
class UmbTestControllerImplementation extends UmbControllerHostMixin(class {}) {
testIsConnected = false;
testIsDestroyed = false;
@@ -47,9 +47,7 @@ export class UmbTestControllerImplementation extends UmbControllerHostMixin(clas
}
describe('UmbController', () => {
type NewType = UmbControllerHostElement;
let hostElement: NewType;
let hostElement: UmbControllerHostElement;
beforeEach(() => {
hostElement = document.createElement('test-my-controller-host') as UmbControllerHostElement;

View File

@@ -32,6 +32,9 @@ export declare class UmbElement extends UmbControllerHostElement {
alias: string | UmbContextToken<BaseType, ResultType>,
callback: UmbContextCallback<ResultType>,
): UmbContextConsumerController<BaseType, ResultType>;
getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
alias: string | UmbContextToken<BaseType, ResultType>,
): Promise<ResultType>;
/**
* Use the UmbLocalizeController to localize your element.
* @see UmbLocalizationController
@@ -86,6 +89,24 @@ export const UmbElementMixin = <T extends HTMLElementConstructor>(superClass: T)
return new UmbContextConsumerController(this, alias, callback);
}
/**
* @description Setup a subscription for a context. The callback is called when the context is resolved.
* @param {string} contextAlias
* @param {method} callback Callback method called when context is resolved.
* @return {UmbContextConsumerController} Reference to a Context Consumer Controller instance
* @memberof UmbElementMixin
*/
async getContext<BaseType = unknown, ResultType extends BaseType = BaseType>(
contextAlias: string | UmbContextToken<BaseType, ResultType>,
): Promise<ResultType> {
const controller = new UmbContextConsumerController(this, contextAlias);
const promise = controller.asPromise().then((result) => {
controller.destroy();
return result;
});
return promise;
}
destroy(): void {
super.destroy();
(this.localize as any) = undefined;

View File

@@ -16,7 +16,7 @@ import { UmbSwitchCondition } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@customElement('umb-test-controller-host')
export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
class UmbTestExtensionController extends UmbBaseExtensionInitializer {
constructor(

View File

@@ -130,6 +130,8 @@ export abstract class UmbBaseExtensionsInitializer<
#notifyChange = () => {
this.#changeDebounce = undefined;
// This means that we have been destroyed:
if (this.#permittedExts === undefined) return;
// The final list of permitted extensions to be displayed, this will be stripped from extensions that are overwritten by another extension and sorted accordingly.
this.#exposedPermittedExts = [...this.#permittedExts];

View File

@@ -9,9 +9,9 @@ import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { type ManifestSection, UmbSwitchCondition } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-test-controller-host')
export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
export class UmbTestApiController extends UmbBaseController {
class UmbTestApiController extends UmbBaseController {
public i_am_test_api_controller = true;
constructor(host: UmbControllerHost) {

View File

@@ -1,13 +1,13 @@
import { expect, fixture } from '@open-wc/testing';
import { UmbExtensionRegistry } from '../registry/extension.registry.js';
import { UmbExtensionElementInitializer } from './index.js';
import type { UmbControllerHostElement} from '@umbraco-cms/backoffice/controller-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
import { customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { type ManifestSection, UmbSwitchCondition } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-test-controller-host')
export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
describe('UmbExtensionElementController', () => {
describe('Manifest without conditions', () => {

View File

@@ -0,0 +1,71 @@
import { expect } from '@open-wc/testing';
import { UmbObjectState } from './states/object-state.js';
import { UmbObserverController } from './observer.controller.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
@customElement('test-my-observer-controller-host')
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
describe('UmbObserverController', () => {
describe('Observer Controllers against other Observer Controllers', () => {
let hostElement: UmbTestControllerHostElement;
beforeEach(() => {
hostElement = document.createElement('test-my-observer-controller-host') as UmbTestControllerHostElement;
});
it('controller is replaced by another controller using the same string as controller-alias', () => {
const state = new UmbObjectState(undefined);
const observable = state.asObservable();
const callbackMethod = (state: unknown) => {};
const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod, 'my-test-alias');
const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod, 'my-test-alias');
expect(hostElement.hasController(firstCtrl)).to.be.false;
expect(hostElement.hasController(secondCtrl)).to.be.true;
});
it('controller is replaced by another controller using the the same symbol as controller-alias', () => {
const state = new UmbObjectState(undefined);
const observable = state.asObservable();
const callbackMethod = (state: unknown) => {};
const mySymbol = Symbol();
const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod, mySymbol);
const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod, mySymbol);
expect(hostElement.hasController(firstCtrl)).to.be.false;
expect(hostElement.hasController(secondCtrl)).to.be.true;
});
it('controller is replacing another controller when using the same callback method and no controller-alias', () => {
const state = new UmbObjectState(undefined);
const observable = state.asObservable();
const callbackMethod = (state: unknown) => {};
const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod);
const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod);
expect(hostElement.hasController(firstCtrl)).to.be.false;
expect(hostElement.hasController(secondCtrl)).to.be.true;
});
it('controller is NOT replacing another controller when using a null for controller-alias', () => {
const state = new UmbObjectState(undefined);
const observable = state.asObservable();
const callbackMethod = (state: unknown) => {};
const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod, null);
const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod, null);
expect(hostElement.hasController(firstCtrl)).to.be.true;
expect(hostElement.hasController(secondCtrl)).to.be.true;
});
});
});

View File

@@ -1,4 +1,5 @@
import { type ObserverCallback, UmbObserver } from './observer.js';
import { simpleHashCode } from './utils/simple-hash-code.function.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbController, UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -14,11 +15,12 @@ export class UmbObserverController<T = unknown> extends UmbObserver<T> implement
host: UmbControllerHost,
source: Observable<T>,
callback: ObserverCallback<T>,
alias?: UmbControllerAlias,
alias?: UmbControllerAlias | null,
) {
super(source, callback);
this.#host = host;
this.#alias = alias;
// Fallback to use a hash of the provided method, but only if the alias is undefined.
this.#alias = alias ?? (alias === undefined ? simpleHashCode(callback.toString()) : undefined);
// Lets check if controller is already here:
// No we don't want this, as multiple different controllers might be looking at the same source.

View File

@@ -8,3 +8,4 @@ export * from './naive-object-comparison.function.js';
export * from './observe-multiple.function.js';
export * from './partial-update-frozen-array.function.js';
export * from './push-to-unique-array.function.js';
export * from './simple-hash-code.function.js';

View File

@@ -0,0 +1,15 @@
/**
* Returns a hash code from a string
* @param {String} str - The string to hash.
* @return {Number} - A 32bit integer
*/
export function simpleHashCode(str: string) {
let hash = 0,
i = 0;
const len = str.length;
while (i < len) {
hash = (hash << 5) - hash + str.charCodeAt(i++);
hash |= 0; // Convert to 32bit integer
}
return hash;
}

View File

@@ -9,7 +9,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
export class UmbBlockGridAreaConfigEntryContext
extends UmbContextBase<UmbBlockGridAreaConfigEntryContext>
implements UmbBlockGridScalableContext
@@ -86,19 +86,14 @@ export class UmbBlockGridAreaConfigEntryContext
);
}
requestDelete() {
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, async (modalManager) => {
const modalContext = modalManager.open(UMB_CONFIRM_MODAL, {
data: {
headline: `Delete ${this.alias}`,
content: 'Are you sure you want to delete this Area?',
confirmLabel: 'Delete',
color: 'danger',
},
});
await modalContext.onSubmit();
this.delete();
async requestDelete() {
await umbConfirmModal(this, {
headline: `Delete ${this.alias}`,
content: 'Are you sure you want to delete this Area?',
confirmLabel: 'Delete',
color: 'danger',
});
this.delete();
}
public delete() {
if (!this.#areaKey || !this.#propertyContext) return;

View File

@@ -244,33 +244,32 @@ export class UmbBlockGridEntriesContext
// Area entries:
if (!this.#areaType) return [];
if (this.#areaType.specifiedAllowance && this.#areaType.specifiedAllowance.length > 0) {
return this.#areaType.specifiedAllowance
.flatMap((permission) => {
if (permission.groupKey) {
return (
this._manager
?.getBlockTypes()
.filter(
(blockType) => blockType.groupKey === permission.groupKey && blockType.allowInAreas === true,
) ?? []
);
} else if (permission.elementTypeKey) {
return (
this._manager?.getBlockTypes().filter((x) => x.contentElementTypeKey === permission.elementTypeKey) ??
[]
);
}
return [];
})
.filter((v, i, a) => a.find((x) => x.contentElementTypeKey === v.contentElementTypeKey) === undefined);
if (this.#areaType.specifiedAllowance && this.#areaType.specifiedAllowance?.length > 0) {
return (
this.#areaType.specifiedAllowance
.flatMap((permission) => {
if (permission.groupKey) {
return (
this._manager?.getBlockTypes().filter((blockType) => blockType.groupKey === permission.groupKey) ?? []
);
} else if (permission.elementTypeKey) {
return (
this._manager?.getBlockTypes().filter((x) => x.contentElementTypeKey === permission.elementTypeKey) ??
[]
);
}
return [];
})
// Remove duplicates:
.filter((v, i, a) => a.findIndex((x) => x.contentElementTypeKey === v.contentElementTypeKey) === i)
);
}
// No specific permissions setup, so we will fallback to items allowed in areas:
return this._manager.getBlockTypes().filter((x) => x.allowInAreas);
}
// If no AreaKey, then we are representing the items of the root:
// Root entries:
// If no AreaKey, then we are in the root, looking for items allowed as root:
return this._manager.getBlockTypes().filter((x) => x.allowAtRoot);
}

View File

@@ -1,8 +1,8 @@
import type { UmbBlockTypeBaseModel } from '../../types.js';
import {
UMB_CONFIRM_MODAL,
UMB_DOCUMENT_TYPE_PICKER_MODAL,
UMB_MODAL_MANAGER_CONTEXT,
umbConfirmModal,
} from '@umbraco-cms/backoffice/modal';
import '../block-type-card/index.js';
import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
@@ -42,35 +42,33 @@ export class UmbInputBlockTypeElement<
});
}
create() {
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, async (modalManager) => {
if (modalManager) {
// TODO: Make as mode for the Picker Modal, so the click to select immediately submits the modal(And in that mode we do not want to see a Submit button).
const modalContext = modalManager.open(UMB_DOCUMENT_TYPE_PICKER_MODAL, {
data: {
hideTreeRoot: true,
multiple: false,
pickableFilter: (docType) =>
// Only pick elements:
docType.isElement &&
// Prevent picking the an already used element type:
this.#filter &&
this.#filter.find((x) => x.contentElementTypeKey === docType.unique) === undefined,
},
});
async create() {
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const modalValue = await modalContext?.onSubmit();
const selectedElementType = modalValue.selection[0];
if (selectedElementType) {
this.dispatchEvent(new CustomEvent('create', { detail: { contentElementTypeKey: selectedElementType } }));
}
}
// TODO: Make as mode for the Picker Modal, so the click to select immediately submits the modal(And in that mode we do not want to see a Submit button).
const modalContext = modalManager.open(UMB_DOCUMENT_TYPE_PICKER_MODAL, {
data: {
hideTreeRoot: true,
multiple: false,
pickableFilter: (docType) =>
// Only pick elements:
docType.isElement &&
// Prevent picking the an already used element type:
this.#filter &&
this.#filter.find((x) => x.contentElementTypeKey === docType.unique) === undefined,
},
});
const modalValue = await modalContext?.onSubmit();
const selectedElementType = modalValue.selection[0];
if (selectedElementType) {
this.dispatchEvent(new CustomEvent('create', { detail: { contentElementTypeKey: selectedElementType } }));
}
}
deleteItem(contentElementTypeKey: string) {
this.value = this._items.filter((x) => x.contentElementTypeKey !== contentElementTypeKey);
this.value = this.value.filter((x) => x.contentElementTypeKey !== contentElementTypeKey);
this.dispatchEvent(new UmbChangeEvent());
}
@@ -78,20 +76,14 @@ export class UmbInputBlockTypeElement<
return undefined;
}
#onRequestDelete(item: BlockType) {
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, async (modalManager) => {
const modalContext = modalManager.open(UMB_CONFIRM_MODAL, {
data: {
color: 'danger',
headline: `Remove [TODO: Get name]?`,
content: 'Are you sure you want to remove this block type?',
confirmLabel: 'Remove',
},
});
await modalContext?.onSubmit();
this.deleteItem(item.contentElementTypeKey);
async #onRequestDelete(item: BlockType) {
await umbConfirmModal(this, {
color: 'danger',
headline: `Remove [TODO: Get name]?`,
content: 'Are you sure you want to remove this block type?',
confirmLabel: 'Remove',
});
this.deleteItem(item.contentElementTypeKey);
}
render() {
@@ -109,7 +101,7 @@ export class UmbInputBlockTypeElement<
.href="${this.workspacePath}/edit/${block.contentElementTypeKey}"
.contentElementTypeKey=${block.contentElementTypeKey}>
<uui-action-bar slot="actions">
<uui-button @click=${this.#onRequestDelete} label="Remove block">
<uui-button @click=${() => this.#onRequestDelete(block)} label="Remove block">
<uui-icon name="icon-trash"></uui-icon>
</uui-button>
</uui-action-bar>

View File

@@ -7,7 +7,7 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbNumberState, UmbObjectState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { encodeFilePath } from '@umbraco-cms/backoffice/utils';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import type { UmbContentTypeModel } from '@umbraco-cms/backoffice/content-type';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
@@ -353,20 +353,16 @@ export abstract class UmbBlockEntryContext<
window.location.href = this.#generateWorkspaceEditSettingsPath(this.#workspacePath.value);
}
requestDelete() {
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, async (modalManager) => {
const modalContext = modalManager.open(UMB_CONFIRM_MODAL, {
data: {
headline: `Delete ${this.getLabel()}`,
content: 'Are you sure you want to delete this [INSERT BLOCK TYPE NAME]?',
confirmLabel: 'Delete',
color: 'danger',
},
});
await modalContext.onSubmit();
this.delete();
async requestDelete() {
await umbConfirmModal(this, {
headline: `Delete ${this.getLabel()}`,
content: 'Are you sure you want to delete this [INSERT BLOCK TYPE NAME]?',
confirmLabel: 'Delete',
color: 'danger',
});
this.delete();
}
public delete() {
if (!this._entries) return;
const contentUdi = this._layout.value?.contentUdi;

View File

@@ -1,7 +1,8 @@
import { UMB_BLOCK_WORKSPACE_MODAL } from '../../workspace/index.js';
import type { UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue } from '@umbraco-cms/backoffice/block';
import type { UmbBlockTypeGroup, UmbBlockTypeWithGroupKey } from '@umbraco-cms/backoffice/block-type';
import type { UmbBlockCatalogueModalData, UmbBlockCatalogueModalValue } from '@umbraco-cms/backoffice/block';
import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit';
import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import {
UMB_MODAL_CONTEXT,
UmbModalBaseElement,
@@ -15,14 +16,19 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
UmbBlockCatalogueModalData,
UmbBlockCatalogueModalValue
> {
@state()
//
private _search = '';
private _groupedBlocks: Array<{ name?: string; blocks: Array<UmbBlockTypeWithGroupKey> }> = [];
@state()
_openClipboard?: boolean;
private _openClipboard?: boolean;
@state()
_workspacePath?: string;
private _workspacePath?: string;
@state()
private _filtered: Array<{ name?: string; blocks: Array<UmbBlockTypeWithGroupKey> }> = [];
constructor() {
super();
@@ -61,6 +67,23 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
}));
this._groupedBlocks = [{ blocks: noGroupBlocks }, ...grouped];
this.#updateFiltered();
}
#updateFiltered() {
if (this._search.length === 0) {
this._filtered = this._groupedBlocks;
} else {
const search = this._search.toLowerCase();
this._filtered = this._groupedBlocks.map((group) => {
return { ...group, blocks: group.blocks.filter((block) => block.label?.toLocaleLowerCase().includes(search)) };
});
}
}
#onSearch(e: UUIInputEvent) {
this._search = e.target.value as string;
this.#updateFiltered();
}
render() {
@@ -85,28 +108,35 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
#renderCreateEmpty() {
return html`
${this._groupedBlocks
? this._groupedBlocks.map(
(group) => html`
${group.name && group.name !== '' ? html`<h4>${group.name}</h4>` : nothing}
<div class="blockGroup">
${repeat(
group.blocks,
(block) => block.contentElementTypeKey,
(block) => html`
<umb-block-type-card
.name=${block.label}
.iconColor=${block.iconColor}
.backgroundColor=${block.backgroundColor}
.contentElementTypeKey=${block.contentElementTypeKey}
.href="${this._workspacePath}create/${block.contentElementTypeKey}">
</umb-block-type-card>
`,
)}
</div>
`,
)
: ''}
${this.data?.blocks && this.data.blocks.length > 8
? html`<uui-input
id="search"
@input=${this.#onSearch}
label=${this.localize.term('general_search')}
placeholder=${this.localize.term('placeholders_search')}>
<uui-icon name="icon-search" slot="prepend"></uui-icon>
</uui-input>`
: nothing}
${this._filtered.map(
(group) => html`
${group.name && group.name !== '' ? html`<h4>${group.name}</h4>` : nothing}
<div class="blockGroup">
${repeat(
group.blocks,
(block) => block.contentElementTypeKey,
(block) => html`
<umb-block-type-card
.name=${block.label}
.iconColor=${block.iconColor}
.backgroundColor=${block.backgroundColor}
.contentElementTypeKey=${block.contentElementTypeKey}
.href="${this._workspacePath}create/${block.contentElementTypeKey}">
</umb-block-type-card>
`,
)}
</div>
`,
)}
`;
}
@@ -133,6 +163,14 @@ export class UmbBlockCatalogueModalElement extends UmbModalBaseElement<
static styles = [
css`
#search {
width: 100%;
align-items: center;
margin-bottom: var(--uui-size-layout-1);
}
#search uui-icon {
padding-left: var(--uui-size-space-3);
}
.blockGroup {
display: grid;
gap: 1rem;

View File

@@ -2,12 +2,12 @@ import { expect } from '@open-wc/testing';
import { UmbCollectionViewManager } from './collection-view.manager.js';
import { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
import type { ManifestCollectionView} from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestCollectionView } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('test-my-controller-host')
export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
const VIEW_1_ALIAS = 'UmbTest.CollectionView.1';
const VIEW_2_ALIAS = 'UmbTest.CollectionView.2';

View File

@@ -1,8 +1,7 @@
import { css, html, nothing, customElement, property, query, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UUIColorPickerElement, UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbChangeEvent, UmbInputEvent, UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -38,32 +37,18 @@ export class UmbMultipleColorPickerItemInputElement extends FormControlMixin(Umb
@query('#color')
protected _colorPicker!: UUIColorPickerElement;
private _modalContext?: UmbModalManagerContext;
@property({ type: Boolean })
showLabels = true;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this._modalContext = instance;
});
}
#onDelete() {
const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: `${this.localize.term('actions_delete')} ${this.value || ''}`,
content: this.localize.term('content_nestedContentDeleteItem'),
color: 'danger',
confirmLabel: this.localize.term('actions_delete'),
},
async #onDelete() {
await umbConfirmModal(this, {
headline: `${this.localize.term('actions_delete')} ${this.value || ''}`,
content: this.localize.term('content_nestedContentDeleteItem'),
color: 'danger',
confirmLabel: this.localize.term('actions_delete'),
});
modalContext?.onSubmit().then(() => {
this.dispatchEvent(new UmbDeleteEvent());
});
this.dispatchEvent(new UmbDeleteEvent());
}
#onLabelInput(event: UUIInputEvent) {

View File

@@ -1,8 +1,7 @@
import { css, html, nothing, customElement, property, query } from '@umbraco-cms/backoffice/external/lit';
import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbChangeEvent, UmbInputEvent, UmbDeleteEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -32,29 +31,15 @@ export class UmbInputMultipleTextStringItemElement extends FormControlMixin(UmbL
@query('#input')
protected _input?: UUIInputElement;
private _modalContext?: UmbModalManagerContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this._modalContext = instance;
});
}
#onDelete() {
const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: `Delete ${this.value || 'item'}`,
content: 'Are you sure you want to delete this item?',
color: 'danger',
confirmLabel: 'Delete',
},
async #onDelete() {
await umbConfirmModal(this, {
headline: `Delete ${this.value || 'item'}`,
content: 'Are you sure you want to delete this item?',
color: 'danger',
confirmLabel: 'Delete',
});
modalContext?.onSubmit().then(() => {
this.dispatchEvent(new UmbDeleteEvent());
});
this.dispatchEvent(new UmbDeleteEvent());
}
#onInput(event: UUIInputEvent) {

View File

@@ -1,39 +1,22 @@
import { UmbEntityActionBase } from '../../entity-action.js';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbModalManagerContext} from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import type { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository';
export class UmbDeleteEntityAction<
T extends UmbDetailRepository<any> & UmbItemRepository<any>,
> extends UmbEntityActionBase<T> {
#modalManager?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
new UmbContextConsumerController(this._host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManager = instance;
});
}
async execute() {
if (!this.repository || !this.#modalManager) return;
if (!this.repository) return;
// TOOD: add back when entity actions can support multiple repositories
//const { data } = await this.repository.requestItems([this.unique]);
const modalContext = this.#modalManager.open(UMB_CONFIRM_MODAL, {
data: {
headline: `Delete`,
content: 'Are you sure you want to delete this item?',
color: 'danger',
confirmLabel: 'Delete',
},
await umbConfirmModal(this._host, {
headline: `Delete`,
content: 'Are you sure you want to delete this item?',
color: 'danger',
confirmLabel: 'Delete',
});
await modalContext.onSubmit();
await this.repository?.delete(this.unique);
}
}

View File

@@ -1,23 +1,10 @@
import { UmbEntityActionBase } from '../../entity-action.js';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbModalManagerContext} from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository';
export class UmbTrashEntityAction<
T extends UmbItemRepository<any> & { trash(unique: string): Promise<void> },
> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
new UmbContextConsumerController(this._host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;
});
}
async execute() {
if (!this.repository) return;
@@ -26,18 +13,14 @@ export class UmbTrashEntityAction<
if (data) {
const item = data[0];
const modalContext = this.#modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: `Trash ${item.name}`,
content: 'Are you sure you want to move this item to the recycle bin?',
color: 'danger',
confirmLabel: 'Trash',
},
await umbConfirmModal(this._host, {
headline: `Trash ${item.name}`,
content: 'Are you sure you want to move this item to the recycle bin?',
color: 'danger',
confirmLabel: 'Trash',
});
modalContext?.onSubmit().then(() => {
this.repository?.trash(this.unique);
});
this.repository?.trash(this.unique);
}
}
}

View File

@@ -2,34 +2,20 @@ import { umbExtensionsRegistry } from '../../index.js';
import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api';
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
@customElement('umb-extension-table-action-column-layout')
export class UmbExtensionTableActionColumnLayoutElement extends UmbLitElement {
@property({ attribute: false })
value!: ManifestBase;
#modalContext?: UmbModalManagerContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;
});
}
async #removeExtension() {
const modalContext = this.#modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: 'Unload extension',
confirmLabel: 'Unload',
content: html`<p>Are you sure you want to unload the extension <strong>${this.value.alias}</strong>?</p>`,
color: 'danger',
},
await umbConfirmModal(this, {
headline: 'Unload extension',
confirmLabel: 'Unload',
content: html`<p>Are you sure you want to unload the extension <strong>${this.value.alias}</strong>?</p>`,
color: 'danger',
});
await modalContext?.onSubmit();
umbExtensionsRegistry.unregister(this.value.alias);
}

View File

@@ -1,8 +1,8 @@
import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api';
import type { UmbItemStore, UmbStoreBase } from '@umbraco-cms/backoffice/store';
import type { UmbItemStore } from '@umbraco-cms/backoffice/store';
import type { UmbTreeStore } from '@umbraco-cms/backoffice/tree';
export interface ManifestStore extends ManifestApi<UmbStoreBase> {
export interface ManifestStore extends ManifestApi<any> {
type: 'store';
}
// TODO: TREE STORE TYPE PROBLEM: Provide a base tree item type?

View File

@@ -0,0 +1,30 @@
import { UMB_CONFIRM_MODAL, type UmbConfirmModalData } from '../../token/confirm-modal.token.js';
import { UMB_MODAL_MANAGER_CONTEXT } from '../../context/modal-manager.context.js';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export interface UmbConfirmModalArgs extends UmbConfirmModalData {}
export class UmbConfirmModalController extends UmbBaseController {
async open(args: UmbConfirmModalArgs): Promise<void> {
const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const modalContext = modalManagerContext.open(UMB_CONFIRM_MODAL, {
data: args,
});
await modalContext.onSubmit().catch(() => {
this.destroy();
});
// This is a one time off, so we can destroy our selfs.
this.destroy();
// Map back into UmbVariantId instances:
return;
}
}
export function umbConfirmModal(host: UmbControllerHost, args: UmbConfirmModalArgs) {
return new UmbConfirmModalController(host).open(args);
}

View File

@@ -1,5 +1,5 @@
import type { UmbModalContext } from './modal.context.js';
import { UMB_MODAL_CONTEXT } from './modal.context.js';
import type { UmbModalContext } from '../context/modal.context.js';
import { UMB_MODAL_CONTEXT } from '../context/modal.context.js';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';

View File

@@ -1,4 +1,4 @@
import type { UmbModalToken } from './token/modal-token.js';
import type { UmbModalToken } from '../token/modal-token.js';
import { UmbModalContext, type UmbModalContextClassArgs } from './modal.context.js';
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
import { UmbBasicState, appendToFrozenArray } from '@umbraco-cms/backoffice/observable-api';

View File

@@ -1,5 +1,5 @@
import type { UmbModalConfig, UmbModalType } from './modal-manager.context.js';
import { UmbModalToken } from './token/modal-token.js';
import { UmbModalToken } from '../token/modal-token.js';
import type { IRouterSlot } from '@umbraco-cms/backoffice/external/router-slot';
import type { UUIModalSidebarSize } from '@umbraco-cms/backoffice/external/uui';
import { UmbId } from '@umbraco-cms/backoffice/id';
@@ -81,6 +81,7 @@ export class UmbModalContext<ModalPreset extends object = object, ModalValue = a
*/
public submit() {
this.#submitResolver?.(this.getValue());
// TODO: Could we clean up this class here? (Example destroy the value state, and other things?)
}
/**
@@ -90,6 +91,7 @@ export class UmbModalContext<ModalPreset extends object = object, ModalValue = a
*/
public reject(reason?: UmbModalRejectReason) {
this.#submitRejecter?.(reason);
// TODO: Could we clean up this class here? (Example destroy the value state, and other things?)
}
/**

View File

@@ -1,10 +1,11 @@
import './modal.element.js';
import './component/modal.element.js';
export * from './modal-manager.context.js';
export * from './modal.context.js';
export * from './modal-route-registration.js';
export * from './modal-route-registration.controller.js';
export * from './context/modal-manager.context.js';
export * from './context/modal.context.js';
export * from './route-registration/modal-route-registration.js';
export * from './route-registration/modal-route-registration.controller.js';
export * from './token/index.js';
export * from './modal.interfaces.js';
export * from './modal-element.element.js';
export * from './modal.element.js';
export * from './types.js';
export * from './component/modal-element.element.js';
export * from './component/modal.element.js';
export * from './common/confirm/confirm-modal.controller.js';

View File

@@ -1,5 +1,5 @@
import { UmbModalRouteRegistration } from './modal-route-registration.js';
import type { UmbModalToken } from './token/index.js';
import type { UmbModalToken } from '../token/index.js';
import { UMB_ROUTE_CONTEXT } from '@umbraco-cms/backoffice/router';
import type { UmbController, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';

View File

@@ -1,6 +1,6 @@
import type { UmbModalContext } from './modal.context.js';
import type { UmbModalConfig, UmbModalManagerContext } from './modal-manager.context.js';
import type { UmbModalToken } from './token/modal-token.js';
import type { UmbModalContext } from '../context/modal.context.js';
import type { UmbModalConfig, UmbModalManagerContext } from '../context/modal-manager.context.js';
import type { UmbModalToken } from '../token/modal-token.js';
import type { IRouterSlot } from '@umbraco-cms/backoffice/external/router-slot';
import { encodeFolderName } from '@umbraco-cms/backoffice/router';
import { UmbId } from '@umbraco-cms/backoffice/id';

View File

@@ -1,4 +1,4 @@
import type { UmbPickerModalValue, UmbTreePickerModalData } from '../modal.interfaces.js';
import type { UmbPickerModalValue, UmbTreePickerModalData } from '../types.js';
import { UmbModalToken } from './modal-token.js';
import type { UmbUniqueTreeItemModel } from '@umbraco-cms/backoffice/tree';

View File

@@ -1,4 +1,4 @@
import type { UmbModalConfig } from '../modal-manager.context.js';
import type { UmbModalConfig } from '../context/modal-manager.context.js';
export interface UmbModalTokenDefaults<ModalDataType extends object = object, ModalValueType = unknown> {
modal?: UmbModalConfig;

View File

@@ -2,13 +2,8 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { type UmbItemRepository, UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import type {
UmbModalManagerContext,
UmbModalToken,
UmbPickerModalData,
UmbPickerModalValue,
} from '@umbraco-cms/backoffice/modal';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { UmbModalToken, UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, umbConfirmModal } from '@umbraco-cms/backoffice/modal';
export class UmbPickerInputContext<ItemType extends { name: string; unique: string }> extends UmbBaseController {
// TODO: We are way too unsecure about the requirements for the Modal Token, as we have certain expectation for the data and value.
@@ -16,10 +11,6 @@ export class UmbPickerInputContext<ItemType extends { name: string; unique: stri
repository?: UmbItemRepository<ItemType>;
#getUnique: (entry: ItemType) => string | undefined;
public modalManager?: UmbModalManagerContext;
#init: Promise<unknown>;
#itemManager;
selection;
@@ -63,13 +54,6 @@ export class UmbPickerInputContext<ItemType extends { name: string; unique: stri
this.selection = this.#itemManager.uniques;
this.selectedItems = this.#itemManager.items;
this.#init = Promise.all([
this.#itemManager.init,
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.modalManager = instance;
}).asPromise(),
]);
}
getSelection() {
@@ -82,10 +66,9 @@ export class UmbPickerInputContext<ItemType extends { name: string; unique: stri
}
async openPicker(pickerData?: Partial<UmbPickerModalData<ItemType>>) {
await this.#init;
if (!this.modalManager) throw new Error('Modal manager context is not initialized');
const modalContext = this.modalManager.open(this.modalAlias, {
await this.#itemManager.init;
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const modalContext = modalManager.open(this.modalAlias, {
data: {
multiple: this._max === 1 ? false : true,
...pickerData,
@@ -105,16 +88,12 @@ export class UmbPickerInputContext<ItemType extends { name: string; unique: stri
const item = this.#itemManager.getItems().find((item) => this.#getUnique(item) === unique);
if (!item) throw new Error('Could not find item with unique: ' + unique);
const modalContext = this.modalManager?.open(UMB_CONFIRM_MODAL, {
data: {
color: 'danger',
headline: `Remove ${item.name}?`,
content: 'Are you sure you want to remove this item',
confirmLabel: 'Remove',
},
await umbConfirmModal(this, {
color: 'danger',
headline: `Remove ${item.name}?`,
content: 'Are you sure you want to remove this item',
confirmLabel: 'Remove',
});
await modalContext?.onSubmit();
this.#removeItem(unique);
}

View File

@@ -1,38 +1,22 @@
import { UmbEntityActionBase } from '../../../../entity-action/entity-action.js';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { UmbModalManagerContext} from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import type { UmbFolderRepository } from '@umbraco-cms/backoffice/tree';
export class UmbDeleteFolderEntityAction<T extends UmbFolderRepository> extends UmbEntityActionBase<T> {
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
new UmbContextConsumerController(this._host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;
});
}
async execute() {
if (!this.repository || !this.#modalContext) return;
if (!this.repository) return;
const { data: folder } = await this.repository.request(this.unique);
if (folder) {
// TODO: maybe we can show something about how many items are part of the folder?
const modalContext = this.#modalContext.open(UMB_CONFIRM_MODAL, {
data: {
headline: `Delete folder ${folder.name}`,
content: 'Are you sure you want to delete this folder?',
color: 'danger',
confirmLabel: 'Delete',
},
});
await modalContext.onSubmit();
await umbConfirmModal(this._host, {
headline: `Delete folder ${folder.name}`,
content: 'Are you sure you want to delete this folder?',
color: 'danger',
confirmLabel: 'Delete',
});
await this.repository?.delete(this.unique);
}
}

View File

@@ -5,7 +5,7 @@ import { customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
@customElement('test-my-controller-host')
export class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {}
describe('UmbSelectionManager', () => {
let manager: UmbSelectionManager;

View File

@@ -8,11 +8,11 @@ import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observab
* @export
* @class UmbSelectionManager
*/
export class UmbSelectionManager extends UmbBaseController {
export class UmbSelectionManager<ValueType = string | null> extends UmbBaseController {
#selectable = new UmbBooleanState(false);
public readonly selectable = this.#selectable.asObservable();
#selection = new UmbArrayState(<Array<string | null>>[], (x) => x);
#selection = new UmbArrayState(<Array<ValueType>>[], (x) => x);
public readonly selection = this.#selection.asObservable();
#multiple = new UmbBooleanState(false);
@@ -51,10 +51,10 @@ export class UmbSelectionManager extends UmbBaseController {
/**
* Sets the current selection.
* @param {Array<string | null>} value
* @param {Array<ValueType>} value
* @memberof UmbSelectionManager
*/
public setSelection(value: Array<string | null>) {
public setSelection(value: Array<ValueType>) {
if (this.getSelectable() === false) return;
if (value === undefined) throw new Error('Value cannot be undefined');
const newSelection = this.getMultiple() ? value : value.slice(0, 1);
@@ -87,20 +87,20 @@ export class UmbSelectionManager extends UmbBaseController {
/**
* Toggles the given unique id in the current selection.
* @param {(string | null)} unique
* @param {(ValueType)} unique
* @memberof UmbSelectionManager
*/
public toggleSelect(unique: string | null) {
public toggleSelect(unique: ValueType) {
if (this.getSelectable() === false) return;
this.isSelected(unique) ? this.deselect(unique) : this.select(unique);
}
/**
* Appends the given unique id to the current selection.
* @param {(string | null)} unique
* @param {(ValueType)} unique
* @memberof UmbSelectionManager
*/
public select(unique: string | null) {
public select(unique: ValueType) {
if (this.getSelectable() === false) return;
if (this.isSelected(unique)) return;
const newSelection = this.getMultiple() ? [...this.getSelection(), unique] : [unique];
@@ -110,10 +110,10 @@ export class UmbSelectionManager extends UmbBaseController {
/**
* Removes the given unique id from the current selection.
* @param {(string | null)} unique
* @param {(ValueType)} unique
* @memberof UmbSelectionManager
*/
public deselect(unique: string | null) {
public deselect(unique: ValueType) {
if (this.getSelectable() === false) return;
const newSelection = this.getSelection().filter((x) => x !== unique);
this.#selection.setValue(newSelection);
@@ -122,11 +122,11 @@ export class UmbSelectionManager extends UmbBaseController {
/**
* Returns true if the given unique id is selected.
* @param {(string | null)} unique
* @param {(ValueType)} unique
* @return {*}
* @memberof UmbSelectionManager
*/
public isSelected(unique: string | null) {
public isSelected(unique: ValueType) {
return this.getSelection().includes(unique);
}

View File

@@ -1,3 +1,5 @@
import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
export interface UmbVariantModel {
createDate: string | null;
culture: string | null;
@@ -5,3 +7,12 @@ export interface UmbVariantModel {
segment: string | null;
updateDate: string | null;
}
export interface UmbVariantOptionModel<VariantType extends UmbVariantModel = UmbVariantModel> {
variant?: VariantType;
language: UmbLanguageDetailModel;
/**
* The unique identifier is a VariantId string.
*/
unique: string;
}

View File

@@ -1,32 +1,45 @@
export type variantObject = {
export type UmbObjectWithVariantProperties = {
culture: string | null;
segment: string | null;
schedule?: { publishTime?: string | null; unpublishTime?: string | null };
};
export function variantPropertiesObjectToString(variant: UmbObjectWithVariantProperties): string {
// Currently a direct copy of the toString method of variantId.
return (variant.culture || UMB_INVARIANT_CULTURE) + (variant.segment ? `_${variant.segment}` : '');
}
export const UMB_INVARIANT_CULTURE = 'invariant';
/**
* An identifier representing a Variant. This is at current state a culture and a segment.
* The identifier is not specific for ContentType Variants, but is used for many type of identification of a culture and a segment. One case is any property of a ContentType can be resolved into a VariantId depending on their structural settings such as Vary by Culture and Vary by Segmentation.
*/
export class UmbVariantId {
public static Create(variantData: variantObject): UmbVariantId {
return Object.freeze(new UmbVariantId(variantData));
public static Create(variantData: UmbObjectWithVariantProperties): UmbVariantId {
return Object.freeze(new UmbVariantId(variantData.culture, variantData.segment));
}
public static CreateInvariant(): UmbVariantId {
return Object.freeze(new UmbVariantId({ culture: null, segment: null }));
return Object.freeze(new UmbVariantId(null, null));
}
public static FromString(str: string): UmbVariantId {
const split = str.split('_');
const culture = split[0] === UMB_INVARIANT_CULTURE ? null : split[0];
const segment = split[1] ?? null;
return Object.freeze(new UmbVariantId(segment, culture));
}
public readonly culture: string | null = null;
public readonly segment: string | null = null;
public readonly schedule: { publishTime?: string | null; unpublishTime?: string | null } | null = null;
constructor(variantData: variantObject) {
this.culture = (variantData.culture === UMB_INVARIANT_CULTURE ? null : variantData.culture) ?? null;
this.segment = variantData.segment ?? null;
this.schedule = variantData.schedule ?? null;
constructor(culture?: string | null, segment?: string | null) {
this.culture = (culture === UMB_INVARIANT_CULTURE ? null : culture?.toLowerCase()) ?? null;
this.segment = segment ?? null;
}
public compare(obj: variantObject): boolean {
return this.equal(new UmbVariantId(obj));
public compare(obj: UmbObjectWithVariantProperties): boolean {
return this.equal(new UmbVariantId(obj.culture, obj.segment));
}
public equal(variantId: UmbVariantId): boolean {
@@ -34,6 +47,7 @@ export class UmbVariantId {
}
public toString(): string {
// Currently a direct copy of the VariantPropertiesObjectToString method const.
return (this.culture || UMB_INVARIANT_CULTURE) + (this.segment ? `_${this.segment}` : '');
}
@@ -57,11 +71,12 @@ export class UmbVariantId {
return this.culture === null && this.segment === null;
}
public toObject(): variantObject {
public toObject(): UmbObjectWithVariantProperties {
return { culture: this.culture, segment: this.segment };
}
// TODO: needs localization option:
// TODO: Consider if this should be handled else where, it does not seem like the responsibility of this class, since it contains wordings:
public toDifferencesString(variantId: UmbVariantId): string {
let r = '';

View File

@@ -5,20 +5,22 @@ import {
UUIInputEvent,
type UUIPopoverContainerElement,
} from '@umbraco-cms/backoffice/external/uui';
import {
css,
html,
nothing,
customElement,
property,
state,
ifDefined,
query,
} from '@umbraco-cms/backoffice/external/lit';
import { css, html, nothing, customElement, state, query } from '@umbraco-cms/backoffice/external/lit';
import { UMB_WORKSPACE_SPLIT_VIEW_CONTEXT, type ActiveVariant } from '@umbraco-cms/backoffice/workspace';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbDocumentVariantModel } from '@umbraco-cms/backoffice/document';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbDocumentWorkspaceContext } from '@umbraco-cms/backoffice/document';
type UmbDocumentVariantOption = {
culture: string | null;
segment: string | null;
title: string;
displayName: string;
state: DocumentVariantStateModel;
};
type UmbDocumentVariantOptions = Array<UmbDocumentVariantOption>;
@customElement('umb-variant-selector')
export class UmbVariantSelectorElement extends UmbLitElement {
@@ -26,73 +28,75 @@ export class UmbVariantSelectorElement extends UmbLitElement {
private _popoverElement?: UUIPopoverContainerElement;
@state()
_variants: Array<UmbDocumentVariantModel> = [];
private _variants: UmbDocumentVariantOptions = [];
// TODO: Stop using document context specific ActiveVariant type.
@state()
_activeVariants: Array<ActiveVariant> = [];
@property({ attribute: false })
public get _activeVariantsCultures(): string[] {
return this._activeVariants.map((el) => el.culture ?? '') ?? [];
}
@state()
_activeVariantsCultures: string[] = [];
#splitViewContext?: typeof UMB_WORKSPACE_SPLIT_VIEW_CONTEXT.TYPE;
#variantContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE;
#datasetContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE;
@state()
private _name?: string;
private _culture?: string | null;
private _segment?: string | null;
@state()
private _variantDisplayName = '';
@state()
private _variantDisplayName?: string;
@state()
private _variantTitleName?: string;
private _variantTitleName = '';
@state()
private _variantSelectorOpen = false;
// TODO: make adapt to backoffice locale.
private _cultureNames = new Intl.DisplayNames('en', { type: 'language' });
constructor() {
super();
this.consumeContext(UMB_WORKSPACE_SPLIT_VIEW_CONTEXT, (instance) => {
this.#splitViewContext = instance;
this._observeVariants();
this._observeActiveVariants();
this.#observeVariants();
this.#observeActiveVariants();
this.#observeCurrentVariant();
});
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (instance) => {
this.#variantContext = instance;
this._observeVariantContext();
this.#datasetContext = instance;
this.#observeDatasetContext();
this.#observeCurrentVariant();
});
}
private async _observeVariants() {
async #observeVariants() {
if (!this.#splitViewContext) return;
const workspaceContext = this.#splitViewContext.getWorkspaceContext();
if (workspaceContext) {
this.observe(
workspaceContext.variants,
(variants) => {
if (variants) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: figure out what we do with the different variant models. Document has a state, but the variant model does not.
this._variants = variants;
}
},
'_observeVariants',
);
}
// NOTICE: This is dirty (the TypeScript casting), we can only accept doing this so far because we currently only use the Variant Selector on Document Workspace. [NL]
// This would need a refactor to enable the code below to work with different ContentTypes. Main problem here is the state, which is not generic for them all. [NL]
const workspaceContext = this.#splitViewContext.getWorkspaceContext() as UmbDocumentWorkspaceContext;
if (!workspaceContext) throw new Error('Split View Workspace context not found');
this.observe(
workspaceContext.variantOptions,
(options) => {
this._variants = options.map<UmbDocumentVariantOption>((option) => {
const name = option.variant?.name ?? option.language.name;
const segment = option.variant?.segment ?? null;
return {
// Notice the option object has a unique property, but it's not used here. (Its equivalent to a UmbVariantId string) [NL]
culture: option.language.unique,
segment: segment,
title: name + (segment ? `${segment}` : ''),
displayName: name + (segment ? `${segment}` : ''),
state: option.variant?.state ?? DocumentVariantStateModel.NOT_CREATED,
};
});
},
'_observeVariants',
);
}
private async _observeActiveVariants() {
async #observeActiveVariants() {
if (!this.#splitViewContext) return;
const workspaceContext = this.#splitViewContext.getWorkspaceContext();
@@ -102,6 +106,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
(activeVariants) => {
if (activeVariants) {
this._activeVariants = activeVariants;
this._activeVariantsCultures = this._activeVariants.map((el) => el.culture ?? '') ?? [];
}
},
'_observeActiveVariants',
@@ -109,16 +114,10 @@ export class UmbVariantSelectorElement extends UmbLitElement {
}
}
private async _observeVariantContext() {
if (!this.#variantContext) return;
const variantId = this.#variantContext.getVariantId();
this._culture = variantId.culture;
this._segment = variantId.segment;
this.updateVariantDisplayName();
async #observeDatasetContext() {
if (!this.#datasetContext) return;
this.observe(
this.#variantContext.name,
this.#datasetContext.name,
(name) => {
this._name = name;
},
@@ -126,48 +125,62 @@ export class UmbVariantSelectorElement extends UmbLitElement {
);
}
private updateVariantDisplayName() {
if (!this._culture && !this._segment) return;
this._variantTitleName =
(this._culture ? this._cultureNames.of(this._culture) + ` (${this._culture})` : '') +
(this._segment ? ' — ' + this._segment : '');
this._variantDisplayName =
(this._culture ? this._cultureNames.of(this._culture) : '') + (this._segment ? ' — ' + this._segment : '');
async #observeCurrentVariant() {
if (!this.#datasetContext || !this.#splitViewContext) return;
const workspaceContext = this.#splitViewContext.getWorkspaceContext();
if (!workspaceContext) return;
const variantId = this.#datasetContext.getVariantId();
// Find the variant option matching this, to get the language name...
const culture = variantId.culture;
const segment = variantId.segment;
this.observe(
workspaceContext.variantOptions,
(options) => {
const option = options.find((option) => option.language.unique === culture);
const languageName = option?.language.name;
this._variantDisplayName = (languageName ? languageName : '') + (segment ? `${segment}` : '');
this._variantTitleName =
(languageName ? `${languageName} (${culture})` : '') + (segment ? `${segment}` : '');
},
'_currentLanguage',
);
}
// TODO: find a way where we don't have to do this for all workspaces.
private _handleInput(event: UUIInputEvent) {
#handleInput(event: UUIInputEvent) {
if (event instanceof UUIInputEvent) {
const target = event.composedPath()[0] as UUIInputElement;
if (
typeof target?.value === 'string' &&
this.#variantContext &&
isNameablePropertyDatasetContext(this.#variantContext)
this.#datasetContext &&
isNameablePropertyDatasetContext(this.#datasetContext)
) {
this.#variantContext.setName(target.value);
this.#datasetContext.setName(target.value);
}
}
}
private _switchVariant(variant: UmbDocumentVariantModel) {
#switchVariant(variant: UmbDocumentVariantOption) {
this.#splitViewContext?.switchVariant(UmbVariantId.Create(variant));
}
private _openSplitView(variant: UmbDocumentVariantModel) {
#openSplitView(variant: UmbDocumentVariantOption) {
this.#splitViewContext?.openSplitView(UmbVariantId.Create(variant));
}
private _closeSplitView() {
#closeSplitView() {
this.#splitViewContext?.closeSplitView();
}
private _isVariantActive(culture: string) {
return this._activeVariantsCultures.includes(culture);
#isVariantActive(culture: string | null) {
return culture !== null ? this._activeVariantsCultures.includes(culture) : true;
}
private _isNotPublishedMode(culture: string, state: DocumentVariantStateModel) {
return state !== DocumentVariantStateModel.PUBLISHED && !this._isVariantActive(culture!);
#isNotPublishedMode(culture: string | null, state: DocumentVariantStateModel) {
return state !== DocumentVariantStateModel.PUBLISHED && !this.#isVariantActive(culture);
}
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
@@ -182,26 +195,27 @@ export class UmbVariantSelectorElement extends UmbLitElement {
if (!isOpen) return;
const host = this.getBoundingClientRect();
// TODO: Ideally this is kept updated while open, but for now we just set it once:
this._popoverElement.style.width = `${host.width}px`;
}
render() {
return html`
<uui-input id="name-input" .value=${this._name} @input="${this._handleInput}">
<uui-input id="name-input" .value=${this._name ?? ''} @input="${this.#handleInput}">
${
this._variants && this._variants.length > 0
this._variants?.length
? html`
<uui-button
id="variant-selector-toggle"
slot="append"
popovertarget="variant-selector-popover"
title=${ifDefined(this._variantTitleName)}>
title=${this._variantTitleName}>
${this._variantDisplayName}
<uui-symbol-expand .open=${this._variantSelectorOpen}></uui-symbol-expand>
</uui-button>
${this._activeVariants.length > 1
? html`
<uui-button slot="append" compact id="variant-close" @click=${this._closeSplitView}>
<uui-button slot="append" compact id="variant-close" @click=${this.#closeSplitView}>
<uui-icon name="remove"></uui-icon>
</uui-button>
`
@@ -212,7 +226,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
</uui-input>
${
this._variants && this._variants.length > 0
this._variants?.length
? html`
<uui-popover-container
id="variant-selector-popover"
@@ -223,25 +237,26 @@ export class UmbVariantSelectorElement extends UmbLitElement {
<ul>
${this._variants.map(
(variant) => html`
<li class="${this._isVariantActive(variant.culture!) ? 'selected' : ''}">
<li class="${this.#isVariantActive(variant.culture) ? 'selected' : ''}">
<button
class="variant-selector-switch-button
${this._isNotPublishedMode(variant.culture!, variant.state!) ? 'add-mode' : ''}"
@click=${() => this._switchVariant(variant)}>
${this._isNotPublishedMode(variant.culture!, variant.state!)
${this.#isNotPublishedMode(variant.culture, variant.state) ? 'add-mode' : ''}"
@click=${() => this.#switchVariant(variant)}>
${this.#isNotPublishedMode(variant.culture, variant.state)
? html`<uui-icon class="add-icon" name="icon-add"></uui-icon>`
: nothing}
<div>
${variant.name} <i>(${variant.culture})</i> ${variant.segment}
${variant.title}
<i>(${variant.culture})</i> ${variant.segment}
<div class="variant-selector-state">${variant.state}</div>
</div>
</button>
${this._isVariantActive(variant.culture!)
${this.#isVariantActive(variant.culture)
? nothing
: html`
<uui-button
class="variant-selector-split-view"
@click=${() => this._openSplitView(variant)}>
@click=${() => this.#openSplitView(variant)}>
Split view
</uui-button>
`}
@@ -260,6 +275,7 @@ export class UmbVariantSelectorElement extends UmbLitElement {
}
static styles = [
UmbTextStyles,
css`
#name-input {
width: 100%;
@@ -331,12 +347,6 @@ export class UmbVariantSelectorElement extends UmbLitElement {
font-size: 14px;
cursor: pointer;
border-bottom: 1px solid var(--uui-color-divider-standalone);
font-family:
Lato,
Helvetica Neue,
Helvetica,
Arial,
sans-serif;
}
.variant-selector-switch-button:hover {

View File

@@ -2,16 +2,18 @@ import type { UmbWorkspaceSplitViewManager } from '../workspace-split-view-manag
import type { UmbPropertyDatasetContext } from '../../property/property-dataset/property-dataset-context.interface.js';
import type { UmbSaveableWorkspaceContextInterface } from './saveable-workspace-context.interface.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbVariantId, UmbVariantModel } from '@umbraco-cms/backoffice/variant';
import type { UmbVariantId, UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export interface UmbVariantableWorkspaceContextInterface extends UmbSaveableWorkspaceContextInterface {
export interface UmbVariantableWorkspaceContextInterface<VariantType extends UmbVariantModel = UmbVariantModel>
extends UmbSaveableWorkspaceContextInterface {
// Name:
getName(variantId?: UmbVariantId): string | undefined;
setName(name: string, variantId?: UmbVariantId): void;
// Variant:
variants: Observable<Array<UmbVariantModel>>;
variantOptions: Observable<Array<UmbVariantOptionModel<VariantType>>>;
splitView: UmbWorkspaceSplitViewManager;
getVariant(variantId: UmbVariantId): UmbVariantModel | undefined;

View File

@@ -25,7 +25,7 @@ export class UmbWorkspaceSplitViewManager {
}
setActiveVariant(index: number, culture: string | null, segment: string | null) {
this.#activeVariantsInfo.appendOne({ index, culture, segment });
this.#activeVariantsInfo.appendOne({ index, culture: culture || null, segment: segment || null });
}
getActiveVariants() {
@@ -53,7 +53,7 @@ export class UmbWorkspaceSplitViewManager {
const newVariants = [...activeVariants];
newVariants[index] = { index, culture: variantId.culture, segment: variantId.segment };
const variantPart: string = newVariants.map((v) => new UmbVariantId(v).toString()).join('_&_');
const variantPart: string = newVariants.map((v) => UmbVariantId.Create(v).toString()).join('_&_');
history.pushState(null, '', `${workspaceRoute}/${variantPart}`);
return true;
@@ -70,7 +70,7 @@ export class UmbWorkspaceSplitViewManager {
const currentVariant = this.getActiveVariants()[0];
const workspaceRoute = this.getWorkspaceRoute();
if (currentVariant && workspaceRoute) {
history.pushState(null, '', `${workspaceRoute}/${new UmbVariantId(currentVariant)}_&_${newVariant.toString()}`);
history.pushState(null, '', `${workspaceRoute}/${UmbVariantId.Create(currentVariant)}_&_${newVariant}`);
return true;
}
return false;
@@ -83,7 +83,7 @@ export class UmbWorkspaceSplitViewManager {
if (activeVariants && index < activeVariants.length) {
const newVariants = activeVariants.filter((x) => x.index !== index);
const variantPart: string = newVariants.map((v) => new UmbVariantId(v).toString()).join('_&_');
const variantPart: string = newVariants.map((v) => UmbVariantId.Create(v)).join('_&_');
history.pushState(null, '', `${workspaceRoute}/${variantPart}`);
return true;

View File

@@ -1,7 +1,6 @@
import type { UUIButtonState, UUIPaginationElement, UUIPaginationEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, nothing, customElement, state, query, property } from '@umbraco-cms/backoffice/external/lit';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { RedirectUrlResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { RedirectManagementResource, RedirectStatusModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -37,15 +36,6 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement {
@query('uui-pagination')
private _pagination?: UUIPaginationElement;
private _modalContext?: UmbModalManagerContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (_instance) => {
this._modalContext = _instance;
});
}
connectedCallback() {
super.connectedCallback();
this.#getTrackerStatus();
@@ -80,29 +70,23 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement {
}
// Delete Redirect Action
#onRequestDelete(data: RedirectUrlResponseModel) {
async #onRequestDelete(data: RedirectUrlResponseModel) {
if (!data.id) return;
const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: 'Delete',
content: html`
<div style="width:300px">
<p>${this.localize.term('redirectUrls_redirectRemoveWarning')}</p>
${this.localize.term('redirectUrls_originalUrl')}: <strong>${data.originalUrl}</strong><br />
${this.localize.term('redirectUrls_redirectedTo')}: <strong>${data.destinationUrl}</strong>
</div>
`,
color: 'danger',
confirmLabel: 'Delete',
},
await umbConfirmModal(this, {
headline: 'Delete',
content: html`
<div style="width:300px">
<p>${this.localize.term('redirectUrls_redirectRemoveWarning')}</p>
${this.localize.term('redirectUrls_originalUrl')}: <strong>${data.originalUrl}</strong><br />
${this.localize.term('redirectUrls_redirectedTo')}: <strong>${data.destinationUrl}</strong>
</div>
`,
color: 'danger',
confirmLabel: 'Delete',
});
modalContext
?.onSubmit()
.then(() => {
this.#redirectDelete(data.id!);
})
.catch(() => undefined);
this.#redirectDelete(data.id!);
}
async #redirectDelete(id: string) {
const { error } = await tryExecuteAndNotify(this, RedirectManagementResource.deleteRedirectManagementById({ id }));
@@ -125,26 +109,20 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement {
}
// Tracker disable/enable
#onRequestTrackerToggle() {
async #onRequestTrackerToggle() {
if (!this._trackerEnabled) {
this.#trackerToggle();
return;
}
const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: `${this.localize.term('redirectUrls_disableUrlTracker')}`,
content: `${this.localize.term('redirectUrls_confirmDisable')}`,
color: 'danger',
confirmLabel: 'Disable',
},
await umbConfirmModal(this, {
headline: `${this.localize.term('redirectUrls_disableUrlTracker')}`,
content: `${this.localize.term('redirectUrls_confirmDisable')}`,
color: 'danger',
confirmLabel: 'Disable',
});
modalContext
?.onSubmit()
.then(() => {
this.#trackerToggle();
})
.catch(() => undefined);
this.#trackerToggle();
}
async #trackerToggle() {

View File

@@ -2,13 +2,11 @@ import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type';
import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui';
import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal';
import {
UMB_CONFIRM_MODAL,
UMB_MODAL_MANAGER_CONTEXT,
UMB_PROPERTY_SETTINGS_MODAL,
UMB_WORKSPACE_MODAL,
UmbModalRouteRegistrationController,
umbConfirmModal,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { generateAlias } from '@umbraco-cms/backoffice/utils';
@@ -57,7 +55,6 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement {
#dataTypeDetailRepository = new UmbDataTypeDetailRepository(this);
#modalRegistration;
private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
@state()
protected _modalRoute?: string;
@@ -113,10 +110,6 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement {
.observeRouteBuilder((routeBuilder) => {
this._editDocumentTypePath = routeBuilder({});
});
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => {
this._modalManagerContext = context;
});
}
_partialUpdate(partialObject: UmbPropertyTypeModel) {
@@ -137,12 +130,12 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement {
this._aliasLocked = !this._aliasLocked;
}
#requestRemove(e: Event) {
async #requestRemove(e: Event) {
e.preventDefault();
e.stopImmediatePropagation();
if (!this.property || !this.property.id) return;
const modalData: UmbConfirmModalData = {
await umbConfirmModal(this, {
headline: `${this.localize.term('actions_delete')} property`,
content: html`<umb-localize key="contentTypeEditor_confirmDeletePropertyMessage" .args=${[
this.property.name || this.property.id,
@@ -152,19 +145,9 @@ export class UmbDocumentTypeWorkspacePropertyElement extends UmbLitElement {
</div>`,
confirmLabel: this.localize.term('actions_delete'),
color: 'danger',
};
});
const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, { data: modalData });
modalHandler
?.onSubmit()
.then(() => {
this.dispatchEvent(new CustomEvent('property-delete'));
})
.catch(() => {
// We do not need to react to cancel, so we will leave an empty method to prevent Uncaught Promise Rejection error.
return;
});
this.dispatchEvent(new CustomEvent('property-delete'));
}
#onNameChange(event: UUIInputEvent) {

View File

@@ -15,7 +15,7 @@ import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
@@ -99,8 +99,6 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple
private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper<UmbDocumentTypeDetailModel>(this);
private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
@state()
private _compositionConfiguration?: UmbCompositionPickerModalData;
@@ -154,10 +152,6 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple
this._observeRootGroups();
});
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => {
this._modalManagerContext = context;
});
}
private _observeRootGroups() {
@@ -220,7 +214,7 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple
this._routes = routes;
}
#requestRemoveTab(tab: PropertyTypeContainerModelBaseModel | undefined) {
async #requestRemoveTab(tab: PropertyTypeContainerModelBaseModel | undefined) {
const modalData: UmbConfirmModalData = {
headline: 'Delete tab',
content: html`<umb-localize key="contentTypeEditor_confirmDeleteTabMessage" .args=${[tab?.name ?? tab?.id]}>
@@ -237,11 +231,9 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple
// TODO: If this tab is composed of other tabs, then notify that it will only delete the local tab.
const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, { data: modalData });
await umbConfirmModal(this, modalData);
modalHandler?.onSubmit().then(() => {
this.#remove(tab?.id);
});
this.#remove(tab?.id);
}
#remove(tabId?: string) {
if (!tabId) return;
@@ -303,7 +295,8 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple
}
async #openCompositionModal() {
const modalContext = this._modalManagerContext?.open(UMB_COMPOSITION_PICKER_MODAL, {
const modalManagerContext = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const modalContext = modalManagerContext.open(UMB_COMPOSITION_PICKER_MODAL, {
data: this._compositionConfiguration,
});
await modalContext?.onSubmit();

View File

@@ -19,9 +19,9 @@ export class UmbDocumentWorkspaceHasCollectionCondition extends UmbBaseControlle
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => {
this.observe(
context.contentTypeCollection,
(collection) => {
this.permitted = !!collection?.unique;
context.contentTypeHasCollection,
(hasCollection) => {
this.permitted = hasCollection;
this.#onChange();
},
'observeCollection',

View File

@@ -1,21 +1,50 @@
import { UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT } from '../global-contexts/index.js';
import type { UmbDocumentPublishingRepository } from '../repository/index.js';
import { umbPickDocumentVariantModal } from '../modals/pick-document-variant-modal.controller.js';
import { type UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '../repository/index.js';
import { UmbDocumentVariantState } from '../types.js';
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbPublishDocumentEntityAction extends UmbEntityActionBase<UmbDocumentPublishingRepository> {
#variantManagerContext?: typeof UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT.TYPE;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT, (context) => {
this.#variantManagerContext = context;
});
}
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
export class UmbPublishDocumentEntityAction extends UmbEntityActionBase<UmbDocumentDetailRepository> {
async execute() {
if (!this.#variantManagerContext) throw new Error('Variant manager context is missing');
await this.#variantManagerContext.publish(this.unique);
if (!this.repository) throw new Error('Document repository not set');
const languageRepository = new UmbLanguageCollectionRepository(this._host);
const { data: languageData } = await languageRepository.requestCollection({});
const { data: documentData } = await this.repository.requestByUnique(this.unique);
if (!documentData) throw new Error('The document was not found');
// If the document has only one variant, we can skip the modal and publish directly:
if (documentData.variants.length === 1) {
const variantId = UmbVariantId.Create(documentData.variants[0]);
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.publish(this.unique, [variantId]);
return;
}
const allOptions = (languageData?.items ?? []).map((language) => ({
language: language,
variant: documentData.variants.find((variant) => variant.culture === language.unique),
unique: new UmbVariantId(language.unique, null).toString(),
}));
// TODO: Maybe move this to modal [NL]
// Only display variants that are relevant to pick from, i.e. variants that are draft or published with pending changes:
const options = allOptions.filter(
(option) =>
option.variant &&
(option.variant.state === UmbDocumentVariantState.DRAFT ||
option.variant.state === UmbDocumentVariantState.PUBLISHED ||
option.variant.state === UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES),
);
// TODO: Missing features to pre-select the variant that fits with the variant-id of the tree/collection? (Again only relevant if the action is executed from a Tree or Collection) [NL]
const selectedVariants = await umbPickDocumentVariantModal(this, { type: 'publish', options });
if (selectedVariants.length) {
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.publish(this.unique, selectedVariants);
}
}
}

View File

@@ -1,21 +1,41 @@
import { UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT } from '../global-contexts/index.js';
import type { UmbDocumentPublishingRepository } from '../repository/index.js';
import { umbPickDocumentVariantModal } from '../modals/pick-document-variant-modal.controller.js';
import { type UmbDocumentDetailRepository, UmbDocumentPublishingRepository } from '../repository/index.js';
import { UmbDocumentVariantState } from '../types.js';
import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language';
import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase<UmbDocumentPublishingRepository> {
#variantManagerContext?: typeof UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT.TYPE;
constructor(host: UmbControllerHostElement, repositoryAlias: string, unique: string, entityType: string) {
super(host, repositoryAlias, unique, entityType);
this.consumeContext(UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT, (context) => {
this.#variantManagerContext = context;
});
}
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
export class UmbUnpublishDocumentEntityAction extends UmbEntityActionBase<UmbDocumentDetailRepository> {
async execute() {
if (!this.#variantManagerContext) throw new Error('Variant manager context is missing');
await this.#variantManagerContext.unpublish(this.unique);
if (!this.repository) throw new Error('Document repository not set');
const languageRepository = new UmbLanguageCollectionRepository(this._host);
const { data: languageData } = await languageRepository.requestCollection({});
const { data: documentData } = await this.repository.requestByUnique(this.unique);
if (!documentData) throw new Error('The document was not found');
const allOptions = (languageData?.items ?? []).map((language) => ({
language: language,
variant: documentData.variants.find((variant) => variant.culture === language.unique),
unique: new UmbVariantId(language.unique, null).toString(),
}));
// TODO: Maybe move this to modal [NL]
// Only display variants that are relevant to pick from, i.e. variants that are published or published with pending changes:
const options = allOptions.filter(
(option) =>
option.variant &&
(option.variant.state === UmbDocumentVariantState.PUBLISHED ||
option.variant.state === UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES),
);
// TODO: Missing features to pre-select the variant that fits with the variant-id of the tree/collection? (Again only relevant if the action is executed from a Tree or Collection) [NL]
const selectedVariants = await umbPickDocumentVariantModal(this, { type: 'unpublish', options });
if (selectedVariants.length) {
const publishingRepository = new UmbDocumentPublishingRepository(this._host);
await publishingRepository.unpublish(this.unique, selectedVariants);
}
}
}

View File

@@ -1,121 +0,0 @@
import { UmbDocumentVariantState, type UmbDocumentVariantModel } from '../types.js';
import { UmbDocumentDetailRepository } from '../repository/detail/document-detail.repository.js';
import {
UMB_DOCUMENT_LANGUAGE_PICKER_MODAL,
type UmbDocumentVariantPickerModalData,
} from '../modals/variant-picker/index.js';
import { UmbDocumentPublishingRepository } from '../repository/publishing/index.js';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
export class UmbDocumentVariantManagerContext
extends UmbContextBase<UmbDocumentVariantManagerContext>
implements UmbApi
{
#publishingRepository = new UmbDocumentPublishingRepository(this);
#documentRepository = new UmbDocumentDetailRepository(this);
#modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
#appLanguageCulture?: string;
constructor(host: UmbControllerHost) {
super(host, UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT);
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalManagerContext = instance;
});
this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, (appLanguageContext) => {
this.observe(appLanguageContext.appLanguageCulture, (culture) => {
this.#appLanguageCulture = culture?.toLowerCase();
});
});
}
/**
* Helps the user pick variants for a specific operation.
* If there is only one variant, it will be selected automatically.
* If there are multiple variants, a modal will be shown to the user.
* @param type The type of operation to perform.
* @param documentUnique The unique identifier of the document.
* @param activeVariantCulture The culture of the active variant (will be pre-selected in the modal).
* @param filterFn Optional filter function to filter the available variants.
* @returns The selected variants to perform the operation on.
*/
async pickVariants(
availableVariants: Array<UmbDocumentVariantModel>,
type: UmbDocumentVariantPickerModalData['type'],
activeVariantCulture?: string,
): Promise<UmbVariantId[]> {
// If there is only one variant, we don't need to select anything.
if (availableVariants.length === 1) {
return [UmbVariantId.Create(availableVariants[0])];
}
if (!this.#modalManagerContext) throw new Error('Modal manager context is missing');
const modalData: UmbDocumentVariantPickerModalData = {
type,
variants: availableVariants,
};
const modalContext = this.#modalManagerContext.open(UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, {
data: modalData,
value: { selection: activeVariantCulture ? [activeVariantCulture] : [] },
});
const result = await modalContext.onSubmit().catch(() => undefined);
if (!result?.selection.length) return [];
const selectedVariants = result.selection.map((x) => x?.toLowerCase() ?? '');
// Match the result to the available variants.
const variantIds = availableVariants
.filter((x) => selectedVariants.includes(x.culture!))
.map((x) => UmbVariantId.Create(x));
return variantIds;
}
/**
* Publish the latest version of a document indescriminately.
* @param documentUnique The unique identifier of the document.
*/
async publish(documentUnique: string) {
const { data } = await this.#documentRepository.requestByUnique(documentUnique);
if (!data) throw new Error('Document not found');
const variantIds = await this.pickVariants(data.variants, 'publish', this.#appLanguageCulture);
if (variantIds.length) {
await this.#publishingRepository.publish(documentUnique, variantIds);
}
}
/**
* Unpublish the latest version of a document indescriminately.
* @param documentUnique The unique identifier of the document.
*/
async unpublish(documentUnique: string) {
const { data } = await this.#documentRepository.requestByUnique(documentUnique);
if (!data) throw new Error('Document not found');
// Only show published variants
const variants = data.variants.filter((variant) => variant.state === UmbDocumentVariantState.PUBLISHED);
const variantIds = await this.pickVariants(variants, 'unpublish', this.#appLanguageCulture);
if (variantIds.length) {
await this.#publishingRepository.unpublish(documentUnique, variantIds);
}
}
}
export default UmbDocumentVariantManagerContext;
export const UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT = new UmbContextToken<UmbDocumentVariantManagerContext>(
'UmbDocumentVariantManagerContext',
);

View File

@@ -1 +0,0 @@
export * from './document-variant-manager.context.js';

View File

@@ -1,10 +0,0 @@
import type { ManifestGlobalContext } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestGlobalContext> = [
{
type: 'globalContext',
alias: 'Umb.GlobalContext.DocumentVariantManager',
name: 'Document Variant Manager Context',
js: () => import('./document-variant-manager.context.js'),
},
];

View File

@@ -12,7 +12,6 @@ import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as treeManifests } from './tree/manifests.js';
import { manifests as userPermissionManifests } from './user-permissions/manifests.js';
import { manifests as workspaceManifests } from './workspace/manifests.js';
import { manifests as globalContextManifests } from './global-contexts/manifests.js';
export const manifests = [
...breadcrumbManifests,
@@ -29,5 +28,4 @@ export const manifests = [
...treeManifests,
...userPermissionManifests,
...workspaceManifests,
...globalContextManifests,
];

View File

@@ -1 +1,2 @@
export * from './variant-picker/index.js';
export * from './pick-document-variant-modal.controller.js';

View File

@@ -0,0 +1,50 @@
import type { UmbDocumentVariantOptionModel } from '../types.js';
import {
UMB_DOCUMENT_LANGUAGE_PICKER_MODAL,
type UmbDocumentVariantPickerModalData,
type UmbDocumentVariantPickerModalType,
} from './variant-picker/document-variant-picker-modal.token.js';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export interface UmbPickDocumentVariantModalArgs {
type: UmbDocumentVariantPickerModalType;
options: Array<UmbDocumentVariantOptionModel>;
selected?: Array<UmbVariantId>;
}
export class UmbPickDocumentVariantModalController extends UmbBaseController {
async open(args: UmbPickDocumentVariantModalArgs): Promise<UmbVariantId[]> {
const modalManagerContext = await this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, () => {}).asPromise();
const selected = args.selected ?? [];
const modalData: UmbDocumentVariantPickerModalData = {
type: args.type,
options: args.options,
};
if (modalData.options.length === 0) {
// TODO: What do to when there is no options?
}
const modalContext = modalManagerContext.open(UMB_DOCUMENT_LANGUAGE_PICKER_MODAL, {
data: modalData,
// We need to turn the selected variant ids into strings for them to be serializable to the value state, in other words the value of a modal cannot hold class instances:
value: { selection: selected.map((x) => x.toString()) ?? [] },
});
const result = await modalContext.onSubmit().catch(() => undefined);
// This is a one time off, so we can destroy our selfs.
this.destroy();
// Map back into UmbVariantId instances:
return result?.selection.map((x) => UmbVariantId.FromString(x)) ?? [];
}
}
export function umbPickDocumentVariantModal(host: UmbControllerHost, args: UmbPickDocumentVariantModalArgs) {
return new UmbPickDocumentVariantModalController(host).open(args);
}

View File

@@ -1,36 +1,66 @@
import { type UmbDocumentVariantModel, UmbDocumentVariantState } from '../../types.js';
import { UmbDocumentVariantState, type UmbDocumentVariantOptionModel } from '../../types.js';
import type {
UmbDocumentVariantPickerModalValue,
UmbDocumentVariantPickerModalData,
} from './document-variant-picker-modal.token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, repeat } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { appendToFrozenArray } from '@umbraco-cms/backoffice/observable-api';
@customElement('umb-document-variant-picker-modal')
export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement<
UmbDocumentVariantPickerModalData,
UmbDocumentVariantPickerModalValue
> {
#selectionManager = new UmbSelectionManager(this);
#selectionManager = new UmbSelectionManager<string>(this);
@state()
_selection: Array<string> = [];
constructor() {
super();
this.observe(this.#selectionManager.selection, (selection) => {
this._selection = selection;
});
}
connectedCallback(): void {
super.connectedCallback();
this.#selectionManager.setSelectable(true);
this.#selectionManager.setMultiple(true);
this.#setInitialSelection();
}
async #setInitialSelection() {
let selected = this.value?.selection ?? [];
if (selected.length === 0) {
// TODO: Make it possible to use consume context without callback. [NL]
const ctrl = this.consumeContext(UMB_APP_LANGUAGE_CONTEXT, () => {});
const context = await ctrl.asPromise();
const appCulture = context.getAppCulture();
// If the app language is one of the options, select it by default:
if (appCulture && this.data?.options.some((o) => o.language.unique === appCulture)) {
selected = appendToFrozenArray(selected, new UmbVariantId(appCulture, null).toString());
}
ctrl.destroy();
}
this.#selectionManager.setMultiple(true);
this.#selectionManager.setSelectable(true);
this.#selectionManager.setSelection(selected);
// Make sure all mandatory variants are selected when not in unpublish mode
this.#selectionManager.setSelection(this.value?.selection ?? []);
if (this.data?.type !== 'unpublish') {
this.#selectMandatoryVariants();
}
}
#selectMandatoryVariants() {
this.data?.variants.forEach((variant) => {
if (variant.isMandatory) {
this.#selectionManager.select(variant.culture);
this.data?.options.forEach((variant) => {
if (variant.language?.isMandatory) {
this.#selectionManager.select(variant.unique);
}
});
}
@@ -87,17 +117,17 @@ export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement<
return html`<umb-body-layout headline=${this.localize.term(this.#headline)}>
<p id="subtitle">${this.localize.term(this.#subtitle)}</p>
${repeat(
this.data?.variants ?? [],
(item) => item.culture,
(item) => html`
this.data?.options ?? [],
(option) => option.unique,
(option) => html`
<uui-menu-item
selectable
label=${item.name}
@selected=${() => this.#selectionManager.select(item.culture)}
@deselected=${() => this.#selectionManager.deselect(item.culture)}
?selected=${this.#selectionManager.isSelected(item.culture)}>
label=${option.variant?.name ?? option.language.name}
@selected=${() => this.#selectionManager.select(option.unique)}
@deselected=${() => this.#selectionManager.deselect(option.unique)}
?selected=${this._selection.includes(option.language.unique)}>
<uui-icon slot="icon" name="icon-globe"></uui-icon>
${this.#renderLabel(item)}
${this.#renderLabel(option)}
</uui-menu-item>
`,
)}
@@ -114,11 +144,14 @@ export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement<
</umb-body-layout> `;
}
#renderLabel(variant: UmbDocumentVariantModel) {
#renderLabel(option: UmbDocumentVariantOptionModel) {
return html`<div class="label" slot="label">
<strong>${variant.segment ? variant.segment + ' - ' : ''}${variant.name}</strong>
<div class="label-status">${this.#renderVariantStatus(variant)}</div>
${variant.isMandatory && variant.state !== UmbDocumentVariantState.PUBLISHED
<strong
>${option.variant?.segment ? option.variant.segment + ' - ' : ''}${option.variant?.name ??
option.language.name}</strong
>
<div class="label-status">${this.#renderVariantStatus(option)}</div>
${option.language.isMandatory && option.variant?.state !== UmbDocumentVariantState.PUBLISHED
? html`<div class="label-status">
<umb-localize key="languages_mandatoryLanguage">Mandatory language</umb-localize>
</div>`
@@ -126,16 +159,17 @@ export class UmbDocumentVariantPickerModalElement extends UmbModalBaseElement<
</div>`;
}
#renderVariantStatus(variant: UmbDocumentVariantModel) {
switch (variant.state) {
#renderVariantStatus(option: UmbDocumentVariantOptionModel) {
switch (option.variant?.state) {
case UmbDocumentVariantState.PUBLISHED:
return this.localize.term('content_published');
case UmbDocumentVariantState.PUBLISHED_PENDING_CHANGES:
return this.localize.term('content_publishedPendingChanges');
case UmbDocumentVariantState.NOT_CREATED:
case UmbDocumentVariantState.DRAFT:
default:
return this.localize.term('content_unpublished');
case UmbDocumentVariantState.NOT_CREATED:
default:
return this.localize.term('content_notCreated');
}
}

View File

@@ -12,17 +12,29 @@ import { html } from '@umbraco-cms/backoffice/external/lit';
const modalData: UmbDocumentVariantPickerModalData = {
type: 'save',
variants: [
options: [
{
name: 'English',
culture: 'en-us',
state: UmbDocumentVariantState.PUBLISHED,
createDate: '2021-08-25T14:00:00Z',
publishDate: null,
updateDate: null,
segment: null,
isMandatory: true,
unique: 'en-us',
variant: {
name: 'English variant name',
culture: 'en-us',
state: UmbDocumentVariantState.PUBLISHED,
createDate: '2021-08-25T14:00:00Z',
publishDate: null,
updateDate: null,
segment: null,
},
language: {
entityType: 'language',
name: 'English',
unique: 'en-us',
isDefault: true,
isMandatory: true,
fallbackIsoCode: null,
},
},
/*
// TODO: We do not support segments currently
{
name: 'English',
culture: 'en-us',
@@ -31,17 +43,27 @@ const modalData: UmbDocumentVariantPickerModalData = {
publishDate: null,
updateDate: null,
segment: 'GTM',
isMandatory: true,
},
*/
{
name: 'Danish',
culture: 'da-dk',
state: UmbDocumentVariantState.NOT_CREATED,
createDate: null,
publishDate: null,
updateDate: null,
segment: null,
isMandatory: false,
unique: 'da-dk',
variant: {
name: 'Danish variant name',
culture: 'da-dk',
state: UmbDocumentVariantState.NOT_CREATED,
createDate: null,
publishDate: null,
updateDate: null,
segment: null,
},
language: {
entityType: 'language',
name: 'Danish',
unique: 'da-dk',
isDefault: false,
isMandatory: false,
fallbackIsoCode: null,
},
},
],
};
@@ -80,7 +102,6 @@ this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => {
publishDate: '2021-08-25T14:00:00Z',
updateDate: null,
segment: null,
isMandatory: true,
},
{
name: 'English',
@@ -90,7 +111,6 @@ this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => {
publishDate: '2021-08-25T14:00:00Z',
updateDate: null,
segment: 'GTM',
isMandatory: false,
},
{
name: 'Danish',
@@ -100,7 +120,6 @@ this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (modalManager) => {
publishDate: null,
updateDate: null,
segment: null,
isMandatory: false,
},
],
}

View File

@@ -1,14 +1,16 @@
import { UMB_DOCUMENT_VARIANT_PICKER_MODAL_ALIAS } from '../manifests.js';
import type { UmbDocumentVariantModel } from '../../types.js';
import type { UmbDocumentVariantOptionModel } from '../../types.js';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export type UmbDocumentVariantPickerModalType = 'save' | 'publish' | 'schedule' | 'unpublish';
export interface UmbDocumentVariantPickerModalData {
type: 'save' | 'publish' | 'schedule' | 'unpublish';
variants: Array<UmbDocumentVariantModel>;
type: UmbDocumentVariantPickerModalType;
options: Array<UmbDocumentVariantOptionModel>;
}
export interface UmbDocumentVariantPickerModalValue {
selection: Array<string | null>;
selection: Array<string>;
}
export const UMB_DOCUMENT_LANGUAGE_PICKER_MODAL = new UmbModalToken<

View File

@@ -1,4 +1,4 @@
import type { UmbDocumentDetailModel } from '../../types.js';
import type { UmbDocumentDetailModel, UmbDocumentVariantModel } from '../../types.js';
import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository';
@@ -47,24 +47,32 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
},
isTrashed: false,
values: [],
variants: [
{
state: null,
culture: null,
segment: null,
name: '',
publishDate: null,
createDate: null,
updateDate: null,
isMandatory: false,
},
],
variants: [],
...preset,
};
return { data };
}
/**
* Creates a new variant scaffold.
* @returns A new variant scaffold.
*/
/*
// TDOD: remove if not used
createVariantScaffold(): UmbDocumentVariantModel {
return {
state: null,
culture: null,
segment: null,
name: '',
publishDate: null,
createDate: null,
updateDate: null,
};
}
*/
/**
* Fetches a Document with the given id from the server
* @param {string} unique
@@ -102,7 +110,6 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource<UmbDocum
publishDate: variant.publishDate || null,
createDate: variant.createDate,
updateDate: variant.updateDate,
isMandatory: false, // TODO: this is not correct. It will be solved when we know where to get the isMandatory from
};
}),
urls: data.urls.map((url) => {

View File

@@ -40,7 +40,8 @@ export class UmbDocumentPublishingServerDataSource {
(variant) => {
return {
culture: variant.isCultureInvariant() ? null : variant.toCultureString(),
schedule: variant.schedule,
// TODO: NO, this does not belong as part of the UmbVariantID, we need another way to parse that around:
//schedule: variant.schedule,
};
},
);

View File

@@ -1,5 +1,5 @@
import type { UmbDocumentEntityType } from './entity.js';
import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant';
import type { UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant';
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
import { DocumentVariantStateModel as UmbDocumentVariantState } from '@umbraco-cms/backoffice/external/backend-api';
export { UmbDocumentVariantState };
@@ -22,7 +22,6 @@ export interface UmbDocumentDetailModel {
export interface UmbDocumentVariantModel extends UmbVariantModel {
state: UmbDocumentVariantState | null;
publishDate: string | null;
isMandatory: boolean;
}
export interface UmbDocumentUrlInfoModel {
@@ -36,3 +35,5 @@ export interface UmbDocumentValueModel<ValueType = unknown> {
alias: string;
value: ValueType;
}
export interface UmbDocumentVariantOptionModel extends UmbVariantOptionModel<UmbDocumentVariantModel> {}

View File

@@ -1,62 +1,36 @@
import type { UmbDocumentVariantOptionModel } from '../types.js';
import { UmbDocumentWorkspaceSplitViewElement } from './document-workspace-split-view.element.js';
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from './document-workspace.context-token.js';
import { customElement, state, css, html } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { ActiveVariant } from '@umbraco-cms/backoffice/workspace';
import type { UmbRoute, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant';
// TODO: This seem fully identical with Media Workspace Editor, so we can refactor this to a generic component. [NL]
@customElement('umb-document-workspace-editor')
export class UmbDocumentWorkspaceEditorElement extends UmbLitElement {
//private _defaultVariant?: VariantViewModelBaseModel;
// TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well.
//
// TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well. [NL]
private splitViewElement = new UmbDocumentWorkspaceSplitViewElement();
#workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE;
@state()
_routes?: Array<UmbRoute>;
@state()
_availableVariants: Array<UmbVariantModel> = [];
@state()
_workspaceSplitViews: Array<ActiveVariant> = [];
#workspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (instance) => {
this.#workspaceContext = instance;
this.#observeVariants();
this.#observeSplitViews();
});
}
#observeVariants() {
if (!this.#workspaceContext) return;
this.observe(
this.#workspaceContext.variants,
(variants) => {
this._availableVariants = variants;
this._generateRoutes();
},
'_observeVariants',
);
}
#observeSplitViews() {
if (!this.#workspaceContext) return;
this.observe(
this.#workspaceContext.splitView.activeVariantsInfo,
(variants) => {
this._workspaceSplitViews = variants;
},
'_observeSplitViews',
);
// TODO: the variantOptions observable is like too broad as this will be triggered then there is any change in the variant options, we need to only update routes when there is a relevant change to them. [NL]
this.observe(this.#workspaceContext.variantOptions, (options) => this._generateRoutes(options), '_observeVariants');
}
private _handleVariantFolderPart(index: number, folderPart: string) {
@@ -66,17 +40,18 @@ export class UmbDocumentWorkspaceEditorElement extends UmbLitElement {
this.#workspaceContext?.splitView.setActiveVariant(index, culture, segment);
}
private _generateRoutes() {
if (!this._availableVariants || this._availableVariants.length === 0) return;
private async _generateRoutes(options: Array<UmbDocumentVariantOptionModel>) {
if (!options || options.length === 0) return;
// Generate split view routes for all available routes
const routes: Array<UmbRoute> = [];
// Split view routes:
this._availableVariants.forEach((variantA) => {
this._availableVariants.forEach((variantB) => {
options.forEach((variantA) => {
options.forEach((variantB) => {
routes.push({
path: new UmbVariantId(variantA).toString() + '_&_' + new UmbVariantId(variantB).toString(),
// TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL]
path: variantA.unique + '_&_' + variantB.unique,
component: this.splitViewElement,
setup: (_component, info) => {
// Set split view/active info..
@@ -90,9 +65,10 @@ export class UmbDocumentWorkspaceEditorElement extends UmbLitElement {
});
// Single view:
this._availableVariants.forEach((variant) => {
options.forEach((variant) => {
routes.push({
path: new UmbVariantId(variant).toString(),
// TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL]
path: variant.unique,
component: this.splitViewElement,
setup: (_component, info) => {
// cause we might come from a split-view, we need to reset index 1.
@@ -106,11 +82,21 @@ export class UmbDocumentWorkspaceEditorElement extends UmbLitElement {
// Using first single view as the default route for now (hence the math below):
routes.push({
path: '',
redirectTo: routes[this._availableVariants.length * this._availableVariants.length]?.path,
redirectTo: routes[options.length * options.length]?.path,
});
}
const oldValue = this._routes;
// is there any differences in the amount ot the paths? [NL]
// TODO: if we make a memorization function as the observer, we can avoid this check and avoid the whole build of routes. [NL]
if (oldValue && oldValue.length === routes.length) {
// is there any differences in the paths? [NL]
const hasDifferences = oldValue.some((route, index) => route.path !== routes[index].path);
if (!hasDifferences) return;
}
this._routes = routes;
this.requestUpdate('_routes', oldValue);
}
private _gotWorkspaceRoute = (e: UmbRouterSlotInitEvent) => {

View File

@@ -2,12 +2,12 @@ import { UmbDocumentTypeDetailRepository } from '../../document-types/repository
import { UmbDocumentPropertyDataContext } from '../property-dataset-context/document-property-dataset-context.js';
import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js';
import { UmbDocumentDetailRepository } from '../repository/index.js';
import type { UmbDocumentDetailModel } from '../types.js';
import type { UmbDocumentVariantPickerModalData } from '../modals/index.js';
import type { UmbDocumentDetailModel, UmbDocumentVariantModel, UmbDocumentVariantOptionModel } from '../types.js';
import { umbPickDocumentVariantModal, type UmbDocumentVariantPickerModalType } from '../modals/index.js';
import { UmbDocumentPublishingRepository } from '../repository/publishing/index.js';
import { UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT } from '../global-contexts/document-variant-manager.context.js';
import { UmbUnpublishDocumentEntityAction } from '../entity-actions/unpublish.action.js';
import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './manifests.js';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type';
import {
UmbEditableWorkspaceContextBase,
@@ -15,13 +15,21 @@ import {
type UmbVariantableWorkspaceContextInterface,
type UmbPublishableWorkspaceContextInterface,
} from '@umbraco-cms/backoffice/workspace';
import { appendToFrozenArray, partialUpdateFrozenArray, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import {
appendToFrozenArray,
mergeObservables,
naiveObjectComparison,
UmbArrayState,
UmbObjectState,
} from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
type EntityType = UmbDocumentDetailModel;
export class UmbDocumentWorkspaceContext
extends UmbEditableWorkspaceContextBase<EntityType>
implements UmbVariantableWorkspaceContextInterface, UmbPublishableWorkspaceContextInterface
implements UmbVariantableWorkspaceContextInterface<UmbDocumentVariantModel>, UmbPublishableWorkspaceContextInterface
{
//
public readonly repository = new UmbDocumentDetailRepository(this);
@@ -30,8 +38,14 @@ export class UmbDocumentWorkspaceContext
/**
* The document is the current state/draft version of the document.
*/
#persistedData = new UmbObjectState<EntityType | undefined>(undefined);
#currentData = new UmbObjectState<EntityType | undefined>(undefined);
#getDataPromise?: Promise<any>;
// TODo: Optimize this so it uses either a App Language Context? [NL]
#languageRepository = new UmbLanguageCollectionRepository(this);
#languages = new UmbArrayState<UmbLanguageDetailModel>([], (x) => x.unique);
public readonly languages = this.#languages.asObservable();
public isLoaded() {
return this.#getDataPromise;
}
@@ -39,29 +53,37 @@ export class UmbDocumentWorkspaceContext
readonly unique = this.#currentData.asObservablePart((data) => data?.unique);
readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.documentType.unique);
readonly contentTypeCollection = this.#currentData.asObservablePart((data) => data?.documentType.collection);
readonly contentTypeHasCollection = this.#currentData.asObservablePart((data) => !!data?.documentType.collection);
readonly variants = this.#currentData.asObservablePart((data) => data?.variants ?? []);
readonly variantOptions = mergeObservables([this.variants, this.languages], ([variants, languages]) => {
return languages.map((language) => {
return {
variant: variants.find((x) => x.culture === language.unique),
language,
// TODO: When including segments, this should be updated to include the segment as well. [NL]
unique: language.unique, // This must be a variantId string!
} as UmbDocumentVariantOptionModel;
});
});
readonly variants = this.#currentData.asObservablePart((data) => data?.variants || []);
readonly urls = this.#currentData.asObservablePart((data) => data?.urls || []);
readonly templateId = this.#currentData.asObservablePart((data) => data?.template?.unique || null);
readonly structure = new UmbContentTypePropertyStructureManager(this, new UmbDocumentTypeDetailRepository(this));
readonly splitView = new UmbWorkspaceSplitViewManager();
#variantManagerContext?: typeof UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT.TYPE;
constructor(host: UmbControllerHost) {
super(host, UMB_DOCUMENT_WORKSPACE_ALIAS);
this.consumeContext(UMB_DOCUMENT_VARIANT_MANAGER_CONTEXT, (instance) => {
this.#variantManagerContext = instance;
});
this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique));
/*
TODO: Make something to ensure all variants are present in data? Seems like a good idea?.
*/
this.loadLanguages();
}
async loadLanguages() {
// TODO: If we don't end up having a Global Context for languages, then we should at least change this into using a asObservable which should be returned from the repository. [Nl]
const { data } = await this.#languageRepository.requestCollection({});
this.#languages.setValue(data?.items ?? []);
}
async load(unique: string) {
@@ -70,7 +92,7 @@ export class UmbDocumentWorkspaceContext
if (!data) return undefined;
this.setIsNew(false);
//this.#persisted.next(data);
this.#persistedData.setValue(data);
this.#currentData.setValue(data);
return data || undefined;
}
@@ -86,6 +108,7 @@ export class UmbDocumentWorkspaceContext
if (!data) return undefined;
this.setIsNew(true);
this.#persistedData.setValue(undefined);
this.#currentData.setValue(data);
return data || undefined;
}
@@ -125,6 +148,7 @@ export class UmbDocumentWorkspaceContext
}
setName(name: string, variantId?: UmbVariantId) {
/*
const oldVariants = this.#currentData.getValue()?.variants || [];
const variants = partialUpdateFrozenArray(
oldVariants,
@@ -132,6 +156,9 @@ export class UmbDocumentWorkspaceContext
variantId ? (x) => variantId.compare(x) : () => true,
);
this.#currentData.update({ variants });
*/
// TODO: We should move this type of logic to the act of saving [NL]
this.#updateVariantData(variantId ?? UmbVariantId.CreateInvariant(), { name });
}
setTemplate(templateUnique: string) {
@@ -171,7 +198,7 @@ export class UmbDocumentWorkspaceContext
value: UmbDocumentValueModel,
variantId?: UmbVariantId,
) {
if (!variantId) throw new Error('VariantId is missing');
variantId ??= UmbVariantId.CreateInvariant();
const entry = { ...variantId.toObject(), alias, value };
const currentData = this.getData();
@@ -182,27 +209,95 @@ export class UmbDocumentWorkspaceContext
(x) => x.alias === alias && (variantId ? variantId.compare(x) : true),
);
this.#currentData.update({ values });
// TODO: We should move this type of logic to the act of saving [NL]
this.#updateVariantData(variantId);
}
}
async #createOrSave(type: UmbDocumentVariantPickerModalData['type']): Promise<UmbVariantId[]> {
const data = this.getData();
if (!data) throw new Error('Data is missing');
if (!data.unique) throw new Error('Unique is missing');
if (!this.#variantManagerContext) throw new Error('Variant manager context is missing');
#calculateChangedVariants() {
const persisted = this.#persistedData.getValue();
const current = this.#currentData.getValue();
if (!current) throw new Error('Current data is missing');
const activeVariants = this.splitView.getActiveVariants();
const activeVariant = activeVariants.length ? activeVariants[0] : undefined;
const changedVariants = current?.variants.map((variant) => {
const persistedVariant = persisted?.variants.find((x) => UmbVariantId.Create(variant).compare(x));
return {
culture: variant.culture,
segment: variant.segment,
equal: persistedVariant ? naiveObjectComparison(variant, persistedVariant) : false,
};
});
const selectedVariants = await this.#variantManagerContext.pickVariants(
data.variants, // TODO: Add a filter function to only show variants that have been changed
type,
activeVariant?.culture ?? undefined,
const changedProperties = current?.values.map((value) => {
const persistedValues = persisted?.values.find((x) => UmbVariantId.Create(value).compare(x));
return {
culture: value.culture,
segment: value.segment,
equal: persistedValues ? naiveObjectComparison(value, persistedValues) : false,
};
});
// calculate the variantIds of those who either have a change in properties or in variants:
return (
changedVariants
?.concat(changedProperties ?? [])
.filter((x) => x.equal === false)
.map((x) => new UmbVariantId(x.culture, x.segment)) ?? []
);
}
#updateVariantData(variantId: UmbVariantId, update?: Partial<UmbDocumentVariantModel>) {
const currentData = this.getData();
if (!currentData) throw new Error('Data is missing');
const variant = currentData.variants.find((x) => variantId.compare(x));
const newVariants = appendToFrozenArray(
currentData.variants,
{
state: null,
name: '',
publishDate: null,
createDate: null,
updateDate: null,
...variantId.toObject(),
...variant,
...update,
},
(x) => variantId.compare(x),
);
this.#currentData.update({ variants: newVariants });
}
async #pickVariantsForAction(type: UmbDocumentVariantPickerModalType): Promise<UmbVariantId[]> {
const activeVariants = this.splitView.getActiveVariants();
// TODO: Picked variants should include the ones that has been changed (but not jet saved) this requires some more awareness about the state of runtime data. [NL]
const activeVariantIds = activeVariants.map((activeVariant) => UmbVariantId.Create(activeVariant));
const selected = activeVariantIds.concat(this.#calculateChangedVariants());
const options = await firstValueFrom(this.variantOptions);
// If there is only one variant, we don't need to open the modal.
if (options.length === 0) {
throw new Error('No variants are available');
} else if (options.length === 1) {
// If only one option we will skip ahead and save the document with the only variant available:
const firstVariant = new UmbVariantId(options[0].language.unique, null);
return await this.#performSaveOrCreate([firstVariant]);
}
const selectedVariants = await umbPickDocumentVariantModal(this, { type, options, selected });
// If no variants are selected, we don't save anything.
if (!selectedVariants.length) return [];
return await this.#performSaveOrCreate(selectedVariants);
}
async #performSaveOrCreate(selectedVariants: Array<UmbVariantId>) {
const data = this.getData();
if (!data) throw new Error('Data is missing');
if (!data.unique) throw new Error('Unique is missing');
if (this.getIsNew()) {
if ((await this.repository.create(data)).data !== undefined) {
this.setIsNew(false);
@@ -215,14 +310,18 @@ export class UmbDocumentWorkspaceContext
}
async save() {
await this.#createOrSave('save');
await this.#pickVariantsForAction('save');
const data = this.getData();
if (!data) throw new Error('Data is missing');
this.#persistedData.setValue(data);
this.#currentData.setValue(data);
this.saveComplete(data);
}
public async publish() {
const variantIds = await this.#createOrSave('publish');
const variantIds = await this.#pickVariantsForAction('publish');
const unique = this.getEntityId();
if (variantIds.length && unique) {
await this.publishingRepository.publish(unique, variantIds);
@@ -237,9 +336,7 @@ export class UmbDocumentWorkspaceContext
const unique = this.getEntityId();
if (!unique) throw new Error('Unique is missing');
if (!this.#variantManagerContext) throw new Error('Variant manager context is missing');
this.#variantManagerContext.unpublish(unique);
new UmbUnpublishDocumentEntityAction(this, '', unique, '').execute();
}
async delete() {

View File

@@ -13,6 +13,10 @@ export class UmbAppLanguageContext extends UmbBaseController implements UmbApi {
appLanguage = this.#appLanguage.asObservable();
appLanguageCulture = this.#appLanguage.asObservablePart((x) => x?.unique);
getAppCulture() {
return this.#appLanguage.getValue()?.unique;
}
constructor(host: UmbControllerHost) {
super(host);
this.provideContext(UMB_APP_LANGUAGE_CONTEXT, this);
@@ -26,7 +30,7 @@ export class UmbAppLanguageContext extends UmbBaseController implements UmbApi {
}
async #observeLanguages() {
const { data } = await this.#languageCollectionRepository.requestCollection({ skip: 0, take: 100 });
const { data } = await this.#languageCollectionRepository.requestCollection({});
// TODO: make this observable / update when languages are added/removed/updated
if (data) {

View File

@@ -65,11 +65,11 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource<UmbLangu
// TODO: make data mapper to prevent errors
const dataType: UmbLanguageDetailModel = {
entityType: UMB_LANGUAGE_ENTITY_TYPE,
fallbackIsoCode: data.fallbackIsoCode || null,
fallbackIsoCode: data.fallbackIsoCode?.toLowerCase() || null,
isDefault: data.isDefault,
isMandatory: data.isMandatory,
name: data.name,
unique: data.isoCode,
unique: data.isoCode.toLowerCase(),
};
return { data: dataType };
@@ -86,10 +86,10 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource<UmbLangu
// TODO: make data mapper to prevent errors
const requestBody: CreateLanguageRequestModel = {
fallbackIsoCode: model.fallbackIsoCode,
fallbackIsoCode: model.fallbackIsoCode?.toLowerCase(),
isDefault: model.isDefault,
isMandatory: model.isMandatory,
isoCode: model.unique,
isoCode: model.unique.toLowerCase(),
name: model.name,
};
@@ -118,7 +118,7 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource<UmbLangu
// TODO: make data mapper to prevent errors
const requestBody: LanguageModelBaseModel = {
fallbackIsoCode: model.fallbackIsoCode,
fallbackIsoCode: model.fallbackIsoCode?.toLowerCase(),
isDefault: model.isDefault,
isMandatory: model.isMandatory,
name: model.name,
@@ -127,7 +127,7 @@ export class UmbLanguageServerDataSource implements UmbDetailDataSource<UmbLangu
const { error } = await tryExecuteAndNotify(
this.#host,
LanguageResource.putLanguageByIsoCode({
isoCode: model.unique,
isoCode: model.unique.toLowerCase(),
requestBody,
}),
);

View File

@@ -7,7 +7,7 @@ import type { SavedLogSearchResponseModel } from '@umbraco-cms/backoffice/extern
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { query as getQuery, path, toQueryString } from '@umbraco-cms/backoffice/router';
import type { UmbModalManagerContext, UmbModalContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UmbModalToken, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UmbModalToken, umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import './log-viewer-search-input-modal.element.js';
import type { UmbDropdownElement } from '@umbraco-cms/backoffice/components';
@@ -126,20 +126,16 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement {
this.#logViewerContext?.saveSearch(savedSearch);
}
#removeSearch(name: string) {
const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: this.localize.term('logViewer_deleteSavedSearch'),
content: `${this.localize.term('defaultdialogs_confirmdelete')} ${name}?`,
color: 'danger',
confirmLabel: 'Delete',
},
async #removeSearch(name: string) {
await umbConfirmModal(this, {
headline: this.localize.term('logViewer_deleteSavedSearch'),
content: `${this.localize.term('defaultdialogs_confirmdelete')} ${name}?`,
color: 'danger',
confirmLabel: 'Delete',
});
modalContext?.onSubmit().then(() => {
this.#logViewerContext?.removeSearch({ name });
//this.dispatchEvent(new UmbDeleteEvent());
});
this.#logViewerContext?.removeSearch({ name });
//this.dispatchEvent(new UmbDeleteEvent());
}
#openSaveSearchDialog() {

View File

@@ -4,11 +4,10 @@ import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit';
import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal';
import {
UMB_CONFIRM_MODAL,
UMB_MODAL_MANAGER_CONTEXT,
UMB_PROPERTY_SETTINGS_MODAL,
UMB_WORKSPACE_MODAL,
UmbModalRouteRegistrationController,
umbConfirmModal,
} from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { generateAlias } from '@umbraco-cms/backoffice/utils';
@@ -57,7 +56,6 @@ export class UmbMediaTypeWorkspacePropertyElement extends UmbLitElement {
#dataTypeDetailRepository = new UmbDataTypeDetailRepository(this);
#modalRegistration;
private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
@state()
protected _modalRoute?: string;
@@ -113,10 +111,6 @@ export class UmbMediaTypeWorkspacePropertyElement extends UmbLitElement {
.observeRouteBuilder((routeBuilder) => {
this._editMediaTypePath = routeBuilder({});
});
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => {
this._modalManagerContext = context;
});
}
_partialUpdate(partialObject: UmbPropertyTypeModel) {
@@ -137,7 +131,7 @@ export class UmbMediaTypeWorkspacePropertyElement extends UmbLitElement {
this._aliasLocked = !this._aliasLocked;
}
#requestRemove(e: Event) {
async #requestRemove(e: Event) {
e.preventDefault();
e.stopImmediatePropagation();
if (!this.property || !this.property.id) return;
@@ -154,17 +148,9 @@ export class UmbMediaTypeWorkspacePropertyElement extends UmbLitElement {
color: 'danger',
};
const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, { data: modalData });
await umbConfirmModal(this, modalData);
modalHandler
?.onSubmit()
.then(() => {
this.dispatchEvent(new CustomEvent('property-delete'));
})
.catch(() => {
// We do not need to react to cancel, so we will leave an empty method to prevent Uncaught Promise Rejection error.
return;
});
this.dispatchEvent(new CustomEvent('property-delete'));
}
#onNameChange(event: UUIInputEvent) {

View File

@@ -11,7 +11,7 @@ import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal';
import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
@@ -89,8 +89,6 @@ export class UmbMediaTypeWorkspaceViewEditElement extends UmbLitElement implemen
private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper<UmbMediaTypeDetailModel>(this);
private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE;
constructor() {
super();
this.sorter = new UmbSorterController(this, this.config);
@@ -116,10 +114,6 @@ export class UmbMediaTypeWorkspaceViewEditElement extends UmbLitElement implemen
);
this._observeRootGroups();
});
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => {
this._modalManagerContext = context;
});
}
private _observeRootGroups() {
@@ -188,7 +182,7 @@ export class UmbMediaTypeWorkspaceViewEditElement extends UmbLitElement implemen
this._routes = routes;
}
#requestRemoveTab(tab: PropertyTypeContainerModelBaseModel | undefined) {
async #requestRemoveTab(tab: PropertyTypeContainerModelBaseModel | undefined) {
const modalData: UmbConfirmModalData = {
headline: 'Delete tab',
content: html`<umb-localize key="contentTypeEditor_confirmDeleteTabMessage" .args=${[tab?.name ?? tab?.id]}>
@@ -204,12 +198,9 @@ export class UmbMediaTypeWorkspaceViewEditElement extends UmbLitElement implemen
};
// TODO: If this tab is composed of other tabs, then notify that it will only delete the local tab.
await umbConfirmModal(this, modalData);
const modalHandler = this._modalManagerContext?.open(UMB_CONFIRM_MODAL, { data: modalData });
modalHandler?.onSubmit().then(() => {
this.#remove(tab?.id);
});
this.#remove(tab?.id);
}
#remove(tabId?: string) {
if (!tabId) return;

View File

@@ -1,7 +1,7 @@
import type { UmbMediaEntityType } from './entity.js';
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant';
import type { UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant';
import type { MediaUrlInfoModel, MediaValueModel } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
export interface UmbMediaDetailModel {
mediaType: {
@@ -16,3 +16,5 @@ export interface UmbMediaDetailModel {
values: Array<MediaValueModel>;
variants: Array<UmbVariantModel>;
}
export interface UmbMediaVariantOptionModel extends UmbVariantOptionModel<UmbVariantModel> {}

View File

@@ -1,28 +1,19 @@
import type { UmbMediaVariantOptionModel } from '../types.js';
import { UmbMediaWorkspaceSplitViewElement } from './media-workspace-split-view.element.js';
import { UMB_MEDIA_WORKSPACE_CONTEXT } from './media-workspace.context-token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { customElement, state, css, html } from '@umbraco-cms/backoffice/external/lit';
import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbRoute, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router';
import type { ActiveVariant } from '@umbraco-cms/backoffice/workspace';
@customElement('umb-media-workspace-editor')
export class UmbMediaWorkspaceEditorElement extends UmbLitElement {
//private _defaultVariant?: VariantViewModelBaseModel;
// TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well.
//
// TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well. [NL]
private splitViewElement = new UmbMediaWorkspaceSplitViewElement();
@state()
_routes?: Array<UmbRoute>;
@state()
_availableVariants: Array<UmbVariantModel> = [];
@state()
_workspaceSplitViews: Array<ActiveVariant> = [];
#workspaceContext?: typeof UMB_MEDIA_WORKSPACE_CONTEXT.TYPE;
constructor() {
@@ -31,31 +22,12 @@ export class UmbMediaWorkspaceEditorElement extends UmbLitElement {
this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (instance) => {
this.#workspaceContext = instance;
this.#observeVariants();
this.#observeSplitViews();
});
}
#observeVariants() {
if (!this.#workspaceContext) return;
this.observe(
this.#workspaceContext.variants,
(variants) => {
this._availableVariants = variants;
this._generateRoutes();
},
'_observeVariants',
);
}
#observeSplitViews() {
if (!this.#workspaceContext) return;
this.observe(
this.#workspaceContext.splitView.activeVariantsInfo,
(variants) => {
this._workspaceSplitViews = variants;
},
'_observeSplitViews',
);
this.observe(this.#workspaceContext.variantOptions, (options) => this._generateRoutes(options), '_observeVariants');
}
private _handleVariantFolderPart(index: number, folderPart: string) {
@@ -65,17 +37,18 @@ export class UmbMediaWorkspaceEditorElement extends UmbLitElement {
this.#workspaceContext?.splitView.setActiveVariant(index, culture, segment);
}
private _generateRoutes() {
if (!this._availableVariants || this._availableVariants.length === 0) return;
private async _generateRoutes(variants: Array<UmbMediaVariantOptionModel>) {
if (!variants || variants.length === 0) return;
// Generate split view routes for all available routes
const routes: Array<UmbRoute> = [];
// Split view routes:
this._availableVariants.forEach((variantA) => {
this._availableVariants.forEach((variantB) => {
variants.forEach((variantA) => {
variants.forEach((variantB) => {
routes.push({
path: new UmbVariantId(variantA).toString() + '_&_' + new UmbVariantId(variantB).toString(),
// TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL]
path: variantA.unique + '_&_' + variantB.unique,
component: this.splitViewElement,
setup: (_component, info) => {
// Set split view/active info..
@@ -89,9 +62,10 @@ export class UmbMediaWorkspaceEditorElement extends UmbLitElement {
});
// Single view:
this._availableVariants.forEach((variant) => {
variants.forEach((variant) => {
routes.push({
path: new UmbVariantId(variant).toString(),
// TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL]
path: variant.unique,
component: this.splitViewElement,
setup: (_component, info) => {
// cause we might come from a split-view, we need to reset index 1.
@@ -105,11 +79,21 @@ export class UmbMediaWorkspaceEditorElement extends UmbLitElement {
// Using first single view as the default route for now (hence the math below):
routes.push({
path: '',
redirectTo: routes[this._availableVariants.length * this._availableVariants.length]?.path,
redirectTo: routes[variants.length * variants.length]?.path,
});
}
const oldValue = this._routes;
// is there any differences in the amount ot the paths? [NL]
// TODO: if we make a memorization function as the observer, we can avoid this check and avoid the whole build of routes. [NL]
if (oldValue && oldValue.length === routes.length) {
// is there any differences in the paths? [NL]
const hasDifferences = oldValue.some((route, index) => route.path !== routes[index].path);
if (!hasDifferences) return;
}
this._routes = routes;
this.requestUpdate('_routes', oldValue);
}
private _gotWorkspaceRoute = (e: UmbRouterSlotInitEvent) => {

View File

@@ -2,7 +2,7 @@ import { UmbMediaTypeDetailRepository } from '../../media-types/repository/detai
import { UmbMediaPropertyDataContext } from '../property-dataset-context/media-property-dataset-context.js';
import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js';
import { UmbMediaDetailRepository } from '../repository/index.js';
import type { UmbMediaDetailModel } from '../types.js';
import type { UmbMediaDetailModel, UmbMediaVariantOptionModel } from '../types.js';
import type { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type';
import {
@@ -10,8 +10,15 @@ import {
UmbWorkspaceSplitViewManager,
type UmbVariantableWorkspaceContextInterface,
} from '@umbraco-cms/backoffice/workspace';
import { appendToFrozenArray, partialUpdateFrozenArray, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import {
appendToFrozenArray,
mergeObservables,
partialUpdateFrozenArray,
UmbArrayState,
UmbObjectState,
} from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language';
type EntityType = UmbMediaDetailModel;
export class UmbMediaWorkspaceContext
@@ -26,6 +33,11 @@ export class UmbMediaWorkspaceContext
*/
#currentData = new UmbObjectState<EntityType | undefined>(undefined);
#getDataPromise?: Promise<any>;
// TODo: Optimize this so it uses either a App Language Context? [NL]
#languageRepository = new UmbLanguageCollectionRepository(this);
#languages = new UmbArrayState<UmbLanguageDetailModel>([], (x) => x.unique);
public readonly languages = this.#languages.asObservable();
public isLoaded() {
return this.#getDataPromise;
}
@@ -35,6 +47,16 @@ export class UmbMediaWorkspaceContext
readonly contentTypeCollection = this.#currentData.asObservablePart((data) => data?.mediaType.collection);
readonly variants = this.#currentData.asObservablePart((data) => data?.variants || []);
readonly variantOptions = mergeObservables([this.variants, this.languages], ([variants, languages]) => {
return languages.map((language) => {
return {
variant: variants.find((x) => x.culture === language.unique),
language,
// TODO: When including segments, this should be updated to include the segment as well. [NL]
unique: language.unique, // This must be a variantId string!
} as UmbMediaVariantOptionModel;
});
});
readonly urls = this.#currentData.asObservablePart((data) => data?.urls || []);
readonly structure = new UmbContentTypePropertyStructureManager(this, new UmbMediaTypeDetailRepository(this));
@@ -47,6 +69,11 @@ export class UmbMediaWorkspaceContext
this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique));
}
async loadLanguages() {
const { data } = await this.#languageRepository.requestCollection({});
this.#languages.setValue(data?.items ?? []);
}
async load(unique: string) {
this.#getDataPromise = this.repository.requestByUnique(unique);
const { data } = await this.#getDataPromise;

View File

@@ -4,8 +4,7 @@ import type { PackageDefinitionResponseModel } from '@umbraco-cms/backoffice/ext
import { PackageResource } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
@customElement('umb-packages-created-overview')
export class UmbPackagesCreatedOverviewElement extends UmbLitElement {
@@ -23,19 +22,10 @@ export class UmbPackagesCreatedOverviewElement extends UmbLitElement {
@state()
private _total?: number;
private _modalContext?: UmbModalManagerContext;
constructor() {
super();
}
connectedCallback(): void {
super.connectedCallback();
this.#getPackages();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this._modalContext = instance;
});
}
async #getPackages() {
@@ -106,17 +96,13 @@ export class UmbPackagesCreatedOverviewElement extends UmbLitElement {
async #deletePackage(p: PackageDefinitionResponseModel) {
if (!p.id) return;
const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
color: 'danger',
headline: `Remove ${p.name}?`,
content: 'Are you sure you want to delete this package',
confirmLabel: 'Delete',
},
await umbConfirmModal(this, {
color: 'danger',
headline: `Remove ${p.name}?`,
content: 'Are you sure you want to delete this package',
confirmLabel: 'Delete',
});
await modalContext?.onSubmit();
const { error } = await tryExecuteAndNotify(this, PackageResource.deletePackageCreatedById({ id: p.id }));
if (error) return;
const index = this._createdPackages.findIndex((x) => x.id === p.id);

View File

@@ -1,8 +1,7 @@
import { html, css, nothing, ifDefined, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
import type { ManifestPackageView } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -43,7 +42,6 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement {
private _packageView?: ManifestPackageView;
#notificationContext?: UmbNotificationContext;
#modalContext?: UmbModalManagerContext;
constructor() {
super();
@@ -51,9 +49,6 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement {
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => {
this.#notificationContext = instance;
});
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;
});
}
#observePackageView() {
@@ -76,16 +71,13 @@ export class UmbInstalledPackagesSectionViewItemElement extends UmbLitElement {
async _onMigration() {
if (!this.name) return;
const modalContext = this.#modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
color: 'positive',
headline: `Run migrations for ${this.name}?`,
content: `Do you want to start run migrations for ${this.name}`,
confirmLabel: 'Run migrations',
},
});
await modalContext?.onSubmit();
await umbConfirmModal(this, {
color: 'positive',
headline: `Run migrations for ${this.name}?`,
content: `Do you want to start run migrations for ${this.name}`,
confirmLabel: 'Run migrations',
});
this._migrationButtonState = 'waiting';
const { error } = await tryExecuteAndNotify(

View File

@@ -1,7 +1,6 @@
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
import { css, html, nothing, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import type { IndexResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { HealthStatusModel, IndexerResource } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -24,16 +23,6 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
@state()
private _loading = true;
private _modalContext?: UmbModalManagerContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (_instance) => {
this._modalContext = _instance;
});
}
connectedCallback() {
super.connectedCallback();
this._getIndexData();
@@ -55,22 +44,19 @@ export class UmbDashboardExamineIndexElement extends UmbLitElement {
}
private async _onRebuildHandler() {
const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: `Rebuild ${this.indexName}`,
content: html`
This will cause the index to be rebuilt.<br />
Depending on how much content there is in your site this could take a while.<br />
It is not recommended to rebuild an index during times of high website traffic or when editors are editing
content.
`,
color: 'danger',
confirmLabel: 'Rebuild',
},
});
modalContext?.onSubmit().then(() => {
this._rebuild();
await umbConfirmModal(this, {
headline: `Rebuild ${this.indexName}`,
content: html`
This will cause the index to be rebuilt.<br />
Depending on how much content there is in your site this could take a while.<br />
It is not recommended to rebuild an index during times of high website traffic or when editors are editing
content.
`,
color: 'danger',
confirmLabel: 'Rebuild',
});
this._rebuild();
}
private async _rebuild() {
this._buttonState = 'waiting';

View File

@@ -1,7 +1,6 @@
import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
import { PublishedCacheResource } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -24,16 +23,6 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement {
@state()
private _buttonStateCollect: UUIButtonState = undefined;
private _modalContext?: UmbModalManagerContext;
constructor() {
super();
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this._modalContext = instance;
});
}
connectedCallback() {
super.connectedCallback();
this._getPublishedStatus();
@@ -68,17 +57,14 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement {
}
}
private async _onReloadCacheHandler() {
const modalContext = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: 'Reload',
content: html` Trigger a in-memory and local file cache reload on all servers.`,
color: 'danger',
confirmLabel: 'Continue',
},
});
modalContext?.onSubmit().then(() => {
this._reloadMemoryCache();
await umbConfirmModal(this, {
headline: 'Reload',
content: html` Trigger a in-memory and local file cache reload on all servers.`,
color: 'danger',
confirmLabel: 'Continue',
});
this._reloadMemoryCache();
}
// Rebuild
@@ -93,17 +79,14 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement {
}
private async _onRebuildCacheHandler() {
const modalContex = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: 'Rebuild',
content: html` Rebuild content in cmsContentNu database table. Expensive.`,
color: 'danger',
confirmLabel: 'Continue',
},
});
modalContex?.onSubmit().then(() => {
this._rebuildDatabaseCache();
await umbConfirmModal(this, {
headline: 'Rebuild',
content: html` Rebuild content in cmsContentNu database table. Expensive.`,
color: 'danger',
confirmLabel: 'Continue',
});
this._rebuildDatabaseCache();
}
//Collect
@@ -118,17 +101,13 @@ export class UmbDashboardPublishedStatusElement extends UmbLitElement {
}
private async _onSnapshotCacheHandler() {
const modalContex = this._modalContext?.open(UMB_CONFIRM_MODAL, {
data: {
headline: 'Snapshot',
content: html` Trigger a NuCache snapshots collection.`,
color: 'danger',
confirmLabel: 'Continue',
},
});
modalContex?.onSubmit().then(() => {
this._cacheCollect();
await umbConfirmModal(this, {
headline: 'Snapshot',
content: html` Trigger a NuCache snapshots collection.`,
color: 'danger',
confirmLabel: 'Continue',
});
this._cacheCollect();
}
render() {

View File

@@ -1,7 +1,7 @@
import { UMB_CURRENT_USER_MODAL } from './modals/current-user/current-user-modal.token.js';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -12,6 +12,9 @@ export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
@state()
private _currentUser?: UmbCurrentUserModel;
@state()
private _userAvatarUrls: Array<{ url: string; scale: string }> = [];
#currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
#modalManagerContext?: UmbModalManagerContext;
@@ -35,6 +38,8 @@ export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
this.#currentUserContext.currentUser,
(currentUser) => {
this._currentUser = currentUser;
if (!currentUser) return;
this.#setUserAvatarUrls(currentUser);
},
'umbCurrentUserObserver',
);
@@ -44,6 +49,41 @@ export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
this.#modalManagerContext?.open(UMB_CURRENT_USER_MODAL);
}
#setUserAvatarUrls = async (user: UmbCurrentUserModel | undefined) => {
if (!user || !user.avatarUrls || user.avatarUrls.length === 0) {
this._userAvatarUrls = [];
return;
}
this._userAvatarUrls = [
{
scale: '1x',
url: user.avatarUrls?.[0],
},
{
scale: '2x',
url: user.avatarUrls?.[1],
},
{
scale: '3x',
url: user.avatarUrls?.[2],
},
];
};
#getAvatarSrcset() {
let string = '';
this._userAvatarUrls?.forEach((url) => {
string += `${url.url} ${url.scale},`;
});
return string;
}
#hasAvatar() {
return this._userAvatarUrls.length > 0;
}
render() {
return html`
<uui-button
@@ -51,7 +91,11 @@ export class UmbCurrentUserHeaderAppElement extends UmbLitElement {
look="primary"
label="${this.localize.term('visuallyHiddenTexts_openCloseBackofficeProfileOptions')}"
compact>
<uui-avatar name="${this._currentUser?.name || 'Unknown'}"></uui-avatar>
<uui-avatar
id="Avatar"
.name=${this._currentUser?.name || 'Unknown'}
img-src=${ifDefined(this.#hasAvatar() ? this._userAvatarUrls[0].url : undefined)}
img-srcset=${ifDefined(this.#hasAvatar() ? this.#getAvatarSrcset() : undefined)}></uui-avatar>
</uui-button>
`;
}

View File

@@ -1,14 +1,14 @@
import type { UmbCurrentUserModel } from './types.js';
import { UmbCurrentUserRepository } from './repository/index.js';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbBaseController } from '@umbraco-cms/backoffice/class-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { umbLocalizationRegistry } from '@umbraco-cms/backoffice/localization';
export class UmbCurrentUserContext extends UmbBaseController {
export class UmbCurrentUserContext extends UmbContextBase<UmbCurrentUserContext> {
#currentUser = new UmbObjectState<UmbCurrentUserModel | undefined>(undefined);
readonly currentUser = this.#currentUser.asObservable();
@@ -18,7 +18,7 @@ export class UmbCurrentUserContext extends UmbBaseController {
#currentUserRepository = new UmbCurrentUserRepository(this);
constructor(host: UmbControllerHost) {
super(host);
super(host, UMB_CURRENT_USER_CONTEXT);
this.consumeContext(UMB_AUTH_CONTEXT, (instance) => {
this.#authContext = instance;
@@ -29,16 +29,18 @@ export class UmbCurrentUserContext extends UmbBaseController {
if (!currentLanguageIsoCode) return;
umbLocalizationRegistry.loadLanguage(currentLanguageIsoCode);
});
this.provideContext(UMB_CURRENT_USER_CONTEXT, this);
}
async requestCurrentUser() {
const { data } = await this.#currentUserRepository.requestCurrentUser();
/**
* Loads the current user
*/
async load() {
const { asObservable } = await this.#currentUserRepository.requestCurrentUser();
if (data) {
// TODO: observe current user
this.#currentUser.setValue(data);
if (asObservable) {
this.observe(asObservable(), (currentUser) => {
this.#currentUser?.setValue(currentUser);
});
}
}
@@ -57,7 +59,7 @@ export class UmbCurrentUserContext extends UmbBaseController {
if (!this.#authContext) return;
this.observe(this.#authContext.isAuthorized, (isAuthorized) => {
if (isAuthorized) {
this.requestCurrentUser();
this.load();
}
});
}

View File

@@ -2,8 +2,8 @@ import { html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-user-profile-app-external-login-providers')
export class UmbUserProfileAppExternalLoginProvidersElement extends UmbLitElement {
@customElement('umb-external-login-providers-user-profile-app')
export class UmbExternalLoginProvidersUserProfileAppElement extends UmbLitElement {
render() {
return html`
<uui-box>
@@ -16,10 +16,10 @@ export class UmbUserProfileAppExternalLoginProvidersElement extends UmbLitElemen
static styles = [UmbTextStyles];
}
export default UmbUserProfileAppExternalLoginProvidersElement;
export default UmbExternalLoginProvidersUserProfileAppElement;
declare global {
interface HTMLElementTagNameMap {
'umb-user-profile-app-external-login-providers': UmbUserProfileAppExternalLoginProvidersElement;
'umb-external-login-providers-user-profile-app': UmbExternalLoginProvidersUserProfileAppElement;
}
}

View File

@@ -0,0 +1,16 @@
import type { ManifestUserProfileApp } from '@umbraco-cms/backoffice/extension-registry';
export const userProfileApps: Array<ManifestUserProfileApp> = [
{
type: 'userProfileApp',
alias: 'Umb.UserProfileApp.CurrentUser.ExternalLoginProviders',
name: 'External Login Providers User Profile App',
element: () => import('./external-login-providers-user-profile-app.element.js'),
weight: 800,
meta: {
label: 'External Login Providers User Profile App',
pathname: 'externalLoginProviders',
},
},
];
export const manifests = [...userProfileApps];

View File

@@ -1,11 +1,11 @@
import type { UmbCurrentUserHistoryItem, UmbCurrentUserHistoryStore } from '../current-user-history.store.js';
import { UMB_CURRENT_USER_HISTORY_STORE_CONTEXT } from '../current-user-history.store.js';
import type { UmbCurrentUserHistoryItem, UmbCurrentUserHistoryStore } from './current-user-history.store.js';
import { UMB_CURRENT_USER_HISTORY_STORE_CONTEXT } from './current-user-history.store.js';
import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@customElement('umb-user-profile-app-history')
export class UmbUserProfileAppHistoryElement extends UmbLitElement {
@customElement('umb-current-user-history-user-profile-app')
export class UmbCurrentUserHistoryUserProfileAppElement extends UmbLitElement {
@state()
private _history: Array<UmbCurrentUserHistoryItem> = [];
@@ -109,10 +109,10 @@ export class UmbUserProfileAppHistoryElement extends UmbLitElement {
];
}
export default UmbUserProfileAppHistoryElement;
export default UmbCurrentUserHistoryUserProfileAppElement;
declare global {
interface HTMLElementTagNameMap {
'umb-user-dashboard-test': UmbUserProfileAppHistoryElement;
'umb-current-user-history-user-profile-app': UmbCurrentUserHistoryUserProfileAppElement;
}
}

View File

@@ -0,0 +1,22 @@
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const userProfileApps: Array<ManifestTypes> = [
{
type: 'userProfileApp',
alias: 'Umb.UserProfileApp.CurrentUser.History',
name: 'Current User History User Profile App',
element: () => import('../history/current-user-history-user-profile-app.element.js'),
weight: 100,
meta: {
label: 'History',
pathname: 'history',
},
},
{
type: 'store',
alias: 'Umb.Store.CurrentUser.History',
name: 'Current User History Store',
api: () => import('./current-user-history.store.js'),
},
];
export const manifests = [...userProfileApps];

View File

@@ -1,5 +1,4 @@
// TODO:Do not export store, but instead export future repository
export * from './current-user-history.store.js';
export * from './history/current-user-history.store.js';
export * from './utils/index.js';
export * from './current-user.context.js';
export * from './types.js';

View File

@@ -1,14 +1,12 @@
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as userProfileAppsManifests } from './user-profile-apps/manifests.js';
import { manifests as externalLoginProviderManifests } from './external-login/manifests.js';
import { manifests as historyManifests } from './history/manifests.js';
import { manifests as profileManifests } from './profile/manifests.js';
import { manifests as themeManifests } from './theme/manifests.js';
import { manifests as repositoryManifests } from './repository/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const headerApps: Array<ManifestTypes> = [
{
type: 'store',
alias: 'Umb.Store.CurrentUser',
name: 'Current User Store',
js: () => import('./current-user-history.store.js'),
},
{
type: 'globalContext',
alias: 'Umb.GlobalContext.CurrentUser',
@@ -19,7 +17,7 @@ export const headerApps: Array<ManifestTypes> = [
type: 'headerApp',
alias: 'Umb.HeaderApp.CurrentUser',
name: 'Current User',
js: () => import('./current-user-header-app.element.js'),
element: () => import('./current-user-header-app.element.js'),
weight: 0,
meta: {
label: 'TODO: how should we enable this to not be set.',
@@ -29,4 +27,12 @@ export const headerApps: Array<ManifestTypes> = [
},
];
export const manifests = [...headerApps, ...modalManifests, ...userProfileAppsManifests];
export const manifests = [
...externalLoginProviderManifests,
...headerApps,
...historyManifests,
...modalManifests,
...profileManifests,
...repositoryManifests,
...themeManifests,
];

View File

@@ -4,8 +4,8 @@ import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_CHANGE_PASSWORD_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import { UMB_CURRENT_USER_CONTEXT, type UmbCurrentUserModel } from '@umbraco-cms/backoffice/current-user';
@customElement('umb-user-profile-app-profile')
export class UmbUserProfileAppProfileElement extends UmbLitElement {
@customElement('umb-current-user-profile-user-profile-app')
export class UmbCurrentUserProfileUserProfileAppElement extends UmbLitElement {
@state()
private _currentUser?: UmbCurrentUserModel;
@@ -70,10 +70,10 @@ export class UmbUserProfileAppProfileElement extends UmbLitElement {
}
}
export default UmbUserProfileAppProfileElement;
export default UmbCurrentUserProfileUserProfileAppElement;
declare global {
interface HTMLElementTagNameMap {
'umb-user-profile-app-profile': UmbUserProfileAppProfileElement;
'umb-current-user-profile-user-profile-app': UmbCurrentUserProfileUserProfileAppElement;
}
}

View File

@@ -0,0 +1,16 @@
import type { ManifestUserProfileApp } from '@umbraco-cms/backoffice/extension-registry';
export const userProfileApps: Array<ManifestUserProfileApp> = [
{
type: 'userProfileApp',
alias: 'Umb.UserProfileApp.CurrentUser.Profile',
name: 'Current User Profile User Profile App',
element: () => import('./current-user-profile-user-profile-app.element.js'),
weight: 900,
meta: {
label: 'Current User Profile User Profile App',
pathname: 'profile',
},
},
];
export const manifests = [...userProfileApps];

View File

@@ -1,4 +1,5 @@
import { UmbCurrentUserServerDataSource } from './current-user.server.data-source.js';
import { UMB_CURRENT_USER_STORE_CONTEXT } from './current-user.store.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
@@ -10,11 +11,19 @@ import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
*/
export class UmbCurrentUserRepository extends UmbRepositoryBase {
#currentUserSource: UmbCurrentUserServerDataSource;
#currentUserStore?: typeof UMB_CURRENT_USER_STORE_CONTEXT.TYPE;
#init: Promise<unknown>;
constructor(host: UmbControllerHost) {
super(host);
this.#currentUserSource = new UmbCurrentUserServerDataSource(host);
this.#init = Promise.all([
this.consumeContext(UMB_CURRENT_USER_STORE_CONTEXT, (instance) => {
this.#currentUserStore = instance;
}).asPromise(),
]);
}
/**
@@ -23,8 +32,14 @@ export class UmbCurrentUserRepository extends UmbRepositoryBase {
* @memberof UmbCurrentUserRepository
*/
async requestCurrentUser() {
// TODO: add observable option
return this.#currentUserSource.getCurrentUser();
await this.#init;
const { data, error } = await this.#currentUserSource.getCurrentUser();
if (data) {
this.#currentUserStore?.set(data);
}
return { data, error, asObservable: () => this.#currentUserStore!.data };
}
}

View File

@@ -35,8 +35,8 @@ export class UmbCurrentUserServerDataSource {
userName: data.userName,
name: data.name,
languageIsoCode: data.languageIsoCode || 'en-us', // TODO: make global variable
documentStartNodeIds: data.documentStartNodeIds,
mediaStartNodeIds: data.mediaStartNodeIds,
documentStartNodeUniques: data.documentStartNodeIds,
mediaStartNodeUniques: data.mediaStartNodeIds,
avatarUrls: data.avatarUrls,
languages: data.languages,
hasAccessToAllLanguages: data.hasAccessToAllLanguages,

View File

@@ -0,0 +1,78 @@
import type { UmbCurrentUserModel } from '../types.js';
import type { UmbUserDetailModel } from '@umbraco-cms/backoffice/user';
import { UMB_USER_DETAIL_STORE_CONTEXT } from '@umbraco-cms/backoffice/user';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
export class UmbCurrentUserStore extends UmbContextBase<UmbCurrentUserStore> {
#data = new UmbObjectState<UmbCurrentUserModel | undefined>(undefined);
readonly data = this.#data.asObservable();
constructor(host: UmbControllerHostElement) {
super(host, UMB_CURRENT_USER_STORE_CONTEXT.toString());
this.consumeContext(UMB_USER_DETAIL_STORE_CONTEXT, (instance) => {
this.observe(instance?.all(), (users) => this.#onUserDetailStoreUpdate(users));
});
}
/**
* Get the current user
* @readonly
* @type {UmbCurrentUserModel}
* @memberof UmbCurrentUserStore
*/
get() {
return this.#data.getValue();
}
/**
* Set the current user
* @param {UmbCurrentUserModel} data
* @memberof UmbCurrentUserStore
*/
set(data: UmbCurrentUserModel) {
this.#data.setValue(data);
}
/**
* Update the current user
* @param {Partial<UmbCurrentUserModel>} data
* @memberof UmbCurrentUserStore
*/
update(data: Partial<UmbCurrentUserModel>) {
this.#data.update(data);
}
/**
* Clear the current user
* @memberof UmbCurrentUserStore
*/
clear() {
this.#data.setValue(undefined);
}
#onUserDetailStoreUpdate = (users: Array<UmbUserDetailModel>) => {
const currentUser = this.get();
if (!currentUser) return;
const updatedCurrentUser = users.find((user) => user.unique === currentUser.unique);
if (!updatedCurrentUser) return;
const mappedCurrentUser: Partial<UmbCurrentUserModel> = {
email: updatedCurrentUser.email,
userName: updatedCurrentUser.userName,
name: updatedCurrentUser.name,
languageIsoCode: updatedCurrentUser.languageIsoCode || '', // TODO: default value?
documentStartNodeUniques: updatedCurrentUser.documentStartNodeUniques,
mediaStartNodeUniques: updatedCurrentUser.mediaStartNodeUniques,
avatarUrls: updatedCurrentUser.avatarUrls,
};
this.update(mappedCurrentUser);
};
}
export const UMB_CURRENT_USER_STORE_CONTEXT = new UmbContextToken<UmbCurrentUserStore>('UmbCurrentUserStore');

View File

@@ -1,2 +1,3 @@
export { UmbCurrentUserRepository } from './current-user.repository.js';
export { UMB_CURRENT_USER_REPOSITORY_ALIAS } from './manifests.js';
export { UmbCurrentUserStore, UMB_CURRENT_USER_STORE_CONTEXT } from './current-user.store.js';

View File

@@ -1,12 +1,20 @@
import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
import { UmbCurrentUserStore } from './current-user.store.js';
import type { ManifestRepository, ManifestStore } from '@umbraco-cms/backoffice/extension-registry';
export const UMB_CURRENT_USER_REPOSITORY_ALIAS = 'Umb.Repository.CurrentUser';
const avatarRepository: ManifestRepository = {
const repository: ManifestRepository = {
type: 'repository',
alias: UMB_CURRENT_USER_REPOSITORY_ALIAS,
name: 'Current User Repository',
api: () => import('./current-user.repository.js'),
};
export const manifests = [avatarRepository];
const store: ManifestStore = {
type: 'store',
alias: 'Umb.Store.CurrentUser',
name: 'Current User Store',
api: UmbCurrentUserStore,
};
export const manifests = [repository, store];

View File

@@ -6,8 +6,8 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { ManifestTheme } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-user-profile-app-themes')
export class UmbUserProfileAppThemesElement extends UmbLitElement {
@customElement('umb-current-user-theme-user-profile-app')
export class UmbCurrentUserThemeUserProfileAppElement extends UmbLitElement {
#themeContext?: UmbThemeContext;
@state()
@@ -77,10 +77,10 @@ export class UmbUserProfileAppThemesElement extends UmbLitElement {
];
}
export default UmbUserProfileAppThemesElement;
export default UmbCurrentUserThemeUserProfileAppElement;
declare global {
interface HTMLElementTagNameMap {
'umb-user-profile-app-themes': UmbUserProfileAppThemesElement;
'umb-current-user-theme-user-profile-app': UmbCurrentUserThemeUserProfileAppElement;
}
}

View File

@@ -0,0 +1,16 @@
import type { ManifestUserProfileApp } from '@umbraco-cms/backoffice/extension-registry';
export const userProfileApps: Array<ManifestUserProfileApp> = [
{
type: 'userProfileApp',
alias: 'Umb.UserProfileApp.CurrentUser.Theme',
name: 'Current User Theme User Profile App',
element: () => import('./current-user-theme-user-profile-app.element.js'),
weight: 200,
meta: {
label: 'Current User Theme User Profile App',
pathname: 'themes',
},
},
];
export const manifests = [...userProfileApps];

View File

@@ -4,8 +4,8 @@ export interface UmbCurrentUserModel {
userName: string;
name: string;
languageIsoCode: string;
documentStartNodeIds: Array<string>;
mediaStartNodeIds: Array<string>;
documentStartNodeUniques: Array<string>;
mediaStartNodeUniques: Array<string>;
avatarUrls: Array<string>;
languages: Array<string>;
hasAccessToAllLanguages: boolean;

View File

@@ -1,49 +0,0 @@
import type { ManifestUserProfileApp } from '@umbraco-cms/backoffice/extension-registry';
export const userProfileApps: Array<ManifestUserProfileApp> = [
{
type: 'userProfileApp',
alias: 'Umb.UserProfileApp.profile',
name: 'Profile User Profile App',
js: () => import('./user-profile-app-profile.element.js'),
weight: 900,
meta: {
label: 'Profile User Profile App',
pathname: 'profile',
},
},
{
type: 'userProfileApp',
alias: 'Umb.UserProfileApp.ExternalLoginProviders',
name: 'External Login Providers User Profile App',
js: () => import('./user-profile-app-external-login-providers.element.js'),
weight: 800,
meta: {
label: 'External Login Providers User Profile App',
pathname: 'externalLoginProviders',
},
},
{
type: 'userProfileApp',
alias: 'Umb.UserProfileApp.Themes',
name: 'Themes User Profile App',
js: () => import('./user-profile-app-themes.element.js'),
weight: 200,
meta: {
label: 'Themes User Profile App',
pathname: 'themes',
},
},
{
type: 'userProfileApp',
alias: 'Umb.UserProfileApp.History',
name: 'History User Profile App',
js: () => import('./user-profile-app-history.element.js'),
weight: 100,
meta: {
label: 'History User Profile App',
pathname: 'history',
},
},
];
export const manifests = [...userProfileApps];

View File

@@ -1,36 +1,19 @@
import type { UmbUserGroupDetailRepository } from '../../repository/index.js';
import { html } from '@umbraco-cms/backoffice/external/lit';
import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextConsumerController } from '@umbraco-cms/backoffice/context-api';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UMB_CONFIRM_MODAL } from '@umbraco-cms/backoffice/modal';
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
export class UmbDeleteUserGroupEntityBulkAction extends UmbEntityBulkActionBase<UmbUserGroupDetailRepository> {
#modalContext?: UmbModalManagerContext;
constructor(host: UmbControllerHostElement, repositoryAlias: string, selection: Array<string>) {
super(host, repositoryAlias, selection);
new UmbContextConsumerController(host, UMB_MODAL_MANAGER_CONTEXT, (instance) => {
this.#modalContext = instance;
});
}
async execute() {
if (!this.#modalContext || this.selection.length === 0) return;
if (this.selection.length === 0) return;
const modalContext = this.#modalContext.open(UMB_CONFIRM_MODAL, {
data: {
color: 'danger',
headline: `Delete user groups?`,
content: html`Are you sure you want to delete selected user groups?`,
confirmLabel: 'Delete',
},
await umbConfirmModal(this._host, {
color: 'danger',
headline: `Delete user groups?`,
content: html`Are you sure you want to delete selected user groups?`,
confirmLabel: 'Delete',
});
await modalContext.onSubmit();
//TODO: How should we handle bulk actions? right now we send a request per item we want to change.
//TODO: For now we have to reload the page to see the update
for (let index = 0; index < this.selection.length; index++) {

View File

@@ -1,4 +1,4 @@
import { getDisplayStateFromUserStatus } from '../../../../utils.js';
import { getDisplayStateFromUserStatus } from '../../../utils.js';
import type { UmbUserCollectionContext } from '../../user-collection.context.js';
import type { UmbUserDetailModel } from '../../../types.js';
import { css, html, nothing, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
@@ -76,6 +76,27 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement {
}
#renderUserCard(user: UmbUserDetailModel) {
const avatarUrls = [
{
scale: '1x',
url: user.avatarUrls?.[0],
},
{
scale: '2x',
url: user.avatarUrls?.[1],
},
{
scale: '3x',
url: user.avatarUrls?.[2],
},
];
let avatarSrcset = '';
avatarUrls.forEach((url) => {
avatarSrcset += `${url.url} ${url.scale},`;
});
return html`
<uui-card-user
.name=${user.name ?? 'Unnamed user'}
@@ -86,6 +107,12 @@ export class UmbUserGridCollectionViewElement extends UmbLitElement {
@selected=${() => this.#onSelect(user)}
@deselected=${() => this.#onDeselect(user)}>
${this.#renderUserTag(user)} ${this.#renderUserGroupNames(user)} ${this.#renderUserLoginDate(user)}
<uui-avatar
slot="avatar"
.name=${user.name || 'Unknown'}
img-src=${ifDefined(user.avatarUrls.length > 0 ? avatarUrls[0].url : undefined)}
img-srcset=${ifDefined(user.avatarUrls.length > 0 ? avatarSrcset : undefined)}></uui-avatar>
</uui-card-user>
`;
}

View File

@@ -1,4 +1,4 @@
import { html, LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { html, LitElement, customElement, property, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { UmbTableColumn, UmbTableItem } from '@umbraco-cms/backoffice/components';
@customElement('umb-user-table-name-column-layout')
@@ -13,9 +13,34 @@ export class UmbUserTableNameColumnLayoutElement extends LitElement {
value!: any;
render() {
const avatarUrls = [
{
scale: '1x',
url: this.value.avatarUrls?.[0],
},
{
scale: '2x',
url: this.value.avatarUrls?.[1],
},
{
scale: '3x',
url: this.value.avatarUrls?.[2],
},
];
let avatarSrcset = '';
avatarUrls.forEach((url) => {
avatarSrcset += `${url.url} ${url.scale},`;
});
return html` <div style="display: flex; align-items: center;">
<uui-avatar name="${this.value.name}" style="margin-right: var(--uui-size-space-3);"></uui-avatar>
<a style="font-weight: bold;" href="section/user-management/view/users/user/${this.item.id}"
<uui-avatar
style="margin-right: var(--uui-size-space-3);"
.name=${this.value.name || 'Unknown'}
img-src=${ifDefined(this.value.avatarUrls.length > 0 ? avatarUrls[0].url : undefined)}
img-srcset=${ifDefined(this.value.avatarUrls.length > 0 ? avatarSrcset : undefined)}></uui-avatar>
<a style="font-weight: bold;" href="section/user-management/view/users/user/${this.value.unique}"
>${this.value.name}</a
>
</div>`;

Some files were not shown because too many files have changed in this diff Show More