diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index ac61c7ef96..40529614de 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -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", diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts index fd54f80ec6..a0b7abb0d2 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app-auth.controller.ts @@ -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 diff --git a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts index 5e1e385e08..6900aebcb6 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/app/app.element.ts @@ -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(); }, }, { diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index b2dcc78cd8..fa0a3caba6 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 7a039d433e..a1f3c17630 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 388571673a..af418baa1b 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -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', diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/debug/context-data.function.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/debug/context-data.function.ts index c7757a2569..74bdc34d9b 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/debug/context-data.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/debug/context-data.function.ts @@ -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): Array { - const contextData = new Array(); +export function contextData(contexts: Map): Array { + const contextData = new Array(); 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): Array { * @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; - properties?: Array; + properties?: Array; 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; } diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts index 8faa782ac2..b2ea3bc187 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/provide/context-provider.ts @@ -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 { + #handleDebugContextRequest = (event: any): void => { // If the event doesn't have an instances property, create it. if (!event.instances) { event.instances = new Map(); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/entity.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/entity.data.ts index d5f8976d8c..847b6fcccd 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/entity.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/entity.data.ts @@ -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 extends UmbMockDBBase { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts index 4b02ecb73e..1c139bf595 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth-flow.ts @@ -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 { + 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 { + async #performTokenRequest(request: TokenRequest): Promise { 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; } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts index 922a66ea83..f4a418727d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/auth.context.ts @@ -174,6 +174,15 @@ export class UmbAuthContext extends UmbContextBase { 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 { + return this.#isBypassed || this.#authFlow.makeRefreshTokenRequest(); + } + /** * Clears the token storage. * @memberof UmbAuthContext @@ -188,7 +197,6 @@ export class UmbAuthContext extends UmbContextBase { * @memberof UmbAuthContext */ timeOut() { - this.clearTokenStorage(); this.#isAuthorized.setValue(false); this.#isTimeout.next(); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts index d652b7b065..98d3934121 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/modals/umb-app-auth-modal.element.ts @@ -95,14 +95,16 @@ export class UmbAppAuthModalElement extends UmbModalBaseElement boolean + manifestFilter?: (manifest: ManifestCollectionView) => boolean; } export class UmbCollectionViewManager extends UmbControllerBase { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.stories.ts index 983c1741e6..01887f294f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-date/input-date.stories.ts @@ -35,6 +35,5 @@ export const Datetimelocal: Story = { args: { type: 'datetime-local', value: '2023-04-01T10:00:00', - displayValue: '', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts index 6800377a71..eb1725152f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-entity/input-entity.element.ts @@ -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, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-eye-dropper/input-eye-dropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-eye-dropper/input-eye-dropper.element.ts index c4187d0fdd..27f73eaadf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-eye-dropper/input-eye-dropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-eye-dropper/input-eye-dropper.element.ts @@ -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` - - - `; + const swatches = this.showPalette ? this.swatches : undefined; + return when( + this.showPalette && !swatches, + () => html` + + + `, + () => html` + + + `, + ); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-eye-dropper/input-eye-dropper.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-eye-dropper/input-eye-dropper.stories.ts index 3b3e9a73c5..b9d3b5aafd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-eye-dropper/input-eye-dropper.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-eye-dropper/input-eye-dropper.stories.ts @@ -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, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-radio-button-list/input-radio-button-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-radio-button-list/input-radio-button-list.element.ts index 6aa77443d1..b699e41913 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-radio-button-list/input-radio-button-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-radio-button-list/input-radio-button-list.element.ts @@ -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 = []; protected getFormElement() { return undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts index cbb92e39f1..5ab257784e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-slider/input-slider.element.ts @@ -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``; + return html` + + + `; } + #renderRangeSlider() { - return html``; + return html` + + + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts index 180606cbad..7aaae0e3ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field-file.element.ts @@ -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 | Map): 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`${this.label}`; + if (this.path) { + // Don't make it a link if it's a temp file upload. + return this.file ? this.label : html`${this.label}`; + } return html`${this.label}`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts index aabd23654f..086630bcd5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.element.ts @@ -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 = []; - /** - * @description Keys to the files that belong to this upload field. - * @type {Array} - * @default [] - */ - @property({ type: Array }) - public set keys(fileKeys: Array) { - 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 { - 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} * @default undefined */ @property({ type: Array }) - set fileExtensions(value: Array) { + set allowedFileExtensions(value: Array) { this.#setExtensions(value); } - get fileExtensions(): Array | undefined { + get allowedFileExtensions(): Array | 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) { - if (!value) { + #setExtensions(extensions: Array) { + 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` -
${this.#renderFiles()}
- ${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` + accept="${ifDefined(this._extensions?.join(', '))}"> `; } - #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` -
- ${getElementTemplate()} - ${file.queueItem?.status === 'waiting' ? html`` : nothing} +
+
+ ${getElementTemplate()} + ${this.temporaryFile?.status === TemporaryFileStatus.WAITING + ? html`` + : nothing} +
+ ${this.#renderButtonRemove()} `; function getElementTemplate() { - switch (type) { + switch (extension) { case 'audio': - return html``; + return html``; case 'video': - return html``; + return html``; case 'image': - return html``; + return html``; case 'svg': - return html``; - case 'file': - return html``; + return html``; + default: + return html``; } } } - #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` ${this.localize.term('content_uploadClear')} `; } #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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.stories.ts index 4a46a89126..07bc0c8492 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-upload-field/input-upload-field.stories.ts @@ -11,7 +11,5 @@ export default meta; type Story = StoryObj; export const Overview: Story = { - args: { - multiple: false, - }, + args: {}, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-item-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-item-input.element.ts index ccd2947ffb..c9ce9656d2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-item-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/multiple-color-picker-input/multiple-color-picker-item-input.element.ts @@ -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}> - +
${when( this.showLabels, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/stack/stack.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/stack/stack.element.ts index 93b23f27df..23ba4bb164 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/stack/stack.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/stack/stack.element.ts @@ -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`
- -
`; - } + render() { + return html` +
+ +
+ `; + } - 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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/debug.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/debug/debug.element.ts index 1059ecd908..057a5ec557 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/debug/debug.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/debug/debug.element.ts @@ -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(); + private _contextData = Array(); @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) => { // The Contexts are collected // When travelling up through the DOM from this element // to the root of 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` -
- -  Debug - -
`; - } - - private _renderPanel() { - return html`
- - - Debug - - -
-
-
    - ${this._renderContextAliases()} -
-
+
+ + + Debug +
+ `; + } + + #renderPanel() { + return html` +
+ + + Debug + + ${when(this._debugPaneOpen, () => this.#renderContextAliases())} +
+ `; + } + + #renderContextAliases() { + return html`
+ ${map(this._contextData, (context) => { + return html` +
+ ${context.alias} + ${this.#renderInstance(context.data)} +
+ `; + })}
`; } - private _renderContextAliases() { - return repeat( - this.contextData, - (contextData) => contextData.alias, - (contextData) => { - return html`
  • - Context: ${contextData.alias} - (${contextData.type}) -
      - ${this._renderInstance(contextData.data)} -
    -
  • `; - }, - ); - } - - private _renderInstance(instance: DebugContextItemData) { - const instanceTemplates: TemplateResult[] = []; - - if (instance.type === 'function') { - return instanceTemplates.push(html`
  • Callable Function
  • `); - } else if (instance.type === 'object') { - if (instance.methods?.length) { - instanceTemplates.push(html` -
  • - Methods -
      - ${instance.methods?.map((methodName) => html`
    • ${methodName}
    • `)} -
    -
  • - `); + #renderInstance(instance: UmbDebugContextItemData) { + switch (instance.type) { + case 'function': { + return html`

    Callable Function

    `; } - const props: TemplateResult[] = []; - instance.properties?.forEach((property) => { - switch (property.type) { - case 'string': - case 'number': - case 'boolean': - case 'object': - props.push(html`
  • ${property.key} (${property.type}) = ${property.value}
  • `); - break; + case 'object': { + return html` +
    + Methods +
      + ${map(instance.methods, (methodName) => html`
    • ${methodName}
    • `)} +
    +
    - default: - props.push(html`
  • ${property.key} (${property.type})
  • `); - break; - } - }); +
    + Properties +
      + ${map(instance.properties, (property) => { + switch (property.type) { + case 'string': + case 'number': + case 'boolean': + case 'object': + return html`
    • ${property.key} (${property.type}) = ${property.value}
    • `; - instanceTemplates.push(html` -
    • - Properties -
        - ${props} -
      -
    • - `); - } else if (instance.type === 'primitive') { - instanceTemplates.push(html`
    • Context is a primitive with value: ${instance.value}
    • `); + default: + return html`
    • ${property.key} (${property.type})
    • `; + } + })} +
    +
    + `; + } + + case 'primitive': { + return html`

    Context is a primitive with value: ${instance.value}

    `; + } + + default: { + return html`

    Unknown type: ${instance.type}

    `; + } } - - 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; } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/modals/debug/debug-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/debug/modals/debug/debug-modal.element.ts index 255ce97a88..7b045e89fb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/debug/modals/debug/debug-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/debug/modals/debug/debug-modal.element.ts @@ -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 { - private _handleClose() { + #close() { this.modalContext?.reject(); } render() { return html` - - Debug: Contexts - - ${this.data?.content} - - Close - + +
    ${this.data?.content}
    +
    + +
    +
    `; } 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 { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/umb-debug-dialog.jpg b/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/umb-debug-dialog.jpg index 449ae6dc14..d913a56860 100644 Binary files a/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/umb-debug-dialog.jpg and b/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/umb-debug-dialog.jpg differ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/umb-debug.jpg b/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/umb-debug.jpg index a33e519014..f9428c061e 100644 Binary files a/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/umb-debug.jpg and b/src/Umbraco.Web.UI.Client/src/packages/core/debug/stories/umb-debug.jpg differ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/duplicate/duplicate-to/modal/duplicate-to-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/duplicate/duplicate-to/modal/duplicate-to-modal.token.ts index 6ddd2a5dd8..a228dbd907 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/duplicate/duplicate-to/modal/duplicate-to-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/duplicate/duplicate-to/modal/duplicate-to-modal.token.ts @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/modal/sort-children-of-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/modal/sort-children-of-modal.element.ts index 88d1336363..fee3a27755 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/modal/sort-children-of-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/modal/sort-children-of-modal.element.ts @@ -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 = []; + _children: Array = []; @state() _currentPage = 1; @@ -27,7 +27,7 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement< #pagination = new UmbPaginationManager(); #sortedUniques = new Set(); - #sorter?: UmbSorterController; + #sorter?: UmbSorterController; 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>( + const treeRepository = await createExtensionApiByAlias>( 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(this, { + this.#sorter = new UmbSorterController(this, { getUniqueOfElement: (element) => { return element.dataset.unique; }, @@ -174,7 +177,7 @@ export class UmbSortChildrenOfModalElement extends UmbModalBaseElement< `; } - #renderChild(item: UmbUniqueTreeItemModel) { + #renderChild(item: UmbTreeItemModel) { return html``; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/modal/sort-children-of-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/modal/sort-children-of-modal.token.ts index b23cb24834..d5b934779c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/modal/sort-children-of-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/sort-children-of/modal/sort-children-of-modal.token.ts @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-action.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-action.event.ts index 138b4a1381..d49ab78791 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-action.event.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-action.event.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/types.ts index 909af23b20..445778b75a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/types.ts @@ -1,5 +1,5 @@ -export interface UmbEntityActionArgs { - entityType: string; - unique: string | null; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export interface UmbEntityActionArgs extends UmbEntityModel { meta: MetaArgsType; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts index 74308b50c9..d3f60a90c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts @@ -1,2 +1,3 @@ export { UMB_ENTITY_CONTEXT } from './entity.context-token.js'; export { UmbEntityContext } from './entity.context.js'; +export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts new file mode 100644 index 0000000000..dd96ef2420 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts @@ -0,0 +1,6 @@ +export type UmbEntityUnique = string | null; + +export interface UmbEntityModel { + unique: UmbEntityUnique; + entityType: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/tree-item.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/tree-item.model.ts index 368392aedc..135c34f42f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/tree-item.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/tree-item.model.ts @@ -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> { + extends ManifestElementAndApi> { type: 'treeItem'; forEntityTypes: Array; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts index cf97cd2c10..9bb012f722 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-tree-structure-workspace-context-base.ts @@ -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([], (x) => x.unique); public readonly structure = this.#structure.asObservable(); + #parent = new UmbObjectState(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 = []; - const treeRepository = await createExtensionApiByAlias< - UmbTreeRepository - >(this, this.#args.treeRepositoryAlias); + const treeRepository = await createExtensionApiByAlias>( + 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); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts index bc53f28278..9e1e1c2c98 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/menu-variant-tree-structure-workspace-context-base.ts @@ -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([], (x) => x.unique); public readonly structure = this.#structure.asObservable(); + #parent = new UmbObjectState(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 = []; const unique = (await this.observe(uniqueObservable, () => {})?.asPromise()) as string; if (!unique) throw new Error('Unique is not available'); - const treeRepository = await createExtensionApiByAlias>( + 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>( 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); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/menu/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/menu/types.ts index 97ed2f677b..2bb4c80182 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/menu/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/menu/types.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/icon-picker/icon-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/icon-picker/icon-picker-modal.element.ts index 0b86ba4bf8..58d696e7f7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/icon-picker/icon-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/icon-picker/icon-picker-modal.element.ts @@ -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 { @@ -106,12 +104,12 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement + @click=${this._rejectModal}> `; @@ -123,7 +121,7 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement `; @@ -136,15 +134,14 @@ export class UmbIconPickerModalElement extends UmbModalBaseElement icon.name, (icon) => html` this.#changeIcon(e, icon.name)} @keyup=${(e: KeyboardEvent) => this.#changeIcon(e, icon.name)}> - + name=${icon.name}> `, ) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts index ac1b193cb6..992fb667a5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/types.ts @@ -1,6 +1,5 @@ export interface UmbPickerModalData { multiple?: boolean; - hideTreeRoot?: boolean; // TODO: this should be moved to a tree picker modal data interface filter?: (item: ItemType) => boolean; pickableFilter?: (item: ItemType) => boolean; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts index f0e900ceab..56d28dd753 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts index 08519dbb51..f2afe03b2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/picker-input/picker-input.context.ts @@ -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 = UmbPickerModalData, + 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, UmbPickerModalValue>; - repository?: UmbItemRepository; - #getUnique: (entry: ItemType) => string | undefined; + modalAlias: string | UmbModalToken, PickerModalValueType>; + repository?: UmbItemRepository; + #getUnique: (entry: PickedItemType) => string | undefined; #itemManager; @@ -48,14 +49,14 @@ export class UmbPickerInputContext< constructor( host: UmbControllerHost, repositoryAlias: string, - modalAlias: string | UmbModalToken, UmbPickerModalValue>, - getUniqueMethod?: (entry: ItemType) => string | undefined, + modalAlias: string | UmbModalToken, PickerModalValueType>, + getUniqueMethod?: (entry: PickedItemType) => string | undefined, ) { super(host); this.modalAlias = modalAlias; this.#getUnique = getUniqueMethod || ((entry) => entry.unique); - this.#itemManager = new UmbRepositoryItemsManager(this, repositoryAlias, this.#getUnique); + this.#itemManager = new UmbRepositoryItemsManager(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); } - async openPicker(pickerData?: Partial>) { + async openPicker(pickerData?: Partial) { 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(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts index f4f41bda12..92527fa278 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-dataset/property-dataset-context.interface.ts @@ -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. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.token.ts index 63b94f4451..b084a4c564 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-action/restore-from-recycle-bin/modal/restore-from-recycle-bin-modal.token.ts @@ -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< diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts index 2f349c4bbf..0f4921433d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts @@ -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 { - file: File; -} - -export class UmbTemporaryFileManager extends UmbControllerBase { +export class UmbTemporaryFileManager< + UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel, +> extends UmbControllerBase { #temporaryFileRepository; - #queue = new UmbArrayState([], (item) => item.unique); + #queue = new UmbArrayState([], (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> { + async uploadOne(uploadableItem: UploadableItem): Promise { 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): Promise> { + async upload(queueItems: Array): Promise> { 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 = []; + async #handleQueue() { + const filesCompleted: Array = []; 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 }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts index 231fab39b7..18f76771fd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-data-source.interface.ts @@ -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 { +export interface UmbTreeDataSourceConstructor { new (host: UmbControllerHost): UmbTreeDataSource; } @@ -23,7 +23,7 @@ export interface UmbTreeDataSourceConstructor { +export interface UmbTreeDataSource { /** * Gets the root items of the tree. * @return {*} {Promise>>} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts index 01a9abb68f..bbb20fe312 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository-base.ts @@ -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, 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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts index 852ea0d38d..17903eb7e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-repository.interface.ts @@ -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. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts index 1c2e14d22e..c95af216a8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/tree-server-data-source-base.ts @@ -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 { #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)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/types.ts index d94863a55d..b4a8bddb39 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/types.ts @@ -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; + }; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/unique-tree-store.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/unique-tree-store.ts index ed61ac2dc9..5a6f94e720 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/unique-tree-store.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/data/unique-tree-store.ts @@ -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 - implements UmbTreeStore -{ +export class UmbUniqueTreeStore extends UmbStoreBase implements UmbTreeStore { constructor(host: UmbControllerHost, storeAlias: string) { - super(host, storeAlias, new UmbArrayState([], (x) => x.unique)); + super(host, storeAlias, new UmbArrayState([], (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} uniques - * @return {*} - * @memberof UmbUniqueTreeStore - */ - items(uniques: Array) { - return this._data.asObservablePart((items) => items.filter((item) => uniques.includes(item.unique))); + return this._data.asObservablePart((items) => items.filter((item) => item.parent.unique === parentUnique)); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index c55e88dd19..d5311e95b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -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 - extends UmbContextBase> +export class UmbDefaultTreeContext + extends UmbContextBase> implements UmbTreeContext { - #treeRoot = new UmbObjectState(undefined); + #treeRoot = new UmbObjectState(undefined); treeRoot = this.#treeRoot.asObservable(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore #rootItems = new UmbArrayState([], (x) => x.unique); rootItems = this.#rootItems.asObservable(); @@ -34,8 +32,14 @@ export class UmbDefaultTreeContext public readonly selection = new UmbSelectionManager(this._host); public readonly pagination = new UmbPaginationManager(); + #hideTreeRoot = new UmbBooleanState(false); + hideTreeRoot = this.#hideTreeRoot.asObservable(); + + #startNode = new UmbObjectState(undefined); + startNode = this.#startNode.asObservable(); + #manifest?: ManifestTree; - #repository?: UmbTreeRepository; + #repository?: UmbTreeRepository; #actionEventContext?: UmbActionEventContext; #paging = { @@ -51,6 +55,8 @@ export class UmbDefaultTreeContext }); 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 // @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 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 #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 // 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 export default UmbDefaultTreeContext; -export const UMB_DEFAULT_TREE_CONTEXT = new UmbContextToken>('UmbTreeContext'); +export const UMB_DEFAULT_TREE_CONTEXT = new UmbContextToken>( + 'UmbTreeContext', +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts index ca9600010e..9123a01161 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts @@ -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; + #treeContext?: UmbDefaultTreeContext; #init: Promise; 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( diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/modal/folder-create-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/modal/folder-create-modal.token.ts index 898e51318a..e7e5f7a643 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/modal/folder-create-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/modal/folder-create-modal.token.ts @@ -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 { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts index 48433be491..ed7d519de4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts @@ -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 = ( - x: TreeItemType, -) => string | null | undefined; - -export abstract class UmbTreeItemContextBase +export abstract class UmbTreeItemContextBase< + TreeItemType extends UmbTreeItemModel, + TreeRootType extends UmbTreeRootModel, + > extends UmbContextBase> implements UmbTreeItemContext { @@ -63,11 +62,10 @@ export abstract class UmbTreeItemContextBase; + treeContext?: UmbDefaultTreeContext; #sectionContext?: UmbSectionContext; #sectionSidebarContext?: UmbSectionSidebarContext; #actionEventContext?: UmbActionEventContext; - #getUniqueFunction: UmbTreeItemUniqueFunction; // TODO: get this from the tree context #paging = { @@ -75,10 +73,9 @@ export abstract class UmbTreeItemContextBase) { + 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 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) => { + 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 { const target = event.target as UmbPaginationManager; this.#paging.skip = target.getSkip(); - this.loadChildren(); + this.loadMore(); }; #debouncedCheckIsActive = debounce(() => this.#checkIsActive(), 100); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts index 4a61d4302d..d9397601db 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts @@ -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 extends UmbLitElement { +export abstract class UmbTreeItemElementBase extends UmbLitElement { _item?: TreeItemModelType; @property({ type: Object, attribute: false }) get item(): TreeItemModelType | undefined { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-context.interface.ts index a95973075f..37d39f983e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-context.interface.ts @@ -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 extends UmbApi { +export interface UmbTreeItemContext extends UmbApi { unique?: string | null; entityType?: string; treeItem: Observable; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-default/tree-item-default.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-default/tree-item-default.context.ts index 04021f3c1a..837fa430f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-default/tree-item-default.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-default/tree-item-default.context.ts @@ -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 { + TreeItemType extends UmbTreeItemModel, + TreeRootType extends UmbTreeRootModel, +> extends UmbTreeItemContextBase { constructor(host: UmbControllerHost) { - super(host, (x: UmbUniqueTreeItemModel) => x.unique); + super(host); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-default/tree-item-default.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-default/tree-item-default.element.ts index f512cc5f2c..b00ecb48a1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-default/tree-item-default.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-default/tree-item-default.element.ts @@ -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 {} +export class UmbDefaultTreeItemElement extends UmbTreeItemElementBase {} export default UmbDefaultTreeItemElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts index 0ba176209b..94fe7a3c8c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker/tree-picker-modal.element.ts @@ -103,6 +103,7 @@ export class UmbTreePickerModalElement extends UmbPickerModalData { + 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; + startNode?: UmbTreeStartNode; } export interface UmbTreePickerModalValue extends UmbPickerModalValue {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts index 8ca974ed9d..f190e15127 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/types.ts @@ -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; }; + +export interface UmbTreeStartNode { + unique: string; + entityType: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/object/deep-merge.function.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/object/deep-merge.function.ts index 94b67c9b94..2031517316 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/object/deep-merge.function.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/object/deep-merge.function.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/pagination-manager/pagination.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/pagination-manager/pagination.manager.ts index 369cc39820..40fd9ca3a9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/pagination-manager/pagination.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/pagination-manager/pagination.manager.ts @@ -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 diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts index 7ab9dafbd3..165969d621 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-menu-breadcrumb/workspace-menu-breadcrumb.element.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts index 4015162467..08f21e1d6f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-breadcrumb/workspace-variant-menu-breadcrumb/workspace-variant-menu-breadcrumb.element.ts @@ -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`${this.#getItemVariantName(structureItem)}${this.localize.string(this.#getItemVariantName(structureItem))}`, )} ${this._name} @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts index c10e1a71b3..a7558ff5c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts @@ -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 { ${this.#renderRoutes()} - ${this.enforceNoFooter - ? '' - : html` - - - - - `} + ${when( + !this.enforceNoFooter, + () => html` + + + + + `, + )} `; } @@ -114,10 +114,10 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { (view) => view.alias, (view) => html` - + .label="${view.meta.label ? this.localize.string(view.meta.label) : view.name}" + ?active=${'view/' + view.meta.pathname === this._activePath}> + ${view.meta.label ? this.localize.string(view.meta.label) : view.name} `, @@ -132,8 +132,8 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { if (!this.backPath) return nothing; return html` @@ -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` - { - this._routerPath = event.target.absoluteRouterPath; - }} - @change=${(event: UmbRouterSlotChangeEvent) => { - this._activePath = event.target.localActiveViewPath; - }}> - ` - : nothing} + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.localActiveViewPath; + }}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.context.ts index 89c582df5a..ce3caee1d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-input/data-type-input.context.ts @@ -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 { +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); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/modal/index.ts index acf6c03d35..a8fb4ee5e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/modal/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/create/modal/index.ts @@ -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( diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-modal.token.ts index d3b0d87e26..4797592b51 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-modal.token.ts @@ -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; +export type UmbDataTypePickerModalData = UmbTreePickerModalData; export type UmbDataTypePickerModalValue = UmbTreePickerModalValue; export const UMB_DATA_TYPE_PICKER_MODAL = new UmbModalToken( diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/property-editor-ui-picker/property-editor-ui-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/property-editor-ui-picker/property-editor-ui-picker-modal.element.ts index 6c19375a8e..fa3eb99eb0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/property-editor-ui-picker/property-editor-ui-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/property-editor-ui-picker/property-editor-ui-picker-modal.element.ts @@ -84,7 +84,7 @@ export class UmbPropertyEditorUIPickerModalElement extends UmbModalBaseElement< render() { return html` - + ${this._renderFilter()} ${this._renderGrid()}
    diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.server.data-source.ts index add7e96529..b78a1482e5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/data-type-tree.server.data-source.ts @@ -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, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/types.ts index b3a44373f3..1594072a69 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/types.ts @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts index e5d7544bcc..0808e07aff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/data-type-workspace.context.ts @@ -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(undefined); #currentData = new UmbObjectState(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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/modals/dictionary-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/modals/dictionary-picker-modal.token.ts index e967c0a391..a8d6622e58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/modals/dictionary-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/modals/dictionary-picker-modal.token.ts @@ -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; +export type UmbDictionaryPickerModalData = UmbTreePickerModalData; export type UmbDictionaryPickerModalValue = UmbTreePickerModalValue; export const UMB_DICTIONARY_PICKER_MODAL = new UmbModalToken< diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.server.data-source.ts index 626ae02e76..7a1df36114 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/dictionary-tree.server.data-source.ts @@ -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, diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/types.ts index dff0fc223b..ab7600d2a6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/tree/types.ts @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts index 63bdb762cb..7ca3b32fc8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/dictionary-workspace.context.ts @@ -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(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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/document-blueprint-options-create-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/document-blueprint-options-create-modal.token.ts index 3ca912c321..6d261ca4c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/document-blueprint-options-create-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity-actions/create/modal/document-blueprint-options-create-modal.token.ts @@ -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 { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity.ts index 9c0cff3d89..8aa8201389 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/entity.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/document-blueprint-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/document-blueprint-tree.server.data-source.ts index f0bd797c6a..2e69d25135 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/document-blueprint-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/document-blueprint-tree.server.data-source.ts @@ -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, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/types.ts index fbe951351d..4c3b9491b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/tree/types.ts @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts index c63c4f4bd3..642472eed8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts @@ -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); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.context.ts index f89c8e3f70..059557f931 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.context.ts @@ -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); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/modal/index.ts index 0b916444d9..2a256bc68d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/modal/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/modal/index.ts @@ -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( diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts index 177b0bef5b..115868acf2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts @@ -7,9 +7,6 @@ import { UMB_TREE_PICKER_MODAL_ALIAS, } from '@umbraco-cms/backoffice/tree'; -/*export interface UmbDocumentTypePickerModalData - extends UmbTreePickerModalData {} -*/ export type UmbDocumentTypePickerModalData = UmbTreePickerModalData< UmbDocumentTypeTreeItemModel, typeof UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.PARAMS diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.server.data-source.ts index f8df358f0f..f24ada9e7d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/document-type.tree.server.data-source.ts @@ -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, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/types.ts index a404aca3ff..24d72d14ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/types.ts @@ -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; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts index af9b15e7c5..5b8d881c09 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace.context.ts @@ -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(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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts index 77f7b4777b..9cfd4c2664 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/structure/document-type-workspace-view-structure.element.ts @@ -83,10 +83,8 @@ export class UmbDocumentTypeWorkspaceViewStructureElement extends UmbLitElement { + const sortedContentTypesList: Array = e.target.selection.map((id, index) => ({ contentType: { unique: id }, sortOrder: index, })); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts index 334daf9de0..7948afae45 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/action/create-document-collection-action.element.ts @@ -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``; + return html` + + `; } #renderDropdown() { @@ -149,6 +161,14 @@ export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { `; } + + static styles = [ + css` + uui-scroll-container { + max-height: 500px; + } + `, + ]; } export default UmbCreateDocumentCollectionActionElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts index 81476fe796..7f31b74cf7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.context.ts @@ -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 { +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); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts index 48dd116fc8..059d7cfbdc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/components/input-document/input-document.element.ts @@ -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, }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.token.ts index 0ecb35b561..e3e7d55634 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/document-create-options-modal.token.ts @@ -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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/modal/duplicate-document-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/modal/duplicate-document-modal.token.ts index 894b715948..ecd9639a4b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/modal/duplicate-document-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/modal/duplicate-document-modal.token.ts @@ -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: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/property-editor-ui-document-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/property-editor-ui-document-picker.element.ts index 042664de21..ee1a04e487 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/property-editor-ui-document-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-editors/document-picker/property-editor-ui-document-picker.element.ts @@ -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('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` `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/document-recycle-bin-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/document-recycle-bin-tree.server.data-source.ts index 45adeddff3..9ce6d1708f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/document-recycle-bin-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/document-recycle-bin-tree.server.data-source.ts @@ -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, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/types.ts index d84c7821f1..1f1d38718b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/types.ts @@ -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 {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts index c10981c388..f3f4c94b89 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/document-tree.server.data-source.ts @@ -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, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/tree-item/document-tree-item.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/tree-item/document-tree-item.context.ts index 1b70651306..e4ab0f8a85 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/tree-item/document-tree-item.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/tree-item/document-tree-item.context.ts @@ -1,9 +1,12 @@ -import type { UmbDocumentTreeItemModel } from '../types.js'; +import type { UmbDocumentTreeItemModel, UmbDocumentTreeRootModel } from '../types.js'; import { UmbDefaultTreeItemContext } from '@umbraco-cms/backoffice/tree'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; -export class UmbDocumentTreeItemContext extends UmbDefaultTreeItemContext { +export class UmbDocumentTreeItemContext extends UmbDefaultTreeItemContext< + UmbDocumentTreeItemModel, + UmbDocumentTreeRootModel +> { // TODO: Provide this together with the EntityContext, ideally this takes part via a extension-type [NL] #isTrashedContext = new UmbIsTrashedEntityContext(this); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts index e31730d723..e97a5168ce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/types.ts @@ -1,9 +1,9 @@ import type { UmbDocumentEntityType, UmbDocumentRootEntityType } from '../entity.js'; -import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; import type { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; -export interface UmbDocumentTreeItemModel extends UmbUniqueTreeItemModel { +export interface UmbDocumentTreeItemModel extends UmbTreeItemModel { entityType: UmbDocumentEntityType; noAccess: boolean; isTrashed: boolean; @@ -16,7 +16,7 @@ export interface UmbDocumentTreeItemModel extends UmbUniqueTreeItemModel { variants: Array; } -export interface UmbDocumentTreeRootModel extends UmbUniqueTreeRootModel { +export interface UmbDocumentTreeRootModel extends UmbTreeRootModel { entityType: UmbDocumentRootEntityType; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index a496a3a08e..eaab4e1709 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -75,6 +75,7 @@ export class UmbDocumentWorkspaceContext #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)); /** * The document is the current state/draft version of the document. @@ -100,6 +101,7 @@ export class UmbDocumentWorkspaceContext } readonly unique = this.#currentData.asObservablePart((data) => data?.unique); + readonly entityType = this.#currentData.asObservablePart((data) => data?.entityType); readonly isTrashed = this.#currentData.asObservablePart((data) => data?.isTrashed); readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.documentType.unique); diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts index 3f8cb8a543..e669b542af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts @@ -544,20 +544,23 @@ export class UmbInputMarkdownElement extends UUIFormControlMixin(UmbLitElement, } render() { - return html`
    ${this._renderBasicActions()}
    + return html` +
    ${this._renderBasicActions()}
    - ${when(this.preview && this.value, () => this.renderPreview(this.value as string))}`; + @input=${this.#onInput}> + + ${when(this.preview && this.value, () => this.renderPreview(this.value as string))} + `; } renderPreview(markdown: string) { const markdownAsHtml = marked.parse(markdown) as string; const sanitizedHtml = markdownAsHtml ? DOMPurify.sanitize(markdownAsHtml) : ''; - return html` ${unsafeHTML(sanitizedHtml)} `; + return html`${unsafeHTML(sanitizedHtml)}`; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts index 91d1abdda4..407ef70206 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts @@ -27,17 +27,19 @@ export class UmbPropertyEditorUIMarkdownEditorElement extends UmbLitElement impl this._overlaySize = config?.getValueByAlias('overlaySize') ?? undefined; } - #onChange(e: Event) { - this.value = (e.target as UmbInputMarkdownElement).value as string; + #onChange(event: Event & { target: UmbInputMarkdownElement }) { + this.value = event.target.value as string; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } render() { - return html``; + return html` + + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.context.ts index dfe3fea1f2..61f98a0a1d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.context.ts @@ -1,13 +1,21 @@ import type { UmbMediaTypeItemModel } from '../../repository/index.js'; import { UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; +import type { + UmbMediaTypePickerModalData, + UmbMediaTypePickerModalValue, +} from '../../tree/media-type-picker-modal.token.js'; import { UMB_MEDIA_TYPE_PICKER_MODAL } from '../../tree/media-type-picker-modal.token.js'; +import type { UmbMediaTypeTreeItemModel } from '../../tree/types.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMediaTypePickerContext extends UmbPickerInputContext { +export class UmbMediaTypePickerContext extends UmbPickerInputContext< + UmbMediaTypeItemModel, + UmbMediaTypeTreeItemModel, + UmbMediaTypePickerModalData, + UmbMediaTypePickerModalValue +> { constructor(host: UmbControllerHost) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore super(host, UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TYPE_PICKER_MODAL); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/modal/index.ts index 579a908682..bbfa4352ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/modal/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/modal/index.ts @@ -1,10 +1,8 @@ +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbMediaTypeCreateOptionsModalData { - parent: { - unique: string | null; - entityType: string; - }; + parent: UmbEntityModel; } export const UMB_MEDIA_TYPE_CREATE_OPTIONS_MODAL = new UmbModalToken( diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts index 601f28885f..39c2b4ea8b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/index.ts @@ -5,9 +5,8 @@ export * from './workspace/index.js'; export * from './repository/index.js'; export * from './tree/types.js'; +export * from './utils.ts/index.js'; export * from './types.js'; export * from './entity.js'; -export * from './utils/index.js'; - export { UMB_MEDIA_TYPE_PICKER_MODAL } from './tree/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts index 0db7b421b7..86733391e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.repository.ts @@ -4,8 +4,22 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContentTypeStructureRepositoryBase } from '@umbraco-cms/backoffice/content-type'; export class UmbMediaTypeStructureRepository extends UmbContentTypeStructureRepositoryBase { + #dataSource; constructor(host: UmbControllerHost) { super(host, UmbMediaTypeStructureServerDataSource); + this.#dataSource = new UmbMediaTypeStructureServerDataSource(host); + } + + async requestMediaTypesOf({ + fileExtension, + skip = 0, + take = 100, + }: { + fileExtension: string; + skip?: number; + take?: number; + }) { + return this.#dataSource.getMediaTypesOfFileExtension({ fileExtension, skip, take }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts index 22b833b379..acdfad98f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/structure/media-type-structure.server.data-source.ts @@ -17,6 +17,10 @@ export class UmbMediaTypeStructureServerDataSource extends UmbContentTypeStructu constructor(host: UmbControllerHost) { super(host, { getAllowedChildrenOf, mapper }); } + + getMediaTypesOfFileExtension({ fileExtension, skip, take }: { fileExtension: string; skip: number; take: number }) { + return getAllowedMediaTypesOfExtension({ fileExtension, skip, take }); + } } const getAllowedChildrenOf = (unique: string | null) => { @@ -37,3 +41,17 @@ const mapper = (item: AllowedMediaTypeModel): UmbAllowedMediaTypeModel => { icon: item.icon || null, }; }; + +const getAllowedMediaTypesOfExtension = async ({ + fileExtension, + skip, + take, +}: { + fileExtension: string; + skip: number; + take: number; +}) => { + // eslint-disable-next-line local-rules/no-direct-api-import + const { items } = await MediaTypeService.getItemMediaTypeAllowed({ fileExtension, skip, take }); + return items.map((item) => mapper(item)); +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-picker-modal.token.ts index c6ecbe0509..e6079fb4ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-picker-modal.token.ts @@ -2,11 +2,11 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; import { type UmbTreePickerModalValue, type UmbTreePickerModalData, - type UmbUniqueTreeItemModel, + type UmbTreeItemModel, UMB_TREE_PICKER_MODAL_ALIAS, } from '@umbraco-cms/backoffice/tree'; -export type UmbMediaTypePickerModalData = UmbTreePickerModalData; +export type UmbMediaTypePickerModalData = UmbTreePickerModalData; export type UmbMediaTypePickerModalValue = UmbTreePickerModalValue; export const UMB_MEDIA_TYPE_PICKER_MODAL = new UmbModalToken( diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.server.data-source.ts index 45ab65334e..2e9e6a6bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/media-type-tree.server.data-source.ts @@ -1,4 +1,8 @@ -import { UMB_MEDIA_TYPE_ENTITY_TYPE, UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE } from '../entity.js'; +import { + UMB_MEDIA_TYPE_ENTITY_TYPE, + UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE, + UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, +} from '../entity.js'; import type { UmbMediaTypeTreeItemModel } from './types.js'; import type { MediaTypeTreeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MediaTypeService } from '@umbraco-cms/backoffice/external/backend-api'; @@ -40,12 +44,12 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) => MediaTypeService.getTreeMediaTypeRoot({ 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 MediaTypeService.getTreeMediaTypeChildren({ - 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 MediaTypeService.getTreeMediaTypeAncestors({ - descendantId: args.descendantUnique, + descendantId: args.treeItem.unique, }); const mapper = (item: MediaTypeTreeItemResponseModel): UmbMediaTypeTreeItemModel => { return { unique: item.id, - parentUnique: item.parent ? item.parent.id : null, + parent: { + unique: item.parent ? item.parent.id : null, + entityType: item.parent ? UMB_MEDIA_TYPE_ENTITY_TYPE : UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, + }, name: item.name, entityType: item.isFolder ? UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE : UMB_MEDIA_TYPE_ENTITY_TYPE, hasChildren: item.hasChildren, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/types.ts index 83e715ea89..daf75873c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/types.ts @@ -1,10 +1,10 @@ import type { UmbMediaTypeEntityType, UmbMediaTypeFolderEntityType, UmbMediaTypeRootEntityType } from '../entity.js'; -import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; -export interface UmbMediaTypeTreeItemModel extends UmbUniqueTreeItemModel { +export interface UmbMediaTypeTreeItemModel extends UmbTreeItemModel { entityType: UmbMediaTypeEntityType | UmbMediaTypeFolderEntityType; } -export interface UmbMediaTypeTreeRootModel extends UmbUniqueTreeRootModel { +export interface UmbMediaTypeTreeRootModel extends UmbTreeRootModel { entityType: UmbMediaTypeRootEntityType; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts new file mode 100644 index 0000000000..fd3a9e966d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils.ts/index.ts @@ -0,0 +1,7 @@ +//TODO Can we trust this is the unique? This probably need a similar solution like the media collection repository method getDefaultConfiguration() +export function getUmbracoFolderUnique(): string { + return 'f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d'; +} +export function isUmbracoFolder(unique?: string): boolean { + return unique === getUmbracoFolderUnique(); +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts deleted file mode 100644 index b67055a38e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/utils/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -export enum UmbMediaTypeFileType { - SVG = 'Vector Graphics (SVG)', - IMAGE = 'Image', - AUDIO = 'Audio', - VIDEO = 'Video', - ARTICLE = 'Article', - FILE = 'File', -} - -export function getMediaTypeByFileExtension(extension: string) { - if (extension === 'svg') return UmbMediaTypeFileType.SVG; - if (['jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff', 'tif', 'webp'].includes(extension)) - return UmbMediaTypeFileType.IMAGE; - if (['mp3', 'weba', 'oga', 'opus'].includes(extension)) return UmbMediaTypeFileType.AUDIO; - if (['mp4', 'webm', 'ogv'].includes(extension)) return UmbMediaTypeFileType.VIDEO; - if (['pdf', 'docx', 'doc'].includes(extension)) return UmbMediaTypeFileType.ARTICLE; - return UmbMediaTypeFileType.FILE; -} - -export function getMediaTypeByFileMimeType(mimetype: string) { - if (mimetype === 'image/svg+xml') return UmbMediaTypeFileType.SVG; - const [type, extension] = mimetype.split('/'); - if (type === 'image') return UmbMediaTypeFileType.IMAGE; - if (type === 'audio') return UmbMediaTypeFileType.AUDIO; - if (type === 'video') return UmbMediaTypeFileType.VIDEO; - if (['pdf', 'docx', 'doc'].includes(extension)) return UmbMediaTypeFileType.ARTICLE; - return UmbMediaTypeFileType.FILE; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts index f11010df0d..ff0642ff09 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts @@ -33,12 +33,14 @@ export class UmbMediaTypeWorkspaceContext #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(undefined); // General for content types: readonly data; readonly unique; + readonly entityType; readonly name; getName(): string | undefined { return this.structure.getOwnerContentType()?.name; @@ -63,6 +65,7 @@ export class UmbMediaTypeWorkspaceContext // General for content types: 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); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts index a636d72ac1..01a482a323 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts @@ -32,7 +32,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { return html` ${when(this._progress >= 0, () => html``)} - + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts index 57d968794a..729e57f8b2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts @@ -5,7 +5,7 @@ export interface UmbMediaCollectionFilterModel extends UmbCollectionFilterModel dataTypeId?: string; orderBy?: string; orderDirection?: 'asc' | 'desc'; - userDefinedProperties: Array<{alias: string, header: string, isSystem: boolean}>; + userDefinedProperties: Array<{ alias: string; header: string; isSystem: boolean }>; } export interface UmbMediaCollectionItemModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts deleted file mode 100644 index 6a03dc5e19..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/dropzone-media.element.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { UmbMediaDetailRepository } from '../../repository/index.js'; -import type { UmbMediaDetailModel } from '../../types.js'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { - type UmbAllowedMediaTypeModel, - UmbMediaTypeStructureRepository, - getMediaTypeByFileMimeType, -} from '@umbraco-cms/backoffice/media-type'; -import { - UmbTemporaryFileManager, - type UmbTemporaryFileQueueModel, - type UmbTemporaryFileModel, -} from '@umbraco-cms/backoffice/temporary-file'; -import { UmbChangeEvent, UmbProgressEvent } from '@umbraco-cms/backoffice/event'; - -@customElement('umb-dropzone-media') -export class UmbDropzoneMediaElement extends UmbLitElement { - #fileManager = new UmbTemporaryFileManager(this); - #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); - #allowedMediaTypes: Array = []; - #mediaDetailRepository = new UmbMediaDetailRepository(this); - - @state() - private queue: Array = []; - - constructor() { - super(); - - this.observe(this.#fileManager.queue, (queue) => { - this.queue = queue; - const completed = queue.filter((item) => item.status !== 'waiting'); - const progress = Math.round((completed.length / queue.length) * 100); - this.dispatchEvent(new UmbProgressEvent(progress)); - }); - - this.#getAllowedMediaTypes(); - document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); - document.addEventListener('dragleave', this.#handleDragLeave.bind(this)); - document.addEventListener('drop', this.#handleDrop.bind(this)); - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - document.removeEventListener('dragenter', this.#handleDragEnter.bind(this)); - document.removeEventListener('dragleave', this.#handleDragLeave.bind(this)); - document.removeEventListener('drop', this.#handleDrop.bind(this)); - } - - #handleDragEnter() { - this.toggleAttribute('dragging', true); - } - - #handleDragLeave() { - this.toggleAttribute('dragging', false); - } - - #handleDrop(event: DragEvent) { - event.preventDefault(); - this.toggleAttribute('dragging', false); - } - - async #getAllowedMediaTypes() { - const { data } = await this.#mediaTypeStructure.requestAllowedChildrenOf(null); - if (!data) return; - this.#allowedMediaTypes = data.items; - } - - #getMediaTypeFromMime(mimetype: string): UmbAllowedMediaTypeModel { - const mediaTypeName = getMediaTypeByFileMimeType(mimetype); - return this.#allowedMediaTypes.find((type) => type.name === mediaTypeName)!; - } - - async #uploadHandler(files: Array) { - const queue = files.map((file): UmbTemporaryFileQueueModel => ({ file })); - const uploaded = await this.#fileManager.upload(queue); - return uploaded; - } - - async #onFileUpload(event: UUIFileDropzoneEvent) { - const files: Array = event.detail.files; - if (!files.length) return; - const uploads = await this.#uploadHandler(files); - - for (const upload of uploads) { - const mediaType = this.#getMediaTypeFromMime(upload.file.type); - - const preset: Partial = { - mediaType: { - unique: mediaType.unique, - collection: null, - }, - variants: [ - { - culture: null, - segment: null, - name: upload.file.name, - createDate: null, - updateDate: null, - }, - ], - values: [ - { - alias: 'umbracoFile', - value: { src: upload.unique }, - culture: null, - segment: null, - }, - ], - }; - - const { data } = await this.#mediaDetailRepository.createScaffold(preset); - if (!data) return; - - await this.#mediaDetailRepository.create(data, null); - - this.dispatchEvent(new UmbChangeEvent()); - } - } - - render() { - return html``; - } - - static styles = [ - css` - :host([dragging]) #dropzone { - opacity: 1; - pointer-events: all; - } - - [dropzone] { - opacity: 0; - } - - #dropzone { - opacity: 0; - pointer-events: none; - display: flex; - align-items: center; - justify-content: center; - position: absolute; - inset: 0px; - z-index: 100; - backdrop-filter: opacity(1); /* Removes the built in blur effect */ - border-radius: var(--uui-border-radius); - overflow: clip; - border: 1px solid var(--uui-color-focus); - } - #dropzone:after { - content: ''; - display: block; - position: absolute; - inset: 0; - border-radius: var(--uui-border-radius); - background-color: var(--uui-color-focus); - opacity: 0.2; - } - `, - ]; -} - -export default UmbDropzoneMediaElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-dropzone-media': UmbDropzoneMediaElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/index.ts deleted file mode 100644 index a6c28ed09c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/dropzone-media/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './dropzone-media.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/index.ts index d884d15b41..3c48dc1e29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/index.ts @@ -1,5 +1,4 @@ import './input-media/index.js'; -export * from './dropzone-media/index.js'; export * from './input-media/index.js'; export * from './input-image-cropper/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts index 089620dfd0..6599c9a769 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/image-cropper.element.ts @@ -1,16 +1,7 @@ import type { UmbImageCropperCrop, UmbImageCropperFocalPoint } from './index.js'; import { calculateExtrapolatedValue, clamp, inverseLerp, lerp } from '@umbraco-cms/backoffice/utils'; -import type { - PropertyValueMap} from '@umbraco-cms/backoffice/external/lit'; -import { - customElement, - property, - query, - state, - LitElement, - css, - html, -} from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, property, query, state, LitElement, css, html } from '@umbraco-cms/backoffice/external/lit'; @customElement('umb-image-cropper') export class UmbImageCropperElement extends LitElement { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts index 673440b690..d5e6900e27 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/input-image-cropper.element.ts @@ -20,6 +20,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { @property({ attribute: false }) value: UmbImageCropperPropertyEditorValue = { + temporaryFileId: null, src: '', crops: [], focalPoint: { left: 0.5, top: 0.5 }, @@ -53,7 +54,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { this.file = file; this.fileUnique = unique; - this.value = assignToFrozenObject(this.value, { src: unique }); + this.value = assignToFrozenObject(this.value, { temporaryFileId: unique }); this.#manager?.uploadOne({ unique, file }); @@ -66,7 +67,7 @@ export class UmbInputImageCropperElement extends UmbLitElement { } #onRemove = () => { - this.value = assignToFrozenObject(this.value, { src: '' }); + this.value = assignToFrozenObject(this.value, { src: '', temporaryFileId: null }); if (!this.fileUnique) return; this.#manager?.removeOne(this.fileUnique); this.fileUnique = undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/types.ts index 95117bcc12..649368b8a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-image-cropper/types.ts @@ -1,4 +1,5 @@ export type UmbImageCropperPropertyEditorValue = { + temporaryFileId?: string | null; crops: Array<{ alias: string; coordinates?: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts index a531ea761a..ae6fa4a43f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.context.ts @@ -1,13 +1,21 @@ import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import type { UmbMediaItemModel } from '../../repository/item/types.js'; +import type { UmbMediaTreeItemModel } from '../../tree/index.js'; import { UMB_MEDIA_TREE_PICKER_MODAL } from '../../tree/index.js'; +import type { + UmbMediaTreePickerModalData, + UmbMediaTreePickerModalValue, +} from '../../tree/media-tree-picker-modal.token.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMediaPickerContext extends UmbPickerInputContext { +export class UmbMediaPickerContext extends UmbPickerInputContext< + UmbMediaItemModel, + UmbMediaTreeItemModel, + UmbMediaTreePickerModalData, + UmbMediaTreePickerModalValue +> { constructor(host: UmbControllerHost) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore super(host, UMB_MEDIA_ITEM_REPOSITORY_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts new file mode 100644 index 0000000000..c96203b8c0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts @@ -0,0 +1,254 @@ +import type { UmbMediaDetailModel } from '../types.js'; +import { UmbMediaDetailRepository } from '../repository/index.js'; +import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; +import { + TemporaryFileStatus, + UmbTemporaryFileManager, + type UmbTemporaryFileModel, +} from '@umbraco-cms/backoffice/temporary-file'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; + +export interface UmbUploadableFileModel extends UmbTemporaryFileModel { + unique: string; + file: File; + mediaTypeUnique: string; +} + +export interface UmbUploadableExtensionModel { + fileExtension: string; + mediaTypes: Array; +} + +/** + * Manages the dropzone and uploads files to the server. + * @method createFilesAsMedia - Upload files to the server and creates the items using corresponding media type. + * @method createFilesAsTemporary - Upload the files as temporary files and returns the data. + * @observable completed - Emits an array of completed uploads. + */ +export class UmbDropzoneManager extends UmbControllerBase { + #host; + + #tempFileManager = new UmbTemporaryFileManager(this); + + #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); + #mediaDetailRepository = new UmbMediaDetailRepository(this); + + #completed = new UmbArrayState([], (upload) => upload.unique); + public readonly completed = this.#completed.asObservable(); + + constructor(host: UmbControllerHost) { + super(host); + this.#host = host; + } + + /** + * Uploads the files as temporary files and returns the data. + * @param files + * @returns Promise> + */ + public async createFilesAsTemporary(files: Array): Promise> { + this.#completed.setValue([]); + const temporaryFiles: Array = []; + + for (const file of files) { + const uploaded = await this.#tempFileManager.uploadOne({ unique: UmbId.new(), file }); + this.#completed.setValue([...this.#completed.getValue(), uploaded]); + temporaryFiles.push(uploaded); + } + + return temporaryFiles; + } + + /** + * Uploads files to the server and creates the items with corresponding media type. + * Allows the user to pick a media type option if multiple types are allowed. + * @param files + * @param parentUnique + */ + public async createFilesAsMedia(files: Array, parentUnique: string | null) { + if (!files.length) return; + if (files.length === 1) return this.#handleOneOneFile(files[0], parentUnique); + + // Handler for multiple files dropped + + this.#completed.setValue([]); + // removes duplicate file types so we don't call endpoints unnecessarily when building options. + const mimeTypes = [...new Set(files.map((file) => file.type))]; + const optionsArray = await this.#buildOptionsArrayFrom( + mimeTypes.map((mimetype) => this.#getExtensionFromMime(mimetype)), + parentUnique, + ); + + if (!optionsArray.length) return; // None of the files are allowed in current dropzone. + + // Building an array of uploadable files. Do we want to build an array of failed files to let the user know which ones? + const uploadableFiles: Array = []; + const notAllowedFiles: Array = []; + + for (const file of files) { + const extension = this.#getExtensionFromMime(file.type); + if (!extension) { + // Folders have no extension on file drop. We assume it is a folder being uploaded. + continue; + } + const options = optionsArray.find((option) => option.fileExtension === extension)?.mediaTypes; + + if (!options || !options.length) { + // TODO Current dropped file not allowed in this area. Find a good way to show this to the user after we finish uploading the rest of the files. + notAllowedFiles.push(file); + continue; + } + + // Since we are uploading multiple files, we will pick first allowed option. + // Consider a way we can handle this differently in the future to let the user choose. Maybe a list of all files with an allowed media type dropdown? + const mediaType = options[0]; + uploadableFiles.push({ unique: UmbId.new(), file, mediaTypeUnique: mediaType.unique }); + } + + notAllowedFiles.forEach((file) => { + try { + throw new Error(`File ${file.name} of type ${file.type} is not allowed here.`); + } catch (e) { + undefined; + } + }); + + if (!uploadableFiles.length) return; + + await this.#handleUpload(uploadableFiles, parentUnique); + } + + async #handleOneOneFile(file: File, parentUnique: string | null) { + this.#completed.setValue([]); + const extension = this.#getExtensionFromMime(file.type); + + if (!extension) { + // TODO Folders have no extension on file drop. Assume it is a folder being uploaded. + return; + } + + const optionsArray = await this.#buildOptionsArrayFrom([extension], parentUnique); + if (!optionsArray.length || !optionsArray[0].mediaTypes.length) { + throw new Error(`File ${file.name} of type ${file.type} is not allowed here.`); // Parent does not allow this file type here. + } + + const mediaTypes = optionsArray[0].mediaTypes; + if (mediaTypes.length === 1) { + // Only one allowed option, upload file using that option. + const uploadableFile: UmbUploadableFileModel = { + unique: UmbId.new(), + file, + mediaTypeUnique: mediaTypes[0].unique, + }; + + await this.#handleUpload([uploadableFile], parentUnique); + return; + } + + // Multiple options, show a dialog for the user to pick one. + const mediaType = await this.#showDialogMediaTypePicker(mediaTypes); + if (!mediaType) return; // Upload cancelled. + + const uploadableFile: UmbUploadableFileModel = { + unique: UmbId.new(), + file, + mediaTypeUnique: mediaType.unique, + }; + await this.#handleUpload([uploadableFile], parentUnique); + } + + #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; + } + } + + async #buildOptionsArrayFrom( + fileExtensions: Array, + parentUnique: string | null, + ): Promise> { + // Getting all media types allowed in our current position based on parent unique. + const { data: allAllowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(parentUnique); + if (!allAllowedMediaTypes?.items.length) return []; + + const allowedByParent = allAllowedMediaTypes.items; + + // Building an array of options the files can be uploaded as. + const options: Array = []; + + for (const fileExtension of fileExtensions) { + const extensionOptions = await this.#mediaTypeStructure.requestMediaTypesOf({ fileExtension }); + const mediaTypes = extensionOptions.filter((option) => { + return allowedByParent.find((allowed) => option.unique === allowed.unique); + }); + options.push({ fileExtension, mediaTypes }); + } + return options; + } + + async #showDialogMediaTypePicker(options: Array) { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalContext = modalManager.open(this.#host, UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL, { data: { options } }); + const value = await modalContext.onSubmit().catch(() => undefined); + return value ? { unique: value.mediaTypeUnique ?? options[0].unique } : null; + } + + async #handleUpload(files: Array, parentUnique: string | null) { + for (const file of files) { + const upload = (await this.#tempFileManager.uploadOne(file)) as UmbUploadableFileModel; + + if (upload.status === TemporaryFileStatus.SUCCESS) { + // Upload successful. Create media item. + const preset: Partial = { + mediaType: { + unique: upload.mediaTypeUnique, + collection: null, + }, + variants: [ + { + culture: null, + segment: null, + name: upload.file.name, + createDate: null, + updateDate: null, + }, + ], + values: [ + { + alias: 'umbracoFile', + value: { temporaryFileId: upload.unique }, + culture: null, + segment: null, + }, + ], + }; + const { data } = await this.#mediaDetailRepository.createScaffold(preset); + await this.#mediaDetailRepository.create(data!, parentUnique); + } + // TODO Find a good way to show files that ended up as TemporaryFileStatus.ERROR. Notice that they were allowed in current area + + this.#completed.setValue([...this.#completed.getValue(), upload]); + } + } + + private _reset() { + // + } + + public destroy() { + this.#tempFileManager.destroy(); + this.#completed.destroy(); + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts new file mode 100644 index 0000000000..d88040d01a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -0,0 +1,123 @@ +import { UmbDropzoneManager } from './dropzone-manager.class.js'; +import { UmbChangeEvent, UmbProgressEvent } from '@umbraco-cms/backoffice/event'; +import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-dropzone') +export class UmbDropzoneElement extends UmbLitElement { + @property({ attribute: false }) + parentUnique: string | null = null; + + public browse() { + const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; + return element.browse(); + } + + constructor() { + super(); + document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); + document.addEventListener('dragleave', this.#handleDragLeave.bind(this)); + document.addEventListener('drop', this.#handleDrop.bind(this)); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + document.removeEventListener('dragenter', this.#handleDragEnter.bind(this)); + document.removeEventListener('dragleave', this.#handleDragLeave.bind(this)); + document.removeEventListener('drop', this.#handleDrop.bind(this)); + } + + #handleDragEnter() { + this.toggleAttribute('dragging', true); + } + + #handleDragLeave() { + this.toggleAttribute('dragging', false); + } + + #handleDrop(event: DragEvent) { + event.preventDefault(); + this.toggleAttribute('dragging', false); + } + + async #onDropFiles(event: UUIFileDropzoneEvent) { + // TODO Handle of folder uploads. + + const files: Array = event.detail.files; + if (!files.length) return; + + const dropzoneManager = new UmbDropzoneManager(this); + this.observe( + dropzoneManager.completed, + (completed) => { + if (!completed.length) return; + + const progress = Math.floor(completed.length / files.length); + this.dispatchEvent(new UmbProgressEvent(progress)); + + if (completed.length === files.length) { + this.dispatchEvent(new UmbChangeEvent()); + dropzoneManager.destroy(); + } + }, + '_observeCompleted', + ); + //TODO Create some placeholder items while files are being uploaded? Could update them as they get completed. + await dropzoneManager.createFilesAsMedia(files, this.parentUnique); + } + + render() { + return html``; + } + + static styles = [ + css` + :host([dragging]) #dropzone { + opacity: 1; + pointer-events: all; + } + + [dropzone] { + opacity: 0; + } + + #dropzone { + opacity: 0; + pointer-events: none; + display: flex; + align-items: center; + justify-content: center; + position: absolute; + inset: 0px; + z-index: 100; + backdrop-filter: opacity(1); /* Removes the built in blur effect */ + border-radius: var(--uui-border-radius); + overflow: clip; + border: 1px solid var(--uui-color-focus); + } + #dropzone:after { + content: ''; + display: block; + position: absolute; + inset: 0; + border-radius: var(--uui-border-radius); + background-color: var(--uui-color-focus); + opacity: 0.2; + } + `, + ]; +} + +export default UmbDropzoneElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-dropzone': UmbDropzoneElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts new file mode 100644 index 0000000000..38624dcb34 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts @@ -0,0 +1,2 @@ +export * from './dropzone.element.js'; +export * from './dropzone-manager.class.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts new file mode 100644 index 0000000000..c777b79000 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts @@ -0,0 +1 @@ +export * from './modals/manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts new file mode 100644 index 0000000000..859a8d77cc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts @@ -0,0 +1,84 @@ +import type { + UmbDropzoneMediaTypePickerModalData, + UmbDropzoneMediaTypePickerModalValue, +} from './dropzone-media-type-picker-modal.token.js'; +import { css, customElement, html, query, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; +import type { UUIButtonElement } from '@umbraco-cms/backoffice/external/uui'; + +@customElement('umb-dropzone-media-type-picker-modal') +export class UmbDropzoneMediaTypePickerModalElement extends UmbModalBaseElement< + UmbDropzoneMediaTypePickerModalData, + UmbDropzoneMediaTypePickerModalValue +> { + @state() + private _options: Array = []; + + @query('#auto') + private _buttonAuto!: UUIButtonElement; + + connectedCallback() { + super.connectedCallback(); + this._options = this.data?.options ?? []; + requestAnimationFrame(() => this._buttonAuto.focus()); + } + + #onMediaTypePick(unique: string | undefined) { + this.value = { mediaTypeUnique: unique }; + this._submitModal(); + } + + render() { + return html`
    + this.#onMediaTypePick(undefined)} + label="Automatically" + compact> + Auto pick + + ${repeat( + this._options, + (option) => option.unique, + (option) => + html` this.#onMediaTypePick(option.unique)} + label=${option.name} + compact> + ${option.name} + `, + )} +
    `; + } + + static styles = [ + UmbTextStyles, + css` + #options { + display: flex; + margin: var(--uui-size-layout-1); + gap: var(--uui-size-3); + } + uui-button { + width: 150px; + height: 150px; + } + umb-icon { + font-size: var(--uui-size-10); + margin-bottom: var(--uui-size-2); + } + `, + ]; +} + +export default UmbDropzoneMediaTypePickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-dropzone-media-type-picker-modal': UmbDropzoneMediaTypePickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts new file mode 100644 index 0000000000..e8437bf919 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts @@ -0,0 +1,20 @@ +import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDropzoneMediaTypePickerModalData { + options: Array; + files?: Array; +} + +export type UmbDropzoneMediaTypePickerModalValue = { + mediaTypeUnique: string | undefined; +}; + +export const UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL = new UmbModalToken< + UmbDropzoneMediaTypePickerModalData, + UmbDropzoneMediaTypePickerModalValue +>('Umb.Modal.Dropzone.MediaTypePicker', { + modal: { + type: 'dialog', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts new file mode 100644 index 0000000000..b698197915 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts @@ -0,0 +1,2 @@ +export * from './dropzone-media-type-picker-modal.element.js'; +export * from './dropzone-media-type-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts new file mode 100644 index 0000000000..cd9b3459fe --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts @@ -0,0 +1 @@ +export * from './dropzone-media-type-picker/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts new file mode 100644 index 0000000000..e6500d5a4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.Dropzone.MediaTypePicker', + name: 'Dropzone Media Type Picker Modal', + js: () => import('./dropzone-media-type-picker/dropzone-media-type-picker-modal.element.js'), + }, +]; + +export const manifests: Array = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.token.ts index b2a7bab22e..ef56265205 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.token.ts @@ -1,10 +1,8 @@ +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbMediaCreateOptionsModalData { - parent: { - unique: string | null; - entityType: string; - }; + parent: UmbEntityModel; mediaType: { unique: string; } | null; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts index 84b18a3c44..81fbdf60f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts @@ -5,6 +5,7 @@ export * from './repository/index.js'; export * from './workspace/index.js'; export * from './reference/index.js'; export * from './components/index.js'; +export * from './dropzone/index.js'; export * from './entity.js'; export * from './paths.js'; export * from './utils/index.js'; @@ -12,5 +13,6 @@ export * from './utils/index.js'; export { UMB_MEDIA_TREE_ALIAS, UMB_MEDIA_TREE_PICKER_MODAL } from './tree/index.js'; export { UMB_MEDIA_COLLECTION_ALIAS } from './collection/index.js'; export { UMB_MEDIA_MENU_ALIAS } from './menu/manifests.js'; +export { UMB_MEDIA_PICKER_MODAL } from './modals/media-picker/index.js'; export type { UmbMediaTreeItemModel } from './tree/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts index 7c90a08ed1..64ef9c80d7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts @@ -1,7 +1,9 @@ import { manifests as collectionManifests } from './collection/manifests.js'; +import { manifests as dropzoneManifests } from './dropzone/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionsManifests } from './entity-bulk-actions/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; +import { manifests as modalManifests } from './modals/manifests.js'; import { manifests as propertyEditorsManifests } from './property-editors/manifests.js'; import { manifests as recycleBinManifests } from './recycle-bin/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; @@ -13,9 +15,11 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ ...collectionManifests, + ...dropzoneManifests, ...entityActionsManifests, ...entityBulkActionsManifests, ...menuManifests, + ...modalManifests, ...propertyEditorsManifests, ...recycleBinManifests, ...repositoryManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts new file mode 100644 index 0000000000..6a64757856 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/index.ts @@ -0,0 +1 @@ +export * from './media-picker/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts new file mode 100644 index 0000000000..749db2bfc0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as mediaPickerManifests } from './media-picker/manifests.js'; + +export const manifests = [...mediaPickerManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts new file mode 100644 index 0000000000..2a80f8400f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/index.ts @@ -0,0 +1,2 @@ +export * from './media-picker-folder-path.element.js'; +export * from './media-picker-create-item.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts new file mode 100644 index 0000000000..607ef6e992 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-create-item.element.ts @@ -0,0 +1,102 @@ +import { UmbMediaDetailRepository } from '../../../repository/detail/media-detail.repository.js'; +import { css, html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { type UmbAllowedMediaTypeModel, UmbMediaTypeStructureRepository } from '@umbraco-cms/backoffice/media-type'; + +@customElement('umb-media-picker-create-item') +export class UmbMediaPickerCreateItemElement extends UmbLitElement { + #mediaTypeStructure = new UmbMediaTypeStructureRepository(this); // used to get allowed media items + #mediaDetailRepository = new UmbMediaDetailRepository(this); // used to get media type of node + + private _node: string | null = null; + + @property() + public set node(value: string | null) { + this._node = value; + this.#getAllowedMediaTypes(); + } + + public get node() { + return this._node; + } + + @state() + private _popoverOpen = false; + + @state() + private _allowedMediaTypes: Array = []; + + async #getNodeMediaType() { + if (!this._node) return null; + + const { data } = await this.#mediaDetailRepository.requestByUnique(this.node!); + return data?.mediaType.unique ?? null; + } + + async #getAllowedMediaTypes() { + const mediaType = await this.#getNodeMediaType(); + + const { data: allowedMediaTypes } = await this.#mediaTypeStructure.requestAllowedChildrenOf(mediaType); + this._allowedMediaTypes = allowedMediaTypes?.items ?? []; + } + + // TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet. + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + #onPopoverToggle(event: ToggleEvent) { + this._popoverOpen = event.newState === 'open'; + } + + render() { + return html` + + ${this.localize.term('actions_create')} + + + + + + ${!this._allowedMediaTypes.length + ? html`
    ${this.localize.term('mediaPicker_notAllowed')}
    ` + : repeat( + this._allowedMediaTypes, + (item) => item.unique, + (item) => + html` + alert( + 'TODO: Open workspace (create) from modal. You can drop the files into this modal for now.', + )}> + + `, + )} +
    +
    +
    + `; + } + + static styles = [ + css` + #not-allowed { + padding: var(--uui-size-space-3); + } + `, + ]; +} + +export default UmbMediaPickerCreateItemElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-create-item': UmbMediaPickerCreateItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts new file mode 100644 index 0000000000..cb03f97e42 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/components/media-picker-folder-path.element.ts @@ -0,0 +1,170 @@ +import type { UmbMediaPathModel } from '../types.js'; +import type { UmbMediaDetailModel } from '../../../types.js'; +import { UmbMediaDetailRepository } from '../../../repository/index.js'; +import { UmbMediaTreeRepository } from '../../../tree/index.js'; +import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../../entity.js'; +import { css, html, customElement, state, repeat, property } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { getUmbracoFolderUnique } from '@umbraco-cms/backoffice/media-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +// TODO: get root from tree repository +const root = { name: 'Media', unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE }; + +@customElement('umb-media-picker-folder-path') +export class UmbMediaPickerFolderPathElement extends UmbLitElement { + #mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure + #mediaDetailRepository = new UmbMediaDetailRepository(this); // used to create folders + + @property({ type: Object, attribute: false }) + public set currentMedia(value: UmbEntityModel | undefined) { + if (value !== this._currentMedia) { + this._currentMedia = value; + this.#loadPath(); + } + } + + public get currentMedia() { + return this._currentMedia; + } + + @state() + private _currentMedia: UmbEntityModel | undefined; + + @state() + private _paths: Array = [root]; + + @state() + private _typingNewFolder = false; + + connectedCallback(): void { + super.connectedCallback(); + this.#loadPath(); + } + + async #loadPath() { + const unique = this._currentMedia?.unique; + const entityType = this._currentMedia?.entityType; + + if (unique && entityType) { + const { data } = await this.#mediaTreeRepository.requestTreeItemAncestors({ + treeItem: { + unique, + entityType, + }, + }); + + if (data) { + this._paths = [ + root, + ...data.map((item) => ({ name: item.name, unique: item.unique, entityType: item.entityType })), + ]; + return; + } + } + + this._paths = [root]; + } + + #goToFolder(entity: UmbEntityModel) { + this._paths = [...this._paths].slice(0, this._paths.findIndex((path) => path.unique === entity.unique) + 1); + this.currentMedia = entity; + } + + #focusFolderInput() { + this._typingNewFolder = true; + requestAnimationFrame(() => { + const element = this.getHostElement().shadowRoot!.querySelector('#new-folder') as UUIInputElement; + element.focus(); + element.select(); + }); + } + + async #addFolder(e: UUIInputEvent) { + e.stopPropagation(); + const newName = e.target.value as string; + this._typingNewFolder = false; + if (!newName) return; + + const newUnique = UmbId.new(); + const parentUnique = this._paths[this._paths.length - 1].unique; + + const preset: Partial = { + unique: newUnique, + mediaType: { + unique: getUmbracoFolderUnique(), + collection: null, + }, + variants: [ + { + culture: null, + segment: null, + name: newName, + createDate: null, + updateDate: null, + }, + ], + }; + const { data: scaffold } = await this.#mediaDetailRepository.createScaffold(preset); + if (!scaffold) return; + + const { data } = await this.#mediaDetailRepository.create(scaffold, parentUnique); + if (!data) return; + + const name = data.variants[0].name; + const unique = data.unique; + const entityType = data.entityType; + + this._paths = [...this._paths, { name, unique, entityType }]; + this.currentMedia = { unique, entityType }; + } + + render() { + return html`
    + ${repeat( + this._paths, + (path) => path.unique, + (path) => + html` this.#goToFolder({ unique: path.unique, entityType: path.entityType })}>/`, + )}${this._typingNewFolder + ? html`` + : html`+`} +
    `; + } + + static styles = [ + css` + #path { + display: flex; + align-items: center; + margin: 0 var(--uui-size-3); + } + #path uui-button { + font-weight: bold; + } + #path uui-input { + height: 100%; + } + `, + ]; +} + +export default UmbMediaPickerFolderPathElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-folder-path': UmbMediaPickerFolderPathElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts new file mode 100644 index 0000000000..134ffc53a6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/index.ts @@ -0,0 +1,4 @@ +export * from './components/index.js'; +export * from './media-picker-modal.element.js'; +export * from './media-picker-modal.token.js'; +export * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts new file mode 100644 index 0000000000..14a43cb9f7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry'; + +const modals: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.MediaPicker', + name: 'Media Picker Modal', + js: () => import('./media-picker-modal.element.js'), + }, +]; + +export const manifests = [...modals]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts new file mode 100644 index 0000000000..137a6ca60e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -0,0 +1,244 @@ +import { type UmbMediaItemModel, UmbMediaItemRepository, UmbMediaUrlRepository } from '../../repository/index.js'; +import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; +import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js'; +import type { UmbMediaCardItemModel } from './types.js'; +import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; +import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +@customElement('umb-media-picker-modal') +export class UmbMediaPickerModalElement extends UmbModalBaseElement { + #mediaTreeRepository = new UmbMediaTreeRepository(this); // used to get file structure + #mediaUrlRepository = new UmbMediaUrlRepository(this); // used to get urls + #mediaItemRepository = new UmbMediaItemRepository(this); // used to search & get media type of current path + + #mediaItemsCurrentFolder: Array = []; + + @state() + private _mediaFilteredList: Array = []; + + @state() + private _searchOnlyThisFolder = false; + + @state() + private _searchQuery = ''; + + @state() + private _currentMediaEntity: UmbEntityModel = { unique: null, entityType: UMB_MEDIA_ROOT_ENTITY_TYPE }; + async connectedCallback(): Promise { + super.connectedCallback(); + + if (this.data?.startNode) { + const { data } = await this.#mediaItemRepository.requestItems([this.data.startNode]); + + if (data?.length) { + this._currentMediaEntity = { unique: data[0].unique, entityType: data[0].entityType }; + } + } + + this.#loadMediaFolder(); + } + + async #loadMediaFolder() { + const { data } = await this.#mediaTreeRepository.requestTreeItemsOf({ + parent: { + unique: this._currentMediaEntity.unique, + entityType: this._currentMediaEntity.entityType, + }, + skip: 0, + take: 100, + }); + + this.#mediaItemsCurrentFolder = await this.#mapMediaUrls(data?.items ?? []); + this.#filterMediaItems(); + } + + async #mapMediaUrls(items: Array): Promise> { + if (!items.length) return []; + + const { data } = await this.#mediaUrlRepository.requestItems(items.map((item) => item.unique)); + + return items.map((item): UmbMediaCardItemModel => { + const url = data?.find((media) => media.unique === item.unique)?.url; + const extension = url?.split('.').pop(); + //TODO Eventually we will get a renderable img from the server. Use this for the url. [LI] + return { name: item.name, unique: item.unique, url, extension, entityType: item.entityType }; + }); + } + + #onOpen(item: UmbMediaCardItemModel) { + this._currentMediaEntity = { + unique: item.unique, + entityType: UMB_MEDIA_ROOT_ENTITY_TYPE, + }; + this.#loadMediaFolder(); + } + + #onSelected(item: UmbMediaCardItemModel) { + const selection = this.data?.multiple ? [...this.value.selection, item.unique!] : [item.unique!]; + this.modalContext?.setValue({ selection }); + } + + #onDeselected(item: UmbMediaCardItemModel) { + const selection = this.value.selection.filter((value) => value !== item.unique); + this.modalContext?.setValue({ selection }); + } + + async #filterMediaItems() { + if (!this._searchQuery) { + // No search query, show all media items in current folder. + this._mediaFilteredList = this.#mediaItemsCurrentFolder; + return; + } + + const query = this._searchQuery; + const { data } = await this.#mediaItemRepository.search({ query, skip: 0, take: 100 }); + + if (!data) { + // No search results. + this._mediaFilteredList = []; + return; + } + + if (this._searchOnlyThisFolder) { + // Don't have to map urls here, because we already have everything loaded within this folder. + this._mediaFilteredList = this.#mediaItemsCurrentFolder.filter((media) => + data.find((item) => item.unique === media.unique), + ); + return; + } + + // Map urls for search results as we are going to show for all folders (as long they aren't trashed). + this._mediaFilteredList = await this.#mapMediaUrls(data.filter((found) => found.isTrashed === false)); + } + + #onSearch(e: UUIInputEvent) { + this._searchQuery = (e.target.value as string).toLocaleLowerCase(); + this.#filterMediaItems(); + } + + #onPathChange(e: CustomEvent) { + this._currentMediaEntity = (e.target as UmbMediaPickerFolderPathElement).currentMedia || { + unique: null, + entityType: UMB_MEDIA_ROOT_ENTITY_TYPE, + }; + this.#loadMediaFolder(); + } + + render() { + return html` + + ${this.#renderBody()} ${this.#renderPath()} +
    + + +
    +
    + `; + } + + #renderBody() { + return html`${this.#renderToolbar()} + this.#loadMediaFolder()} .parentUnique=${this._currentMediaEntity.unique}> + ${ + !this._mediaFilteredList.length + ? html`

    ${this.localize.term('content_listViewNoItems')}

    ` + : html`
    + ${repeat( + this._mediaFilteredList, + (item) => item.unique, + (item) => this.#renderCard(item), + )} +
    ` + } +
    `; + } + + #renderToolbar() { + return html`
    + + + alert('TODO: Show media items as list/grid')} + > +
    `; + } + + // Where should this be placed, without it looking terrible? + // (this._searchOnlyThisFolder = !this._searchOnlyThisFolder)} label=${this.localize.term('general_excludeFromSubFolders')}> + + #renderCard(item: UmbMediaCardItemModel) { + return html` + this.#onOpen(item)} + @selected=${() => this.#onSelected(item)} + @deselected=${() => this.#onDeselected(item)} + ?selected=${this.value?.selection?.find((value) => value === item.unique)} + selectable + file-ext=${ifDefined(item.extension)}> + ${item.url ? html`${ifDefined(item.name)}` : ''} + + `; + } + + #renderPath() { + return html``; + } + + static styles = [ + css` + #toolbar { + display: flex; + gap: var(--uui-size-6); + align-items: flex-start; + margin-bottom: var(--uui-size-3); + } + #search { + flex: 1; + } + #search uui-input { + width: 100%; + margin-bottom: var(--uui-size-3); + } + #search uui-icon { + height: 100%; + display: flex; + align-items: stretch; + padding-left: var(--uui-size-3); + } + #media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-template-rows: repeat(auto-fill, 200px); + gap: var(--uui-size-space-5); + padding-bottom: 5px; /** The modal is a bit jumpy due to the img card focus/hover border. This fixes the issue. */ + } + `, + ]; +} + +export default UmbMediaPickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-media-picker-modal': UmbMediaPickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts new file mode 100644 index 0000000000..2b08f93b92 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.token.ts @@ -0,0 +1,20 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbMediaPickerModalData { + startNode?: string | null; + multiple?: boolean; +} + +export type UmbMediaPickerModalValue = { + selection: string[]; +}; + +export const UMB_MEDIA_PICKER_MODAL = new UmbModalToken( + 'Umb.Modal.MediaPicker', + { + modal: { + type: 'sidebar', + size: 'medium', + }, + }, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts new file mode 100644 index 0000000000..434bd4c12a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/types.ts @@ -0,0 +1,14 @@ +import type { UmbMediaEntityType } from '../../entity.js'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export interface UmbMediaCardItemModel { + name: string; + unique: string; + entityType: UmbMediaEntityType; + url?: string; + extension?: string; +} + +export interface UmbMediaPathModel extends UmbEntityModel { + name: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.server.data-source.ts index 9907d34e88..10a94011f7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/media-recycle-bin-tree.server.data-source.ts @@ -1,4 +1,4 @@ -import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; +import { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js'; import type { UmbMediaRecycleBinTreeItemModel } from './types.js'; import type { MediaRecycleBinItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MediaService } from '@umbraco-cms/backoffice/external/backend-api'; @@ -40,12 +40,12 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) => MediaService.getRecycleBinMediaRoot({ 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 MediaService.getRecycleBinMediaChildren({ - 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 MediaService.getTreeMediaAncestors({ - descendantId: args.descendantUnique, + descendantId: args.treeItem.unique, }); const mapper = (item: MediaRecycleBinItemResponseModel): UmbMediaRecycleBinTreeItemModel => { return { unique: item.id, - parentUnique: item.parent ? item.parent.id : null, + parent: { + unique: item.parent ? item.parent.id : null, + entityType: item.parent ? UMB_MEDIA_ENTITY_TYPE : UMB_MEDIA_ROOT_ENTITY_TYPE, + }, entityType: UMB_MEDIA_ENTITY_TYPE, noAccess: false, isTrashed: true, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/types.ts index b09498103f..cc6ea5b8ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/tree/types.ts @@ -1,6 +1,6 @@ import type { UmbMediaTreeItemModel } from '../../tree/index.js'; -import type { UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import type { UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; export interface UmbMediaRecycleBinTreeItemModel extends UmbMediaTreeItemModel {} -export interface UmbMediaRecycleBinTreeRootModel extends UmbUniqueTreeRootModel {} +export interface UmbMediaRecycleBinTreeRootModel extends UmbTreeRootModel {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts index 480a724215..30dfbb63e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/index.ts @@ -1,4 +1,5 @@ export { UmbMediaDetailRepository, UMB_MEDIA_DETAIL_REPOSITORY_ALIAS } from './detail/index.js'; export { UmbMediaItemRepository, UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from './item/index.js'; +export { UmbMediaUrlRepository, UMB_MEDIA_URL_REPOSITORY_ALIAS } from './url/index.js'; export type { UmbMediaItemModel } from './item/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.repository.ts index be841f4023..43c46ccd5c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.repository.ts @@ -5,8 +5,15 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbItemRepositoryBase } from '@umbraco-cms/backoffice/repository'; export class UmbMediaItemRepository extends UmbItemRepositoryBase { + #dataSource: UmbMediaItemServerDataSource; + constructor(host: UmbControllerHost) { super(host, UmbMediaItemServerDataSource, UMB_MEDIA_ITEM_STORE_CONTEXT); + this.#dataSource = new UmbMediaItemServerDataSource(this); + } + + async search({ query, skip, take }: { query: string; skip: number; take: number }) { + return this.#dataSource.search({ query, skip, take }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.data-source.ts index 77c9bda32c..837e358c94 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/item/media-item.server.data-source.ts @@ -4,6 +4,7 @@ import type { MediaItemResponseModel } from '@umbraco-cms/backoffice/external/ba import { MediaService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; /** * A data source for Media items that fetches data from the server @@ -15,6 +16,7 @@ export class UmbMediaItemServerDataSource extends UmbItemServerDataSourceBase< MediaItemResponseModel, UmbMediaItemModel > { + #host: UmbControllerHost; /** * Creates an instance of UmbMediaItemServerDataSource. * @param {UmbControllerHost} host @@ -25,6 +27,15 @@ export class UmbMediaItemServerDataSource extends UmbItemServerDataSourceBase< getItems, mapper, }); + this.#host = host; + } + async search({ query, skip, take }: { query: string; skip: number; take: number }) { + const { data, error } = await tryExecuteAndNotify( + this.#host, + MediaService.getItemMediaSearch({ query, skip, take }), + ); + const mapped = data?.items.map((item) => mapper(item)); + return { data: mapped, error }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts index 37dcb889ef..093ff5190c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/manifests.ts @@ -1,5 +1,6 @@ import { manifests as detailManifests } from './detail/manifests.js'; import { manifests as itemManifests } from './item/manifests.js'; +import { manifests as urlManifests } from './url/manifests.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [...detailManifests, ...itemManifests]; +export const manifests: Array = [...detailManifests, ...itemManifests, ...urlManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts new file mode 100644 index 0000000000..1d3ef9c518 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/index.ts @@ -0,0 +1,2 @@ +export { UmbMediaUrlRepository } from './media-url.repository.js'; +export { UMB_MEDIA_URL_REPOSITORY_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts new file mode 100644 index 0000000000..c60e50c095 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/manifests.ts @@ -0,0 +1,21 @@ +import { UmbMediaUrlRepository } from './media-url.repository.js'; +import type { ManifestItemStore, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_MEDIA_URL_REPOSITORY_ALIAS = 'Umb.Repository.Media.Url'; +export const UMB_MEDIA_URL_STORE_ALIAS = 'Umb.Store.MediaUrl'; + +const urlRepository: ManifestRepository = { + type: 'repository', + alias: UMB_MEDIA_URL_REPOSITORY_ALIAS, + name: 'Media Url Repository', + api: UmbMediaUrlRepository, +}; + +const urlStore: ManifestItemStore = { + type: 'itemStore', + alias: UMB_MEDIA_URL_STORE_ALIAS, + name: 'Media Url Store', + api: () => import('./media-url.store.js'), +}; + +export const manifests = [urlRepository, urlStore]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts new file mode 100644 index 0000000000..7e3a58f5b1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.repository.ts @@ -0,0 +1,13 @@ +import type { UmbMediaUrlModel } from './types.js'; +import { UMB_MEDIA_URL_STORE_CONTEXT } from './media-url.store.js'; +import { UmbMediaUrlServerDataSource } from './media-url.server.data-source.js'; +import { UmbItemRepositoryBase } from '@umbraco-cms/backoffice/repository'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMediaUrlRepository extends UmbItemRepositoryBase { + constructor(host: UmbControllerHost) { + super(host, UmbMediaUrlServerDataSource, UMB_MEDIA_URL_STORE_CONTEXT); + } +} + +export default UmbMediaUrlRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts new file mode 100644 index 0000000000..7a424240ed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.server.data-source.ts @@ -0,0 +1,45 @@ +import type { UmbMediaUrlModel } from './types.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { MediaService, type MediaUrlInfoResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { UmbItemServerDataSourceBase } from '@umbraco-cms/backoffice/repository'; + +/** + * A server data source for Media Urls + * @export + * @class UmbMediaUrlServerDataSource + * @implements {DocumentTreeDataSource} + */ +export class UmbMediaUrlServerDataSource extends UmbItemServerDataSourceBase< + MediaUrlInfoResponseModel, + UmbMediaUrlModel +> { + /** + * Creates an instance of UmbMediaUrlServerDataSource. + * @param {UmbControllerHost} host + * @memberof UmbMediaUrlServerDataSource + */ + constructor(host: UmbControllerHost) { + super(host, { + getItems, + mapper, + }); + } +} + +/* eslint-disable local-rules/no-direct-api-import */ +const getItems = (uniques: Array) => MediaService.getMediaUrls({ id: uniques }); + +const mapper = (item: MediaUrlInfoResponseModel): UmbMediaUrlModel => { + const url = item.urlInfos.length ? item.urlInfos[0].url : undefined; + const extension = url ? url.slice(url.lastIndexOf('.') + 1, url.length) : undefined; + + return { + unique: item.id, + url, + extension, + /*info: item.urlInfos.map((urlInfo) => ({ + ...urlInfo, + extension: '', + })),*/ + }; +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts new file mode 100644 index 0000000000..26f874b757 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/media-url.store.ts @@ -0,0 +1,26 @@ +import type { UmbMediaDetailModel } from '../../types.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbItemStoreBase } from '@umbraco-cms/backoffice/store'; + +/** + * @export + * @class UmbMediaUrlStore + * @extends {UmbStoreBase} + * @description - Data Store for Media urls + */ + +export class UmbMediaUrlStore extends UmbItemStoreBase { + /** + * Creates an instance of UmbMediaUrlStore. + * @param {UmbControllerHost} host + * @memberof UmbMediaUrlStore + */ + constructor(host: UmbControllerHost) { + super(host, UMB_MEDIA_URL_STORE_CONTEXT.toString()); + } +} + +export default UmbMediaUrlStore; + +export const UMB_MEDIA_URL_STORE_CONTEXT = new UmbContextToken('UmbMediaUrlStore'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts new file mode 100644 index 0000000000..b582e7685f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/url/types.ts @@ -0,0 +1,12 @@ +export interface UmbMediaUrlModel { + unique: string; + url?: string; + extension?: string; + //info?: Array; +} +/* +export interface UmbMediaUrlInfoModel { + culture?: string | null; + url: string; + extension?: string; +}*/ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts index 3522d289fd..b8828c58b0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/media-tree.server.data-source.ts @@ -1,4 +1,4 @@ -import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; +import { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE } from '../entity.js'; import type { UmbMediaTreeItemModel } from './types.js'; import type { UmbTreeAncestorsOfRequestArgs, @@ -39,12 +39,12 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) => MediaService.getTreeMediaRoot({ 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 MediaService.getTreeMediaChildren({ - parentId: args.parentUnique, + parentId: args.parent.unique, skip: args.skip, take: args.take, }); @@ -54,13 +54,16 @@ const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { const getAncestorsOf = (args: UmbTreeAncestorsOfRequestArgs) => // eslint-disable-next-line local-rules/no-direct-api-import MediaService.getTreeMediaAncestors({ - descendantId: args.descendantUnique, + descendantId: args.treeItem.unique, }); const mapper = (item: MediaTreeItemResponseModel): UmbMediaTreeItemModel => { return { unique: item.id, - parentUnique: item.parent ? item.parent.id : null, + parent: { + unique: item.parent ? item.parent.id : null, + entityType: item.parent ? UMB_MEDIA_ENTITY_TYPE : UMB_MEDIA_ROOT_ENTITY_TYPE, + }, entityType: UMB_MEDIA_ENTITY_TYPE, hasChildren: item.hasChildren, noAccess: item.noAccess, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/tree-item/media-tree-item.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/tree-item/media-tree-item.context.ts index 114805613a..54acef5951 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/tree-item/media-tree-item.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/tree-item/media-tree-item.context.ts @@ -1,9 +1,9 @@ -import type { UmbMediaTreeItemModel } from '../types.js'; +import type { UmbMediaTreeItemModel, UmbMediaTreeRootModel } from '../types.js'; import { UmbDefaultTreeItemContext } from '@umbraco-cms/backoffice/tree'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; -export class UmbMediaTreeItemContext extends UmbDefaultTreeItemContext { +export class UmbMediaTreeItemContext extends UmbDefaultTreeItemContext { #isTrashedContext = new UmbIsTrashedEntityContext(this); constructor(host: UmbControllerHost) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/types.ts index 8c872803cc..ff270f525c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/types.ts @@ -1,8 +1,8 @@ import type { UmbMediaEntityType, UmbMediaRootEntityType } from '../entity.js'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; -import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; -export interface UmbMediaTreeItemModel extends UmbUniqueTreeItemModel { +export interface UmbMediaTreeItemModel extends UmbTreeItemModel { entityType: UmbMediaEntityType; noAccess: boolean; isTrashed: boolean; @@ -14,7 +14,7 @@ export interface UmbMediaTreeItemModel extends UmbUniqueTreeItemModel { variants: Array; } -export interface UmbMediaTreeRootModel extends UmbUniqueTreeRootModel { +export interface UmbMediaTreeRootModel extends UmbTreeRootModel { entityType: UmbMediaRootEntityType; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index 8ea7e656cf..4f90532c8f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -41,6 +41,7 @@ export class UmbMediaWorkspaceContext #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)); /** * The media is the current state/draft version of the media. @@ -58,6 +59,7 @@ export class UmbMediaWorkspaceContext } readonly unique = this.#currentData.asObservablePart((data) => data?.unique); + readonly entityType = this.#currentData.asObservablePart((data) => data?.entityType); readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.mediaType.unique); readonly contentTypeHasCollection = this.#currentData.asObservablePart((data) => !!data?.mediaType.collection); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.context.ts index 30b7ab1cea..f619b2c031 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/components/input-member-type/input-member-type.context.ts @@ -1,9 +1,20 @@ +import type { + UmbMemberTypePickerModalData, + UmbMemberTypePickerModalValue, +} from '../../modal/member-type-picker-modal.token.js'; import { UMB_MEMBER_TYPE_PICKER_MODAL } from '../../modal/member-type-picker-modal.token.js'; +import type { UmbMemberTypeItemModel } from '../../repository/item/types.js'; +import type { UmbMemberTypeTreeItemModel } from '@umbraco-cms/backoffice/member-type'; import { UMB_MEMBER_TYPE_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/member-type'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMemberTypePickerContext extends UmbPickerInputContext { +export class UmbMemberTypePickerContext extends UmbPickerInputContext< + UmbMemberTypeItemModel, + UmbMemberTypeTreeItemModel, + UmbMemberTypePickerModalData, + UmbMemberTypePickerModalValue +> { constructor(host: UmbControllerHost) { super(host, UMB_MEMBER_TYPE_ITEM_REPOSITORY_ALIAS, UMB_MEMBER_TYPE_PICKER_MODAL); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/modal/member-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/modal/member-type-picker-modal.token.ts index d6df51fe11..4f6c5c71fb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/modal/member-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/modal/member-type-picker-modal.token.ts @@ -1,12 +1,8 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; import { UMB_TREE_PICKER_MODAL_ALIAS } from '@umbraco-cms/backoffice/tree'; -import type { - UmbTreePickerModalValue, - UmbTreePickerModalData, - UmbUniqueTreeItemModel, -} from '@umbraco-cms/backoffice/tree'; +import type { UmbTreePickerModalValue, UmbTreePickerModalData, UmbTreeItemModel } from '@umbraco-cms/backoffice/tree'; -export type UmbMemberTypePickerModalData = UmbTreePickerModalData; +export type UmbMemberTypePickerModalData = UmbTreePickerModalData; export type UmbMemberTypePickerModalValue = UmbTreePickerModalValue; export const UMB_MEMBER_TYPE_PICKER_MODAL = new UmbModalToken< diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.server.data-source.ts index 41be9cbe23..6fffeb62da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/member-type-tree.server.data-source.ts @@ -1,4 +1,4 @@ -import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '../entity.js'; +import { UMB_MEMBER_TYPE_ENTITY_TYPE, UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE } from '../entity.js'; import type { UmbMemberTypeTreeItemModel } from './types.js'; import type { UmbTreeChildrenOfRequestArgs, UmbTreeRootItemsRequestArgs } from '@umbraco-cms/backoffice/tree'; import { UmbTreeServerDataSourceBase } from '@umbraco-cms/backoffice/tree'; @@ -36,7 +36,7 @@ const getRootItems = (args: UmbTreeRootItemsRequestArgs) => MemberTypeService.getTreeMemberTypeRoot({ skip: args.skip, take: args.take }); const getChildrenOf = (args: UmbTreeChildrenOfRequestArgs) => { - if (args.parentUnique === null) { + if (args.parent.unique === null) { return getRootItems(args); } else { throw new Error('Not supported for the member type tree'); @@ -50,7 +50,10 @@ const getAncestorsOf = () => { const mapper = (item: NamedEntityTreeItemResponseModel): UmbMemberTypeTreeItemModel => { return { unique: item.id, - parentUnique: item.parent ? item.parent.id : null, + parent: { + unique: item.parent ? item.parent.id : null, + entityType: item.parent ? UMB_MEMBER_TYPE_ENTITY_TYPE : UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE, + }, name: item.name, entityType: UMB_MEMBER_TYPE_ENTITY_TYPE, hasChildren: item.hasChildren, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/types.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/types.ts index fd2bc07f54..a453e96de1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/tree/types.ts @@ -1,10 +1,10 @@ import type { UmbMemberTypeEntityType, UmbMemberTypeRootEntityType } from '../entity.js'; -import type { UmbUniqueTreeItemModel, UmbUniqueTreeRootModel } from '@umbraco-cms/backoffice/tree'; +import type { UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree'; -export interface UmbMemberTypeTreeItemModel extends UmbUniqueTreeItemModel { +export interface UmbMemberTypeTreeItemModel extends UmbTreeItemModel { entityType: UmbMemberTypeEntityType; } -export interface UmbMemberTypeTreeRootModel extends UmbUniqueTreeRootModel { +export interface UmbMemberTypeTreeRootModel extends UmbTreeRootModel { entityType: UmbMemberTypeRootEntityType; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts index e17f605597..054dd486d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context.ts @@ -30,6 +30,7 @@ export class UmbMemberTypeWorkspaceContext #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(undefined); diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/components/input-checkbox-list/input-checkbox-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/components/input-checkbox-list/input-checkbox-list.element.ts index 55c17a169e..abda4ede6b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/components/input-checkbox-list/input-checkbox-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/components/input-checkbox-list/input-checkbox-list.element.ts @@ -4,11 +4,12 @@ import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; +type UmbCheckboxListItem = { label: string; value: string; checked: boolean }; + @customElement('umb-input-checkbox-list') export class UmbInputCheckboxListElement extends UUIFormControlMixin(UmbLitElement, '') { - // TODO: Could this use a type that we export to ensure TS failure, or hook this up with a type coming from backend? @property({ attribute: false }) - public list: Array<{ label: string; value: string; checked: boolean }> = []; + public list: Array = []; #selection: Array = []; @property({ type: Array }) @@ -29,10 +30,10 @@ export class UmbInputCheckboxListElement extends UUIFormControlMixin(UmbLitEleme return undefined; } - #onChange(e: UUIBooleanInputEvent) { - e.stopPropagation(); - if (e.target.checked) this.selection = [...this.selection, e.target.value]; - else this.#removeFromSelection(this.selection.findIndex((value) => e.target.value === value)); + #onChange(event: UUIBooleanInputEvent) { + event.stopPropagation(); + if (event.target.checked) this.selection = [...this.selection, event.target.value]; + else this.#removeFromSelection(this.selection.findIndex((value) => event.target.value === value)); this.dispatchEvent(new UmbChangeEvent()); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts index 1341fde46e..ca6226ee2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/checkbox-list/property-editor-ui-checkbox-list.element.ts @@ -12,26 +12,38 @@ import './components/input-checkbox-list/input-checkbox-list.element.js'; */ @customElement('umb-property-editor-ui-checkbox-list') export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement implements UmbPropertyEditorUiElement { - #value: Array = []; + #selection: Array = []; + @property({ type: Array }) - public set value(value: Array) { - this.#value = value ?? []; + public set value(value: Array | string | undefined) { + this.#selection = Array.isArray(value) ? value : value ? [value] : []; } - public get value(): Array { - return this.#value; + public get value(): Array | undefined { + return this.#selection; } public set config(config: UmbPropertyEditorConfigCollection | undefined) { - const listData: string[] | undefined = config?.getValueByAlias('items'); - this._list = listData?.map((item) => ({ label: item, value: item, checked: this.#value.includes(item) })) ?? []; + if (!config) return; + + const items = config.getValueByAlias('items'); + + if (Array.isArray(items) && items.length > 0) { + this._list = + typeof items[0] === 'string' + ? items.map((item) => ({ label: item, value: item, checked: this.#selection.includes(item) })) + : items.map((item) => ({ + label: item.name, + value: item.value, + checked: this.#selection.includes(item.value), + })); + } } @state() private _list: UmbInputCheckboxListElement['list'] = []; - #onChange(event: CustomEvent) { - const element = event.target as UmbInputCheckboxListElement; - this.value = element.selection; + #onChange(event: CustomEvent & { target: UmbInputCheckboxListElement }) { + this.value = event.target.selection; this.dispatchEvent(new UmbPropertyValueChangeEvent()); } @@ -39,7 +51,7 @@ export class UmbPropertyEditorUICheckboxListElement extends UmbLitElement implem return html` `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts index 966114331f..6c63297ad2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/components/input-content/input-content.element.ts @@ -7,6 +7,7 @@ import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; import type { UmbInputMemberElement } from '@umbraco-cms/backoffice/member'; import type { UmbReferenceByUniqueAndType } from '@umbraco-cms/backoffice/models'; +import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; const elementName = 'umb-input-content'; @customElement(elementName) @@ -28,15 +29,15 @@ export class UmbInputContentElement extends UUIFormControlMixin(UmbLitElement, ' return this._type; } - @property({ type: String }) - startNodeId?: string; - @property({ type: Number }) min = 0; @property({ type: Number }) max = 0; + @property({ type: Object, attribute: false }) + startNode?: UmbTreeStartNode; + private _allowedContentTypeIds: Array = []; @property() public set allowedContentTypeIds(value: string) { @@ -113,7 +114,7 @@ export class UmbInputContentElement extends UUIFormControlMixin(UmbLitElement, ' #renderDocumentPicker() { return html` { - return { - alias: step.alias!, - documentTypeIds: step.anyOfDocTypeKeys!, - }; - }), + steps: + query.querySteps?.map((step) => { + return { + alias: step.alias!, + documentTypeIds: step.anyOfDocTypeKeys!, + }; + }) || [], }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts index ac486f206e..780f1ea40c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/content-picker/property-editor-ui-content-picker.element.ts @@ -1,12 +1,16 @@ import { UmbContentPickerDynamicRootRepository } from './dynamic-root/repository/index.js'; import type { UmbInputContentElement } from './components/input-content/index.js'; -import type { UmbContentPickerSource } from './types.js'; +import type { UmbContentPickerSource, UmbContentPickerSourceType } from './types.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 { UMB_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_DOCUMENT_ENTITY_TYPE } from '@umbraco-cms/backoffice/document'; +import { UMB_MEDIA_ENTITY_TYPE } from '@umbraco-cms/backoffice/media'; +import { UMB_MEMBER_ENTITY_TYPE } from '@umbraco-cms/backoffice/member'; +import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; // import of local component import './components/input-content/index.js'; @@ -20,66 +24,78 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple value: UmbInputContentElement['items'] = []; @state() - type: UmbContentPickerSource['type'] = 'content'; + _type: UmbContentPickerSource['type'] = 'content'; @state() - startNodeId?: string | null; + _min = 0; @state() - min = 0; + _max = Infinity; @state() - max = Infinity; + _allowedContentTypeUniques?: string | null; @state() - allowedContentTypeIds?: string | null; + _showOpenButton?: boolean; @state() - showOpenButton?: boolean; + _ignoreUserStartNodes?: boolean; @state() - ignoreUserStartNodes?: boolean; + _rootUnique?: string | null; + + @state() + _rootEntityType?: string; #dynamicRoot?: UmbContentPickerSource['dynamicRoot']; - #dynamicRootRepository = new UmbContentPickerDynamicRootRepository(this); + #entityTypeDictionary: { [type in UmbContentPickerSourceType]: string } = { + content: UMB_DOCUMENT_ENTITY_TYPE, + media: UMB_MEDIA_ENTITY_TYPE, + member: UMB_MEMBER_ENTITY_TYPE, + }; + public set config(config: UmbPropertyEditorConfigCollection | undefined) { if (!config) return; const startNode = config.getValueByAlias('startNode'); if (startNode) { - this.type = startNode.type; - this.startNodeId = startNode.id; + this._type = startNode.type; + this._rootUnique = startNode.id; + this._rootEntityType = this.#entityTypeDictionary[startNode.type]; this.#dynamicRoot = startNode.dynamicRoot; } - this.min = Number(config.getValueByAlias('minNumber')) || 0; - this.max = Number(config.getValueByAlias('maxNumber')) || Infinity; + this._min = Number(config.getValueByAlias('minNumber')) || 0; + this._max = Number(config.getValueByAlias('maxNumber')) || Infinity; - this.allowedContentTypeIds = config.getValueByAlias('filter'); - this.showOpenButton = config.getValueByAlias('showOpenButton'); - this.ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes'); + this._allowedContentTypeUniques = config.getValueByAlias('filter'); + this._showOpenButton = config.getValueByAlias('showOpenButton'); + this._ignoreUserStartNodes = config.getValueByAlias('ignoreUserStartNodes'); } connectedCallback() { super.connectedCallback(); - - this.#setStartNodeId(); + this.#setPickerRootUnique(); } - async #setStartNodeId() { - if (this.startNodeId) return; + async #setPickerRootUnique() { + // If we have a root unique value, we don't need to fetch it from the dynamic root + if (this._rootUnique) return; + if (!this.#dynamicRoot) return; - // TODO: Awaiting the workspace context to have a parent entity ID value. [LK] - // e.g. const parentEntityId = this.#workspaceContext?.getParentEntityId(); const workspaceContext = await this.getContext(UMB_ENTITY_WORKSPACE_CONTEXT); const unique = workspaceContext.getUnique(); - if (unique && this.#dynamicRoot) { - const result = await this.#dynamicRootRepository.requestRoot(this.#dynamicRoot, unique); - if (result && result.length > 0) { - this.startNodeId = result[0]; - } + if (!unique) return; + + const menuStructureWorkspaceContext = (await this.getContext('UmbMenuStructureWorkspaceContext')) as any; + const parent = (await this.observe(menuStructureWorkspaceContext.parent, () => {})?.asPromise()) as any; + const parentUnique = parent?.unique; + + const result = await this.#dynamicRootRepository.requestRoot(this.#dynamicRoot, unique, parentUnique); + if (result && result.length > 0) { + this._rootUnique = result[0]; } } @@ -89,15 +105,20 @@ export class UmbPropertyEditorUIContentPickerElement extends UmbLitElement imple } render() { + const startNode: UmbTreeStartNode | undefined = + this._rootUnique && this._rootEntityType + ? { unique: this._rootUnique, entityType: this._rootEntityType } + : undefined; + return html``; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts index 33ce19ca04..cc06836382 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/dropdown/property-editor-ui-dropdown.element.ts @@ -21,20 +21,30 @@ export class UmbPropertyEditorUIDropdownElement extends UmbLitElement implements return this.#selection; } - @state() - private _items: Array
    `; }