Merge branch 'main' into chore/move-extension-components

This commit is contained in:
Mads Rasmussen
2024-05-15 08:07:31 +02:00
212 changed files with 2936 additions and 1478 deletions

View File

@@ -15833,18 +15833,6 @@
"node": ">=8.6"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@@ -18415,6 +18403,18 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
},
"node_modules/send/node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",

View File

@@ -7,6 +7,7 @@ import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
export class UmbAppAuthController extends UmbControllerBase {
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
#isFirstCheck = true;
constructor(host: UmbControllerHost) {
super(host);
@@ -37,7 +38,18 @@ export class UmbAppAuthController extends UmbControllerBase {
const isAuthorized = this.#authContext.getIsAuthorized();
if (isAuthorized) {
return true;
// If this is the first time we are checking the authorization state (i.e. on first load), we need to make sure
// that the token is still valid. If it is not, we need to start the authorization flow.
// If the token is still valid, we can return true.
if (this.#isFirstCheck) {
this.#isFirstCheck = false;
const isValid = await this.#authContext.validateToken();
if (isValid) {
return true;
}
} else {
return true;
}
}
// Make a request to the auth server to start the auth flow

View File

@@ -62,21 +62,36 @@ export class UmbAppElement extends UmbLitElement {
path: 'oauth_complete',
component: () => import('./app-error.element.js'),
setup: (component) => {
if (!this.#authContext) {
throw new Error('[Fatal] Auth context is not available');
}
const searchParams = new URLSearchParams(window.location.search);
const hasCode = searchParams.has('code');
(component as UmbAppErrorElement).hideBackButton = true;
(component as UmbAppErrorElement).errorHeadline = this.localize.term('general_login');
(component as UmbAppErrorElement).errorMessage = hasCode
? this.localize.term('errors_externalLoginSuccess')
: this.localize.term('errors_externalLoginFailed');
// Complete the authorization request
this.#authContext?.completeAuthorizationRequest().finally(() => {
// If we don't have an opener, redirect to the root
if (!window.opener) {
history.replaceState(null, '', '');
}
});
// If there is an opener, we are in a popup window, and we should show a different message
// than if we are in the main window. If we are in the main window, we should redirect to the root.
// The authorization request will be completed in the active window (main or popup) and the authorization signal will be sent.
// If we are in a popup window, the storage event in UmbAuthContext will catch the signal and close the window.
// If we are in the main window, the signal will be caught right here and the user will be redirected to the root.
if (window.opener) {
(component as UmbAppErrorElement).errorMessage = hasCode
? this.localize.term('errors_externalLoginSuccess')
: this.localize.term('errors_externalLoginFailed');
} else {
(component as UmbAppErrorElement).errorMessage = hasCode
? this.localize.term('errors_externalLoginRedirectSuccess')
: this.localize.term('errors_externalLoginFailed');
this.observe(this.#authContext.authorizationSignal, () => {
window.location.href = '/';
});
}
// Complete the authorization request, which will send the authorization signal
this.#authContext.completeAuthorizationRequest();
},
},
{

View File

@@ -711,6 +711,7 @@ export default {
externalLoginFailed:
'Serveren mislykkedes i at logge ind med den eksterne loginudbyder. Luk dette vindue og prøv igen.',
externalLoginSuccess: 'Du er nu logget ind. Du kan nu lukke dette vindue.',
externalLoginRedirectSuccess: 'Du er nu logget ind. Du vil blive omdirigeret om et øjeblik.',
},
openidErrors: {
accessDenied: 'Access denied',

View File

@@ -720,6 +720,7 @@ export default {
externalLoginFailed:
'The server failed to authorize you against the external login provider. Please close the window and try again.',
externalLoginSuccess: 'You have successfully logged in. You may now close this window.',
externalLoginRedirectSuccess: 'You have successfully logged in. You will be redirected shortly.',
},
openidErrors: {
accessDenied: 'Access denied',
@@ -1243,8 +1244,8 @@ export default {
openMediaPicker: 'Open media picker',
},
propertyEditorPicker: {
title: 'Select Property Editor',
openPropertyEditorPicker: 'Select Property Editor',
title: 'Select a property editor',
openPropertyEditorPicker: 'Select a property editor UI',
},
relatedlinks: {
enterExternal: 'enter external link',

View File

@@ -731,6 +731,7 @@ export default {
externalLoginFailed:
'The server failed to authorize you against the external login provider. Please close the window and try again.',
externalLoginSuccess: 'You have successfully logged in. You may now close this window.',
externalLoginRedirectSuccess: 'You have successfully logged in. You will be redirected shortly.',
},
openidErrors: {
accessDenied: 'Access denied',
@@ -2542,8 +2543,8 @@ export default {
searchResults: 'items returned',
},
propertyEditorPicker: {
title: 'Select Property Editor',
openPropertyEditorPicker: 'Select Property Editor',
title: 'Select a property editor',
openPropertyEditorPicker: 'Select a property editor UI',
},
analytics: {
consentForAnalytics: 'Consent for telemetry data',

View File

@@ -5,10 +5,10 @@
* @param contexts This is a map of the collected contexts from umb-debug
* @returns An array of simplified context data
*/
export function contextData(contexts: Map<any, any>): Array<DebugContextData> {
const contextData = new Array<DebugContextData>();
export function contextData(contexts: Map<any, any>): Array<UmbDebugContextData> {
const contextData = new Array<UmbDebugContextData>();
for (const [alias, instance] of contexts) {
const data: DebugContextItemData = contextItemData(instance);
const data = contextItemData(instance);
contextData.push({ alias: alias, type: typeof instance, data });
}
return contextData;
@@ -20,8 +20,8 @@ export function contextData(contexts: Map<any, any>): Array<DebugContextData> {
* @param contextInstance The instance of the context
* @returns A simplied object contain the properties and methods of the context
*/
function contextItemData(contextInstance: any): DebugContextItemData {
let contextItemData: DebugContextItemData = { type: 'unknown' };
function contextItemData(contextInstance: any): UmbDebugContextItemData {
let contextItemData: UmbDebugContextItemData = { type: 'unknown' };
if (typeof contextInstance === 'function') {
contextItemData = { ...contextItemData, type: 'function' };
@@ -59,7 +59,7 @@ function contextItemData(contextInstance: any): DebugContextItemData {
valueToDisplay = `Web Component <${tagName}>`;
} else if (isSubscribeLike) {
valueToDisplay = 'Subscribable';
valueToDisplay = 'Observable';
}
props.push({ key: key, type: typeof value, value: valueToDisplay });
@@ -71,7 +71,7 @@ function contextItemData(contextInstance: any): DebugContextItemData {
}
}
contextItemData = { ...contextItemData, properties: props };
contextItemData = { ...contextItemData, properties: props.sort((a, b) => a.key.localeCompare(b.key)) };
}
} else {
contextItemData = { ...contextItemData, type: 'primitive', value: contextInstance };
@@ -83,7 +83,7 @@ function contextItemData(contextInstance: any): DebugContextItemData {
/**
* Gets a list of methods from a class
*
* @param klass The class to get the methods from
* @param class The class to get the methods from
* @returns An array of method names as strings
*/
function getClassMethodNames(klass: any) {
@@ -98,15 +98,15 @@ function getClassMethodNames(klass: any) {
const allMethods =
typeof klass.prototype === 'undefined' ? distinctDeepFunctions(klass) : Object.getOwnPropertyNames(klass.prototype);
return allMethods.filter((name: any) => name !== 'constructor' && !name.startsWith('_'));
return allMethods.filter((name: any) => name !== 'constructor' && !name.startsWith('_')).sort();
}
export interface DebugContextData {
export interface UmbDebugContextData {
/**
* The alias of the context
*
* @type {string}
* @memberof DebugContextData
* @memberof UmbDebugContextData
*/
alias: string;
@@ -114,32 +114,32 @@ export interface DebugContextData {
* The type of the context such as object or string
*
* @type {("string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function")}
* @memberof DebugContextData
* @memberof UmbDebugContextData
*/
type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';
/**
* Data about the context that includes method and property names
*
* @type {DebugContextItemData}
* @memberof DebugContextData
* @type {UmbDebugContextItemData}
* @memberof UmbDebugContextData
*/
data: DebugContextItemData;
data: UmbDebugContextItemData;
}
export interface DebugContextItemData {
export interface UmbDebugContextItemData {
type: string;
methods?: Array<unknown>;
properties?: Array<DebugContextItemPropertyData>;
properties?: Array<UmbDebugContextItemPropertyData>;
value?: unknown;
}
export interface DebugContextItemPropertyData {
export interface UmbDebugContextItemPropertyData {
/**
* The name of the property
*
* @type {string}
* @memberof DebugContextItemPropertyData
* @memberof UmbDebugContextItemPropertyData
*/
key: string;
@@ -147,7 +147,7 @@ export interface DebugContextItemPropertyData {
* The type of the property's value such as string or number
*
* @type {("string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function")}
* @memberof DebugContextItemPropertyData
* @memberof UmbDebugContextItemPropertyData
*/
type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function';
@@ -155,7 +155,7 @@ export interface DebugContextItemPropertyData {
* Simple types such as string or number can have their value displayed stored inside the property
*
* @type {("string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function")}
* @memberof DebugContextItemPropertyData
* @memberof UmbDebugContextItemPropertyData
*/
value?: unknown;
}

View File

@@ -1,10 +1,7 @@
import type { UmbContextRequestEvent } from '../consume/context-request.event.js';
import { UMB_CONTENT_REQUEST_EVENT_TYPE, UMB_DEBUG_CONTEXT_EVENT_TYPE } from '../consume/context-request.event.js';
import type { UmbContextToken } from '../token/index.js';
import {
UmbContextProvideEventImplementation,
//UmbContextUnprovidedEventImplementation,
} from './context-provide.event.js';
import { UMB_CONTENT_REQUEST_EVENT_TYPE, UMB_DEBUG_CONTEXT_EVENT_TYPE } from '../consume/context-request.event.js';
import { UmbContextProvideEventImplementation } from './context-provide.event.js';
/**
* @export
@@ -76,7 +73,7 @@ export class UmbContextProvider<BaseType = unknown, ResultType extends BaseType
this.#eventTarget.dispatchEvent(new UmbContextProvideEventImplementation(this.#contextAlias));
// Listen to our debug event 'umb:debug-contexts'
this.#eventTarget.addEventListener(UMB_DEBUG_CONTEXT_EVENT_TYPE, this._handleDebugContextRequest);
this.#eventTarget.addEventListener(UMB_DEBUG_CONTEXT_EVENT_TYPE, this.#handleDebugContextRequest);
}
/**
@@ -88,10 +85,10 @@ export class UmbContextProvider<BaseType = unknown, ResultType extends BaseType
//window.dispatchEvent(new UmbContextUnprovidedEventImplementation(this._contextAlias, this.#instance));
// Stop listen to our debug event 'umb:debug-contexts'
this.#eventTarget?.removeEventListener(UMB_DEBUG_CONTEXT_EVENT_TYPE, this._handleDebugContextRequest);
this.#eventTarget?.removeEventListener(UMB_DEBUG_CONTEXT_EVENT_TYPE, this.#handleDebugContextRequest);
}
private _handleDebugContextRequest = (event: any): void => {
#handleDebugContextRequest = (event: any): void => {
// If the event doesn't have an instances property, create it.
if (!event.instances) {
event.instances = new Map();

View File

@@ -1,6 +1,10 @@
import { UmbMockDBBase } from './utils/mock-db-base.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import type { UmbEntityBase } from '@umbraco-cms/backoffice/models';
type UmbEntityBase = {
id?: string;
name?: string;
};
// Temp mocked database
export class UmbEntityData<T extends UmbEntityBase> extends UmbMockDBBase<T> {

View File

@@ -307,29 +307,17 @@ export class UmbAuthFlow {
return Promise.resolve(this.#tokenResponse.accessToken);
}
// if the refresh token is not set (maybe the provider doesn't support them)
if (!this.#tokenResponse?.refreshToken) {
this.#timeoutSignal.next();
return Promise.reject('Missing refreshToken.');
}
const success = await this.makeRefreshTokenRequest();
const request = new TokenRequest({
client_id: this.#clientId,
redirect_uri: this.#redirectUri,
grant_type: GRANT_TYPE_REFRESH_TOKEN,
code: undefined,
refresh_token: this.#tokenResponse.refreshToken,
extras: undefined,
});
await this.#performTokenRequest(request);
if (!this.#tokenResponse) {
if (!success) {
this.clearTokenStorage();
this.#timeoutSignal.next();
return Promise.reject('Missing tokenResponse.');
}
return Promise.resolve(this.#tokenResponse.accessToken);
return this.#tokenResponse
? Promise.resolve(this.#tokenResponse.accessToken)
: Promise.reject('Missing tokenResponse.');
}
/**
@@ -364,18 +352,36 @@ export class UmbAuthFlow {
await this.#performTokenRequest(request);
}
async makeRefreshTokenRequest(): Promise<boolean> {
if (!this.#tokenResponse?.refreshToken) {
return false;
}
const request = new TokenRequest({
client_id: this.#clientId,
redirect_uri: this.#redirectUri,
grant_type: GRANT_TYPE_REFRESH_TOKEN,
code: undefined,
refresh_token: this.#tokenResponse.refreshToken,
extras: undefined,
});
return this.#performTokenRequest(request);
}
/**
* This method will make a token request to the server using the refresh token.
* If the request fails, it will sign the user out (clear the token state).
*/
async #performTokenRequest(request: TokenRequest): Promise<void> {
async #performTokenRequest(request: TokenRequest): Promise<boolean> {
try {
this.#tokenResponse = await this.#tokenHandler.performTokenRequest(this.#configuration, request);
this.#saveTokenState();
return true;
} catch (error) {
// If the token request fails, it means the code or refresh token is invalid
this.clearTokenStorage();
console.error('Token request error', error);
this.clearTokenStorage();
return false;
}
}
}

View File

@@ -174,6 +174,15 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
return this.#authFlow.performWithFreshTokens();
}
/**
* Validates the token against the server and returns true if the token is valid.
* @memberof UmbAuthContext
* @returns True if the token is valid, otherwise false
*/
async validateToken(): Promise<boolean> {
return this.#isBypassed || this.#authFlow.makeRefreshTokenRequest();
}
/**
* Clears the token storage.
* @memberof UmbAuthContext
@@ -188,7 +197,6 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
* @memberof UmbAuthContext
*/
timeOut() {
this.clearTokenStorage();
this.#isAuthorized.setValue(false);
this.#isTimeout.next();
}

View File

@@ -95,14 +95,16 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement<UmbModalAppAuthC
const providerName =
typeof providerOrManifest === 'string' ? providerOrManifest : providerOrManifest.forProviderName;
await authContext.makeAuthorizationRequest(providerName, false, loginHint, manifest);
// If the user is timed out, we do not want to lose the state, so avoid redirecting to the provider
// and instead just make the authorization request. In all other cases, we want to redirect to the provider.
const isTimedOut = this.data?.userLoginState === 'timedOut';
await authContext.makeAuthorizationRequest(providerName, isTimedOut ? false : true, loginHint, manifest);
const isAuthed = authContext.getIsAuthorized();
this.value = { success: isAuthed };
if (isAuthed) {
this._submitModal();
} else {
this._error = 'Failed to authenticate';
}
} catch (error) {
console.error('[AuthModal] Error submitting auth request', error);

View File

@@ -8,7 +8,7 @@ import type { UmbRoute } from '@umbraco-cms/backoffice/router';
export interface UmbCollectionViewManagerConfig {
defaultViewAlias?: string;
manifestFilter?: (manifest: ManifestCollectionView) => boolean
manifestFilter?: (manifest: ManifestCollectionView) => boolean;
}
export class UmbCollectionViewManager extends UmbControllerBase {

View File

@@ -35,6 +35,5 @@ export const Datetimelocal: Story = {
args: {
type: 'datetime-local',
value: '2023-04-01T10:00:00',
displayValue: '',
},
};

View File

@@ -29,6 +29,7 @@ export class UmbInputEntityElement extends UUIFormControlMixin(UmbLitElement, ''
protected getFormElement() {
return undefined;
}
@property({ type: Number })
public set min(value: number) {
this.#min = value;
@@ -125,6 +126,10 @@ export class UmbInputEntityElement extends UUIFormControlMixin(UmbLitElement, ''
#openPicker() {
this.#pickerContext?.openPicker({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: ignoring this for now to prevent breaking existing functionality.
// if we want a very generic input it should be possible to pass in picker config
hideTreeRoot: true,
});
}

View File

@@ -1,4 +1,4 @@
import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { customElement, html, property, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
@@ -19,22 +19,42 @@ export class UmbInputEyeDropperElement extends UUIFormControlMixin(UmbLitElement
@property({ type: Boolean })
opacity = false;
@property({ type: Array })
swatches: string[] = [];
@property({ type: Boolean })
showPalette = false;
//TODO if empty swatches, the color picker still shows the area where they are supposed to be rendered.
// BTW in the old backoffice "palette" seemed to be true/false setting, but here its an array.
@property({ type: Array })
swatches?: string[];
// HACK: Since `uui-color-picker` doesn't have an option to hide the swatches, we had to get creative.
// Based on UUI v1.8.0-rc3, the value of `swatches` must be a falsey value to hide them.
// https://github.com/umbraco/Umbraco.UI/blob/v1.8.0-rc.3/packages/uui-color-picker/lib/uui-color-picker.element.ts#L517
// However, the object-type for `swatches` is a `string[]` (non-nullable).
// https://github.com/umbraco/Umbraco.UI/blob/v1.8.0-rc.3/packages/uui-color-picker/lib/uui-color-picker.element.ts#L157
// To do this, we must omit the `.swatches` attribute, otherwise the default swatches can't be used.
// So, we've use a `when()` render both configurations. [LK]
render() {
return html`
<uui-color-picker
label="Eye dropper"
.opacity=${this.opacity}
.swatches=${this.swatches}
.value=${this.value as string}
@change=${this.#onChange}>
</uui-color-picker>
`;
const swatches = this.showPalette ? this.swatches : undefined;
return when(
this.showPalette && !swatches,
() => html`
<uui-color-picker
label="Eye dropper"
.opacity=${this.opacity}
.value=${this.value as string}
@change=${this.#onChange}>
</uui-color-picker>
`,
() => html`
<uui-color-picker
label="Eye dropper"
.opacity=${this.opacity}
.swatches=${swatches!}
.value=${this.value as string}
@change=${this.#onChange}>
</uui-color-picker>
`,
);
}
}

View File

@@ -22,6 +22,13 @@ export const WithOpacity: Story = {
export const WithSwatches: Story = {
args: {
showPalette: true,
swatches: ['#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff'],
},
};
export const ShowPalette: Story = {
args: {
showPalette: true,
},
};

View File

@@ -4,6 +4,8 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UUIRadioEvent } from '@umbraco-cms/backoffice/external/uui';
type UmbRadioButtonItem = { label: string; value: string };
@customElement('umb-input-radio-button-list')
export class UmbInputRadioButtonListElement extends UUIFormControlMixin(UmbLitElement, '') {
#value: string = '';
@@ -17,7 +19,7 @@ export class UmbInputRadioButtonListElement extends UUIFormControlMixin(UmbLitEl
}
@property({ type: Array })
public list: Array<{ label: string; value: string }> = [];
public list: Array<UmbRadioButtonItem> = [];
protected getFormElement() {
return undefined;

View File

@@ -1,8 +1,8 @@
import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UUISliderEvent } from '@umbraco-cms/backoffice/external/uui';
import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UUISliderEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-input-slider')
export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, '') {
@@ -28,9 +28,9 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, ''
return undefined;
}
#onChange(e: UUISliderEvent) {
e.stopPropagation();
this.value = e.target.value as string;
#onChange(event: UUISliderEvent) {
event.stopPropagation();
this.value = event.target.value as string;
this.dispatchEvent(new UmbChangeEvent());
}
@@ -39,20 +39,27 @@ export class UmbInputSliderElement extends UUIFormControlMixin(UmbLitElement, ''
}
#renderSlider() {
return html`<uui-slider
.min="${this.min}"
.max="${this.max}"
.step="${this.step}"
.value="${this.valueLow.toString()}"
@change="${this.#onChange}"></uui-slider>`;
return html`
<uui-slider
.min=${this.min}
.max=${this.max}
.step=${this.step}
.value=${this.valueLow.toString()}
@change=${this.#onChange}>
</uui-slider>
`;
}
#renderRangeSlider() {
return html`<uui-range-slider
.min="${this.min}"
.max="${this.max}"
.step="${this.step}"
.value="${this.valueLow},${this.valueHigh}"
@change="${this.#onChange}"></uui-range-slider>`;
return html`
<uui-range-slider
.min=${this.min}
.max=${this.max}
.step=${this.step}
.value="${this.valueLow},${this.valueHigh}"
@change=${this.#onChange}>
</uui-range-slider>
`;
}
}

View File

@@ -23,23 +23,22 @@ export class UmbInputUploadFieldFileElement extends UmbLitElement {
label = '';
#serverUrl = '';
#serverUrlPromise;
/**
*
*/
constructor() {
super();
this.#serverUrlPromise = this.consumeContext(UMB_APP_CONTEXT, (instance) => {
this.consumeContext(UMB_APP_CONTEXT, (instance) => {
this.#serverUrl = instance.getServerUrl();
}).asPromise();
}
protected updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
super.updated(_changedProperties);
if (_changedProperties.has('file')) {
this.extension = this.file?.name.split('.').pop() || '';
this.label = this.file?.name || 'loading...';
if (_changedProperties.has('file') && this.file) {
this.extension = this.#getExtensionFromMime(this.file.type) ?? '';
this.label = this.file.name || 'loading...';
}
if (_changedProperties.has('path')) {
@@ -52,8 +51,23 @@ export class UmbInputUploadFieldFileElement extends UmbLitElement {
}
}
#getExtensionFromMime(mime: string): string {
//TODO Temporary solution.
if (!mime) return ''; //folders
const extension = mime.split('/')[1];
switch (extension) {
case 'svg+xml':
return 'svg';
default:
return extension;
}
}
#renderLabel() {
if (this.path) return html`<a id="label" href=${this.path}>${this.label}</a>`;
if (this.path) {
// Don't make it a link if it's a temp file upload.
return this.file ? this.label : html`<a id="label" href=${this.path} target="_blank">${this.label}</a>`;
}
return html`<span id="label">${this.label}</span>`;
}

View File

@@ -1,5 +1,6 @@
import type { MediaValueType } from '../../../property-editors/upload-field/property-editor-ui-upload-field.element.js';
import type { UmbTemporaryFileModel } from '../../temporary-file/temporary-file-manager.class.js';
import { UmbTemporaryFileManager } from '../../temporary-file/temporary-file-manager.class.js';
import { TemporaryFileStatus, UmbTemporaryFileManager } from '../../temporary-file/temporary-file-manager.class.js';
import { UmbId } from '@umbraco-cms/backoffice/id';
import {
css,
@@ -10,62 +11,41 @@ import {
property,
query,
state,
repeat,
} from '@umbraco-cms/backoffice/external/lit';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import './input-upload-field-file.element.js';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app';
import './input-upload-field-file.element.js';
@customElement('umb-input-upload-field')
export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElement, '') {
private _keys: Array<string> = [];
/**
* @description Keys to the files that belong to this upload field.
* @type {Array<String>}
* @default []
*/
@property({ type: Array })
public set keys(fileKeys: Array<string>) {
this._keys = fileKeys;
super.value = this._keys.join(',');
this.#setFilePaths();
export class UmbInputUploadFieldElement extends UmbLitElement {
@property({ type: Object })
set value(value: MediaValueType) {
if (!value?.src) return;
this._src = value.src;
}
public get keys(): Array<string> {
return this._keys;
get value(): MediaValueType {
return !this.temporaryFile ? { src: this._src } : { temporaryFileId: this.temporaryFile.unique };
}
/**
* @description Allowed file extensions. If left empty, all are allowed.
* @description Allowed file extensions. Allow all if empty.
* @type {Array<String>}
* @default undefined
*/
@property({ type: Array })
set fileExtensions(value: Array<string>) {
set allowedFileExtensions(value: Array<string>) {
this.#setExtensions(value);
}
get fileExtensions(): Array<string> | undefined {
get allowedFileExtensions(): Array<string> | undefined {
return this._extensions;
}
/**
* @description Allows the user to upload multiple files.
* @default false
* @attr
*/
@property({ type: Boolean })
public multiple = false;
@state()
public temporaryFile?: UmbTemporaryFileModel;
@state()
private _files: Array<{
path: string;
unique: string;
queueItem?: UmbTemporaryFileModel;
file?: File;
}> = [];
private _src = '';
@state()
private _extensions?: string[];
@@ -73,110 +53,36 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen
@query('#dropzone')
private _dropzone?: UUIFileDropzoneElement;
#manager;
#serverUrl = '';
#serverUrlPromise;
#manager = new UmbTemporaryFileManager(this);
protected getFormElement() {
return undefined;
}
constructor() {
super();
this.#manager = new UmbTemporaryFileManager(this);
/*this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (context) => {
this.observe(await context.propertyValueByAlias('umbracoExtension'), (value) => {
//const test = value;
});
});*/
this.#serverUrlPromise = this.consumeContext(UMB_APP_CONTEXT, (instance) => {
this.#serverUrl = instance.getServerUrl();
}).asPromise();
this.observe(this.#manager.queue, (value) => {
this.error = !value.length;
this._files = this._files.map((file) => {
const queueItem = value.find((item) => item.unique === file.unique);
if (queueItem) {
file.queueItem = queueItem;
}
return file;
});
});
}
async #setFilePaths() {
await this.#serverUrlPromise;
this.keys.forEach((key) => {
if (!UmbId.validate(key) && key.startsWith('/')) {
this._files.push({
path: this.#serverUrl + key,
unique: UmbId.new(),
});
this.requestUpdate();
}
});
}
#setExtensions(value: Array<string>) {
if (!value) {
#setExtensions(extensions: Array<string>) {
if (!extensions?.length) {
this._extensions = undefined;
return;
}
// TODO: The dropzone uui component does not support file extensions without a dot. Remove this when it does.
this._extensions = value?.map((extension) => {
return `.${extension}`;
});
this._extensions = extensions?.map((extension) => `.${extension}`);
}
#onUpload(e: UUIFileDropzoneEvent) {
const files: File[] = e.detail.files;
async #onUpload(e: UUIFileDropzoneEvent) {
//Property Editor for Upload field will always only have one file.
const item: UmbTemporaryFileModel = {
unique: UmbId.new(),
file: e.detail.files[0],
};
const upload = this.#manager.uploadOne(item);
if (!files?.length) return;
const reader = new FileReader();
reader.onload = () => {
this._src = reader.result as string;
};
reader.readAsDataURL(item.file);
// TODO: Should we validate the mimetype some how?
this.#setFiles(files);
}
#setFiles(files: File[]) {
const items = files.map(
(file): UmbTemporaryFileModel => ({
unique: UmbId.new(),
file,
status: 'waiting',
}),
);
this.#manager.upload(items);
this.keys = items.map((item) => item.unique);
this.value = this.keys.join(',');
this.dispatchEvent(new UmbChangeEvent());
// Read files to get their paths and add them to the file paths array.
items.forEach((item) => {
this._files.push({
path: '',
unique: item.unique,
queueItem: item,
file: item.file,
});
const reader = new FileReader();
reader.onload = () => {
this._files = this._files.map((file) => {
if (file.unique === item.unique) {
file.path = reader.result as string;
}
return file;
});
this.requestUpdate();
};
reader.readAsDataURL(item.file);
});
const uploaded = await upload;
if (uploaded.status === TemporaryFileStatus.SUCCESS) {
this.temporaryFile = { unique: item.unique, file: item.file };
this.dispatchEvent(new UmbChangeEvent());
}
}
#handleBrowse() {
@@ -185,67 +91,53 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen
}
render() {
return html`
<div id="wrapper">${this.#renderFiles()}</div>
${this.#renderDropzone()} ${this.#renderButtonRemove()}
`;
return html`${this._src ? this.#renderFile(this._src, this.temporaryFile?.file) : this.#renderDropzone()}`;
}
//TODO When the property editor gets saved, it seems that the property editor gets the file path from the server rather than key/id.
// This however does not work when there is multiple files. Can the server not handle multiple files uploaded into one property editor?
#renderDropzone() {
if (!this.multiple && this._files.length) return nothing;
return html`
<uui-file-dropzone
id="dropzone"
label="dropzone"
@change="${this.#onUpload}"
accept="${ifDefined(this._extensions?.join(', '))}"
?multiple="${this.multiple}">
accept="${ifDefined(this._extensions?.join(', '))}">
<uui-button label=${this.localize.term('media_clickToUpload')} @click="${this.#handleBrowse}"></uui-button>
</uui-file-dropzone>
`;
}
#renderFiles() {
return repeat(
this._files,
(path) => path,
(path) => this.#renderFile(path),
);
}
#renderFile(file: { path: string; unique: string; queueItem?: UmbTemporaryFileModel; file?: File }) {
// TODO: Get the mime type from the server and use that to determine the file type.
const type = this.#getFileTypeFromPath(file.path);
#renderFile(src: string, file?: File) {
const extension = this.#getFileExtensionFromPath(src);
return html`
<div style="position:relative; display: flex; width: fit-content; max-width: 100%">
${getElementTemplate()}
${file.queueItem?.status === 'waiting' ? html`<umb-temporary-file-badge></umb-temporary-file-badge>` : nothing}
<div id="wrapper">
<div style="position:relative; display: flex; width: fit-content; max-width: 100%">
${getElementTemplate()}
${this.temporaryFile?.status === TemporaryFileStatus.WAITING
? html`<umb-temporary-file-badge></umb-temporary-file-badge>`
: nothing}
</div>
</div>
${this.#renderButtonRemove()}
`;
function getElementTemplate() {
switch (type) {
switch (extension) {
case 'audio':
return html`<umb-input-upload-field-audio .path=${file.path}></umb-input-upload-field-audio>`;
return html`<umb-input-upload-field-audio .path=${src}></umb-input-upload-field-audio>`;
case 'video':
return html`<umb-input-upload-field-video .path=${file.path}></umb-input-upload-field-video>`;
return html`<umb-input-upload-field-video .path=${src}></umb-input-upload-field-video>`;
case 'image':
return html`<umb-input-upload-field-image .path=${file.path}></umb-input-upload-field-image>`;
return html`<umb-input-upload-field-image .path=${src}></umb-input-upload-field-image>`;
case 'svg':
return html`<umb-input-upload-field-svg .path=${file.path}></umb-input-upload-field-svg>`;
case 'file':
return html`<umb-input-upload-field-file
.path=${file.path}
.file=${file.file as any}></umb-input-upload-field-file>`;
return html`<umb-input-upload-field-svg .path=${src}></umb-input-upload-field-svg>`;
default:
return html`<umb-input-upload-field-file .path=${src} .file=${file}></umb-input-upload-field-file>`;
}
}
}
#getFileTypeFromPath(path: string): 'audio' | 'video' | 'image' | 'svg' | 'file' {
#getFileExtensionFromPath(path: string): 'audio' | 'video' | 'image' | 'svg' | 'file' {
// Extract the MIME type from the data URL
if (path.startsWith('data:')) {
const mimeType = path.substring(5, path.indexOf(';'));
@@ -267,20 +159,14 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen
}
#renderButtonRemove() {
if (!this._files.length) return;
return html`<uui-button compact @click=${this.#handleRemove} label=${this.localize.term('content_uploadClear')}>
<uui-icon name="icon-trash"></uui-icon>${this.localize.term('content_uploadClear')}
</uui-button>`;
}
#handleRemove() {
const uniques = this._files.map((file) => file.unique);
this.#manager.remove(uniques);
this._files = [];
this.value = '';
this.keys = [];
this._src = '';
this.temporaryFile = undefined;
this.dispatchEvent(new UmbChangeEvent());
}
@@ -297,6 +183,7 @@ export class UmbInputUploadFieldElement extends UUIFormControlMixin(UmbLitElemen
gap: var(--uui-size-space-4);
box-sizing: border-box;
}
#wrapper:has(umb-input-upload-field-file) {
padding: var(--uui-size-space-4);
border: 1px solid var(--uui-color-border);

View File

@@ -11,7 +11,5 @@ export default meta;
type Story = StoryObj<UmbInputUploadFieldElement>;
export const Overview: Story = {
args: {
multiple: false,
},
args: {},
};

View File

@@ -102,7 +102,7 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin(
this.dispatchEvent(new UmbInputEvent());
}
#onColorInput(event: InputEvent) {
#onColorChange(event: Event) {
event.stopPropagation();
this.value = this._colorPicker.value;
this.dispatchEvent(new UmbChangeEvent());
@@ -153,7 +153,7 @@ export class UmbMultipleColorPickerItemInputElement extends UUIFormControlMixin(
value=${this._valueHex}
@click=${this.#onColorClick}></uui-color-swatch>
</uui-input>
<input aria-hidden="${true}" type="color" id="color" value=${this.value} @input=${this.#onColorInput} />
<input aria-hidden="${true}" type="color" id="color" value=${this.value} @change=${this.#onColorChange} />
</div>
${when(
this.showLabels,

View File

@@ -1,5 +1,5 @@
import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
import { customElement, html, css, property, classMap } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { classMap, customElement, css, html, property } from '@umbraco-cms/backoffice/external/lit';
/**
* @element umb-stack
@@ -7,72 +7,73 @@ import { customElement, html, css, property, classMap } from "@umbraco-cms/backo
* @extends LitElement
*/
@customElement('umb-stack')
export class UmbStackElement extends UmbLitElement
{
/**
* Look
* @type {String}
* @memberof UmbStackElement
*/
@property({ type:String })
look: 'compact' | 'default' = 'default';
export class UmbStackElement extends UmbLitElement {
/**
* Look
* @type {String}
* @memberof UmbStackElement
*/
@property({ type: String })
look: 'compact' | 'default' = 'default';
/**
* Divide
* @type {Boolean}
* @memberof UmbStackElement
*/
@property({ type:Boolean })
divide: boolean = false;
/**
* Divide
* @type {Boolean}
* @memberof UmbStackElement
*/
@property({ type: Boolean })
divide: boolean = false;
render() {
return html`<div class=${classMap({ divide: this.divide, compact: this.look === 'compact' })}>
<slot></slot>
</div>`;
}
render() {
return html`
<div class=${classMap({ divide: this.divide, compact: this.look === 'compact' })}>
<slot></slot>
</div>
`;
}
static styles = [
css`
div {
display: block;
position: relative;
}
static styles = [
css`
div {
display: block;
position: relative;
}
::slotted(*) {
position: relative;
margin-top: var(--uui-size-space-6);
}
::slotted(*) {
position: relative;
margin-top: var(--uui-size-space-6);
}
.divide ::slotted(*)::before {
content: '';
position: absolute;
top: calc((var(--uui-size-space-6) / 2) * -1);
height: 0;
width: 100%;
border-top: solid 1px var(--uui-color-divider-standalone);
}
.divide ::slotted(*)::before {
content: '';
position: absolute;
top: calc((var(--uui-size-space-6) / 2) * -1);
height: 0;
width: 100%;
border-top: solid 1px var(--uui-color-divider-standalone);
}
::slotted(*:first-child) {
margin-top: 0;
}
::slotted(*:first-child) {
margin-top: 0;
}
.divide ::slotted(*:first-child)::before {
display: none;
}
.divide ::slotted(*:first-child)::before {
display: none;
}
.compact ::slotted(*) {
margin-top: var(--uui-size-space-3);
}
.compact ::slotted(*) {
margin-top: var(--uui-size-space-3);
}
.compact ::slotted(*:first-child) {
margin-top: 0;
}
.compact ::slotted(*:first-child) {
margin-top: 0;
}
.compact.divide ::slotted(*)::before {
display: none;
}
`
];
.compact.divide ::slotted(*)::before {
display: none;
}
`,
];
}
export default UmbStackElement;

View File

@@ -1,23 +1,20 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { TemplateResult } from '@umbraco-cms/backoffice/external/lit';
import { css, html, nothing, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import type { DebugContextData, DebugContextItemData } from '@umbraco-cms/backoffice/context-api';
import { css, customElement, html, map, nothing, property, state, when } from '@umbraco-cms/backoffice/external/lit';
import { contextData, UmbContextDebugRequest } from '@umbraco-cms/backoffice/context-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
import { UMB_CONTEXT_DEBUGGER_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { UmbDebugContextData, UmbDebugContextItemData } from '@umbraco-cms/backoffice/context-api';
import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal';
@customElement('umb-debug')
export class UmbDebugElement extends UmbLitElement {
@property({ reflect: true, type: Boolean })
@property({ type: Boolean })
visible = false;
@property({ reflect: true, type: Boolean })
@property({ type: Boolean })
dialog = false;
@state()
contextData = Array<DebugContextData>();
private _contextData = Array<UmbDebugContextData>();
@state()
private _debugPaneOpen = false;
@@ -31,160 +28,144 @@ export class UmbDebugElement extends UmbLitElement {
});
}
render() {
if (this.visible) {
return this.dialog ? this._renderDialog() : this._renderPanel();
} else {
return nothing;
}
}
private _update() {
// Dispatch it
#update() {
this.dispatchEvent(
new UmbContextDebugRequest((contexts: Map<any, any>) => {
// The Contexts are collected
// When travelling up through the DOM from this element
// to the root of <umb-app> which then uses the callback prop
// of the this event tha has been raised to assign the contexts
// of this event that has been raised to assign the contexts
// back to this property of the WebComponent
// Massage the data into a simplier array of objects
// From a function in the context-api '
this.contextData = contextData(contexts);
this.requestUpdate('contextData');
// from a function in the context-api.
this._contextData = contextData(contexts);
this.requestUpdate('_contextData');
}),
);
}
private _toggleDebugPane() {
#toggleDebugPane() {
this._debugPaneOpen = !this._debugPaneOpen;
if (this._debugPaneOpen) {
this._update();
this.#update();
}
}
private _openDialog() {
this._update();
#openDialog() {
this.#update();
this._modalContext?.open(this, UMB_CONTEXT_DEBUGGER_MODAL, {
data: {
content: html`${this._renderContextAliases()}`,
content: this.#renderContextAliases(),
},
});
}
private _renderDialog() {
render() {
if (!this.visible) return nothing;
return this.dialog ? this.#renderDialog() : this.#renderPanel();
}
#renderDialog() {
return html`
<div id="container">
<uui-badge color="danger" look="primary" attention @click="${this._openDialog}">
<uui-icon name="icon-bug"></uui-icon>&nbsp;Debug
</uui-badge>
</div>`;
}
private _renderPanel() {
return html` <div id="container">
<uui-button color="danger" look="primary" @click="${this._toggleDebugPane}">
<uui-icon name="icon-bug"></uui-icon>
Debug
</uui-button>
<div class="events ${this._debugPaneOpen ? 'open' : ''}">
<div>
<ul>
${this._renderContextAliases()}
</ul>
</div>
<div>
<uui-badge color="danger" look="primary" @click=${this.#openDialog}>
<uui-icon name="icon-bug"></uui-icon>
<span>Debug</span>
</uui-badge>
</div>
`;
}
#renderPanel() {
return html`
<div id="container">
<uui-button color="danger" look="primary" @click=${this.#toggleDebugPane}>
<uui-icon name="icon-bug"></uui-icon>
<span>Debug</span>
</uui-button>
${when(this._debugPaneOpen, () => this.#renderContextAliases())}
</div>
`;
}
#renderContextAliases() {
return html`<div class="events">
${map(this._contextData, (context) => {
return html`
<details>
<summary><strong>${context.alias}</strong></summary>
${this.#renderInstance(context.data)}
</details>
`;
})}
</div>`;
}
private _renderContextAliases() {
return repeat(
this.contextData,
(contextData) => contextData.alias,
(contextData) => {
return html` <li>
Context: <strong>${contextData.alias}</strong>
<em>(${contextData.type})</em>
<ul>
${this._renderInstance(contextData.data)}
</ul>
</li>`;
},
);
}
private _renderInstance(instance: DebugContextItemData) {
const instanceTemplates: TemplateResult[] = [];
if (instance.type === 'function') {
return instanceTemplates.push(html`<li>Callable Function</li>`);
} else if (instance.type === 'object') {
if (instance.methods?.length) {
instanceTemplates.push(html`
<li>
<strong>Methods</strong>
<ul>
${instance.methods?.map((methodName) => html`<li>${methodName}</li>`)}
</ul>
</li>
`);
#renderInstance(instance: UmbDebugContextItemData) {
switch (instance.type) {
case 'function': {
return html`<h3>Callable Function</h3>`;
}
const props: TemplateResult[] = [];
instance.properties?.forEach((property) => {
switch (property.type) {
case 'string':
case 'number':
case 'boolean':
case 'object':
props.push(html`<li>${property.key} <em>(${property.type})</em> = ${property.value}</li>`);
break;
case 'object': {
return html`
<details>
<summary>Methods</summary>
<ul>
${map(instance.methods, (methodName) => html`<li>${methodName}</li>`)}
</ul>
</details>
default:
props.push(html`<li>${property.key} <em>(${property.type})</em></li>`);
break;
}
});
<details>
<summary>Properties</summary>
<ul>
${map(instance.properties, (property) => {
switch (property.type) {
case 'string':
case 'number':
case 'boolean':
case 'object':
return html`<li>${property.key} <em>(${property.type})</em> = ${property.value}</li>`;
instanceTemplates.push(html`
<li>
<strong>Properties</strong>
<ul>
${props}
</ul>
</li>
`);
} else if (instance.type === 'primitive') {
instanceTemplates.push(html`<li>Context is a primitive with value: ${instance.value}</li>`);
default:
return html`<li>${property.key} <em>(${property.type})</em></li>`;
}
})}
</ul>
</details>
`;
}
case 'primitive': {
return html`<p>Context is a primitive with value: ${instance.value}</p>`;
}
default: {
return html`<p>Unknown type: ${instance.type}</p>`;
}
}
return instanceTemplates;
}
static styles = [
UmbTextStyles,
css`
:host {
float: right;
font-family: monospace;
position: relative;
z-index: 10000;
}
#container {
display: block;
font-family: monospace;
z-index: 10000;
position: relative;
width: 100%;
padding: 10px 0;
display: flex;
flex-direction: column;
align-items: flex-end;
}
uui-badge {
cursor: pointer;
gap: 0.5rem;
}
uui-icon {
@@ -194,22 +175,19 @@ export class UmbDebugElement extends UmbLitElement {
.events {
background-color: var(--uui-color-danger);
color: var(--uui-color-selected-contrast);
max-height: 0;
transition: max-height 0.25s ease-out;
overflow: hidden;
padding: 1rem;
}
.events.open {
max-height: 500px;
overflow: auto;
summary {
cursor: pointer;
}
.events > div {
padding: 10px;
details > details {
margin-left: 1rem;
}
h4 {
margin: 0;
ul {
margin-top: 0;
}
`,
];

View File

@@ -1,66 +1,34 @@
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbContextDebuggerModalData} from '@umbraco-cms/backoffice/modal';
import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbContextDebuggerModalData } from '@umbraco-cms/backoffice/modal';
@customElement('umb-context-debugger-modal')
export default class UmbContextDebuggerModalElement extends UmbModalBaseElement<UmbContextDebuggerModalData> {
private _handleClose() {
#close() {
this.modalContext?.reject();
}
render() {
return html`
<uui-dialog-layout>
<span slot="headline"> <uui-icon name="icon-bug"></uui-icon> Debug: Contexts </span>
<uui-scroll-container id="field-settings">
${this.data?.content}
</uui-scroll-container>
<uui-button slot="actions" look="primary" label="Close sidebar" @click="${this._handleClose}">Close</uui-button>
</uui-dialog-layout>
<umb-body-layout headline="Debug: Contexts">
<div id="main">${this.data?.content}</div>
<div slot="actions">
<uui-button @click=${this.#close} label=${this.localize.term('general_close')}></uui-button>
</div>
</umb-body-layout>
`;
}
static styles = [
UmbTextStyles,
css`
uui-dialog-layout {
display: flex;
flex-direction: column;
height: 100%;
padding: var(--uui-size-space-5);
box-sizing: border-box;
summary {
cursor: pointer;
}
uui-scroll-container {
overflow-y: scroll;
max-height: 100%;
min-height: 0;
flex: 1;
}
uui-icon {
vertical-align: text-top;
color: var(--uui-color-danger);
}
.context {
padding: 15px 0;
border-bottom: 1px solid var(--uui-color-danger-emphasis);
}
h3 {
margin-top: 0;
margin-bottom: 0;
}
h3 > span {
border-radius: var(--uui-size-4);
background-color: var(--uui-color-danger);
color: var(--uui-color-danger-contrast);
padding: 8px;
font-size: 12px;
details > details {
margin-left: 1rem;
}
ul {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -1,9 +1,8 @@
import { UMB_DUPLICATE_TO_MODAL_ALIAS } from './constants.js';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDuplicateToModalData {
unique: string | null;
entityType: string;
export interface UmbDuplicateToModalData extends UmbEntityModel {
treeAlias: string;
}

View File

@@ -5,7 +5,7 @@ import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbTreeRepository, UmbUniqueTreeItemModel } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeRepository, UmbTreeItemModel } from '@umbraco-cms/backoffice/tree';
import { UmbPaginationManager } from '@umbraco-cms/backoffice/utils';
import { observeMultiple } from '@umbraco-cms/backoffice/observable-api';
@@ -17,7 +17,7 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement<
UmbSortChildrenOfModalValue
> {
@state()
_children: Array<UmbUniqueTreeItemModel> = [];
_children: Array<UmbTreeItemModel> = [];
@state()
_currentPage = 1;
@@ -27,7 +27,7 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement<
#pagination = new UmbPaginationManager();
#sortedUniques = new Set<string>();
#sorter?: UmbSorterController<UmbUniqueTreeItemModel>;
#sorter?: UmbSorterController<UmbTreeItemModel>;
constructor() {
super();
@@ -52,13 +52,16 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement<
if (!this.data?.unique === undefined) throw new Error('unique is required');
if (!this.data?.treeRepositoryAlias) throw new Error('treeRepositoryAlias is required');
const treeRepository = await createExtensionApiByAlias<UmbTreeRepository<UmbUniqueTreeItemModel>>(
const treeRepository = await createExtensionApiByAlias<UmbTreeRepository<UmbTreeItemModel>>(
this,
this.data.treeRepositoryAlias,
);
const { data } = await treeRepository.requestTreeItemsOf({
parentUnique: this.data.unique,
parent: {
unique: this.data.unique,
entityType: this.data.entityType,
},
skip: this.#pagination.getSkip(),
take: this.#pagination.getPageSize(),
});
@@ -77,7 +80,7 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement<
#initSorter() {
if (this.#sorter) return;
this.#sorter = new UmbSorterController<UmbUniqueTreeItemModel>(this, {
this.#sorter = new UmbSorterController<UmbTreeItemModel>(this, {
getUniqueOfElement: (element) => {
return element.dataset.unique;
},
@@ -174,7 +177,7 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement<
`;
}
#renderChild(item: UmbUniqueTreeItemModel) {
#renderChild(item: UmbTreeItemModel) {
return html`<uui-ref-node .name=${item.name} data-unique=${item.unique}></uui-ref-node>`;
}

View File

@@ -1,9 +1,8 @@
import { UMB_SORT_CHILDREN_OF_MODAL_ALIAS } from './constants.js';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbSortChildrenOfModalData {
unique: string | null;
entityType: string;
export interface UmbSortChildrenOfModalData extends UmbEntityModel {
treeRepositoryAlias: string;
sortChildrenOfRepositoryAlias: string;
}

View File

@@ -1,9 +1,7 @@
import { UmbControllerEvent } from '@umbraco-cms/backoffice/controller-api';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
export interface UmbEntityActionEventArgs {
unique: string | null;
entityType: string;
}
export interface UmbEntityActionEventArgs extends UmbEntityModel {}
export class UmbEntityActionEvent extends UmbControllerEvent {
#args: UmbEntityActionEventArgs;

View File

@@ -1,5 +1,5 @@
export interface UmbEntityActionArgs<MetaArgsType> {
entityType: string;
unique: string | null;
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
export interface UmbEntityActionArgs<MetaArgsType> extends UmbEntityModel {
meta: MetaArgsType;
}

View File

@@ -1,2 +1,3 @@
export { UMB_ENTITY_CONTEXT } from './entity.context-token.js';
export { UmbEntityContext } from './entity.context.js';
export * from './types.js';

View File

@@ -0,0 +1,6 @@
export type UmbEntityUnique = string | null;
export interface UmbEntityModel {
unique: UmbEntityUnique;
entityType: string;
}

View File

@@ -1,10 +1,10 @@
import type { UmbTreeItemModelBase } from '../../tree/types.js';
import type { UmbTreeItemModel } from '../../tree/types.js';
import type { UmbTreeItemContext } from '../../tree/tree-item/index.js';
import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api';
import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api';
export interface ManifestTreeItem
extends ManifestElementAndApi<UmbControllerHostElement, UmbTreeItemContext<UmbTreeItemModelBase>> {
extends ManifestElementAndApi<UmbControllerHostElement, UmbTreeItemContext<UmbTreeItemModel>> {
type: 'treeItem';
forEntityTypes: Array<string>;
}

View File

@@ -1,9 +1,9 @@
import type { UmbStructureItemModel } from './types.js';
import type { UmbTreeRepository, UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeRepository, UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
interface UmbMenuTreeStructureWorkspaceContextBaseArgs {
@@ -17,6 +17,9 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex
#structure = new UmbArrayState<UmbStructureItemModel>([], (x) => x.unique);
public readonly structure = this.#structure.asObservable();
#parent = new UmbObjectState<UmbStructureItemModel | undefined>(undefined);
public readonly parent = this.#parent.asObservable();
constructor(host: UmbControllerHost, args: UmbMenuTreeStructureWorkspaceContextBaseArgs) {
// TODO: set up context token
super(host, 'UmbMenuStructureWorkspaceContext');
@@ -36,9 +39,10 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex
async #requestStructure() {
let structureItems: Array<UmbStructureItemModel> = [];
const treeRepository = await createExtensionApiByAlias<
UmbTreeRepository<UmbUniqueTreeItemModel, UmbUniqueTreeRootModel>
>(this, this.#args.treeRepositoryAlias);
const treeRepository = await createExtensionApiByAlias<UmbTreeRepository<UmbTreeItemModel, UmbTreeRootModel>>(
this,
this.#args.treeRepositoryAlias,
);
const { data: root } = await treeRepository.requestTreeRoot();
@@ -55,11 +59,15 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex
const isNew = this.#workspaceContext?.getIsNew();
const uniqueObservable = isNew ? this.#workspaceContext?.parentUnique : this.#workspaceContext?.unique;
const entityTypeObservable = isNew ? this.#workspaceContext?.parentEntityType : this.#workspaceContext?.entityType;
const unique = (await this.observe(uniqueObservable, () => {})?.asPromise()) as string;
if (!unique) throw new Error('Unique is not available');
const { data } = await treeRepository.requestTreeItemAncestors({ descendantUnique: unique });
const entityType = (await this.observe(entityTypeObservable, () => {})?.asPromise()) as string;
if (!entityType) throw new Error('Entity type is not available');
const { data } = await treeRepository.requestTreeItemAncestors({ treeItem: { unique, entityType } });
if (data) {
const ancestorItems = data.map((treeItem) => {
@@ -70,9 +78,12 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex
isFolder: treeItem.isFolder,
};
});
structureItems.push(...ancestorItems);
}
const parent = structureItems[structureItems.length - 2];
this.#parent.setValue(parent);
this.#structure.setValue(structureItems);
}
}

View File

@@ -1,9 +1,9 @@
import type { UmbVariantStructureItemModel } from './types.js';
import type { UmbTreeRepository } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeItemModel, UmbTreeRepository, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UMB_VARIANT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
interface UmbMenuVariantTreeStructureWorkspaceContextBaseArgs {
@@ -18,6 +18,9 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
#structure = new UmbArrayState<UmbVariantStructureItemModel>([], (x) => x.unique);
public readonly structure = this.#structure.asObservable();
#parent = new UmbObjectState<UmbVariantStructureItemModel | undefined>(undefined);
public readonly parent = this.#parent.asObservable();
constructor(host: UmbControllerHost, args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs) {
// TODO: set up context token
super(host, 'UmbMenuStructureWorkspaceContext');
@@ -37,18 +40,38 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
async #requestStructure() {
const isNew = this.#workspaceContext?.getIsNew();
const uniqueObservable = isNew ? this.#workspaceContext?.parentUnique : this.#workspaceContext?.unique;
const entityTypeObservable = isNew ? this.#workspaceContext?.parentEntityType : this.#workspaceContext?.entityType;
let structureItems: Array<UmbVariantStructureItemModel> = [];
const unique = (await this.observe(uniqueObservable, () => {})?.asPromise()) as string;
if (!unique) throw new Error('Unique is not available');
const treeRepository = await createExtensionApiByAlias<UmbTreeRepository<any>>(
const entityType = (await this.observe(entityTypeObservable, () => {})?.asPromise()) as string;
if (!entityType) throw new Error('Entity type is not available');
// TODO: add correct tree variant item model
const treeRepository = await createExtensionApiByAlias<UmbTreeRepository<any, UmbTreeRootModel>>(
this,
this.#args.treeRepositoryAlias,
);
const { data } = await treeRepository.requestTreeItemAncestors({ descendantUnique: unique });
const { data: root } = await treeRepository.requestTreeRoot();
if (root) {
structureItems = [
{
unique: root.unique,
entityType: root.entityType,
variants: [{ name: root.name, culture: null, segment: null }],
},
];
}
const { data } = await treeRepository.requestTreeItemAncestors({ treeItem: { unique, entityType } });
if (data) {
const structureItems = data.map((treeItem) => {
const ancestorItems = data.map((treeItem) => {
return {
unique: treeItem.unique,
entityType: treeItem.entityType,
@@ -62,6 +85,10 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
};
});
structureItems.push(...ancestorItems);
const parent = structureItems[structureItems.length - 2];
this.#parent.setValue(parent);
this.#structure.setValue(structureItems);
}
}

View File

@@ -1,7 +1,6 @@
export interface UmbStructureItemModelBase {
unique: string | null;
entityType: string;
}
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
export interface UmbStructureItemModelBase extends UmbEntityModel {}
export interface UmbStructureItemModel extends UmbStructureItemModelBase {
name: string;

View File

@@ -1,13 +1,11 @@
import type { UUIColorSwatchesEvent } from '@umbraco-cms/backoffice/external/uui';
import { css, html, customElement, state, repeat, query, nothing } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UmbIconPickerModalData, UmbIconPickerModalValue } from '@umbraco-cms/backoffice/modal';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { css, customElement, html, nothing, query, repeat, state } from '@umbraco-cms/backoffice/external/lit';
import { extractUmbColorVariable, umbracoColors } from '@umbraco-cms/backoffice/resources';
import { umbFocus } from '@umbraco-cms/backoffice/lit-element';
import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_ICON_REGISTRY_CONTEXT, type UmbIconDefinition } from '@umbraco-cms/backoffice/icon';
import type { UmbIconPickerModalData, UmbIconPickerModalValue } from '@umbraco-cms/backoffice/modal';
import type { UUIColorSwatchesEvent } from '@umbraco-cms/backoffice/external/uui';
@customElement('umb-icon-picker-modal')
export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPickerModalData, UmbIconPickerModalValue> {
@@ -106,12 +104,12 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
<uui-button
slot="actions"
label=${this.localize.term('general_close')}
@click="${this._rejectModal}"></uui-button>
@click=${this._rejectModal}></uui-button>
<uui-button
slot="actions"
color="positive"
look="primary"
@click="${this._submitModal}"
@click=${this._submitModal}
label=${this.localize.term('general_submit')}></uui-button>
</umb-body-layout>
`;
@@ -123,7 +121,7 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
placeholder=${this.localize.term('placeholders_filter')}
label=${this.localize.term('placeholders_filter')}
id="search"
@keyup="${this.#filterIcons}"
@keyup=${this.#filterIcons}
${umbFocus()}>
<uui-icon name="search" slot="prepend" id="search_icon"></uui-icon>
</uui-input>`;
@@ -136,15 +134,14 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement<UmbIconPicker
(icon) => icon.name,
(icon) => html`
<uui-button
label="${icon.name}"
title="${icon.name}"
class="${icon.name === this._currentIcon ? 'selected' : ''}"
label=${icon.name}
title=${icon.name}
class=${icon.name === this._currentIcon ? 'selected' : ''}
@click=${(e: InputEvent) => this.#changeIcon(e, icon.name)}
@keyup=${(e: KeyboardEvent) => this.#changeIcon(e, icon.name)}>
<uui-icon
style="--uui-icon-color: var(${extractUmbColorVariable(this._currentColor)})"
name="${icon.name}">
</uui-icon>
name=${icon.name}></uui-icon>
</uui-button>
`,
)

View File

@@ -1,6 +1,5 @@
export interface UmbPickerModalData<ItemType> {
multiple?: boolean;
hideTreeRoot?: boolean; // TODO: this should be moved to a tree picker modal data interface
filter?: (item: ItemType) => boolean;
pickableFilter?: (item: ItemType) => boolean;
}

View File

@@ -1,11 +1,3 @@
export type UmbEntityUnique = string | null;
/** Tried to find a common base of our entities — used by Entity Workspace Context */
export type UmbEntityBase = {
id?: string;
name?: string;
};
export interface UmbSwatchDetails {
label: string;
value: string;

View File

@@ -8,13 +8,14 @@ import type { UmbModalToken, UmbPickerModalData, UmbPickerModalValue } from '@um
type PickerItemBaseType = { name: string; unique: string };
export class UmbPickerInputContext<
ItemType extends PickerItemBaseType,
TreeItemType extends PickerItemBaseType = ItemType,
PickedItemType extends PickerItemBaseType,
PickerItemType extends PickerItemBaseType = PickedItemType,
PickerModalConfigType extends UmbPickerModalData<PickerItemType> = UmbPickerModalData<PickerItemType>,
PickerModalValueType extends UmbPickerModalValue = UmbPickerModalValue,
> extends UmbControllerBase {
// TODO: We are way too unsecure about the requirements for the Modal Token, as we have certain expectation for the data and value.
modalAlias: string | UmbModalToken<UmbPickerModalData<TreeItemType>, UmbPickerModalValue>;
repository?: UmbItemRepository<ItemType>;
#getUnique: (entry: ItemType) => string | undefined;
modalAlias: string | UmbModalToken<UmbPickerModalData<PickerItemType>, PickerModalValueType>;
repository?: UmbItemRepository<PickedItemType>;
#getUnique: (entry: PickedItemType) => string | undefined;
#itemManager;
@@ -48,14 +49,14 @@ export class UmbPickerInputContext<
constructor(
host: UmbControllerHost,
repositoryAlias: string,
modalAlias: string | UmbModalToken<UmbPickerModalData<TreeItemType>, UmbPickerModalValue>,
getUniqueMethod?: (entry: ItemType) => string | undefined,
modalAlias: string | UmbModalToken<UmbPickerModalData<PickerItemType>, PickerModalValueType>,
getUniqueMethod?: (entry: PickedItemType) => string | undefined,
) {
super(host);
this.modalAlias = modalAlias;
this.#getUnique = getUniqueMethod || ((entry) => entry.unique);
this.#itemManager = new UmbRepositoryItemsManager<ItemType>(this, repositoryAlias, this.#getUnique);
this.#itemManager = new UmbRepositoryItemsManager<PickedItemType>(this, repositoryAlias, this.#getUnique);
this.selection = this.#itemManager.uniques;
this.selectedItems = this.#itemManager.items;
@@ -70,7 +71,7 @@ export class UmbPickerInputContext<
this.#itemManager.setUniques(selection.filter((value) => value !== null) as Array<string>);
}
async openPicker(pickerData?: Partial<UmbPickerModalData<TreeItemType>>) {
async openPicker(pickerData?: Partial<PickerModalConfigType>) {
await this.#itemManager.init;
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const modalContext = modalManager.open(this, this.modalAlias, {
@@ -80,7 +81,7 @@ export class UmbPickerInputContext<
},
value: {
selection: this.getSelection(),
},
} as PickerModalValueType,
});
const modalValue = await modalContext?.onSubmit();

View File

@@ -1,7 +1,7 @@
import type { UmbVariantId } from '../../variant/variant-id.class.js';
import type { UmbContext } from '@umbraco-cms/backoffice/class-api';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbEntityUnique } from '@umbraco-cms/backoffice/models';
import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
/**
* A property dataset context, represents the data of a set of properties.

View File

@@ -1,3 +1,4 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import type { UmbPickerModalData, UmbPickerModalValue } from '@umbraco-cms/backoffice/modal';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
@@ -10,12 +11,7 @@ export interface UmbRestoreFromRecycleBinModalData {
}
export interface UmbRestoreFromRecycleBinModalValue {
destination:
| {
unique: string | null;
entityType: string;
}
| undefined;
destination: UmbEntityModel | undefined;
}
export const UMB_RESTORE_FROM_RECYCLE_BIN_MODAL = new UmbModalToken<

View File

@@ -4,22 +4,26 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import { UmbId } from '@umbraco-cms/backoffice/id';
export type TemporaryFileStatus = 'success' | 'waiting' | 'error';
///export type TemporaryFileStatus = 'success' | 'waiting' | 'error';
export enum TemporaryFileStatus {
SUCCESS = 'success',
WAITING = 'waiting',
ERROR = 'error',
}
export interface UmbTemporaryFileModel {
file: File;
unique: string;
status: TemporaryFileStatus;
status?: TemporaryFileStatus;
}
export interface UmbTemporaryFileQueueModel extends Partial<UmbTemporaryFileModel> {
file: File;
}
export class UmbTemporaryFileManager extends UmbControllerBase {
export class UmbTemporaryFileManager<
UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel,
> extends UmbControllerBase {
#temporaryFileRepository;
#queue = new UmbArrayState<UmbTemporaryFileModel>([], (item) => item.unique);
#queue = new UmbArrayState<UploadableItem>([], (item) => item.unique);
public readonly queue = this.#queue.asObservable();
constructor(host: UmbControllerHost) {
@@ -27,28 +31,24 @@ export class UmbTemporaryFileManager extends UmbControllerBase {
this.#temporaryFileRepository = new UmbTemporaryFileRepository(host);
}
async uploadOne(queueItem: UmbTemporaryFileQueueModel): Promise<Array<UmbTemporaryFileModel>> {
async uploadOne(uploadableItem: UploadableItem): Promise<UploadableItem> {
this.#queue.setValue([]);
const item: UmbTemporaryFileModel = {
file: queueItem.file,
unique: queueItem.unique ?? UmbId.new(),
status: queueItem.status ?? 'waiting',
const item: UploadableItem = {
status: TemporaryFileStatus.WAITING,
...uploadableItem,
};
this.#queue.appendOne(item);
return this.handleQueue();
return (await this.#handleQueue())[0];
}
async upload(queueItems: Array<UmbTemporaryFileQueueModel>): Promise<Array<UmbTemporaryFileModel>> {
async upload(queueItems: Array<UploadableItem>): Promise<Array<UploadableItem>> {
this.#queue.setValue([]);
const items = queueItems.map(
(item): UmbTemporaryFileModel => ({
file: item.file,
unique: item.unique ?? UmbId.new(),
status: item.status ?? 'waiting',
}),
);
const items = queueItems.map((item): UploadableItem => ({ status: TemporaryFileStatus.WAITING, ...item }));
this.#queue.append(items);
return this.handleQueue();
return this.#handleQueue();
}
removeOne(unique: string) {
@@ -59,8 +59,8 @@ export class UmbTemporaryFileManager extends UmbControllerBase {
this.#queue.remove(uniques);
}
private async handleQueue() {
const filesCompleted: Array<UmbTemporaryFileModel> = [];
async #handleQueue() {
const filesCompleted: Array<UploadableItem> = [];
const queue = this.#queue.getValue();
if (!queue.length) return filesCompleted;
@@ -69,14 +69,14 @@ export class UmbTemporaryFileManager extends UmbControllerBase {
if (!item.unique) throw new Error(`Unique is missing for item ${item}`);
const { error } = await this.#temporaryFileRepository.upload(item.unique, item.file);
await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown
//await new Promise((resolve) => setTimeout(resolve, (Math.random() + 0.5) * 1000)); // simulate small delay so that the upload badge is properly shown
let status: TemporaryFileStatus;
if (error) {
status = 'error';
status = TemporaryFileStatus.ERROR;
this.#queue.updateOne(item.unique, { ...item, status });
} else {
status = 'success';
status = TemporaryFileStatus.SUCCESS;
this.#queue.updateOne(item.unique, { ...item, status });
}

View File

@@ -1,4 +1,4 @@
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeItemModel } from '../types.js';
import type {
UmbTreeAncestorsOfRequestArgs,
UmbTreeChildrenOfRequestArgs,
@@ -13,7 +13,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
* @interface UmbTreeDataSourceConstructor
* @template TreeItemType
*/
export interface UmbTreeDataSourceConstructor<TreeItemType extends UmbTreeItemModelBase> {
export interface UmbTreeDataSourceConstructor<TreeItemType extends UmbTreeItemModel> {
new (host: UmbControllerHost): UmbTreeDataSource<TreeItemType>;
}
@@ -23,7 +23,7 @@ export interface UmbTreeDataSourceConstructor<TreeItemType extends UmbTreeItemMo
* @interface UmbTreeDataSource
* @template TreeItemType
*/
export interface UmbTreeDataSource<TreeItemType extends UmbTreeItemModelBase> {
export interface UmbTreeDataSource<TreeItemType extends UmbTreeItemModel> {
/**
* Gets the root items of the tree.
* @return {*} {Promise<UmbDataSourceResponse<UmbPagedModel<TreeItemType>>>}

View File

@@ -1,8 +1,12 @@
import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '../types.js';
import type { UmbTreeItemModel, UmbTreeRootModel } from '../types.js';
import type { UmbTreeStore } from './tree-store.interface.js';
import type { UmbTreeRepository } from './tree-repository.interface.js';
import type { UmbTreeDataSource, UmbTreeDataSourceConstructor } from './tree-data-source.interface.js';
import type { UmbTreeAncestorsOfRequestArgs } from './types.js';
import type {
UmbTreeAncestorsOfRequestArgs,
UmbTreeChildrenOfRequestArgs,
UmbTreeRootItemsRequestArgs,
} from './types.js';
import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository';
import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
@@ -21,8 +25,8 @@ import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
* @template TreeRootType
*/
export abstract class UmbTreeRepositoryBase<
TreeItemType extends UmbUniqueTreeItemModel,
TreeRootType extends UmbUniqueTreeRootModel,
TreeItemType extends UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel,
>
extends UmbRepositoryBase
implements UmbTreeRepository<TreeItemType, TreeRootType>, UmbApi
@@ -63,7 +67,7 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
async requestRootTreeItems(args: any) {
async requestRootTreeItems(args: UmbTreeRootItemsRequestArgs) {
await this._init;
const { data, error: _error } = await this._treeSource.getRootItems(args);
@@ -81,8 +85,10 @@ export abstract class UmbTreeRepositoryBase<
* @return {*}
* @memberof UmbTreeRepositoryBase
*/
async requestTreeItemsOf(args: any) {
if (args.parentUnique === undefined) throw new Error('Parent unique is missing');
async requestTreeItemsOf(args: UmbTreeChildrenOfRequestArgs) {
if (!args.parent) throw new Error('Parent is missing');
if (args.parent.unique === undefined) throw new Error('Parent unique is missing');
if (args.parent.entityType === null) throw new Error('Parent entity type is missing');
await this._init;
const { data, error: _error } = await this._treeSource.getChildrenOf(args);
@@ -91,7 +97,7 @@ export abstract class UmbTreeRepositoryBase<
this._treeStore!.appendItems(data.items);
}
return { data, error, asObservable: () => this._treeStore!.childrenOf(args.parentUnique) };
return { data, error, asObservable: () => this._treeStore!.childrenOf(args.parent.unique) };
}
/**
@@ -101,7 +107,7 @@ export abstract class UmbTreeRepositoryBase<
* @memberof UmbTreeRepositoryBase
*/
async requestTreeItemAncestors(args: UmbTreeAncestorsOfRequestArgs) {
if (args.descendantUnique === undefined) throw new Error('Descendant unique is missing');
if (args.treeItem.unique === undefined) throw new Error('Descendant unique is missing');
await this._init;
const { data, error: _error } = await this._treeSource.getAncestorsOf(args);

View File

@@ -1,4 +1,4 @@
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeItemModel, UmbTreeRootModel } from '../types.js';
import type {
UmbTreeChildrenOfRequestArgs,
UmbTreeAncestorsOfRequestArgs,
@@ -18,8 +18,8 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
* @template TreeRootType
*/
export interface UmbTreeRepository<
TreeItemType extends UmbTreeItemModelBase = UmbTreeItemModelBase,
TreeRootType extends UmbTreeItemModelBase = UmbTreeItemModelBase,
TreeItemType extends UmbTreeItemModel = UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel = UmbTreeRootModel,
> extends UmbApi {
/**
* Requests the root of the tree.

View File

@@ -1,4 +1,4 @@
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeItemModelBase, UmbTreeItemModel } from '../types.js';
import type { UmbTreeDataSource } from './tree-data-source.interface.js';
import type {
UmbTreeAncestorsOfRequestArgs,
@@ -27,7 +27,7 @@ export interface UmbTreeServerDataSourceBaseArgs<
*/
export abstract class UmbTreeServerDataSourceBase<
ServerTreeItemType extends { hasChildren: boolean },
ClientTreeItemType extends UmbTreeItemModelBase,
ClientTreeItemType extends UmbTreeItemModel,
> implements UmbTreeDataSource<ClientTreeItemType>
{
#host;
@@ -73,7 +73,7 @@ export abstract class UmbTreeServerDataSourceBase<
* @memberof UmbTreeServerDataSourceBase
*/
async getChildrenOf(args: UmbTreeChildrenOfRequestArgs) {
if (args.parentUnique === undefined) throw new Error('Parent unique is missing');
if (args.parent.unique === undefined) throw new Error('Parent unique is missing');
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getChildrenOf(args));
@@ -92,7 +92,7 @@ export abstract class UmbTreeServerDataSourceBase<
* @memberof UmbTreeServerDataSourceBase
*/
async getAncestorsOf(args: UmbTreeAncestorsOfRequestArgs) {
if (!args.descendantUnique) throw new Error('Parent unique is missing');
if (!args.treeItem.entityType) throw new Error('Parent unique is missing');
const { data, error } = await tryExecuteAndNotify(this.#host, this.#getAncestorsOf(args));

View File

@@ -1,14 +1,19 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
export interface UmbTreeRootItemsRequestArgs {
skip: number;
take: number;
skip?: number;
take?: number;
}
export interface UmbTreeChildrenOfRequestArgs {
parentUnique: string | null;
skip: number;
take: number;
parent: UmbEntityModel;
skip?: number;
take?: number;
}
export interface UmbTreeAncestorsOfRequestArgs {
descendantUnique: string;
treeItem: {
unique: string;
entityType: string;
};
}

View File

@@ -1,4 +1,4 @@
import type { UmbUniqueTreeItemModel } from '../types.js';
import type { UmbTreeItemModel } from '../types.js';
import type { UmbTreeStore } from './tree-store.interface.js';
import { UmbStoreBase } from '@umbraco-cms/backoffice/store';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
@@ -11,19 +11,16 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
* @extends {UmbStoreBase}
* @description - Entity Tree Store
*/
export class UmbUniqueTreeStore
extends UmbStoreBase<UmbUniqueTreeItemModel>
implements UmbTreeStore<UmbUniqueTreeItemModel>
{
export class UmbUniqueTreeStore extends UmbStoreBase<UmbTreeItemModel> implements UmbTreeStore<UmbTreeItemModel> {
constructor(host: UmbControllerHost, storeAlias: string) {
super(host, storeAlias, new UmbArrayState<UmbUniqueTreeItemModel>([], (x) => x.unique));
super(host, storeAlias, new UmbArrayState<UmbTreeItemModel>([], (x) => x.unique));
}
/**
* An observable to observe the root items
* @memberof UmbUniqueTreeStore
*/
rootItems = this._data.asObservablePart((items) => items.filter((item) => item.parentUnique === null));
rootItems = this._data.asObservablePart((items) => items.filter((item) => item.parent.unique === null));
/**
* Returns an observable to observe the children of a given parent
@@ -32,16 +29,6 @@ export class UmbUniqueTreeStore
* @memberof UmbUniqueTreeStore
*/
childrenOf(parentUnique: string | null) {
return this._data.asObservablePart((items) => items.filter((item) => item.parentUnique === parentUnique));
}
/**
* Returns an observable to observe the items with the given uniques
* @param {Array<string>} uniques
* @return {*}
* @memberof UmbUniqueTreeStore
*/
items(uniques: Array<string | null>) {
return this._data.asObservablePart((items) => items.filter((item) => uniques.includes(item.unique)));
return this._data.asObservablePart((items) => items.filter((item) => item.parent.unique === parentUnique));
}
}

View File

@@ -1,5 +1,5 @@
import { UmbRequestReloadTreeItemChildrenEvent } from '../reload-tree-item-children/index.js';
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeItemModel, UmbTreeRootModel, UmbTreeStartNode } from '../types.js';
import type { UmbTreeRepository } from '../data/tree-repository.interface.js';
import type { UmbTreeContext } from '../tree-context.interface.js';
import { type UmbActionEventContext, UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
@@ -11,21 +11,19 @@ import {
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbPaginationManager, UmbSelectionManager } from '@umbraco-cms/backoffice/utils';
import { UmbPaginationManager, UmbSelectionManager, debounce } from '@umbraco-cms/backoffice/utils';
import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action';
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbArrayState, UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
extends UmbContextBase<UmbDefaultTreeContext<TreeItemType>>
export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModel, TreeRootType extends UmbTreeRootModel>
extends UmbContextBase<UmbDefaultTreeContext<TreeItemType, TreeRootType>>
implements UmbTreeContext
{
#treeRoot = new UmbObjectState<TreeItemType | undefined>(undefined);
#treeRoot = new UmbObjectState<TreeRootType | undefined>(undefined);
treeRoot = this.#treeRoot.asObservable();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
#rootItems = new UmbArrayState<TreeItemType>([], (x) => x.unique);
rootItems = this.#rootItems.asObservable();
@@ -34,8 +32,14 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
public readonly selection = new UmbSelectionManager(this._host);
public readonly pagination = new UmbPaginationManager();
#hideTreeRoot = new UmbBooleanState(false);
hideTreeRoot = this.#hideTreeRoot.asObservable();
#startNode = new UmbObjectState<UmbTreeStartNode | undefined>(undefined);
startNode = this.#startNode.asObservable();
#manifest?: ManifestTree;
#repository?: UmbTreeRepository<TreeItemType>;
#repository?: UmbTreeRepository<TreeItemType, TreeRootType>;
#actionEventContext?: UmbActionEventContext;
#paging = {
@@ -51,6 +55,8 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
});
constructor(host: UmbControllerHost) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
super(host, UMB_DEFAULT_TREE_CONTEXT);
this.pagination.setPageSize(this.#paging.take);
this.#consumeContexts();
@@ -67,16 +73,16 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
// @ts-ignore
hostElement.addEventListener('temp-reload-tree-item-parent', (event: CustomEvent) => {
const treeRoot = this.#treeRoot.getValue();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const unique = treeRoot.unique;
const unique = treeRoot?.unique;
if (event.detail.unique === unique) {
event.stopPropagation();
this.loadRootItems();
this.loadTree();
}
});
this.loadTreeRoot();
// always load the tree root because we need the root entity to reload the entire tree
this.#loadTreeRoot();
}
// TODO: find a generic way to do this
@@ -115,31 +121,127 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
return this.#repository;
}
public async loadTreeRoot() {
await this.#init;
const { data } = await this.#repository!.requestTreeRoot();
/**
* Loads the tree
* @memberof UmbDefaultTreeContext
*/
// TODO: debouncing the load tree method because multiple props can be set at the same time
// that would trigger multiple loadTree calls. This is a temporary solution to avoid that.
public loadTree = debounce(() => this.#debouncedLoadTree(), 100);
if (data) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.#treeRoot.setValue(data);
/**
* Reloads the tree
* @memberof UmbDefaultTreeContext
*/
public loadMore = () => this.#debouncedLoadTree(true);
#debouncedLoadTree(reload = false) {
if (this.getStartFrom()) {
this.#loadRootItems(reload);
return;
}
const hideTreeRoot = this.getHideTreeRoot();
if (hideTreeRoot) {
this.#loadRootItems(reload);
return;
}
}
public async loadRootItems() {
async #loadTreeRoot() {
await this.#init;
const { data } = await this.#repository!.requestRootTreeItems({
skip: this.#paging.skip,
take: this.#paging.take,
});
const { data } = await this.#repository!.requestTreeRoot();
if (data) {
this.#rootItems.setValue(data.items);
this.#treeRoot.setValue(data);
this.pagination.setTotalItems(1);
}
}
async #loadRootItems(loadMore = false) {
await this.#init;
const skip = loadMore ? this.#paging.skip : 0;
const take = loadMore ? this.#paging.take : this.pagination.getCurrentPageNumber() * this.#paging.take;
// If we have a start node get children of that instead of the root
const startNode = this.getStartFrom();
const { data } = startNode?.unique
? await this.#repository!.requestTreeItemsOf({
parent: {
unique: startNode.unique,
entityType: startNode.entityType,
},
skip,
take,
})
: await this.#repository!.requestRootTreeItems({
skip,
take,
});
if (data) {
if (loadMore) {
const currentItems = this.#rootItems.getValue();
this.#rootItems.setValue([...currentItems, ...data.items]);
} else {
this.#rootItems.setValue(data.items);
}
this.pagination.setTotalItems(data.total);
}
}
/**
* Sets the hideTreeRoot config
* @param {boolean} hideTreeRoot
* @memberof UmbDefaultTreeContext
*/
setHideTreeRoot(hideTreeRoot: boolean) {
this.#hideTreeRoot.setValue(hideTreeRoot);
// we need to reset the tree if this config changes
this.#resetTree();
this.loadTree();
}
/**
* Gets the hideTreeRoot config
* @return {boolean}
* @memberof UmbDefaultTreeContext
*/
getHideTreeRoot() {
return this.#hideTreeRoot.getValue();
}
/**
* Sets the startNode config
* @param {UmbTreeStartNode} startNode
* @memberof UmbDefaultTreeContext
*/
setStartFrom(startNode: UmbTreeStartNode | undefined) {
this.#startNode.setValue(startNode);
// we need to reset the tree if this config changes
this.#resetTree();
this.loadTree();
}
/**
* Gets the startNode config
* @return {UmbTreeStartNode}
* @memberof UmbDefaultTreeContext
*/
getStartFrom() {
return this.#startNode.getValue();
}
#resetTree() {
this.#treeRoot.setValue(undefined);
this.#rootItems.setValue([]);
this.pagination.clear();
}
#consumeContexts() {
this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (instance) => {
this.#actionEventContext = instance;
@@ -157,7 +259,7 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
#onPageChange = (event: UmbChangeEvent) => {
const target = event.target as UmbPaginationManager;
this.#paging.skip = target.getSkip();
this.loadRootItems();
this.loadMore();
};
#observeRepository(repositoryAlias?: string) {
@@ -179,11 +281,9 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
// Only handle root request here. Items are handled by the tree item context
const treeRoot = this.#treeRoot.getValue();
if (treeRoot === undefined) return;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (event.getUnique() !== treeRoot.unique) return;
if (event.getEntityType() !== treeRoot.entityType) return;
this.loadRootItems();
this.loadTree();
};
destroy(): void {
@@ -197,4 +297,6 @@ export class UmbDefaultTreeContext<TreeItemType extends UmbTreeItemModelBase>
export default UmbDefaultTreeContext;
export const UMB_DEFAULT_TREE_CONTEXT = new UmbContextToken<UmbDefaultTreeContext<any>>('UmbTreeContext');
export const UMB_DEFAULT_TREE_CONTEXT = new UmbContextToken<UmbDefaultTreeContext<UmbTreeItemModel, UmbTreeRootModel>>(
'UmbTreeContext',
);

View File

@@ -1,4 +1,10 @@
import type { UmbTreeItemModelBase, UmbTreeSelectionConfiguration } from '../types.js';
import type {
UmbTreeItemModel,
UmbTreeItemModelBase,
UmbTreeRootModel,
UmbTreeSelectionConfiguration,
UmbTreeStartNode,
} from '../types.js';
import type { UmbDefaultTreeContext } from './default-tree.context.js';
import { UMB_DEFAULT_TREE_CONTEXT } from './default-tree.context.js';
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
@@ -22,6 +28,9 @@ export class UmbDefaultTreeElement extends UmbLitElement {
@property({ type: Boolean, attribute: false })
hideTreeRoot: boolean = false;
@property({ type: Object, attribute: false })
startNode?: UmbTreeStartNode;
@property({ attribute: false })
selectableFilter: (item: UmbTreeItemModelBase) => boolean = () => true;
@@ -29,10 +38,10 @@ export class UmbDefaultTreeElement extends UmbLitElement {
filter: (item: UmbTreeItemModelBase) => boolean = () => true;
@state()
private _rootItems: UmbTreeItemModelBase[] = [];
private _rootItems: UmbTreeItemModel[] = [];
@state()
private _treeRoot?: UmbTreeItemModelBase;
private _treeRoot?: UmbTreeRootModel;
@state()
private _currentPage = 1;
@@ -40,7 +49,7 @@ export class UmbDefaultTreeElement extends UmbLitElement {
@state()
private _totalPages = 1;
#treeContext?: UmbDefaultTreeContext<UmbTreeItemModelBase>;
#treeContext?: UmbDefaultTreeContext<UmbTreeItemModel, UmbTreeRootModel>;
#init: Promise<unknown>;
constructor() {
@@ -70,11 +79,12 @@ export class UmbDefaultTreeElement extends UmbLitElement {
this.#treeContext!.selection.setSelection(this._selectionConfiguration.selection ?? []);
}
if (_changedProperties.has('startNode')) {
this.#treeContext!.setStartFrom(this.startNode);
}
if (_changedProperties.has('hideTreeRoot')) {
if (this.hideTreeRoot === true) {
await this.#init;
this.#treeContext!.loadRootItems();
}
this.#treeContext!.setHideTreeRoot(this.hideTreeRoot);
}
if (_changedProperties.has('selectableFilter')) {
@@ -104,7 +114,7 @@ export class UmbDefaultTreeElement extends UmbLitElement {
}
#renderRootItems() {
// only shot the root items directly if the tree root is hidden
// only show the root items directly if the tree root is hidden
if (this.hideTreeRoot === true) {
return html`
${repeat(

View File

@@ -1,12 +1,10 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
import type { UmbFolderModel } from '@umbraco-cms/backoffice/tree';
export interface UmbFolderCreateModalData {
folderRepositoryAlias: string;
parent: {
unique: string | null;
entityType: string;
};
parent: UmbEntityModel;
}
export interface UmbFolderCreateModalValue {

View File

@@ -1,6 +1,6 @@
import type { UmbTreeItemContext } from '../tree-item-context.interface.js';
import { UMB_DEFAULT_TREE_CONTEXT, type UmbDefaultTreeContext } from '../../default/default-tree.context.js';
import type { UmbTreeItemModelBase } from '../../types.js';
import type { UmbTreeItemModel, UmbTreeRootModel } from '../../types.js';
import { UmbRequestReloadTreeItemChildrenEvent } from '../../reload-tree-item-children/index.js';
import { map } from '@umbraco-cms/backoffice/external/rxjs';
import { UMB_SECTION_CONTEXT, UMB_SECTION_SIDEBAR_CONTEXT } from '@umbraco-cms/backoffice/section';
@@ -17,11 +17,10 @@ import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action
import { UmbPaginationManager, debounce } from '@umbraco-cms/backoffice/utils';
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
export type UmbTreeItemUniqueFunction<TreeItemType extends UmbTreeItemModelBase> = (
x: TreeItemType,
) => string | null | undefined;
export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemModelBase>
export abstract class UmbTreeItemContextBase<
TreeItemType extends UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel,
>
extends UmbContextBase<UmbTreeItemContext<TreeItemType>>
implements UmbTreeItemContext<TreeItemType>
{
@@ -63,11 +62,10 @@ export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemMod
#path = new UmbStringState('');
readonly path = this.#path.asObservable();
treeContext?: UmbDefaultTreeContext<TreeItemType>;
treeContext?: UmbDefaultTreeContext<TreeItemType, TreeRootType>;
#sectionContext?: UmbSectionContext;
#sectionSidebarContext?: UmbSectionSidebarContext;
#actionEventContext?: UmbActionEventContext;
#getUniqueFunction: UmbTreeItemUniqueFunction<TreeItemType>;
// TODO: get this from the tree context
#paging = {
@@ -75,10 +73,9 @@ export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemMod
take: 50,
};
constructor(host: UmbControllerHost, getUniqueFunction: UmbTreeItemUniqueFunction<TreeItemType>) {
constructor(host: UmbControllerHost) {
super(host, UMB_TREE_ITEM_CONTEXT);
this.pagination.setPageSize(this.#paging.take);
this.#getUniqueFunction = getUniqueFunction;
this.#consumeContexts();
// listen for page changes on the pagination manager
@@ -134,10 +131,9 @@ export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemMod
return;
}
const unique = this.#getUniqueFunction(treeItem);
// Only check for undefined. The tree root has null as unique
if (unique === undefined) throw new Error('Could not create tree item context, unique key is missing');
this.unique = unique;
if (treeItem.unique === undefined) throw new Error('Could not create tree item context, unique is missing');
this.unique = treeItem.unique;
if (!treeItem.entityType) throw new Error('Could not create tree item context, tree item type is missing');
this.entityType = treeItem.entityType;
@@ -152,22 +148,48 @@ export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemMod
this.#observeSectionPath();
}
public async loadChildren() {
/**
* Load children of the tree item
* @memberof UmbTreeItemContextBase
*/
public loadChildren = () => this.#loadChildren();
/**
* Load more children of the tree item
* @memberof UmbTreeItemContextBase
*/
public loadMore = () => this.#loadChildren(true);
async #loadChildren(loadMore = false) {
if (this.unique === undefined) throw new Error('Could not request children, unique key is missing');
if (this.entityType === undefined) throw new Error('Could not request children, entity type is missing');
// TODO: wait for tree context to be ready
const repository = this.treeContext?.getRepository();
if (!repository) throw new Error('Could not request children, repository is missing');
this.#isLoading.setValue(true);
const skip = loadMore ? this.#paging.skip : 0;
const take = loadMore ? this.#paging.take : this.pagination.getCurrentPageNumber() * this.#paging.take;
const { data } = await repository.requestTreeItemsOf({
parentUnique: this.unique,
skip: this.#paging.skip,
take: this.#paging.take,
parent: {
unique: this.unique,
entityType: this.entityType,
},
skip,
take,
});
if (data) {
this.#childItems.setValue(data.items);
if (loadMore) {
const currentItems = this.#childItems.getValue();
this.#childItems.setValue([...currentItems, ...data.items]);
} else {
this.#childItems.setValue(data.items);
}
this.#hasChildren.setValue(data.total > 0);
this.pagination.setTotalItems(data.total);
}
@@ -207,7 +229,9 @@ export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemMod
this.#sectionSidebarContext = instance;
});
this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (treeContext: UmbDefaultTreeContext<TreeItemType>) => {
this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (treeContext) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.treeContext = treeContext;
this.#observeIsSelectable();
this.#observeIsSelected();
@@ -329,7 +353,7 @@ export abstract class UmbTreeItemContextBase<TreeItemType extends UmbTreeItemMod
#onPageChange = (event: UmbChangeEvent) => {
const target = event.target as UmbPaginationManager;
this.#paging.skip = target.getSkip();
this.loadChildren();
this.loadMore();
};
#debouncedCheckIsActive = debounce(() => this.#checkIsActive(), 100);

View File

@@ -1,11 +1,11 @@
import type { UmbTreeItemContext } from '../index.js';
import type { UmbTreeItemModelBase } from '../../types.js';
import type { UmbTreeItemModel } from '../../types.js';
import { UMB_TREE_ITEM_CONTEXT } from './tree-item-context-base.js';
import { html, nothing, state, ifDefined, repeat, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
// eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name
export abstract class UmbTreeItemElementBase<TreeItemModelType extends UmbTreeItemModelBase> extends UmbLitElement {
export abstract class UmbTreeItemElementBase<TreeItemModelType extends UmbTreeItemModel> extends UmbLitElement {
_item?: TreeItemModelType;
@property({ type: Object, attribute: false })
get item(): TreeItemModelType | undefined {

View File

@@ -1,9 +1,9 @@
import type { UmbTreeItemModelBase } from '../types.js';
import type { UmbTreeItemModel } from '../types.js';
import type { UmbPaginationManager } from '../../utils/pagination-manager/pagination.manager.js';
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
export interface UmbTreeItemContext<TreeItemType extends UmbTreeItemModelBase> extends UmbApi {
export interface UmbTreeItemContext<TreeItemType extends UmbTreeItemModel> extends UmbApi {
unique?: string | null;
entityType?: string;
treeItem: Observable<TreeItemType | undefined>;

View File

@@ -1,12 +1,13 @@
import { UmbTreeItemContextBase } from '../tree-item-base/index.js';
import type { UmbUniqueTreeItemModel } from '../../types.js';
import type { UmbTreeItemModel, UmbTreeRootModel } from '../../types.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbDefaultTreeItemContext<
TreeItemModelType extends UmbUniqueTreeItemModel,
> extends UmbTreeItemContextBase<TreeItemModelType> {
TreeItemType extends UmbTreeItemModel,
TreeRootType extends UmbTreeRootModel,
> extends UmbTreeItemContextBase<TreeItemType, TreeRootType> {
constructor(host: UmbControllerHost) {
super(host, (x: UmbUniqueTreeItemModel) => x.unique);
super(host);
}
}

View File

@@ -1,9 +1,9 @@
import { UmbTreeItemElementBase } from '../tree-item-base/index.js';
import type { UmbUniqueTreeItemModel } from '../../types.js';
import type { UmbTreeItemModel } from '../../types.js';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('umb-default-tree-item')
export class UmbDefaultTreeItemElement extends UmbTreeItemElementBase<UmbUniqueTreeItemModel> {}
export class UmbDefaultTreeItemElement extends UmbTreeItemElementBase<UmbTreeItemModel> {}
export default UmbDefaultTreeItemElement;

View File

@@ -103,6 +103,7 @@ export class UmbTreePickerModalElement<TreeItemType extends UmbTreeItemModelBase
selectionConfiguration: this._selectionConfiguration,
filter: this.data?.filter,
selectableFilter: this.data?.pickableFilter,
startNode: this.data?.startNode,
}}
@selection-change=${this.#onSelectionChange}
@selected=${this.#onSelected}

View File

@@ -1,3 +1,4 @@
import type { UmbTreeStartNode } from '../types.js';
import { UMB_TREE_PICKER_MODAL_ALIAS } from './constants.js';
import type { UmbPickerModalData, UmbPickerModalValue, UmbWorkspaceModalData } from '@umbraco-cms/backoffice/modal';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
@@ -15,9 +16,11 @@ export interface UmbTreePickerModalData<
TreeItemType,
PathPatternParamsType extends UmbPathPatternParamsType = UmbPathPatternParamsType,
> extends UmbPickerModalData<TreeItemType> {
hideTreeRoot?: boolean;
treeAlias?: string;
// Consider if it makes sense to move this into the UmbPickerModalData interface, but for now this is a TreePicker feature. [NL]
createAction?: UmbTreePickerModalCreateActionData<PathPatternParamsType>;
startNode?: UmbTreeStartNode;
}
export interface UmbTreePickerModalValue extends UmbPickerModalValue {}

View File

@@ -1,17 +1,18 @@
export interface UmbTreeItemModelBase {
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
export interface UmbTreeItemModelBase extends UmbEntityModel {
name: string;
entityType: string;
hasChildren: boolean;
isFolder: boolean;
icon?: string | null;
}
export interface UmbUniqueTreeItemModel extends UmbTreeItemModelBase {
export interface UmbTreeItemModel extends UmbTreeItemModelBase {
unique: string;
parentUnique: string | null;
parent: UmbEntityModel;
}
export interface UmbUniqueTreeRootModel extends UmbTreeItemModelBase {
export interface UmbTreeRootModel extends UmbTreeItemModelBase {
unique: null;
}
@@ -20,3 +21,8 @@ export type UmbTreeSelectionConfiguration = {
selectable?: boolean;
selection?: Array<string | null>;
};
export interface UmbTreeStartNode {
unique: string;
entityType: string;
}

View File

@@ -13,7 +13,7 @@ export function umbDeepMerge<
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key) && source[key] !== undefined) {
if (source[key]?.constructor === Object && fallback[key].constructor === Object) {
if (source[key]?.constructor === Object && fallback[key]?.constructor === Object) {
result[key] = umbDeepMerge(source[key] as any, fallback[key]);
} else {
result[key] = source[key] as any;

View File

@@ -2,16 +2,22 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
import { UmbNumberState } from '@umbraco-cms/backoffice/observable-api';
export class UmbPaginationManager extends EventTarget {
#defaultValues = {
totalItems: 0,
totalPages: 1,
currentPage: 1,
};
#pageSize = new UmbNumberState(10);
public readonly pageSize = this.#pageSize.asObservable();
#totalItems = new UmbNumberState(0);
#totalItems = new UmbNumberState(this.#defaultValues.totalItems);
public readonly totalItems = this.#totalItems.asObservable();
#totalPages = new UmbNumberState(1);
#totalPages = new UmbNumberState(this.#defaultValues.totalPages);
public readonly totalPages = this.#totalPages.asObservable();
#currentPage = new UmbNumberState(1);
#currentPage = new UmbNumberState(this.#defaultValues.currentPage);
public readonly currentPage = this.#currentPage.asObservable();
#skip = new UmbNumberState(0);
@@ -101,6 +107,17 @@ export class UmbPaginationManager extends EventTarget {
return this.#skip.getValue();
}
/**
* Clears the pagination manager values and resets them to their default values
* @memberof UmbPaginationManager
*/
public clear() {
this.#totalItems.setValue(this.#defaultValues.totalItems);
this.#totalPages.setValue(this.#defaultValues.totalPages);
this.#currentPage.setValue(this.#defaultValues.currentPage);
this.#skip.setValue(0);
}
/**
* Calculates the total number of pages
* @memberof UmbPaginationManager

View File

@@ -1,8 +1,8 @@
import { html, customElement, state, ifDefined, map } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, customElement, html, ifDefined, map, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section';
import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbMenuStructureWorkspaceContext, UmbStructureItemModel } from '@umbraco-cms/backoffice/menu';
@customElement('umb-workspace-breadcrumb')
@@ -84,7 +84,14 @@ export class UmbWorkspaceBreadcrumbElement extends UmbLitElement {
`;
}
static styles = [UmbTextStyles];
static styles = [
UmbTextStyles,
css`
:host {
margin-left: var(--uui-size-layout-1);
}
`,
];
}
export default UmbWorkspaceBreadcrumbElement;

View File

@@ -1,12 +1,12 @@
import { html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, customElement, html, ifDefined, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbVariantDatasetWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
import { UMB_VARIANT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
import type { UmbAppLanguageContext } from '@umbraco-cms/backoffice/language';
import { UMB_APP_LANGUAGE_CONTEXT } from '@umbraco-cms/backoffice/language';
import { UMB_SECTION_CONTEXT } from '@umbraco-cms/backoffice/section';
import { UMB_VARIANT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
import type { UmbAppLanguageContext } from '@umbraco-cms/backoffice/language';
import type { UmbVariantDatasetWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
import type { UmbVariantStructureItemModel } from '@umbraco-cms/backoffice/menu';
@customElement('umb-workspace-variant-menu-breadcrumb')
@@ -116,7 +116,7 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement {
${this._structure.map(
(structureItem) =>
html`<uui-breadcrumb-item href="${ifDefined(this.#getHref(structureItem))}"
>${this.#getItemVariantName(structureItem)}</uui-breadcrumb-item
>${this.localize.string(this.#getItemVariantName(structureItem))}</uui-breadcrumb-item
>`,
)}
<uui-breadcrumb-item>${this._name}</uui-breadcrumb-item>
@@ -124,7 +124,14 @@ export class UmbWorkspaceVariantMenuBreadcrumbElement extends UmbLitElement {
`;
}
static styles = [UmbTextStyles];
static styles = [
UmbTextStyles,
css`
:host {
margin-left: var(--uui-size-layout-1);
}
`,
];
}
export default UmbWorkspaceVariantMenuBreadcrumbElement;

View File

@@ -1,11 +1,10 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, nothing, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router';
import type { ManifestWorkspaceView } from '@umbraco-cms/backoffice/extension-registry';
import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
import { createExtensionElement, UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UmbExtensionsManifestInitializer, createExtensionElement } from '@umbraco-cms/backoffice/extension-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { ManifestWorkspaceView } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router';
/**
* @element umb-workspace-editor
@@ -92,14 +91,15 @@ export class UmbWorkspaceEditorElement extends UmbLitElement {
<slot name="action-menu" slot="action-menu"></slot>
${this.#renderRoutes()}
<slot></slot>
${this.enforceNoFooter
? ''
: html`
<umb-workspace-footer slot="footer">
<slot name="footer-info"></slot>
<slot name="actions" slot="actions"></slot>
</umb-workspace-footer>
`}
${when(
!this.enforceNoFooter,
() => html`
<umb-workspace-footer slot="footer">
<slot name="footer-info"></slot>
<slot name="actions" slot="actions"></slot>
</umb-workspace-footer>
`,
)}
</umb-body-layout>
`;
}
@@ -114,10 +114,10 @@ export class UmbWorkspaceEditorElement extends UmbLitElement {
(view) => view.alias,
(view) => html`
<uui-tab
.label="${view.meta.label ? this.localize.string(view.meta.label) : view.name}"
href="${this._routerPath}/view/${view.meta.pathname}"
?active="${'view/' + view.meta.pathname === this._activePath}">
<umb-icon slot="icon" name="${view.meta.icon}"></umb-icon>
.label="${view.meta.label ? this.localize.string(view.meta.label) : view.name}"
?active=${'view/' + view.meta.pathname === this._activePath}>
<umb-icon slot="icon" name=${view.meta.icon}></umb-icon>
${view.meta.label ? this.localize.string(view.meta.label) : view.name}
</uui-tab>
`,
@@ -132,8 +132,8 @@ export class UmbWorkspaceEditorElement extends UmbLitElement {
if (!this.backPath) return nothing;
return html`
<uui-button
class="back-button"
slot="header"
class="back-button"
compact
href=${this.backPath}
label=${this.localize.term('general_back')}>
@@ -143,20 +143,17 @@ export class UmbWorkspaceEditorElement extends UmbLitElement {
}
#renderRoutes() {
if (!this._routes || this._routes.length === 0) return nothing;
return html`
${this._routes && this._routes.length > 0
? html`
<umb-router-slot
id="router-slot"
.routes="${this._routes}"
@init=${(event: UmbRouterSlotInitEvent) => {
this._routerPath = event.target.absoluteRouterPath;
}}
@change=${(event: UmbRouterSlotChangeEvent) => {
this._activePath = event.target.localActiveViewPath;
}}></umb-router-slot>
`
: nothing}
<umb-router-slot
id="router-slot"
.routes=${this._routes}
@init=${(event: UmbRouterSlotInitEvent) => {
this._routerPath = event.target.absoluteRouterPath;
}}
@change=${(event: UmbRouterSlotChangeEvent) => {
this._activePath = event.target.localActiveViewPath;
}}></umb-router-slot>
`;
}

View File

@@ -1,13 +1,17 @@
import { UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js';
import type { UmbDataTypeItemModel } from '../../repository/item/types.js';
import type { UmbDataTypePickerModalData } from '../../modals/index.js';
import { UMB_DATA_TYPE_PICKER_MODAL } from '../../modals/index.js';
import type { UmbDataTypeTreeItemModel } from '../../tree/types.js';
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbDataTypePickerContext extends UmbPickerInputContext<UmbDataTypeItemModel> {
export class UmbDataTypePickerContext extends UmbPickerInputContext<
UmbDataTypeItemModel,
UmbDataTypeTreeItemModel,
UmbDataTypePickerModalData
> {
constructor(host: UmbControllerHost) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
super(host, UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS, UMB_DATA_TYPE_PICKER_MODAL);
}
}

View File

@@ -1,10 +1,8 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDataTypeCreateOptionsModalData {
parent: {
entityType: string;
unique: string | null;
};
parent: UmbEntityModel;
}
export const UMB_DATA_TYPE_CREATE_OPTIONS_MODAL = new UmbModalToken<UmbDataTypeCreateOptionsModalData>(

View File

@@ -1,12 +1,12 @@
import {
type UmbTreePickerModalValue,
type UmbTreePickerModalData,
type UmbUniqueTreeItemModel,
type UmbTreeItemModel,
UMB_TREE_PICKER_MODAL_ALIAS,
} from '@umbraco-cms/backoffice/tree';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export type UmbDataTypePickerModalData = UmbTreePickerModalData<UmbUniqueTreeItemModel>;
export type UmbDataTypePickerModalData = UmbTreePickerModalData<UmbTreeItemModel>;
export type UmbDataTypePickerModalValue = UmbTreePickerModalValue;
export const UMB_DATA_TYPE_PICKER_MODAL = new UmbModalToken<UmbDataTypePickerModalData, UmbDataTypePickerModalValue>(

View File

@@ -84,7 +84,7 @@ export class UmbPropertyEditorUIPickerModalElement extends UmbModalBaseElement<
render() {
return html`
<umb-body-layout headline="Select Property Editor UI">
<umb-body-layout headline=${this.localize.term('propertyEditorPicker_openPropertyEditorPicker')}>
<uui-box> ${this._renderFilter()} ${this._renderGrid()} </uui-box>
<div slot="actions">
<uui-button label="Close" @click=${this._rejectModal}></uui-button>

View File

@@ -1,3 +1,8 @@
import {
UMB_DATA_TYPE_ENTITY_TYPE,
UMB_DATA_TYPE_FOLDER_ENTITY_TYPE,
UMB_DATA_TYPE_ROOT_ENTITY_TYPE,
} from '../entity.js';
import type { UmbDataTypeTreeItemModel } from './types.js';
import type {
UmbTreeChildrenOfRequestArgs,
@@ -49,12 +54,12 @@ const getRootItems = async (args: UmbTreeRootItemsRequestArgs) => {
};
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
if (args.parentUnique === null) {
if (args.parent.unique === null) {
return getRootItems(args);
} else {
// eslint-disable-next-line local-rules/no-direct-api-import
return DataTypeService.getTreeDataTypeChildren({
parentId: args.parentUnique,
parentId: args.parent.unique,
skip: args.skip,
take: args.take,
});
@@ -64,16 +69,19 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) =>
// eslint-disable-next-line local-rules/no-direct-api-import
DataTypeService.getTreeDataTypeAncestors({
descendantId: args.descendantUnique,
descendantId: args.treeItem.unique,
});
const mapper = (item: DataTypeTreeItemResponseModel): UmbDataTypeTreeItemModel => {
return {
unique: item.id,
parentUnique: item.parent?.id || null,
parent: {
unique: item.parent?.id || null,
entityType: item.parent ? UMB_DATA_TYPE_ENTITY_TYPE : UMB_DATA_TYPE_ROOT_ENTITY_TYPE,
},
icon: manifestPropertyEditorUis.find((ui) => ui.alias === item.editorUiAlias)?.meta.icon,
name: item.name,
entityType: item.isFolder ? 'data-type-folder' : 'data-type',
entityType: item.isFolder ? UMB_DATA_TYPE_FOLDER_ENTITY_TYPE : UMB_DATA_TYPE_ENTITY_TYPE,
isFolder: item.isFolder,
hasChildren: item.hasChildren,
};

View File

@@ -1,10 +1,10 @@
import type { UmbDataTypeEntityType, UmbDataTypeFolderEntityType, UmbDataTypeRootEntityType } from '../entity.js';
import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
export interface UmbDataTypeTreeItemModel extends UmbUniqueTreeItemModel {
export interface UmbDataTypeTreeItemModel extends UmbTreeItemModel {
entityType: UmbDataTypeEntityType | UmbDataTypeFolderEntityType;
}
export interface UmbDataTypeTreeRootModel extends UmbUniqueTreeRootModel {
export interface UmbDataTypeTreeRootModel extends UmbTreeRootModel {
entityType: UmbDataTypeRootEntityType;
}

View File

@@ -40,6 +40,7 @@ export class UmbDataTypeWorkspaceContext
#parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined);
readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined));
readonly parentEntityType = this.#parent.asObservablePart((parent) => (parent ? parent.entityType : undefined));
#persistedData = new UmbObjectState<EntityType | undefined>(undefined);
#currentData = new UmbObjectState<EntityType | undefined>(undefined);
@@ -52,6 +53,7 @@ export class UmbDataTypeWorkspaceContext
readonly name = this.#currentData.asObservablePart((data) => data?.name);
readonly unique = this.#currentData.asObservablePart((data) => data?.unique);
readonly entityType = this.#currentData.asObservablePart((data) => data?.entityType);
readonly propertyEditorUiAlias = this.#currentData.asObservablePart((data) => data?.editorUiAlias);
readonly propertyEditorSchemaAlias = this.#currentData.asObservablePart((data) => data?.editorAlias);

View File

@@ -1,4 +1,4 @@
import type { UmbUniqueTreeItemModel } from '../../core/tree/types.js';
import type { UmbTreeItemModel } from '../../core/tree/types.js';
import { UmbModalToken } from '../../core/modal/token/modal-token.js';
import {
type UmbTreePickerModalValue,
@@ -6,7 +6,7 @@ import {
UMB_TREE_PICKER_MODAL_ALIAS,
} from '@umbraco-cms/backoffice/tree';
export type UmbDictionaryPickerModalData = UmbTreePickerModalData<UmbUniqueTreeItemModel>;
export type UmbDictionaryPickerModalData = UmbTreePickerModalData<UmbTreeItemModel>;
export type UmbDictionaryPickerModalValue = UmbTreePickerModalValue;
export const UMB_DICTIONARY_PICKER_MODAL = new UmbModalToken<

View File

@@ -1,4 +1,4 @@
import { UMB_DICTIONARY_ENTITY_TYPE } from '../entity.js';
import { UMB_DICTIONARY_ENTITY_TYPE, UMB_DICTIONARY_ROOT_ENTITY_TYPE } from '../entity.js';
import type { UmbDictionaryTreeItemModel } from './types.js';
import type {
UmbTreeAncestorsOfRequestArgs,
@@ -40,12 +40,12 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) =>
DictionaryService.getTreeDictionaryRoot({ skip: args.skip, take: args.take });
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
if (args.parentUnique === null) {
if (args.parent.unique === null) {
return getRootItems(args);
} else {
// eslint-disable-next-line local-rules/no-direct-api-import
return DictionaryService.getTreeDictionaryChildren({
parentId: args.parentUnique,
parentId: args.parent.unique,
skip: args.skip,
take: args.take,
});
@@ -55,13 +55,16 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) =>
// eslint-disable-next-line local-rules/no-direct-api-import
DictionaryService.getTreeDictionaryAncestors({
descendantId: args.descendantUnique,
descendantId: args.treeItem.unique,
});
const mapper = (item: NamedEntityTreeItemResponseModel): UmbDictionaryTreeItemModel => {
return {
unique: item.id,
parentUnique: item.parent?.id || null,
parent: {
unique: item.parent?.id || null,
entityType: item.parent ? UMB_DICTIONARY_ENTITY_TYPE : UMB_DICTIONARY_ROOT_ENTITY_TYPE,
},
name: item.name,
entityType: UMB_DICTIONARY_ENTITY_TYPE,
hasChildren: item.hasChildren,

View File

@@ -1,10 +1,10 @@
import type { UmbDictionaryEntityType, UmbDictionaryRootEntityType } from '../entity.js';
import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
export interface UmbDictionaryTreeItemModel extends UmbUniqueTreeItemModel {
export interface UmbDictionaryTreeItemModel extends UmbTreeItemModel {
entityType: UmbDictionaryEntityType;
}
export interface UmbDictionaryTreeRootModel extends UmbUniqueTreeRootModel {
export interface UmbDictionaryTreeRootModel extends UmbTreeRootModel {
entityType: UmbDictionaryRootEntityType;
}

View File

@@ -23,11 +23,14 @@ export class UmbDictionaryWorkspaceContext
#parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined);
readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined));
readonly parentEntityType = this.#parent.asObservablePart((parent) => (parent ? parent.entityType : undefined));
#data = new UmbObjectState<UmbDictionaryDetailModel | undefined>(undefined);
readonly data = this.#data.asObservable();
readonly unique = this.#data.asObservablePart((data) => data?.unique);
readonly entityType = this.#data.asObservablePart((data) => data?.entityType);
readonly name = this.#data.asObservablePart((data) => data?.name);
readonly dictionary = this.#data.asObservablePart((data) => data);

View File

@@ -1,10 +1,8 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDocumentBlueprintOptionsCreateModalData {
parent: {
unique: string | null;
entityType: string;
};
parent: UmbEntityModel;
}
export interface UmbDocumentBlueprintOptionsCreateModalValue {

View File

@@ -1,5 +1,7 @@
export const UMB_DOCUMENT_BLUEPRINT_ROOT_ENTITY_TYPE = 'document-blueprint-root';
export const UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE = 'document-blueprint';
export const UMB_DOCUMENT_BLUEPRINT_FOLDER_ENTITY_TYPE = 'document-blueprint-folder';
export type UmbDocumentBlueprintRootEntityType = typeof UMB_DOCUMENT_BLUEPRINT_ROOT_ENTITY_TYPE;
export type UmbDocumentBlueprintEntityType = typeof UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE;
export type UmbDocumentBlueprintFolderEntityType = typeof UMB_DOCUMENT_BLUEPRINT_FOLDER_ENTITY_TYPE;

View File

@@ -1,4 +1,8 @@
import { UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE, UMB_DOCUMENT_BLUEPRINT_FOLDER_ENTITY_TYPE } from '../entity.js';
import {
UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE,
UMB_DOCUMENT_BLUEPRINT_FOLDER_ENTITY_TYPE,
UMB_DOCUMENT_BLUEPRINT_ROOT_ENTITY_TYPE,
} from '../entity.js';
import type { UmbDocumentBlueprintTreeItemModel } from './types.js';
import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree';
import { DocumentBlueprintService } from '@umbraco-cms/backoffice/external/backend-api';
@@ -40,12 +44,12 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) =>
DocumentBlueprintService.getTreeDocumentBlueprintRoot({ skip: args.skip, take: args.take });
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
if (args.parentUnique === null) {
if (args.parent.unique === null) {
return getRootItems(args);
} else {
// eslint-disable-next-line local-rules/no-direct-api-import
return DocumentBlueprintService.getTreeDocumentBlueprintChildren({
parentId: args.parentUnique,
parentId: args.parent.unique,
});
}
};
@@ -58,7 +62,10 @@ const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => {
const mapper = (item: DocumentBlueprintTreeItemResponseModel): UmbDocumentBlueprintTreeItemModel => {
return {
unique: item.id,
parentUnique: item.parent?.id || null,
parent: {
unique: item.parent ? item.parent.id : null,
entityType: item.parent ? UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE : UMB_DOCUMENT_BLUEPRINT_ROOT_ENTITY_TYPE,
},
name: (item as any).variants?.[0].name ?? item.name,
entityType: item.isFolder ? UMB_DOCUMENT_BLUEPRINT_FOLDER_ENTITY_TYPE : UMB_DOCUMENT_BLUEPRINT_ENTITY_TYPE,
isFolder: item.isFolder,

View File

@@ -1,8 +1,8 @@
import type { UmbDocumentBlueprintEntityType, UmbDocumentBlueprintFolderEntityType } from '../entity.js';
import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
export interface UmbDocumentBlueprintTreeRootModel extends UmbUniqueTreeRootModel {}
export interface UmbDocumentBlueprintTreeRootModel extends UmbTreeRootModel {}
export interface UmbDocumentBlueprintTreeItemModel extends UmbUniqueTreeItemModel {
export interface UmbDocumentBlueprintTreeItemModel extends UmbTreeItemModel {
entityType: UmbDocumentBlueprintEntityType | UmbDocumentBlueprintFolderEntityType;
}

View File

@@ -45,6 +45,7 @@ export class UmbDocumentBlueprintWorkspaceContext
#parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined);
readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined));
readonly parentEntityType = this.#parent.asObservablePart((parent) => (parent ? parent.entityType : undefined));
/**
*/
@@ -62,6 +63,8 @@ export class UmbDocumentBlueprintWorkspaceContext
}
readonly unique = this.#currentData.asObservablePart((data) => data?.unique);
readonly entityType = this.#currentData.asObservablePart((data) => data?.entityType);
readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.documentType.unique);
readonly variants = this.#currentData.asObservablePart((data) => data?.variants || []);
@@ -427,20 +430,6 @@ export class UmbDocumentBlueprintWorkspaceContext
}
}
/*
concept notes:
public saveAndPreview() {
}
*/
/*public createPropertyDatasetContext(host: UmbControllerHost, variantId: UmbVariantId) {
// TODO: [LK] Temporary workaround/hack to get the workspace to load.
const docCxt = new UmbDocumentWorkspaceContext(host);
return new UmbDocumentPropertyDataContext(host, docCxt, variantId);
}*/
public createPropertyDatasetContext(host: UmbControllerHost, variantId: UmbVariantId) {
return new UmbDocumentBlueprintPropertyDataContext(host, this, variantId);
}

View File

@@ -1,3 +1,4 @@
import type { UmbDocumentTypePickerModalData, UmbDocumentTypePickerModalValue } from '../../modals/index.js';
import { UMB_DOCUMENT_TYPE_PICKER_MODAL } from '../../modals/index.js';
import type { UmbDocumentTypeItemModel } from '../../repository/index.js';
import type { UmbDocumentTypeTreeItemModel } from '../../tree/types.js';
@@ -7,11 +8,11 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbDocumentTypePickerContext extends UmbPickerInputContext<
UmbDocumentTypeItemModel,
UmbDocumentTypeTreeItemModel
UmbDocumentTypeTreeItemModel,
UmbDocumentTypePickerModalData,
UmbDocumentTypePickerModalValue
> {
constructor(host: UmbControllerHost) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
super(host, UMB_DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, UMB_DOCUMENT_TYPE_PICKER_MODAL);
}
}

View File

@@ -1,10 +1,8 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDocumentTypeCreateOptionsModalData {
parent: {
unique: string | null;
entityType: string;
};
parent: UmbEntityModel;
}
export const UMB_DOCUMENT_TYPE_CREATE_OPTIONS_MODAL = new UmbModalToken<UmbDocumentTypeCreateOptionsModalData>(

View File

@@ -7,9 +7,6 @@ import {
UMB_TREE_PICKER_MODAL_ALIAS,
} from '@umbraco-cms/backoffice/tree';
/*export interface UmbDocumentTypePickerModalData
extends UmbTreePickerModalData<UmbDocumentTypeTreeItemModel, typeof umbCreateDocumentTypeWorkspacePathGenerator> {}
*/
export type UmbDocumentTypePickerModalData = UmbTreePickerModalData<
UmbDocumentTypeTreeItemModel,
typeof UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.PARAMS

View File

@@ -1,4 +1,8 @@
import { UMB_DOCUMENT_TYPE_ENTITY_TYPE, UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE } from '../entity.js';
import {
UMB_DOCUMENT_TYPE_ENTITY_TYPE,
UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE,
UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE,
} from '../entity.js';
import type { UmbDocumentTypeTreeItemModel } from './types.js';
import type {
UmbTreeAncestorsOfRequestArgs,
@@ -40,12 +44,12 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) =>
DocumentTypeService.getTreeDocumentTypeRoot({ skip: args.skip, take: args.take });
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
if (args.parentUnique === null) {
if (args.parent.unique === null) {
return getRootItems({ skip: args.skip, take: args.take });
} else {
// eslint-disable-next-line local-rules/no-direct-api-import
return DocumentTypeService.getTreeDocumentTypeChildren({
parentId: args.parentUnique,
parentId: args.parent.unique,
skip: args.skip,
take: args.take,
});
@@ -55,13 +59,16 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) =>
// eslint-disable-next-line local-rules/no-direct-api-import
DocumentTypeService.getTreeDocumentTypeAncestors({
descendantId: args.descendantUnique,
descendantId: args.treeItem.unique,
});
const mapper = (item: DocumentTypeTreeItemResponseModel): UmbDocumentTypeTreeItemModel => {
return {
unique: item.id,
parentUnique: item.parent ? item.parent.id : null,
parent: {
unique: item.parent ? item.parent.id : null,
entityType: item.parent ? UMB_DOCUMENT_TYPE_ENTITY_TYPE : UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE,
},
name: item.name,
entityType: item.isFolder ? UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE : UMB_DOCUMENT_TYPE_ENTITY_TYPE,
hasChildren: item.hasChildren,

View File

@@ -3,13 +3,13 @@ import type {
UmbDocumentTypeFolderEntityType,
UmbDocumentTypeRootEntityType,
} from '../entity.js';
import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
export interface UmbDocumentTypeTreeItemModel extends UmbUniqueTreeItemModel {
export interface UmbDocumentTypeTreeItemModel extends UmbTreeItemModel {
entityType: UmbDocumentTypeEntityType | UmbDocumentTypeFolderEntityType;
isElement: boolean;
}
export interface UmbDocumentTypeTreeRootModel extends UmbUniqueTreeRootModel {
export interface UmbDocumentTypeTreeRootModel extends UmbTreeRootModel {
entityType: UmbDocumentTypeRootEntityType;
}

View File

@@ -42,12 +42,14 @@ export class UmbDocumentTypeWorkspaceContext
#parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined);
readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined));
readonly parentEntityType = this.#parent.asObservablePart((parent) => (parent ? parent.entityType : undefined));
#persistedData = new UmbObjectState<EntityType | undefined>(undefined);
// General for content types:
//readonly data;
readonly unique;
readonly entityType;
readonly name;
getName(): string | undefined {
return this.structure.getOwnerContentType()?.name;
@@ -81,6 +83,8 @@ export class UmbDocumentTypeWorkspaceContext
//this.data = this.structure.ownerContentType;
this.unique = this.structure.ownerContentTypeObservablePart((data) => data?.unique);
this.entityType = this.structure.ownerContentTypeObservablePart((data) => data?.entityType);
this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name);
this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias);
this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description);

View File

@@ -83,10 +83,8 @@ export class UmbDocumentTypeWorkspaceViewStructureElement extends UmbLitElement
<umb-input-document-type
.documentTypesOnly=${true}
.selection=${this._allowedContentTypeUniques ?? []}
@change="${(e: CustomEvent) => {
const sortedContentTypesList: Array<UmbContentTypeSortModel> = (
e.target as UmbInputDocumentTypeElement
).selection.map((id, index) => ({
@change="${(e: CustomEvent & { target: UmbInputDocumentTypeElement }) => {
const sortedContentTypesList: Array<UmbContentTypeSortModel> = e.target.selection.map((id, index) => ({
contentType: { unique: id },
sortOrder: index,
}));

View File

@@ -1,4 +1,4 @@
import { html, customElement, property, state, map } from '@umbraco-cms/backoffice/external/lit';
import { css, customElement, html, map, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbDocumentTypeStructureRepository } from '@umbraco-cms/backoffice/document-type';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
@@ -8,9 +8,9 @@ import {
UMB_DOCUMENT_ROOT_ENTITY_TYPE,
UMB_DOCUMENT_WORKSPACE_CONTEXT,
} from '@umbraco-cms/backoffice/document';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import type { ManifestCollectionAction } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbAllowedDocumentTypeModel } from '@umbraco-cms/backoffice/document-type';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
@customElement('umb-create-document-collection-action')
export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
@@ -20,6 +20,9 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
@state()
private _createDocumentPath = '';
@state()
private _currentView?: string;
@state()
private _documentUnique?: string;
@@ -29,6 +32,9 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
@state()
private _popoverOpen = false;
@state()
private _rootPathName?: string;
@state()
private _useInfiniteEditor = false;
@@ -59,6 +65,12 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
});
this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
this.observe(collectionContext.view.currentView, (currentView) => {
this._currentView = currentView?.meta.pathName;
});
this.observe(collectionContext.view.rootPathName, (rootPathName) => {
this._rootPathName = rootPathName;
});
this.observe(collectionContext.filter, (filter) => {
this._useInfiniteEditor = filter.useInfiniteEditor == true;
});
@@ -87,20 +99,22 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
}
#getCreateUrl(item: UmbAllowedDocumentTypeModel) {
// TODO: [LK] I need help with this. I don't know what the infinity editor URL should be.
// TODO: Yes, revisit the path extension of the routable modal, cause this is not pretty...? [NL]
return this._useInfiniteEditor
? this._createDocumentPath +
UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({
parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
parentUnique: this._documentUnique ?? 'null',
documentTypeUnique: item.unique,
})
: UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateAbsolute({
if (this._useInfiniteEditor) {
return (
this._createDocumentPath.replace(`${this._rootPathName}`, `${this._rootPathName}/${this._currentView}`) +
UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({
parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
parentUnique: this._documentUnique ?? 'null',
documentTypeUnique: item.unique,
});
})
);
}
return UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateAbsolute({
parentEntityType: this._documentUnique ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
parentUnique: this._documentUnique ?? 'null',
documentTypeUnique: item.unique,
});
}
render() {
@@ -113,11 +127,9 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
const item = this._allowedDocumentTypes[0];
const label = (this.manifest?.meta.label ?? this.localize.term('general_create')) + ' ' + item.name;
return html`<uui-button
color="default"
href=${this.#getCreateUrl(item)}
label=${label}
look="outline"></uui-button>`;
return html`
<uui-button color="default" href=${this.#getCreateUrl(item)} label=${label} look="outline"></uui-button>
`;
}
#renderDropdown() {
@@ -149,6 +161,14 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement {
</uui-popover-container>
`;
}
static styles = [
css`
uui-scroll-container {
max-height: 500px;
}
`,
];
}
export default UmbCreateDocumentCollectionActionElement;

View File

@@ -1,10 +1,17 @@
import type { UmbDocumentPickerModalData, UmbDocumentPickerModalValue } from '../../modals/index.js';
import { UMB_DOCUMENT_PICKER_MODAL } from '../../modals/index.js';
import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js';
import type { UmbDocumentItemModel } from '../../repository/index.js';
import type { UmbDocumentTreeItemModel } from '../../tree/types.js';
import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbDocumentPickerContext extends UmbPickerInputContext<UmbDocumentItemModel> {
export class UmbDocumentPickerContext extends UmbPickerInputContext<
UmbDocumentItemModel,
UmbDocumentTreeItemModel,
UmbDocumentPickerModalData,
UmbDocumentPickerModalValue
> {
constructor(host: UmbControllerHost) {
super(host, UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, UMB_DOCUMENT_PICKER_MODAL, (entry) => entry.unique);
}

View File

@@ -7,6 +7,7 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter';
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui';
import type { UmbDocumentItemModel } from '@umbraco-cms/backoffice/document';
import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree';
@customElement('umb-input-document')
export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement, '') {
@@ -80,8 +81,8 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement,
return this.#pickerContext.getSelection();
}
@property({ type: String })
startNodeId?: string;
@property({ type: Object, attribute: false })
startNode?: UmbTreeStartNode;
@property({ type: Array })
allowedContentTypeIds?: string[] | undefined;
@@ -156,6 +157,7 @@ export class UmbInputDocumentElement extends UUIFormControlMixin(UmbLitElement,
this.#pickerContext.openPicker({
hideTreeRoot: true,
pickableFilter: this.#pickableFilter,
startNode: this.startNode,
});
}

View File

@@ -1,10 +1,8 @@
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDocumentCreateOptionsModalData {
parent: {
unique: string | null;
entityType: string;
};
parent: UmbEntityModel;
documentType: {
unique: string;
} | null;

View File

@@ -1,10 +1,8 @@
import { UMB_DUPLICATE_DOCUMENT_MODAL_ALIAS } from './constants.js';
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
import { UmbModalToken } from '@umbraco-cms/backoffice/modal';
export interface UmbDuplicateDocumentModalData {
unique: string | null;
entityType: string;
}
export interface UmbDuplicateDocumentModalData extends UmbEntityModel {}
export interface UmbDuplicateDocumentModalValue {
destination: {

View File

@@ -1,10 +1,12 @@
import type { UmbInputDocumentElement } from '../../components/input-document/input-document.element.js';
import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js';
import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor';
import type { NumberRangeValueType } from '@umbraco-cms/backoffice/models';
import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree';
@customElement('umb-property-editor-ui-document-picker')
export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement {
@@ -15,28 +17,30 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
if (!config) return;
const minMax = config.getValueByAlias<NumberRangeValueType>('validationLimit');
this.min = minMax?.min ?? 0;
this.max = minMax?.max ?? Infinity;
if (minMax) {
this._min = minMax.min && minMax.min > 0 ? minMax.min : 0;
this._max = minMax.max && minMax.max > 0 ? minMax.max : Infinity;
}
this.ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes') ?? false;
this.startNodeId = config.getValueByAlias('startNodeId');
this.showOpenButton = config.getValueByAlias('showOpenButton') ?? false;
this._ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes') ?? false;
this._startNodeId = config.getValueByAlias('startNodeId');
this._showOpenButton = config.getValueByAlias('showOpenButton') ?? false;
}
@state()
min = 0;
private _min = 0;
@state()
max = Infinity;
private _max = Infinity;
@state()
startNodeId?: string;
private _startNodeId?: string;
@state()
showOpenButton?: boolean;
private _showOpenButton?: boolean;
@state()
ignoreUserStartNodes?: boolean;
private _ignoreUserStartNodes?: boolean;
#onChange(event: CustomEvent & { target: UmbInputDocumentElement }) {
this.value = event.target.selection.join(',');
@@ -44,14 +48,18 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl
}
render() {
const startNode: UmbTreeStartNode | undefined = this._startNodeId
? { unique: this._startNodeId, entityType: UMB_DOCUMENT_ENTITY_TYPE }
: undefined;
return html`
<umb-input-document
.min=${this.min}
.max=${this.max}
.startNodeId=${this.startNodeId ?? ''}
.min=${this._min}
.max=${this._max}
.startNode=${startNode}
.value=${this.value ?? ''}
?showOpenButton=${this.showOpenButton}
?ignoreUserStartNodes=${this.ignoreUserStartNodes}
?ignoreUserStartNodes=${this._ignoreUserStartNodes}
?showOpenButton=${this._showOpenButton}
@change=${this.#onChange}>
</umb-input-document>
`;

View File

@@ -1,4 +1,5 @@
import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js';
import { UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE } from '../entity.js';
import type { UmbDocumentRecycleBinTreeItemModel } from './types.js';
import type { DocumentRecycleBinItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { DocumentService } from '@umbraco-cms/backoffice/external/backend-api';
@@ -40,12 +41,12 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) =>
DocumentService.getRecycleBinDocumentRoot({ skip: args.skip, take: args.take });
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
if (args.parentUnique === null) {
if (args.parent.unique === null) {
return getRootItems(args);
} else {
// eslint-disable-next-line local-rules/no-direct-api-import
return DocumentService.getRecycleBinDocumentChildren({
parentId: args.parentUnique,
parentId: args.parent.unique,
skip: args.skip,
take: args.take,
});
@@ -55,13 +56,16 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) =>
// eslint-disable-next-line local-rules/no-direct-api-import
DocumentService.getTreeDocumentAncestors({
descendantId: args.descendantUnique,
descendantId: args.treeItem.unique,
});
const mapper = (item: DocumentRecycleBinItemResponseModel): UmbDocumentRecycleBinTreeItemModel => {
return {
unique: item.id,
parentUnique: item.parent ? item.parent.id : null,
parent: {
unique: item.parent ? item.parent.id : null,
entityType: item.parent ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE,
},
entityType: UMB_DOCUMENT_ENTITY_TYPE,
noAccess: false,
isTrashed: true,

View File

@@ -1,6 +1,6 @@
import type { UmbDocumentTreeItemModel } from '../../tree/index.js';
import type { UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree';
import type { UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
export interface UmbDocumentRecycleBinTreeItemModel extends UmbDocumentTreeItemModel {}
export interface UmbDocumentRecycleBinTreeRootModel extends UmbUniqueTreeRootModel {}
export interface UmbDocumentRecycleBinTreeRootModel extends UmbTreeRootModel {}

View File

@@ -1,4 +1,4 @@
import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js';
import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_ROOT_ENTITY_TYPE } from '../entity.js';
import type { UmbDocumentTreeItemModel } from './types.js';
import type {
UmbTreeAncestorsOfRequestArgs,
@@ -40,12 +40,12 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) =>
DocumentService.getTreeDocumentRoot({ skip: args.skip, take: args.take });
const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
if (args.parentUnique === null) {
if (args.parent.unique === null) {
return getRootItems(args);
} else {
// eslint-disable-next-line local-rules/no-direct-api-import
return DocumentService.getTreeDocumentChildren({
parentId: args.parentUnique,
parentId: args.parent.unique,
skip: args.skip,
take: args.take,
});
@@ -55,13 +55,16 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => {
const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) =>
// eslint-disable-next-line local-rules/no-direct-api-import
DocumentService.getTreeDocumentAncestors({
descendantId: args.descendantUnique,
descendantId: args.treeItem.unique,
});
const mapper = (item: DocumentTreeItemResponseModel): UmbDocumentTreeItemModel => {
return {
unique: item.id,
parentUnique: item.parent ? item.parent.id : null,
parent: {
unique: item.parent ? item.parent.id : null,
entityType: item.parent ? UMB_DOCUMENT_ENTITY_TYPE : UMB_DOCUMENT_ROOT_ENTITY_TYPE,
},
entityType: UMB_DOCUMENT_ENTITY_TYPE,
noAccess: item.noAccess,
isTrashed: item.isTrashed,

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