diff --git a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/incrementor-workspace-action.ts b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/incrementor-workspace-action.ts index 4e7fff5932..a7d4f28535 100644 --- a/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/incrementor-workspace-action.ts +++ b/src/Umbraco.Web.UI.Client/examples/workspace-context-counter/incrementor-workspace-action.ts @@ -1,9 +1,8 @@ -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; +import { UmbWorkspaceActionBase, type UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import { EXAMPLE_COUNTER_CONTEXT } from './counter-workspace-context'; // The Example Incrementor Workspace Action Controller: -export class ExampleIncrementorWorkspaceAction extends UmbControllerBase implements UmbWorkspaceAction { +export class ExampleIncrementorWorkspaceAction extends UmbWorkspaceActionBase implements UmbWorkspaceAction { // This method is executed async execute() { await this.consumeContext(EXAMPLE_COUNTER_CONTEXT, (context) => { diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 5c21ba9881..f70b3b950e 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1,12 +1,12 @@ { "name": "@umbraco-cms/backoffice", - "version": "14.0.0--beta001", + "version": "14.0.0-beta001", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@umbraco-cms/backoffice", - "version": "14.0.0--beta001", + "version": "14.0.0-beta001", "license": "MIT", "dependencies": { "@openid/appauth": "^1.3.1", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 17a3768172..cee2260680 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -1,7 +1,7 @@ { "name": "@umbraco-cms/backoffice", "license": "MIT", - "version": "14.0.0--beta001", + "version": "14.0.0-beta001", "type": "module", "exports": { ".": null, @@ -44,6 +44,7 @@ "./lit-element": "./dist-cms/packages/core/lit-element/index.js", "./localization": "./dist-cms/packages/core/localization/index.js", "./log-viewer": "./dist-cms/packages/log-viewer/index.js", + "./markdown-editor": "./dist-cms/packages/markdown-editor/index.js", "./media-type": "./dist-cms/packages/media/media-types/index.js", "./media": "./dist-cms/packages/media/media/index.js", "./member-group": "./dist-cms/packages/members/member-group/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts index 923ebbc225..51c2146779 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/backoffice.element.ts @@ -22,6 +22,7 @@ const CORE_PACKAGES = [ import('../../packages/health-check/umbraco-package.js'), import('../../packages/language/umbraco-package.js'), import('../../packages/log-viewer/umbraco-package.js'), + import('../../packages/markdown-editor/umbraco-package.js'), import('../../packages/media/umbraco-package.js'), import('../../packages/members/umbraco-package.js'), import('../../packages/models-builder/umbraco-package.js'), diff --git a/src/Umbraco.Web.UI.Client/src/apps/installer/consent/installer-consent.element.ts b/src/Umbraco.Web.UI.Client/src/apps/installer/consent/installer-consent.element.ts index 282505cffe..c808303f8b 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/installer/consent/installer-consent.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/installer/consent/installer-consent.element.ts @@ -50,7 +50,7 @@ export class UmbInstallerConsentElement extends UmbLitElement { const target = e.target as HTMLInputElement; const value: { [key: string]: string } = {}; - value[target.name] = this._telemetryLevels[parseInt(target.value) - 1].level ?? TelemetryLevelModel.BASIC; + value[target.name] = this._telemetryLevels[parseInt(target.value) - 1].level ?? TelemetryLevelModel.DETAILED; this._installerContext?.appendData(value); } @@ -83,7 +83,6 @@ export class UmbInstallerConsentElement extends UmbLitElement { min="1" max=${this._telemetryLevels.length}>

${this._selectedTelemetry.level}

-

${unsafeHTML(this._selectedTelemetry.description)}

`; } diff --git a/src/Umbraco.Web.UI.Client/src/apps/installer/database/installer-database.element.ts b/src/Umbraco.Web.UI.Client/src/apps/installer/database/installer-database.element.ts index 66ce27cd73..2830778239 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/installer/database/installer-database.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/installer/database/installer-database.element.ts @@ -139,6 +139,7 @@ export class UmbInstallerDatabaseElement extends UmbLitElement { const server = formData.get('server') as string; const name = formData.get('name') as string; const useIntegratedAuthentication = formData.has('useIntegratedAuthentication'); + const trustServerCertificate = formData.has('trustServerCertificate'); const connectionString = formData.get('connectionString') as string; // Validate connection @@ -157,10 +158,10 @@ export class UmbInstallerDatabaseElement extends UmbLitElement { password, server, useIntegratedAuthentication, + trustServerCertificate, name, connectionString, providerName: selectedDatabase.providerName, - trustServerCertificate: false, }; const { error } = await tryExecute( @@ -184,6 +185,7 @@ export class UmbInstallerDatabaseElement extends UmbLitElement { server, name, useIntegratedAuthentication, + trustServerCertificate, connectionString, providerName: selectedDatabase.providerName, }; @@ -289,39 +291,47 @@ export class UmbInstallerDatabaseElement extends UmbLitElement { name="useIntegratedAuthentication" label="Use integrated authentication" @change=${this._handleChange} - .checked=${this.databaseFormData.useIntegratedAuthentication || false}> - Use integrated authentication - + .checked=${this.databaseFormData.useIntegratedAuthentication || false}> + + + - ${!this.databaseFormData.useIntegratedAuthentication - ? html` - Username - - + ${ + !this.databaseFormData.useIntegratedAuthentication + ? html` + Username + + - - Password - - ` - : ''} + + Password + + ` + : '' + } + `; private _renderCustom = () => html` diff --git a/src/Umbraco.Web.UI.Client/src/apps/installer/installer.context.ts b/src/Umbraco.Web.UI.Client/src/apps/installer/installer.context.ts index 6950a0f0ec..aa8d67d497 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/installer/installer.context.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/installer/installer.context.ts @@ -18,7 +18,7 @@ export class UmbInstallerContext { private _data = new UmbObjectState({ user: { name: '', email: '', password: '', subscribeToNewsletter: false }, database: { id: '', providerName: '', useIntegratedAuthentication: false, trustServerCertificate: false }, - telemetryLevel: TelemetryLevelModel.BASIC, + telemetryLevel: TelemetryLevelModel.DETAILED, }); public readonly data = this._data.asObservable(); diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/index.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/index.ts index 5df372f9d3..0d1532cf44 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/index.ts @@ -253,6 +253,7 @@ export type { PagedAllowedMediaTypeModel } from './models/PagedAllowedMediaTypeM export type { PagedAuditLogResponseModel } from './models/PagedAuditLogResponseModel'; export type { PagedAuditLogWithUsernameResponseModel } from './models/PagedAuditLogWithUsernameResponseModel'; export type { PagedCultureReponseModel } from './models/PagedCultureReponseModel'; +export type { PagedDataTypeItemResponseModel } from './models/PagedDataTypeItemResponseModel'; export type { PagedDataTypeTreeItemResponseModel } from './models/PagedDataTypeTreeItemResponseModel'; export type { PagedDictionaryOverviewResponseModel } from './models/PagedDictionaryOverviewResponseModel'; export type { PagedDocumentBlueprintTreeItemResponseModel } from './models/PagedDocumentBlueprintTreeItemResponseModel'; diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/PagedDataTypeItemResponseModel.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/PagedDataTypeItemResponseModel.ts new file mode 100644 index 0000000000..6f06b0a81c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/models/PagedDataTypeItemResponseModel.ts @@ -0,0 +1,12 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +import type { DataTypeItemResponseModel } from './DataTypeItemResponseModel'; + +export type PagedDataTypeItemResponseModel = { + total: number; + items: Array; +}; + diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DataTypeResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DataTypeResource.ts index 6eb1b95c8e..b430515cb7 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DataTypeResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/DataTypeResource.ts @@ -11,6 +11,7 @@ import type { DataTypeReferenceResponseModel } from '../models/DataTypeReference import type { DataTypeResponseModel } from '../models/DataTypeResponseModel'; import type { FolderResponseModel } from '../models/FolderResponseModel'; import type { MoveDataTypeRequestModel } from '../models/MoveDataTypeRequestModel'; +import type { PagedDataTypeItemResponseModel } from '../models/PagedDataTypeItemResponseModel'; import type { PagedDataTypeTreeItemResponseModel } from '../models/PagedDataTypeTreeItemResponseModel'; import type { UpdateDataTypeRequestModel } from '../models/UpdateDataTypeRequestModel'; import type { UpdateFolderResponseModel } from '../models/UpdateFolderResponseModel'; @@ -327,6 +328,39 @@ export class DataTypeResource { }); } + /** + * @returns PagedDataTypeItemResponseModel Success + * @throws ApiError + */ + public static getFilterDataType({ + skip, + take = 100, + name = '', + editorUiAlias, + editorAlias, + }: { + skip?: number, + take?: number, + name?: string, + editorUiAlias?: string, + editorAlias?: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/filter/data-type', + query: { + 'skip': skip, + 'take': take, + 'name': name, + 'editorUiAlias': editorUiAlias, + 'editorAlias': editorAlias, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + }, + }); + } + /** * @returns any Success * @throws ApiError @@ -348,27 +382,6 @@ export class DataTypeResource { }); } - /** - * @returns any Success - * @throws ApiError - */ - public static getItemDataTypeByAlias({ - alias, - }: { - alias: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/umbraco/management/api/v1/item/data-type/{alias}', - path: { - 'alias': alias, - }, - errors: { - 401: `The resource is protected and requires an authentication token`, - }, - }); - } - /** * @returns PagedDataTypeTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MemberGroupResource.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MemberGroupResource.ts index 43af148e65..506c860fe9 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MemberGroupResource.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/services/MemberGroupResource.ts @@ -81,6 +81,52 @@ export class MemberGroupResource { }); } + /** + * @returns any Success + * @throws ApiError + */ + public static getMemberGroupById({ + id, + }: { + id: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/umbraco/management/api/v1/member-group/{id}', + path: { + 'id': id, + }, + errors: { + 401: `The resource is protected and requires an authentication token`, + 404: `Not Found`, + }, + }); + } + + /** + * @returns string Success + * @throws ApiError + */ + public static deleteMemberGroupById({ + id, + }: { + id: string, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/umbraco/management/api/v1/member-group/{id}', + path: { + 'id': id, + }, + responseHeader: 'Umb-Notifications', + errors: { + 400: `Bad Request`, + 401: `The resource is protected and requires an authentication token`, + 404: `Not Found`, + }, + }); + } + /** * @returns any Success * @throws ApiError @@ -108,30 +154,6 @@ export class MemberGroupResource { }); } - /** - * @returns string Success - * @throws ApiError - */ - public static deleteMemberGroupByKey({ - key, - }: { - key: string, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'DELETE', - url: '/umbraco/management/api/v1/member-group/{key}', - path: { - 'key': key, - }, - responseHeader: 'Umb-Notifications', - errors: { - 400: `Bad Request`, - 401: `The resource is protected and requires an authentication token`, - 404: `Not Found`, - }, - }); - } - /** * @returns PagedNamedEntityTreeItemResponseModel Success * @throws ApiError diff --git a/src/Umbraco.Web.UI.Client/src/libs/class-api/class-mixin.interface.ts b/src/Umbraco.Web.UI.Client/src/libs/class-api/class-mixin.interface.ts new file mode 100644 index 0000000000..5ff5d47edf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/class-api/class-mixin.interface.ts @@ -0,0 +1,6 @@ +import type { UmbClassInterface } from './class.interface.js'; +import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api'; + +export interface UmbClassMixinInterface extends UmbClassInterface, UmbController { + _host: UmbControllerHost; +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/class-api/class.interface.ts b/src/Umbraco.Web.UI.Client/src/libs/class-api/class.interface.ts index 49ed3116d3..7235a74eb9 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/class-api/class.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/class-api/class.interface.ts @@ -4,19 +4,64 @@ import type { UmbContextProviderController, UmbContextToken, } from '../context-api/index.js'; -import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { ObserverCallback, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; -export interface UmbClassMixinInterface extends UmbControllerHost, UmbController { - observe( - source: Observable | { asObservable: () => Observable }, - callback: (_value: T) => void, - unique?: string, - ): UmbObserverController; +export interface UmbClassInterface extends UmbControllerHost { + /** + * @description Observe an Observable. An Observable is a declared source of data that can be observed. An observables is declared from a UmbState. + * @param {Observable} source An Observable to observe from. + * @param {method} callback Callback method called when data is changed. + * @return {UmbObserverController} Reference to the created Observer Controller instance. + * @memberof UmbClassMixin + */ + observe< + ObservableType extends Observable | undefined, + T, + SpecificT = ObservableType extends Observable + ? ObservableType extends undefined + ? U | undefined + : U + : undefined, + SpecificR = ObservableType extends undefined + ? UmbObserverController | undefined + : UmbObserverController, + >( + // This type dance checks if the Observable given could be undefined, if it potentially could be undefined it means that this potentially could return undefined and then call the callback with undefined. [NL] + source: ObservableType, + callback: ObserverCallback, + controllerAlias?: UmbControllerAlias, + ): SpecificR; + + /** + * @description Provide a context API for this or child elements. + * @param {string} contextAlias + * @param {instance} instance The API instance to be exposed. + * @return {UmbContextProviderController} Reference to the created Context Provider Controller instance + * @memberof UmbClassMixin + */ provideContext(alias: string | UmbContextToken, instance: R): UmbContextProviderController; + + /** + * @description Setup a subscription for a context. The callback is called when the context is resolved. + * @param {string} contextAlias + * @param {method} callback Callback method called when context is resolved. + * @return {UmbContextConsumerController} Reference to the created Context Consumer Controller instance + * @memberof UmbClassMixin + */ consumeContext( alias: string | UmbContextToken, callback: UmbContextCallback, ): UmbContextConsumerController; + + /** + * @description Retrieve a context. Notice this is a one time retrieving of a context, meaning if you expect this to be up to date with reality you should instead use the consumeContext method. + * @param {string} contextAlias + * @return {Promise} A Promise with the reference to the Context Api Instance + * @memberof UmbClassMixin + */ + getContext( + alias: string | UmbContextToken, + ): Promise; } diff --git a/src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts b/src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts index a755cebc59..ce5c4c6f5a 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/class-api/class.mixin.ts @@ -1,10 +1,9 @@ -import type { UmbClassMixinInterface } from './class.interface.js'; +import type { UmbClassMixinInterface } from './class-mixin.interface.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api'; import { type UmbControllerHost, UmbControllerHostMixin, - type UmbController, type UmbControllerAlias, } from '@umbraco-cms/backoffice/controller-api'; import { @@ -13,89 +12,16 @@ import { UmbContextConsumerController, UmbContextProviderController, } from '@umbraco-cms/backoffice/context-api'; -import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { type ObserverCallback, UmbObserverController, simpleHashCode } from '@umbraco-cms/backoffice/observable-api'; type UmbClassMixinConstructor = new ( host: UmbControllerHost, controllerAlias?: UmbControllerAlias, -) => UmbClassMixinDeclaration; +) => UmbClassMixinInterface; -// TODO: we need the interface from EventTarget to be part of the controller base. As a temp solution the UmbClassMixinDeclaration extends EventTarget. -declare class UmbClassMixinDeclaration extends EventTarget implements UmbClassMixinInterface { - _host: UmbControllerHost; - - /** - * @description Observe a RxJS source of choice. - * @param {Observable} source RxJS source - * @param {method} callback Callback method called when data is changed. - * @return {UmbObserverController} Reference to a Observer Controller instance - * @memberof UmbClassMixin - */ - observe( - source: Observable, - callback: (_value: T) => void, - controllerAlias?: UmbControllerAlias, - ): UmbObserverController; - - /** - * @description Provide a context API for this or child elements. - * @param {string} contextAlias - * @param {instance} instance The API instance to be exposed. - * @return {UmbContextProviderController} Reference to a Context Provider Controller instance - * @memberof UmbClassMixin - */ - provideContext< - BaseType = unknown, - ResultType extends BaseType = BaseType, - InstanceType extends ResultType = ResultType, - >( - alias: string | UmbContextToken, - instance: InstanceType, - ): UmbContextProviderController; - - /** - * @description Setup a subscription for a context. The callback is called when the context is resolved. - * @param {string} contextAlias - * @param {method} callback Callback method called when context is resolved. - * @return {UmbContextConsumerController} Reference to a Context Consumer Controller instance - * @memberof UmbClassMixin - */ - consumeContext( - alias: string | UmbContextToken, - callback: UmbContextCallback, - ): UmbContextConsumerController; - - /** - * @description Retrieve a context. Notice this is a one time retrieving of a context, meaning if you expect this to be up to date with reality you should instead use the consumeContext method. - * @param {string} contextAlias - * @return {Promise} A Promise with the reference to the Context Api Instance - * @memberof UmbClassMixin - */ - getContext( - alias: string | UmbContextToken, - ): Promise; - - hasController(controller: UmbController): boolean; - getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[]; - addController(controller: UmbController): void; - removeControllerByAlias(controllerAlias: UmbControllerAlias): void; - removeController(controller: UmbController): void; - getHostElement(): Element; - - get controllerAlias(): UmbControllerAlias; - hostConnected(): void; - hostDisconnected(): void; - - /** - * @description Destroys the controller and removes it from the host. - * @memberof UmbClassMixin - */ - destroy(): void; -} - -export const UmbClassMixin = (superClass: T) => { - class UmbClassMixinClass extends UmbControllerHostMixin(superClass) implements UmbControllerHost { - protected _host: UmbControllerHost; +export const UmbClassMixin = >(superClass: T) => { + class UmbClassMixinClass extends UmbControllerHostMixin(superClass) implements UmbClassMixinInterface { + _host: UmbControllerHost; protected _controllerAlias: UmbControllerAlias; constructor(host: UmbControllerHost, controllerAlias?: UmbControllerAlias) { @@ -113,12 +39,38 @@ export const UmbClassMixin = (superClass: T) => { return this._controllerAlias; } - observe( - source: Observable, - callback: (_value: T) => void, + observe< + ObservableType extends Observable | undefined, + T, + SpecificT = ObservableType extends Observable + ? ObservableType extends undefined + ? U | undefined + : U + : undefined, + SpecificR = ObservableType extends undefined + ? UmbObserverController | undefined + : UmbObserverController, + >( + // This type dance checks if the Observable given could be undefined, if it potentially could be undefined it means that this potentially could return undefined and then call the callback with undefined. [NL] + source: ObservableType, + callback: ObserverCallback, controllerAlias?: UmbControllerAlias, - ): UmbObserverController { - return new UmbObserverController(this, source, callback, controllerAlias); + ): SpecificR { + // Fallback to use a hash of the provided method, but only if the alias is undefined. + controllerAlias ??= controllerAlias === undefined ? simpleHashCode(callback.toString()) : undefined; + + if (source) { + return new UmbObserverController( + this, + source, + callback as unknown as ObserverCallback, + controllerAlias, + ) as unknown as SpecificR; + } else { + callback(undefined as SpecificT); + this.removeControllerByAlias(controllerAlias); + return undefined as SpecificR; + } } provideContext< @@ -159,5 +111,5 @@ export const UmbClassMixin = (superClass: T) => { } } - return UmbClassMixinClass as unknown as UmbClassMixinConstructor & UmbClassMixinDeclaration; + return UmbClassMixinClass as unknown as UmbClassMixinConstructor & T; }; diff --git a/src/Umbraco.Web.UI.Client/src/libs/class-api/controller-base.class.ts b/src/Umbraco.Web.UI.Client/src/libs/class-api/controller-base.class.ts index 6d70f03106..3229af0c06 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/class-api/controller-base.class.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/class-api/controller-base.class.ts @@ -1,9 +1,12 @@ import type { UmbController } from '../controller-api/controller.interface.js'; import { UmbClassMixin } from './class.mixin.js'; +import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api'; /** * This mixin enables a web-component to host controllers. * This enables controllers to be added to the life cycle of this element. * */ -export abstract class UmbControllerBase extends UmbClassMixin(EventTarget) implements UmbController {} +export abstract class UmbControllerBase + extends UmbClassMixin>(EventTarget) + implements UmbController {} diff --git a/src/Umbraco.Web.UI.Client/src/libs/class-api/index.ts b/src/Umbraco.Web.UI.Client/src/libs/class-api/index.ts index dd4339f71f..71adb0afdb 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/class-api/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/class-api/index.ts @@ -1,4 +1,5 @@ export * from './class.interface.js'; +export * from './class-mixin.interface.js'; export * from './class.mixin.js'; export * from './context.interface.js'; export * from './context-base.class.js'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts index b45fec04d1..f49942f71e 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/context-api/consume/context-consumer.ts @@ -13,7 +13,9 @@ import { UmbContextRequestEventImplementation } from './context-request.event.js * @class UmbContextConsumer */ export class UmbContextConsumer { - #skipOrigin?: boolean; + protected _host: Element; + + #skipHost?: boolean; #stopAtContextMatch = true; #callback?: UmbContextCallback; #promise?: Promise; @@ -31,16 +33,17 @@ export class UmbContextConsumer, callback?: UmbContextCallback, ) { + this._host = host; const idSplit = contextIdentifier.toString().split('#'); this.#contextAlias = idSplit[0]; this.#apiAlias = idSplit[1] ?? 'default'; @@ -51,10 +54,11 @@ export class UmbContextConsumer { @@ -119,7 +124,7 @@ export class UmbContextConsumer boolean): UmbController[]; - addController(controller: UmbController): void; - removeControllerByAlias(alias: UmbControllerAlias): void; - removeController(controller: UmbController): void; - getHostElement(): Element; - - destroy(): void; -} - /** * This mixin enables a web-component to host controllers. * This enables controllers to be added to the life cycle of this element. @@ -24,7 +10,10 @@ export declare class UmbControllerHostImplementationElement extends HTMLElement * @mixin */ export const UmbControllerHostElementMixin = (superClass: T) => { - class UmbControllerHostElementClass extends UmbControllerHostMixin(superClass) implements UmbControllerHost { + class UmbControllerHostElementClass + extends UmbControllerHostMixin(superClass) + implements UmbControllerHostElement + { getHostElement(): Element { return this; } @@ -38,9 +27,11 @@ export const UmbControllerHostElementMixin = ( super.disconnectedCallback?.(); this.hostDisconnected(); } + + destroy(): void {} } - return UmbControllerHostElementClass as unknown as HTMLElementConstructor & T; + return UmbControllerHostElementClass as unknown as HTMLElementConstructor & T; }; declare global { diff --git a/src/Umbraco.Web.UI.Client/src/libs/controller-api/controller-host.mixin.ts b/src/Umbraco.Web.UI.Client/src/libs/controller-api/controller-host.mixin.ts index 5b5888121a..0407aa9bed 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/controller-api/controller-host.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/controller-api/controller-host.mixin.ts @@ -2,13 +2,7 @@ import type { ClassConstructor } from '../extension-api/types/utils.js'; import type { UmbControllerHost } from './controller-host.interface.js'; import type { UmbController } from './controller.interface.js'; -declare class UmbControllerHostBaseDeclaration implements Omit { - hasController(controller: UmbController): boolean; - getControllers(filterMethod: (ctrl: UmbController) => boolean): UmbController[]; - addController(controller: UmbController): void; - removeControllerByAlias(unique: UmbController['controllerAlias']): void; - removeController(controller: UmbController): void; - +interface UmbControllerHostBaseDeclaration extends Omit { hostConnected(): void; hostDisconnected(): void; destroy(): void; @@ -22,11 +16,15 @@ declare class UmbControllerHostBaseDeclaration implements Omit(superClass: T) => { - class UmbControllerHostBaseClass extends superClass { + class UmbControllerHostBaseClass extends superClass implements UmbControllerHostBaseDeclaration { #controllers: UmbController[] = []; #attached = false; + getHostElement() { + return undefined as any; + } + /** * Tests if a controller is assigned to this element. * @param {UmbController} ctrl @@ -58,7 +56,7 @@ export const UmbControllerHostMixin = (superClass: T this.#controllers.push(ctrl); if (this.#attached) { - // If a controller is created on a already attached element, then it will be added directly. This might not be optimal. As the controller it self has not finished its constructor method jet. therefor i postpone the call: + // If a controller is created on a already attached element, then it will be added directly. This might not be optimal. As the controller it self has not finished its constructor method jet. therefor i postpone the call: [NL] Promise.resolve().then(() => { // Extra check to see if we are still attached at this point: if (this.#attached) { @@ -123,7 +121,7 @@ export const UmbControllerHostMixin = (superClass: T throw new Error( `Controller with controller alias: '${ctrl.controllerAlias?.toString()}' and class name: '${ (ctrl as any).constructor.name - }', does not remove it self when destroyed. This can cause memory leaks. Please fix this issue.`, + }', does not remove it self when destroyed. This can cause memory leaks. Please fix this issue.\r\nThis usually occurs when you have a destroy() method that doesn't call super.destroy().`, ); } prev = ctrl; diff --git a/src/Umbraco.Web.UI.Client/src/libs/element-api/element.interface.ts b/src/Umbraco.Web.UI.Client/src/libs/element-api/element.interface.ts new file mode 100644 index 0000000000..ca72552a06 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/element-api/element.interface.ts @@ -0,0 +1,11 @@ +import type { UmbControllerHostElement } from '../controller-api/controller-host-element.interface.js'; +import type { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import type { UmbClassInterface } from '@umbraco-cms/backoffice/class-api'; + +export interface UmbElement extends UmbClassInterface, UmbControllerHostElement { + /** + * Use the UmbLocalizeController to localize your element. + * @see UmbLocalizationController + */ + localize: UmbLocalizationController; +} diff --git a/src/Umbraco.Web.UI.Client/src/libs/element-api/element.mixin.ts b/src/Umbraco.Web.UI.Client/src/libs/element-api/element.mixin.ts index 534201a5f7..aea4582c81 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/element-api/element.mixin.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/element-api/element.mixin.ts @@ -1,72 +1,51 @@ +import type { UmbElement } from './element.interface.js'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { HTMLElementConstructor } from '@umbraco-cms/backoffice/extension-api'; -import { - UmbControllerHostElementMixin, - type UmbControllerHostImplementationElement, -} from '@umbraco-cms/backoffice/controller-api'; +import { type UmbControllerAlias, UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; import type { UmbContextToken, UmbContextCallback } from '@umbraco-cms/backoffice/context-api'; import { UmbContextConsumerController, UmbContextProviderController } from '@umbraco-cms/backoffice/context-api'; import type { ObserverCallback } from '@umbraco-cms/backoffice/observable-api'; -import { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; - -export declare class UmbElement extends UmbControllerHostImplementationElement { - /** - * @description Observe a RxJS source of choice. - * @param {Observable} source RxJS source - * @param {method} callback Callback method called when data is changed. - * @return {UmbObserverController} Reference to a Observer Controller instance - * @memberof UmbElementMixin - */ - observe( - source: Observable | { asObservable: () => Observable }, - callback: ObserverCallback, - unique?: string, - ): UmbObserverController; - provideContext< - BaseType = unknown, - ResultType extends BaseType = BaseType, - InstanceType extends ResultType = ResultType, - >( - alias: string | UmbContextToken, - instance: InstanceType, - ): UmbContextProviderController; - consumeContext( - alias: string | UmbContextToken, - callback: UmbContextCallback, - ): UmbContextConsumerController; - getContext( - alias: string | UmbContextToken, - ): Promise; - /** - * Use the UmbLocalizeController to localize your element. - * @see UmbLocalizationController - */ - localize: UmbLocalizationController; -} +import { UmbObserverController, simpleHashCode } from '@umbraco-cms/backoffice/observable-api'; export const UmbElementMixin = (superClass: T) => { class UmbElementMixinClass extends UmbControllerHostElementMixin(superClass) implements UmbElement { localize: UmbLocalizationController = new UmbLocalizationController(this); - /** - * @description Observe a RxJS source of choice. - * @param {Observable} source RxJS source - * @param {method} callback Callback method called when data is changed. - * @return {UmbObserverController} Reference to a Observer Controller instance - * @memberof UmbElementMixin - */ - observe(source: Observable, callback: ObserverCallback, unique?: string): UmbObserverController { - return new UmbObserverController(this, source, callback, unique); + observe< + ObservableType extends Observable | undefined, + T, + SpecificT = ObservableType extends Observable + ? ObservableType extends undefined + ? U | undefined + : U + : undefined, + SpecificR = ObservableType extends undefined + ? UmbObserverController | undefined + : UmbObserverController, + >( + // This type dance checks if the Observable given could be undefined, if it potentially could be undefined it means that this potentially could return undefined and then call the callback with undefined. [NL] + source: ObservableType, + callback: ObserverCallback, + controllerAlias?: UmbControllerAlias, + ): SpecificR { + // Fallback to use a hash of the provided method, but only if the alias is undefined. + controllerAlias ??= controllerAlias === undefined ? simpleHashCode(callback.toString()) : undefined; + + if (source) { + return new UmbObserverController( + this, + source, + callback as unknown as ObserverCallback, + controllerAlias, + ) as unknown as SpecificR; + } else { + callback(undefined as SpecificT); + this.removeControllerByAlias(controllerAlias); + return undefined as SpecificR; + } } - /** - * @description Provide a context API for this or child elements. - * @param {string} alias - * @param {instance} instance The API instance to be exposed. - * @return {UmbContextProviderController} Reference to a Context Provider Controller instance - * @memberof UmbElementMixin - */ provideContext< BaseType = unknown, ResultType extends BaseType = BaseType, @@ -78,13 +57,6 @@ export const UmbElementMixin = (superClass: T) return new UmbContextProviderController(this, alias, instance); } - /** - * @description Setup a subscription for a context. The callback is called when the context is resolved. - * @param {string} alias - * @param {method} callback Callback method called when context is resolved. - * @return {UmbContextConsumerController} Reference to a Context Consumer Controller instance - * @memberof UmbElementMixin - */ consumeContext( alias: string | UmbContextToken, callback: UmbContextCallback, @@ -92,13 +64,6 @@ export const UmbElementMixin = (superClass: T) return new UmbContextConsumerController(this, alias, callback); } - /** - * @description Setup a subscription for a context. The callback is called when the context is resolved. - * @param {string} contextAlias - * @param {method} callback Callback method called when context is resolved. - * @return {UmbContextConsumerController} Reference to a Context Consumer Controller instance - * @memberof UmbElementMixin - */ async getContext( contextAlias: string | UmbContextToken, ): Promise { @@ -109,11 +74,6 @@ export const UmbElementMixin = (superClass: T) }); return promise; } - - destroy(): void { - super.destroy(); - (this.localize as any) = undefined; - } } return UmbElementMixinClass as unknown as HTMLElementConstructor & T; diff --git a/src/Umbraco.Web.UI.Client/src/libs/element-api/element.test.ts b/src/Umbraco.Web.UI.Client/src/libs/element-api/element.test.ts new file mode 100644 index 0000000000..4e363414c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/element-api/element.test.ts @@ -0,0 +1,212 @@ +import { expect } from '@open-wc/testing'; +import { UmbElementMixin } from './element.mixin.js'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbStringState } from '@umbraco-cms/backoffice/observable-api'; + +@customElement('test-my-umb-element') +class UmbTestUmbElement extends UmbElementMixin(HTMLElement) {} + +describe('UmbElementMixin', () => { + let hostElement: UmbTestUmbElement; + + beforeEach(() => { + hostElement = document.createElement('test-my-umb-element') as UmbTestUmbElement; + }); + + describe('Element general controller API', () => { + describe('methods', () => { + it('has an hasController method', () => { + expect(hostElement).to.have.property('hasController').that.is.a('function'); + }); + it('has an getControllers method', () => { + expect(hostElement).to.have.property('getControllers').that.is.a('function'); + }); + it('has an addController method', () => { + expect(hostElement).to.have.property('addController').that.is.a('function'); + }); + it('has an removeControllerByAlias method', () => { + expect(hostElement).to.have.property('removeControllerByAlias').that.is.a('function'); + }); + it('has an removeController method', () => { + expect(hostElement).to.have.property('removeController').that.is.a('function'); + }); + it('has an destroy method', () => { + expect(hostElement).to.have.property('destroy').that.is.a('function'); + }); + }); + }); + + describe('Element helper methods API', () => { + describe('methods', () => { + it('has an hasController method', () => { + expect(hostElement).to.have.property('getHostElement').that.is.a('function'); + }); + it('has an hasController should return it self', () => { + expect(hostElement.getHostElement()).to.be.equal(hostElement); + }); + it('has an observe method', () => { + expect(hostElement).to.have.property('observe').that.is.a('function'); + }); + it('has an provideContext method', () => { + expect(hostElement).to.have.property('provideContext').that.is.a('function'); + }); + it('has an consumeContext method', () => { + expect(hostElement).to.have.property('consumeContext').that.is.a('function'); + }); + it('has an getContext method', () => { + expect(hostElement).to.have.property('getContext').that.is.a('function'); + }); + it('has an localization class instance', () => { + expect(hostElement).to.have.property('localize').that.is.a('object'); + }); + }); + }); + + describe('Controllers lifecycle', () => { + it('observe is removed when destroyed', () => { + const myState = new UmbStringState('hello'); + const myObservable = myState.asObservable(); + + const ctrl = hostElement.observe(myObservable, () => {}, 'observer'); + + // The controller is now added to the host: + expect(hostElement.hasController(ctrl)).to.be.true; + + ctrl.destroy(); + + // The controller is removed from the host: + expect(hostElement.hasController(ctrl)).to.be.false; + }); + + it('observe is destroyed then removed', () => { + const myState = new UmbStringState('hello'); + const myObservable = myState.asObservable(); + + const ctrl = hostElement.observe(myObservable, () => {}, 'observer'); + + // The controller is now added to the host: + expect(hostElement.hasController(ctrl)).to.be.true; + + hostElement.removeController(ctrl); + + // The controller is removed from the host: + expect(hostElement.hasController(ctrl)).to.be.false; + }); + + it('observe is destroyed then removed via alias', () => { + const myState = new UmbStringState('hello'); + const myObservable = myState.asObservable(); + + const ctrl = hostElement.observe(myObservable, () => {}, 'observer'); + + // The controller is now added to the host: + expect(hostElement.hasController(ctrl)).to.be.true; + + hostElement.removeControllerByAlias('observer'); + + // The controller is removed from the host: + expect(hostElement.hasController(ctrl)).to.be.false; + }); + + it('observe is removed when replaced with alias', () => { + const myState = new UmbStringState('hello'); + const myObservable = myState.asObservable(); + + const ctrl = hostElement.observe(myObservable, () => {}, 'observer'); + + // The controller is now added to the host: + expect(hostElement.hasController(ctrl)).to.be.true; + + const ctrl2 = hostElement.observe(myObservable, () => {}, 'observer'); + + // The controller is removed from the host: + expect(hostElement.hasController(ctrl)).to.be.false; + // The controller is new one is there instead: + expect(hostElement.hasController(ctrl2)).to.be.true; + }); + + it('observe is removed when replaced with alias made of hash of callback method', () => { + const myState = new UmbStringState('hello'); + const myObservable = myState.asObservable(); + + const ctrl = hostElement.observe(myObservable, () => {}); + + // The controller is now added to the host: + expect(hostElement.hasController(ctrl)).to.be.true; + + const ctrl2 = hostElement.observe(myObservable, () => {}); + + // The controller is removed from the host: + expect(hostElement.hasController(ctrl)).to.be.false; + // The controller is new one is there instead: + expect(hostElement.hasController(ctrl2)).to.be.true; + }); + + it('observe is NOT removed when controller alias does not align', () => { + const myState = new UmbStringState('hello'); + const myObservable = myState.asObservable(); + + const ctrl = hostElement.observe(myObservable, () => {}); + + // The controller is now added to the host: + expect(hostElement.hasController(ctrl)).to.be.true; + + const ctrl2 = hostElement.observe(myObservable, (value) => { + const a = value + 'bla'; + }); + + // The controller is not removed from the host: + expect(hostElement.hasController(ctrl)).to.be.true; + expect(hostElement.hasController(ctrl2)).to.be.true; + }); + + it('observe is removed when observer is undefined and using the same alias', () => { + const myState = new UmbStringState('hello'); + const myObservable = myState.asObservable(); + + const ctrl = hostElement.observe(myObservable, () => {}, 'observer'); + + // The controller is now added to the host: + expect(hostElement.hasController(ctrl)).to.be.true; + + const ctrl2 = hostElement.observe( + undefined, + () => { + const a = 1; + }, + 'observer', + ); + + // The controller is removed from the host, and the new one was NOT added: + expect(hostElement.hasController(ctrl)).to.be.false; + expect(ctrl2).to.be.undefined; + }); + + it('observe is removed when observer is undefined and using the same callback method', () => { + const myState = new UmbStringState('hello'); + const myObservable = myState.asObservable(); + + const ctrl = hostElement.observe(myObservable, () => {}); + + // The controller is now added to the host: + expect(hostElement.hasController(ctrl)).to.be.true; + + const ctrl2 = hostElement.observe(undefined, () => {}); + + // The controller is removed from the host, and the new one was NOT added: + expect(hostElement.hasController(ctrl)).to.be.false; + expect(ctrl2).to.be.undefined; + }); + + it('an undefined observer executes the callback method with undefined', () => { + let callbackWasCalled = false; + const ctrl = hostElement.observe(undefined, (value) => { + expect(value).to.be.undefined; + callbackWasCalled = true; + }); + expect(callbackWasCalled).to.be.true; + expect(ctrl).to.be.undefined; + expect(hostElement.hasController(ctrl)).to.be.false; + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/libs/element-api/index.ts b/src/Umbraco.Web.UI.Client/src/libs/element-api/index.ts index 9b02476fa4..355ddd3d68 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/element-api/index.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/element-api/index.ts @@ -1 +1,2 @@ +export * from './element.interface.js'; export * from './element.mixin.js'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extension-initializer.controller.test.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extension-initializer.controller.test.ts index 3485494af4..79e517b91e 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extension-initializer.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extension-initializer.controller.test.ts @@ -20,7 +20,7 @@ class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLEle class UmbTestExtensionController extends UmbBaseExtensionInitializer { constructor( - host: UmbControllerHostElement, + host: UmbControllerHost, extensionRegistry: UmbExtensionRegistry, alias: string, onPermissionChanged: (isPermitted: boolean) => void, @@ -40,16 +40,16 @@ class UmbTestExtensionController extends UmbBaseExtensionInitializer { class UmbTestConditionAlwaysValid extends UmbControllerBase implements UmbExtensionCondition { config: UmbConditionConfigBase; - constructor(args: { host: UmbControllerHost; config: UmbConditionConfigBase }) { - super(args.host); + constructor(host: UmbControllerHost, args: { config: UmbConditionConfigBase }) { + super(host); this.config = args.config; } permitted = true; } class UmbTestConditionAlwaysInvalid extends UmbControllerBase implements UmbExtensionCondition { config: UmbConditionConfigBase; - constructor(args: { host: UmbControllerHost; config: UmbConditionConfigBase }) { - super(args.host); + constructor(host: UmbControllerHost, args: { config: UmbConditionConfigBase }) { + super(host); this.config = args.config; } permitted = false; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extension-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extension-initializer.controller.ts index a9af1b03fb..d675825b52 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extension-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extension-initializer.controller.ts @@ -205,9 +205,8 @@ export abstract class UmbBaseExtensionInitializer< // Check if we already have a controller for this config: const existing = this.#conditionControllers.find((controller) => controller.config === conditionConfig); if (!existing) { - const conditionController = await createExtensionApi(conditionManifest, [ + const conditionController = await createExtensionApi(this, conditionManifest, [ { - host: this, manifest: conditionManifest, config: conditionConfig, onChange: this.#onConditionsChangedCallback, diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.test.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.test.ts index 2123b6647b..9ded7ad7d6 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/base-extensions-initializer.controller.test.ts @@ -67,16 +67,16 @@ class UmbTestExtensionsController< class UmbTestConditionAlwaysValid extends UmbControllerBase implements UmbExtensionCondition { config: UmbConditionConfigBase; - constructor(args: { host: UmbControllerHost; config: UmbConditionConfigBase }) { - super(args.host); + constructor(host: UmbControllerHost, args: { config: UmbConditionConfigBase }) { + super(host); this.config = args.config; } permitted = true; } class UmbTestConditionAlwaysInvalid extends UmbControllerBase implements UmbExtensionCondition { config: UmbConditionConfigBase; - constructor(args: { host: UmbControllerHost; config: UmbConditionConfigBase }) { - super(args.host); + constructor(host: UmbControllerHost, args: { config: UmbConditionConfigBase }) { + super(host); this.config = args.config; } permitted = false; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-api-initializer.controller.ts index 873ad46c60..b8c5c5c3c5 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-api-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-api-initializer.controller.ts @@ -1,4 +1,5 @@ import { createExtensionApi } from '../functions/create-extension-api.function.js'; +import type { UmbApiConstructorArgumentsMethodType } from '../functions/types.js'; import type { UmbApi } from '../models/api.interface.js'; import type { UmbExtensionRegistry } from '../registry/extension.registry.js'; import type { ManifestApi, ManifestCondition } from '../types/index.js'; @@ -24,7 +25,7 @@ export class UmbExtensionApiInitializer< : UmbApi, > extends UmbBaseExtensionInitializer { #api?: ExtensionApiInterface; - #constructorArguments?: Array; + #constructorArguments?: Array | UmbApiConstructorArgumentsMethodType; /** * The api that is created for this extension. @@ -65,7 +66,7 @@ export class UmbExtensionApiInitializer< host: UmbControllerHost, extensionRegistry: UmbExtensionRegistry, alias: string, - constructorArguments: Array | undefined, + constructorArguments: Array | UmbApiConstructorArgumentsMethodType | undefined, onPermissionChanged?: (isPermitted: boolean, controller: ControllerType) => void, ) { super(host, extensionRegistry, 'extApi_', alias, onPermissionChanged); @@ -88,8 +89,9 @@ export class UmbExtensionApiInitializer< const manifest = this.manifest!; // In this case we are sure its not undefined. const newApi = await createExtensionApi( + this._host, manifest as unknown as ManifestApi, - this.#constructorArguments, + this.#constructorArguments as any, ); if (!this._isConditionsPositive) { // We are not positive anymore, so we will back out of this creation. diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts index 4abe43cd99..258f949601 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extension-element-and-api-initializer.controller.ts @@ -122,7 +122,7 @@ export class UmbExtensionElementAndApiInitializer< // TODO: we could optimize this so we only re-set the updated props. Object.keys(this.#apiProps).forEach((key) => { - (this.#component as any)[key] = this.#apiProps![key]; + (this.#api as any)[key] = this.#apiProps![key]; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extensions-api-initializer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extensions-api-initializer.controller.ts index 09f91f7509..de723847ce 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extensions-api-initializer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/controller/extensions-api-initializer.controller.ts @@ -4,7 +4,12 @@ import { UmbBaseExtensionsInitializer, } from './base-extensions-initializer.controller.js'; import { UmbExtensionApiInitializer } from './extension-api-initializer.controller.js'; -import type { ManifestApi, ManifestBase, UmbExtensionRegistry } from '@umbraco-cms/backoffice/extension-api'; +import type { + ManifestApi, + ManifestBase, + UmbApiConstructorArgumentsMethodType, + UmbExtensionRegistry, +} from '@umbraco-cms/backoffice/extension-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; /** @@ -50,13 +55,13 @@ export class UmbExtensionsApiInitializer< } */ - #constructorArgs: Array | undefined; + #constructorArgs: Array | UmbApiConstructorArgumentsMethodType | undefined; constructor( host: UmbControllerHost, extensionRegistry: UmbExtensionRegistry, type: ManifestTypeName | Array, - constructorArguments: Array | undefined, + constructorArguments: Array | UmbApiConstructorArgumentsMethodType | undefined, filter?: undefined | null | ((manifest: ManifestTypeAsApi) => boolean), onChange?: (permittedManifests: Array) => void, controllerAlias?: string, diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.function.ts index 2a95855aa0..ef0b8612b9 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.function.ts @@ -2,19 +2,21 @@ import type { UmbApi } from '../models/api.interface.js'; import type { ManifestApi, ManifestElementAndApi } from '../types/base.types.js'; import { loadManifestApi } from './load-manifest-api.function.js'; import type { UmbApiConstructorArgumentsMethodType } from './types.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export async function createExtensionApi( + host: UmbControllerHost, manifest: ManifestApi | ManifestElementAndApi, - constructorArgs: + constructorArgs?: | Array - | UmbApiConstructorArgumentsMethodType | ManifestElementAndApi> = [], + | UmbApiConstructorArgumentsMethodType | ManifestElementAndApi>, ): Promise { if (manifest.api) { const apiConstructor = await loadManifestApi(manifest.api); if (apiConstructor) { const additionalArgs = (typeof constructorArgs === 'function' ? constructorArgs(manifest) : constructorArgs) ?? []; - return new apiConstructor(...additionalArgs); + return new apiConstructor(host, ...additionalArgs); } else { console.error( `-- Extension of alias "${manifest.alias}" did not succeed instantiate a API class via the extension manifest property 'api', using either a 'api' or 'default' export`, @@ -28,7 +30,7 @@ export async function createExtensionApi( if (apiConstructor2) { const additionalArgs = (typeof constructorArgs === 'function' ? constructorArgs(manifest) : constructorArgs) ?? []; - return new apiConstructor2(...additionalArgs); + return new apiConstructor2(host, ...additionalArgs); } else { console.error( `-- Extension of alias "${manifest.alias}" did not succeed instantiate a API class via the extension manifest property 'js', using either a 'api' or 'default' export`, diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.test.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.test.ts index 5129c098c3..a9e3a0d80f 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-api.test.ts @@ -1,7 +1,12 @@ -import { expect } from '@open-wc/testing'; +import { expect, fixture } from '@open-wc/testing'; import type { ManifestApi } from '../types/index.js'; import type { UmbApi } from '../models/api.interface.js'; import { createExtensionApi } from './create-extension-api.function.js'; +import { customElement, html } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; + +@customElement('umb-test-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} class UmbExtensionApiTrueTestClass implements UmbApi { isValidClassInstance() { @@ -30,7 +35,13 @@ const jsModuleWithDefaultAndApiExport = { api: UmbExtensionApiTrueTestClass, }; -describe('Extension-Api: Create Extension Api', () => { +describe('Create Extension Api Method', () => { + let hostElement: UmbTestControllerHostElement; + + beforeEach(async () => { + hostElement = await fixture(html``); + }); + it('Returns `undefined` when manifest does not have any correct properties', async () => { const manifest: ManifestApi = { type: 'my-test-type', @@ -38,7 +49,7 @@ describe('Extension-Api: Create Extension Api', () => { name: 'pretty name', }; - const api = await createExtensionApi(manifest, []); + const api = await createExtensionApi(hostElement, manifest, []); expect(api).to.be.undefined; }); @@ -50,7 +61,7 @@ describe('Extension-Api: Create Extension Api', () => { api: UmbExtensionApiTrueTestClass, }; - const api = await createExtensionApi(manifest, []); + const api = await createExtensionApi(hostElement, manifest, []); expect(api).to.not.be.undefined; if (api) { expect(api.isValidClassInstance()).to.be.true; @@ -65,7 +76,7 @@ describe('Extension-Api: Create Extension Api', () => { js: () => Promise.resolve(jsModuleWithDefaultExport), }; - const api = await createExtensionApi(manifest, []); + const api = await createExtensionApi(hostElement, manifest, []); expect(api).to.not.be.undefined; if (api) { expect(api.isValidClassInstance()).to.be.true; @@ -80,7 +91,7 @@ describe('Extension-Api: Create Extension Api', () => { js: () => Promise.resolve(jsModuleWithApiExport), }; - const api = await createExtensionApi(manifest, []); + const api = await createExtensionApi(hostElement, manifest, []); expect(api).to.not.be.undefined; if (api) { expect(api.isValidClassInstance()).to.be.true; @@ -95,7 +106,7 @@ describe('Extension-Api: Create Extension Api', () => { js: () => Promise.resolve(jsModuleWithDefaultAndApiExport), }; - const api = await createExtensionApi(manifest, []); + const api = await createExtensionApi(hostElement, manifest, []); expect(api).to.not.be.undefined; if (api) { expect(api.isValidClassInstance()).to.be.true; diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element-with-api.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element-with-api.function.ts index a8f1475b5e..6e775e9cf0 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element-with-api.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element-with-api.function.ts @@ -45,11 +45,10 @@ export async function createExtensionElementWithApi< apiConstructor = await apiPromise; } - if (manifest.elementName) { + if (!element && manifest.elementName) { element = document.createElement(manifest.elementName) as ElementType; } - - if (fallbackElement) { + if (!element && fallbackElement) { element = document.createElement(fallbackElement) as ElementType; } @@ -62,9 +61,37 @@ export async function createExtensionElementWithApi< return { element, api }; } - console.error( - `-- Extension of alias "${manifest.alias}" did not succeed creating an element with api, missing one or two JavaScript files via the 'element' and 'api' or the 'js' property or with just 'api' and the Element Name in 'elementName' in the manifest.`, - manifest, - ); + // Debug messages: + if (!element && apiConstructor) { + // If we have a elementPropValue, that means that element or js property was defined, but the element was not created. + if (elementPropValue) { + console.error( + `-- Extension of alias "${manifest.alias}" did not succeed creating an Element with Api, Api was created but the imported Element JS file did not export a 'element' or 'default'. Alternatively define the 'elementName' in the manifest.`, + manifest, + ); + } else { + console.error( + `-- Extension of alias "${manifest.alias}" did not succeed creating an Element with Api, Api was created but the Element was missing a JavaScript file via the 'element' or the 'js' property. Alternatively define a Element Name in 'elementName' in the manifest.`, + manifest, + ); + } + } else if (element && !apiConstructor) { + if (apiPropValue) { + console.error( + `-- Extension of alias "${manifest.alias}" did not succeed creating an Element with Api, Element was created but the imported Api JS file did not export a 'api' or 'default'.`, + manifest, + ); + } else { + console.error( + `-- Extension of alias "${manifest.alias}" did not succeed creating an Element with Api, Element was created but the Api is missing a JavaScript file via the 'api' or 'js' property.`, + manifest, + ); + } + } else { + console.error( + `-- Extension of alias "${manifest.alias}" did not succeed creating an Element with Api, neither an Element or Api was created, missing one or two JavaScript files via the 'element' and 'api' or the 'js' property or with just 'api' and the Element Name in 'elementName' in the manifest.`, + manifest, + ); + } return {}; } diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element-with-api.test.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element-with-api.test.ts new file mode 100644 index 0000000000..3b72ec300c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element-with-api.test.ts @@ -0,0 +1,199 @@ +import { expect } from '@open-wc/testing'; +import type { ManifestElementAndApi } from '../types/index.js'; +import type { UmbApi } from '../index.js'; +import { createExtensionElementWithApi } from './create-extension-element-with-api.function.js'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +interface UmbExtensionApiBooleanTestElement extends UmbLitElement { + isValidElementClassInstance(): boolean; +} + +@customElement('umb-extension-api-true-test-element') +class UmbExtensionApiTrueTestElement extends UmbLitElement implements UmbExtensionApiBooleanTestElement { + isValidElementClassInstance() { + return true; + } +} + +@customElement('umb-extension-api-false-test-element') +class UmbExtensionApiFalseTestElement extends UmbLitElement implements UmbExtensionApiBooleanTestElement { + isValidElementClassInstance() { + return false; + } +} + +const elementJsModuleWithDefaultExport = { + default: UmbExtensionApiTrueTestElement, +}; + +const elementJsModuleWithElementExport = { + element: UmbExtensionApiTrueTestElement, +}; + +const elementJsModuleWithDefaultAndElementExport = { + default: UmbExtensionApiFalseTestElement, + element: UmbExtensionApiTrueTestElement, +}; + +interface UmbTestApi extends UmbApi { + isValidApiClassInstance(): boolean; +} + +class UmbTestApiTrue implements UmbTestApi { + isValidApiClassInstance() { + return true; + } + destroy() {} +} +class UmbTestApiFalse implements UmbTestApi { + isValidApiClassInstance() { + return true; + } + destroy() {} +} + +const apiJsModuleWithDefaultExport = { + default: UmbTestApiTrue, +}; + +const apiJsModuleWithApiExport = { + api: UmbTestApiTrue, +}; + +const apiJsModuleWithDefaultAndApiExport = { + default: UmbTestApiFalse, + api: UmbTestApiTrue, +}; + +describe('Create Extension Element and Api Method', () => { + it('Returns `undefined` when manifest does not have any correct properties', async () => { + const manifest: ManifestElementAndApi = { + type: 'my-test-type', + alias: 'Umb.Test.CreateManifestElement', + name: 'pretty name', + }; + + const { element, api } = await createExtensionElementWithApi(manifest); + expect(element).to.be.undefined; + expect(api).to.be.undefined; + }); + + it('Returns fallback element instance when manifest does not provide element', async () => { + const manifest: ManifestElementAndApi = { + type: 'my-test-type', + alias: 'Umb.Test.CreateManifestElement', + name: 'pretty name', + js: () => Promise.resolve(apiJsModuleWithApiExport), + }; + + const { element, api } = await createExtensionElementWithApi(manifest, 'umb-extension-api-true-test-element'); + expect(element).to.not.be.undefined; + expect(api).to.not.be.undefined; + if (element) { + expect(element.isValidElementClassInstance()).to.be.true; + } + if (api) { + expect(api.isValidApiClassInstance()).to.be.true; + } + }); + + it('Still returns fallback element instance when manifest does not provide element and manifest has a js with an api', async () => { + const manifest: ManifestElementAndApi = { + type: 'my-test-type', + alias: 'Umb.Test.CreateManifestElement', + name: 'pretty name', + js: () => Promise.resolve(apiJsModuleWithApiExport), + }; + + const { element, api } = await createExtensionElementWithApi(manifest, 'umb-extension-api-true-test-element'); + expect(element).to.not.be.undefined; + expect(api).to.not.be.undefined; + if (element) { + expect(element.isValidElementClassInstance()).to.be.true; + } + if (api) { + expect(api.isValidApiClassInstance()).to.be.true; + } + }); + + it('Handles when `api` property contains a class constructor', async () => { + const manifest: ManifestElementAndApi = { + type: 'my-test-type', + alias: 'Umb.Test.CreateManifestElement', + name: 'pretty name', + elementName: 'umb-extension-api-true-test-element', + api: () => Promise.resolve(apiJsModuleWithDefaultExport), + }; + + const { element, api } = await createExtensionElementWithApi(manifest); + expect(element).to.not.be.undefined; + expect(api).to.not.be.undefined; + if (element) { + expect(element.isValidElementClassInstance()).to.be.true; + } + if (api) { + expect(api.isValidApiClassInstance()).to.be.true; + } + }); + + it('Handles when `loader` has a default export', async () => { + const manifest: ManifestElementAndApi = { + type: 'my-test-type', + alias: 'Umb.Test.CreateManifestElement', + name: 'pretty name', + js: () => Promise.resolve(elementJsModuleWithDefaultExport), + api: () => Promise.resolve(apiJsModuleWithDefaultExport), + }; + + const { element, api } = await createExtensionElementWithApi(manifest); + expect(element).to.not.be.undefined; + expect(api).to.not.be.undefined; + if (element) { + expect(element.isValidElementClassInstance()).to.be.true; + } + if (api) { + expect(api.isValidApiClassInstance()).to.be.true; + } + }); + + it('Handles when `loader` has a element export', async () => { + const manifest: ManifestElementAndApi = { + type: 'my-test-type', + alias: 'Umb.Test.CreateManifestElement', + name: 'pretty name', + js: () => Promise.resolve(elementJsModuleWithElementExport), + api: () => Promise.resolve(apiJsModuleWithApiExport), + }; + + const { element, api } = await createExtensionElementWithApi(manifest); + expect(element).to.not.be.undefined; + expect(api).to.not.be.undefined; + if (element) { + expect(element.isValidElementClassInstance()).to.be.true; + } + if (api) { + expect(api.isValidApiClassInstance()).to.be.true; + } + }); + + it('Prioritizes api export from loader property', async () => { + const manifest: ManifestElementAndApi = { + type: 'my-test-type', + alias: 'Umb.Test.CreateManifestElement', + name: 'pretty name', + js: () => Promise.resolve(elementJsModuleWithDefaultAndElementExport), + api: () => Promise.resolve(apiJsModuleWithDefaultAndApiExport), + }; + + const { element, api } = await createExtensionElementWithApi(manifest); + expect(element).to.not.be.undefined; + expect(api).to.not.be.undefined; + if (element) { + expect(element.isValidElementClassInstance()).to.be.true; + } + if (api) { + expect(api.isValidApiClassInstance()).to.be.true; + } + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element.function.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element.function.ts index 22fe577de1..754b8ea7aa 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element.function.ts @@ -5,25 +5,15 @@ export async function createExtensionElement( manifest: ManifestElement | ManifestElementAndApi, fallbackElement?: string, ): Promise { - if (manifest.element) { - const elementConstructor = await loadManifestElement(manifest.element); + const elementPropValue = manifest.element ?? manifest.js; + + if (elementPropValue) { + const elementConstructor = await loadManifestElement(elementPropValue); if (elementConstructor) { return new elementConstructor(); } else { console.error( - `-- Extension of alias "${manifest.alias}" did not succeed creating an element class instance via the extension manifest property 'element', using either a 'element' or 'default' export`, - manifest, - ); - } - } - - if (manifest.js) { - const elementConstructor2 = await loadManifestElement(manifest.js); - if (elementConstructor2) { - return new elementConstructor2(); - } else { - console.error( - `-- Extension of alias "${manifest.alias}" did not succeed creating an element class instance via the extension manifest property 'js', using either a 'element' or 'default' export`, + `-- Extension of alias "${manifest.alias}" did not succeed creating an element class instance via the extension manifest property '${elementPropValue}'. The imported Element JS file did not export a 'element' or 'default'. Alternatively define the 'elementName' in the manifest.`, manifest, ); } @@ -38,8 +28,9 @@ export async function createExtensionElement( } console.error( - `-- Extension of alias "${manifest.alias}" did not succeed creating an element, missing a JavaScript file via the 'element' or 'js' property or a Element Name in 'elementName' in the manifest.`, + `-- Extension of alias "${manifest.alias}" did not succeed creating an Element, missing a JavaScript file via the 'element' or 'js' property. Alternatively define a Element Name in 'elementName' in the manifest.`, manifest, ); + return undefined; } diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element.test.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element.test.ts index 5f2f8567df..0bcad1161f 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/functions/create-extension-element.test.ts @@ -31,7 +31,7 @@ const jsModuleWithDefaultAndElementExport = { element: UmbExtensionApiTrueTestElement, }; -describe('Extension-Api: Create Extension Element', () => { +describe('Create Extension Element Method', () => { it('Returns `undefined` when manifest does not have any correct properties', async () => { const manifest: ManifestElement = { type: 'my-test-type', diff --git a/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts b/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts index 613000e663..1459943d78 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/extension-api/registry/extension.registry.ts @@ -266,10 +266,6 @@ export class UmbExtensionRegistry< distinctUntilChanged(extensionAndKindMatchSingleMemoization), ) as Observable; } - /** - * @deprecated Use `byAlias` instead. - */ - getByAlias = this.byAlias.bind(this); /** * Get an observable that provides extensions matching the given type and alias. @@ -292,10 +288,6 @@ export class UmbExtensionRegistry< distinctUntilChanged(extensionAndKindMatchSingleMemoization), ) as Observable; } - /** - * @deprecated Use `byTypeAndAlias` instead. - */ - getByTypeAndAlias = this.byTypeAndAlias.bind(this); byTypeAndAliases>( type: Key, @@ -312,10 +304,6 @@ export class UmbExtensionRegistry< distinctUntilChanged(extensionAndKindMatchArrayMemoization), ) as Observable>; } - /** - * @deprecated Use `byTypeAndAliases` instead. - */ - getByTypeAndAliases = this.byTypeAndAliases.bind(this); /** * Get an observable of extensions by type and a given filter method. @@ -383,10 +371,6 @@ export class UmbExtensionRegistry< distinctUntilChanged(extensionAndKindMatchArrayMemoization), ) as Observable>; } - /** - * @deprecated Use `byType` instead. - */ - extensionsOfType = this.byType.bind(this); /** * Get an observable that provides extensions matching given types. @@ -399,9 +383,4 @@ export class UmbExtensionRegistry< distinctUntilChanged(extensionAndKindMatchArrayMemoization), ) as Observable>; } - - /** - * @deprecated Use `byTypes` instead. - */ - extensionsOfTypes = this.byTypes.bind(this); } diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.test.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.test.ts index 076b93d452..69aa55ab50 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.test.ts @@ -1,6 +1,7 @@ import { expect } from '@open-wc/testing'; import { UmbObjectState } from './states/object-state.js'; import { UmbObserverController } from './observer.controller.js'; +import { simpleHashCode } from './utils/simple-hash-code.function.js'; import { customElement } from '@umbraco-cms/backoffice/external/lit'; import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; @@ -42,27 +43,21 @@ describe('UmbObserverController', () => { expect(hostElement.hasController(secondCtrl)).to.be.true; }); - it('controller is replacing another controller when using the same callback method and no controller-alias', () => { - const state = new UmbObjectState(undefined); - const observable = state.asObservable(); - - const callbackMethod = (state: unknown) => {}; - - const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod); - const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod); - - expect(hostElement.hasController(firstCtrl)).to.be.false; - expect(hostElement.hasController(secondCtrl)).to.be.true; - }); - it('controller is NOT replacing another controller when using a null for controller-alias', () => { const state = new UmbObjectState(undefined); const observable = state.asObservable(); const callbackMethod = (state: unknown) => {}; - const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod, null); - const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod, null); + // Imitates the behavior of the observe method in the UmbClassMixin + let controllerAlias1 = null; + controllerAlias1 ??= controllerAlias1 === undefined ? simpleHashCode(callbackMethod.toString()) : undefined; + + const firstCtrl = new UmbObserverController(hostElement, observable, callbackMethod, controllerAlias1); + + let controllerAlias2 = null; + controllerAlias2 ??= controllerAlias2 === undefined ? simpleHashCode(callbackMethod.toString()) : undefined; + const secondCtrl = new UmbObserverController(hostElement, observable, callbackMethod, controllerAlias2); expect(hostElement.hasController(firstCtrl)).to.be.true; expect(hostElement.hasController(secondCtrl)).to.be.true; diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.ts index 9fa48b5bff..fdd5689913 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.controller.ts @@ -1,5 +1,4 @@ import { type ObserverCallback, UmbObserver } from './observer.js'; -import { simpleHashCode } from './utils/simple-hash-code.function.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import type { UmbController, UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -15,12 +14,11 @@ export class UmbObserverController extends UmbObserver implement host: UmbControllerHost, source: Observable, callback: ObserverCallback, - alias?: UmbControllerAlias | null, + alias: UmbControllerAlias, ) { super(source, callback); this.#host = host; - // Fallback to use a hash of the provided method, but only if the alias is undefined. - this.#alias = alias ?? (alias === undefined ? simpleHashCode(callback.toString()) : undefined); + this.#alias = alias; // Lets check if controller is already here: // No we don't want this, as multiple different controllers might be looking at the same source. diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.ts index 35372c2eb2..f9bee2b51b 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/observer.ts @@ -6,7 +6,9 @@ export type ObserverCallbackStack = { complete?: () => void; }; -export type ObserverCallback = ((_value: T) => void) | ObserverCallbackStack; +export type ObserverCallback = (_value: T) => void; +// We do not use the ObserverCallbackStack type, and it was making things more complicated than they need to be so I have taken it out.. +//export type ObserverCallback = ((_value: T) => void) | ObserverCallbackStack; export class UmbObserver { #source!: Observable; diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts index e2d248b5e7..6da96a31f1 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/array-state.ts @@ -56,11 +56,6 @@ export class UmbArrayState extends UmbDeepState { } } - /** - * @deprecated - Use `setValue` instead. - */ - next = this.setValue; - /** * @method remove * @param {unknown[]} uniques - The unique values to remove. diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/basic-state.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/basic-state.ts index 7279c7476d..57aaaa84b6 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/basic-state.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/states/basic-state.ts @@ -72,9 +72,4 @@ export class UmbBasicState { this._subject.next(data); } } - - /** - * @deprecated - Use `setValue` instead. - */ - next = this.setValue; } diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/append-to-frozen-array.function.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/append-to-frozen-array.function.ts index 0f7eba5a52..b70fa4b335 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/append-to-frozen-array.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/append-to-frozen-array.function.ts @@ -8,8 +8,8 @@ * @description - Inserts or replaces an entry in a frozen array and returns a new array. * @example Example append new entry for a UmbArrayState or a part of UmbObjectState/UmbDeepState which is an array. Where the key is unique and the item will be updated if matched with existing. * const entry = {id: 'myKey', value: 'myValue'}; - * const newDataSet = appendToFrozenArray(mySubject.getValue(), entry, x => x.id === id); - * mySubject.next(newDataSet); + * const newDataSet = appendToFrozenArray(myState.getValue(), entry, x => x.id === id); + * myState.setValue(newDataSet); */ export function appendToFrozenArray(data: T[], entry: T, getUniqueMethod?: (entry: T) => unknown): T[] { const unFrozenDataSet = [...data]; diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/filter-frozen-array.function.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/filter-frozen-array.function.ts index 7d3e5a2e0c..e9e2659ca4 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/filter-frozen-array.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/filter-frozen-array.function.ts @@ -5,8 +5,8 @@ * @param {(entry: T) => boolean} filterMethod - Method to filter the array. * @description - Creates a RxJS Observable from RxJS Subject. * @example Example remove an entry of a ArrayState or a part of DeepState/ObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing. - * const newDataSet = filterFrozenArray(mySubject.getValue(), x => x.id !== "myKey"); - * mySubject.next(newDataSet); + * const newDataSet = filterFrozenArray(myState.getValue(), x => x.id !== "myKey"); + * myState.setValue(newDataSet); */ export function filterFrozenArray(data: T[], filterMethod: (entry: T) => boolean): T[] { return [...data].filter((x) => filterMethod(x)); diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/partial-update-frozen-array.function.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/partial-update-frozen-array.function.ts index e7ebbbc57a..c97db83e42 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/partial-update-frozen-array.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/partial-update-frozen-array.function.ts @@ -7,8 +7,8 @@ * @description - Creates a RxJS Observable from RxJS Subject. * @example Example append new entry for a ArrayState or a part of UmbDeepState/UmbObjectState it which is an array. Where the key is unique and the item will be updated if matched with existing. * const partialEntry = {value: 'myValue'}; - * const newDataSet = partialUpdateFrozenArray(mySubject.getValue(), partialEntry, x => x.key === 'myKey'); - * mySubject.next(newDataSet); + * const newDataSet = partialUpdateFrozenArray(myState.getValue(), partialEntry, x => x.key === 'myKey'); + * myState.setValue(newDataSet); */ export function partialUpdateFrozenArray( data: T[], diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/push-at-to-unique-array.function.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/push-at-to-unique-array.function.ts index 4d27c9c0bb..5c96db318f 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/push-at-to-unique-array.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/push-at-to-unique-array.function.ts @@ -8,7 +8,7 @@ * @example Example append new entry for a Array. Where the key is unique and the item will be updated if matched with existing. * const entry = {key: 'myKey', value: 'myValue'}; * const newDataSet = pushToUniqueArray([], entry, x => x.key === key, 1); - * mySubject.next(newDataSet); + * myState.setValue(newDataSet); */ export function pushAtToUniqueArray( data: T[], diff --git a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/push-to-unique-array.function.ts b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/push-to-unique-array.function.ts index 35e4e3169c..4c6f4e0978 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/push-to-unique-array.function.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/observable-api/utils/push-to-unique-array.function.ts @@ -8,7 +8,7 @@ * @example Example append new entry for a Array. Where the key is unique and the item will be updated if matched with existing. * const entry = {key: 'myKey', value: 'myValue'}; * const newDataSet = pushToUniqueArray([], entry, x => x.key === key); - * mySubject.next(newDataSet); + * myState.setValue(newDataSet); */ export function pushToUniqueArray(data: T[], entry: T, getUniqueMethod: (entry: T) => unknown): T[] { const unique = getUniqueMethod(entry); diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index d1544fbc6e..52f12c19dc 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -533,8 +533,8 @@ export const data: Array = [ name: 'Checkbox List', id: 'dt-checkboxList', parent: null, - editorAlias: 'Umbraco.CheckboxList', - editorUiAlias: 'Umb.PropertyEditorUi.CheckboxList', + editorAlias: 'Umbraco.CheckBoxList', + editorUiAlias: 'Umb.PropertyEditorUi.CheckBoxList', hasChildren: false, isFolder: false, isDeletable: true, @@ -619,7 +619,7 @@ export const data: Array = [ name: 'Media Picker', id: 'dt-mediaPicker', parent: null, - editorAlias: 'Umbraco.MediaPicker3', + editorAlias: 'Umbraco.MediaPicker', editorUiAlias: 'Umb.PropertyEditorUi.MediaPicker', hasChildren: false, isFolder: false, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts index c108e83cfd..182e04ef9c 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts @@ -12,6 +12,61 @@ type UmbMockDocumentTypeModelHack = DocumentTypeResponseModel & export interface UmbMockDocumentTypeModel extends Omit {} export const data: Array = [ + { + allowedTemplates: [], + defaultTemplate: { id: 'the-simplest-document-type-id' }, + id: 'the-simplest-document-type-id', + alias: 'theSimplestDocumentType', + name: 'The simple document type', + description: null, + icon: 'icon-document', + allowedAsRoot: true, + variesByCulture: false, + variesBySegment: false, + isElement: false, + hasChildren: false, + parent: null, + isFolder: false, + properties: [ + { + id: '1680d4d2-cda8-4ac2-affd-a69fc10382b1', + container: { id: 'the-simplest-document-type-id-container' }, + alias: 'prop1', + name: 'Prop 1', + description: null, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + ], + containers: [ + { + id: 'the-simplest-document-type-id-container', + parent: null, + name: 'Content', + type: 'Group', + sortOrder: 0, + }, + ], + allowedDocumentTypes: [], + compositions: [], + cleanup: { + preventCleanup: false, + keepAllVersionsNewerThanDays: null, + keepLatestVersionPerDayForDays: null, + }, + }, + { allowedTemplates: [], defaultTemplate: null, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index d70519af4a..cf47af845a 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -10,6 +10,44 @@ type UmbMockDocumentTypeModelHack = DocumentResponseModel & DocumentTreeItemResp export interface UmbMockDocumentModel extends Omit {} export const data: Array = [ + { + urls: [ + { + culture: 'en-US', + url: '/', + }, + ], + template: null, + id: 'the-simplest-document-id', + parent: null, + documentType: { + id: 'the-simplest-document-type-id', + icon: 'icon-document', + }, + hasChildren: false, + noAccess: false, + isProtected: false, + isTrashed: false, + variants: [ + { + state: DocumentVariantStateModel.DRAFT, + publishDate: '2023-02-06T15:32:24.957009', + culture: 'en-us', + segment: null, + name: 'The Simplest Document', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', + }, + ], + values: [ + { + alias: 'prop1', + culture: null, + segment: null, + value: 'default value here', + }, + ], + }, { urls: [ { diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts index 3acd9907d2..88b7fa5735 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member-type/member-type.data.ts @@ -17,8 +17,37 @@ export const data: Array = [ description: 'Member type 1 description', alias: 'memberType1', icon: 'icon-bug', - properties: [], - containers: [], + properties: [ + { + id: '1680d4d2-cda8-4ac2-affd-a69fc10382b1', + container: { id: 'the-simplest-document-type-id-container' }, + alias: 'prop1', + name: 'Prop 1', + description: null, + dataType: { id: '0cc0eba1-9960-42c9-bf9b-60e150b429ae' }, + variesByCulture: false, + variesBySegment: false, + sortOrder: 0, + validation: { + mandatory: false, + mandatoryMessage: null, + regEx: null, + regExMessage: null, + }, + appearance: { + labelOnTop: false, + }, + }, + ], + containers: [ + { + id: 'the-simplest-document-type-id-container', + parent: null, + name: 'Content', + type: 'Group', + sortOrder: 0, + }, + ], allowedAsRoot: false, variesByCulture: false, variesBySegment: false, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts index 89194a6adf..d82092d4a2 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/member/member.data.ts @@ -19,11 +19,11 @@ export const data: Array = [ values: [], variants: [ { - name: 'Member 1', culture: 'en-us', segment: null, - createDate: '2023-02-06T15:31:46.876902', - updateDate: '2023-02-06T15:31:51.354764', + name: 'The Simplest Member', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', }, ], }, @@ -43,11 +43,11 @@ export const data: Array = [ values: [], variants: [ { - name: 'Member 2', culture: 'en-us', segment: null, - createDate: '2023-02-06T15:31:46.876902', - updateDate: '2023-02-06T15:31:51.354764', + name: 'The Simplest Member', + createDate: '2023-02-06T15:32:05.350038', + updateDate: '2023-02-06T15:32:24.957009', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/filter.handlers.ts b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/filter.handlers.ts index 5fad748ccb..8d078cfb57 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/filter.handlers.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/handlers/user/filter.handlers.ts @@ -4,7 +4,7 @@ import { UMB_SLUG } from './slug.js'; import { umbracoPath } from '@umbraco-cms/backoffice/utils'; export const handlers = [ - rest.get(umbracoPath(`${UMB_SLUG}/filter`), (req, res, ctx) => { + rest.get(umbracoPath(`/filter${UMB_SLUG}`), (req, res, ctx) => { const skip = Number(req.url.searchParams.get('skip')); const take = Number(req.url.searchParams.get('take')); const orderBy = req.url.searchParams.get('orderBy'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.element.ts index c70112a1bd..6b0213c456 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/workspace/block-grid-area-type-workspace.element.ts @@ -25,7 +25,7 @@ export class UmbBlockGridAreaTypeWorkspaceElement extends UmbLitElement { public set manifest(manifest: ManifestWorkspace) { this.#manifest = manifest; - createExtensionApi(manifest, [this, { manifest: manifest }]).then((context) => { + createExtensionApi(this, manifest, [{ manifest: manifest }]).then((context) => { if (context) { this.#gotWorkspaceContext(context); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts index 5cb684509f..6008078fb8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts @@ -10,18 +10,12 @@ import type { UmbBlockGridLayoutModel } from '@umbraco-cms/backoffice/block-grid import { html, customElement, state, repeat, css, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import '../block-grid-entry/index.js'; -import { - UmbSorterController, - type UmbSorterConfig, - type resolveVerticalDirectionArgs, -} from '@umbraco-cms/backoffice/sorter'; +import { UmbSorterController, type UmbSorterConfig, type resolvePlacementArgs } from '@umbraco-cms/backoffice/sorter'; /** * Notice this utility method is not really shareable with others as it also takes areas into account. [NL] */ -function resolveVerticalDirectionAsGrid( - args: resolveVerticalDirectionArgs, -) { +function resolvePlacementAsGrid(args: resolvePlacementArgs) { // If this has areas, we do not want to move, unless we are at the edge if (args.relatedModel.areas.length > 0 && isWithinRect(args.pointerX, args.pointerY, args.relatedRect, -10)) { return null; @@ -94,7 +88,7 @@ const SORTER_CONFIG: UmbSorterConfig { return modelEntry.contentUdi; }, - resolveVerticalDirection: resolveVerticalDirectionAsGrid, + resolvePlacement: resolvePlacementAsGrid, identifier: 'block-grid-editor', itemSelector: 'umb-block-grid-entry', containerSelector: '.umb-block-grid__layout-container', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts index 359b4998b2..edc11c5282 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-type-configuration/property-editor-ui-block-grid-type-configuration.element.ts @@ -45,6 +45,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement this._groupsWithBlockTypes = model; }, onEnd: () => { + // TODO: make one method for updating the blockGroupsDataSetValue: this.#datasetContext?.setPropertyValue( 'blockGroups', this._groupsWithBlockTypes.map((group) => ({ key: group.key, name: group.name })), @@ -143,6 +144,8 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement // TODO: Implement confirm dialog #deleteGroup(groupKey: string) { + // TODO: make one method for updating the blockGroupsDataSetValue: + // This one that deletes might require the ability to parse what to send as an argument to the method, then a filtering can occur before. this.#datasetContext?.setPropertyValue( 'blockGroups', this._blockGroups.filter((group) => group.key !== groupKey), @@ -154,6 +157,7 @@ export class UmbPropertyEditorUIBlockGridTypeConfigurationElement #changeGroupName(e: UUIInputEvent, groupKey: string) { const groupName = e.target.value as string; + // TODO: make one method for updating the blockGroupsDataSetValue: this.#datasetContext?.setPropertyValue( 'blockGroups', this._blockGroups.map((group) => (group.key === groupKey ? { ...group, name: groupName } : group)), diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts index 936a15b636..70fd3b2bb5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/inline-list-block/inline-list-block.element.ts @@ -39,7 +39,7 @@ export class UmbInlineListBlockElement extends UmbLitElement { }); this.observe(umbExtensionsRegistry.byTypeAndAlias('workspace', UMB_BLOCK_WORKSPACE_ALIAS), (manifest) => { if (manifest) { - createExtensionApi(manifest, [this, { manifest: manifest }]).then((context) => { + createExtensionApi(this, manifest, [{ manifest: manifest }]).then((context) => { if (context) { this.#workspaceContext = context as typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; this.#load(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts index 744b78a9ca..af2608a99a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/components/input-block-type/input-block-type.element.ts @@ -1,15 +1,12 @@ import type { UmbBlockTypeBaseModel } from '../../types.js'; -import { - UMB_DOCUMENT_TYPE_PICKER_MODAL, - UMB_MODAL_MANAGER_CONTEXT, - umbConfirmModal, -} from '@umbraco-cms/backoffice/modal'; +import { UMB_MODAL_MANAGER_CONTEXT, umbConfirmModal } from '@umbraco-cms/backoffice/modal'; import '../block-type-card/index.js'; import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property'; import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import { UMB_DOCUMENT_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/document-type'; @customElement('umb-input-block-type') export class UmbInputBlockTypeElement< diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.element.ts index c4b924cafd..9aba23cbc5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-type/workspace/block-type-workspace.element.ts @@ -26,7 +26,7 @@ export class UmbBlockTypeWorkspaceElement extends UmbLitElement { public set manifest(manifest: ManifestWorkspace) { this.#manifest = manifest; - createExtensionApi(manifest, [this, { manifest: manifest }]).then((context) => { + createExtensionApi(this, manifest, [{ manifest: manifest }]).then((context) => { if (context) { this.#gotWorkspaceContext(context); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/conditions/block-entry-show-content-edit.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/conditions/block-entry-show-content-edit.condition.ts index aa8b87b6ca..c0bed4622c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/conditions/block-entry-show-content-edit.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/conditions/block-entry-show-content-edit.condition.ts @@ -5,13 +5,17 @@ import type { UmbConditionControllerArguments, UmbExtensionCondition, } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbBlockEntryShowContentEditCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor( + host: UmbControllerHost, + args: UmbConditionControllerArguments, + ) { + super(host, args); this.consumeContext(UMB_BLOCK_ENTRY_CONTEXT, (context) => { this.observe( diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/conditions/block-workspace-has-settings.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/conditions/block-workspace-has-settings.condition.ts index cde3b8eba1..07cb74f5d6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/conditions/block-workspace-has-settings.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/conditions/block-workspace-has-settings.condition.ts @@ -5,13 +5,17 @@ import type { UmbConditionControllerArguments, UmbExtensionCondition, } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbBlockWorkspaceHasSettingsCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor( + host: UmbControllerHost, + args: UmbConditionControllerArguments, + ) { + super(host, args); this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (context) => { this.observe( diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.element.ts index e01c33cbb3..45e7262b62 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.element.ts @@ -27,7 +27,7 @@ export class UmbBlockWorkspaceElement extends UmbLitElement { public set manifest(manifest: ManifestWorkspace) { this.#manifest = manifest; - createExtensionApi(manifest, [this, { manifest: manifest }]).then((context) => { + createExtensionApi(this, manifest, [{ manifest: manifest }]).then((context) => { if (context) { this.#gotWorkspaceContext(context); // TODO: Do we need to recreate when ID changed? Or is that a responsibility of the context it self? diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/action/collection-action-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/action/collection-action-button.element.ts index 6092a23ff8..90aa744d03 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/action/collection-action-button.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/action/collection-action-button.element.ts @@ -43,7 +43,7 @@ export class UmbCollectionActionButtonElement extends UmbLitElement { async #createApi() { if (!this._manifest) throw new Error('No manifest defined'); if (!this._manifest.api) return; - this.#api = (await createExtensionApi(this._manifest, [this])) as unknown as UmbCollectionAction; + this.#api = (await createExtensionApi(this, this._manifest)) as unknown as UmbCollectionAction; } #api?: UmbCollectionAction; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-alias.condition.ts index 7f8736c98d..1327d72ef6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-alias.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-alias.condition.ts @@ -2,13 +2,14 @@ import { UMB_DEFAULT_COLLECTION_CONTEXT } from './default/collection-default.con import type { CollectionAliasConditionConfig } from './collection-alias.manifest.js'; import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbCollectionAliasCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (context) => { this.permitted = context.getManifest()?.alias === this.config.match; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-bulk-action-permission.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-bulk-action-permission.condition.ts index 0d76423db9..1b2d798509 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-bulk-action-permission.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection-bulk-action-permission.condition.ts @@ -2,13 +2,17 @@ import { UMB_DEFAULT_COLLECTION_CONTEXT } from './default/collection-default.con import type { CollectionBulkActionPermissionConditionConfig } from './collection-bulk-action-permission.manifest.js'; import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; import { UmbConditionBase } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbCollectionBulkActionPermissionCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor( + host: UmbControllerHost, + args: UmbConditionControllerArguments, + ) { + super(host, args); this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (context) => { const allowedActions = context.getConfig().allowedEntityBulkActions; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection.element.ts index 5a395ce140..aedfa7e9d3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection.element.ts @@ -50,7 +50,7 @@ export class UmbCollectionElement extends UmbLitElement { async #createApi() { if (!this.#manifest) throw new Error('No manifest'); - this.#api = (await createExtensionApi(this.#manifest, [this])) as unknown as UmbCollectionContext; + this.#api = (await createExtensionApi(this, this.#manifest)) as unknown as UmbCollectionContext; if (!this.#api) throw new Error('No api'); this.#api.setManifest(this.#manifest); this.#setConfig(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/extension-with-api-slot/extension-with-api-slot.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/extension-with-api-slot/extension-with-api-slot.test.ts index 94000f2098..7e4bc57f7f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/extension-with-api-slot/extension-with-api-slot.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/extension-with-api-slot/extension-with-api-slot.test.ts @@ -6,7 +6,7 @@ import type { ManifestElementAndApi, ManifestWithDynamicConditions, UmbApi, - UmbExtensionElementInitializer, + UmbExtensionElementAndApiInitializer, } from '@umbraco-cms/backoffice/extension-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbControllerHostElementMixin, type UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; @@ -109,7 +109,8 @@ describe('UmbExtensionWithApiSlotElement', () => { html` x.alias === 'unit-test-ext-slot-element-manifest'} - .renderMethod=${(controller: UmbExtensionElementInitializer) => html`${controller.component}`}> + .renderMethod=${(controller: UmbExtensionElementAndApiInitializer) => + html`${controller.component}`}> `, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.element.ts index 12929ca3f9..370ccd8e0a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/field-dropdown-list/field-dropdown-list.element.ts @@ -1,5 +1,5 @@ import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; -import { UmbDocumentTypeDetailRepository } from '@umbraco-cms/backoffice/document-type'; +import { UmbDocumentTypeDetailRepository, UMB_DOCUMENT_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/document-type'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { css, @@ -16,7 +16,6 @@ import type { UUIComboboxEvent, UUIComboboxElement } from '@umbraco-cms/backoffi import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbMediaTypeDetailRepository } from '@umbraco-cms/backoffice/media-type'; import { - UMB_DOCUMENT_TYPE_PICKER_MODAL, UMB_MEDIA_TYPE_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT, type UmbModalManagerContext, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/icon/icon.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/icon/icon.stories.ts index a42463d6b8..e8a5cfae35 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/icon/icon.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/icon/icon.stories.ts @@ -3,7 +3,7 @@ import './icon.element.js'; import type { UmbIconElement } from './icon.element.js'; const meta: Meta = { - title: 'Components/Inputs/Color', + title: 'Components/Inputs/Icon', component: 'umb-icon', }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts index 0ce899d6a4..9034604aff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/index.ts @@ -22,7 +22,6 @@ export * from './input-list-base/index.js'; export * from './input-multi-url/index.js'; export * from './input-number-range/index.js'; export * from './input-radio-button-list/index.js'; -export * from './input-section/index.js'; export * from './input-slider/index.js'; export * from './input-tree-picker-source/index.js'; export * from './input-toggle/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-collection-configuration/input-collection-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-collection-configuration/input-collection-configuration.element.ts index 3f73467245..9e27a0707c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-collection-configuration/input-collection-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-collection-configuration/input-collection-configuration.element.ts @@ -3,11 +3,13 @@ import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository'; -import { UMB_DATATYPE_WORKSPACE_MODAL, UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/data-type'; import { - UmbModalRouteRegistrationController, + UMB_DATATYPE_WORKSPACE_MODAL, + UMB_DATA_TYPE_ENTITY_TYPE, + UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS, UMB_DATA_TYPE_PICKER_FLOW_DATA_TYPE_PICKER_MODAL, -} from '@umbraco-cms/backoffice/modal'; +} from '@umbraco-cms/backoffice/data-type'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import type { UmbDataTypeItemModel } from '@umbraco-cms/backoffice/data-type'; @customElement('umb-input-collection-configuration') @@ -72,7 +74,7 @@ export class UmbInputCollectionConfigurationElement extends FormControlMixin(Umb this.#createDataTypeModal = new UmbModalRouteRegistrationController(this, UMB_DATATYPE_WORKSPACE_MODAL) .addAdditionalPath(':uiAlias') .onSetup((params) => { - return { data: { entityType: 'data-type', preset: { editorUiAlias: params.uiAlias } } }; + return { data: { entityType: UMB_DATA_TYPE_ENTITY_TYPE, preset: { editorUiAlias: params.uiAlias } } }; }) .onSubmit((value) => { this.#setValue(value?.unique ?? this.defaultValue ?? ''); @@ -92,7 +94,10 @@ export class UmbInputCollectionConfigurationElement extends FormControlMixin(Umb } #createDataType() { - this.#createDataTypeModal.open({ uiAlias: this.#propertyEditorUiAlias }, 'create/null'); + this.#createDataTypeModal.open( + { uiAlias: this.#propertyEditorUiAlias }, + `create/parent/${UMB_DATA_TYPE_ENTITY_TYPE}/null`, + ); } render() { 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 b64bb63ac9..2b6d05ffd5 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 @@ -1,4 +1,13 @@ -import { css, html, nothing, customElement, property, query, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { + css, + html, + nothing, + customElement, + property, + query, + ifDefined, + state, +} from '@umbraco-cms/backoffice/external/lit'; import type { UUIColorPickerElement, UUIInputElement, UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; @@ -10,6 +19,23 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; */ @customElement('umb-multiple-color-picker-item-input') export class UmbMultipleColorPickerItemInputElement extends FormControlMixin(UmbLitElement) { + @property({ type: String }) + public set value(value: string) { + if (value.startsWith('#')) { + this._valueHex = value; + super.value = value.substring(1); + } else { + super.value = value; + this._valueHex = `#${value}`; + } + } + public get value() { + return super.value as string; + } + + @state() + private _valueHex = ''; + /** * Disables the input * @type {boolean} @@ -65,13 +91,13 @@ export class UmbMultipleColorPickerItemInputElement extends FormControlMixin(Umb #onValueChange(event: UUIInputEvent) { event.stopPropagation(); - this.value = event.target.value; + this.value = event.target.value as string; this.dispatchEvent(new UmbChangeEvent()); } #onValueInput(event: UUIInputEvent) { event.stopPropagation(); - this.value = event.target.value; + this.value = event.target.value as string; this.dispatchEvent(new UmbInputEvent()); } @@ -123,7 +149,7 @@ export class UmbMultipleColorPickerItemInputElement extends FormControlMixin(Umb diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-structure-manager.class.ts index 603944def7..4bbc6e7abb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/content-type-structure-manager.class.ts @@ -144,22 +144,23 @@ export class UmbContentTypePropertyStructureManager { - if (docType) { - // TODO: Handle if there was changes made to the owner document type in this context. - /* - possible easy solutions could be to notify user wether they want to update(Discard the changes to accept the new ones). - */ - this.#contentTypes.appendOne(docType); - } - }, - 'observeContentType_' + data.unique, - ), + const ctrl = this.observe( + // Then lets start observation of the content type: + await this.#contentTypeRepository.byUnique(data.unique), + (docType) => { + if (docType) { + // TODO: Handle if there was changes made to the owner document type in this context. [NL] + /* + possible easy solutions could be to notify user wether they want to update(Discard the changes to accept the new ones). [NL] + */ + this.#contentTypes.appendOne(docType); + } + // TODO: Do we need to handle the undefined case? [NL] + }, + 'observeContentType_' + data.unique, ); + + this.#contentTypeObservers.push(ctrl); } private async _loadContentTypeCompositions(contentType: T) { @@ -493,7 +494,7 @@ export class UmbContentTypePropertyStructureManager { return data.filter((x) => x.name === name && x.type === containerType); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/copy/copy.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/copy/copy.action.ts deleted file mode 100644 index f7529a94ac..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/copy/copy.action.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { UmbEntityActionBase } from '../../entity-action-base.js'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; - -export class UmbCopyEntityAction extends UmbEntityActionBase { - constructor(host: UmbControllerHost, args: any) { - super(host, args); - } - - async execute() { - console.log(`execute copy for: ${this.args.unique}`); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.kind.ts index 06c856c704..bf2f0136f6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.kind.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.kind.ts @@ -1,4 +1,4 @@ -import { UmbDeleteEntityAction } from './delete.action.js'; +import { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from '../../default/default.action.kind.js'; import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifest: UmbBackofficeManifestKind = { @@ -7,9 +7,10 @@ export const manifest: UmbBackofficeManifestKind = { matchKind: 'delete', matchType: 'entityAction', manifest: { + ...UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST.manifest, type: 'entityAction', kind: 'delete', - api: UmbDeleteEntityAction, + api: () => import('./delete.action.js'), weight: 900, forEntityTypes: [], meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts index bcb87274c0..e351478096 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/delete/delete.action.ts @@ -6,6 +6,8 @@ import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; export class UmbDeleteEntityAction extends UmbEntityActionBase { // TODO: make base type for item and detail models @@ -58,5 +60,14 @@ export class UmbDeleteEntityAction extends UmbEntityActionBase import('./duplicate.action.js'), weight: 600, forEntityTypes: [], meta: { @@ -17,7 +18,7 @@ export const manifest: UmbBackofficeManifestKind = { label: 'Duplicate to...', itemRepositoryAlias: '', duplicateRepositoryAlias: '', - pickerModalAlias: '', + pickerModal: '', }, }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/duplicate/duplicate.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/duplicate/duplicate.action.ts index 6317f7905e..14a1df3b0c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/duplicate/duplicate.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/duplicate/duplicate.action.ts @@ -5,12 +5,12 @@ import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-ap import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import type { MetaEntityActionDuplicateKind } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import type { UmbCopyRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import type { UmbDuplicateRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository'; export class UmbDuplicateEntityAction extends UmbEntityActionBase { // TODO: make base type for item and detail models #itemRepository?: UmbItemRepository; - #duplicateRepository?: UmbCopyRepository; + #duplicateRepository?: UmbDuplicateRepository; #init: Promise; constructor(host: UmbControllerHost, args: UmbEntityActionArgs) { @@ -35,7 +35,7 @@ export class UmbDuplicateEntityAction extends UmbEntityActionBase { - this.#duplicateRepository = permitted ? (ctrl.api as UmbCopyRepository) : undefined; + this.#duplicateRepository = permitted ? (ctrl.api as UmbDuplicateRepository) : undefined; }, ).asPromise(), ]); @@ -46,9 +46,11 @@ export class UmbDuplicateEntityAction extends UmbEntityActionBase import('./move.action.js'), weight: 700, forEntityTypes: [], meta: { @@ -17,7 +18,7 @@ export const manifest: UmbBackofficeManifestKind = { label: 'Move to (TBD)...', itemRepositoryAlias: '', moveRepositoryAlias: '', - pickerModalAlias: '', + pickerModal: '', }, }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/move/move.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/move/move.action.ts index 966b28ed18..cefe1cc8f8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/move/move.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/move/move.action.ts @@ -45,9 +45,11 @@ export class UmbMoveEntityAction extends UmbEntityActionBase { await this.#init; const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); - const modalContext = modalManager.open(this, this.args.meta.pickerModalAlias) as any; // TODO: make generic picker interface with selection + const modalContext = modalManager.open(this, this.args.meta.pickerModal) as any; // TODO: make generic picker interface with selection const value = await modalContext.onSubmit(); if (!value) return; await this.#moveRepository!.move(this.args.unique, value.selection[0]); } } + +export default UmbMoveEntityAction; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/rename/rename.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/rename/rename.action.kind.ts index eaa95f54c1..744a0b35b6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/rename/rename.action.kind.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/rename/rename.action.kind.ts @@ -1,4 +1,4 @@ -import { UmbRenameEntityAction } from './rename.action.js'; +import { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from '../../default/default.action.kind.js'; import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifest: UmbBackofficeManifestKind = { @@ -7,9 +7,10 @@ export const manifest: UmbBackofficeManifestKind = { matchKind: 'rename', matchType: 'entityAction', manifest: { + ...UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST.manifest, type: 'entityAction', kind: 'rename', - api: UmbRenameEntityAction, + api: () => import('./rename.action.js'), weight: 200, forEntityTypes: [], meta: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/rename/rename.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/rename/rename.action.ts index d1f870da67..a55bdc9bca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/rename/rename.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/rename/rename.action.ts @@ -19,3 +19,5 @@ export class UmbRenameEntityAction extends UmbEntityActionBase { console.log(`execute sort for: ${this.args.unique}`); } } + +export default UmbSortChildrenOfEntityAction; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/manifests.ts new file mode 100644 index 0000000000..3f3fc9c2c7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/manifests.ts @@ -0,0 +1,3 @@ +import { manifest as trashKindManifest } from './trash.action.kind.js'; + +export const manifests = [trashKindManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/trash.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/trash.action.kind.ts new file mode 100644 index 0000000000..beb3103a7b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/trash.action.kind.ts @@ -0,0 +1,23 @@ +import { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from '../../default/default.action.kind.js'; +import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: UmbBackofficeManifestKind = { + type: 'kind', + alias: 'Umb.Kind.EntityAction.Trash', + matchKind: 'trash', + matchType: 'entityAction', + manifest: { + ...UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST.manifest, + type: 'entityAction', + kind: 'trash', + api: () => import('./trash.action.js'), + weight: 900, + forEntityTypes: [], + meta: { + icon: 'icon-trash', + label: 'Trash', + itemRepositoryAlias: '', + trashRepositoryAlias: '', + }, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/trash.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/trash.action.ts index 37075d2dbe..3c4fc550c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/trash.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/trash/trash.action.ts @@ -30,3 +30,5 @@ export class UmbTrashEntityAction extends UmbEntityActionBase { */ } } + +export default UmbTrashEntityAction; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/default/default.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/default/default.action.kind.ts index 8eb15d6bc6..b75aec9476 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/default/default.action.kind.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/default/default.action.kind.ts @@ -1,6 +1,6 @@ import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifest: UmbBackofficeManifestKind = { +export const UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST: UmbBackofficeManifestKind = { type: 'kind', alias: 'Umb.Kind.EntityAction.Default', matchKind: 'default', @@ -16,3 +16,5 @@ export const manifest: UmbBackofficeManifestKind = { }, }, }; + +export const manifest = UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-action-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-action-list.element.ts index b2c78db5b3..4db7611b63 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-action-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-action-list.element.ts @@ -56,7 +56,6 @@ export class UmbEntityActionListElement extends UmbLitElement { ? html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts index 4413b77bfb..b665b5f67b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts @@ -1,7 +1,8 @@ export * from './common/index.js'; export * from './entity-action-base.js'; export * from './entity-action-list.element.js'; -export * from './default/entity-action.element.js'; export * from './entity-action.event.js'; export * from './entity-action.interface.js'; export * from './types.js'; + +export { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from './default/default.action.kind.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/manifests.ts index 5640f2160a..c9c8908eb2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/manifests.ts @@ -1,13 +1,15 @@ -import { manifests as copyEntityActionManifests } from './common/duplicate/manifests.js'; +import { manifests as defaultEntityActionManifests } from './default/manifests.js'; import { manifests as deleteEntityActionManifests } from './common/delete/manifests.js'; +import { manifests as duplicateEntityActionManifests } from './common/duplicate/manifests.js'; import { manifests as moveEntityActionManifests } from './common/move/manifests.js'; import { manifests as renameEntityActionManifests } from './common/rename/manifests.js'; -import { manifests as defaultEntityActionManifests } from './default/manifests.js'; +import { manifests as trashEntityActionManifests } from './common/trash/manifests.js'; export const manifests = [ - ...copyEntityActionManifests, + ...defaultEntityActionManifests, ...deleteEntityActionManifests, + ...duplicateEntityActionManifests, ...moveEntityActionManifests, ...renameEntityActionManifests, - ...defaultEntityActionManifests, + ...trashEntityActionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/event/deselected.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/event/deselected.event.ts index 401db9833f..307c11a6cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/event/deselected.event.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/event/deselected.event.ts @@ -2,9 +2,9 @@ export class UmbDeselectedEvent extends Event { public static readonly TYPE = 'deselected'; public unique: string | null; - public constructor(unique: string | null) { + public constructor(unique: string | null, args?: EventInit) { // mimics the native change event - super(UmbDeselectedEvent.TYPE, { bubbles: true, composed: false, cancelable: false }); + super(UmbDeselectedEvent.TYPE, { bubbles: true, composed: false, cancelable: false, ...args }); this.unique = unique; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/event/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/event/index.ts index 7fb35d2757..af975b4ddc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/event/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/event/index.ts @@ -5,3 +5,4 @@ export * from './deselected.event.js'; export * from './input.event.js'; export * from './selected.event.js'; export * from './selection-change.event.js'; +export * from './request-reload-structure-for-entity.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/event/request-reload-structure-for-entity.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/event/request-reload-structure-for-entity.event.ts new file mode 100644 index 0000000000..ad25c7b1c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/event/request-reload-structure-for-entity.event.ts @@ -0,0 +1,9 @@ +import { UmbEntityActionEvent, type UmbEntityActionEventArgs } from '@umbraco-cms/backoffice/entity-action'; + +export class UmbRequestReloadStructureForEntityEvent extends UmbEntityActionEvent { + static readonly TYPE = 'request-reload-structure-for-entity'; + + constructor(args: UmbEntityActionEventArgs) { + super(UmbRequestReloadStructureForEntityEvent.TYPE, args); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/event/selected.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/event/selected.event.ts index 14374a1769..e3be48c430 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/event/selected.event.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/event/selected.event.ts @@ -2,9 +2,9 @@ export class UmbSelectedEvent extends Event { public static readonly TYPE = 'selected'; public unique: string | null; - public constructor(unique: string | null) { + public constructor(unique: string | null, args?: EventInit) { // mimics the native change event - super(UmbSelectedEvent.TYPE, { bubbles: true, composed: false, cancelable: false }); + super(UmbSelectedEvent.TYPE, { bubbles: true, composed: false, cancelable: false, ...args }); this.unique = unique; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/event/selection-change.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/event/selection-change.event.ts index ac3351a749..dbf015e380 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/event/selection-change.event.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/event/selection-change.event.ts @@ -1,8 +1,8 @@ export class UmbSelectionChangeEvent extends Event { public static readonly TYPE = 'selection-change'; - public constructor() { + public constructor(args?: EventInit) { // mimics the native change event - super(UmbSelectionChangeEvent.TYPE, { bubbles: true, composed: false, cancelable: false }); + super(UmbSelectionChangeEvent.TYPE, { bubbles: true, composed: false, cancelable: false, ...args }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/extension-table-action-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/extension-table-action-column-layout.element.ts index 92cdf10ec1..21e61f9820 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/extension-table-action-column-layout.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/collection/views/extension-table-action-column-layout.element.ts @@ -1,14 +1,26 @@ import { umbExtensionsRegistry } from '../../index.js'; -import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api'; +import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +import type { ManifestBase } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; @customElement('umb-extension-table-action-column-layout') export class UmbExtensionTableActionColumnLayoutElement extends UmbLitElement { @property({ attribute: false }) value!: ManifestBase; + #collectionContext?: UmbDefaultCollectionContext; + + constructor() { + super(); + + this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance; + }); + } + async #removeExtension() { await umbConfirmModal(this, { headline: 'Unload extension', @@ -16,7 +28,10 @@ export class UmbExtensionTableActionColumnLayoutElement extends UmbLitElement { content: html`

Are you sure you want to unload the extension ${this.value.alias}?

`, color: 'danger', }); + umbExtensionsRegistry.unregister(this.value.alias); + + this.#collectionContext?.requestCollection(); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/condition-base.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/condition-base.controller.ts index 3d6d570db4..b722af45a0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/condition-base.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/condition-base.controller.ts @@ -18,8 +18,8 @@ export class UmbConditionBase void; - constructor(args: { host: UmbControllerHost; config: ConditionConfigType; onChange: () => void }) { - super(args.host); + constructor(host: UmbControllerHost, args: { config: ConditionConfigType; onChange: () => void }) { + super(host); this.config = args.config; this.#onChange = args.onChange; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/menu-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/menu-alias.condition.ts index 347e3ac1eb..01ccf3bcb3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/menu-alias.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/menu-alias.condition.ts @@ -1,5 +1,6 @@ import { UMB_MENU_CONTEXT } from '../../menu/menu.context.js'; import { UmbConditionBase } from './condition-base.controller.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { ManifestCondition, UmbConditionConfigBase, @@ -12,8 +13,8 @@ export type MenuAliasConditionConfig = UmbConditionConfigBase & { }; export class UmbMenuAliasCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); this.consumeContext(UMB_MENU_CONTEXT, (context) => { this.observe(context.alias, (MenuAlias) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/section-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/section-alias.condition.ts index 0827c8073f..34461d7f3d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/section-alias.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/section-alias.condition.ts @@ -1,4 +1,5 @@ import { UmbConditionBase } from './condition-base.controller.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { ManifestCondition, UmbConditionConfigBase, @@ -11,8 +12,8 @@ export class UmbSectionAliasCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); this.consumeContext(UMB_SECTION_CONTEXT, (context) => { this.observe(context.alias, (sectionAlias) => { this.permitted = sectionAlias === this.config.match; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/switch.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/switch.condition.ts index b9f572b6dd..3bf8fc1f6b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/switch.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/conditions/switch.condition.ts @@ -1,4 +1,5 @@ import { UmbConditionBase } from './condition-base.controller.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { ManifestCondition, UmbConditionConfigBase, @@ -9,8 +10,8 @@ import type { export class UmbSwitchCondition extends UmbConditionBase implements UmbExtensionCondition { #timer?: ReturnType; - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); this.startApprove(); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts index cfe4bc484a..3c05c5991f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extension-element-and-api-slot-element-base.ts @@ -1,5 +1,5 @@ import { umbExtensionsRegistry } from './registry.js'; -import { html, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; import { UmbExtensionElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api'; @@ -60,7 +60,14 @@ export abstract class UmbExtensionElementAndApiSlotElementBase< this.requestUpdate('_element'); }; - render() { - return html`${this._element}`; + protected render() { + return this._element; + } + + /** + * Disable the Shadow DOM for this element. This is needed because this is a wrapper element and should not stop the event propagation. + */ + protected createRenderRoot() { + return this; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/entity-action.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/entity-action.model.ts index 7a303e1296..ff5359de7b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/entity-action.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/entity-action.model.ts @@ -1,6 +1,7 @@ import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import type { UmbEntityAction } from '@umbraco-cms/backoffice/entity-action'; import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbModalToken } from '@umbraco-cms/backoffice/modal'; /** * An action to perform on an entity @@ -15,6 +16,7 @@ export interface ManifestEntityAction { type: 'entityAction'; kind: 'default'; @@ -53,6 +55,17 @@ export interface MetaEntityActionDeleteKind extends MetaEntityActionDefaultKind itemRepositoryAlias: string; } +// TRASH +export interface ManifestEntityActionTrashKind extends ManifestEntityAction { + type: 'entityAction'; + kind: 'trash'; +} + +export interface MetaEntityActionTrashKind extends MetaEntityActionDefaultKind { + trashRepositoryAlias: string; + itemRepositoryAlias: string; +} + // RENAME export interface ManifestEntityActionRenameKind extends ManifestEntityAction { type: 'entityAction'; @@ -82,7 +95,7 @@ export interface ManifestEntityActionDuplicateKind extends ManifestEntityAction< export interface MetaEntityActionDuplicateKind extends MetaEntityActionDefaultKind { duplicateRepositoryAlias: string; itemRepositoryAlias: string; - pickerModalAlias: string; + pickerModal: UmbModalToken | string; } // MOVE @@ -94,7 +107,7 @@ export interface ManifestEntityActionMoveKind extends ManifestEntityAction | ManifestCondition @@ -104,16 +122,7 @@ export type ManifestTypes = | ManifestDashboardCollection | ManifestDynamicRootOrigin | ManifestDynamicRootQueryStep - | ManifestEntityAction - | ManifestEntityActionDefaultKind - | ManifestEntityActionDeleteKind - | ManifestEntityActionRenameKind - | ManifestEntityActionReloadTreeItemChildrenKind - | ManifestEntityActionDuplicateKind - | ManifestEntityActionMoveKind - | ManifestEntityActionCreateFolderKind - | ManifestEntityActionUpdateFolderKind - | ManifestEntityActionDeleteFolderKind + | ManifestEntityActions | ManifestEntityBulkAction | ManifestEntryPoint | ManifestExternalLoginProvider @@ -127,7 +136,7 @@ export type ManifestTypes = | ManifestMenuItemTreeKind | ManifestModal | ManifestPackageView - | ManifestPropertyAction + | ManifestPropertyActions | ManifestPropertyEditorSchema | ManifestPropertyEditorUi | ManifestRepository @@ -144,7 +153,7 @@ export type ManifestTypes = | ManifestTreeStore | ManifestUserProfileApp | ManifestWorkspace - | ManifestWorkspaceAction + | ManifestWorkspaceActions | ManifestWorkspaceActionMenuItem | ManifestWorkspaceContext | ManifestWorkspaceFooterApp diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/property-action.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/property-action.model.ts index aa0091ef19..27484ac995 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/property-action.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/property-action.model.ts @@ -1,13 +1,43 @@ import type { ConditionTypes } from '../conditions/types.js'; -import type { ManifestElement, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbPropertyAction } from '../../property-action/components/property-action/property-action.interface.js'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; -export interface ManifestPropertyAction - extends ManifestElement, +export interface ManifestPropertyAction + extends ManifestElementAndApi>, ManifestWithDynamicConditions { type: 'propertyAction'; - meta: MetaPropertyAction; + forPropertyEditorUis: string[]; + meta: MetaType; } -export interface MetaPropertyAction { - propertyEditors: string[]; +export interface MetaPropertyAction {} + +export interface ManifestPropertyActionDefaultKind< + MetaType extends MetaPropertyActionDefaultKind = MetaPropertyActionDefaultKind, +> extends ManifestPropertyAction { + type: 'propertyAction'; + kind: 'default'; +} + +export interface MetaPropertyActionDefaultKind extends MetaPropertyAction { + /** + * An icon to represent the action to be performed + * + * @examples [ + * "icon-box", + * "icon-grid" + * ] + */ + icon: string; + + /** + * The friendly name of the action to perform + * + * @examples [ + * "Create", + * "Create Content Template" + * ] + */ + label: string; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-action-menu-item.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-action-menu-item.model.ts index 46b61f193b..e2178dcb4c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-action-menu-item.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-action-menu-item.model.ts @@ -1,10 +1,11 @@ import type { ConditionTypes } from '../conditions/types.js'; +import type { UmbWorkspaceActionMenuItem } from '../../workspace/components/workspace-action-menu-item/workspace-action-menu-item.interface.js'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbraco-cms/backoffice/extension-api'; -import type { UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; -export interface ManifestWorkspaceActionMenuItem - extends ManifestElementAndApi, +export interface ManifestWorkspaceActionMenuItem< + MetaType extends MetaWorkspaceActionMenuItem = MetaWorkspaceActionMenuItem, +> extends ManifestElementAndApi>, ManifestWithDynamicConditions { type: 'workspaceActionMenuItem'; /** @@ -16,10 +17,19 @@ export interface ManifestWorkspaceActionMenuItem * @required */ forWorkspaceActions: string | string[]; - meta: MetaWorkspaceActionMenuItem; + meta: MetaType; } -export interface MetaWorkspaceActionMenuItem { +export interface MetaWorkspaceActionMenuItem {} + +export interface ManifestWorkspaceActionMenuItemDefaultKind< + MetaType extends MetaWorkspaceActionMenuItemDefaultKind = MetaWorkspaceActionMenuItemDefaultKind, +> extends ManifestWorkspaceActionMenuItem { + type: 'workspaceActionMenuItem'; + kind: 'default'; +} + +export interface MetaWorkspaceActionMenuItemDefaultKind extends MetaWorkspaceActionMenuItem { /** * An icon to represent the action to be performed * diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-action.model.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-action.model.ts index 8096a56761..ad01995e55 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-action.model.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/models/workspace-action.model.ts @@ -4,14 +4,21 @@ import type { ManifestElementAndApi, ManifestWithDynamicConditions } from '@umbr import type { UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -export interface ManifestWorkspaceAction - extends ManifestElementAndApi, +export interface ManifestWorkspaceAction + extends ManifestElementAndApi>, ManifestWithDynamicConditions { type: 'workspaceAction'; - meta: MetaWorkspaceAction; + meta: MetaType; } -export interface MetaWorkspaceAction { +export interface MetaWorkspaceAction {} + +export interface ManifestWorkspaceActionDefaultKind extends ManifestWorkspaceAction { + type: 'workspaceAction'; + kind: 'default'; +} + +export interface MetaWorkspaceActionDefaultKind extends MetaWorkspaceAction { label?: string; //TODO: Use or implement additional label-key look?: UUIInterfaceLook; color?: UUIInterfaceColor; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/workspace/manifests.ts index 5ac83bbc20..fadbc1388f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/workspace/manifests.ts @@ -1,6 +1,6 @@ import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -18,6 +18,6 @@ const workspace: ManifestWorkspace = { const workspaceViews: Array = []; -const workspaceActions: Array = []; +const workspaceActions: Array = []; export const manifests = [workspace, ...workspaceViews, ...workspaceActions]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts index a72acdc198..460ad72c22 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/manifests.ts @@ -7,6 +7,7 @@ import { manifests as localizationManifests } from './localization/manifests.js' import { manifests as modalManifests } from './modal/common/manifests.js'; import { manifests as propertyActionManifests } from './property-action/manifests.js'; import { manifests as propertyEditorManifests } from './property-editor/manifests.js'; +import { manifests as sectionManifests } from './section/manifests.js'; import { manifests as settingsManifests } from './settings/manifests.js'; import { manifests as themeManifests } from './themes/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; @@ -24,6 +25,7 @@ export const manifests: Array = [ ...modalManifests, ...propertyActionManifests, ...propertyEditorManifests, + ...sectionManifests, ...settingsManifests, ...themeManifests, ...treeManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/manifests.ts index 1252ac2c69..6635895244 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/manifests.ts @@ -25,12 +25,6 @@ const modals: Array = [ name: 'Property Settings Modal', js: () => import('./property-settings/property-settings-modal.element.js'), }, - { - type: 'modal', - alias: 'Umb.Modal.SectionPicker', - name: 'Section Picker Modal', - js: () => import('./section-picker/section-picker-modal.element.js'), - }, { type: 'modal', alias: 'Umb.Modal.CodeEditor', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/property-settings/property-settings-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/property-settings/property-settings-modal.element.ts index 832bd3cdc0..6e6e313c9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/property-settings/property-settings-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/property-settings/property-settings-modal.element.ts @@ -65,7 +65,7 @@ export class UmbPropertySettingsModalElement extends UmbModalBaseElement< this.observe(instance.variesByCulture, (variesByCulture) => (this._documentVariesByCulture = variesByCulture)); this.observe(instance.variesBySegment, (variesBySegment) => (this._documentVariesBySegment = variesBySegment)); - }).skipOrigin(); + }).skipHost(); this._originalPropertyData = this.value; this.#isNew = this.value.alias === ''; @@ -357,7 +357,7 @@ export class UmbPropertySettingsModalElement extends UmbModalBaseElement< name="pattern-message" @change=${this.#onValidationMessageChange} .value=${this.value.validation?.regExMessage ?? ''}> - ` + ` : nothing} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts index 83f5786e33..b5a431d7cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/component/modal.element.ts @@ -6,8 +6,7 @@ import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registr import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; -import { BehaviorSubject } from '@umbraco-cms/backoffice/external/rxjs'; -import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbBasicState, type UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; import { UUIModalCloseEvent, type UUIDialogElement, @@ -39,7 +38,7 @@ export class UmbModalElement extends UmbLitElement { public element?: UUIModalDialogElement | UUIModalSidebarElement; - #innerElement = new BehaviorSubject(undefined); + #innerElement = new UmbBasicState(undefined); #modalExtensionObserver?: UmbObserverController; #modalRouterElement: UmbRouterSlotElement = document.createElement('umb-router-slot'); @@ -153,14 +152,14 @@ export class UmbModalElement extends UmbLitElement { #appendInnerElement(element: HTMLElement) { this.#modalRouterElement.appendChild(element); - this.#innerElement.next(element); + this.#innerElement.setValue(element); } #removeInnerElement() { const innerElement = this.#innerElement.getValue(); if (innerElement) { this.#modalRouterElement.removeChild(innerElement); - this.#innerElement.next(undefined); + this.#innerElement.setValue(undefined); } } @@ -178,7 +177,7 @@ export class UmbModalElement extends UmbLitElement { }; destroy() { - this.#innerElement.complete(); + this.#innerElement.destroy(); this.#modalExtensionObserver?.destroy(); this.#modalExtensionObserver = undefined; if (this.#modalContext) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/create-dictionary-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/create-dictionary-modal.token.ts deleted file mode 100644 index 38254ca72c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/create-dictionary-modal.token.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { UmbModalToken } from './modal-token.js'; -import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; - -export interface UmbCreateDictionaryModalData { - parentId: string | null; - parentName?: Observable; -} - -export interface UmbCreateDictionaryModalValue { - name: string; - parentId: string | null; -} - -export const UMB_CREATE_DICTIONARY_MODAL = new UmbModalToken< - UmbCreateDictionaryModalData, - UmbCreateDictionaryModalValue ->('Umb.Modal.Dictionary.Create', { - modal: { - type: 'sidebar', - size: 'small', - }, -}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/dictionary-item-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/dictionary-item-picker-modal.token.ts deleted file mode 100644 index 0e6966abbf..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/dictionary-item-picker-modal.token.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { UmbUniqueTreeItemModel } from '../../tree/types.js'; -import { UmbModalToken } from './modal-token.js'; -import type { UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; - -export type UmbDictionaryItemPickerModalData = UmbTreePickerModalData; -export type UmbDictionaryItemPickerModalValue = UmbPickerModalValue; - -export const UMB_DICTIONARY_ITEM_PICKER_MODAL = new UmbModalToken< - UmbDictionaryItemPickerModalData, - UmbDictionaryItemPickerModalValue ->('Umb.Modal.TreePicker', { - modal: { - type: 'sidebar', - size: 'small', - }, - data: { - hideTreeRoot: true, - treeAlias: 'Umb.Tree.Dictionary', - }, -}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts index 8d49914d48..ad9c8dc47e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/index.ts @@ -1,14 +1,7 @@ export * from './change-password-modal.token.js'; export * from './code-editor-modal.token.js'; export * from './confirm-modal.token.js'; -export * from './create-dictionary-modal.token.js'; -export * from './data-type-picker-flow-data-type-picker-modal.token.js'; -export * from './data-type-picker-flow-modal.token.js'; -export * from './data-type-picker-modal.token.js'; export * from './debug-modal.token.js'; -export * from './dictionary-item-picker-modal.token.js'; -export * from './document-picker-modal.token.js'; -export * from './document-type-picker-modal.token.js'; export * from './embedded-media-modal.token.js'; export * from './entity-user-permission-settings-modal.token.js'; export * from './examine-fields-settings-modal.token.js'; @@ -17,9 +10,6 @@ export * from './link-picker-modal.token.js'; export * from './media-tree-picker-modal.token.js'; export * from './media-type-picker-modal.token.js'; export * from './modal-token.js'; -export * from './permissions-modal.token.js'; export * from './property-editor-ui-picker-modal.token.js'; export * from './property-settings-modal.token.js'; -export * from './section-picker-modal.token.js'; -export * from './template-picker-modal.token.js'; export * from './workspace-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/permissions-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/permissions-modal.token.ts deleted file mode 100644 index 36cef88278..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/permissions-modal.token.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UmbModalToken } from './modal-token.js'; - -export interface UmbPermissionsModalData { - unique: string; - entityType: string; -} - -export type UmbPermissionsModalValue = undefined; - -export const UMB_PERMISSIONS_MODAL = new UmbModalToken( - 'Umb.Modal.Permissions', - { - modal: { - type: 'sidebar', - }, - }, -); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.controller.ts new file mode 100644 index 0000000000..2dfaf5c948 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.controller.ts @@ -0,0 +1,10 @@ +import { UmbPropertyActionBase } from '../../components/property-action/property-action-base.controller.js'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; + +export class UmbClearPropertyAction extends UmbPropertyActionBase { + async execute() { + const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT); + propertyContext.clearValue(); + } +} +export default UmbClearPropertyAction; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.element.ts deleted file mode 100644 index 443fdeab02..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.element.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { UmbPropertyAction } from '../../shared/property-action/property-action.interface.js'; -import type { UmbPropertyContext } from '@umbraco-cms/backoffice/property'; -import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; -import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; - -@customElement('umb-property-action-clear') -export class UmbPropertyActionClearElement extends UmbLitElement implements UmbPropertyAction { - @property() - value = ''; - - // THESE OUT COMMENTED CODE IS USED FOR THE EXAMPLE BELOW, TODO: Should be transferred to some documentation. - //private _propertyActionMenuContext?: UmbPropertyActionMenuContext; - private _propertyContext?: UmbPropertyContext; - - constructor() { - super(); - - /* - this.consumeContext('umbPropertyActionMenu', (propertyActionsContext: UmbPropertyActionMenuContext) => { - this._propertyActionMenuContext = propertyActionsContext; - }); - */ - this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext: UmbPropertyContext) => { - this._propertyContext = propertyContext; - }); - } - - private _handleLabelClick() { - this._clearValue(); - this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true })); - // Or you can do this: - //this._propertyActionMenuContext?.close(); - } - - private _clearValue() { - // TODO: how do we want to update the value? Testing an event based approach. We need to test an api based approach too. - //this.value = '';// This is though bad as it assumes we are dealing with a string. So wouldn't work as a generalized element. - //this.dispatchEvent(new CustomEvent('property-value-change')); - // Or you can do this: - this._propertyContext?.resetValue(); // This resets value to what the property wants. - } - - render() { - return html` - - `; - } -} - -export default UmbPropertyActionClearElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-property-action-clear': UmbPropertyActionClearElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.stories.ts deleted file mode 100644 index 33036675f6..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/clear/property-action-clear.stories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, Story } from '@storybook/web-components'; -import type { UmbPropertyActionClearElement } from './property-action-clear.element.js'; -import { html } from '@umbraco-cms/backoffice/external/lit'; - -import './property-action-clear.element.js'; - -export default { - title: 'Property Actions/Clear', - component: 'umb-property-action-clear', - id: 'umb-property-action-clear', -} as Meta; - -export const AAAOverview: Story = () => - html` `; -AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.controller.ts new file mode 100644 index 0000000000..acae0225fb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.controller.ts @@ -0,0 +1,16 @@ +import { UmbPropertyActionBase } from '../../components/property-action/property-action-base.controller.js'; +import { UMB_NOTIFICATION_CONTEXT, type UmbNotificationDefaultData } from '@umbraco-cms/backoffice/notification'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; + +export class UmbCopyPropertyAction extends UmbPropertyActionBase { + async execute() { + const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT); + const value = propertyContext.getValue(); + + const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + // TODO: Temporary solution to make something happen: [NL] + const data: UmbNotificationDefaultData = { headline: 'Copied to clipboard', message: value }; + notificationContext?.peek('positive', { data }); + } +} +export default UmbCopyPropertyAction; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.element.ts deleted file mode 100644 index a2d5dcc142..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.element.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { UmbPropertyAction } from '../../shared/property-action/property-action.interface.js'; -import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbNotificationDefaultData, UmbNotificationContext } from '@umbraco-cms/backoffice/notification'; -import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; -//import { UMB_WORKSPACE_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/workspace'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; - -@customElement('umb-property-action-copy') -export class UmbPropertyActionCopyElement extends UmbLitElement implements UmbPropertyAction { - @property() - value = ''; - - private _notificationContext?: UmbNotificationContext; - - constructor() { - super(); - - //this.consumeContext(UMB_WORKSPACE_PROPERTY_CONTEXT, (property) => { - //console.log('Got a reference to the editor element', property.getEditor()); - // Be aware that the element might switch, so using the direct reference is not recommended, instead observe the .element Observable() - //}); - - this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { - this._notificationContext = instance; - }); - } - - private _handleLabelClick() { - const data: UmbNotificationDefaultData = { message: 'Copied to clipboard' }; - this._notificationContext?.peek('positive', { data }); - // TODO: how do we want to close the menu? Testing an event based approach - this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true })); - } - - render() { - return html` - - `; - } -} - -export default UmbPropertyActionCopyElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-property-action-copy': UmbPropertyActionCopyElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.stories.ts deleted file mode 100644 index 6b80d553a3..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/common/copy/property-action-copy.stories.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Meta, Story } from '@storybook/web-components'; -import type { UmbPropertyActionCopyElement } from './property-action-copy.element.js'; -import { html } from '@umbraco-cms/backoffice/external/lit'; - -import './property-action-copy.element.js'; - -export default { - title: 'Property Actions/Copy', - component: 'umb-property-action-copy', - id: 'umb-property-action-copy', -} as Meta; - -export const AAAOverview: Story = () => - html` `; -AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action-menu/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action-menu/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action-menu/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action-menu/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action-menu/property-action-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action-menu/property-action-menu.element.ts similarity index 63% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action-menu/property-action-menu.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action-menu/property-action-menu.element.ts index 7aeeafff2b..c5b182725e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action-menu/property-action-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action-menu/property-action-menu.element.ts @@ -1,39 +1,35 @@ +import type { UmbPropertyActionArgs } from '../property-action/types.js'; import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { ManifestPropertyAction, ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; +import type { + ManifestPropertyAction, + ManifestTypes, + MetaPropertyAction, +} from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbExtensionElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { UmbExtensionsElementAndApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +function ExtensionApiArgsMethod(manifest: ManifestPropertyAction): [UmbPropertyActionArgs] { + return [{ meta: manifest.meta }]; +} @customElement('umb-property-action-menu') export class UmbPropertyActionMenuElement extends UmbLitElement { - #actionsInitializer?: UmbExtensionsElementInitializer; - #value: unknown; + #actionsInitializer?: UmbExtensionsElementAndApiInitializer; #propertyEditorUiAlias = ''; - @property({ attribute: false }) - public set value(value: unknown) { - this.#value = value; - if (this.#actionsInitializer) { - this.#actionsInitializer.properties = { value }; - } - } - public get value(): unknown { - return this.#value; - } - @property() set propertyEditorUiAlias(alias: string) { this.#propertyEditorUiAlias = alias; - // TODO: Stop using string for 'propertyAction', we need to start using Const. - // TODO: Align property actions with entity actions. - this.#actionsInitializer = new UmbExtensionsElementInitializer( + // TODO: Stop using string for 'propertyAction', we need to start using Const. [NL] + this.#actionsInitializer = new UmbExtensionsElementAndApiInitializer( this, umbExtensionsRegistry, 'propertyAction', - (propertyAction) => propertyAction.meta.propertyEditors.includes(alias), + ExtensionApiArgsMethod, + (propertyAction) => propertyAction.forPropertyEditorUis.includes(alias), (ctrls) => { this._actions = ctrls; }, @@ -45,7 +41,7 @@ export class UmbPropertyActionMenuElement extends UmbLitElement { } @state() - private _actions: Array> = []; + private _actions: Array> = []; render() { return this._actions.length > 0 @@ -54,13 +50,17 @@ export class UmbPropertyActionMenuElement extends UmbLitElement { id="popover-trigger" popovertarget="property-action-popover" look="secondary" - label="More" + label="Open actions menu" compact> - + ${repeat( + this._actions, + (action) => action.alias, + (action) => action.component, + )} ` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/default.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/default.action.kind.ts new file mode 100644 index 0000000000..fdc53a61a0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/default.action.kind.ts @@ -0,0 +1,18 @@ +import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: UmbBackofficeManifestKind = { + type: 'kind', + alias: 'Umb.Kind.PropertyAction.Default', + matchKind: 'default', + matchType: 'propertyAction', + manifest: { + type: 'propertyAction', + kind: 'default', + weight: 1000, + element: () => import('./property-action.element.js'), + meta: { + icon: 'icon-bug', + label: '(Missing label in manifest)', + }, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/manifests.ts new file mode 100644 index 0000000000..e8a0dcdedd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/manifests.ts @@ -0,0 +1,3 @@ +import { manifest as defaultKindManifest } from './default.action.kind.js'; + +export const manifests = [defaultKindManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/property-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/property-action.element.ts new file mode 100644 index 0000000000..e1c2a5fa63 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/default/property-action.element.ts @@ -0,0 +1,70 @@ +import type { UmbPropertyAction } from '../index.js'; +import { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event'; +import { html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { + ManifestPropertyActionDefaultKind, + MetaPropertyActionDefaultKind, +} from '@umbraco-cms/backoffice/extension-registry'; +import type { UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui'; + +@customElement('umb-property-action') +export class UmbPropertyActionElement< + MetaType extends MetaPropertyActionDefaultKind = MetaPropertyActionDefaultKind, + ApiType extends UmbPropertyAction = UmbPropertyAction, +> extends UmbLitElement { + #api?: ApiType; + + @state() + _href?: string; + + @property({ attribute: false }) + public manifest?: ManifestPropertyActionDefaultKind; + + @property({ attribute: false }) + public set api(api: ApiType | undefined) { + this.#api = api; + + // TODO: Fix so when we use a HREF it does not refresh the page? + this.#api?.getHref?.().then((href) => { + this._href = href; + // TODO: Do we need to update the component here? [NL] + }); + } + + async #onClickLabel(event: UUIMenuItemEvent) { + if (!this._href) { + event.stopPropagation(); + await this.#api?.execute(); + } + this.dispatchEvent(new UmbActionExecutedEvent()); + } + + // TODO: we need to stop the regular click event from bubbling up to the table so it doesn't select the row. + // This should probably be handled in the UUI Menu item component. so we don't dispatch a label-click event and click event at the same time. + #onClick(event: PointerEvent) { + event.stopPropagation(); + } + + render() { + return html` + + ${this.manifest?.meta.icon + ? html`` + : nothing} + + `; + } +} + +export default UmbPropertyActionElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-action': UmbPropertyActionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/manifests.ts new file mode 100644 index 0000000000..8bc9a56ab9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as defaultWorkspaceActionManifests } from './default/manifests.js'; + +export const manifests = [...defaultWorkspaceActionManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/property-action-base.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/property-action-base.controller.ts new file mode 100644 index 0000000000..bec96eb168 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/property-action-base.controller.ts @@ -0,0 +1,35 @@ +import type { UmbPropertyActionArgs } from './types.js'; +import type { UmbPropertyAction } from './property-action.interface.js'; +import { UmbActionBase } from '@umbraco-cms/backoffice/action'; + +/** + * Base class for an property action. + * @export + * @abstract + * @class UmbPropertyActionBase + * @extends {UmbActionBase} + * @implements {UmbPropertyAction} + */ +export abstract class UmbPropertyActionBase + extends UmbActionBase> + implements UmbPropertyAction +{ + /** + * By specifying the href, the action will act as a link. + * The `execute` method will not be called. + * @abstract + * @returns {string | undefined} + */ + public getHref(): Promise { + return Promise.resolve(undefined); + } + + /** + * By specifying the `execute` method, the action will act as a button. + * @abstract + * @returns {Promise} + */ + public execute(): Promise { + return Promise.resolve(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/property-action.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/property-action.interface.ts new file mode 100644 index 0000000000..49223d5c06 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/property-action.interface.ts @@ -0,0 +1,16 @@ +import type { UmbPropertyActionArgs } from './types.js'; +import type { UmbAction } from '@umbraco-cms/backoffice/action'; + +export interface UmbPropertyAction extends UmbAction> { + /** + * The href location, the action will act as a link. + * @returns {Promise} + */ + getHref(): Promise; + + /** + * The `execute` method, the action will act as a button. + * @returns {Promise} + */ + execute(): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/types.ts new file mode 100644 index 0000000000..d339df0cf7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/components/property-action/types.ts @@ -0,0 +1,3 @@ +export interface UmbPropertyActionArgs { + meta: MetaArgsType; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/index.ts index 4aac0d5288..8a8c2711ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/index.ts @@ -1 +1 @@ -export * from './shared/index.js'; +export * from './components/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/manifests.ts index 1d9096fd90..da0692574e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/manifests.ts @@ -1,22 +1,31 @@ -import type { ManifestPropertyAction } from '@umbraco-cms/backoffice/extension-registry'; +import { manifests as defaultManifests } from './components/property-action/manifests.js'; +import type { ManifestPropertyActions } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [ +export const propertyActionManifests: Array = [ { type: 'propertyAction', + kind: 'default', alias: 'Umb.PropertyAction.Copy', name: 'Copy Property Action', - js: () => import('./common/copy/property-action-copy.element.js'), + api: () => import('./common/copy/property-action-copy.controller.js'), + forPropertyEditorUis: ['Umb.PropertyEditorUi.TextBox'], meta: { - propertyEditors: ['Umb.PropertyEditorUi.TextBox'], + icon: 'icon-paste-in', + label: 'Copy', }, }, { type: 'propertyAction', + kind: 'default', alias: 'Umb.PropertyAction.Clear', name: 'Clear Property Action', - js: () => import('./common/clear/property-action-clear.element.js'), + api: () => import('./common/clear/property-action-clear.controller.js'), + forPropertyEditorUis: ['Umb.PropertyEditorUi.TextBox'], meta: { - propertyEditors: ['Umb.PropertyEditorUi.TextBox'], + icon: 'icon-trash', + label: 'Clear', }, }, ]; + +export const manifests = [...defaultManifests, ...propertyActionManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action/property-action.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action/property-action.interface.ts deleted file mode 100644 index 53cfe0823d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-action/shared/property-action/property-action.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface UmbPropertyAction extends HTMLElement { - value?: unknown; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.CheckboxList.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.CheckboxList.ts index 1999866902..36213ac286 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.CheckboxList.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.CheckboxList.ts @@ -3,8 +3,8 @@ import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/exten export const manifest: ManifestPropertyEditorSchema = { type: 'propertyEditorSchema', name: 'Checkbox List', - alias: 'Umbraco.CheckboxList', + alias: 'Umbraco.CheckBoxList', meta: { - defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.CheckboxList', + defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.CheckBoxList', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MediaPicker3.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MediaPicker.ts similarity index 94% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MediaPicker3.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MediaPicker.ts index e0e21070d0..12b32ace13 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MediaPicker3.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MediaPicker.ts @@ -2,17 +2,17 @@ import type { ManifestPropertyEditorSchema } from '@umbraco-cms/backoffice/exten export const manifest: ManifestPropertyEditorSchema = { type: 'propertyEditorSchema', - name: 'Media Picker 3', + name: 'Media Picker', alias: 'Umbraco.MediaPicker3', meta: { - defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.MediaPicker3', + defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.MediaPicker', settings: { properties: [ { alias: 'filter', label: 'Accepted types', description: 'Limit to specific types', - propertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.MediaTypePicker', }, { alias: 'multiple', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MultiNodeTreePicker.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MultiNodeTreePicker.ts index ea1a72b72a..9a6625b149 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MultiNodeTreePicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/Umbraco.MultiNodeTreePicker.ts @@ -5,7 +5,7 @@ export const manifest: ManifestPropertyEditorSchema = { name: 'Multi Node Tree Picker', alias: 'Umbraco.MultiNodeTreePicker', meta: { - defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.MultiNodeTreePicker', + defaultPropertyEditorUiAlias: 'Umb.PropertyEditorUi.TreePicker', settings: { properties: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/manifests.ts index 5661955fbd..2108dfc379 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/schemas/manifests.ts @@ -14,7 +14,7 @@ import { manifest as integer } from './Umbraco.Integer.js'; import { manifest as label } from './Umbraco.Label.js'; import { manifest as listView } from './Umbraco.ListView.js'; import { manifest as markdownEditor } from './Umbraco.MarkdownEditor.js'; -import { manifest as mediaPicker } from './Umbraco.MediaPicker3.js'; +import { manifest as mediaPicker } from './Umbraco.MediaPicker.js'; import { manifest as memberGroupPicker } from './Umbraco.MemberGroupPicker.js'; import { manifest as memberPicker } from './Umbraco.MemberPicker.js'; import { manifest as multiNodeTreePicker } from './Umbraco.MultiNodeTreePicker.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/checkbox-list/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/checkbox-list/manifests.ts index c8c836b805..a4fb438be5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/checkbox-list/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/checkbox-list/manifests.ts @@ -2,12 +2,12 @@ import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension export const manifest: ManifestPropertyEditorUi = { type: 'propertyEditorUi', - alias: 'Umb.PropertyEditorUi.CheckboxList', + alias: 'Umb.PropertyEditorUi.CheckBoxList', name: 'Checkbox List Property Editor UI', element: () => import('./property-editor-ui-checkbox-list.element.js'), meta: { label: 'Checkbox List', - propertyEditorSchemaAlias: 'Umbraco.CheckboxList', + propertyEditorSchemaAlias: 'Umbraco.CheckBoxList', icon: 'icon-bulleted-list', group: 'lists', settings: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/dropdown/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/dropdown/manifests.ts index c639aea701..0a54e6771c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/dropdown/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/dropdown/manifests.ts @@ -8,7 +8,7 @@ export const manifest: ManifestPropertyEditorUi = { meta: { label: 'Dropdown', propertyEditorSchemaAlias: 'Umbraco.DropDown.Flexible', - icon: 'icon-time', + icon: 'icon-list', group: 'pickers', }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/manifests.ts index caeaf5e4c8..56a9d0a8b1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/manifests.ts @@ -6,7 +6,6 @@ import { manifest as dropdown } from './dropdown/manifests.js'; import { manifest as eyeDropper } from './eye-dropper/manifests.js'; import { manifest as iconPicker } from './icon-picker/manifests.js'; import { manifest as label } from './label/manifests.js'; -import { manifest as markdownEditor } from './markdown-editor/manifests.js'; import { manifest as memberGroupPicker } from './member-group-picker/manifests.js'; import { manifest as memberPicker } from './member-picker/manifests.js'; import { manifest as multipleTextString } from './multiple-text-string/manifests.js'; @@ -36,7 +35,6 @@ export const manifests: Array = [ eyeDropper, iconPicker, label, - markdownEditor, memberGroupPicker, memberPicker, multipleTextString, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/member-group-picker/property-editor-ui-member-group-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/member-group-picker/property-editor-ui-member-group-picker.element.ts index f5fbc15d87..b2110e4df5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/member-group-picker/property-editor-ui-member-group-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/member-group-picker/property-editor-ui-member-group-picker.element.ts @@ -1,21 +1,69 @@ -import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbInputMemberGroupElement } from '@umbraco-cms/backoffice/member-group'; /** * @element umb-property-editor-ui-member-group-picker */ @customElement('umb-property-editor-ui-member-group-picker') export class UmbPropertyEditorUIMemberGroupPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property() - value = ''; + // private _value: Array = []; + + // @property({ type: Array }) + // public get value(): Array { + // return this._value; + // } + // public set value(value: Array) { + // this._value = Array.isArray(value) ? value : value ? [value] : []; + // } + + @property({ type: String }) + public value: string = ''; @property({ attribute: false }) - public config?: UmbPropertyEditorConfigCollection; + public set config(config: UmbPropertyEditorConfigCollection | undefined) { + const validationLimit = config?.find((x) => x.alias === 'validationLimit'); + + this._limitMin = (validationLimit?.value as any)?.min; + this._limitMax = (validationLimit?.value as any)?.max; + } + + @state() + _items: Array = []; + + @state() + private _limitMin?: number; + @state() + private _limitMax?: number; + + protected updated(_changedProperties: PropertyValueMap | Map): void { + super.updated(_changedProperties); + if (_changedProperties.has('value')) { + this._items = this.value ? this.value.split(',') : []; + } + } + + private _onChange(event: CustomEvent) { + //TODO: This is a hack, something changed so now we need to convert the array to a comma separated string to make it work with the server. + const toCommaSeparatedString = (event.target as UmbInputMemberGroupElement).selectedIds.join(','); + // this.value = (event.target as UmbInputMemberGroupElement).selectedIds; + this.value = toCommaSeparatedString; + this.dispatchEvent(new CustomEvent('property-value-change')); + } render() { - return html`
umb-property-editor-ui-member-group-picker
`; + return html` + Add + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/member-picker/property-editor-ui-member-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/member-picker/property-editor-ui-member-picker.element.ts index 1de57e69dd..175a7c1286 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/member-picker/property-editor-ui-member-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/member-picker/property-editor-ui-member-picker.element.ts @@ -1,21 +1,69 @@ -import { html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; +import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import type { UmbInputMemberElement } from '@umbraco-cms/backoffice/member'; /** * @element umb-property-editor-ui-member-picker */ @customElement('umb-property-editor-ui-member-picker') export class UmbPropertyEditorUIMemberPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property() - value = ''; + // private _value: Array = []; + + // @property({ type: Array }) + // public get value(): Array { + // return this._value; + // } + // public set value(value: Array) { + // this._value = Array.isArray(value) ? value : value ? [value] : []; + // } + + @property({ type: String }) + public value: string = ''; @property({ attribute: false }) - public config?: UmbPropertyEditorConfigCollection; + public set config(config: UmbPropertyEditorConfigCollection | undefined) { + const validationLimit = config?.find((x) => x.alias === 'validationLimit'); + + this._limitMin = (validationLimit?.value as any)?.min; + this._limitMax = (validationLimit?.value as any)?.max; + } + + @state() + _items: Array = []; + + @state() + private _limitMin?: number; + @state() + private _limitMax?: number; + + protected updated(_changedProperties: PropertyValueMap | Map): void { + super.updated(_changedProperties); + if (_changedProperties.has('value')) { + this._items = this.value ? this.value.split(',') : []; + } + } + + private _onChange(event: CustomEvent) { + //TODO: This is a hack, something changed so now we need to convert the array to a comma separated string to make it work with the server. + const toCommaSeparatedString = (event.target as UmbInputMemberElement).selectedIds.join(','); + // this.value = (event.target as UmbInputMemberElement).selectedIds; + this.value = toCommaSeparatedString; + this.dispatchEvent(new CustomEvent('property-value-change')); + } render() { - return html`
umb-property-editor-ui-member-picker
`; + return html` + Add + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/multiple-text-string/property-editor-ui-multiple-text-string.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/multiple-text-string/property-editor-ui-multiple-text-string.element.ts index 328ac08bb3..8fa9154671 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/multiple-text-string/property-editor-ui-multiple-text-string.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/multiple-text-string/property-editor-ui-multiple-text-string.element.ts @@ -12,7 +12,7 @@ import type { UmbInputMultipleTextStringElement } from '@umbraco-cms/backoffice/ @customElement('umb-property-editor-ui-multiple-text-string') export class UmbPropertyEditorUIMultipleTextStringElement extends UmbLitElement implements UmbPropertyEditorUiElement { @property({ type: Array }) - public value: Array = []; + value?: Array; @property({ attribute: false }) public set config(config: UmbPropertyEditorConfigCollection | undefined) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts index c01c6eae0c..d1952c415c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/tree-picker/property-editor-ui-tree-picker.element.ts @@ -56,6 +56,7 @@ export class UmbPropertyEditorUITreePickerElement extends UmbLitElement implemen this.min = Number(config?.getValueByAlias('minNumber')) || 0; this.max = Number(config?.getValueByAlias('maxNumber')) || 0; + this.type = config?.getValueByAlias('type') ?? 'content'; this.allowedContentTypeIds = config?.getValueByAlias('filter'); this.showOpenButton = config?.getValueByAlias('showOpenButton'); this.ignoreUserStartNodes = config?.getValueByAlias('ignoreUserStartNodes'); 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 ad103dd2ea..e6523ce269 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 @@ -29,6 +29,9 @@ export interface UmbPropertyDatasetContext extends UmbContext { // Property methods: propertyVariantId?: (propertyAlias: string) => Promise>; - propertyValueByAlias(propertyAlias: string): Promise>; + propertyValueByAlias( + propertyAlias: string, + ): Promise | undefined>; + // TODO: Append the andCulture method as well.. setPropertyValue(propertyAlias: string, value: unknown): void; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts index f978f65011..93f96b9c0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.context.ts @@ -1,8 +1,7 @@ import { UMB_PROPERTY_DATASET_CONTEXT } from '../property-dataset/index.js'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbArrayState, UmbBasicState, @@ -10,14 +9,12 @@ import { UmbDeepState, UmbStringState, } from '@umbraco-cms/backoffice/observable-api'; -import { UmbContextProviderController, UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import type { UmbPropertyEditorConfigProperty } from '@umbraco-cms/backoffice/property-editor'; import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; -export class UmbPropertyContext extends UmbControllerBase { - private _providerController: UmbContextProviderController; - +export class UmbPropertyContext extends UmbContextBase> { #alias = new UmbStringState(undefined); public readonly alias = this.#alias.asObservable(); #label = new UmbStringState(undefined); @@ -29,8 +26,8 @@ export class UmbPropertyContext extends UmbControllerBase { #configValues = new UmbArrayState([], (x) => x.alias); public readonly configValues = this.#configValues.asObservable(); - #configCollection = new UmbClassState(undefined); - public readonly config = this.#configCollection.asObservable(); + #config = new UmbClassState(undefined); + public readonly config = this.#config.asObservable(); private _editor = new UmbBasicState(undefined); public readonly editor = this._editor.asObservable(); @@ -51,7 +48,7 @@ export class UmbPropertyContext extends UmbControllerBase { #datasetContext?: typeof UMB_PROPERTY_DATASET_CONTEXT.TYPE; constructor(host: UmbControllerHost) { - super(host); + super(host, UMB_PROPERTY_CONTEXT); this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (variantContext) => { this.#datasetContext = variantContext; @@ -63,10 +60,8 @@ export class UmbPropertyContext extends UmbControllerBase { this._observeProperty(); }); - this._providerController = new UmbContextProviderController(host, UMB_PROPERTY_CONTEXT, this); - this.observe(this.configValues, (configValues) => { - this.#configCollection.setValue(configValues ? new UmbPropertyEditorConfigCollection(configValues) : undefined); + this.#config.setValue(configValues ? new UmbPropertyEditorConfigCollection(configValues) : undefined); }); this.observe(this.variantId, () => { @@ -74,29 +69,25 @@ export class UmbPropertyContext extends UmbControllerBase { }); } - private _observePropertyVariant?: UmbObserverController; - private _observePropertyValue?: UmbObserverController; private async _observeProperty(): Promise { const alias = this.#alias.getValue(); if (!this.#datasetContext || !alias) return; - const variantIdSubject = (await this.#datasetContext.propertyVariantId?.(alias)) ?? undefined; - this._observePropertyVariant?.destroy(); - if (variantIdSubject) { - this._observePropertyVariant = this.observe(variantIdSubject, (variantId) => { + this.observe( + await this.#datasetContext.propertyVariantId?.(alias), + (variantId) => { this.#variantId.setValue(variantId); - }); - } + }, + 'observeVariantId', + ); - // TODO: Verify if we need to optimize runtime by parsing the propertyVariantID, cause this method retrieves it again: - const subject = await this.#datasetContext.propertyValueByAlias(alias); - - this._observePropertyValue?.destroy(); - if (subject) { - this._observePropertyValue = this.observe(subject, (value) => { + this.observe( + await this.#datasetContext.propertyValueByAlias(alias), + (value) => { this.#value.setValue(value); - }); - } + }, + 'observeValue', + ); } private _generateVariantDifferenceString() { @@ -156,6 +147,9 @@ export class UmbPropertyContext extends UmbControllerBase { } public resetValue(): void { + this.setValue(undefined); // TODO: We should get the value from the server aka. the value from the persisted data. + } + public clearValue(): void { this.setValue(undefined); // TODO: We should get the default value from Property Editor maybe even later the DocumentType, as that would hold the default value for the property. } @@ -166,8 +160,7 @@ export class UmbPropertyContext extends UmbControllerBase { this.#description.destroy(); this.#configValues.destroy(); this.#value.destroy(); - this.#configCollection.destroy(); - this._providerController.destroy(); // This would also be handled by the controller host, but if someone wanted to replace/remove this context without the host being destroyed. Then we have clean up out selfs here. + this.#config.destroy(); this.#datasetContext = undefined; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.element.ts index ca2364b4d8..70b077011f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property/property.element.ts @@ -101,8 +101,9 @@ export class UmbPropertyElement extends UmbLitElement { @state() private _element?: ManifestPropertyEditorUi['ELEMENT_TYPE']; - @state() - private _value?: unknown; + // Not begin used currently [NL] + //@state() + //private _value?: unknown; @state() private _alias?: string; @@ -178,12 +179,12 @@ export class UmbPropertyElement extends UmbLitElement { this.#propertyContext.setEditor(this._element); if (this._element) { - // TODO: Could this be changed to change event? (or additionally support change?) + // TODO: Could this be changed to change event? (or additionally support the change event? [NL]) this._element.addEventListener('property-value-change', this._onPropertyEditorChange as any as EventListener); // No need for a controller alias, as the clean is handled via the observer prop: this.#valueObserver = this.observe(this.#propertyContext.value, (value) => { - this._value = value; + //this._value = value;// This was not used currently [NL] if (this._element) { this._element.value = value; } @@ -220,8 +221,7 @@ export class UmbPropertyElement extends UmbLitElement { ? html`` + .propertyEditorUiAlias=${this._propertyEditorUiAlias}>` : ''}`; } @@ -238,6 +238,7 @@ export class UmbPropertyElement extends UmbLitElement { #action-menu { opacity: 0; + transition: opacity 90ms; } #layout:focus-within #action-menu, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/copy-data-source.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/copy-data-source.interface.ts deleted file mode 100644 index 4ac1317a33..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/copy-data-source.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; - -export interface UmbCopyDataSource { - copy(unique: string, targetUnique: string | null): Promise>; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/copy-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/copy-repository.interface.ts deleted file mode 100644 index 201e416e5e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/copy-repository.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { UmbRepositoryResponse } from '../types.js'; - -export interface UmbCopyRepository { - copy(unique: string, targetUnique: string): Promise>; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/index.ts deleted file mode 100644 index e5630ce0f5..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/copy/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { UmbCopyDataSource } from './copy-data-source.interface.js'; -export type { UmbCopyRepository } from './copy-repository.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts index c7669d284b..62613a2689 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts @@ -149,4 +149,10 @@ export abstract class UmbDetailRepositoryBase>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/duplicate/duplicate-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/duplicate/duplicate-repository.interface.ts new file mode 100644 index 0000000000..0b7ff3aae0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/duplicate/duplicate-repository.interface.ts @@ -0,0 +1,5 @@ +import type { UmbRepositoryResponse } from '../types.js'; + +export interface UmbDuplicateRepository { + duplicate(unique: string, targetUnique: string): Promise>; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/duplicate/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/duplicate/index.ts new file mode 100644 index 0000000000..4c9af6a56c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/duplicate/index.ts @@ -0,0 +1,2 @@ +export type { UmbDuplicateDataSource } from './duplicate-data-source.interface.js'; +export type { UmbDuplicateRepository } from './duplicate-repository.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/index.ts index 4fc2411629..0002dac5a9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/index.ts @@ -6,5 +6,5 @@ export * from './detail/index.js'; export type { UmbDataSourceResponse, UmbDataSourceErrorResponse } from './data-source-response.interface.js'; export type { UmbMoveDataSource, UmbMoveRepository } from './move/index.js'; -export type { UmbCopyDataSource, UmbCopyRepository } from './copy/index.js'; +export type { UmbDuplicateDataSource, UmbDuplicateRepository } from './duplicate/index.js'; export type { UmbPagedModel } from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts index 7b1e6dcad5..b7dc4e9992 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts @@ -62,6 +62,11 @@ export class UmbResourceController extends UmbControllerBase { // Cancelled - do nothing return {}; } else { + console.group('ApiError caught in UmbResourceController'); + console.error('Request failed', error.request); + console.error('ProblemDetails', error.body); + console.error('Error', error); + // ApiError - body could hold a ProblemDetails from the server if (typeof error.body !== 'undefined' && !!error.body) { try { @@ -78,7 +83,7 @@ export class UmbResourceController extends UmbControllerBase { console.log('Unauthorized'); // TODO: Do not remove the token here but instead let whatever is listening to the event decide what to do - localStorage.removeItem('tokenResponse'); + localStorage.removeItem('umb:userAuthTokenResponse'); // TODO: Show a modal dialog to login either by bubbling an event to UmbAppElement or by showing a modal directly this.#notificationContext?.peek('warning', { @@ -88,6 +93,32 @@ export class UmbResourceController extends UmbControllerBase { }, }); break; + case 500: + // Server Error + + if (this.#notificationContext) { + let headline = error.body?.title ?? error.name ?? 'Server Error'; + let message = 'A fatal server error occurred. If this continues, please reach out to your administrator.'; + + // Special handling for ObjectCacheAppCache corruption errors, which we are investigating + if ( + error.body?.detail?.includes('ObjectCacheAppCache') || + error.body?.detail?.includes('Umbraco.Cms.Infrastructure.Scoping.Scope.DisposeLastScope()') + ) { + headline = 'Please restart the server'; + message = + 'The Umbraco object cache is corrupt, but your action may still have been executed. Please restart the server to reset the cache. This is a work in progress.'; + } + + this.#notificationContext.peek('danger', { + data: { + headline, + message, + }, + ...options, + }); + } + break; default: // Other errors if (this.#notificationContext) { @@ -98,12 +129,10 @@ export class UmbResourceController extends UmbControllerBase { }, ...options, }); - } else { - console.group('UmbResourceController'); - console.error(error); - console.groupEnd(); } } + + console.groupEnd(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/index.ts index 19f19d7935..bd007f31f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/section/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/index.ts @@ -5,3 +5,5 @@ export * from './section-sidebar-menu/index.js'; export * from './section-sidebar-menu-with-entity-actions/index.js'; export * from './section-default.element.js'; export * from './section.context.js'; +export * from './input-section/index.js'; +export * from './section-picker/section-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/input-section.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/input-section.element.ts similarity index 92% rename from src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/input-section.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/input-section.element.ts index 1e1743c1ad..99ac8aa0d0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/input-section.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/input-section.element.ts @@ -1,7 +1,7 @@ -import { UmbInputListBaseElement } from '../input-list-base/input-list-base.js'; +import { UmbInputListBaseElement } from '../../components/input-list-base/input-list-base.js'; +import { UMB_SECTION_PICKER_MODAL } from '../section-picker/section-picker-modal.token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { UMB_SECTION_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/input-section.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/input-section.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/input-section.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/input-section.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/input-section.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/input-section.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/components/input-section/input-section.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/section/input-section/input-section.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/section/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/manifests.ts new file mode 100644 index 0000000000..90eff35d58 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/manifests.ts @@ -0,0 +1,8 @@ +export const manifests = [ + { + type: 'modal', + alias: 'Umb.Modal.SectionPicker', + name: 'Section Picker Modal', + js: () => import('./section-picker/section-picker-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/section-picker/section-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-picker/section-picker-modal.element.ts similarity index 97% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/common/section-picker/section-picker-modal.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/section/section-picker/section-picker-modal.element.ts index 3adb11b866..5a4061c4e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/section-picker/section-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-picker/section-picker-modal.element.ts @@ -1,8 +1,8 @@ +import type { UmbSectionPickerModalData, UmbSectionPickerModalValue } from './section-picker-modal.token.js'; import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import type { UmbSectionPickerModalData, UmbSectionPickerModalValue } from '@umbraco-cms/backoffice/modal'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; @customElement('umb-section-picker-modal') diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/section-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-picker/section-picker-modal.token.ts similarity index 85% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/token/section-picker-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/section/section-picker/section-picker-modal.token.ts index d90f5ac181..56ce67e642 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/section-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-picker/section-picker-modal.token.ts @@ -1,4 +1,4 @@ -import { UmbModalToken } from './modal-token.js'; +import { UmbModalToken } from '../../modal/token/modal-token.js'; export interface UmbSectionPickerModalData { multiple: boolean; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/section-picker/section-picker.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/section/section-picker/section-picker.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/common/section-picker/section-picker.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/section/section-picker/section-picker.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts index 1b5a8fd094..b17249cc83 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/sorter/sorter.controller.ts @@ -57,7 +57,7 @@ function destroyPreventEvent(element: Element) { //element.removeAttribute('draggable'); } -export type resolveVerticalDirectionArgs = { +export type resolvePlacementArgs = { containerElement: Element; containerRect: DOMRect; item: T; @@ -134,9 +134,21 @@ type INTERNAL_UmbSorterConfig = { onRequestMove?: (argument: { item: T }) => boolean; /** * This callback is executed when an item is hovered within this container. - * The callback should return true if the item should be placed after based on a vertical logic. Other wise false for horizontal. True is default. + * The callback should return true if the item should be placed after the hovered item, or false if it should be placed before the hovered item. + * In this way the callback can control the placement of the item. + * If it returns null the placement will be prevented. + * @example + * This is equivalent to the default behavior: + * ```ts + * resolvePlacement: (argument) => { + * if(argument.pointerY > argument.relatedRect.top + argument.relatedRect.height * 0.5) { + * return true; // Place after + * } else { + * return false; // Place before + * } + * } */ - resolveVerticalDirection?: (argument: resolveVerticalDirectionArgs) => boolean | null; + resolvePlacement?: (argument: resolvePlacementArgs) => boolean | null; /** * This callback is executed when an item is moved within this container. */ @@ -186,6 +198,7 @@ export class UmbSorterController; #observer; @@ -239,12 +252,16 @@ export class UmbSorterController): void { @@ -263,11 +280,13 @@ export class UmbSorterController elementUnique === this.#config.getUniqueOfModel(entry)); } 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 ddaa96d7f0..98bf9e6e89 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 @@ -13,7 +13,7 @@ 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 type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @@ -24,6 +24,11 @@ export class UmbDefaultTreeContext #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(); + public selectableFilter?: (item: TreeItemType) => boolean = () => true; public filter?: (item: TreeItemType) => boolean = () => true; public readonly selection = new UmbSelectionManager(this._host); @@ -52,7 +57,26 @@ export class UmbDefaultTreeContext // listen for page changes on the pagination manager this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange); - this.requestTreeRoot(); + + /* TODO: revisit. This is a temp solution to notify the parent it needs to reload its children + there might be a better way to do this through a tree item parent context. + It does not look like there is a way to have a "dynamic" parent context that will stop when a + specific parent is reached (a tree item unique that matches the parentUnique of this item) */ + const hostElement = this.getHostElement(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @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; + if (event.detail.unique === unique) { + event.stopPropagation(); + this.loadRootItems(); + } + }); + + this.loadTreeRoot(); } // TODO: find a generic way to do this @@ -91,35 +115,29 @@ export class UmbDefaultTreeContext return this.#repository; } - public async requestTreeRoot() { + public async loadTreeRoot() { await this.#init; const { data } = await this.#repository!.requestTreeRoot(); if (data) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore + // @ts-ignore this.#treeRoot.setValue(data); } } - public async requestRootItems() { + public async loadRootItems() { await this.#init; - const { data, error, asObservable } = await this.#repository!.requestRootTreeItems({ + const { data } = await this.#repository!.requestRootTreeItems({ skip: this.#paging.skip, take: this.#paging.take, }); if (data) { + this.#rootItems.setValue(data.items); this.pagination.setTotalItems(data.total); } - - return { data, error, asObservable }; - } - - public async rootItems() { - await this.#init; - return this.#repository!.rootTreeItems(); } #consumeContexts() { @@ -139,7 +157,7 @@ export class UmbDefaultTreeContext #onPageChange = (event: UmbChangeEvent) => { const target = event.target as UmbPaginationManager; this.#paging.skip = target.getSkip(); - this.requestRootItems(); + this.loadRootItems(); }; #observeRepository(repositoryAlias?: string) { @@ -165,7 +183,7 @@ export class UmbDefaultTreeContext // @ts-ignore if (event.getUnique() !== treeRoot.unique) return; if (event.getEntityType() !== treeRoot.entityType) return; - this.requestRootItems(); + this.loadRootItems(); }; destroy(): void { 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 07d1f88f76..18bb19b036 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 @@ -26,7 +26,7 @@ export class UmbDefaultTreeElement extends UmbLitElement { filter: (item: UmbTreeItemModelBase) => boolean = () => true; @state() - private _items: UmbTreeItemModelBase[] = []; + private _rootItems: UmbTreeItemModelBase[] = []; @state() private _treeRoot?: UmbTreeItemModelBase; @@ -47,11 +47,10 @@ export class UmbDefaultTreeElement extends UmbLitElement { // TODO: Notice this can be retrieve via a api property. [NL] this.consumeContext(UMB_DEFAULT_TREE_CONTEXT, (instance) => { this.#treeContext = instance; - + this.observe(this.#treeContext.treeRoot, (treeRoot) => (this._treeRoot = treeRoot)); + this.observe(this.#treeContext.rootItems, (rootItems) => (this._rootItems = rootItems)); this.observe(this.#treeContext.pagination.currentPage, (value) => (this._currentPage = value)); this.observe(this.#treeContext.pagination.totalPages, (value) => (this._totalPages = value)); - - this.#observeTreeRoot(); }).asPromise(), ]); } @@ -70,7 +69,8 @@ export class UmbDefaultTreeElement extends UmbLitElement { if (_changedProperties.has('hideTreeRoot')) { if (this.hideTreeRoot === true) { - this.#observeRootItems(); + await this.#init; + this.#treeContext!.loadRootItems(); } } @@ -83,36 +83,6 @@ export class UmbDefaultTreeElement extends UmbLitElement { } } - #observeTreeRoot() { - if (!this.#treeContext) return; - this.observe( - this.#treeContext.treeRoot, - (treeRoot) => { - this._treeRoot = treeRoot; - }, - 'umbTreeRootObserver', - ); - } - - async #observeRootItems() { - await this.#init; - if (!this.#treeContext?.requestRootItems) throw new Error('Tree does not support root items'); - - const { asObservable } = await this.#treeContext.requestRootItems(); - - if (asObservable) { - this.observe( - asObservable(), - (rootItems) => { - const oldValue = this._items; - this._items = rootItems; - this.requestUpdate('_items', oldValue); - }, - 'umbRootItemsObserver', - ); - } - } - getSelection() { return this.#treeContext?.selection.getSelection(); } @@ -129,15 +99,19 @@ export class UmbDefaultTreeElement extends UmbLitElement { } #renderRootItems() { - if (this._items?.length === 0) return nothing; - return html` - ${repeat( - this._items, - (item, index) => item.name + '___' + index, - (item) => html``, - )} - ${this.#renderPaging()} - `; + // only shot the root items directly if the tree root is hidden + if (this.hideTreeRoot === true) { + return html` + ${repeat( + this._rootItems, + (item, index) => item.name + '___' + index, + (item) => html``, + )} + ${this.#renderPaging()} + `; + } else { + return nothing; + } } #onLoadMoreClick = (event: any) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.kind.ts index f1402adccf..4a03faa74f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.kind.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.kind.ts @@ -1,5 +1,6 @@ import { UmbCreateFolderEntityAction } from './create-folder.action.js'; import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST } from '@umbraco-cms/backoffice/entity-action'; export const manifest: UmbBackofficeManifestKind = { type: 'kind', @@ -7,6 +8,7 @@ export const manifest: UmbBackofficeManifestKind = { matchKind: 'folderCreate', matchType: 'entityAction', manifest: { + ...UMB_ENTITY_ACTION_DEFAULT_KIND_MANIFEST.manifest, type: 'entityAction', kind: 'folderCreate', api: UmbCreateFolderEntityAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.ts index 984626729a..a17235798b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/folder/entity-action/create-folder/create-folder.action.ts @@ -1,7 +1,11 @@ +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import type { MetaEntityActionFolderKind } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { UMB_FOLDER_CREATE_MODAL } from '@umbraco-cms/backoffice/tree'; +import { + UMB_FOLDER_CREATE_MODAL, + UmbReloadTreeItemChildrenRequestEntityActionEvent, +} from '@umbraco-cms/backoffice/tree'; export class UmbCreateFolderEntityAction extends UmbEntityActionBase { async execute() { @@ -17,5 +21,13 @@ export class UmbCreateFolderEntityAction extends UmbEntityActionBase = ( - item: UmbFolderModel, - parentUnique: string | null, -) => FolderTreeItemType; - -export abstract class UmbFolderRepositoryBase - extends UmbRepositoryBase - implements UmbFolderRepository -{ +export abstract class UmbFolderRepositoryBase extends UmbRepositoryBase implements UmbFolderRepository { protected _init: Promise; - protected _treeStore?: UmbTreeStore; #folderDataSource: UmbFolderDataSource; - #folderToTreeItemMapper: UmbFolderToTreeItemMapper; #notificationContext?: UmbNotificationContext; - constructor( - host: UmbControllerHost, - folderDataSource: UmbFolderDataSourceConstructor, - treeStoreContextAlias: string | UmbContextToken, - folderToTreeItemMapper: UmbFolderToTreeItemMapper, - ) { + constructor(host: UmbControllerHost, folderDataSource: UmbFolderDataSourceConstructor) { super(host); this.#folderDataSource = new folderDataSource(this); - this.#folderToTreeItemMapper = folderToTreeItemMapper; this._init = Promise.all([ - this.consumeContext(treeStoreContextAlias, (instance) => { - this._treeStore = instance; - }).asPromise(), - this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { this.#notificationContext = instance; }).asPromise(), @@ -73,9 +51,6 @@ export abstract class UmbFolderRepositoryBase import('./reload-tree-item-children.action.js'), diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/reload-tree-item-children/reload-tree-item-children.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/reload-tree-item-children/reload-tree-item-children.action.ts index 73eaf64dcd..16b21e69e3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/reload-tree-item-children/reload-tree-item-children.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/reload-tree-item-children/reload-tree-item-children.action.ts @@ -1,4 +1,4 @@ -import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from './reload-tree-item-children-request.event.js'; +import { UmbRequestReloadTreeItemChildrenEvent } from './reload-tree-item-children-request.event.js'; import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; @@ -14,7 +14,7 @@ export class UmbReloadTreeItemChildrenEntityAction extends UmbEntityActionBase = ( x: TreeItemType, @@ -33,6 +33,11 @@ export abstract class UmbTreeItemContextBase(undefined); treeItem = this.#treeItem.asObservable(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + #childItems = new UmbArrayState([], (x) => x.unique); + childItems = this.#childItems.asObservable(); + #hasChildren = new UmbBooleanState(false); hasChildren = this.#hasChildren.asObservable(); #hasChildrenInitValueFlag = false; @@ -78,6 +83,24 @@ export abstract class UmbTreeItemContextBase { + const treeItem = this.getTreeItem(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const unique = treeItem?.unique; + if (event.detail.unique === unique) { + event.stopPropagation(); + this.loadChildren(); + } + }); } /** @@ -129,25 +152,26 @@ export abstract class UmbTreeItemContextBase { this.#sectionContext = instance; this.#observeSectionPath(); @@ -187,15 +211,27 @@ export abstract class UmbTreeItemContextBase { - this.#actionEventContext = instance; - this.#actionEventContext.removeEventListener( + this.#actionEventContext?.removeEventListener( UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE, this.#onReloadRequest as EventListener, ); + + this.#actionEventContext?.removeEventListener( + UmbRequestReloadStructureForEntityEvent.TYPE, + this.#onReloadStructureRequest as unknown as EventListener, + ); + + this.#actionEventContext = instance; + this.#actionEventContext.addEventListener( UmbReloadTreeItemChildrenRequestEntityActionEvent.TYPE, this.#onReloadRequest as EventListener, ); + + this.#actionEventContext.addEventListener( + UmbRequestReloadStructureForEntityEvent.TYPE, + this.#onReloadStructureRequest as unknown as EventListener, + ); }); } @@ -281,17 +317,36 @@ export abstract class UmbTreeItemContextBase { - // Only handle children request here. Root request is handled by the tree context + if (event.getUnique() !== this.unique) return; + if (event.getEntityType() !== this.entityType) return; + this.loadChildren(); + }; + + #onReloadStructureRequest = async (event: UmbRequestReloadStructureForEntityEvent) => { if (!this.unique) return; if (event.getUnique() !== this.unique) return; if (event.getEntityType() !== this.entityType) return; - this.requestChildren(); + + /* TODO: revisit. This is a temp solution to notify the parent it needs to reload its children + there might be a better way to do this through a tree item parent context. + It does not look like there is a way to have a "dynamic" parent context that will stop when a + specific parent is reached (a tree item unique that matches the parentUnique of this item) */ + const treeItem = this.getTreeItem(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const parentUnique = treeItem?.parentUnique; + const customEvent = new CustomEvent('temp-reload-tree-item-parent', { + detail: { unique: parentUnique }, + bubbles: true, + composed: true, + }); + this.getHostElement().dispatchEvent(customEvent); }; #onPageChange = (event: UmbChangeEvent) => { const target = event.target as UmbPaginationManager; this.#paging.skip = target.getSkip(); - this.requestChildren(); + this.loadChildren(); }; // TODO: use router context 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 5e64919b39..37a2d713f2 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 @@ -60,6 +60,7 @@ export abstract class UmbTreeItemElementBase (this._item = value)); + this.observe(this.#treeItemContext.childItems, (value) => (this._childItems = value)); this.observe(this.#treeItemContext.hasChildren, (value) => (this._hasChildren = value)); this.observe(this.#treeItemContext.isLoading, (value) => (this._isLoading = value)); this.observe(this.#treeItemContext.isSelectableContext, (value) => (this._isSelectableContext = value)); @@ -89,21 +90,7 @@ export abstract class UmbTreeItemElementBase 0) return; - this.#observeChildren(); - } - - async #observeChildren() { - if (!this.#treeItemContext?.requestChildren) return; - - const { asObservable } = await this.#treeItemContext.requestChildren(); - if (!asObservable) return; - - this.observe(asObservable(), (childItems) => { - const oldValue = this._childItems; - this._childItems = childItems; - this.requestUpdate('_childItems', oldValue); - }); + this.#treeItemContext?.loadChildren(); } #onLoadMoreClick = (event: any) => { @@ -176,7 +163,7 @@ export abstract class UmbTreeItemElementBase - ` + ` : ''; } @@ -187,7 +174,7 @@ export abstract class UmbTreeItemElementBase item.name + '___' + index, (item) => html``, - ) + ) : ''} `; } 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 8ae64f95df..a95973075f 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,14 +1,13 @@ import type { UmbTreeItemModelBase } from '../types.js'; import type { UmbPaginationManager } from '../../utils/pagination-manager/pagination.manager.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; -import type { ProblemDetails } from '@umbraco-cms/backoffice/external/backend-api'; -import type { UmbPagedModel } from '@umbraco-cms/backoffice/repository'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; export interface UmbTreeItemContext extends UmbApi { unique?: string | null; entityType?: string; treeItem: Observable; + childItems: Observable; hasChildren: Observable; isLoading: Observable; isSelectableContext: Observable; @@ -19,11 +18,7 @@ export interface UmbTreeItemContext e path: Observable; pagination: UmbPaginationManager; setTreeItem(treeItem: TreeItemType | undefined): void; - requestChildren(): Promise<{ - data?: UmbPagedModel | undefined; - error?: ProblemDetails | undefined; - asObservable?: () => Observable; - }>; + loadChildren(): void; toggleContextMenu(): void; select(): void; deselect(): void; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/selection-manager/selection.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/selection-manager/selection.manager.ts index 78d0311780..099c3d744e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/selection-manager/selection.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/selection-manager/selection.manager.ts @@ -81,7 +81,9 @@ export class UmbSelectionManager 1) { - this.setSelection([this.getSelection()[0]]); + const first = this.getSelection()[0]; + this.clearSelection(); + this.select(first); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/index.ts index be0ca6343a..f851e0b3cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/index.ts @@ -1,5 +1,7 @@ -export * from './workspace-entity-action-menu/index.js'; +export * from './workspace-action-menu-item/index.js'; +export * from './workspace-action-menu/index.js'; export * from './workspace-action/index.js'; export * from './workspace-editor/index.js'; +export * from './workspace-entity-action-menu/index.js'; export * from './workspace-footer/index.js'; export * from './workspace-split-view/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/manifests.ts new file mode 100644 index 0000000000..42fa69d567 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/manifests.ts @@ -0,0 +1,4 @@ +import { manifests as workspaceActionManifests } from './workspace-action/manifests.js'; +import { manifests as workspaceActionMenuItemManifests } from './workspace-action-menu-item/manifests.js'; + +export const manifests = [...workspaceActionManifests, ...workspaceActionMenuItemManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/default.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/default.action.kind.ts new file mode 100644 index 0000000000..28349aa484 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/default.action.kind.ts @@ -0,0 +1,18 @@ +import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: UmbBackofficeManifestKind = { + type: 'kind', + alias: 'Umb.Kind.WorkspaceActionMenuItem.Default', + matchKind: 'default', + matchType: 'workspaceActionMenuItem', + manifest: { + type: 'workspaceActionMenuItem', + kind: 'default', + weight: 1000, + element: () => import('./workspace-action-menu-item.element.js'), + meta: { + icon: '', + label: '(Missing label in manifest)', + }, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/manifests.ts new file mode 100644 index 0000000000..e8a0dcdedd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/manifests.ts @@ -0,0 +1,3 @@ +import { manifest as defaultKindManifest } from './default.action.kind.js'; + +export const manifests = [defaultKindManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/workspace-action-menu-item.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/workspace-action-menu-item.element.ts new file mode 100644 index 0000000000..02b14df507 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/default/workspace-action-menu-item.element.ts @@ -0,0 +1,70 @@ +import type { UmbWorkspaceActionMenuItem } from '../index.js'; +import { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event'; +import { html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { + ManifestWorkspaceActionMenuItemDefaultKind, + MetaWorkspaceActionMenuItemDefaultKind, +} from '@umbraco-cms/backoffice/extension-registry'; +import type { UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui'; + +@customElement('umb-workspace-action-menu-item') +export class UmbWorkspaceActionMenuItemElement< + MetaType extends MetaWorkspaceActionMenuItemDefaultKind = MetaWorkspaceActionMenuItemDefaultKind, + ApiType extends UmbWorkspaceActionMenuItem = UmbWorkspaceActionMenuItem, +> extends UmbLitElement { + #api?: ApiType; + + @state() + _href?: string; + + @property({ attribute: false }) + public manifest?: ManifestWorkspaceActionMenuItemDefaultKind; + + @property({ attribute: false }) + public set api(api: ApiType | undefined) { + this.#api = api; + + // TODO: Fix so when we use a HREF it does not refresh the page? + this.#api?.getHref?.().then((href) => { + this._href = href; + // TODO: Do we need to update the component here? [NL] + }); + } + + async #onClickLabel(event: UUIMenuItemEvent) { + if (!this._href) { + event.stopPropagation(); + await this.#api?.execute(); + } + this.dispatchEvent(new UmbActionExecutedEvent()); + } + + // TODO: we need to stop the regular click event from bubbling up to the table so it doesn't select the row. + // This should probably be handled in the UUI Menu item component. so we don't dispatch a label-click event and click event at the same time. + #onClick(event: PointerEvent) { + event.stopPropagation(); + } + + render() { + return html` + + ${this.manifest?.meta.icon + ? html`` + : nothing} + + `; + } +} + +export default UmbWorkspaceActionMenuItemElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-action-menu-item': UmbWorkspaceActionMenuItemElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/index.ts new file mode 100644 index 0000000000..5be3d13fc0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/index.ts @@ -0,0 +1,3 @@ +export * from './workspace-action-menu-item-base.controller.js'; +export type * from './types.js'; +export type * from './workspace-action-menu-item.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/manifests.ts new file mode 100644 index 0000000000..8bc9a56ab9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as defaultWorkspaceActionManifests } from './default/manifests.js'; + +export const manifests = [...defaultWorkspaceActionManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/types.ts new file mode 100644 index 0000000000..7f3c638c81 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/types.ts @@ -0,0 +1,3 @@ +export interface UmbWorkspaceActionMenuItemArgs { + meta: MetaArgsType; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/workspace-action-menu-item-base.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/workspace-action-menu-item-base.controller.ts new file mode 100644 index 0000000000..97b0c44fd5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/workspace-action-menu-item-base.controller.ts @@ -0,0 +1,36 @@ +import type { UmbWorkspaceActionMenuItemArgs } from './types.js'; +import type { UmbWorkspaceActionMenuItem } from './workspace-action-menu-item.interface.js'; +import { UmbActionBase } from '@umbraco-cms/backoffice/action'; + +/** + * Base class for an workspace action. + * @export + * @abstract + * @class UmbWorkspaceActionMenuItemBase + * @extends {UmbActionBase} + * @implements {UmbWorkspaceActionMenuItem} + * @template RepositoryType + */ +export abstract class UmbWorkspaceActionMenuItemBase + extends UmbActionBase> + implements UmbWorkspaceActionMenuItem +{ + /** + * By specifying the href, the action will act as a link. + * The `execute` method will not be called. + * @abstract + * @returns {string | undefined} + */ + public getHref(): Promise { + return Promise.resolve(undefined); + } + + /** + * By specifying the `execute` method, the action will act as a button. + * @abstract + * @returns {Promise} + */ + public execute(): Promise { + return Promise.resolve(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/workspace-action-menu-item.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/workspace-action-menu-item.interface.ts new file mode 100644 index 0000000000..40b1ace84e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu-item/workspace-action-menu-item.interface.ts @@ -0,0 +1,17 @@ +import type { UmbWorkspaceActionMenuItemArgs } from './types.js'; +import type { UmbAction } from '@umbraco-cms/backoffice/action'; + +export interface UmbWorkspaceActionMenuItem + extends UmbAction> { + /** + * The href location, the action will act as a link. + * @returns {Promise} + */ + getHref(): Promise; + + /** + * The `execute` method, the action will act as a button. + * @returns {Promise} + */ + execute(): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action-menu/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action-menu/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/workspace-action-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/workspace-action-menu.element.ts new file mode 100644 index 0000000000..9f7d6da016 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action-menu/workspace-action-menu.element.ts @@ -0,0 +1,155 @@ +import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, property, state, nothing, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { + umbExtensionsRegistry, + type ManifestWorkspaceActionMenuItem, +} from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UUIInterfaceColor, UUIInterfaceLook } from '@umbraco-cms/backoffice/external/uui'; +import { + type UmbExtensionElementAndApiInitializer, + UmbExtensionsElementAndApiInitializer, +} from '@umbraco-cms/backoffice/extension-api'; + +function ExtensionApiArgsMethod(manifest: ManifestWorkspaceActionMenuItem) { + return [{ meta: manifest.meta }]; +} +@customElement('umb-workspace-action-menu') +export class UmbWorkspaceActionMenuElement extends UmbLitElement { + #extensionsController?: UmbExtensionsElementAndApiInitializer< + ManifestWorkspaceActionMenuItem, + 'workspaceActionMenuItem', + ManifestWorkspaceActionMenuItem + >; + + /** + * The workspace actions to filter the available actions by. + * @example ['Umb.WorkspaceAction.Document.Save', 'Umb.WorkspaceAction.Document.SaveAndPublishNew'] + */ + @property({ attribute: false }) + public set forWorkspaceActions(value: Array) { + if (value === this._forWorkspaceActions) return; + this._forWorkspaceActions = value; + this._filter = (action) => { + return Array.isArray(action.forWorkspaceActions) + ? action.forWorkspaceActions.some((alias) => this.forWorkspaceActions.includes(alias)) + : this.forWorkspaceActions.includes(action.forWorkspaceActions); + }; + this.#observeExtensions(); + } + public get forWorkspaceActions(): Array { + return this._forWorkspaceActions; + } + private _forWorkspaceActions: Array = []; + + @state() + _filter?: (action: ManifestWorkspaceActionMenuItem) => boolean; + + @property() + look: UUIInterfaceLook = 'secondary'; + + @property() + color: UUIInterfaceColor = 'default'; + + @state() + _items: Array> = []; + + @state() + _popoverOpen = false; + + #observeExtensions(): void { + this.#extensionsController?.destroy(); + if (this._filter) { + this.#extensionsController = new UmbExtensionsElementAndApiInitializer< + ManifestWorkspaceActionMenuItem, + 'workspaceActionMenuItem', + ManifestWorkspaceActionMenuItem + >( + this, + umbExtensionsRegistry, + 'workspaceActionMenuItem', + ExtensionApiArgsMethod, + this._filter, + (extensionControllers) => { + this._items = extensionControllers; + }, + undefined, // We can leave the alias to undefined, as we destroy this our selfs. + ); + //this.#extensionsController.elementProperties = this.#elProps; + } + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + #onPopoverToggle(event: ToggleEvent) { + this._popoverOpen = event.newState === 'open'; + } + + render() { + return this._items && this._items.length > 0 + ? html` + + + + + + + ${this._items.length > 0 + ? repeat( + this._items, + (ext) => ext.alias, + (ext) => ext.component, + ) + : ''} + + + + ` + : nothing; + } + + static styles: CSSResultGroup = [ + UmbTextStyles, + css` + :host { + --uui-menu-item-flat-structure: 1; + } + + #expand-symbol { + /* TODO: remove this hack and use a proper UUI symbol for this */ + transform: rotate(-90deg); + } + + #expand-symbol[open] { + transform: rotate(0deg); + } + + #workspace-action-popover { + min-width: 200px; + } + + #popover-trigger { + --uui-button-padding-top-factor: 0.5; + --uui-button-padding-bottom-factor: 0.1; + --uui-button-border-radius: 0; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-workspace-action-menu': UmbWorkspaceActionMenuElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/index.ts index 54f738af58..b21a44dbd5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/index.ts @@ -1,2 +1 @@ export * from './save/index.js'; -export * from './workspace-action-base.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts index 86ffff2ef9..fd4fe5107b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/save/save.action.ts @@ -1,22 +1,17 @@ import type { UmbSaveableWorkspaceContextInterface } from '../../../../workspace-context/saveable-workspace-context.interface.js'; -import { UmbWorkspaceActionBase } from '../workspace-action-base.js'; +import { UmbWorkspaceActionBase } from '../../workspace-action-base.controller.js'; +import { UMB_SAVEABLE_WORKSPACE_CONTEXT, type UmbWorkspaceActionArgs } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -// TODO: add interface for repo/partial repo/save-repo export class UmbSaveWorkspaceAction extends UmbWorkspaceActionBase { - constructor(host: UmbControllerHost) { - super(host); + constructor(host: UmbControllerHost, args: UmbWorkspaceActionArgs) { + super(host, args); // TODO: Could we make change label depending on the state? - // So its called 'Create' when the workspace is new and 'Save' when the workspace is not new. } - /* TODO: we need a solution for all actions to notify the system that is has been executed. - There might be cases where we need to do something after the action has been executed. - Ex. "reset" a workspace after a save action has been executed. - */ async execute() { - if (!this.workspaceContext) return; - this.workspaceContext.save(); + const workspaceContext = await this.getContext(UMB_SAVEABLE_WORKSPACE_CONTEXT); + return workspaceContext.save(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/workspace-action-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/workspace-action-base.ts deleted file mode 100644 index 5963d28f27..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/common/workspace-action-base.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { UmbWorkspaceContextInterface } from '../../../workspace-context/index.js'; -import { UMB_WORKSPACE_CONTEXT } from '../../../workspace-context/index.js'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; - -export interface UmbWorkspaceAction extends UmbApi { - execute(): Promise; -} - -export abstract class UmbWorkspaceActionBase - extends UmbControllerBase - implements UmbWorkspaceAction -{ - workspaceContext?: WorkspaceContextType; - constructor(host: UmbControllerHost) { - super(host); - - // TODO, we most likely should require a context token here in this type, and mane it specifically for workspace actions with context workspace request. - this.consumeContext(UMB_WORKSPACE_CONTEXT, (instance) => { - // TODO: Be aware we are casting here. We should consider a better solution for typing the contexts. (But notice we still want to capture the first workspace...) - this.workspaceContext = instance as unknown as WorkspaceContextType; - }); - } - abstract execute(): Promise; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/default.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/default.action.kind.ts new file mode 100644 index 0000000000..1187c81413 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/default.action.kind.ts @@ -0,0 +1,18 @@ +import type { UmbBackofficeManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: UmbBackofficeManifestKind = { + type: 'kind', + alias: 'Umb.Kind.WorkspaceAction.Default', + matchKind: 'default', + matchType: 'workspaceAction', + manifest: { + type: 'workspaceAction', + kind: 'default', + weight: 1000, + element: () => import('./workspace-action.element.js'), + meta: { + icon: '', + label: '(Missing label in manifest)', + }, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/manifests.ts new file mode 100644 index 0000000000..e8a0dcdedd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/manifests.ts @@ -0,0 +1,3 @@ +import { manifest as defaultKindManifest } from './default.action.kind.js'; + +export const manifests = [defaultKindManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action/workspace-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/workspace-action.element.ts similarity index 56% rename from src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action/workspace-action.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/workspace-action.element.ts index d21e502781..e6bccb0b30 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action/workspace-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/default/workspace-action.element.ts @@ -3,14 +3,20 @@ import { UmbActionExecutedEvent } from '@umbraco-cms/backoffice/event'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UUIButtonState } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { ManifestWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry'; -import { createExtensionApi } from '@umbraco-cms/backoffice/extension-api'; +import type { + ManifestWorkspaceAction, + MetaWorkspaceActionDefaultKind, +} from '@umbraco-cms/backoffice/extension-registry'; -import '../workspace-action-menu/index.js'; +import '../../workspace-action-menu/index.js'; @customElement('umb-workspace-action') -export class UmbWorkspaceActionElement extends UmbLitElement { - #manifest?: ManifestWorkspaceAction; +export class UmbWorkspaceActionElement< + MetaType extends MetaWorkspaceActionDefaultKind = MetaWorkspaceActionDefaultKind, + ApiType extends UmbWorkspaceAction = UmbWorkspaceAction, +> extends UmbLitElement { + #manifest?: ManifestWorkspaceAction; + #api?: ApiType; @state() private _buttonState?: UUIButtonState; @@ -18,39 +24,50 @@ export class UmbWorkspaceActionElement extends UmbLitElement { @state() private _aliases: Array = []; + @state() + _href?: string; + @property({ type: Object, attribute: false }) - public get manifest() { - return this.#manifest; - } - public set manifest(value: ManifestWorkspaceAction | undefined) { + public set manifest(value: ManifestWorkspaceAction | undefined) { if (!value) return; const oldValue = this.#manifest; this.#manifest = value; if (oldValue !== this.#manifest) { - this.#createApi(); this.#createAliases(); this.requestUpdate('manifest', oldValue); } } + public get manifest() { + return this.#manifest; + } - async #createApi() { - if (!this.manifest) return; - this.#api = await createExtensionApi(this.manifest, [this]); + @property({ attribute: false }) + public set api(api: ApiType | undefined) { + this.#api = api; + + // TODO: Fix so when we use a HREF it does not refresh the page? + this.#api?.getHref?.().then((href) => { + this._href = href; + // TODO: Do we need to update the component here? [NL] + }); + } + public get api(): ApiType | undefined { + return this.#api; } /** * Create a list of original and overwritten aliases of workspace actions for the action. */ async #createAliases() { - if (!this.manifest) return; + if (!this.#manifest) return; const aliases = new Set(); - if (this.manifest) { - aliases.add(this.manifest.alias); + if (this.#manifest) { + aliases.add(this.#manifest.alias); // TODO: This works on one level for now, which will be enough for the current use case. However, you can overwrite the overwrites, so we need to make this recursive. Perhaps we could move this to the extensions initializer. // Add overwrites so that we can show any previously registered actions on the original workspace action - if (this.manifest.overwrites) { - for (const alias of this.manifest.overwrites) { + if (this.#manifest.overwrites) { + for (const alias of this.#manifest.overwrites) { aliases.add(alias); } } @@ -58,9 +75,11 @@ export class UmbWorkspaceActionElement extends UmbLitElement { this._aliases = Array.from(aliases); } - #api?: UmbWorkspaceAction; + private async _onClick(event: MouseEvent) { + if (this._href) { + event.stopPropagation(); + } - private async _onClick() { this._buttonState = 'waiting'; try { @@ -79,15 +98,16 @@ export class UmbWorkspaceActionElement extends UmbLitElement { + .forWorkspaceActions=${this._aliases} + color="${this.#manifest?.meta.color || 'default'}" + look="${this.#manifest?.meta.look || 'default'}"> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/index.ts index 591ece3cc3..9a6b6a0812 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/index.ts @@ -1,2 +1,4 @@ -export * from './shared/index.js'; export * from './common/index.js'; +export * from './workspace-action-base.controller.js'; +export type * from './types.js'; +export type * from './workspace-action.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/manifests.ts new file mode 100644 index 0000000000..8bc9a56ab9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as defaultWorkspaceActionManifests } from './default/manifests.js'; + +export const manifests = [...defaultWorkspaceActionManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/index.ts deleted file mode 100644 index e4c8c546f4..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './workspace-action/index.js'; -export * from './workspace-action-menu/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action-menu/workspace-action-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action-menu/workspace-action-menu.element.ts deleted file mode 100644 index 8546559801..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action-menu/workspace-action-menu.element.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit'; -import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { ManifestTypes, ManifestWorkspaceActionMenuItem } from '@umbraco-cms/backoffice/extension-registry'; -import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import type { UmbExtensionElementInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; -import type { UUIInterfaceColor, UUIInterfaceLook } from '@umbraco-cms/backoffice/external/uui'; - -@customElement('umb-workspace-action-menu') -export class UmbWorkspaceActionMenuElement extends UmbLitElement { - #workspaceContext?: typeof UMB_WORKSPACE_CONTEXT.TYPE; - #actionsInitializer?: UmbExtensionsElementInitializer; - - /** - * The workspace actions to filter the available actions by. - * @example ['Umb.WorkspaceAction.Document.Save', 'Umb.WorkspaceAction.Document.SaveAndPublishNew'] - */ - @property({ attribute: false }) - forWorkspaceActions: Array = []; - - @property() - look: UUIInterfaceLook = 'secondary'; - - @property() - color: UUIInterfaceColor = 'default'; - - @state() - private _actions: Array> = []; - - @state() - _popoverOpen = false; - - constructor() { - super(); - - this.consumeContext(UMB_WORKSPACE_CONTEXT, (context) => { - this.#workspaceContext = context; - this.#initialise(); - }); - } - - #initialise() { - if (!this.#workspaceContext) throw new Error('No workspace context'); - - // If there are no workspace action aliases, then there is no need to initialize the actions. - if (!this.forWorkspaceActions.length) return; - - const unique = this.#workspaceContext.getUnique(); - const entityType = this.#workspaceContext.getEntityType(); - - this.#actionsInitializer = new UmbExtensionsElementInitializer( - this, - umbExtensionsRegistry, - 'workspaceActionMenuItem', // TODO: Stop using string for 'workspaceActionMenuItem', we need to start using Const. - (action) => { - const containsAlias = Array.isArray(action.forWorkspaceActions) - ? action.forWorkspaceActions.some((alias) => this.forWorkspaceActions.includes(alias)) - : this.forWorkspaceActions.includes(action.forWorkspaceActions); - //const isValidEntityType = !action.forEntityTypes.length || action.forEntityTypes.includes(entityType); - return containsAlias; // && isValidEntityType; - }, - (ctrls) => { - this._actions = ctrls; - }, - 'workspaceActionExtensionsInitializer', - 'umb-entity-action', - ); - - this.#actionsInitializer.properties = { unique, entityType }; - } - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - #onPopoverToggle(event: ToggleEvent) { - this._popoverOpen = event.newState === 'open'; - } - - render() { - return this._actions.length > 0 - ? html` - - - - - - - ${repeat( - this._actions, - (action) => action.alias, - (action) => action.component, - )} - - - - ` - : nothing; - } - - static styles: CSSResultGroup = [ - UmbTextStyles, - css` - :host { - --uui-menu-item-flat-structure: 1; - } - - #expand-symbol { - transform: rotate(-90deg); - } - - #expand-symbol[open] { - transform: rotate(0deg); - } - - #workspace-action-popover { - min-width: 200px; - } - - #popover-trigger { - --uui-button-padding-top-factor: 0.5; - --uui-button-padding-bottom-factor: 0.1; - --uui-button-border-radius: 0; - } - `, - ]; -} - -declare global { - interface HTMLElementTagNameMap { - 'umb-workspace-action-menu': UmbWorkspaceActionMenuElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action/index.ts deleted file mode 100644 index 24b7e3dc14..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/shared/workspace-action/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './workspace-action.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/types.ts new file mode 100644 index 0000000000..e4af37630c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/types.ts @@ -0,0 +1,3 @@ +export interface UmbWorkspaceActionArgs { + meta: MetaArgsType; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/workspace-action-base.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/workspace-action-base.controller.ts new file mode 100644 index 0000000000..e49549f732 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/workspace-action-base.controller.ts @@ -0,0 +1,36 @@ +import type { UmbWorkspaceActionArgs } from './types.js'; +import type { UmbWorkspaceAction } from './workspace-action.interface.js'; +import { UmbActionBase } from '@umbraco-cms/backoffice/action'; + +/** + * Base class for an workspace action. + * @export + * @abstract + * @class UmbWorkspaceActionBase + * @extends {UmbActionBase} + * @implements {UmbEntityAction} + * @template RepositoryType + */ +export abstract class UmbWorkspaceActionBase + extends UmbActionBase> + implements UmbWorkspaceAction +{ + /** + * By specifying the href, the action will act as a link. + * The `execute` method will not be called. + * @abstract + * @returns {string | undefined} + */ + public getHref(): Promise { + return Promise.resolve(undefined); + } + + /** + * By specifying the `execute` method, the action will act as a button. + * @abstract + * @returns {Promise} + */ + public execute(): Promise { + return Promise.resolve(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/workspace-action.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/workspace-action.interface.ts new file mode 100644 index 0000000000..27f2516f9f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-action/workspace-action.interface.ts @@ -0,0 +1,16 @@ +import type { UmbWorkspaceActionArgs } from './types.js'; +import type { UmbAction } from '@umbraco-cms/backoffice/action'; + +export interface UmbWorkspaceAction extends UmbAction> { + /** + * The href location, the action will act as a link. + * @returns {Promise} + */ + getHref(): Promise; + + /** + * The `execute` method, the action will act as a button. + * @returns {Promise} + */ + execute(): Promise; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts index 3670b36317..2a11e8d8ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts @@ -5,6 +5,14 @@ import { css, html, customElement, state } from '@umbraco-cms/backoffice/externa import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbModalContext } from '@umbraco-cms/backoffice/modal'; import { UMB_MODAL_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import type { ManifestWorkspaceAction, MetaWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbWorkspaceActionArgs } from '@umbraco-cms/backoffice/workspace'; + +function ExtensionApiArgsMethod( + manifest: ManifestWorkspaceAction, +): [UmbWorkspaceActionArgs] { + return [{ meta: manifest.meta }]; +} /** * @element umb-workspace-footer @@ -54,10 +62,11 @@ export class UmbWorkspaceFooterLayoutElement extends UmbLitElement { @click=${this.#rejectModal}>` : ''} - + .apiArgs=${ExtensionApiArgsMethod}> + `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/manifests.ts index 80ad04072c..fdcf26b4c7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/manifests.ts @@ -1,5 +1,11 @@ +import { manifests as componentManifests } from './components/manifests.js'; import { manifests as workspaceModals } from './workspace-modal/manifests.js'; import { manifest as workspaceAliasCondition } from './workspace-alias.condition.js'; import { manifest as workspaceEntityTypeCondition } from './workspace-entity-type.condition.js'; -export const manifests = [...workspaceModals, workspaceAliasCondition, workspaceEntityTypeCondition]; +export const manifests = [ + ...componentManifests, + ...workspaceModals, + workspaceAliasCondition, + workspaceEntityTypeCondition, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-alias.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-alias.condition.ts index ac8f784ea3..6006ddedf1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-alias.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-alias.condition.ts @@ -6,13 +6,14 @@ import type { UmbConditionControllerArguments, UmbExtensionCondition, } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbWorkspaceAliasCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); let permissionCheck: ((context: UmbWorkspaceContextInterface) => boolean) | undefined = undefined; if (this.config.match) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-variantable-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-variantable-context.interface.ts index 341f6b897a..764f370182 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-variantable-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-context/workspace-variantable-context.interface.ts @@ -22,7 +22,7 @@ export interface UmbVariantableWorkspaceContextInterface( alias: string, variantId?: UmbVariantId, - ): Promise>; + ): Promise | undefined>; getPropertyValue(alias: string, variantId?: UmbVariantId): ReturnValue | undefined; setPropertyValue(alias: string, value: unknown, variantId?: UmbVariantId): Promise; //propertyDataByAlias(alias: string, variantId?: UmbVariantId): Observable; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-entity-type.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-entity-type.condition.ts index eaaa5fab43..7fb321bc35 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-entity-type.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/workspace-entity-type.condition.ts @@ -6,13 +6,14 @@ import type { UmbConditionControllerArguments, UmbExtensionCondition, } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbWorkspaceEntityTypeCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); this.consumeContext(UMB_WORKSPACE_CONTEXT, (context) => { this.permitted = context.getEntityType().toLowerCase() === this.config.match.toLowerCase(); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/index.ts new file mode 100644 index 0000000000..2e8ea8d164 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/index.ts @@ -0,0 +1,2 @@ +export { UmbDataTypeCollectionRepository } from './repository/index.js'; +export { UMB_DATA_TYPE_COLLECTION_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/manifests.ts new file mode 100644 index 0000000000..15583d57df --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/manifests.ts @@ -0,0 +1,17 @@ +import { UMB_DATA_TYPE_COLLECTION_REPOSITORY_ALIAS } from './repository/index.js'; +import { manifests as collectionRepositoryManifests } from './repository/manifests.js'; +import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_DATA_TYPE_COLLECTION_ALIAS = 'Umb.Collection.DataType'; + +const collectionManifest: ManifestTypes = { + type: 'collection', + kind: 'default', + alias: UMB_DATA_TYPE_COLLECTION_ALIAS, + name: 'Data Type Collection', + meta: { + repositoryAlias: UMB_DATA_TYPE_COLLECTION_REPOSITORY_ALIAS, + }, +}; + +export const manifests = [collectionManifest, ...collectionRepositoryManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.repository.ts new file mode 100644 index 0000000000..64cc038870 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.repository.ts @@ -0,0 +1,44 @@ +import { UMB_DATA_TYPE_ITEM_STORE_CONTEXT } from '../../repository/item/data-type-item.store.js'; +import type { UmbDataTypeItemStore } from '../../repository/item/data-type-item.store.js'; +import type { UmbDataTypeCollectionFilterModel } from '../types.js'; +import { UmbDataTypeCollectionServerDataSource } from './data-type-collection.server.data-source.js'; +import type { UmbDataTypeCollectionDataSource } from './types.js'; +import type { UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; + +export class UmbDataTypeCollectionRepository extends UmbRepositoryBase implements UmbCollectionRepository { + #init; + #itemStore?: UmbDataTypeItemStore; + #collectionSource: UmbDataTypeCollectionDataSource; + + constructor(host: UmbControllerHost) { + super(host); + + this.#init = Promise.all([ + this.consumeContext(UMB_DATA_TYPE_ITEM_STORE_CONTEXT, (instance) => { + this.#itemStore = instance; + }).asPromise(), + ]); + + this.#collectionSource = new UmbDataTypeCollectionServerDataSource(host); + } + + async requestCollection(query: UmbDataTypeCollectionFilterModel) { + await this.#init; + + const { data, error } = await this.#collectionSource.getCollection(query); + + if (data) { + this.#itemStore!.appendItems(data.items); + } + + const uniques = data?.items.map((item) => item.unique) ?? []; + + return { data, error, asObservable: () => this.#itemStore!.items(uniques) }; + } + + destroy(): void {} +} + +export default UmbDataTypeCollectionRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.server.data-source.ts new file mode 100644 index 0000000000..c6dd9ce6f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/data-type-collection.server.data-source.ts @@ -0,0 +1,58 @@ +import type { UmbDataTypeCollectionFilterModel } from '../types.js'; +import type { UmbDataTypeItemModel } from '../../repository/index.js'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { DataTypeResource } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbCollectionDataSource } from '@umbraco-cms/backoffice/collection'; +import type { DataTypeItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +/** + * A data source that fetches the data-type collection data from the server. + * @export + * @class UmbDataTypeCollectionServerDataSource + * @implements {UmbCollectionDataSource} + */ +export class UmbDataTypeCollectionServerDataSource implements UmbCollectionDataSource { + #host: UmbControllerHost; + + /** + * Creates an instance of UmbDataTypeCollectionServerDataSource. + * @param {UmbControllerHost} host + * @DataTypeof UmbDataTypeCollectionServerDataSource + */ + constructor(host: UmbControllerHost) { + this.#host = host; + } + + /** + * Gets the DataType collection filtered by the given filter. + * @param {UmbDataTypeCollectionFilterModel} filter + * @return {*} + * @DataTypeof UmbDataTypeCollectionServerDataSource + */ + async getCollection(filter: UmbDataTypeCollectionFilterModel) { + const { data, error } = await tryExecuteAndNotify(this.#host, DataTypeResource.getFilterDataType(filter)); + + if (error) { + return { error }; + } + + if (!data) { + return { data: { items: [], total: 0 } }; + } + + const { items, total } = data; + + const mappedItems: Array = items.map((item: DataTypeItemResponseModel) => { + const dataTypeDetail: UmbDataTypeItemModel = { + unique: item.id, + name: item.name, + propertyEditorUiAlias: item.editorUiAlias!, + }; + + return dataTypeDetail; + }); + + return { data: { items: mappedItems, total } }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/index.ts new file mode 100644 index 0000000000..2dc8a030ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/index.ts @@ -0,0 +1,2 @@ +export { UMB_DATA_TYPE_COLLECTION_REPOSITORY_ALIAS } from './manifests.js'; +export { UmbDataTypeCollectionRepository } from './data-type-collection.repository.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/manifests.ts new file mode 100644 index 0000000000..398220279c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/manifests.ts @@ -0,0 +1,12 @@ +import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_DATA_TYPE_COLLECTION_REPOSITORY_ALIAS = 'Umb.Repository.DataType.Collection'; + +const repository: ManifestRepository = { + type: 'repository', + alias: UMB_DATA_TYPE_COLLECTION_REPOSITORY_ALIAS, + name: 'Data Type Collection Repository', + api: () => import('./data-type-collection.repository.js'), +}; + +export const manifests = [repository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/types.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/types.ts new file mode 100644 index 0000000000..3b07d1b2bc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/repository/types.ts @@ -0,0 +1,8 @@ +import type { UmbDataTypeCollectionFilterModel } from '../types.js'; +import type { UmbDataTypeItemModel } from '@umbraco-cms/backoffice/data-type'; +import type { UmbCollectionDataSource } from '@umbraco-cms/backoffice/collection'; + +export type UmbDataTypeCollectionDataSource = UmbCollectionDataSource< + UmbDataTypeItemModel, + UmbDataTypeCollectionFilterModel +>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/types.ts new file mode 100644 index 0000000000..edeaf61acd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/collection/types.ts @@ -0,0 +1,7 @@ +export interface UmbDataTypeCollectionFilterModel { + skip?: number; + take?: number; + name?: string; + editorUiAlias?: string; + editorAlias?: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-flow-input/data-type-flow-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-flow-input/data-type-flow-input.element.ts index dd2e5889e4..ab01b797aa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-flow-input/data-type-flow-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/components/data-type-flow-input/data-type-flow-input.element.ts @@ -1,8 +1,9 @@ import { UMB_DATATYPE_WORKSPACE_MODAL } from '../../index.js'; +import { UMB_DATA_TYPE_PICKER_FLOW_MODAL } from '../../modals/index.js'; import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbModalRouteRegistrationController, UMB_DATA_TYPE_PICKER_FLOW_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; // Note: Does only support picking a single data type. But this could be developed later into this same component. To follow other picker input components. 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 e174e7aeee..89c582df5a 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,8 +1,8 @@ import { UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import type { UmbDataTypeItemModel } from '../../repository/item/types.js'; +import { UMB_DATA_TYPE_PICKER_MODAL } from '../../modals/index.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UMB_DATA_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; export class UmbDataTypePickerContext extends UmbPickerInputContext { constructor(host: UmbControllerHost) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/duplicate/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/duplicate/manifests.ts index 727338362a..bd49cc29e6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/duplicate/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/duplicate/manifests.ts @@ -1,7 +1,7 @@ import { UMB_DATA_TYPE_ENTITY_TYPE } from '../../entity.js'; -import { UMB_COPY_DATA_TYPE_REPOSITORY_ALIAS } from '../../repository/copy/manifests.js'; +import { UMB_DATA_TYPE_PICKER_MODAL } from '../../modals/data-type-picker-modal.token.js'; +import { UMB_DUPLICATE_DATA_TYPE_REPOSITORY_ALIAS } from '../../repository/duplicate/manifests.js'; import { UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; -import { UMB_DATA_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ @@ -12,9 +12,9 @@ const entityActions: Array = [ name: 'Duplicate Data Type Entity Action', forEntityTypes: [UMB_DATA_TYPE_ENTITY_TYPE], meta: { - duplicateRepositoryAlias: UMB_COPY_DATA_TYPE_REPOSITORY_ALIAS, + duplicateRepositoryAlias: UMB_DUPLICATE_DATA_TYPE_REPOSITORY_ALIAS, itemRepositoryAlias: UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS, - pickerModalAlias: UMB_DATA_TYPE_PICKER_MODAL.toString(), + pickerModal: UMB_DATA_TYPE_PICKER_MODAL, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/manifests.ts index 1a95aa69ac..e75c7625ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/manifests.ts @@ -3,7 +3,7 @@ import { UMB_DATA_TYPE_DETAIL_REPOSITORY_ALIAS } from '../repository/detail/inde import { UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS } from '../repository/item/manifests.js'; import { manifests as createManifests } from './create/manifests.js'; import { manifests as moveManifests } from './move/manifests.js'; -import { manifests as copyManifests } from './duplicate/manifests.js'; +import { manifests as duplicateManifests } from './duplicate/manifests.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ @@ -20,4 +20,4 @@ const entityActions: Array = [ }, ]; -export const manifests = [...entityActions, ...createManifests, ...moveManifests, ...copyManifests]; +export const manifests = [...entityActions, ...createManifests, ...moveManifests, ...duplicateManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/move/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/move/manifests.ts index 4c4919f230..79ef41ec28 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/move/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/entity-actions/move/manifests.ts @@ -1,7 +1,7 @@ import { UMB_DATA_TYPE_ENTITY_TYPE } from '../../entity.js'; +import { UMB_DATA_TYPE_PICKER_MODAL } from '../../modals/index.js'; import { UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import { UMB_MOVE_DATA_TYPE_REPOSITORY_ALIAS } from '../../repository/move/manifests.js'; -import { UMB_DATA_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ @@ -14,7 +14,7 @@ const entityActions: Array = [ meta: { itemRepositoryAlias: UMB_DATA_TYPE_ITEM_REPOSITORY_ALIAS, moveRepositoryAlias: UMB_MOVE_DATA_TYPE_REPOSITORY_ALIAS, - pickerModalAlias: UMB_DATA_TYPE_PICKER_MODAL.toString(), + pickerModal: UMB_DATA_TYPE_PICKER_MODAL, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/index.ts index 2e147ef302..b7e28226cd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/index.ts @@ -4,5 +4,6 @@ export * from './components/index.js'; export * from './entity.js'; export * from './repository/index.js'; export * from './workspace/index.js'; +export * from './modals/index.js'; export type { UmbDataTypeDetailModel } from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts index 0f768f4901..867300c07f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/manifests.ts @@ -4,6 +4,7 @@ import { manifests as menuItemManifests } from './menu-item/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as modalManifests } from './modals/manifests.js'; +import { manifests as collectionManifests } from './collection/manifests.js'; export const manifests = [ ...entityActions, @@ -12,4 +13,5 @@ export const manifests = [ ...treeManifests, ...workspaceManifests, ...modalManifests, + ...collectionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/menu-item/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/menu-item/manifests.ts index 2f4a0dcc07..ee50bd8fa2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/menu-item/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/menu-item/manifests.ts @@ -9,7 +9,7 @@ const menuItem: ManifestMenuItemTreeKind = { meta: { label: 'Data Types', entityType: 'data-type', - treeAlias: 'Umb.Tree.DataTypes', + treeAlias: 'Umb.Tree.DataType', menus: ['Umb.Menu.Settings'], }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.ts index 74f7d98e5e..9df268dfcf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.element.ts @@ -1,13 +1,12 @@ -import { UmbDataTypeDetailRepository } from '../../repository/detail/data-type-detail.repository.js'; -import { UmbDataTypeTreeRepository } from '../../tree/data-type-tree.repository.js'; -import type { UmbDataTypeDetailModel } from '../../types.js'; -import { css, html, customElement, state, repeat, when } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbDataTypeCollectionRepository } from '../../collection/index.js'; import type { UmbDataTypePickerFlowDataTypePickerModalData, UmbDataTypePickerFlowDataTypePickerModalValue, -} from '@umbraco-cms/backoffice/modal'; +} from './data-type-picker-flow-data-type-picker-modal.token.js'; +import { css, html, customElement, state, repeat, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import type { UmbDataTypeItemModel } from '@umbraco-cms/backoffice/data-type'; @customElement('umb-data-type-picker-flow-data-type-picker-modal') export class UmbDataTypePickerFlowDataTypePickerModalElement extends UmbModalBaseElement< @@ -15,7 +14,7 @@ export class UmbDataTypePickerFlowDataTypePickerModalElement extends UmbModalBas UmbDataTypePickerFlowDataTypePickerModalValue > { @state() - private _dataTypes?: Array; + private _dataTypes?: Array; private _propertyEditorUiAlias!: string; @@ -32,33 +31,20 @@ export class UmbDataTypePickerFlowDataTypePickerModalElement extends UmbModalBas private async _observeDataTypesOf(propertyEditorUiAlias: string) { if (!this.data) return; - const dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); - const dataTypeTreeRepository = new UmbDataTypeTreeRepository(this); + const dataTypeCollectionRepository = new UmbDataTypeCollectionRepository(this); - // TODO: This is a hack to get the data types of a property editor ui alias. - // TODO: Make sure filtering works data-type that does not have a property editor ui, but should be using the default property editor UI for those. - // TODO: make an end-point just retrieving the data types using a given property editor ui alias. - const { data } = await dataTypeTreeRepository.requestRootTreeItems({ skip: 0, take: 100 }); + const collection = await dataTypeCollectionRepository.requestCollection({ + skip: 0, + take: 100, + editorUiAlias: propertyEditorUiAlias, + }); - if (!data) return; - - await Promise.all( - data.items.map((item) => { - if (item.unique) { - return dataTypeDetailRepository.requestByUnique(item.unique); - } - return Promise.resolve(); - }), - ); - - // TODO: Use the asObservable from above onces end-point has been made. - const source = await dataTypeDetailRepository.byPropertyEditorUiAlias(propertyEditorUiAlias); - this.observe(source, (dataTypes) => { + this.observe(collection.asObservable(), (dataTypes) => { this._dataTypes = dataTypes; }); } - private _handleClick(dataType: UmbDataTypeDetailModel) { + private _handleClick(dataType: UmbDataTypeItemModel) { if (dataType.unique) { this.value = { dataTypeId: dataType.unique }; this.modalContext?.submit(); @@ -79,7 +65,7 @@ export class UmbDataTypePickerFlowDataTypePickerModalElement extends UmbModalBas ${this._renderDataTypes()} ${this._renderCreate()}
- +
`; @@ -104,7 +90,7 @@ export class UmbDataTypePickerFlowDataTypePickerModalElement extends UmbModalBas ${dataType.name} - ` + ` : '', )} `, @@ -112,7 +98,7 @@ export class UmbDataTypePickerFlowDataTypePickerModalElement extends UmbModalBas } private _renderCreate() { return html` - +
Create new diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-flow-data-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.token.ts similarity index 89% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-flow-data-type-picker-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.token.ts index 3333633be4..d9712ad265 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-flow-data-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-data-type-picker-modal.token.ts @@ -1,4 +1,4 @@ -import { UmbModalToken } from './modal-token.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbDataTypePickerFlowDataTypePickerModalData { propertyEditorUiAlias: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts index e66c9a729c..af03a97473 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.element.ts @@ -1,21 +1,20 @@ import { UmbDataTypeTreeRepository } from '../../tree/data-type-tree.repository.js'; -import type { UmbDataTypeTreeItemModel } from '../../tree/types.js'; import { UMB_DATATYPE_WORKSPACE_MODAL } from '../../workspace/data-type-workspace.modal-token.js'; -import { css, html, repeat, customElement, state, when, nothing } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UMB_DATA_TYPE_ENTITY_TYPE } from '../../entity.js'; +import { UmbDataTypeCollectionRepository } from '../../collection/index.js'; +import { UMB_DATA_TYPE_PICKER_FLOW_DATA_TYPE_PICKER_MODAL } from './data-type-picker-flow-data-type-picker-modal.token.js'; import type { UmbDataTypePickerFlowModalData, UmbDataTypePickerFlowModalValue, - UmbModalRouteBuilder, -} from '@umbraco-cms/backoffice/modal'; -import { - UMB_DATA_TYPE_PICKER_FLOW_DATA_TYPE_PICKER_MODAL, - UmbModalBaseElement, - UmbModalRouteRegistrationController, -} from '@umbraco-cms/backoffice/modal'; -import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; +} from './data-type-picker-flow-modal.token.js'; +import { css, html, repeat, customElement, state, when, nothing } from '@umbraco-cms/backoffice/external/lit'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbModalBaseElement, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbDataTypeItemModel } from '@umbraco-cms/backoffice/data-type'; +import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/modal'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; interface GroupedItems { [key: string]: Array; @@ -31,7 +30,7 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement< } @state() - private _groupedDataTypes?: GroupedItems; + private _groupedDataTypes?: GroupedItems; @state() private _groupedPropertyEditorUIs: GroupedItems = {}; @@ -44,14 +43,15 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement< private _createDataTypeModal: UmbModalRouteRegistrationController; - #treeRepository; - #dataTypes: Array = []; + #collectionRepository; + #dataTypes: Array = []; #propertyEditorUIs: Array = []; #currentFilterQuery = ''; constructor() { super(); - this.#treeRepository = new UmbDataTypeTreeRepository(this); + + this.#collectionRepository = new UmbDataTypeCollectionRepository(this); new UmbModalRouteRegistrationController(this, UMB_DATA_TYPE_PICKER_FLOW_DATA_TYPE_PICKER_MODAL) .addAdditionalPath(':uiAlias') @@ -79,7 +79,7 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement< this._createDataTypeModal = new UmbModalRouteRegistrationController(this, UMB_DATATYPE_WORKSPACE_MODAL) .addAdditionalPath(':uiAlias') .onSetup((params) => { - return { data: { entityType: 'data-type', preset: { editorUiAlias: params.uiAlias } } }; + return { data: { entityType: UMB_DATA_TYPE_ENTITY_TYPE, preset: { editorUiAlias: params.uiAlias } } }; }) .onSubmit((value) => { this._select(value?.unique); @@ -92,17 +92,17 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement< private _createDataType(propertyEditorUiAlias: string) { // TODO: Could be nice with a more pretty way to prepend to the URL: // Open create modal: - this._createDataTypeModal.open({ uiAlias: propertyEditorUiAlias }, 'create/null'); + this._createDataTypeModal.open( + { uiAlias: propertyEditorUiAlias }, + `create/parent/${UMB_DATA_TYPE_ENTITY_TYPE}/null`, + ); } async #init() { - // TODO: Get ALL items, or traverse the structure aka. multiple recursive calls. this.observe( - (await this.#treeRepository.requestRootTreeItems({ skip: 0, take: 100 })).asObservable(), - (items) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.#dataTypes = items; + (await this.#collectionRepository.requestCollection({ skip: 0, take: 100 })).asObservable(), + (dataTypes) => { + this.#dataTypes = dataTypes; this._performFiltering(); }, '_repositoryItemsObserver', @@ -113,12 +113,11 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement< this.#propertyEditorUIs = propertyEditorUIs.filter( (propertyEditorUi) => !!propertyEditorUi.meta.propertyEditorSchemaAlias, ); - this._performFiltering(); }); } - private _handleDataTypeClick(dataType: UmbDataTypeTreeItemModel) { + private _handleDataTypeClick(dataType: UmbDataTypeItemModel) { if (dataType.unique) { this._select(dataType.unique); this._submitModal(); @@ -136,8 +135,8 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement< } private _performFiltering() { if (this.#currentFilterQuery) { - const filteredDataTypes = this.#dataTypes.filter( - (dataType) => dataType.name?.toLowerCase().includes(this.#currentFilterQuery), + const filteredDataTypes = this.#dataTypes.filter((dataType) => + dataType.name?.toLowerCase().includes(this.#currentFilterQuery), ); /* TODO: data type items doesn't have a group property. We will need a reference to the Property Editor UI to get the group. @@ -157,7 +156,7 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement< propertyEditorUI.name.toLowerCase().includes(this.#currentFilterQuery) || propertyEditorUI.alias.toLowerCase().includes(this.#currentFilterQuery) ); - }); + }); // TODO: groupBy is not known by TS yet // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -265,12 +264,12 @@ export class UmbDataTypePickerFlowModalElement extends UmbModalBaseElement<
`, - ) + ) : ''} `; } - private _renderGroupDataTypes(dataTypes: Array) { + private _renderGroupDataTypes(dataTypes: Array) { return html`
    ${repeat( dataTypes, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-flow-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.token.ts similarity index 85% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-flow-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.token.ts index 5d566cc85d..d6acec2fda 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-flow-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-flow/data-type-picker-flow-modal.token.ts @@ -1,4 +1,4 @@ -import { UmbModalToken } from './modal-token.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbDataTypePickerFlowModalData { submitLabel?: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-modal.token.ts similarity index 79% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-modal.token.ts index 5c0a42653f..1208d6e2c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/data-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/modals/data-type-picker-modal.token.ts @@ -1,5 +1,5 @@ -import type { UmbPickerModalValue, UmbTreePickerModalData } from '../types.js'; -import { UmbModalToken } from './modal-token.js'; +import type { UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; import type { UmbUniqueTreeItemModel } from '@umbraco-cms/backoffice/tree'; export type UmbDataTypePickerModalData = UmbTreePickerModalData; @@ -13,7 +13,7 @@ export const UMB_DATA_TYPE_PICKER_MODAL = new UmbModalToken; - #copySource: UmbCopyDataSource; - #detailRepository: UmbDataTypeDetailRepository; - #treeStore?: UmbDataTypeTreeStore; - #notificationContext?: UmbNotificationContext; - - constructor(host: UmbControllerHost) { - super(host); - this.#copySource = new UmbDataTypeCopyServerDataSource(this); - this.#detailRepository = new UmbDataTypeDetailRepository(this); - - this.#init = Promise.all([ - this.consumeContext(UMB_DATA_TYPE_TREE_STORE_CONTEXT, (instance) => { - this.#treeStore = instance; - }).asPromise(), - - this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { - this.#notificationContext = instance; - }).asPromise(), - ]); - } - - async copy(unique: string, targetUnique: string | null) { - await this.#init; - const { data: dataTypeCopyUnique, error } = await this.#copySource.copy(unique, targetUnique); - if (error) return { error }; - - if (dataTypeCopyUnique) { - const { data: dataTypeCopy } = await this.#detailRepository.requestByUnique(dataTypeCopyUnique); - if (!dataTypeCopy) throw new Error('Could not find copied data type'); - - // TODO: Be aware about this responsibility. - // this.#treeStore!.append(dataTypeCopy); - - const notification = { data: { message: `Data type copied` } }; - this.#notificationContext!.peek('positive', notification); - } - - return { data: dataTypeCopyUnique }; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/index.ts deleted file mode 100644 index 22e8f625fb..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { UmbCopyDataTypeRepository } from './data-type-copy.repository.js'; -export { UMB_COPY_DATA_TYPE_REPOSITORY_ALIAS as COPY_DATA_TYPE_REPOSITORY_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/manifests.ts deleted file mode 100644 index caaa980747..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/manifests.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { UmbCopyDataTypeRepository } from './data-type-copy.repository.js'; -import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; - -export const UMB_COPY_DATA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.DataType.Copy'; - -const copyRepository: ManifestRepository = { - type: 'repository', - alias: UMB_COPY_DATA_TYPE_REPOSITORY_ALIAS, - name: 'Copy Data Type Repository', - api: UmbCopyDataTypeRepository, -}; - -export const manifests = [copyRepository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/data-type-duplicate.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/data-type-duplicate.repository.ts new file mode 100644 index 0000000000..febd4545ff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/data-type-duplicate.repository.ts @@ -0,0 +1,42 @@ +import { UmbDataTypeDetailRepository } from '../detail/data-type-detail.repository.js'; +import { UmbDataTypeDuplicateServerDataSource } from './data-type-duplicate.server.data-source.js'; +import type { UmbNotificationContext } from '@umbraco-cms/backoffice/notification'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbDuplicateRepository, UmbDuplicateDataSource } from '@umbraco-cms/backoffice/repository'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; + +export class UmbDuplicateDataTypeRepository extends UmbRepositoryBase implements UmbDuplicateRepository { + #init: Promise; + #duplicateSource: UmbDuplicateDataSource; + #detailRepository: UmbDataTypeDetailRepository; + #notificationContext?: UmbNotificationContext; + + constructor(host: UmbControllerHost) { + super(host); + this.#duplicateSource = new UmbDataTypeDuplicateServerDataSource(this); + this.#detailRepository = new UmbDataTypeDetailRepository(this); + + this.#init = Promise.all([ + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { + this.#notificationContext = instance; + }).asPromise(), + ]); + } + + async duplicate(unique: string, targetUnique: string | null) { + await this.#init; + const { data: dataTypeDuplicateUnique, error } = await this.#duplicateSource.duplicate(unique, targetUnique); + if (error) return { error }; + + if (dataTypeDuplicateUnique) { + const { data: dataTypeDuplicate } = await this.#detailRepository.requestByUnique(dataTypeDuplicateUnique); + if (!dataTypeDuplicate) throw new Error('Could not find copied data type'); + // TODO: reload tree + const notification = { data: { message: `Data type copied` } }; + this.#notificationContext!.peek('positive', notification); + } + + return { data: dataTypeDuplicateUnique }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/data-type-copy.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/data-type-duplicate.server.data-source.ts similarity index 67% rename from src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/data-type-copy.server.data-source.ts rename to src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/data-type-duplicate.server.data-source.ts index e066526440..5caf207fcd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/copy/data-type-copy.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/data-type-duplicate.server.data-source.ts @@ -1,20 +1,20 @@ import { DataTypeResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; -import type { UmbCopyDataSource } from '@umbraco-cms/backoffice/repository'; +import type { UmbDuplicateDataSource } from '@umbraco-cms/backoffice/repository'; /** * A data source for Data Type items that fetches data from the server * @export - * @class UmbDataTypeCopyServerDataSource + * @class UmbDataTypeDuplicateServerDataSource */ -export class UmbDataTypeCopyServerDataSource implements UmbCopyDataSource { +export class UmbDataTypeDuplicateServerDataSource implements UmbDuplicateDataSource { #host: UmbControllerHost; /** - * Creates an instance of UmbDataTypeCopyServerDataSource. + * Creates an instance of UmbDataTypeDuplicateServerDataSource. * @param {UmbControllerHost} host - * @memberof UmbDataTypeCopyServerDataSource + * @memberof UmbDataTypeDuplicateServerDataSource */ constructor(host: UmbControllerHost) { this.#host = host; @@ -24,9 +24,9 @@ export class UmbDataTypeCopyServerDataSource implements UmbCopyDataSource { * Copy an item for the given unique to the target unique * @param {Array} unique * @return {*} - * @memberof UmbDataTypeCopyServerDataSource + * @memberof UmbDataTypeDuplicateServerDataSource */ - async copy(unique: string, targetUnique: string | null) { + async duplicate(unique: string, targetUnique: string | null) { if (!unique) throw new Error('Unique is missing'); if (targetUnique === undefined) throw new Error('Target unique is missing'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/index.ts new file mode 100644 index 0000000000..eeb675e8c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/index.ts @@ -0,0 +1,2 @@ +export { UmbDuplicateDataTypeRepository } from './data-type-duplicate.repository.js'; +export { UMB_DUPLICATE_DATA_TYPE_REPOSITORY_ALIAS } from './manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/manifests.ts new file mode 100644 index 0000000000..f249083646 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/duplicate/manifests.ts @@ -0,0 +1,13 @@ +import { UmbDuplicateDataTypeRepository } from './data-type-duplicate.repository.js'; +import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_DUPLICATE_DATA_TYPE_REPOSITORY_ALIAS = 'Umb.Repository.DataType.Duplicate'; + +const duplicateRepository: ManifestRepository = { + type: 'repository', + alias: UMB_DUPLICATE_DATA_TYPE_REPOSITORY_ALIAS, + name: 'Duplicate Data Type Repository', + api: UmbDuplicateDataTypeRepository, +}; + +export const manifests = [duplicateRepository]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/index.ts index 71a8f8fe99..dc76f38eaf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/index.ts @@ -1,4 +1,4 @@ -export { UmbCopyDataTypeRepository, COPY_DATA_TYPE_REPOSITORY_ALIAS } from './copy/index.js'; +export { UmbDuplicateDataTypeRepository, UMB_DUPLICATE_DATA_TYPE_REPOSITORY_ALIAS } from './duplicate/index.js'; export { UmbDataTypeDetailRepository, UMB_DATA_TYPE_DETAIL_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/manifests.ts index 9c5065f084..d2d7f2a1c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/repository/manifests.ts @@ -1,6 +1,6 @@ -import { manifests as copyManifests } from './copy/manifests.js'; +import { manifests as duplicateManifests } from './duplicate/manifests.js'; import { manifests as detailManifests } from './detail/manifests.js'; import { manifests as itemManifests } from './item/manifests.js'; import { manifests as moveManifests } from './move/manifests.js'; -export const manifests = [...copyManifests, ...detailManifests, ...itemManifests, ...moveManifests]; +export const manifests = [...duplicateManifests, ...detailManifests, ...itemManifests, ...moveManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/data-type-folder.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/data-type-folder.repository.ts index ec4cd7dd8f..f30dfe66b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/data-type-folder.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/data-type-folder.repository.ts @@ -1,27 +1,9 @@ -import { UMB_DATA_TYPE_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import { UMB_DATA_TYPE_TREE_STORE_CONTEXT } from '../data-type-tree.store.js'; import { UmbDataTypeFolderServerDataSource } from './data-type-folder.server.data-source.js'; -import type { UmbDataTypeFolderTreeItemModel } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbFolderModel } from '@umbraco-cms/backoffice/tree'; import { UmbFolderRepositoryBase } from '@umbraco-cms/backoffice/tree'; -export class UmbDataTypeFolderRepository extends UmbFolderRepositoryBase { +export class UmbDataTypeFolderRepository extends UmbFolderRepositoryBase { constructor(host: UmbControllerHost) { - super(host, UmbDataTypeFolderServerDataSource, UMB_DATA_TYPE_TREE_STORE_CONTEXT, folderToDataTypeTreeItemMapper); + super(host, UmbDataTypeFolderServerDataSource); } } - -const folderToDataTypeTreeItemMapper = ( - folder: UmbFolderModel, - parentUnique: string | null, -): UmbDataTypeFolderTreeItemModel => { - return { - unique: folder.unique, - parentUnique, - name: folder.name, - entityType: UMB_DATA_TYPE_FOLDER_ENTITY_TYPE, - hasChildren: false, - isFolder: true, - }; -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/data-type-folder.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/data-type-folder.server.data-source.ts index 312cbdb919..e5f5be8799 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/data-type-folder.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/data-type-folder.server.data-source.ts @@ -61,7 +61,7 @@ export class UmbDataTypeFolderServerDataSource implements UmbFolderDataSource { const requestBody = { id: args.unique, - parentId: args.parentUnique, + parent: args.parentUnique ? { id: args.parentUnique } : null, name: args.name, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/manifests.ts index 0a72f86ea6..a0215bf1b7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/folder/manifests.ts @@ -14,7 +14,7 @@ const folderRepository: ManifestRepository = { const entityActions: Array = [ { type: 'entityAction', - kind: 'folderRename', + kind: 'folderUpdate', alias: 'Umb.EntityAction.DataType.Folder.Rename', name: 'Rename Data Type Folder Entity Action', forEntityTypes: [UMB_DATA_TYPE_FOLDER_ENTITY_TYPE], diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/manifests.ts index af60feefb7..e854377970 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/tree/manifests.ts @@ -29,7 +29,7 @@ const treeStore: ManifestTreeStore = { const tree: ManifestTree = { type: 'tree', kind: 'default', - alias: 'Umb.Tree.DataTypes', + alias: 'Umb.Tree.DataType', name: 'Data Types Tree', meta: { repositoryAlias: UMB_DATA_TYPE_TREE_REPOSITORY_ALIAS, 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 48f9ba838e..221f429d13 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 @@ -22,6 +22,7 @@ import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registr import { UMB_PROPERTY_EDITOR_SCHEMA_ALIAS_DEFAULT } from '@umbraco-cms/backoffice/property-editor'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; type EntityType = UmbDataTypeDetailModel; export class UmbDataTypeWorkspaceContext @@ -283,6 +284,14 @@ export class UmbDataTypeWorkspaceContext eventContext.dispatchEvent(event); } else { await this.repository.save(this.#currentData.value); + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } this.setIsNew(false); diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/manifests.ts index 3a454b0526..27d8aa516e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/manifests.ts @@ -1,7 +1,7 @@ import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -56,9 +56,10 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.DataType.Save', name: 'Save Data Type Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts index bdd188a3b2..b00b10ea19 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts @@ -60,7 +60,8 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im selection: this._propertyEditorUiAlias ? [this._propertyEditorUiAlias] : [], }, }) - .onSubmit(); + .onSubmit() + .catch(() => undefined); this._workspaceContext?.setPropertyEditorUiAlias(value?.selection[0]); } @@ -90,7 +91,7 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im - ` + ` : html` - `} + `} `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary-picker-modal.token.ts new file mode 100644 index 0000000000..8ab75f0952 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/dictionary-picker-modal.token.ts @@ -0,0 +1,20 @@ +import type { UmbUniqueTreeItemModel } from '../core/tree/types.js'; +import { UmbModalToken } from '../core/modal/token/modal-token.js'; +import type { UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; + +export type UmbDictionaryPickerModalData = UmbTreePickerModalData; +export type UmbDictionaryPickerModalValue = UmbPickerModalValue; + +export const UMB_DICTIONARY_PICKER_MODAL = new UmbModalToken< + UmbDictionaryPickerModalData, + UmbDictionaryPickerModalValue +>('Umb.Modal.TreePicker', { + modal: { + type: 'sidebar', + size: 'small', + }, + data: { + hideTreeRoot: true, + treeAlias: 'Umb.Tree.Dictionary', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/create/create-dictionary-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/create/create-dictionary-modal.element.ts deleted file mode 100644 index 2c81e8bad1..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/create/create-dictionary-modal.element.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { html, customElement, query, when } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UmbCreateDictionaryModalData, UmbCreateDictionaryModalValue } from '@umbraco-cms/backoffice/modal'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; - -@customElement('umb-create-dictionary-modal') -export class UmbCreateDictionaryModalElement extends UmbModalBaseElement< - UmbCreateDictionaryModalData, - UmbCreateDictionaryModalValue -> { - @query('#form') - private _form!: HTMLFormElement; - - #parentName?: string; - - connectedCallback() { - super.connectedCallback(); - - if (this.data?.parentName) { - this.observe(this.data.parentName, (value) => (this.#parentName = value)); - } - } - - #handleCancel() { - this.modalContext?.reject(); - } - - #submitForm() { - this._form?.requestSubmit(); - } - - async #handleSubmit(e: SubmitEvent) { - e.preventDefault(); - - const form = e.target as HTMLFormElement; - if (!form || !form.checkValidity()) return; - - const formData = new FormData(form); - const name = formData.get('name') as string; - - this.value = { - name, - parentId: this.data?.parentId ?? null, - }; - this.modalContext?.submit(); - } - - render() { - return html` - ${when(this.#parentName, () => html`

    Create a dictionary item under ${this.#parentName}

    `)} - -
    - - Name -
    - -
    -
    -
    -
    - - -
    `; - } - - static styles = [UmbTextStyles]; -} - -export default UmbCreateDictionaryModalElement; - -declare global { - interface HTMLElementTagNameMap { - 'umb-create-dictionary-modal': UmbCreateDictionaryModalElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/manifests.ts index 317f2dd222..e3503e1c88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/entity-action/manifests.ts @@ -66,12 +66,6 @@ const entityActions: Array = [ ]; const modals: Array = [ - { - type: 'modal', - alias: 'Umb.Modal.Dictionary.Create', - name: 'Create Dictionary Modal', - element: () => import('./create/create-dictionary-modal.element.js'), - }, { type: 'modal', alias: 'Umb.Modal.Dictionary.Export', diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/index.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/index.ts index 5c342dd939..41568b52c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/index.ts @@ -1,2 +1,3 @@ export * from './repository/index.js'; export * from './tree/index.js'; +export { UMB_DICTIONARY_PICKER_MODAL } from './dictionary-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/detail/dictionary-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/detail/dictionary-detail.server.data-source.ts index c4a27a99be..2f7a01821c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/detail/dictionary-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/repository/detail/dictionary-detail.server.data-source.ts @@ -76,13 +76,13 @@ export class UmbDictionaryServerDataSource implements UmbDetailDataSource = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.Dictionary.Tree.ReloadChildrenOf', name: 'Reload Dictionary Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', forEntityTypes: [UMB_DICTIONARY_ROOT_ENTITY_TYPE, UMB_DICTIONARY_ENTITY_TYPE], }, ]; 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 5779f153be..ea4be0ef11 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 @@ -9,6 +9,7 @@ import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; export class UmbDictionaryWorkspaceContext extends UmbEditableWorkspaceContextBase @@ -111,6 +112,14 @@ export class UmbDictionaryWorkspaceContext this.setIsNew(false); } else { await this.detailRepository.save(this.#data.value); + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } const data = this.getData(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/manifests.ts index 24c2cdf863..9fa7840c41 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/manifests.ts @@ -2,7 +2,7 @@ import { UMB_DICTIONARY_ENTITY_TYPE } from '../entity.js'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -37,9 +37,10 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.Dictionary.Save', name: 'Save Dictionary Workspace Action', weight: 90, 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 fdf59b96ee..6e890c91bf 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 @@ -2,7 +2,7 @@ import type { UmbDocumentTypeItemModel } from '../../repository/index.js'; import { DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UMB_DOCUMENT_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UMB_DOCUMENT_TYPE_PICKER_MODAL } from '@umbraco-cms/backoffice/document-type'; export class UmbDocumentTypePickerContext extends UmbPickerInputContext { constructor(host: UmbControllerHost) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts index 3e5be1075f..89978f248b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/components/input-document-type/input-document-type.element.ts @@ -72,12 +72,13 @@ export class UmbInputDocumentTypeElement extends FormControlMixin(UmbLitElement) @property({ type: String, attribute: 'min-message' }) maxMessage = 'This field exceeds the allowed amount of items'; - public get selectedIds(): Array { - return this.#pickerContext.getSelection(); - } + @property({ type: Array }) public set selectedIds(ids: Array | undefined) { this.#pickerContext.setSelection(ids ?? []); } + public get selectedIds(): Array { + return this.#pickerContext.getSelection(); + } @property() public set value(idsString: string) { @@ -165,9 +166,7 @@ export class UmbInputDocumentTypeElement extends FormControlMixin(UmbLitElement) id="add-button" look="placeholder" @click=${this.#openPicker} - label="${this.localize.term('general_choose')}" - >${this.localize.term('general_choose')} + label="${this.localize.term('general_choose')}"> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts index f427c27beb..e22aaa3ac6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/manifests.ts @@ -9,6 +9,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.DocumentType.Create', name: 'Create Document Type Entity Action', weight: 1000, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/modal/document-type-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/modal/document-type-create-options-modal.element.ts index 360f1ee851..a3077a9e30 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/modal/document-type-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/create/modal/document-type-create-options-modal.element.ts @@ -12,15 +12,16 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.DocumentType.Delete', name: 'Delete Document-Type Entity Action', - kind: 'delete', forEntityTypes: [entityType], meta: { itemRepositoryAlias: DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, @@ -23,41 +19,26 @@ const entityActions: Array = [ }, { type: 'entityAction', + kind: 'move', alias: 'Umb.EntityAction.DocumentType.Move', - name: 'Move Document-Type Entity Action', - weight: 700, - api: UmbMoveEntityAction, + name: 'Move Document Type Entity Action', forEntityTypes: [entityType], meta: { - icon: 'icon-enter', - label: 'Move', - repositoryAlias: DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS, + itemRepositoryAlias: DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, + moveRepositoryAlias: DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS, + pickerModal: UMB_DOCUMENT_TYPE_PICKER_MODAL, }, }, { type: 'entityAction', - alias: 'Umb.EntityAction.DocumentType.Copy', - name: 'Copy Document-Type Entity Action', - weight: 600, - api: UmbDuplicateEntityAction, + kind: 'duplicate', + alias: 'Umb.EntityAction.DocumentType.Duplicate', + name: 'Duplicate Document Type Entity Action', forEntityTypes: [entityType], meta: { - icon: 'icon-documents', - label: 'Copy', - repositoryAlias: DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS, - }, - }, - { - type: 'entityAction', - alias: 'Umb.EntityAction.DocumentType.Sort', - name: 'Sort Document-Type Entity Action', - weight: 500, - api: UmbSortChildrenOfEntityAction, - forEntityTypes: [entityType], - meta: { - icon: 'icon-navigation-vertical', - label: 'Sort', - repositoryAlias: DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS, + itemRepositoryAlias: DOCUMENT_TYPE_ITEM_REPOSITORY_ALIAS, + duplicateRepositoryAlias: DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS, + pickerModal: UMB_DOCUMENT_TYPE_PICKER_MODAL, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/document-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts similarity index 89% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/token/document-type-picker-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts index 2c83f29639..b607f833ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/document-type-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/document-type-picker-modal.token.ts @@ -1,4 +1,4 @@ -import { UmbModalToken } from './modal-token.js'; +import { UmbModalToken } from '../../../core/modal/token/modal-token.js'; import type { UmbDocumentTypeTreeItemModel } from '@umbraco-cms/backoffice/document-type'; import type { UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/index.ts index bda362c82e..cb78b5f545 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/modals/index.ts @@ -1 +1,2 @@ export * from './composition-picker/index.js'; +export * from './document-type-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/property-editors/document-type-picker/property-editor-ui-document-type-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/property-editors/document-type-picker/property-editor-ui-document-type-picker.element.ts index f7c7c37b07..b90e5184f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/property-editors/document-type-picker/property-editor-ui-document-type-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/property-editors/document-type-picker/property-editor-ui-document-type-picker.element.ts @@ -2,18 +2,15 @@ import type { UmbInputDocumentTypeElement } from '../../components/input-documen import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import { + UmbPropertyValueChangeEvent, + type UmbPropertyEditorConfigCollection, +} from '@umbraco-cms/backoffice/property-editor'; @customElement('umb-property-editor-ui-document-type-picker') export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - @property({ type: Array }) - public get value(): Array | string | undefined { - return this._value; - } - public set value(value: Array | string | undefined) { - this._value = value; - } - private _value?: Array | string; + @property() + public value?: string; @property({ attribute: false }) public set config(config: UmbPropertyEditorConfigCollection | undefined) { @@ -27,6 +24,9 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement this._onlyElementTypes = config.getValueByAlias('onlyPickElementTypes') ?? false; } } + public get config() { + return undefined; + } @state() private _limitMin?: number; @@ -39,8 +39,8 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement private _onChange(event: CustomEvent) { const selectedIds = (event.target as UmbInputDocumentTypeElement).selectedIds; - this.value = this._multiPicker ? selectedIds : selectedIds[0]; - this.dispatchEvent(new CustomEvent('property-value-change')); + this.value = this._multiPicker ? selectedIds.join(',') : selectedIds[0]; + this.dispatchEvent(new UmbPropertyValueChangeEvent()); } // TODO: Implement mandatory? @@ -49,17 +49,13 @@ export class UmbPropertyEditorUIDocumentTypePickerElement extends UmbLitElement ? html` ) ?? [] - : this._value - ? [this._value as string] - : []} + .value=${this.value ?? ''} .min=${this._limitMin ?? 0} .max=${this._limitMax ?? Infinity} - .elementTypesOnly=${this._onlyElementTypes ?? false} - >Add - ` + .elementTypesOnly=${this._onlyElementTypes ?? false}> + Add + + ` : ''; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/document-type-folder.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/document-type-folder.repository.ts index 50691b9d13..6844713b43 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/document-type-folder.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/document-type-folder.repository.ts @@ -1,33 +1,9 @@ -import { UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import { UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT } from '../document-type.tree.store.js'; import { UmbDocumentTypeFolderServerDataSource } from './document-type-folder.server.data-source.js'; -import type { UmbDocumentTypeFolderTreeItemModel } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbFolderModel } from '@umbraco-cms/backoffice/tree'; import { UmbFolderRepositoryBase } from '@umbraco-cms/backoffice/tree'; -export class UmbDocumentTypeFolderRepository extends UmbFolderRepositoryBase { +export class UmbDocumentTypeFolderRepository extends UmbFolderRepositoryBase { constructor(host: UmbControllerHost) { - super( - host, - UmbDocumentTypeFolderServerDataSource, - UMB_DOCUMENT_TYPE_TREE_STORE_CONTEXT, - folderToDocumentTypeTreeItemMapper, - ); + super(host, UmbDocumentTypeFolderServerDataSource); } } - -const folderToDocumentTypeTreeItemMapper = ( - folder: UmbFolderModel, - parentUnique: string | null, -): UmbDocumentTypeFolderTreeItemModel => { - return { - unique: folder.unique, - parentUnique, - name: folder.name, - entityType: UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE, - hasChildren: false, - isFolder: true, - isElement: false, - }; -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/document-type-folder.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/document-type-folder.server.data-source.ts index bf13084cad..5371e5c03f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/document-type-folder.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/document-type-folder.server.data-source.ts @@ -61,7 +61,7 @@ export class UmbDocumentTypeFolderServerDataSource implements UmbFolderDataSourc const requestBody = { id: args.unique, - parentId: args.parentUnique, + parent: args.parentUnique ? { id: args.parentUnique } : null, name: args.name, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/manifests.ts index 4cad258c2d..4ceb20018d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/tree/folder/manifests.ts @@ -1,7 +1,6 @@ import { UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE } from '../../entity.js'; import { UmbDocumentTypeFolderRepository } from './document-type-folder.repository.js'; -import { UmbDeleteFolderEntityAction, UmbUpdateFolderEntityAction } from '@umbraco-cms/backoffice/tree'; -import type { ManifestEntityAction, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestEntityActions, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS = 'Umb.Repository.DocumentType.Folder'; @@ -12,31 +11,25 @@ const folderRepository: ManifestRepository = { api: UmbDocumentTypeFolderRepository, }; -const entityActions: Array = [ +const entityActions: Array = [ { type: 'entityAction', - alias: 'Umb.EntityAction.DocumentType.RenameFolder', + kind: 'folderUpdate', + alias: 'Umb.EntityAction.DocumentType.Folder.Rename', name: 'Rename Document Type Folder Entity Action', - weight: 800, - api: UmbUpdateFolderEntityAction, forEntityTypes: [UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE], meta: { - icon: 'icon-edit', - label: 'Rename Folder...', - repositoryAlias: UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS, + folderRepositoryAlias: UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS, }, }, { type: 'entityAction', - alias: 'Umb.EntityAction.DocumentType.DeleteFolder', + kind: 'folderDelete', + alias: 'Umb.EntityAction.DocumentType.Folder.Delete', name: 'Delete Document Type Folder Entity Action', - weight: 700, - api: UmbDeleteFolderEntityAction, forEntityTypes: [UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE], meta: { - icon: 'icon-trash', - label: 'Delete Folder...', - repositoryAlias: UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS, + folderRepositoryAlias: UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-sorter.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-sorter.ts deleted file mode 100644 index a1d06df80f..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type-workspace-sorter.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/external/backend-api'; -import type { UmbSorterConfig } from '@umbraco-cms/backoffice/sorter'; - -const SORTER_CONFIG_HORIZONTAL: UmbSorterConfig = { - getUniqueOfElement: (element) => { - return element.getAttribute('data-umb-tabs-id'); - }, - getUniqueOfModel: (modelEntry) => { - return modelEntry.id; - }, - identifier: 'content-type-tabs-sorter', - itemSelector: '[data-umb-tabs-id]', - containerSelector: '#tabs-group', - disabledItemSelector: '[inherited]', - resolveVerticalDirection: () => { - return false; - }, -}; - -const SORTER_CONFIG_VERTICAL: UmbSorterConfig = { - getUniqueOfElement: (element) => { - return element.getAttribute('data-umb-property-id'); - }, - getUniqueOfModel: (modelEntry) => { - return modelEntry.id; - }, - identifier: 'content-type-property-sorter', - itemSelector: '[data-umb-property-id]', - containerSelector: '#property-list', - disabledItemSelector: '[inherited]', -}; - -export { SORTER_CONFIG_VERTICAL, SORTER_CONFIG_HORIZONTAL }; 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 02cf9f480d..d2951889a3 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 @@ -10,6 +10,7 @@ import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; type EntityType = UmbDocumentTypeDetailModel; export class UmbDocumentTypeWorkspaceContext @@ -197,6 +198,14 @@ export class UmbDocumentTypeWorkspaceContext } } else { await this.structure.save(); + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } this.setIsNew(false); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/manifests.ts index c213e8544f..6468da3c54 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/manifests.ts @@ -1,7 +1,7 @@ import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -92,9 +92,10 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.DocumentType.Save', name: 'Save Document Type Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts index 88cb810286..d018e4d57a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/views/design/document-type-workspace-view-edit.element.ts @@ -21,17 +21,15 @@ import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; @customElement('umb-document-type-workspace-view-edit') export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { - #model: Array = []; #sorter = new UmbSorterController(this, { getUniqueOfElement: (element) => element.getAttribute('data-umb-tabs-id'), - getUniqueOfModel: (modelEntry) => modelEntry.id, + getUniqueOfModel: (tab) => tab.id, identifier: 'document-type-tabs-sorter', itemSelector: 'uui-tab', containerSelector: 'uui-tab-group', disabledItemSelector: '#root-tab', - resolveVerticalDirection: () => false, + resolvePlacement: (args) => args.relatedRect.left + args.relatedRect.width * 0.5 > args.pointerX, onChange: ({ model }) => { - this.#model = model; this._tabs = model; }, onEnd: ({ item }) => { @@ -39,7 +37,7 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... */ - const model = this.#model; + const model = this._tabs ?? []; const newIndex = model.findIndex((entry) => entry.id === item.id); // Doesn't exist in model @@ -105,18 +103,15 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple constructor() { super(); + this.#sorter.disable(); + //TODO: We need to differentiate between local and composition tabs (and hybrids) this._tabsStructureHelper.setIsRoot(true); this._tabsStructureHelper.setContainerChildType('Tab'); this.observe(this._tabsStructureHelper.containers, (tabs) => { this._tabs = tabs; - if (this._sortModeActive) { - this.#sorter.setModel(tabs); - } else { - this.#sorter.setModel([]); - } - + this.#sorter.setModel(tabs); this._createRoutes(); }); @@ -130,9 +125,9 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple (isSorting) => { this._sortModeActive = isSorting; if (isSorting) { - this.#sorter.setModel(this._tabs!); + this.#sorter.enable(); } else { - this.#sorter.setModel([]); + this.#sorter.disable(); } }, '_observeIsSorting', @@ -369,7 +364,7 @@ export class UmbDocumentTypeWorkspaceViewEditElement extends UmbLitElement imple ${this.renderRootTab()} ${repeat( this._tabs, - (tab) => tab.id! + tab.name, + (tab) => tab.id, (tab) => this.renderTab(tab), )} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/index.ts index cd713ce848..37c99b7620 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/index.ts @@ -6,6 +6,8 @@ export function getPropertyValueByAlias(sortOrder: number, item: UmbDocumentColl switch (alias) { case 'createDate': return item.createDate.toLocaleString(); + case 'creator': + return item.creator; case 'entityName': return item.name; case 'entityState': 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 f93958f17e..b05b320192 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,8 +1,8 @@ import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; import type { UmbDocumentItemModel } from '../../repository/index.js'; +import { UMB_DOCUMENT_PICKER_MODAL } from '../../modals/index.js'; import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UMB_DOCUMENT_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; export class UmbDocumentPickerContext extends UmbPickerInputContext { constructor(host: UmbControllerHost) { 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 200afe809a..ac14ad3495 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 @@ -96,6 +96,9 @@ export class UmbInputDocumentElement extends FormControlMixin(UmbLitElement) { // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. this.selectedIds = splitStringToArray(idsString); } + public get value() { + return this.selectedIds.join(','); + } @state() private _editDocumentPath = ''; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/conditions/document-workspace-has-collection.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/conditions/document-workspace-has-collection.condition.ts index 173619135d..8c7670b5d3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/conditions/document-workspace-has-collection.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/conditions/document-workspace-has-collection.condition.ts @@ -6,13 +6,17 @@ import type { UmbConditionControllerArguments, UmbExtensionCondition, } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbDocumentWorkspaceHasCollectionCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor( + host: UmbControllerHost, + args: UmbConditionControllerArguments, + ) { + super(host, args); this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => { this.observe( diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts index c7c6e713aa..5443887e0e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts @@ -14,17 +14,6 @@ const entityActions: Array = [ icon: 'icon-add', label: 'Create', }, - /* removed until we have permissions in place - conditions: [ - { - alias: 'Umb.Condition.UserPermission', - // TODO: investigate why the match property is not typed - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - match: 'Umb.UserPermission.Document.Create', - }, - ], - */ }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/manifests.ts index edc10c62f2..0742dc2c0e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/manifests.ts @@ -5,6 +5,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Document.CultureAndHostnames', name: 'Culture And Hostnames Document Entity Action', weight: 400, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts index 098a4ae8d2..87e1dfeb6e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts @@ -1,10 +1,10 @@ import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js'; import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; +import { UMB_DOCUMENT_PICKER_MODAL } from '../modals/index.js'; import { manifests as createManifests } from './create/manifests.js'; import { manifests as publicAccessManifests } from './public-access/manifests.js'; import { manifests as cultureAndHostnamesManifests } from './culture-and-hostnames/manifests.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; -import { UMB_DOCUMENT_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; const entityActions: Array = [ ...createManifests, @@ -12,11 +12,10 @@ const entityActions: Array = [ ...cultureAndHostnamesManifests, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Document.CreateBlueprint', name: 'Create Document Blueprint Entity Action', - weight: 800, api: () => import('./create-blueprint.action.js'), - kind: 'default', forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { icon: 'icon-blueprint', @@ -32,27 +31,27 @@ const entityActions: Array = [ meta: { moveRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, itemRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, - pickerModelAlias: UMB_DOCUMENT_PICKER_MODAL.toString(), + pickerModelAlias: UMB_DOCUMENT_PICKER_MODAL, }, }, { type: 'entityAction', - alias: 'Umb.EntityAction.Document.Copy', - name: 'Duplicate Document Entity Action', kind: 'duplicate', + alias: 'Umb.EntityAction.Document.Duplicate', + name: 'Duplicate Document Entity Action', forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { duplicateRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, itemRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, - pickerModalAlias: UMB_DOCUMENT_PICKER_MODAL.toString(), + pickerModal: UMB_DOCUMENT_PICKER_MODAL, }, }, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Document.Publish', name: 'Publish Document Entity Action', api: () => import('./publish.action.js'), - kind: 'default', forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { icon: 'icon-globe', @@ -61,10 +60,10 @@ const entityActions: Array = [ }, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Document.Unpublish', name: 'Unpublish Document Entity Action', api: () => import('./unpublish.action.js'), - kind: 'default', forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { icon: 'icon-globe', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/public-access/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/public-access/manifests.ts index dd1ad71c3c..4b43318dd1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/public-access/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/public-access/manifests.ts @@ -1,11 +1,11 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_DOCUMENT_PUBLIC_ACCESS_REPOSITORY_ALIAS } from './repository/manifests.js'; import { UmbDocumentPublicAccessEntityAction } from './public-access.action.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Document.PublicAccess', name: 'Document Permissions Entity Action', api: UmbDocumentPublicAccessEntityAction, @@ -13,7 +13,6 @@ const entityActions: Array = [ meta: { icon: 'icon-lock', label: 'Restrict Public Access', - repositoryAlias: UMB_DOCUMENT_PUBLIC_ACCESS_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/copy/copy.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/copy/copy.action.ts deleted file mode 100644 index bf20777467..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/copy/copy.action.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; - -export class UmbDocumentCopyEntityBulkAction extends UmbEntityBulkActionBase { - async execute() { - console.log('execute bulk copy'); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/duplicate/duplicate.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/duplicate/duplicate.action.ts new file mode 100644 index 0000000000..2f3fdd2ad1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/duplicate/duplicate.action.ts @@ -0,0 +1,7 @@ +import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; + +export class UmbDocumentDuplicateEntityBulkAction extends UmbEntityBulkActionBase { + async execute() { + console.log('execute bulk duplicate'); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts index 2796870034..6842b08013 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts @@ -1,7 +1,7 @@ import type { UmbCollectionBulkActionPermissions } from '../../../core/collection/types.js'; import { UMB_DOCUMENT_COLLECTION_ALIAS } from '../collection/index.js'; import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; -import { UmbDocumentCopyEntityBulkAction } from './copy/copy.action.js'; +import { UmbDocumentDuplicateEntityBulkAction } from './duplicate/duplicate.action.js'; import { UmbDocumentDeleteEntityBulkAction } from './delete/delete.action.js'; import { UmbMoveDocumentEntityBulkAction } from './move/move.action.js'; import { UmbDocumentPublishEntityBulkAction } from './publish/publish.action.js'; @@ -57,12 +57,12 @@ export const manifests: Array = [ }, { type: 'entityBulkAction', - alias: 'Umb.EntityBulkAction.Document.Copy', - name: 'Copy Document Entity Bulk Action', + alias: 'Umb.EntityBulkAction.Document.Duplicate', + name: 'Duplicate Document Entity Bulk Action', weight: 30, - api: UmbDocumentCopyEntityBulkAction, + api: UmbDocumentDuplicateEntityBulkAction, meta: { - label: 'Copy', + label: 'Duplicate...', }, forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], conditions: [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/document-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/document-picker-modal.token.ts similarity index 90% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/token/document-picker-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/document-picker-modal.token.ts index 5a94afe09c..9e04756f58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/document-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/document-picker-modal.token.ts @@ -1,4 +1,4 @@ -import { UmbModalToken } from './modal-token.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; import type { UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; import type { UmbDocumentItemModel } from '@umbraco-cms/backoffice/document'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/index.ts index 1d4ae479e6..5edb21c156 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/modals/index.ts @@ -1,2 +1,3 @@ export * from './variant-picker/index.js'; export * from './pick-document-variant-modal.controller.js'; +export * from './document-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset-context.ts index f42c8124d2..e6582e070b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset-context.ts @@ -3,7 +3,7 @@ import type { UmbNameablePropertyDatasetContext, UmbPropertyDatasetContext } fro import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; -import { map } from '@umbraco-cms/backoffice/external/rxjs'; +import { type Observable, map } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; @@ -83,21 +83,24 @@ export class UmbDocumentPropertyDataContext /** * TODO: Write proper JSDocs here. - * Ideally do not use these methods, its better to communicate directly with the workspace, but if you do not know the property variant id, then this will figure it out for you. So good for externals to set or get values of a property. + * Ideally do not use this method, its better to communicate directly with the workspace, but if you do not know the property variant id, then this will figure it out for you. So good for externals to set or get values of a property. */ - async propertyValueByAlias(propertyAlias: string) { + async propertyValueByAlias( + propertyAlias: string, + ): Promise | undefined> { await this.#workspace.isLoaded(); - return (await this.#workspace.structure.propertyStructureByAlias(propertyAlias)).pipe( - map((property) => - property?.alias - ? this.#workspace.getPropertyValue(property.alias, this.#createPropertyVariantId(property)) - : undefined, - ), - ); + const structure = await this.#workspace.structure.getPropertyStructureByAlias(propertyAlias); + if (structure) { + return this.#workspace.propertyValueByAlias(propertyAlias, this.#createPropertyVariantId(structure)); + } + return; } // TODO: Refactor: Not used currently, but should investigate if we can implement this, to spare some energy. - async propertyValueByAliasAndCulture(propertyAlias: string, propertyVariantId: UmbVariantId) { + async propertyValueByAliasAndCulture( + propertyAlias: string, + propertyVariantId: UmbVariantId, + ): Promise | undefined> { return this.#workspace.propertyValueByAlias(propertyAlias, propertyVariantId); } 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 1239e92c6b..e70230c44a 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 @@ -2,19 +2,15 @@ import type { UmbInputDocumentElement } from '../../components/input-document/in import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import { + UmbPropertyValueChangeEvent, + type UmbPropertyEditorConfigCollection, +} from '@umbraco-cms/backoffice/property-editor'; @customElement('umb-property-editor-ui-document-picker') export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - private _value: Array = []; - - @property({ type: Array }) - public get value(): Array { - return this._value; - } - public set value(value: Array) { - this._value = Array.isArray(value) ? value : value ? [value] : []; - } + @property() + public value?: string; @property({ attribute: false }) public set config(config: UmbPropertyEditorConfigCollection | undefined) { @@ -23,6 +19,9 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl this._limitMin = (validationLimit?.value as any)?.min; this._limitMax = (validationLimit?.value as any)?.max; } + public get config() { + return undefined; + } @state() private _limitMin?: number; @@ -30,8 +29,8 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl private _limitMax?: number; private _onChange(event: CustomEvent) { - this.value = (event.target as UmbInputDocumentElement).selectedIds; - this.dispatchEvent(new CustomEvent('property-value-change')); + this.value = (event.target as UmbInputDocumentElement).selectedIds.join(','); + this.dispatchEvent(new UmbPropertyValueChangeEvent()); } // TODO: Implement mandatory? @@ -39,11 +38,11 @@ export class UmbPropertyEditorUIDocumentPickerElement extends UmbLitElement impl return html` Add + .max=${this._limitMax ?? Infinity}> + Add + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts index 6d7792acd5..2c654126d5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts @@ -1,19 +1,16 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from '../../repository/index.js'; -import { UmbTrashEntityAction } from '@umbraco-cms/backoffice/entity-action'; +import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; export const manifests = [ { type: 'entityAction', + kind: 'trash', alias: 'Umb.EntityAction.Document.Trash', name: 'Trash Document Entity Action', - weight: 900, - api: UmbTrashEntityAction, forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], meta: { - icon: 'icon-trash', - label: 'Trash', - repositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, + itemRepositoryAlias: UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, + trashRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/reload-tree-item-children/manifests.ts index eab0202163..9b90625f81 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/tree/reload-tree-item-children/manifests.ts @@ -4,9 +4,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.DocumentRecycleBin.Tree.ReloadChildrenOf', name: 'Reload Document Recycle Bin Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', forEntityTypes: [UMB_DOCUMENT_RECYCLE_BIN_ENTITY_TYPE, UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/reload-tree-item-children/manifests.ts index 1fd4461844..e67a033cfe 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/tree/reload-tree-item-children/manifests.ts @@ -4,9 +4,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.Document.Tree.ReloadChildrenOf', name: 'Reload Document Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_ROOT_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/input-document-granular-user-permission/input-document-granular-user-permission.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/input-document-granular-user-permission/input-document-granular-user-permission.element.ts index a8bd2b2dcc..7eb0367957 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/input-document-granular-user-permission/input-document-granular-user-permission.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/input-document-granular-user-permission/input-document-granular-user-permission.element.ts @@ -1,13 +1,10 @@ import type { UmbDocumentUserPermissionModel } from '../types.js'; import { UmbDocumentItemRepository, type UmbDocumentItemModel } from '../../repository/index.js'; +import { UMB_DOCUMENT_PICKER_MODAL } from '../../modals/index.js'; import { css, customElement, html, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; -import { - UMB_DOCUMENT_PICKER_MODAL, - UMB_ENTITY_USER_PERMISSION_MODAL, - UMB_MODAL_MANAGER_CONTEXT, -} from '@umbraco-cms/backoffice/modal'; +import { UMB_ENTITY_USER_PERMISSION_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { UmbDeselectedEvent } from '@umbraco-cms/backoffice/event'; import { UmbChangeEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event'; import type { ManifestEntityUserPermission } from '@umbraco-cms/backoffice/extension-registry'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts index 6b75458fdf..c80c994fc7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts @@ -1,18 +1,13 @@ -import type { UmbDocumentWorkspaceContext } from '../document-workspace.context.js'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../index.js'; import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; - -export class UmbDocumentSaveAndPreviewWorkspaceAction extends UmbWorkspaceActionBase { - constructor(host: UmbControllerHost) { - super(host); - } +export class UmbDocumentSaveAndPreviewWorkspaceAction extends UmbWorkspaceActionBase { async execute() { - if (!this.workspaceContext) return; - // TODO: it doesn't get the updated value - const document = this.workspaceContext.getData(); + const workspaceContext = await this.getContext(UMB_DOCUMENT_WORKSPACE_CONTEXT); + //const document = workspaceContext.getData(); // TODO: handle errors - if (!document) return; + //if (!document) return; //this.workspaceContext.repository?.saveAndPreview(); + //Remember to return a promise. } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-publish.action.ts index 0d210cfabc..818ab26a80 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-publish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-publish.action.ts @@ -1,19 +1,9 @@ -import type { UmbDocumentWorkspaceContext } from '../document-workspace.context.js'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document'; import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; - -export class UmbDocumentSaveAndPublishWorkspaceAction extends UmbWorkspaceActionBase { - constructor(host: UmbControllerHost) { - super(host); - } +export class UmbDocumentSaveAndPublishWorkspaceAction extends UmbWorkspaceActionBase { async execute() { - if (!this.workspaceContext) return; - // TODO: Revisit, its unclear to me why we check the data of the context, should not matter IMO. - // TODO: it doesn't get the updated value - //const document = this.workspaceContext.getData(); - // TODO: handle errors - //if (!document) return; - this.workspaceContext.saveAndPublish(); + const workspaceContext = await this.getContext(UMB_DOCUMENT_WORKSPACE_CONTEXT); + return workspaceContext.saveAndPublish(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-schedule.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-schedule.action.ts index 03881c3ccc..1f0f7a6daf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-schedule.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-schedule.action.ts @@ -1,18 +1,10 @@ -import type { UmbDocumentWorkspaceContext } from '../document-workspace.context.js'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../document-workspace.context-token.js'; import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; - -export class UmbSaveAndScheduleDocumentWorkspaceAction extends UmbWorkspaceActionBase { - constructor(host: UmbControllerHost) { - super(host); - } +export class UmbSaveAndScheduleDocumentWorkspaceAction extends UmbWorkspaceActionBase { async execute() { - if (!this.workspaceContext) return; - // TODO: it doesn't get the updated value - const document = this.workspaceContext.getData(); - // TODO: handle errors - if (!document) return; - //this.workspaceContext.repository.saveAndSchedule(); + const workspaceContext = await this.getContext(UMB_DOCUMENT_WORKSPACE_CONTEXT); + //workspaceContext.repository.saveAndSchedule(); + //Remember to return a promise. } } 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 4d82421377..dd8f95e9b7 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 @@ -29,9 +29,10 @@ import { } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; -import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; +import { type Observable, firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; type EntityType = UmbDocumentDetailModel; export class UmbDocumentWorkspaceContext @@ -230,7 +231,10 @@ export class UmbDocumentWorkspaceContext return this.structure.propertyStructureById(propertyId); } - async propertyValueByAlias(propertyAlias: string, variantId?: UmbVariantId) { + async propertyValueByAlias( + propertyAlias: string, + variantId?: UmbVariantId, + ): Promise | undefined> { return this.#currentData.asObservablePart( (data) => data?.values?.find((x) => x?.alias === propertyAlias && (variantId ? variantId.compare(x) : true)) @@ -254,20 +258,16 @@ export class UmbDocumentWorkspaceContext } return undefined; } - async setPropertyValue( - alias: string, - value: UmbDocumentValueModel, - variantId?: UmbVariantId, - ) { + async setPropertyValue(alias: string, value: ValueType, variantId?: UmbVariantId) { variantId ??= UmbVariantId.CreateInvariant(); - const entry = { ...variantId.toObject(), alias, value }; + const entry = { ...variantId.toObject(), alias, value } as UmbDocumentValueModel; const currentData = this.getData(); if (currentData) { const values = appendToFrozenArray( - currentData.values || [], + currentData.values ?? [], entry, - (x) => x.alias === alias && (variantId ? variantId.compare(x) : true), + (x) => x.alias === alias && variantId!.compare(x), ); this.#currentData.update({ values }); @@ -458,6 +458,14 @@ export class UmbDocumentWorkspaceContext console.error('Error saving document', error); throw new Error('Error saving document'); } + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } return selectedVariants; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.element.ts index ffb97061a0..e4768b55e4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.element.ts @@ -18,7 +18,7 @@ export class UmbDocumentWorkspaceElement extends UmbLitElement { _routes: UmbRoute[] = []; public set manifest(manifest: ManifestWorkspace) { - createExtensionApi(manifest, [this]).then((context) => { + createExtensionApi(this, manifest).then((context) => { if (context) { this.#gotWorkspaceContext(context); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts index fe046e216f..9ab6259089 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts @@ -8,7 +8,7 @@ import { UmbDocumentSaveAndPublishWorkspaceAction } from './actions/save-and-pub import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceActionMenuItem, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -82,9 +82,10 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.Document.SaveAndPublish', name: 'Save And Publish Document Workspace Action', weight: 70, @@ -103,6 +104,7 @@ const workspaceActions: Array = [ }, { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.Document.Save', name: 'Save Document Workspace Action', weight: 80, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts index 7d10dfd8dd..031aed1397 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info.element.ts @@ -5,24 +5,23 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; import { UMB_MODAL_MANAGER_CONTEXT, - UMB_TEMPLATE_PICKER_MODAL, UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController, } from '@umbraco-cms/backoffice/modal'; import './document-workspace-view-info-history.element.js'; import './document-workspace-view-info-reference.element.js'; -import type { UmbDocumentWorkspaceContext } from '@umbraco-cms/backoffice/document'; +import type { UmbDocumentVariantModel, UmbDocumentWorkspaceContext } from '@umbraco-cms/backoffice/document'; import { DocumentVariantStateModel, type DocumentUrlInfoModel } from '@umbraco-cms/backoffice/external/backend-api'; import { type UmbDocumentTypeDetailModel, UmbDocumentTypeDetailRepository, } from '@umbraco-cms/backoffice/document-type'; -import { UmbTemplateDetailRepository } from '@umbraco-cms/backoffice/template'; +import { UmbTemplateDetailRepository, UMB_TEMPLATE_PICKER_MODAL } from '@umbraco-cms/backoffice/template'; @customElement('umb-document-workspace-view-info') export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { @state() - private _nodeName = ''; + private _invariantCulture = 'en-US'; @state() private _documentUnique = ''; @@ -31,10 +30,7 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { private _urls?: Array; @state() - private _createDate = 'Unknown'; - - @state() - private _state = DocumentVariantStateModel.DRAFT; + private _createDate?: string; /**Document Type */ @state() @@ -62,6 +58,9 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { @state() private _editTemplatePath = ''; + @state() + private _variants: UmbDocumentVariantModel[] = []; + #workspaceContext?: UmbDocumentWorkspaceContext; #templateRepository = new UmbTemplateDetailRepository(this); @@ -97,8 +96,6 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { private _observeContent() { if (!this.#workspaceContext) return; - this._nodeName = 'TBD, with variants this is not as simple.'; - this.observe(this.#workspaceContext.urls, (urls) => { this._urls = urls; }); @@ -132,34 +129,59 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { '_templateUnique', ); - /** TODO: Doubt this is the right way to get the create date... */ - this.observe(this.#workspaceContext.variants, (variants) => { - this._createDate = Array.isArray(variants) ? variants[0].createDate || 'Unknown' : 'Unknown'; - if (variants[0].state) this._state = variants[0].state; - }); + this.observe( + this.#workspaceContext.variants, + (variants) => { + this._variants = variants; + this.#observeVariants(); + }, + '_variants', + ); } - #renderStateTag() { - switch (this._state) { + #observeVariants() { + // Find the oldest variant + const oldestVariant = this._variants + .filter((v) => !!v.createDate) + .reduce((prev, current) => (prev.createDate! < current.createDate! ? prev : current)); + + this._createDate = oldestVariant?.createDate ?? new Date().toISOString(); + } + + #renderVariantStates() { + return repeat( + this._variants, + (variant) => `${variant.culture}_${variant.segment}`, + (variant) => + html`
    + ${variant.culture ?? this._invariantCulture} ${this.#renderStateTag( + variant, + )} +
    `, + ); + } + + #renderStateTag(variant: UmbDocumentVariantModel) { + switch (variant.state) { case DocumentVariantStateModel.DRAFT: - return html`${this.localize.term('content_unpublished')}`; + return html` + ${this.localize.term('content_unpublished')} + `; case DocumentVariantStateModel.PUBLISHED: - return html`${this.localize.term('content_published')}`; + return html` + ${this.localize.term('content_published')} + `; case DocumentVariantStateModel.PUBLISHED_PENDING_CHANGES: return html`${this.localize.term('content_published')}`; + label=${this.localize.term('content_publishedPendingChanges')}> + ${this.localize.term('content_published')} + `; default: - return html`${this.localize.term('content_published')}`; + return html` + ${this.localize.term('content_published')} + `; } } @@ -198,7 +220,7 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { `; } else { return html``; } @@ -208,7 +230,7 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement { return html`
    Publication Status - ${this.#renderStateTag()} + ${this.#renderVariantStates()}
    Created @@ -219,22 +241,23 @@ export class UmbDocumentWorkspaceViewInfoElement extends UmbLitElement {
    Document Type - +
    Template ${this._templateUnique ? html` - + + + ` : html`(undefined); - public readonly checks = this._checks.asObservable(); + #checks = new UmbBasicState(undefined); + public readonly checks = this.#checks.asObservable(); - private _results = new BehaviorSubject(undefined); - public readonly results = this._results.asObservable(); + #results = new UmbBasicState(undefined); + public readonly results = this.#results.asObservable(); async getGroupChecks(name: string) { const { data } = await tryExecuteAndNotify(this, HealthCheckResource.getHealthCheckGroupByName({ name })); if (data) { - this._checks.next(data); + this.#checks.setValue(data); } else { - this._checks.next(undefined); + this.#checks.setValue(undefined); } } @@ -30,9 +30,9 @@ export class UmbHealthCheckContext extends UmbControllerBase implements UmbApi { const { data } = await tryExecuteAndNotify(this, HealthCheckResource.postHealthCheckGroupByNameCheck({ name })); if (data) { - this._results.next(data); + this.#results.setValue(data); } else { - this._results.next(undefined); + this.#results.setValue(undefined); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/entity-actions/manifests.ts index 618e7e6d60..e1f0f005cb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/entity-actions/manifests.ts @@ -6,9 +6,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.Language.Delete', name: 'Delete Language Entity Action', - kind: 'delete', forEntityTypes: [UMB_LANGUAGE_ENTITY_TYPE], meta: { itemRepositoryAlias: UMB_LANGUAGE_ITEM_REPOSITORY_ALIAS, @@ -17,6 +17,7 @@ const entityActions: Array = [ }, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Language.Create', name: 'Create Language Entity Action', weight: 900, @@ -25,7 +26,6 @@ const entityActions: Array = [ meta: { icon: 'icon-add', label: 'Create', - repositoryAlias: UMB_LANGUAGE_DETAIL_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/manifests.ts index 7f19a16a3c..5922e114fa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/workspace/language/manifests.ts @@ -1,7 +1,7 @@ import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -36,9 +36,10 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.Language.Save', name: 'Save Language Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer.context.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer.context.ts index 91c612ec5a..6dbbe79933 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer.context.ts @@ -100,7 +100,7 @@ export class UmbLogViewerWorkspaceContext extends UmbControllerBase implements U #polling = new UmbObjectState({ enabled: false, interval: 2000 }); polling = this.#polling.asObservable(); - #sortingDirection = new UmbBasicState(DirectionModel.ASCENDING); + #sortingDirection = new UmbBasicState(DirectionModel.DESCENDING); sortingDirection = this.#sortingDirection.asObservable(); #intervalID: number | null = null; diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer/manifests.ts index 8cc5594d4e..7769b94a05 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/logviewer/manifests.ts @@ -1,7 +1,7 @@ import type { ManifestModal, ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -56,7 +56,7 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = []; +const workspaceActions: Array = []; const modals: Array = [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts index 34c4100136..aeaa9df8f1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-search-input.element.ts @@ -42,6 +42,7 @@ export class UmbLogViewerSearchInputElement extends UmbLitElement { @state() private _isQuerySaved = false; + // TODO: Revisit this code, to not use RxJS directly: private inputQuery$ = new Subject(); #logViewerContext?: UmbLogViewerWorkspaceContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/index.ts new file mode 100644 index 0000000000..f215ea19db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/index.ts @@ -0,0 +1 @@ +export * from './input-markdown-editor/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/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 similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/markdown-editor/components/input-markdown-editor/input-markdown.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/index.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/index.ts new file mode 100644 index 0000000000..8a8c2711ca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/index.ts @@ -0,0 +1 @@ +export * from './components/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/manifests.ts new file mode 100644 index 0000000000..81153a42ce --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as propertyEditors } from './property-editors/manifests.js'; + +export const manifests = [...propertyEditors]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/manifests.ts new file mode 100644 index 0000000000..cbd2b41c49 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/manifests.ts @@ -0,0 +1,4 @@ +import { manifest as markdownManifest } from './markdown-editor/manifests.js'; +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [markdownManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/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 similarity index 90% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.element.ts index 16603a50b5..ebdc5796c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/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 @@ -1,5 +1,5 @@ -import type { UmbInputMarkdownElement } from '../../../components/input-markdown-editor/index.js'; -import '../../../components/input-markdown-editor/index.js'; +import type { UmbInputMarkdownElement } from '../../components/input-markdown-editor/index.js'; +import '../../components/input-markdown-editor/index.js'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.stories.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.stories.ts rename to src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.stories.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.test.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/property-editor/uis/markdown-editor/property-editor-ui-markdown-editor.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/markdown-editor/property-editors/markdown-editor/property-editor-ui-markdown-editor.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/umbraco-package.ts new file mode 100644 index 0000000000..ded7b5d107 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/markdown-editor/umbraco-package.ts @@ -0,0 +1,9 @@ +export const name = 'Umbraco.Core.MarkdownEditor'; +export const extensions = [ + { + name: 'Markdown Editor Bundle', + alias: 'Umb.Bundle.MarkdownEditor', + type: 'bundle', + js: () => import('./manifests.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts index f6f590da8e..b17f6f80e4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/components/input-media-type/input-media-type.element.ts @@ -53,18 +53,22 @@ export class UmbInputMediaTypeElement extends FormControlMixin(UmbLitElement) { @property({ type: String, attribute: 'min-message' }) maxMessage = 'This field exceeds the allowed amount of items'; - public get selectedIds(): Array { - return this.#pickerContext.getSelection(); - } + @property({ type: Array }) public set selectedIds(ids: Array) { this.#pickerContext.setSelection(ids); } + public get selectedIds(): Array { + return this.#pickerContext.getSelection(); + } @property() public set value(idsString: string) { // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. this.selectedIds = splitStringToArray(idsString); } + public get value() { + return this.selectedIds.join(','); + } @state() private _items?: Array; @@ -107,13 +111,13 @@ export class UmbInputMediaTypeElement extends FormControlMixin(UmbLitElement) { #renderItems() { if (!this._items) return; return html` - ${repeat( + + ${repeat( this._items, (item) => item.unique, (item) => this.#renderItem(item), - )} + )} + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/manifests.ts index a9dc06e605..950ba4c98f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/manifests.ts @@ -3,13 +3,13 @@ import { UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE, UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, } from '../../entity.js'; -import { UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS } from '../../repository/index.js'; import { UmbCreateMediaTypeEntityAction } from './create.action.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.MediaType.Create', name: 'Create Media Type Entity Action', weight: 1000, @@ -18,7 +18,6 @@ const entityActions: Array = [ meta: { icon: 'icon-add', label: 'Create...', - repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, }, }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/manifests.ts index 1e1773b09b..c80d3fead0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/manifests.ts @@ -1,41 +1,39 @@ import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../entity.js'; import { UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS } from '../repository/index.js'; import { manifests as createManifests } from './create/manifests.js'; -import { UmbMoveEntityAction, UmbDuplicateEntityAction } from '@umbraco-cms/backoffice/entity-action'; +import { UMB_MEDIA_TREE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'move', alias: 'Umb.EntityAction.MediaType.Move', name: 'Move Media Type Entity Action', - weight: 400, - api: UmbMoveEntityAction, forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE], meta: { - icon: 'icon-enter', - label: 'Move', - repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, + moveRepositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, + itemRepositoryAlias: UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS, + pickerModal: UMB_MEDIA_TREE_PICKER_MODAL, }, }, { type: 'entityAction', - alias: 'Umb.EntityAction.MediaType.Copy', - name: 'Copy Media Type Entity Action', - weight: 300, - api: UmbDuplicateEntityAction, + kind: 'duplicate', + alias: 'Umb.EntityAction.MediaType.Duplicate', + name: 'Duplicate Media Type Entity Action', forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE], meta: { - icon: 'icon-documents', - label: 'Copy', - repositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, + duplicateRepositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, + itemRepositoryAlias: UMB_MEDIA_TYPE_ITEM_REPOSITORY_ALIAS, + pickerModal: UMB_MEDIA_TREE_PICKER_MODAL, }, }, { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.MediaType.Delete', name: 'Delete Media Type Entity Action', - kind: 'delete', forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts index 51dc7d38ed..208a10d90d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts @@ -3,6 +3,7 @@ import { manifests as menuItemManifests } from './menu-item/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; +import { manifests as propertyEditorUiManifests } from './property-editors/manifests.js'; export const manifests = [ ...entityActionsManifests, @@ -10,4 +11,5 @@ export const manifests = [ ...repositoryManifests, ...treeManifests, ...workspaceManifests, + ...propertyEditorUiManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/manifests.ts new file mode 100644 index 0000000000..7e8e7ff3b2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/manifests.ts @@ -0,0 +1,4 @@ +import { manifest as mediaTypePickerUI } from './media-type-picker/manifests.js'; +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [mediaTypePickerUI]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/manifests.ts new file mode 100644 index 0000000000..3d30182b52 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/manifests.ts @@ -0,0 +1,23 @@ +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifest: ManifestPropertyEditorUi = { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.MediaTypePicker', + name: 'Media Type Picker Property Editor UI', + js: () => import('./property-editor-ui-media-type-picker.element.js'), + meta: { + label: 'Media Type Picker', + icon: 'icon-media-dashed-line', + group: 'advanced', + settings: { + properties: [ + { + alias: 'showOpenButton', + label: 'Show open button', + description: 'Opens the node in a dialog', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle', + }, + ], + }, + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.element.ts new file mode 100644 index 0000000000..38365a5c35 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.element.ts @@ -0,0 +1,56 @@ +import type { UmbInputMediaTypeElement } from '../../components/index.js'; +import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { + UmbPropertyValueChangeEvent, + type UmbPropertyEditorConfigCollection, +} from '@umbraco-cms/backoffice/property-editor'; + +@customElement('umb-property-editor-ui-media-type-picker') +export class UmbPropertyEditorUIMediaTypePickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { + @property() + public value?: string; + + @property({ attribute: false }) + public set config(config: UmbPropertyEditorConfigCollection | undefined) { + if (config) { + const validationLimit = config.getValueByAlias('validationLimit'); + this._limitMin = validationLimit?.min; + this._limitMax = validationLimit?.max; + } + } + public get config() { + return undefined; + } + + @state() + private _limitMin?: number; + @state() + private _limitMax?: number; + + private _onChange(event: CustomEvent) { + this.value = (event.target as UmbInputMediaTypeElement).selectedIds.join(','); + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + render() { + return html` + + Add + + `; + } +} + +export default UmbPropertyEditorUIMediaTypePickerElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-media-type-picker': UmbPropertyEditorUIMediaTypePickerElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.stories.ts new file mode 100644 index 0000000000..5ceb9df55a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/property-editors/media-type-picker/property-editor-ui-media-type-picker.stories.ts @@ -0,0 +1,15 @@ +import type { Meta, Story } from '@storybook/web-components'; +import type { UmbPropertyEditorUIMediaTypePickerElement } from './property-editor-ui-media-type-picker.element.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +import './property-editor-ui-media-type-picker.element.js'; + +export default { + title: 'Property Editor UIs/Media Type Picker', + component: 'umb-property-editor-ui-media-type-picker', + id: 'umb-property-editor-ui-media-type-picker', +} as Meta; + +export const AAAOverview: Story = () => + html` `; +AAAOverview.storyName = 'Overview'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.data-source.ts index 1e0529824e..cc8f9099c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/item/media-type-item.server.data-source.ts @@ -34,6 +34,6 @@ const mapper = (item: MediaTypeItemResponseModel): UmbMediaTypeItemModel => { return { icon: item.icon || null, name: item.name, - unique: item.name, + unique: item.id, }; }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/manifests.ts index 8c44daaa40..3eee5b1763 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/manifests.ts @@ -1,5 +1,4 @@ import { UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import { UmbDeleteFolderEntityAction, UmbUpdateFolderEntityAction } from '@umbraco-cms/backoffice/tree'; import type { ManifestEntityAction, ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_MEDIA_TYPE_FOLDER_REPOSITORY_ALIAS = 'Umb.Repository.MediaType.Folder'; @@ -14,28 +13,22 @@ const folderRepository: ManifestRepository = { const entityActions: Array = [ { type: 'entityAction', - alias: 'Umb.EntityAction.MediaType.RenameFolder', + kind: 'folderUpdate', + alias: 'Umb.EntityAction.MediaType.Folder.Update', name: 'Rename Media Type Folder Entity Action', - weight: 800, - api: UmbUpdateFolderEntityAction, forEntityTypes: [UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE], meta: { - icon: 'icon-edit', - label: 'Rename Folder...', - repositoryAlias: UMB_MEDIA_TYPE_FOLDER_REPOSITORY_ALIAS, + folderRepositoryAlias: UMB_MEDIA_TYPE_FOLDER_REPOSITORY_ALIAS, }, }, { type: 'entityAction', - alias: 'Umb.EntityAction.MediaType.DeleteFolder', + kind: 'folderDelete', + alias: 'Umb.EntityAction.MediaType.Folder.Delete', name: 'Delete Media Type Folder Entity Action', - weight: 700, - api: UmbDeleteFolderEntityAction, forEntityTypes: [UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE], meta: { - icon: 'icon-trash', - label: 'Delete Folder...', - repositoryAlias: UMB_MEDIA_TYPE_FOLDER_REPOSITORY_ALIAS, + folderRepositoryAlias: UMB_MEDIA_TYPE_FOLDER_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.repository.ts index 0353ed94e4..89f0481a3f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.repository.ts @@ -1,28 +1,11 @@ -import { UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import { UMB_MEDIA_TYPE_TREE_STORE_CONTEXT } from '../media-type-tree.store.js'; import { UmbMediaTypeFolderServerDataSource } from './media-type-folder.server.data-source.js'; -import type { UmbMediaTypeFolderTreeItemModel } from './types.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbFolderModel } from '@umbraco-cms/backoffice/tree'; import { UmbFolderRepositoryBase } from '@umbraco-cms/backoffice/tree'; -export class UmbMediaTypeFolderRepository extends UmbFolderRepositoryBase { +export class UmbMediaTypeFolderRepository extends UmbFolderRepositoryBase { constructor(host: UmbControllerHost) { - super(host, UmbMediaTypeFolderServerDataSource, UMB_MEDIA_TYPE_TREE_STORE_CONTEXT, folderToMediaTypeTreeItemMapper); + super(host, UmbMediaTypeFolderServerDataSource); } } export default UmbMediaTypeFolderRepository; - -const folderToMediaTypeTreeItemMapper = (folder: UmbFolderModel, parentUnique: string | null) => { - const folderTreeItem: UmbMediaTypeFolderTreeItemModel = { - unique: folder.unique, - parentUnique, - name: folder.name, - entityType: UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE, - hasChildren: false, - isFolder: true, - }; - - return folderTreeItem; -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.server.data-source.ts index 86f58e066f..5976006ded 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/folder/media-type-folder.server.data-source.ts @@ -61,7 +61,7 @@ export class UmbMediaTypeFolderServerDataSource implements UmbFolderDataSource { const requestBody = { id: args.unique, - parentId: args.parentUnique, + parentId: args.parentUnique ? { id: args.parentUnique } : null, name: args.name, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/reload-tree-item-children/manifests.ts index cb184922c2..37cb482a42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/tree/reload-tree-item-children/manifests.ts @@ -4,9 +4,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.MediaType.Tree.ReloadChildrenOf', name: 'Reload Media Type Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', forEntityTypes: [UMB_MEDIA_TYPE_ENTITY_TYPE, UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/manifests.ts index a21c2fc8e8..85fabefde5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/manifests.ts @@ -1,6 +1,6 @@ import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -55,9 +55,10 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.MediaType.Save', name: 'Save Media Type Workspace Action', api: UmbSaveWorkspaceAction, 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 381b3822f6..374818b70e 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 @@ -11,6 +11,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; type EntityType = UmbMediaTypeDetailModel; export class UmbMediaTypeWorkspaceContext @@ -161,6 +162,14 @@ export class UmbMediaTypeWorkspaceContext } } else { await this.structure.save(); + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } this.setIsNew(false); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit.element.ts index 3979152bf3..622c2163e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/views/design/media-type-workspace-view-edit.element.ts @@ -27,7 +27,7 @@ const SORTER_CONFIG: UmbSorterConfig = { itemSelector: '[data-umb-tabs-id]', containerSelector: '#tabs-group', disabledItemSelector: '[inherited]', - resolveVerticalDirection: () => { + resolvePlacement: () => { return false; }, }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts index 5b01bdfece..5ba5107c1d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/action/create-media-collection-action.element.ts @@ -78,11 +78,15 @@ export class UmbCreateMediaCollectionActionElement extends UmbLitElement { this._popoverOpen = event.newState === 'open'; } - #getCreateUrl(item: UmbAllowedMediaTypeModel) { + #getCreateUrl(mediaType: UmbAllowedMediaTypeModel) { // TODO: [LK] I need help with this. I don't know what the infinity editor URL should be. + + const mediaEntityType = 'media-root'; // TODO: this should be dynamic return this._useInfiniteEditor - ? `${this._createMediaPath}create/${this._mediaUnique ?? 'null'}/${item.unique}` - : `section/media/workspace/media/create/${this._mediaUnique ?? 'null'}/${item.unique}`; + ? `${this._createMediaPath}create/${this._mediaUnique ?? 'null'}/${mediaType.unique}` + : `section/media/workspace/media/create/parent/${mediaEntityType}/${this._mediaUnique ?? 'null'}/${ + mediaType.unique + }`; } render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts index 1542ef6b55..9174a1f707 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts @@ -40,7 +40,7 @@ export class UmbMediaGridCollectionViewElement extends UmbLitElement { #onOpen(item: UmbMediaCollectionItemModel) { //TODO: Fix when we have dynamic routing - history.pushState(null, '', 'section/media-management/workspace/media/edit/' + item.unique); + history.pushState(null, '', 'section/media/workspace/media/edit/' + item.unique); } #onSelect(item: UmbMediaCollectionItemModel) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts index 2e7f622da6..dfe4b9d75e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-media/input-media.element.ts @@ -96,6 +96,9 @@ export class UmbInputMediaElement extends FormControlMixin(UmbLitElement) { // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. this.selectedIds = splitStringToArray(idsString); } + public get value() { + return this.selectedIds.join(','); + } @state() private _editMediaPath = ''; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/conditions/media-workspace-has-collection.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/conditions/media-workspace-has-collection.condition.ts index 16b25fd8a1..9641679f12 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/conditions/media-workspace-has-collection.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/conditions/media-workspace-has-collection.condition.ts @@ -6,13 +6,17 @@ import type { UmbConditionControllerArguments, UmbExtensionCondition, } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbMediaWorkspaceHasCollectionCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor( + host: UmbControllerHost, + args: UmbConditionControllerArguments, + ) { + super(host, args); this.consumeContext(UMB_MEDIA_WORKSPACE_CONTEXT, (context) => { this.observe( diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/create.action.ts index ed085f0485..403afd86aa 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/create.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/create.action.ts @@ -1,5 +1,4 @@ import { UmbMediaItemRepository } from '../../repository/index.js'; -import type { UmbMediaCreateOptionsModalData } from './media-create-options-modal.token.js'; import { UMB_MEDIA_CREATE_OPTIONS_MODAL } from './media-create-options-modal.token.js'; import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action'; import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; @@ -7,11 +6,8 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; export class UmbCreateMediaEntityAction extends UmbEntityActionBase { - #itemRepository; - constructor(host: UmbControllerHost, args: UmbEntityActionArgs) { super(host, args); - this.#itemRepository = new UmbMediaItemRepository(host); } async execute() { @@ -20,23 +16,18 @@ export class UmbCreateMediaEntityAction extends UmbEntityActionBase { if (this.args.unique) { // get media item to get the doc type id - const { data, error } = await this.#itemRepository.requestItems([this.args.unique]); + const itemRepository = new UmbMediaItemRepository(this._host); + const { data, error } = await itemRepository.requestItems([this.args.unique]); if (error || !data) throw new Error(`Failed to load media item`); mediaItem = data[0]; } - if (!mediaItem) throw new Error(`Failed to load media item`); - - this._openModal({ - parent: { unique: this.args.unique, entityType: this.args.entityType }, - mediaType: { unique: mediaItem.mediaType.unique }, - }); - } - - private async _openModal(modalData: UmbMediaCreateOptionsModalData) { const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); const modalContext = modalManager.open(this, UMB_MEDIA_CREATE_OPTIONS_MODAL, { - data: modalData, + data: { + parent: { unique: this.args.unique, entityType: this.args.entityType }, + mediaType: mediaItem ? { unique: mediaItem.mediaType.unique } : null, + }, }); await modalContext.onSubmit(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/manifests.ts index d409a55428..01adf09894 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/manifests.ts @@ -5,6 +5,7 @@ import type { ManifestModal, ManifestTypes } from '@umbraco-cms/backoffice/exten const entityActions: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Media.Create', name: 'Create Media Entity Action', weight: 1000, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts index fe483e0dd1..0bedee161a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-actions/create/media-create-options-modal.element.ts @@ -23,7 +23,7 @@ export class UmbMediaCreateOptionsModalElement extends UmbModalBaseElement< async firstUpdated() { const mediaUnique = this.data?.parent.unique; - const mediaTypeUnique = this.data?.mediaType.unique || null; + const mediaTypeUnique = this.data?.mediaType?.unique || null; this.#retrieveAllowedMediaTypesOf(mediaTypeUnique); @@ -68,7 +68,7 @@ export class UmbMediaCreateOptionsModalElement extends UmbModalBaseElement< data-id=${ifDefined(mediaType.unique)} href="${`section/media/workspace/media/create/parent/${this.data?.parent.entityType}/${ this.data?.parent.unique ?? 'null' - }}/${mediaType.unique}`}" + }/${mediaType.unique}`}" label="${mediaType.name}" @click=${this.#onNavigate}> > ${mediaType.icon ? html`` : nothing} 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 300ad55299..b2a7bab22e 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 @@ -7,7 +7,7 @@ export interface UmbMediaCreateOptionsModalData { }; mediaType: { unique: string; - }; + } | null; } export interface UmbMediaCreateOptionsModalValue {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/copy/copy.action.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/duplicate/duplicate.action.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/copy/copy.action.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/duplicate/duplicate.action.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/manifests.ts index 3ed11d0162..dc643b3063 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/manifests.ts @@ -1,8 +1,7 @@ import type { UmbCollectionBulkActionPermissions } from '../../../core/collection/types.js'; -import { UMB_MEDIA_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js'; import { UMB_MEDIA_COLLECTION_ALIAS } from '../collection/index.js'; import { UmbMediaMoveEntityBulkAction } from './move/move.action.js'; -import { UmbDuplicateMediaEntityBulkAction } from './copy/copy.action.js'; +import { UmbDuplicateMediaEntityBulkAction } from './duplicate/duplicate.action.js'; import { UmbMediaDeleteEntityBulkAction } from './delete/delete.action.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; import { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/manifests.ts index 43f21ff442..1fb988c2cd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/manifests.ts @@ -3,7 +3,7 @@ import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/extension export const manifest: ManifestPropertyEditorUi = { type: 'propertyEditorUi', alias: 'Umb.PropertyEditorUi.MediaPicker', - name: 'Markdown Editor Property Editor UI', + name: 'Media Picker Property Editor UI', js: () => import('./property-editor-ui-media-picker.element.js'), meta: { label: 'Media Picker', diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts index 46c7a69758..2443efbfa6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/property-editors/media-picker/property-editor-ui-media-picker.element.ts @@ -1,7 +1,10 @@ import type { UmbInputMediaElement } from '../../components/input-media/input-media.element.js'; import '../../components/input-media/input-media.element.js'; import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import { + UmbPropertyValueChangeEvent, + type UmbPropertyEditorConfigCollection, +} from '@umbraco-cms/backoffice/property-editor'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -10,15 +13,8 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; */ @customElement('umb-property-editor-ui-media-picker') export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { - private _value: Array = []; - - @property({ type: Array }) - public get value(): Array { - return this._value; - } - public set value(value: Array | undefined) { - this._value = value || []; - } + @property() + public value?: string; @property({ attribute: false }) public set config(config: UmbPropertyEditorConfigCollection | undefined) { @@ -30,6 +26,9 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme this._limitMin = minMax.min ?? 0; this._limitMax = minMax.max ?? Infinity; } + public get config() { + return undefined; + } @state() private _limitMin: number = 0; @@ -37,19 +36,19 @@ export class UmbPropertyEditorUIMediaPickerElement extends UmbLitElement impleme private _limitMax: number = Infinity; private _onChange(event: CustomEvent) { - this.value = (event.target as UmbInputMediaElement).selectedIds; - this.dispatchEvent(new CustomEvent('property-value-change')); + this.value = (event.target as UmbInputMediaElement).selectedIds.join(','); + this.dispatchEvent(new UmbPropertyValueChangeEvent()); } render() { return html` Add + .max=${this._limitMax}> + Add + `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/detail/media-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/detail/media-detail.server.data-source.ts index 4b10db2fdd..5aba5c5b97 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/detail/media-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/repository/detail/media-detail.server.data-source.ts @@ -76,7 +76,7 @@ export class UmbMediaServerDataSource implements UmbDetailDataSource { return { state: null, @@ -87,7 +87,7 @@ export class UmbMediaServerDataSource implements UmbDetailDataSource ({ + culture: variant.culture || null, + segment: variant.segment || null, + name: variant.name, + })), }; const { data, error } = await tryExecuteAndNotify( diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/reload-tree-item-children/manifests.ts index 038f24f09d..44720b5759 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/tree/reload-tree-item-children/manifests.ts @@ -4,9 +4,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.Media.Tree.ReloadChildrenOf', name: 'Reload Media Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', forEntityTypes: [UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts index a30cf376ef..65dfefdd6c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts @@ -1,6 +1,5 @@ import type { UmbMediaEntityType } from './entity.js'; import type { UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant'; -import type { MediaUrlInfoModel, MediaValueModel } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; export interface UmbMediaDetailModel { @@ -11,9 +10,23 @@ export interface UmbMediaDetailModel { entityType: UmbMediaEntityType; isTrashed: boolean; unique: string; - urls: Array; - values: Array; + urls: Array; + values: Array; variants: Array; } +export interface UmbMediaUrlInfoModel { + culture: string | null; + url: string; +} + +export interface UmbMediaVariantModel extends UmbVariantModel {} + +export interface UmbMediaValueModel { + culture: string | null; + segment: string | null; + alias: string; + value: ValueType; +} + export interface UmbMediaVariantOptionModel extends UmbVariantOptionModel {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts index d346b7695c..e702e90afc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts @@ -2,7 +2,7 @@ import { UMB_MEDIA_WORKSPACE_HAS_COLLECTION_CONDITION } from '../conditions/medi import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -73,9 +73,10 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.Media.Save', name: 'Save Media Workspace Action', api: UmbSaveWorkspaceAction, 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 948265af11..f36ba974b9 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 @@ -2,8 +2,8 @@ import { UmbMediaTypeDetailRepository } from '../../media-types/repository/detai import { UmbMediaPropertyDataContext } from '../property-dataset-context/media-property-dataset-context.js'; import { UMB_MEDIA_ENTITY_TYPE } from '../entity.js'; import { UmbMediaDetailRepository } from '../repository/index.js'; -import type { UmbMediaDetailModel, UmbMediaVariantOptionModel } from '../types.js'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbMediaDetailModel, UmbMediaVariantModel, UmbMediaVariantOptionModel } from '../types.js'; +import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type'; import { UmbEditableWorkspaceContextBase, @@ -12,6 +12,7 @@ import { } from '@umbraco-cms/backoffice/workspace'; import { appendToFrozenArray, + jsonStringComparison, mergeObservables, partialUpdateFrozenArray, UmbArrayState, @@ -21,6 +22,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; type EntityType = UmbMediaDetailModel; export class UmbMediaWorkspaceContext @@ -52,25 +54,59 @@ export class UmbMediaWorkspaceContext readonly contentTypeCollection = this.#currentData.asObservablePart((data) => data?.mediaType.collection); readonly variants = this.#currentData.asObservablePart((data) => data?.variants || []); - readonly variantOptions = mergeObservables([this.variants, this.languages], ([variants, languages]) => { - return languages.map((language) => { - return { - variant: variants.find((x) => x.culture === language.unique), - language, - // TODO: When including segments, this should be updated to include the segment as well. [NL] - unique: language.unique, // This must be a variantId string! - } as UmbMediaVariantOptionModel; - }); - }); + readonly urls = this.#currentData.asObservablePart((data) => data?.urls || []); readonly structure = new UmbContentTypePropertyStructureManager(this, new UmbMediaTypeDetailRepository(this)); + readonly variesByCulture = this.structure.ownerContentTypePart((x) => x?.variesByCulture); + //#variesByCulture?: boolean; + readonly variesBySegment = this.structure.ownerContentTypePart((x) => x?.variesBySegment); + //#variesBySegment?: boolean; + readonly varies = this.structure.ownerContentTypePart((x) => + x ? x.variesByCulture || x.variesBySegment : undefined, + ); + #varies?: boolean; + readonly splitView = new UmbWorkspaceSplitViewManager(); + readonly variantOptions = mergeObservables( + [this.varies, this.variants, this.languages], + ([varies, variants, languages]) => { + // TODO: When including segments, when be aware about the case of segment varying when not culture varying. [NL] + if (varies === true) { + return languages.map((language) => { + return { + variant: variants.find((x) => x.culture === language.unique), + language, + // TODO: When including segments, this object should be updated to include a object for the segment. [NL] + // TODO: When including segments, the unique should be updated to include the segment as well. [NL] + unique: language.unique, // This must be a variantId string! + culture: language.unique, + segment: null, + } as UmbMediaVariantOptionModel; + }); + } else if (varies === false) { + return [ + { + variant: variants.find((x) => x.culture === null), + language: languages.find((x) => x.isDefault), + culture: null, + segment: null, + unique: UMB_INVARIANT_CULTURE, // This must be a variantId string! + } as UmbMediaVariantOptionModel, + ]; + } + return [] as Array; + }, + ); + constructor(host: UmbControllerHost) { super(host, 'Umb.Workspace.Media'); this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique)); + this.observe(this.varies, (varies) => (this.#varies = varies)); + + this.loadLanguages(); } resetState() { @@ -80,6 +116,7 @@ export class UmbMediaWorkspaceContext } async loadLanguages() { + // TODO: If we don't end up having a Global Context for languages, then we should at least change this into using a asObservable which should be returned from the repository. [Nl] const { data } = await this.#languageRepository.requestCollection({}); this.#languages.setValue(data?.items ?? []); } @@ -125,6 +162,11 @@ export class UmbMediaWorkspaceContext return this.getData()?.mediaType.unique; } + // TODO: Check if this is used: + getVaries() { + return this.#varies; + } + variantById(variantId: UmbVariantId) { return this.#currentData.asObservablePart((data) => data?.variants?.find((x) => variantId.compare(x))); } @@ -144,13 +186,15 @@ export class UmbMediaWorkspaceContext } setName(name: string, variantId?: UmbVariantId) { - const oldVariants = this.#currentData.getValue()?.variants || []; - const variants = partialUpdateFrozenArray( - oldVariants, - { name }, - variantId ? (x) => variantId.compare(x) : () => true, - ); - this.#currentData.update({ variants }); + // const oldVariants = this.#currentData.getValue()?.variants || []; + // const variants = partialUpdateFrozenArray( + // oldVariants, + // { name }, + // variantId ? (x) => variantId.compare(x) : () => true, + // ); + // this.#currentData.update({ variants }); + + this.#updateVariantData(variantId ?? UmbVariantId.CreateInvariant(), { name }); } async propertyStructureById(propertyId: string) { @@ -197,6 +241,82 @@ export class UmbMediaWorkspaceContext (x) => x.alias === alias && (variantId ? variantId.compare(x as any) : true), ); this.#currentData.update({ values }); + + // TODO: We should move this type of logic to the act of saving [NL] + this.#updateVariantData(variantId); + } + } + + /* #calculateChangedVariants() { + const persisted = this.#persistedData.getValue(); + const current = this.#currentData.getValue(); + if (!current) throw new Error('Current data is missing'); + + const changedVariants = current?.variants.map((variant) => { + const persistedVariant = persisted?.variants.find((x) => UmbVariantId.Create(variant).compare(x)); + return { + culture: variant.culture, + segment: variant.segment, + equal: persistedVariant ? jsonStringComparison(variant, persistedVariant) : false, + }; + }); + + const changedProperties = current?.values.map((value) => { + const persistedValues = persisted?.values.find((x) => UmbVariantId.Create(value).compare(x)); + return { + culture: value.culture, + segment: value.segment, + equal: persistedValues ? jsonStringComparison(value, persistedValues) : false, + }; + }); + + // calculate the variantIds of those who either have a change in properties or in variants: + return ( + changedVariants + ?.concat(changedProperties ?? []) + .filter((x) => x.equal === false) + .map((x) => new UmbVariantId(x.culture, x.segment)) ?? [] + ); + } */ + + #updateVariantData(variantId: UmbVariantId, update?: Partial) { + const currentData = this.getData(); + if (!currentData) throw new Error('Data is missing'); + if (this.#varies === true) { + // If variant Id is invariant, we don't to have the variant appended to our data. + if (variantId.isInvariant()) return; + const variant = currentData.variants.find((x) => variantId.compare(x)); + const newVariants = appendToFrozenArray( + currentData.variants, + { + name: '', + createDate: null, + updateDate: null, + ...variantId.toObject(), + ...variant, + ...update, + }, + (x) => variantId.compare(x), + ); + this.#currentData.update({ variants: newVariants }); + } else if (this.#varies === false) { + // TODO: Beware about segments, in this case we need to also consider segments, if its allowed to vary by segments. + const invariantVariantId = UmbVariantId.CreateInvariant(); + const variant = currentData.variants.find((x) => invariantVariantId.compare(x)); + // Cause we are invariant, we will just overwrite all variants with this one: + const newVariants = [ + { + name: '', + createDate: null, + updateDate: null, + ...invariantVariantId.toObject(), + ...variant, + ...update, + }, + ]; + this.#currentData.update({ variants: newVariants }); + } else { + throw new Error('Varies by culture is missing'); } } @@ -205,7 +325,6 @@ export class UmbMediaWorkspaceContext if (this.getIsNew()) { if (!this.#parent) throw new Error('Parent is not set'); - await this.repository.create(this.#currentData.value, this.#parent.unique); const value = this.#currentData.value; if ((await this.repository.create(value, this.#parent.unique)).data !== undefined) { @@ -221,6 +340,14 @@ export class UmbMediaWorkspaceContext } } else { await this.repository.save(this.#currentData.value); + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.element.ts index 0a4a1a5337..cbf0f424b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.element.ts @@ -18,7 +18,7 @@ export class UmbMediaWorkspaceElement extends UmbLitElement { _routes: UmbRoute[] = []; public set manifest(manifest: ManifestWorkspace) { - createExtensionApi(manifest, [this]).then((context) => { + createExtensionApi(this, manifest).then((context) => { if (context) { this.#gotWorkspaceContext(context); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/section.manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/section.manifests.ts index acc9e2cd6a..7799804a8e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/section.manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/section.manifests.ts @@ -10,7 +10,7 @@ const section: ManifestSection = { weight: 500, meta: { label: 'Media', - pathname: 'media-management', + pathname: 'media', }, conditions: [], }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts index 4948741a19..f51c138ecb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/manifests.ts @@ -4,6 +4,7 @@ import { manifests as memberSectionManifests } from './member-section/manifests. import { manifests as memberTypeManifests } from './member-type/manifests.js'; import './member/components/index.js'; +import './member-group/components/index.js'; export const manifests = [ ...memberGroupManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/repository/member-group-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/repository/member-group-collection.server.data-source.ts index cd9cefd13e..4ba9a19e08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/repository/member-group-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/repository/member-group-collection.server.data-source.ts @@ -1,9 +1,11 @@ -import type { UmbMemberGroupCollectionFilterModel, UmbMemberGroupCollectionModel } from '../types.js'; +import type { UmbMemberGroupCollectionFilterModel } from '../types.js'; import type { UmbMemberGroupDetailModel } from '../../types.js'; import { UMB_MEMBER_GROUP_ENTITY_TYPE } from '../../entity.js'; import type { UmbCollectionDataSource } from '@umbraco-cms/backoffice/collection'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import type { MemberGroupResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { MemberGroupResource } from '@umbraco-cms/backoffice/external/backend-api'; /** * A data source that fetches the member collection data from the server. @@ -30,29 +32,28 @@ export class UmbMemberGroupCollectionServerDataSource implements UmbCollectionDa * @memberof UmbMemberGroupCollectionServerDataSource */ async getCollection(filter: UmbMemberGroupCollectionFilterModel) { - //const { data, error } = await tryExecuteAndNotify(this.#host, MemberGroupResource.getCollectionMemberGroup(filter)); + const { data, error } = await tryExecuteAndNotify(this.#host, MemberGroupResource.getMemberGroup(filter)); - // TODO => use backend cli when available. - const { data, error } = (await tryExecuteAndNotify( - this.#host, - fetch(`/umbraco/management/api/v1/member-group/filter`), - )) as any; - - if (data) { - const json = await data.json(); // remove this line when backend cli is available - const items = json.items.map((item: any) => { - const model: UmbMemberGroupCollectionModel = { - unique: item.id, - name: item.name, - entityType: UMB_MEMBER_GROUP_ENTITY_TYPE, - }; - - return model; - }); - - return { data: { items, total: json.total } }; + if (error) { + return { error }; } - return { error }; + if (!data) { + return { data: { items: [], total: 0 } }; + } + + const { items, total } = data; + + const mappedItems: Array = items.map((item: MemberGroupResponseModel) => { + const memberDetail: UmbMemberGroupDetailModel = { + entityType: UMB_MEMBER_GROUP_ENTITY_TYPE, + unique: item.id, + name: item.name, + }; + + return memberDetail; + }); + + return { data: { items: mappedItems, total } }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts index 2065ad1bdb..a6d8cd3fc2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/collection/views/table/member-group-table-collection-view.element.ts @@ -70,6 +70,10 @@ export class UmbMemberGroupTableCollectionViewElement extends UmbLitElement { display: flex; flex-direction: column; } + + umb-table { + padding: 0; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/index.ts new file mode 100644 index 0000000000..81f5157a50 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/index.ts @@ -0,0 +1,5 @@ +import './input-member-group/input-member-group.element.js'; + +export * from './input-member-group/input-member-group.element.js'; + +export * from './member-group-picker-modal/member-group-picker-modal.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.context.ts new file mode 100644 index 0000000000..8703697856 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.context.ts @@ -0,0 +1,11 @@ +import type { UmbMemberGroupItemModel } from '../../repository/index.js'; +import { UMB_MEMBER_GROUP_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; +import { UMB_MEMBER_GROUP_PICKER_MODAL } from '../member-group-picker-modal/member-group-picker-modal.token.js'; +import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMemberPickerContext extends UmbPickerInputContext { + constructor(host: UmbControllerHostElement) { + super(host, UMB_MEMBER_GROUP_ITEM_REPOSITORY_ALIAS, UMB_MEMBER_GROUP_PICKER_MODAL); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts new file mode 100644 index 0000000000..3c70cd64f4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/input-member-group/input-member-group.element.ts @@ -0,0 +1,242 @@ +import type { UmbMemberGroupItemModel } from '../../repository/index.js'; +import { UmbMemberPickerContext } from './input-member-group.context.js'; +import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { MemberItemResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { splitStringToArray } from '@umbraco-cms/backoffice/utils'; +import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { type UmbSorterConfig, UmbSorterController } from '@umbraco-cms/backoffice/sorter'; + +const SORTER_CONFIG: UmbSorterConfig = { + getUniqueOfElement: (element) => { + return element.getAttribute('detail'); + }, + getUniqueOfModel: (modelEntry) => { + return modelEntry; + }, + identifier: 'Umb.SorterIdentifier.InputMemberGroup', + itemSelector: 'uui-ref-node', + containerSelector: 'uui-ref-list', +}; + +@customElement('umb-input-member-group') +export class UmbInputMemberGroupElement extends FormControlMixin(UmbLitElement) { + #sorter = new UmbSorterController(this, { + ...SORTER_CONFIG, + onChange: ({ model }) => { + this.selectedIds = model; + }, + }); + + /** + * This is a minimum amount of selected items in this input. + * @type {number} + * @attr + * @default 0 + */ + @property({ type: Number }) + public get min(): number { + return this.#pickerContext.min; + } + public set min(value: number) { + this.#pickerContext.min = value; + } + + /** + * Min validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + minMessage = 'This field need more items'; + + /** + * This is a maximum amount of selected items in this input. + * @type {number} + * @attr + * @default Infinity + */ + @property({ type: Number }) + public get max(): number { + return this.#pickerContext.max; + } + public set max(value: number) { + this.#pickerContext.max = value; + } + + /** + * Max validation message. + * @type {boolean} + * @attr + * @default + */ + @property({ type: String, attribute: 'min-message' }) + maxMessage = 'This field exceeds the allowed amount of items'; + + public get selectedIds(): Array { + return this.#pickerContext.getSelection(); + } + public set selectedIds(ids: Array) { + this.#pickerContext.setSelection(ids); + this.#sorter.setModel(ids); + } + + @property({ type: Boolean }) + showOpenButton?: boolean; + + @property({ type: Array }) + allowedContentTypeIds?: string[] | undefined; + + @property() + public set value(idsString: string) { + // Its with full purpose we don't call super.value, as thats being handled by the observation of the context selection. + this.selectedIds = splitStringToArray(idsString); + } + + @property({ type: Object, attribute: false }) + public filter: (memberGroup: UmbMemberGroupItemModel) => boolean = () => true; + + @state() + private _editMemberGroupPath = ''; + + @state() + private _items?: Array; + + #pickerContext = new UmbMemberPickerContext(this); + + constructor() { + super(); + + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath('member-group') + .onSetup(() => { + return { data: { entityType: 'member-group', preset: {} } }; + }) + .observeRouteBuilder((routeBuilder) => { + this._editMemberGroupPath = routeBuilder({}); + }); + + this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); + this.observe(this.#pickerContext.selectedItems, (selectedItems) => { + this._items = selectedItems; + }); + } + + connectedCallback(): void { + super.connectedCallback(); + + this.addValidator( + 'rangeUnderflow', + () => this.minMessage, + () => !!this.min && this.#pickerContext.getSelection().length < this.min, + ); + + this.addValidator( + 'rangeOverflow', + () => this.maxMessage, + () => !!this.max && this.#pickerContext.getSelection().length > this.max, + ); + } + + protected _openPicker() { + this.#pickerContext.openPicker({ + hideTreeRoot: true, + }); + } + + protected _requestRemoveItem(item: UmbMemberGroupItemModel) { + this.#pickerContext.requestRemoveItem(item.unique!); + } + + protected getFormElement() { + return undefined; + } + + #openPicker() { + this.#pickerContext.openPicker({ + filter: this.filter, + }); + } + + #requestRemoveItem(item: MemberItemResponseModel) { + this.#pickerContext.requestRemoveItem(item.id!); + } + + render() { + return html` ${this.#renderItems()} ${this.#renderAddButton()} `; + } + + #renderItems() { + if (!this._items) return; + return html` + ${repeat( + this._items, + (item) => item.unique, + (item) => this.#renderItem(item), + )} + `; + } + + #renderAddButton() { + if (this.max > 0 && this.selectedIds.length >= this.max) return; + return html``; + } + + #renderItem(item: UmbMemberGroupItemModel) { + if (!item.unique) return; + // TODO: get the correct variant name + const name = item.name; + return html` + + + ${this.#renderOpenButton(item)} + this._requestRemoveItem(item)} + label="${this.localize.term('general_remove')} ${name}"> + ${this.localize.term('general_remove')} + + + + `; + } + + #renderOpenButton(item: UmbMemberGroupItemModel) { + if (!this.showOpenButton) return; + // TODO: get the correct variant name + const name = item.name; + return html` + + + + `; + } + + static styles = [ + css` + #add-button { + width: 100%; + } + + uui-ref-node[drag-placeholder] { + opacity: 0.2; + } + `, + ]; +} + +export default UmbInputMemberGroupElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-member-group': UmbInputMemberGroupElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/index.ts new file mode 100644 index 0000000000..14299dec79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/index.ts @@ -0,0 +1 @@ +export { UMB_MEMBER_GROUP_PICKER_MODAL } from './member-group-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/manifests.ts new file mode 100644 index 0000000000..8c57173863 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/manifests.ts @@ -0,0 +1,8 @@ +export const manifests = [ + { + type: 'modal', + alias: 'Umb.Modal.MemberGroupPicker', + name: 'Member Group Picker Modal', + element: () => import('./member-group-picker-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.element.ts new file mode 100644 index 0000000000..35956318ec --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.element.ts @@ -0,0 +1,83 @@ +import { UmbMemberGroupCollectionRepository } from '../../collection/index.js'; +import type { UmbMemberGroupDetailModel } from '../../types.js'; +import type { + UmbMemberGroupPickerModalValue, + UmbMemberGroupPickerModalData, +} from './member-group-picker-modal.token.js'; +import { html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-member-group-picker-modal') +export class UmbMemberGroupPickerModalElement extends UmbModalBaseElement< + UmbMemberGroupPickerModalData, + UmbMemberGroupPickerModalValue +> { + @state() + private _memberGroups: Array = []; + + #collectionRepository = new UmbMemberGroupCollectionRepository(this); + #selectionManager = new UmbSelectionManager(this); + + connectedCallback(): void { + super.connectedCallback(); + this.#selectionManager.setSelectable(true); + this.#selectionManager.setMultiple(this.data?.multiple ?? false); + this.#selectionManager.setSelection(this.value?.selection ?? []); + } + + async firstUpdated() { + const { data } = await this.#collectionRepository.requestCollection({}); + this._memberGroups = data?.items ?? []; + } + + get #filteredMemberGroups() { + if (this.data?.filter) { + return this._memberGroups.filter(this.data.filter as any); + } else { + return this._memberGroups; + } + } + + #submit() { + this.value = { selection: this.#selectionManager.getSelection() }; + this.modalContext?.submit(); + } + + #close() { + this.modalContext?.reject(); + } + + render() { + return html` + + ${repeat( + this.#filteredMemberGroups, + (item) => item.unique, + (item) => html` + this.#selectionManager.select(item.unique)} + @deselected=${() => this.#selectionManager.deselect(item.unique)} + ?selected=${this.#selectionManager.isSelected(item.unique)}> + + + `, + )} + +
    + + +
    +
    `; + } +} + +export default UmbMemberGroupPickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-group-picker-modal': UmbMemberGroupPickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.token.ts new file mode 100644 index 0000000000..79f5005669 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/components/member-group-picker-modal/member-group-picker-modal.token.ts @@ -0,0 +1,21 @@ +import type { UmbMemberGroupItemModel } from '../../repository/index.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbMemberGroupPickerModalData { + multiple?: boolean; + filter?: (memberGroup: UmbMemberGroupItemModel) => boolean; +} + +export interface UmbMemberGroupPickerModalValue { + selection: Array; +} + +export const UMB_MEMBER_GROUP_PICKER_MODAL = new UmbModalToken< + UmbMemberGroupPickerModalData, + UmbMemberGroupPickerModalValue +>('Umb.Modal.MemberGroupPicker', { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/entity-actions/manifests.ts index 3ae8f036ce..218658c438 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/entity-actions/manifests.ts @@ -8,9 +8,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.MemberGroup.Delete', name: 'Delete Member Group Entity Action ', - kind: 'delete', forEntityTypes: [UMB_MEMBER_GROUP_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_MEMBER_GROUP_DETAIL_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/index.ts index 52416b663f..ec2441254f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/index.ts @@ -1,2 +1,5 @@ +import './components/index.js'; + export * from './repository/index.js'; +export * from './components/index.js'; export type { UmbMemberGroupDetailModel } from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/manifests.ts index e0b615ae9e..b86b2211b7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/manifests.ts @@ -3,6 +3,7 @@ import { manifests as entityActionManifests } from './entity-actions/manifests.j import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as sectionViewManifests } from './section-view/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; +import { manifests as memberGroupPickerModalManifests } from './components/member-group-picker-modal/manifests.js'; export const manifests = [ ...repositoryManifests, @@ -10,4 +11,5 @@ export const manifests = [ ...workspaceManifests, ...sectionViewManifests, ...collectionManifests, + ...memberGroupPickerModalManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/detail/member-group-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/detail/member-group-detail.server.data-source.ts index 35d612fbf0..099082a55e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/detail/member-group-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/repository/detail/member-group-detail.server.data-source.ts @@ -4,6 +4,8 @@ import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import type { CreateMemberGroupRequestModel } from '@umbraco-cms/backoffice/external/backend-api'; +import { MemberGroupResource } from '@umbraco-cms/backoffice/external/backend-api'; /** * A data source for the Member Group that fetches data from the server @@ -48,33 +50,22 @@ export class UmbMemberGroupServerDataSource implements UmbDetailDataSource use backend cli when available. - const { data, error } = (await tryExecuteAndNotify( - this.#host, - fetch(`/umbraco/management/api/v1/member-group/${unique}`), - )) as any; - - const json = await data.json(); // remove this line when backend cli is available if (error || !data) { return { error }; } - // TODO: make data mapper to prevent errors - const memberGroup: UmbMemberGroupDetailModel = { + const MemberGroup: UmbMemberGroupDetailModel = { entityType: UMB_MEMBER_GROUP_ENTITY_TYPE, - unique: json.id, - name: json.name, + unique: data.id, + name: data.name, }; - return { data: memberGroup }; + return { data: MemberGroup }; } /** @@ -86,32 +77,20 @@ export class UmbMemberGroupServerDataSource implements UmbDetailDataSource use backend cli when available. - const { data, error } = (await tryExecuteAndNotify( - this.#host, - fetch(`/umbraco/management/api/v1/member-group`, { method: 'POST', body: JSON.stringify(requestBody) }), - )) as any; - - const newUnqiue = data.headers.get('Umb-Generated-Resource'); // TODO: remove when backend cli is available if (data) { - return this.read(newUnqiue); + return this.read(data.id); } return { error }; @@ -166,22 +145,11 @@ export class UmbMemberGroupServerDataSource implements UmbDetailDataSource = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.MemberGroup.Save', name: 'Save Member Group Workspace Action', api: UmbSaveWorkspaceAction, @@ -34,4 +39,25 @@ const workspaceActions: Array = [ }, ]; -export const manifests = [workspace, ...workspaceActions]; +export const workspaceViews: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.Member.Info', + name: 'Member Workspace info View', + js: () => import('./views/info/member-type-workspace-view-info.element.js'), + weight: 300, + meta: { + label: 'Info', + pathname: 'info', + icon: 'icon-document', + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: UMB_MEMBER_GROUP_WORKSPACE_ALIAS, + }, + ], + }, +]; + +export const manifests = [workspace, ...workspaceActions, ...workspaceViews]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace-editor.element.ts index 385f8c2668..0d183f8eaf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace-editor.element.ts @@ -1,6 +1,6 @@ import { UMB_MEMBER_GROUP_WORKSPACE_CONTEXT } from './member-group-workspace.context.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; @@ -14,6 +14,9 @@ export class UmbMemberGroupWorkspaceEditorElement extends UmbLitElement { @state() private _name = ''; + @state() + private _unique?: string; + #workspaceContext?: typeof UMB_MEMBER_GROUP_WORKSPACE_CONTEXT.TYPE; constructor() { @@ -21,17 +24,14 @@ export class UmbMemberGroupWorkspaceEditorElement extends UmbLitElement { this.consumeContext(UMB_MEMBER_GROUP_WORKSPACE_CONTEXT, (workspaceContext) => { this.#workspaceContext = workspaceContext; - this.#observeName(); + if (!this.#workspaceContext) return; + this.observe(this.#workspaceContext.name, (name) => (this._name = name ?? '')); + this.observe(this.#workspaceContext.unique, (unique) => (this._unique = unique)); }); } - #observeName() { - if (!this.#workspaceContext) return; - this.observe(this.#workspaceContext.name, (name) => (this._name = name ?? '')); - } - // TODO. find a way where we don't have to do this for all Workspaces. - #handleInput(event: UUIInputEvent) { + #onInput(event: UUIInputEvent) { if (event instanceof UUIInputEvent) { const target = event.composedPath()[0] as UUIInputElement; @@ -41,10 +41,29 @@ export class UmbMemberGroupWorkspaceEditorElement extends UmbLitElement { } } + #renderBackButton() { + return html` + + + + `; + } + + #renderActions() { + // Actions only works if we have a valid unique. + if (!this._unique || this.#workspaceContext?.getIsNew()) return nothing; + + return html``; + } + render() { return html` - + ${this.#renderActions()} + `; } @@ -57,6 +76,15 @@ export class UmbMemberGroupWorkspaceEditorElement extends UmbLitElement { width: 100%; height: 100%; } + #header { + display: flex; + gap: var(--uui-size-space-4); + align-items: center; + width: 100%; + } + uui-input { + width: 100%; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context.ts index c3b4f66aeb..f6de381fca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group-workspace.context.ts @@ -13,17 +13,23 @@ export class UmbMemberGroupWorkspaceContext extends UmbEditableWorkspaceContextBase implements UmbSaveableWorkspaceContextInterface { - public readonly detailRepository = new UmbMemberGroupDetailRepository(this); + public readonly repository = new UmbMemberGroupDetailRepository(this); + #getDataPromise?: Promise; #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); + readonly unique = this.#data.asObservablePart((data) => data?.unique); readonly name = this.#data.asObservablePart((data) => data?.name); constructor(host: UmbControllerHost) { super(host, UMB_MEMBER_GROUP_WORKSPACE_ALIAS); } + public isLoaded() { + return this.#getDataPromise; + } + protected resetState(): void { super.resetState(); this.#data.setValue(undefined); @@ -31,7 +37,9 @@ export class UmbMemberGroupWorkspaceContext async load(unique: string) { this.resetState(); - const { data } = await this.detailRepository.requestByUnique(unique); + this.#getDataPromise = this.repository.requestByUnique(unique); + const { data } = await this.#getDataPromise; + if (!data) return undefined; if (data) { this.setIsNew(false); @@ -41,7 +49,8 @@ export class UmbMemberGroupWorkspaceContext async create() { this.resetState(); - const { data } = await this.detailRepository.createScaffold(); + this.#getDataPromise = this.repository.createScaffold(); + const { data } = await this.#getDataPromise; if (data) { this.setIsNew(true); @@ -56,9 +65,9 @@ export class UmbMemberGroupWorkspaceContext if (!data) throw new Error('No data to save'); if (this.getIsNew()) { - await this.detailRepository.create(data); + await this.repository.create(data); } else { - await this.detailRepository.save(data); + await this.repository.save(data); } this.setIsNew(false); @@ -70,7 +79,7 @@ export class UmbMemberGroupWorkspaceContext } getUnique() { - return this.getData()?.unique || ''; + return this.getData()?.unique; } getEntityType() { @@ -86,7 +95,8 @@ export class UmbMemberGroupWorkspaceContext } public destroy(): void { - console.log('destroy'); + this.#data.destroy(); + super.destroy(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/views/info/member-type-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/views/info/member-type-workspace-view-info.element.ts new file mode 100644 index 0000000000..fe7267c904 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/views/info/member-type-workspace-view-info.element.ts @@ -0,0 +1,83 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import type { UmbMemberGroupWorkspaceContext } from '../../member-group-workspace.context.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; + +@customElement('umb-member-type-workspace-view-member-info') +export class UmbMemberTypeWorkspaceViewMemberInfoElement extends UmbLitElement implements UmbWorkspaceViewElement { + private _workspaceContext?: UmbMemberGroupWorkspaceContext; + + @state() + private _unique = ''; + + constructor() { + super(); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, async (context) => { + this._workspaceContext = context as UmbMemberGroupWorkspaceContext; + this._unique = this._workspaceContext.getUnique() ?? ''; + }); + } + + render() { + return html`
    + +
    + +
    +
    +
    + +
    + +
    + Id + ${this._unique} +
    +
    +
    `; + } + + static styles = [ + UmbTextStyles, + css` + #no-properties { + display: flex; + justify-content: center; + align-items: center; + color: var(--uui-color-text); + opacity: 0.5; + } + #left-column { + /* Is there a way to make the wrapped right column grow only when wrapped? */ + flex: 9999 1 500px; + } + #right-column { + flex: 1 1 350px; + display: flex; + flex-direction: column; + gap: var(--uui-size-space-4); + } + :host { + display: flex; + gap: var(--uui-size-space-4); + padding: var(--uui-size-space-4); + } + .property { + display: flex; + flex-direction: column; + } + `, + ]; +} + +export default UmbMemberTypeWorkspaceViewMemberInfoElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-member-info': UmbMemberTypeWorkspaceViewMemberInfoElement; + } +} 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 0915ef4d24..30b7ab1cea 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,11 +1,10 @@ import { UMB_MEMBER_TYPE_PICKER_MODAL } from '../../modal/member-type-picker-modal.token.js'; +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 { constructor(host: UmbControllerHost) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore 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/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/manifests.ts index 82316b04d2..177b69c006 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/manifests.ts @@ -7,6 +7,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.MemberType.Create', name: 'Create Member Type Entity Action', weight: 1000, @@ -15,14 +16,13 @@ const entityActions: Array = [ meta: { icon: 'icon-add', label: 'Create...', - repositoryAlias: UMB_MEMBER_TYPE_DETAIL_REPOSITORY_ALIAS, }, }, { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.MemberType.Delete', name: 'Delete Member Type Entity Action', - kind: 'delete', forEntityTypes: [UMB_MEMBER_TYPE_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_MEMBER_TYPE_DETAIL_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts index 48ebd24242..b67effb41a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/index.ts @@ -1,5 +1,10 @@ import './components/index.js'; +export * from './workspace/index.js'; + export * from './components/index.js'; export * from './repository/index.js'; export * from './entity.js'; +export * from './tree/index.js'; + +export type { UmbMemberTypeDetailModel } from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts index c1be8af104..86ff25b87a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts @@ -42,7 +42,7 @@ export class UmbMemberTypeServerDataSource implements UmbDetailDataSource = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.MemberType.Tree.ReloadChildrenOf', name: 'Reload Member Type Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', forEntityTypes: [UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/index.ts new file mode 100644 index 0000000000..20a2aba255 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/index.ts @@ -0,0 +1 @@ +export * from './member-type-workspace.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts index 7f4b217861..08eb408fe3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/manifests.ts @@ -1,10 +1,12 @@ import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; +export const UMB_MEMBER_TYPE_WORKSPACE_ALIAS = 'Umb.Workspace.MemberType'; + const workspace: ManifestWorkspace = { type: 'workspace', alias: 'Umb.Workspace.MemberType', @@ -15,11 +17,31 @@ const workspace: ManifestWorkspace = { }, }; -const workspaceViews: Array = []; +const workspaceViews: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.MemberType.Design', + name: 'Member Type Workspace Design View', + js: () => import('./views/design/member-type-workspace-view-edit.element.js'), + weight: 1000, + meta: { + label: 'Design', + pathname: 'design', + icon: 'icon-member-dashed-line', + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: UMB_MEMBER_TYPE_WORKSPACE_ALIAS, + }, + ], + }, +]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.MemberType.Save', name: 'Save Member Type Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts index 79daf8f9b0..2573a83051 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts @@ -1,52 +1,166 @@ import { UMB_MEMBER_TYPE_WORKSPACE_CONTEXT } from './member-type-workspace.context.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; +import { UMB_ICON_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { generateAlias } from '@umbraco-cms/backoffice/utils'; @customElement('umb-member-type-workspace-editor') export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement { @state() - private _name = ''; + private _name?: string; + + @state() + private _alias?: string; + + @state() + private _aliasLocked = true; + + @state() + private _icon?: string; + + @state() + private _iconColorAlias?: string; + // TODO: Color should be using an alias, and look up in some dictionary/key/value) of project-colors. #workspaceContext?: typeof UMB_MEMBER_TYPE_WORKSPACE_CONTEXT.TYPE; + private _modalContext?: UmbModalManagerContext; + constructor() { super(); - this.consumeContext(UMB_MEMBER_TYPE_WORKSPACE_CONTEXT, (workspaceContext) => { - this.#workspaceContext = workspaceContext; - this.#observeName(); + this.consumeContext(UMB_MEMBER_TYPE_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + this.#observeMemberType(); + }); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (instance) => { + this._modalContext = instance; }); } - #observeName() { + #observeMemberType() { if (!this.#workspaceContext) return; - this.observe(this.#workspaceContext.name, (name) => (this._name = name ?? '')); + this.observe(this.#workspaceContext.name, (name) => (this._name = name), '_observeName'); + this.observe(this.#workspaceContext.alias, (alias) => (this._alias = alias), '_observeAlias'); + this.observe(this.#workspaceContext.icon, (icon) => (this._icon = icon), '_observeIcon'); + + this.observe( + this.#workspaceContext.isNew, + (isNew) => { + if (isNew) { + // TODO: Would be good with a more general way to bring focus to the name input. + (this.shadowRoot?.querySelector('#name') as HTMLElement)?.focus(); + } + this.removeControllerByAlias('_observeIsNew'); + }, + '_observeIsNew', + ); } - // TODO. find a way where we don't have to do this for all Workspaces. - #handleInput(event: UUIInputEvent) { + // TODO. find a way where we don't have to do this for all workspaces. + #onNameChange(event: UUIInputEvent) { if (event instanceof UUIInputEvent) { const target = event.composedPath()[0] as UUIInputElement; if (typeof target?.value === 'string') { + const oldName = this._name; + const oldAlias = this._alias; + const newName = event.target.value.toString(); + if (this._aliasLocked) { + const expectedOldAlias = generateAlias(oldName ?? ''); + // Only update the alias if the alias matches a generated alias of the old name (otherwise the alias is considered one written by the user.) + if (expectedOldAlias === oldAlias) { + this.#workspaceContext?.set('alias', generateAlias(newName)); + } + } this.#workspaceContext?.setName(target.value); } } } + // TODO. find a way where we don't have to do this for all workspaces. + #onAliasChange(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + this.#workspaceContext?.set('alias', target.value); + } + } + event.stopPropagation(); + } + + #onToggleAliasLock() { + this._aliasLocked = !this._aliasLocked; + } + + private async _handleIconClick() { + const [alias, color] = this._icon?.replace('color-', '')?.split(' ') ?? []; + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + const modalContext = modalManager.open(this, UMB_ICON_PICKER_MODAL, { + value: { + icon: alias, + color: color, + }, + }); + + modalContext?.onSubmit().then((saved) => { + if (saved.icon && saved.color) { + this.#workspaceContext?.set('icon', `${saved.icon} color-${saved.color}`); + } else if (saved.icon) { + this.#workspaceContext?.set('icon', saved.icon); + } + }); + } + render() { return html` - + + +
    + + + Keyboard Shortcuts + + ALT + + + shift + + + k + + +
    `; } static styles = [ - UmbTextStyles, css` :host { display: block; @@ -55,10 +169,31 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement { } #header { - /* TODO: can this be applied from layout slot CSS? */ - margin: 0 var(--uui-size-layout-1); + display: flex; flex: 1 1 auto; } + + #name { + width: 100%; + flex: 1 1 auto; + align-items: center; + } + + #alias-lock { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + #alias-lock uui-icon { + margin-bottom: 2px; + } + + #icon { + font-size: calc(var(--uui-size-layout-3) / 2); + margin-right: var(--uui-size-space-2); + margin-left: calc(var(--uui-size-space-4) * -1); + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts new file mode 100644 index 0000000000..3307c71b19 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace.context-token.ts @@ -0,0 +1,12 @@ +import type { UmbMemberTypeWorkspaceContext } from './member-type-workspace.context.js'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import type { UmbSaveableWorkspaceContextInterface } from '@umbraco-cms/backoffice/workspace'; + +export const UMB_MEMBER_TYPE_WORKSPACE_CONTEXT = new UmbContextToken< + UmbSaveableWorkspaceContextInterface, + UmbMemberTypeWorkspaceContext +>( + 'UmbWorkspaceContext', + undefined, + (context): context is UmbMemberTypeWorkspaceContext => context.getEntityType?.() === 'member-type', +); 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 36ea041df4..a7f13d0df2 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 @@ -4,64 +4,102 @@ import { type UmbSaveableWorkspaceContextInterface, UmbEditableWorkspaceContextBase, } from '@umbraco-cms/backoffice/workspace'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; +import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; +type EntityType = UmbMemberTypeDetailModel; export class UmbMemberTypeWorkspaceContext - extends UmbEditableWorkspaceContextBase + extends UmbEditableWorkspaceContextBase implements UmbSaveableWorkspaceContextInterface { - public readonly detailRepository = new UmbMemberTypeDetailRepository(this); + #isSorting = new UmbBooleanState(undefined); + isSorting = this.#isSorting.asObservable(); + public readonly repository = new UmbMemberTypeDetailRepository(this); #parent?: { entityType: string; unique: string | null }; + #persistedData = new UmbObjectState(undefined); - #data = new UmbObjectState(undefined); - readonly data = this.#data.asObservable(); + // General for content types: + readonly data; + readonly name; + readonly alias; + readonly description; + readonly icon; - readonly name = this.#data.asObservablePart((data) => data?.name); + readonly allowedAtRoot; + readonly variesByCulture; + readonly variesBySegment; + readonly isElement; + readonly allowedContentTypes; + readonly compositions; + + readonly structure = new UmbContentTypePropertyStructureManager(this, this.repository); constructor(host: UmbControllerHost) { super(host, 'Umb.Workspace.MemberType'); + + // General for content types: + this.data = this.structure.ownerContentType; + this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name); + this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias); + this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description); + this.icon = this.structure.ownerContentTypeObservablePart((data) => data?.icon); + this.allowedAtRoot = this.structure.ownerContentTypeObservablePart((data) => data?.allowedAtRoot); + this.variesByCulture = this.structure.ownerContentTypeObservablePart((data) => data?.variesByCulture); + this.variesBySegment = this.structure.ownerContentTypeObservablePart((data) => data?.variesBySegment); + this.isElement = this.structure.ownerContentTypeObservablePart((data) => data?.isElement); + this.allowedContentTypes = this.structure.ownerContentTypeObservablePart((data) => data?.allowedContentTypes); + this.compositions = this.structure.ownerContentTypeObservablePart((data) => data?.compositions); + } + + setIsSorting(isSorting: boolean) { + this.#isSorting.setValue(isSorting); + } + + set(propertyName: PropertyName, value: EntityType[PropertyName]) { + this.structure.updateOwnerContentType({ [propertyName]: value }); } protected resetState(): void { super.resetState(); - this.#data.setValue(undefined); + this.#persistedData.setValue(undefined); + this.#isSorting.setValue(undefined); } async load(unique: string) { + const { data } = await this.structure.loadType(unique); + if (!data) return undefined; this.resetState(); - const { data } = await this.detailRepository.requestByUnique(unique); - if (data) { - this.setIsNew(false); - this.#data.update(data); - } + this.setIsNew(false); + this.setIsSorting(false); + return { data } || undefined; } async create(parent: { entityType: string; unique: string | null }) { this.resetState(); this.#parent = parent; - const { data } = await this.detailRepository.createScaffold(); + const { data } = await this.structure.createScaffold(); + if (!data) return undefined; - if (data) { - this.setIsNew(true); - this.#data.setValue(data); - } - - return { data }; + this.setIsNew(true); + this.setIsSorting(false); + this.#persistedData.setValue(data); + return data; } async save() { const data = this.getData(); - if (!data) throw new Error('No data to save'); + if (data === undefined) throw new Error('Cannot save, no data'); if (this.getIsNew()) { if (!this.#parent) throw new Error('Parent is not set'); - await this.detailRepository.create(data, this.#parent.unique); + await this.repository.create(data, this.#parent.unique); // TODO: this might not be the right place to alert the tree, but it works for now const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); @@ -71,15 +109,28 @@ export class UmbMemberTypeWorkspaceContext }); eventContext.dispatchEvent(event); } else { - await this.detailRepository.save(data); + await this.structure.save(); + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } this.setIsNew(false); this.workspaceComplete(data); } + public destroy(): void { + this.structure.destroy(); + super.destroy(); + } + getData() { - return this.#data.getValue(); + return this.structure.getOwnerContentType(); } getUnique() { @@ -90,12 +141,14 @@ export class UmbMemberTypeWorkspaceContext return 'member-type'; } - getName() { - return this.#data.getValue()?.name; + setName(name: string) { + this.structure.updateOwnerContentType({ name }); } - - setName(name: string | undefined) { - this.#data.update({ name }); + setAlias(alias: string) { + this.structure.updateOwnerContentType({ alias }); + } + setDescription(description: string) { + this.structure.updateOwnerContentType({ description }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts new file mode 100644 index 0000000000..aefd7d051b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-properties.element.ts @@ -0,0 +1,254 @@ +import type { UmbMemberTypeWorkspaceContext } from '../../member-type-workspace.context.js'; +import './member-type-workspace-view-edit-property.element.js'; +import type { UmbMemberTypeDetailModel } from '../../../types.js'; +import type { UmbMemberTypeWorkspacePropertyElement } from './member-type-workspace-view-edit-property.element.js'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { css, html, customElement, property, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbPropertyContainerTypes, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_PROPERTY_SETTINGS_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-member-type-workspace-view-edit-properties') +export class UmbMemberTypeWorkspaceViewEditPropertiesElement extends UmbLitElement { + #model: Array = []; + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => { + return element.getAttribute('data-umb-property-id'); + }, + getUniqueOfModel: (modelEntry) => { + return modelEntry.id; + }, + identifier: 'member-type-property-sorter', + itemSelector: 'umb-member-type-workspace-view-edit-property', + //disabledItemSelector: '[inherited]', + //TODO: Set the property list (sorter wrapper) to inherited, if its inherited + // This is because we don't want to move local properties into an inherited group container. + // Or maybe we do, but we still need to check if the group exists locally, if not, then it needs to be created before we move a property into it. + // TODO: Fix bug where a local property turn into an inherited when moved to a new group container. + containerSelector: '#property-list', + onChange: ({ item, model }) => { + this.#model = model; + this._propertyStructure = model; + }, + onEnd: ({ item }) => { + /** Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder. + * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update + * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... + */ + const model = this.#model; + const newIndex = model.findIndex((entry) => entry.id === item.id); + + // Doesn't exist in model + if (newIndex === -1) return; + + // First in list + if (newIndex === 0 && model.length > 1) { + this._propertyStructureHelper.partialUpdateProperty(item.id, { + sortOrder: model[1].sortOrder - 1, + container: this._containerId ? { id: this._containerId } : null, + }); + return; + } + + // Not first in list + if (newIndex > 0 && model.length > 1) { + const prevItemSortOrder = model[newIndex - 1].sortOrder; + + let weight = 1; + this._propertyStructureHelper.partialUpdateProperty(item.id, { + sortOrder: prevItemSortOrder + weight, + container: this._containerId ? { id: this._containerId } : null, + }); + + // Check for overlaps + model.some((entry, index) => { + if (index <= newIndex) return; + if (entry.sortOrder === prevItemSortOrder + weight) { + weight++; + this._propertyStructureHelper.partialUpdateProperty(entry.id, { sortOrder: prevItemSortOrder + weight }); + } + // Break the loop + return true; + }); + } + }, + }); + + private _containerId: string | undefined; + + @property({ type: String, attribute: 'container-id', reflect: false }) + public get containerId(): string | undefined { + return this._containerId; + } + public set containerId(value: string | undefined) { + if (value === this._containerId) return; + const oldValue = this._containerId; + this._containerId = value; + this.requestUpdate('containerId', oldValue); + } + + @property({ type: String, attribute: 'container-name', reflect: false }) + public get containerName(): string | undefined { + return this._propertyStructureHelper.getContainerName(); + } + public set containerName(value: string | undefined) { + this._propertyStructureHelper.setContainerName(value); + } + + @property({ type: String, attribute: 'container-type', reflect: false }) + public get containerType(): UmbPropertyContainerTypes | undefined { + return this._propertyStructureHelper.getContainerType(); + } + public set containerType(value: UmbPropertyContainerTypes | undefined) { + this._propertyStructureHelper.setContainerType(value); + } + + _propertyStructureHelper = new UmbContentTypePropertyStructureHelper(this); + + @state() + _propertyStructure: Array = []; + + @state() + _ownerMemberTypes?: UmbMemberTypeDetailModel[]; + + @state() + protected _modalRouteNewProperty?: string; + + @state() + _sortModeActive?: boolean; + + constructor() { + super(); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, async (workspaceContext) => { + this._propertyStructureHelper.setStructureManager((workspaceContext as UmbMemberTypeWorkspaceContext).structure); + this.observe( + (workspaceContext as UmbMemberTypeWorkspaceContext).isSorting, + (isSorting) => { + this._sortModeActive = isSorting; + if (isSorting) { + this.#sorter.setModel(this._propertyStructure); + } else { + this.#sorter.setModel([]); + } + }, + '_observeIsSorting', + ); + const docTypesObservable = await this._propertyStructureHelper.ownerDocumentTypes(); + if (!docTypesObservable) return; + this.observe( + docTypesObservable, + (members) => { + this._ownerMemberTypes = members; + }, + 'observeOwnerMemberTypes', + ); + }); + this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => { + this._propertyStructure = propertyStructure; + if (this._sortModeActive) { + this.#sorter.setModel(this._propertyStructure); + } else { + this.#sorter.setModel([]); + } + }); + + // Note: Route for adding a new property + new UmbModalRouteRegistrationController(this, UMB_PROPERTY_SETTINGS_MODAL) + .addAdditionalPath('new-property') + .onSetup(async () => { + const memberTypeId = this._ownerMemberTypes?.find( + (types) => types.containers?.find((containers) => containers.id === this.containerId), + )?.unique; + if (memberTypeId === undefined) return false; + const propertyData = await this._propertyStructureHelper.createPropertyScaffold(this._containerId); + if (propertyData === undefined) return false; + return { data: { documentTypeId: memberTypeId }, value: propertyData }; + }) + .onSubmit((value) => { + if (!value.dataType) { + throw new Error('No data type selected'); + } + this.#addProperty(value as UmbPropertyTypeModel); + }) + .observeRouteBuilder((routeBuilder) => { + this._modalRouteNewProperty = routeBuilder(null); + }); + } + + async #addProperty(propertyData: UmbPropertyTypeModel) { + const propertyPlaceholder = await this._propertyStructureHelper.addProperty(this._containerId); + if (!propertyPlaceholder) return; + + this._propertyStructureHelper.partialUpdateProperty(propertyPlaceholder.id, propertyData); + } + + render() { + return html` +
    + ${repeat( + this._propertyStructure, + (property) => '' + property.container?.id + property.id + '' + property.sortOrder, + (property) => { + // Note: This piece might be moved into the property component + const inheritedFromMember = this._ownerMemberTypes?.find( + (types) => types.containers?.find((containers) => containers.id === property.container?.id), + ); + + return html` + { + this._propertyStructureHelper.partialUpdateProperty(property.id, event.detail); + }} + @property-delete=${() => { + this._propertyStructureHelper.removeProperty(property.id!); + }}> + + `; + }, + )} +
    + + ${!this._sortModeActive + ? html` + Add property + ` + : ''} + `; + } + + static styles = [ + UmbTextStyles, + css` + #add { + width: 100%; + } + + #property-list[sort-mode-active]:not(:has(umb-member-type-workspace-view-edit-property)) { + /* Some height so that the sorter can target the area if the group is empty*/ + min-height: var(--uui-size-layout-1); + } + `, + ]; +} + +export default UmbMemberTypeWorkspaceViewEditPropertiesElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-edit-properties': UmbMemberTypeWorkspaceViewEditPropertiesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-property.element.ts new file mode 100644 index 0000000000..478ec3c53f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-property.element.ts @@ -0,0 +1,486 @@ +import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type'; +import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; +import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { css, html, customElement, property, state, ifDefined, nothing } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal'; +import { + UMB_CONFIRM_MODAL, + UMB_MODAL_MANAGER_CONTEXT, + UMB_PROPERTY_SETTINGS_MODAL, + UMB_WORKSPACE_MODAL, + UmbModalRouteRegistrationController, + umbConfirmModal, +} from '@umbraco-cms/backoffice/modal'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { generateAlias } from '@umbraco-cms/backoffice/utils'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbPropertyTypeModel, UmbPropertyTypeScaffoldModel } from '@umbraco-cms/backoffice/content-type'; + +/** + * @element umb-member-type-workspace-view-edit-property + * @description - Element for displaying a property in an workspace. + * @slot editor - Slot for rendering the Property Editor + */ +@customElement('umb-member-type-workspace-view-edit-property') +export class UmbMemberTypeWorkspacePropertyElement extends UmbLitElement { + private _property?: UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined; + /** + * Property, the data object for the property. + * @type {UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined} + * @attr + * @default undefined + */ + @property({ type: Object }) + public get property(): UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined { + return this._property; + } + public set property(value: UmbPropertyTypeModel | UmbPropertyTypeScaffoldModel | undefined) { + const oldValue = this._property; + this._property = value; + this.#modalRegistration.setUniquePathValue('propertyId', value?.id?.toString()); + this.setDataType(this._property?.dataType?.unique); + this.requestUpdate('property', oldValue); + } + + /** + * Inherited, Determines if the property is part of the main member type thats being edited. + * If true, then the property is inherited from another member type, not a part of the main member type. + * @type {boolean} + * @attr + * @default undefined + */ + @property({ type: Boolean }) + public inherited?: boolean; + + @property({ type: Boolean, reflect: true, attribute: 'sort-mode-active' }) + public sortModeActive = false; + + #dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); + + #modalRegistration; + private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; + + @state() + protected _modalRoute?: string; + + @state() + protected _editMemberTypePath?: string; + + @property() + public get modalRoute() { + return this._modalRoute; + } + + @property({ type: String, attribute: 'owner-member-type-id' }) + public ownerMemberTypeId?: string; + + @property({ type: String, attribute: 'owner-member-type-name' }) + public ownerMemberTypeName?: string; + + @state() + private _dataTypeName?: string; + + async setDataType(dataTypeId: string | undefined) { + if (!dataTypeId) return; + this.#dataTypeDetailRepository.requestByUnique(dataTypeId).then((x) => (this._dataTypeName = x?.data?.name)); + } + + constructor() { + super(); + this.#modalRegistration = new UmbModalRouteRegistrationController(this, UMB_PROPERTY_SETTINGS_MODAL) + .addUniquePaths(['propertyId']) + .onSetup(() => { + const memberTypeId = this.ownerMemberTypeId; + if (memberTypeId === undefined) return false; + const propertyData = this.property; + if (propertyData === undefined) return false; + return { data: { documentTypeId: memberTypeId }, value: propertyData }; + }) + .onSubmit((result) => { + if (!result.dataType) { + throw new Error('No dataType found on property'); + } + this._partialUpdate(result as UmbPropertyTypeModel); + }) + .observeRouteBuilder((routeBuilder) => { + this._modalRoute = routeBuilder(null); + }); + + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath('member-type') + .onSetup(() => { + return { data: { entityType: 'member-type', preset: {} } }; + }) + .observeRouteBuilder((routeBuilder) => { + this._editMemberTypePath = routeBuilder({}); + }); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => { + this._modalManagerContext = context; + }); + } + + _partialUpdate(partialObject: UmbPropertyTypeModel) { + this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject })); + } + + _singleValueUpdate(propertyName: string, value: string | number | boolean | null | undefined) { + const partialObject = {} as any; + partialObject[propertyName] = value; + + this.dispatchEvent(new CustomEvent('partial-property-update', { detail: partialObject })); + } + + @state() + private _aliasLocked = true; + + #onToggleAliasLock() { + this._aliasLocked = !this._aliasLocked; + } + + async #requestRemove(e: Event) { + e.preventDefault(); + e.stopImmediatePropagation(); + if (!this.property || !this.property.id) return; + + await umbConfirmModal(this, { + headline: `${this.localize.term('actions_delete')} property`, + content: html` + Are you sure you want to delete the property ${this.property.name || this.property.id} + +
    `, + confirmLabel: this.localize.term('actions_delete'), + color: 'danger', + }); + + this.dispatchEvent(new CustomEvent('property-delete')); + } + + #onNameChange(event: UUIInputEvent) { + if (event instanceof UUIInputEvent) { + const target = event.composedPath()[0] as UUIInputElement; + + if (typeof target?.value === 'string') { + const oldName = this.property?.name ?? ''; + const oldAlias = this.property?.alias ?? ''; + const newName = event.target.value.toString(); + if (this._aliasLocked) { + const expectedOldAlias = generateAlias(oldName ?? ''); + // Only update the alias if the alias matches a generated alias of the old name (otherwise the alias is considered one written by the user.) + if (expectedOldAlias === oldAlias) { + this._singleValueUpdate('alias', generateAlias(newName ?? '')); + } + } + this._singleValueUpdate('name', newName); + } + } + } + renderSortableProperty() { + if (!this.property) return; + return html` +
    + + ${this.property.name} (${this.property.alias}) +
    + + this._partialUpdate({ sortOrder: parseInt(e.target.value as string) || 0 } as UmbPropertyTypeModel)} + .value=${this.property.sortOrder ?? 0}> + `; + } + + renderEditableProperty() { + if (!this.property) return; + + if (this.sortModeActive) { + return this.renderSortableProperty(); + } else { + return html` + + + ${this.renderPropertyTags()} + + + + + + + `; + } + } + + renderInheritedProperty() { + if (!this.property) return; + + if (this.sortModeActive) { + return this.renderSortableProperty(); + } else { + return html` + +
    + ${this.renderPropertyTags()} + + + ${this.localize.term('contentTypeEditor_inheritedFrom')} + + ${this.ownerMemberTypeName ?? '??'} + + + +
    + `; + } + } + + renderPropertyAlias() { + return this.property + ? html` { + if (e.target) this._singleValueUpdate('alias', (e.target as HTMLInputElement).value); + }}> + + +
    ''} id="alias-lock" slot="prepend"> + +
    +
    ` + : ''; + } + + renderPropertyTags() { + return this.property + ? html`
    + ${this.property.dataType?.unique ? html`${this._dataTypeName}` : nothing} + ${this.property.variesByCulture + ? html` + ${this.localize.term('contentTypeEditor_cultureVariantLabel')} + ` + : nothing} + ${this.property.appearance?.labelOnTop == true + ? html` + ${this.localize.term('contentTypeEditor_displaySettingsLabelOnTop')} + ` + : nothing} + ${this.property.validation.mandatory === true + ? html` + * ${this.localize.term('general_mandatory')} + ` + : nothing} +
    ` + : nothing; + } + + render() { + // TODO: Only show alias on label if user has access to MemberType within settings: + return this.inherited ? this.renderInheritedProperty() : this.renderEditableProperty(); + } + + static styles = [ + UmbTextStyles, + css` + :host(:not([sort-mode-active])) { + display: grid; + grid-template-columns: 200px auto; + column-gap: var(--uui-size-layout-2); + border-bottom: 1px solid var(--uui-color-divider); + padding: var(--uui-size-layout-1) 0; + container-type: inline-size; + } + + :host > div { + grid-column: span 2; + } + + @container (width > 600px) { + :host(:not([orientation='vertical'])) > div { + grid-column: span 1; + } + } + + :host(:first-of-type) { + padding-top: 0; + } + :host(:last-of-type) { + border-bottom: none; + } + + :host([sort-mode-active]) { + position: relative; + display: flex; + padding: 0; + margin-bottom: var(--uui-size-3); + } + + :host([sort-mode-active]:last-of-type) { + margin-bottom: 0; + } + + :host([sort-mode-active]:not([inherited])) { + cursor: grab; + } + + :host([sort-mode-active]) .sortable { + flex: 1; + display: flex; + background-color: var(--uui-color-divider); + align-items: center; + padding: 0 var(--uui-size-3); + gap: var(--uui-size-3); + } + + :host([sort-mode-active]) uui-input { + max-width: 75px; + } + + /* Placeholder style, used when property is being dragged.*/ + :host(.--umb-sorter-placeholder) > * { + visibility: hidden; + } + + :host(.--umb-sorter-placeholder)::after { + content: ''; + inset: 0; + position: absolute; + border: 1px dashed var(--uui-color-divider-emphasis); + border-radius: var(--uui-border-radius); + } + + p { + margin-bottom: 0; + } + + #header { + position: sticky; + top: var(--uui-size-space-4); + height: min-content; + z-index: 2; + } + + #editor { + position: relative; + background-color: var(--uui-color-background); + } + #alias-input, + #label-input, + #description-input { + width: 100%; + } + + #alias-input { + border-color: transparent; + background: var(--uui-color-surface); + } + + #label-input { + font-weight: bold; /* TODO: UUI Input does not support bold text yet */ + --uui-input-border-color: transparent; + } + #label-input input { + font-weight: bold; + --uui-input-border-color: transparent; + } + + #alias-lock { + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + #alias-lock uui-icon { + margin-bottom: 2px; + /* margin: 0; */ + } + #description-input { + --uui-textarea-border-color: transparent; + font-weight: 0.5rem; /* TODO: Cant change font size of UUI textarea yet */ + } + + .types > div uui-icon, + .inherited uui-icon { + vertical-align: sub; + } + + .inherited { + position: absolute; + top: var(--uui-size-space-2); + right: var(--uui-size-space-2); + } + + .types { + position: absolute; + top: var(--uui-size-space-2); + left: var(--uui-size-space-2); + display: flex; + gap: var(--uui-size-space-2); + } + + #editor uui-action-bar { + position: absolute; + top: var(--uui-size-space-2); + right: var(--uui-size-space-2); + display: none; + } + #editor:hover uui-action-bar, + #editor:focus uui-action-bar { + display: block; + } + + a { + color: inherit; + } + + :host([drag-placeholder]) { + opacity: 0.5; + } + :host([drag-placeholder]) uui-input { + visibility: hidden; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-edit-property': UmbMemberTypeWorkspacePropertyElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-tab.element.ts new file mode 100644 index 0000000000..31280fc50a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit-tab.element.ts @@ -0,0 +1,321 @@ +import type { UmbMemberTypeDetailModel } from '../../../types.js'; +import type { UmbMemberTypeWorkspaceContext } from '../../member-type-workspace.context.js'; +import type { UmbMemberTypeWorkspaceViewEditPropertiesElement } from './member-type-workspace-view-edit-properties.element.js'; +import type { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { css, html, customElement, property, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { + UmbContentTypeContainerStructureHelper, + type UmbPropertyTypeContainerModel, +} from '@umbraco-cms/backoffice/content-type'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; + +import './member-type-workspace-view-edit-properties.element.js'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; + +@customElement('umb-member-type-workspace-view-edit-tab') +export class UmbMemberTypeWorkspaceViewEditTabElement extends UmbLitElement { + #model: Array = []; + #sorter = new UmbSorterController( + this, + { + getUniqueOfElement: (element) => + element.querySelector('umb-member-type-workspace-view-edit-properties')!.getAttribute('container-id'), + getUniqueOfModel: (modelEntry) => modelEntry.id, + identifier: 'member-type-container-sorter', + itemSelector: '.container-handle', + containerSelector: '.container-list', + onChange: ({ model }) => { + this._groups = model; + this.#model = model; + }, + onEnd: ({ item }) => { + /** Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder. + * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update + * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... + */ + const model = this.#model; + const newIndex = model.findIndex((entry) => entry.id === item.id); + + // Doesn't exist in model + if (newIndex === -1) return; + + // First in list + if (newIndex === 0 && model.length > 1) { + this._groupStructureHelper.partialUpdateContainer(item.id, { sortOrder: model[1].sortOrder - 1 }); + return; + } + + // Not first in list + if (newIndex > 0 && model.length > 1) { + const prevItemSortOrder = model[newIndex - 1].sortOrder; + + let weight = 1; + this._groupStructureHelper.partialUpdateContainer(item.id, { sortOrder: prevItemSortOrder + weight }); + + // Check for overlaps + model.some((entry, index) => { + if (index <= newIndex) return; + if (entry.sortOrder === prevItemSortOrder + weight) { + weight++; + this._groupStructureHelper.partialUpdateContainer(entry.id, { sortOrder: prevItemSortOrder + weight }); + } + // Break the loop + return true; + }); + } + }, + }, + ); + + private _ownerTabId?: string | null; + + // TODO: get rid of this: + @property({ type: String }) + public get ownerTabId(): string | null | undefined { + return this._ownerTabId; + } + public set ownerTabId(value: string | null | undefined) { + if (value === this._ownerTabId) return; + const oldValue = this._ownerTabId; + this._ownerTabId = value; + this._groupStructureHelper.setOwnerId(value); + this.requestUpdate('ownerTabId', oldValue); + } + + private _tabName?: string | undefined; + + @property({ type: String }) + public get tabName(): string | undefined { + return this._groupStructureHelper.getName(); + } + public set tabName(value: string | undefined) { + if (value === this._tabName) return; + const oldValue = this._tabName; + this._tabName = value; + this._groupStructureHelper.setName(value); + this.requestUpdate('tabName', oldValue); + } + + @state() + private _noTabName?: boolean; + + @property({ type: Boolean }) + public get noTabName(): boolean { + return this._groupStructureHelper.getIsRoot(); + } + public set noTabName(value: boolean) { + this._noTabName = value; + this._groupStructureHelper.setIsRoot(value); + } + + _groupStructureHelper = new UmbContentTypeContainerStructureHelper(this); + + @state() + _groups: Array = []; + + @state() + _hasProperties = false; + + @state() + _sortModeActive?: boolean; + + constructor() { + super(); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, (context) => { + this._groupStructureHelper.setStructureManager((context as UmbMemberTypeWorkspaceContext).structure); + this.observe( + (context as UmbMemberTypeWorkspaceContext).isSorting, + (isSorting) => { + this._sortModeActive = isSorting; + + if (isSorting) { + this.#sorter.setModel(this._groups); + } else { + this.#sorter.setModel([]); + } + }, + '_observeIsSorting', + ); + }); + this.observe(this._groupStructureHelper.containers, (groups) => { + this._groups = groups; + if (this._sortModeActive) { + this.#sorter.setModel(this._groups); + } else { + this.#sorter.setModel([]); + } + this.requestUpdate('_groups'); + }); + this.observe(this._groupStructureHelper.hasProperties, (hasProperties) => { + this._hasProperties = hasProperties; + this.requestUpdate('_hasProperties'); + }); + } + + #onAddGroup = () => { + // Idea, maybe we can gather the sortOrder from the last group rendered and add 1 to it? + this._groupStructureHelper.addContainer(this._ownerTabId); + }; + + render() { + return html` + ${ + this._sortModeActive + ? html`` + : '' + } + + ${ + !this._noTabName + ? html` + + + + ` + : '' + } +
    + ${repeat( + this._groups, + (group) => group.id + '' + group.name + group.sortOrder, + (group) => + html` + ${this.#renderHeader(group)} + + `, + )} +
    + ${this.#renderAddGroupButton()} +
    + `; + } + + #renderHeader(group: UmbPropertyTypeContainerModel) { + const inherited = !this._groupStructureHelper.isOwnerChildContainer(group.id!); + + if (this._sortModeActive) { + return html`
    +
    + + ${this.#renderInputGroupName(group)} +
    + + this._groupStructureHelper.partialUpdateContainer(group.id!, { + sortOrder: parseInt(e.target.value as string) || 0, + })} + .value=${group.sortOrder || 0} + ?disabled=${inherited}> +
    `; + } else { + return html`
    + ${inherited ? html`` : this.#renderInputGroupName(group)} +
    `; + } + } + + #renderInputGroupName(group: UmbPropertyTypeContainerModel) { + return html` { + const newName = (e.target as HTMLInputElement).value; + this._groupStructureHelper.updateContainerName(group.id!, group.parent?.id ?? null, newName); + }}>`; + } + + #renderAddGroupButton() { + if (this._sortModeActive) return; + return html` + ${this.localize.term('contentTypeEditor_addGroup')} + `; + } + + static styles = [ + UmbTextStyles, + css` + [drag-placeholder] { + opacity: 0.5; + } + + [drag-placeholder] > * { + visibility: hidden; + } + + #add { + width: 100%; + } + + #add:first-child { + margin-top: var(--uui-size-layout-1); + } + + uui-box { + margin-bottom: var(--uui-size-layout-1); + } + + [data-umb-group-id] { + display: block; + position: relative; + } + + div[slot='header'] { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + } + + div[slot='header'] > div { + display: flex; + align-items: center; + gap: var(--uui-size-3); + } + + uui-input[type='number'] { + max-width: 75px; + } + + [sort-mode-active] div[slot='header'] { + cursor: grab; + } + + .container-list { + display: grid; + gap: 10px; + } + + #convert-to-tab { + margin-bottom: var(--uui-size-layout-1); + display: flex; + } + `, + ]; +} + +export default UmbMemberTypeWorkspaceViewEditTabElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-edit-tab': UmbMemberTypeWorkspaceViewEditTabElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit.element.ts new file mode 100644 index 0000000000..fea69605e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/views/design/member-type-workspace-view-edit.element.ts @@ -0,0 +1,573 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import type { UmbMemberTypeWorkspaceContext } from '../../member-type-workspace.context.js'; +import type { UmbMemberTypeDetailModel } from '../../../types.js'; +import type { UmbMemberTypeWorkspaceViewEditTabElement } from './member-type-workspace-view-edit-tab.element.js'; +import { css, html, customElement, state, repeat, nothing, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIInputElement, UUIInputEvent, UUITabElement } from '@umbraco-cms/backoffice/external/uui'; +import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import { encodeFolderName } from '@umbraco-cms/backoffice/router'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { + CompositionTypeModel, + type PropertyTypeContainerModelBaseModel, +} from '@umbraco-cms/backoffice/external/backend-api'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbConfirmModalData } from '@umbraco-cms/backoffice/modal'; +import { UMB_CONFIRM_MODAL, UMB_MODAL_MANAGER_CONTEXT, umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbSorterController } from '@umbraco-cms/backoffice/sorter'; + +@customElement('umb-member-type-workspace-view-edit') +export class UmbMemberTypeWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { + #model: Array = []; + #sorter = new UmbSorterController(this, { + getUniqueOfElement: (element) => element.getAttribute('data-umb-tabs-id'), + getUniqueOfModel: (modelEntry) => modelEntry.id, + identifier: 'member-type-tabs-sorter', + itemSelector: 'uui-tab', + containerSelector: 'uui-tab-group', + disabledItemSelector: '#root-tab', + resolvePlacement: () => false, + onChange: ({ model }) => { + this.#model = model; + this._tabs = model; + }, + onEnd: ({ item }) => { + /** Explanation: If the item is the first in list, we compare it to the item behind it to set a sortOrder. + * If it's not the first in list, we will compare to the item in before it, and check the following item to see if it caused overlapping sortOrder, then update + * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... + */ + const model = this.#model; + const newIndex = model.findIndex((entry) => entry.id === item.id); + + // Doesn't exist in model + if (newIndex === -1) return; + + // First in list + if (newIndex === 0 && model.length > 1) { + this._tabsStructureHelper.partialUpdateContainer(item.id, { sortOrder: model[1].sortOrder - 1 }); + return; + } + + // Not first in list + if (newIndex > 0 && model.length > 1) { + const prevItemSortOrder = model[newIndex - 1].sortOrder; + + let weight = 1; + this._tabsStructureHelper.partialUpdateContainer(item.id, { sortOrder: prevItemSortOrder + weight }); + + // Check for overlaps + model.some((entry, index) => { + if (index <= newIndex) return; + if (entry.sortOrder === prevItemSortOrder + weight) { + weight++; + this._tabsStructureHelper.partialUpdateContainer(entry.id, { sortOrder: prevItemSortOrder + weight }); + } + // Break the loop + return true; + }); + } + }, + }); + + //private _hasRootProperties = false; + + @state() + private _hasRootGroups = false; + + @state() + private _routes: UmbRoute[] = []; + + @state() + _tabs?: Array; + + @state() + private _routerPath?: string; + + @state() + private _activePath = ''; + + @state() + private _sortModeActive?: boolean; + + @state() + private _buttonDisabled: boolean = false; + + private _workspaceContext?: UmbMemberTypeWorkspaceContext; + + private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); + + private _modalManagerContext?: typeof UMB_MODAL_MANAGER_CONTEXT.TYPE; + + // @state() + // private _compositionConfiguration?: UmbCompositionPickerModalData; + + constructor() { + super(); + + //TODO: We need to differentiate between local and composition tabs (and hybrids) + + this._tabsStructureHelper.setIsRoot(true); + this._tabsStructureHelper.setContainerChildType('Tab'); + this.observe(this._tabsStructureHelper.containers, (tabs) => { + this._tabs = tabs; + if (this._sortModeActive) { + this.#sorter.setModel(tabs); + } else { + this.#sorter.setModel([]); + } + + this._createRoutes(); + }); + + // _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently. + + this.consumeContext(UMB_WORKSPACE_CONTEXT, (workspaceContext) => { + this._workspaceContext = workspaceContext as UmbMemberTypeWorkspaceContext; + this._tabsStructureHelper.setStructureManager((workspaceContext as UmbMemberTypeWorkspaceContext).structure); + this.observe( + this._workspaceContext.isSorting, + (isSorting) => { + this._sortModeActive = isSorting; + if (isSorting) { + this.#sorter.setModel(this._tabs!); + } else { + this.#sorter.setModel([]); + } + }, + '_observeIsSorting', + ); + + const unique = this._workspaceContext.getUnique(); + + // //TODO Figure out the correct data that needs to be sent to the compositions modal. Do we really have to send isElement, currentPropertyAliases - isn't unique enough? + // this.observe(this._workspaceContext.structure.contentTypes, (contentTypes) => { + // this._compositionConfiguration = { + // unique: unique ?? '', + // selection: contentTypes.map((contentType) => contentType.unique).filter((id) => id !== unique), + // isElement: contentTypes.find((contentType) => contentType.unique === unique)?.isElement ?? false, + // currentPropertyAliases: [], + // }; + // }); + + this._observeRootGroups(); + }); + + this.consumeContext(UMB_MODAL_MANAGER_CONTEXT, (context) => { + this._modalManagerContext = context; + }); + } + + private _observeRootGroups() { + if (!this._workspaceContext) return; + + this.observe( + this._workspaceContext.structure.hasRootContainers('Group'), + (hasRootGroups) => { + this._hasRootGroups = hasRootGroups; + this._createRoutes(); + }, + '_observeGroups', + ); + } + + #changeMode() { + this._workspaceContext?.setIsSorting(!this._sortModeActive); + } + + private _createRoutes() { + if (!this._workspaceContext || !this._tabs) return; + const routes: UmbRoute[] = []; + + if (this._tabs.length > 0) { + this._tabs?.forEach((tab) => { + const tabName = tab.name ?? ''; + routes.push({ + path: `tab/${encodeFolderName(tabName).toString()}`, + component: () => import('./member-type-workspace-view-edit-tab.element.js'), + setup: (component) => { + (component as UmbMemberTypeWorkspaceViewEditTabElement).tabName = tabName; + (component as UmbMemberTypeWorkspaceViewEditTabElement).ownerTabId = + this._tabsStructureHelper.isOwnerContainer(tab.id!) ? tab.id : undefined; + }, + }); + }); + } + + routes.push({ + path: 'root', + component: () => import('./member-type-workspace-view-edit-tab.element.js'), + setup: (component) => { + (component as UmbMemberTypeWorkspaceViewEditTabElement).noTabName = true; + (component as UmbMemberTypeWorkspaceViewEditTabElement).ownerTabId = null; + }, + }); + + if (this._hasRootGroups) { + routes.push({ + path: '', + redirectTo: 'root', + }); + } else if (routes.length !== 0) { + routes.push({ + path: '', + redirectTo: routes[0]?.path, + }); + } + + this._routes = routes; + } + + async #requestRemoveTab(tab: PropertyTypeContainerModelBaseModel | undefined) { + const modalData: UmbConfirmModalData = { + headline: 'Delete tab', + content: html` + Are you sure you want to delete the tab ${tab?.name ?? tab?.id} + +
    + + This will delete all items that doesn't belong to a composition. + +
    `, + confirmLabel: this.localize.term('actions_delete'), + color: 'danger', + }; + + // TODO: If this tab is composed of other tabs, then notify that it will only delete the local tab. + + await umbConfirmModal(this, modalData); + + this.#remove(tab?.id); + } + + #remove(tabId?: string) { + if (!tabId) return; + this._workspaceContext?.structure.removeContainer(null, tabId); + this._tabsStructureHelper?.isOwnerContainer(tabId) + ? window.history.replaceState(null, '', this._routerPath + (this._routes[0]?.path ?? '/root')) + : ''; + } + async #addTab() { + if ( + (this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement) && + (this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement).value === '' + ) { + this.#focusInput(); + return; + } + + const tab = await this._workspaceContext?.structure.createContainer(null, null, 'Tab'); + if (tab) { + const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); + window.history.replaceState(null, '', path); + this.#focusInput(); + } + } + + async #focusInput() { + setTimeout(() => { + (this.shadowRoot?.querySelector('uui-tab[active] uui-input') as UUIInputElement | undefined)?.focus(); + }, 100); + } + + async #tabNameChanged(event: InputEvent, tab: PropertyTypeContainerModelBaseModel) { + if (this._buttonDisabled) this._buttonDisabled = !this._buttonDisabled; + let newName = (event.target as HTMLInputElement).value; + + if (newName === '') { + newName = 'Unnamed'; + (event.target as HTMLInputElement).value = 'Unnamed'; + } + + const changedName = this._workspaceContext?.structure.makeContainerNameUniqueForOwnerContentType( + newName, + 'Tab', + tab.id, + ); + + // Check if it collides with another tab name of this same member-type, if so adjust name: + if (changedName) { + newName = changedName; + (event.target as HTMLInputElement).value = newName; + } + + this._tabsStructureHelper.partialUpdateContainer(tab.id!, { + name: newName, + }); + + // Update the current URL, so we are still on this specific tab: + window.history.replaceState(null, '', this._routerPath + '/tab/' + encodeFolderName(newName)); + } + + async #openCompositionModal() { + throw new Error('Not implemented'); + // const modalContext = this._modalManagerContext?.open(UMB_COMPOSITION_PICKER_MODAL, { + // data: this._compositionConfiguration, + // }); + // await modalContext?.onSubmit(); + + // if (!modalContext?.value) return; + + // const compositionIds = modalContext.getValue().selection; + + // this._workspaceContext?.setCompositions( + // compositionIds.map((unique) => ({ contentType: { unique }, compositionType: CompositionTypeModel.COMPOSITION })), + // ); + } + + render() { + return html` + + + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.absoluteActiveViewPath || ''; + }}> + + + `; + } + + renderAddButton() { + if (this._sortModeActive) return; + return html` + + Add tab + `; + } + + renderActions() { + const sortButtonText = this._sortModeActive + ? this.localize.term('general_reorderDone') + : this.localize.term('general_reorder'); + + return html`
    + + + ${this.localize.term('contentTypeEditor_compositions')} + + + + ${sortButtonText} + +
    `; + } + + renderTabsNavigation() { + if (!this._tabs) return; + + return html`
    + + ${this.renderRootTab()} + ${repeat( + this._tabs, + (tab) => tab.id! + tab.name, + (tab) => this.renderTab(tab), + )} + +
    `; + } + + renderRootTab() { + const rootTabPath = this._routerPath + '/root'; + const rootTabActive = rootTabPath === this._activePath; + return html` + ${this.localize.term('general_content')} + `; + } + + renderTab(tab: PropertyTypeContainerModelBaseModel) { + const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); + const tabActive = path === this._activePath; + const tabInherited = !this._tabsStructureHelper.isOwnerContainer(tab.id!); + + return html` + ${this.renderTabInner(tab, tabActive, tabInherited)} + `; + } + + renderTabInner(tab: PropertyTypeContainerModelBaseModel, tabActive: boolean, tabInherited: boolean) { + if (this._sortModeActive) { + return html`
    + ${tabInherited + ? html`${tab.name!}` + : html` ${tab.name!} + this.#changeOrderNumber(tab, e)}>`} +
    `; + } + + if (tabActive && !tabInherited) { + return html`
    + this.#tabNameChanged(e, tab)} + @blur=${(e: InputEvent) => this.#tabNameChanged(e, tab)} + @input=${() => (this._buttonDisabled = true)} + @focus=${(e: UUIInputEvent) => (e.target.value ? nothing : (this._buttonDisabled = true))}> + ${this.renderDeleteFor(tab)} + +
    `; + } + + if (tabInherited) { + return html`
    ${tab.name!}
    `; + } else { + return html`
    ${tab.name!} ${this.renderDeleteFor(tab)}
    `; + } + } + + #changeOrderNumber(tab: PropertyTypeContainerModelBaseModel, e: UUIInputEvent) { + if (!e.target.value || !tab.id) return; + const sortOrder = Number(e.target.value); + this._tabsStructureHelper.partialUpdateContainer(tab.id, { sortOrder }); + } + + renderDeleteFor(tab: PropertyTypeContainerModelBaseModel) { + return html` this.#requestRemoveTab(tab)} + compact> + + `; + } + + static styles = [ + UmbTextStyles, + css` + #buttons-wrapper { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + align-items: stretch; + } + + :host { + position: relative; + display: flex; + flex-direction: column; + height: 100%; + --uui-tab-background: var(--uui-color-surface); + } + + [drag-placeholder] { + opacity: 0.5; + } + + [drag-placeholder] uui-input { + visibility: hidden; + } + + /* TODO: This should be replaced with a general workspace bar — naming is hard */ + + #header { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; + } + + .flex { + display: flex; + } + + uui-tab-group { + flex-wrap: nowrap; + } + + .content-tab-is-empty { + align-self: center; + border-radius: 3px; + --uui-tab-text: var(--uui-color-text-alt); + border: dashed 1px var(--uui-color-border-emphasis); + } + + uui-tab { + position: relative; + border-left: 1px hidden transparent; + border-right: 1px solid var(--uui-color-border); + } + + .no-edit uui-input { + pointer-events: auto; + } + + .no-edit { + pointer-events: none; + display: inline-flex; + padding-left: var(--uui-size-space-3); + border: 1px solid transparent; + align-items: center; + gap: var(--uui-size-space-3); + } + + .trash { + opacity: 1; + transition: opacity 120ms; + } + + uui-tab:not(:hover, :focus) .trash { + opacity: 0; + transition: opacity 120ms; + } + + uui-input:not(:focus, :hover) { + border: 1px solid transparent; + } + + .inherited { + vertical-align: sub; + } + + [drag-placeholder] { + opacity: 0.2; + } + `, + ]; +} + +export default UmbMemberTypeWorkspaceViewEditElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-type-workspace-view-edit': UmbMemberTypeWorkspaceViewEditElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts new file mode 100644 index 0000000000..6aaed94120 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/create-member-collection-action.element.ts @@ -0,0 +1,97 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { html, customElement, state, repeat, css, until, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbMemberTypeTreeRepository } from '@umbraco-cms/backoffice/member-type'; + +@customElement('umb-create-member-collection-action') +export class UmbCreateDocumentCollectionActionElement extends UmbLitElement { + @state() + private _options: Array<{ label: string; unique: string; icon: string }> = []; + + #memberTypeTreeRepository = new UmbMemberTypeTreeRepository(this); + #optionRequestPromise?: Promise; + + async #getOptions() { + //TODO: Should we use the tree repository or make a collection repository? + //TODO: And how would we get all the member types? + //TODO: This only works because member types can't have folders. + const { data } = await this.#memberTypeTreeRepository.requestRootTreeItems({}); + if (!data) return; + + this._options = data.items.map((item) => { + return { + label: item.name, + unique: item.unique, + icon: item.icon || '', + }; + }); + this.requestUpdate(); + } + + #onButtonClick = async () => { + if (this._options.length > 0) return; + + await this.#getOptions(); + + if (this._options.length === 1) { + history.pushState({}, '', `section/member-management/workspace/member/create/${this._options[0].unique}`); + } + }; + + async #renderOptions() { + await this.#optionRequestPromise; + + // If we only have one option, we don't need to render the popover. We will go directly to it on click. + if (this._options.length === 1) return nothing; + + return html` + ${repeat( + this._options, + (option) => option.unique, + (option) => + html` + + ${option.label} + `, + )} + `; + } + + render() { + return html` + + +
    ${until(this.#renderOptions(), html``)}
    +
    + `; + } + + static styles = [ + UmbTextStyles, + css` + #popover-content { + background-color: var(--uui-color-surface); + box-shadow: var(--uui-shadow-depth-3); + border-radius: var(--uui-border-radius); + display: flex; + flex-direction: column; + --uui-button-content-align: left; + } + `, + ]; +} + +export default UmbCreateDocumentCollectionActionElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-create-member-collection-action': UmbCreateDocumentCollectionActionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts index 516aa7f1b5..f84bbe5029 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts @@ -3,20 +3,22 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const createManifest: ManifestTypes = { type: 'collectionAction', - kind: 'button', name: 'Create Member Collection Action', + kind: 'button', alias: 'Umb.CollectionAction.Member.Create', weight: 200, meta: { label: 'Create', href: 'section/member-management/workspace/member/create/member-type-1-id', // TODO: remove hardcoded member type id }, + js: () => import('./create-member-collection-action.element.js'), conditions: [ { alias: UMB_COLLECTION_ALIAS_CONDITION, match: 'Umb.Collection.Member', }, ], + // element: () => import('./create-member-collection-action.element.js'), }; export const manifests = [createManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/manifests.ts index 91e14b657e..46fa9d358a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/manifests.ts @@ -8,9 +8,10 @@ export const UMB_MEMBER_COLLECTION_ALIAS = 'Umb.Collection.Member'; const collectionManifest: ManifestTypes = { type: 'collection', - kind: 'default', alias: UMB_MEMBER_COLLECTION_ALIAS, name: 'Member Collection', + api: () => import('./member-collection.context.js'), + element: () => import('./member-collection.element.js'), meta: { repositoryAlias: UMB_MEMBER_COLLECTION_REPOSITORY_ALIAS, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts new file mode 100644 index 0000000000..073675600f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection-header.element.ts @@ -0,0 +1,133 @@ +import { UmbMemberTypeTreeRepository } from '../../member-type/tree/member-type-tree.repository.js'; +import type { UmbMemberTypeItemModel } from '../../member-type/repository/item/types.js'; +import type { UmbMemberCollectionContext } from './member-collection.context.js'; +import { css, customElement, html, ifDefined, repeat, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; + +@customElement('umb-member-collection-header') +export class UmbMemberCollectionHeaderElement extends UmbLitElement { + @state() + private _contentTypes: Array = []; + + #inputTimer?: NodeJS.Timeout; + #inputTimerAmount = 300; + + #collectionContext?: UmbMemberCollectionContext; + // TODO: Should we make a collection repository for member types? + #contentTypeRepository = new UmbMemberTypeTreeRepository(this); + + @state() + private _selectedContentTypeUnique?: string; + + constructor() { + super(); + + this.consumeContext(UMB_DEFAULT_COLLECTION_CONTEXT, (instance) => { + this.#collectionContext = instance as UmbMemberCollectionContext; + }); + + this.#requestContentTypes(); + } + + async #requestContentTypes() { + const { data } = await this.#contentTypeRepository.requestRootTreeItems({}); + + if (data) { + this._contentTypes = data.items.map((item) => ({ + unique: item.unique, + name: item.name, + icon: item.icon || '', + entityType: item.entityType, + })); + } + } + + get #getContentTypeFilterLabel() { + if (!this._selectedContentTypeUnique) return this.localize.term('general_all') + ' Member types'; + + return ( + this._contentTypes.find((type) => type.unique === this._selectedContentTypeUnique)?.name || + this.localize.term('general_all') + ); + } + + #onSearch(event: InputEvent) { + const target = event.target as HTMLInputElement; + const filter = target.value || ''; + clearTimeout(this.#inputTimer); + this.#inputTimer = setTimeout(() => this.#collectionContext?.setFilter({ filter }), this.#inputTimerAmount); + } + + #onContentTypeFilterChange(contentTypeUnique: string) { + this._selectedContentTypeUnique = contentTypeUnique; + this.#collectionContext?.setMemberTypeFilter(contentTypeUnique); + } + + render() { + return html` + + ${this.#renderContentTypeFilter()} `; + } + + #renderContentTypeFilter() { + return html` + + ${this.#getContentTypeFilterLabel} + + + `; + } + static styles = [ + css` + :host { + height: 100%; + width: 100%; + display: flex; + justify-content: space-between; + white-space: nowrap; + gap: var(--uui-size-space-5); + align-items: center; + } + + #dropdown-layout { + display: flex; + flex-direction: column; + --uui-button-content-align: left; + } + + uui-input { + width: 100%; + } + `, + ]; +} + +export default UmbMemberCollectionHeaderElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-collection-header': UmbMemberCollectionHeaderElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts new file mode 100644 index 0000000000..20d8614c73 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.context.ts @@ -0,0 +1,25 @@ +import type { UmbMemberDetailModel } from '../types.js'; +import type { UmbMemberCollectionFilterModel } from './types.js'; +import { UMB_MEMBER_TABLE_COLLECTION_VIEW_ALIAS } from './views/manifests.js'; +import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMemberCollectionContext extends UmbDefaultCollectionContext< + UmbMemberDetailModel, + UmbMemberCollectionFilterModel +> { + constructor(host: UmbControllerHostElement) { + super(host, UMB_MEMBER_TABLE_COLLECTION_VIEW_ALIAS); + } + + /** + * Sets the member type filter for the collection and refreshes the collection. + * @param {Array} selection + * @memberof UmbMemberCollectionContext + */ + setMemberTypeFilter(selection: string) { + this.setFilter({ memberTypeId: selection }); + } +} + +export default UmbMemberCollectionContext; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts new file mode 100644 index 0000000000..7abebe8305 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/member-collection.element.ts @@ -0,0 +1,19 @@ +import { html, customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; + +import './member-collection-header.element.js'; + +@customElement('umb-member-collection') +export class UmbMemberCollectionElement extends UmbCollectionDefaultElement { + protected renderToolbar() { + return html` `; + } +} + +export default UmbMemberCollectionElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-collection': UmbMemberCollectionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.repository.ts index 3cf5f4de08..62ec20d00d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.repository.ts @@ -1,21 +1,29 @@ +import { UmbMemberRepositoryBase } from '../../repository/member-repository-base.js'; import type { UmbMemberCollectionFilterModel } from '../types.js'; import { UmbMemberCollectionServerDataSource } from './member-collection.server.data-source.js'; import type { UmbMemberCollectionDataSource } from './types.js'; import type { UmbCollectionRepository } from '@umbraco-cms/backoffice/collection'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbMemberCollectionRepository implements UmbCollectionRepository { +export class UmbMemberCollectionRepository extends UmbMemberRepositoryBase implements UmbCollectionRepository { #collectionSource: UmbMemberCollectionDataSource; constructor(host: UmbControllerHost) { + super(host); this.#collectionSource = new UmbMemberCollectionServerDataSource(host); } - async requestCollection(filter: UmbMemberCollectionFilterModel) { - return this.#collectionSource.getCollection(filter); - } + async requestCollection(filter: UmbMemberCollectionFilterModel = { skip: 0, take: 100 }) { + await this.init; - destroy(): void {} + const { data, error } = await this.#collectionSource.getCollection(filter); + + if (data) { + this.detailStore!.appendItems(data.items); + } + + return { data, error, asObservable: () => this.detailStore!.all() }; + } } export default UmbMemberCollectionRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.server.data-source.ts index 5c07640b3f..c1bd0e153a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/repository/member-collection.server.data-source.ts @@ -1,10 +1,12 @@ -import type { UmbMemberCollectionFilterModel, UmbMemberCollectionModel } from '../types.js'; -import type { UmbMemberDetailModel } from '../../types.js'; +import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import type { UmbMemberDetailModel, UmbMemberValueModel } from '../../types.js'; import { UMB_MEMBER_ENTITY_TYPE } from '../../entity.js'; +import type { UmbMemberCollectionFilterModel } from '../types.js'; import type { UmbCollectionDataSource } from '@umbraco-cms/backoffice/collection'; +import type { MemberResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { MemberResource } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; /** * A data source that fetches the member collection data from the server. @@ -31,28 +33,40 @@ export class UmbMemberCollectionServerDataSource implements UmbCollectionDataSou * @memberof UmbMemberCollectionServerDataSource */ async getCollection(filter: UmbMemberCollectionFilterModel) { - //const { data, error } = await tryExecuteAndNotify(this.#host, MemberResource.getCollectionMember(filter)); + const { data, error } = await tryExecuteAndNotify(this.#host, MemberResource.getFilterMember(filter)); - const { data, error } = await tryExecuteAndNotify( - this.#host, - fetch('/umbraco/management/api/v1/collection/member'), - ); - - if (data) { - const json = await data.json(); - const items = json.items.map((item: any) => { - const model: UmbMemberCollectionModel = { - unique: item.id, - entityType: UMB_MEMBER_ENTITY_TYPE, - variants: item.variants, - }; - - return model; - }); - - return { data: { items, total: json.total } }; + if (error) { + return { error }; } - return { error }; + if (!data) { + return { data: { items: [], total: 0 } }; + } + + const { items, total } = data; + + const mappedItems: Array = items.map((item: MemberResponseModel) => { + const memberDetail: UmbMemberDetailModel = { + entityType: UMB_MEMBER_ENTITY_TYPE, + email: item.email, + variants: item.variants as UmbVariantModel[], + unique: item.id, + lastLoginDate: item.lastLoginDate || null, + lastLockoutDate: item.lastLockoutDate || null, + lastPasswordChangeDate: item.lastPasswordChangeDate || null, + failedPasswordAttempts: item.failedPasswordAttempts, + isApproved: item.isApproved, + isLockedOut: item.isLockedOut, + groups: item.groups, + isTwoFactorEnabled: item.isTwoFactorEnabled, + memberType: { unique: item.memberType.id }, + username: item.username, + values: item.values as UmbMemberValueModel[], + }; + + return memberDetail; + }); + + return { data: { items: mappedItems, total } }; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/types.ts index ff3ffe3bbc..bb3b83cc3b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/types.ts @@ -3,6 +3,8 @@ import type { UmbMemberEntityType } from '../entity.js'; export interface UmbMemberCollectionFilterModel { skip?: number; take?: number; + memberTypeId?: string; + filter?: string; } export interface UmbMemberCollectionModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts index 3b30d688c3..ce68591370 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/views/table/member-table-collection-view.element.ts @@ -1,9 +1,9 @@ +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbMemberCollectionModel } from '../../types.js'; import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; import { UMB_DEFAULT_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection'; import type { UmbTableColumn, UmbTableConfig, UmbTableItem } from '@umbraco-cms/backoffice/components'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-member-table-collection-view') @@ -71,6 +71,9 @@ export class UmbMemberTableCollectionViewElement extends UmbLitElement { display: flex; flex-direction: column; } + umb-table { + padding: 0; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/index.ts index 90f47707f8..e1cb5f49af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/index.ts @@ -1,3 +1,5 @@ import './input-member/input-member.element.js'; export * from './input-member/input-member.element.js'; + +export * from './member-picker-modal/member-picker-modal.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.context.ts new file mode 100644 index 0000000000..feb383067c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.context.ts @@ -0,0 +1,11 @@ +import type { UmbMemberItemModel } from '../../repository/index.js'; +import { UMB_MEMBER_ITEM_REPOSITORY_ALIAS } from '../../repository/index.js'; +import { UMB_MEMBER_PICKER_MODAL } from '../member-picker-modal/member-picker-modal.token.js'; +import { UmbPickerInputContext } from '@umbraco-cms/backoffice/picker-input'; +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbMemberPickerContext extends UmbPickerInputContext { + constructor(host: UmbControllerHostElement) { + super(host, UMB_MEMBER_ITEM_REPOSITORY_ALIAS, UMB_MEMBER_PICKER_MODAL); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts index 3bb4ca8096..d5730315cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/input-member/input-member.element.ts @@ -1,3 +1,5 @@ +import type { UmbMemberItemModel } from '../../repository/index.js'; +import { UmbMemberPickerContext } from './input-member.context.js'; import { css, html, customElement, property, state, ifDefined, repeat } from '@umbraco-cms/backoffice/external/lit'; import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -35,13 +37,10 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { */ @property({ type: Number }) public get min(): number { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - //return this.#pickerContext.min; - return 0; + return this.#pickerContext.min; } public set min(value: number) { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - //this.#pickerContext.min = value; + this.#pickerContext.min = value; } /** @@ -61,13 +60,10 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { */ @property({ type: Number }) public get max(): number { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - //return this.#pickerContext.max; - return Infinity; + return this.#pickerContext.max; } public set max(value: number) { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - //this.#pickerContext.max = value; + this.#pickerContext.max = value; } /** @@ -80,13 +76,10 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { maxMessage = 'This field exceeds the allowed amount of items'; public get selectedIds(): Array { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - //return this.#pickerContext.getSelection(); - return []; + return this.#pickerContext.getSelection(); } public set selectedIds(ids: Array) { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - //this.#pickerContext.setSelection(ids); + this.#pickerContext.setSelection(ids); this.#sorter.setModel(ids); } @@ -102,14 +95,16 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { this.selectedIds = splitStringToArray(idsString); } + @property({ type: Object, attribute: false }) + public filter: (member: UmbMemberItemModel) => boolean = () => true; + @state() private _editMemberPath = ''; @state() - private _items?: Array; + private _items?: Array; - // TODO: Create the `UmbMemberPickerContext` [LK] - //#pickerContext = new UmbMemberPickerContext(this); + #pickerContext = new UmbMemberPickerContext(this); constructor() { super(); @@ -122,61 +117,51 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { .observeRouteBuilder((routeBuilder) => { this._editMemberPath = routeBuilder({}); }); - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - // this.addValidator( - // 'rangeUnderflow', - // () => this.minMessage, - // () => !!this.min && this.#pickerContext.getSelection().length < this.min, - // ); - // this.addValidator( - // 'rangeOverflow', - // () => this.maxMessage, - // () => !!this.max && this.#pickerContext.getSelection().length > this.max, - // ); + this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); + this.observe(this.#pickerContext.selectedItems, (selectedItems) => { + this._items = selectedItems; + }); + } - // this.observe(this.#pickerContext.selection, (selection) => (super.value = selection.join(','))); - // this.observe(this.#pickerContext.selectedItems, (selectedItems) => (this._items = selectedItems)); + connectedCallback(): void { + super.connectedCallback(); + + this.addValidator( + 'rangeUnderflow', + () => this.minMessage, + () => !!this.min && this.#pickerContext.getSelection().length < this.min, + ); + + this.addValidator( + 'rangeOverflow', + () => this.maxMessage, + () => !!this.max && this.#pickerContext.getSelection().length > this.max, + ); } protected _openPicker() { - console.log('member.openPicker'); - // this.#pickerContext.openPicker({ - // hideTreeRoot: true, - // }); + this.#pickerContext.openPicker({ + hideTreeRoot: true, + }); } - protected _requestRemoveItem(item: MemberItemResponseModel) { - console.log('member.requestRemoveItem', item); - //this.#pickerContext.requestRemoveItem(item.id!); + protected _requestRemoveItem(item: UmbMemberItemModel) { + this.#pickerContext.requestRemoveItem(item.unique!); } protected getFormElement() { return undefined; } - #pickableFilter: (item: MemberItemResponseModel) => boolean = (item) => { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - console.log('member.pickableFilter', item); - // if (this.allowedContentTypeIds && this.allowedContentTypeIds.length > 0) { - // return this.allowedContentTypeIds.includes(item.contentTypeId); - // } - return true; - }; - #openPicker() { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - console.log('member.openPicker'); - // this.#pickerContext.openPicker({ - // hideTreeRoot: true, - // pickableFilter: this.#pickableFilter, - // }); + this.#pickerContext.openPicker({ + filter: this.filter, + }); } #requestRemoveItem(item: MemberItemResponseModel) { - // TODO: Uncomment, once `UmbMemberPickerContext` has been implemented. [LK] - console.log('member.requestRemoveItem', item); - //this.#pickerContext.requestRemoveItem(item.id!); + this.#pickerContext.requestRemoveItem(item.id!); } render() { @@ -188,7 +173,7 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { return html` ${repeat( this._items, - (item) => item.id, + (item) => item.unique, (item) => this.#renderItem(item), )} `; @@ -203,12 +188,12 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { label=${this.localize.term('general_choose')}>`; } - #renderItem(item: MemberItemResponseModel) { - if (!item.id) return; + #renderItem(item: UmbMemberItemModel) { + if (!item.unique) return; // TODO: get the correct variant name const name = item.variants[0].name; return html` - + ${this.#renderIsTrashed(item)} ${this.#renderOpenButton(item)} @@ -222,23 +207,23 @@ export class UmbInputMemberElement extends FormControlMixin(UmbLitElement) { `; } - #renderOpenButton(item: MemberItemResponseModel) { + #renderOpenButton(item: UmbMemberItemModel) { if (!this.showOpenButton) return; // TODO: get the correct variant name const name = item.variants[0].name; return html` `; } - #renderIsTrashed(item: MemberItemResponseModel) { + #renderIsTrashed(item: UmbMemberItemModel) { // TODO: Uncomment, once the Management API model support deleted members. [LK] - //if (!item.isTrashed) return; - //return html`Trashed`; + // if (!item.isTrashed) return; + // return html`Trashed`; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/index.ts new file mode 100644 index 0000000000..d57edc65a4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/index.ts @@ -0,0 +1 @@ +export { UMB_MEMBER_PICKER_MODAL } from './member-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/manifests.ts new file mode 100644 index 0000000000..b9fa9c0cb9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/manifests.ts @@ -0,0 +1,8 @@ +export const manifests = [ + { + type: 'modal', + alias: 'Umb.Modal.MemberPicker', + name: 'Member Picker Modal', + element: () => import('./member-picker-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts new file mode 100644 index 0000000000..a5ab4a5928 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.element.ts @@ -0,0 +1,80 @@ +import { UmbMemberCollectionRepository } from '../../collection/index.js'; +import type { UmbMemberDetailModel } from '../../types.js'; +import type { UmbMemberPickerModalValue, UmbMemberPickerModalData } from './member-picker-modal.token.js'; +import { html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; + +@customElement('umb-member-picker-modal') +export class UmbMemberPickerModalElement extends UmbModalBaseElement< + UmbMemberPickerModalData, + UmbMemberPickerModalValue +> { + @state() + private _members: Array = []; + + #collectionRepository = new UmbMemberCollectionRepository(this); + #selectionManager = new UmbSelectionManager(this); + + connectedCallback(): void { + super.connectedCallback(); + this.#selectionManager.setSelectable(true); + this.#selectionManager.setMultiple(this.data?.multiple ?? false); + this.#selectionManager.setSelection(this.value?.selection ?? []); + } + + async firstUpdated() { + const { data } = await this.#collectionRepository.requestCollection({}); + this._members = data?.items ?? []; + } + + get #filteredMembers() { + if (this.data?.filter) { + return this._members.filter(this.data.filter as any); + } else { + return this._members; + } + } + + #submit() { + this.value = { selection: this.#selectionManager.getSelection() }; + this.modalContext?.submit(); + } + + #close() { + this.modalContext?.reject(); + } + + render() { + return html` + + ${repeat( + this.#filteredMembers, + (item) => item.unique, + (item) => html` + this.#selectionManager.select(item.unique)} + @deselected=${() => this.#selectionManager.deselect(item.unique)} + ?selected=${this.#selectionManager.isSelected(item.unique)}> + + + `, + )} + +
    + + +
    +
    `; + } +} + +export default UmbMemberPickerModalElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-picker-modal': UmbMemberPickerModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts new file mode 100644 index 0000000000..b77c28327f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/components/member-picker-modal/member-picker-modal.token.ts @@ -0,0 +1,21 @@ +import type { UmbMemberItemModel } from '../../repository/index.js'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbMemberPickerModalData { + multiple?: boolean; + filter?: (member: UmbMemberItemModel) => boolean; +} + +export interface UmbMemberPickerModalValue { + selection: Array; +} + +export const UMB_MEMBER_PICKER_MODAL = new UmbModalToken( + 'Umb.Modal.MemberPicker', + { + modal: { + type: 'sidebar', + size: 'small', + }, + }, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/entity-actions/manifests.ts index 663551da79..712696d704 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/entity-actions/manifests.ts @@ -5,9 +5,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.Member.Delete', name: 'Delete Member Entity Action', - kind: 'delete', forEntityTypes: [UMB_MEMBER_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_MEMBER_DETAIL_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/manifests.ts index f287a0f18f..db74e42f8a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/manifests.ts @@ -3,6 +3,7 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as sectionViewManifests } from './section-view/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; +import { manifests as memberPickerModalManifests } from './components/member-picker-modal/manifests.js'; export const manifests = [ ...entityActionManifests, @@ -10,4 +11,5 @@ export const manifests = [ ...sectionViewManifests, ...workspaceManifests, ...collectionManifests, + ...memberPickerModalManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/property-dataset-context/member-property-dataset-context.token.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/property-dataset-context/member-property-dataset-context.token.ts new file mode 100644 index 0000000000..9bd76b6ada --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/property-dataset-context/member-property-dataset-context.token.ts @@ -0,0 +1,13 @@ +import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js'; +import type { UmbMemberPropertyDataContext } from './member-property-dataset-context.js'; +import type { UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property'; +import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; + +export const IsMemberVariantContext = (context: UmbPropertyDatasetContext): context is UmbMemberPropertyDataContext => + context.getEntityType() === UMB_MEMBER_ENTITY_TYPE; + +export const UMB_MEMBER_VARIANT_CONTEXT = new UmbContextToken( + 'UmbVariantContext', + undefined, + IsMemberVariantContext, +); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/property-dataset-context/member-property-dataset-context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/property-dataset-context/member-property-dataset-context.ts new file mode 100644 index 0000000000..4c496ac364 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/property-dataset-context/member-property-dataset-context.ts @@ -0,0 +1,118 @@ +import type { UmbMemberWorkspaceContext } from '../workspace/member-workspace.context.js'; +import type { UmbNameablePropertyDatasetContext, UmbPropertyDatasetContext } from '@umbraco-cms/backoffice/property'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { map } from '@umbraco-cms/backoffice/external/rxjs'; +import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; + +// TODO: This code can be split into a UmbContentTypePropertyDatasetContext, leaving just the publishing state and methods to this class. +export class UmbMemberPropertyDataContext + extends UmbContextBase + implements UmbPropertyDatasetContext, UmbNameablePropertyDatasetContext +{ + #workspace: UmbMemberWorkspaceContext; + #variantId: UmbVariantId; + public getVariantId() { + return this.#variantId; + } + + #currentVariant = new UmbObjectState(undefined); + currentVariant = this.#currentVariant.asObservable(); + + name = this.#currentVariant.asObservablePart((x) => x?.name); + culture = this.#currentVariant.asObservablePart((x) => x?.culture); + segment = this.#currentVariant.asObservablePart((x) => x?.segment); + + // TODO: Refactor: Make a properties observable. (with such I think i mean a property value object array.. array with object with properties, alias, value, culture and segment) + // TO make such happen I think we need to maintain all properties and their value of this object. + // This will actually make it simpler if multiple are watching the same property. + // But it will also mean that we wil watch all properties and their structure, for variantID, all the time for all of the properties. + + getEntityType(): string { + return this.#workspace.getEntityType(); + } + getUnique(): string | undefined { + return this.#workspace.getUnique(); + } + getName(): string | undefined { + return this.#workspace.getName(this.#variantId); + } + setName(name: string) { + this.#workspace.setName(name, this.#variantId); + } + getVariantInfo() { + return this.#workspace.getVariant(this.#variantId); + } + + constructor(host: UmbControllerHost, workspace: UmbMemberWorkspaceContext, variantId?: UmbVariantId) { + // The controller alias, is a very generic name cause we want only one of these for this controller host. + super(host, UMB_PROPERTY_DATASET_CONTEXT); + this.#workspace = workspace; + this.#variantId = variantId ?? UmbVariantId.CreateInvariant(); + + this.observe( + this.#workspace.variantById(this.#variantId), + async (variantInfo) => { + if (!variantInfo) return; + this.#currentVariant.setValue(variantInfo); + }, + '_observeActiveVariant', + ); + } + + #createPropertyVariantId(property: UmbPropertyTypeModel) { + return UmbVariantId.Create({ + culture: property.variesByCulture ? this.#variantId.culture : null, + segment: property.variesBySegment ? this.#variantId.segment : null, + }); + } + + /** + * TODO: Write proper JSDocs here. + * Ideally do not use these methods, its better to communicate directly with the workspace, but if you do not know the property variant id, then this will figure it out for you. So good for externals to set or get values of a property. + */ + async propertyVariantId(propertyAlias: string) { + return (await this.#workspace.structure.propertyStructureByAlias(propertyAlias)).pipe( + map((property) => (property ? this.#createPropertyVariantId(property) : undefined)), + ); + } + + /** + * TODO: Write proper JSDocs here. + * Ideally do not use these methods, its better to communicate directly with the workspace, but if you do not know the property variant id, then this will figure it out for you. So good for externals to set or get values of a property. + */ + async propertyValueByAlias(propertyAlias: string) { + await this.#workspace.isLoaded(); + return (await this.#workspace.structure.propertyStructureByAlias(propertyAlias)).pipe( + map((property) => + property?.alias + ? this.#workspace.getPropertyValue(property.alias, this.#createPropertyVariantId(property)) + : undefined, + ), + ); + } + + // TODO: Refactor: Not used currently, but should investigate if we can implement this, to spare some energy. + async propertyValueByAliasAndCulture(propertyAlias: string, propertyVariantId: UmbVariantId) { + return this.#workspace.propertyValueByAlias(propertyAlias, propertyVariantId); + } + + /** + * TODO: Write proper JSDocs here. + * Ideally do not use these methods, its better to communicate directly with the workspace, but if you do not know the property variant id, then this will figure it out for you. So good for externals to set or get values of a property. + */ + async setPropertyValue(propertyAlias: string, value: unknown) { + // This is not reacting to if the property variant settings changes while running. + const property = await this.#workspace.structure.getPropertyStructureByAlias(propertyAlias); + if (property) { + const variantId = this.#createPropertyVariantId(property); + + // This is not reacting to if the property variant settings changes while running. + this.#workspace.setPropertyValue(propertyAlias, value, variantId); + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts index 66612890a1..4a1a62c604 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts @@ -49,7 +49,15 @@ export class UmbMemberServerDataSource implements UmbDetailDataSource) => MemberResource.getItemMember({ id: const mapper = (item: MemberItemResponseModel): UmbMemberItemModel => { return { unique: item.id, + name: item.variants[0].name || '', memberType: { unique: item.memberType.id, icon: item.memberType.icon, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/item/types.ts index 4993561027..fef6a44474 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/item/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/item/types.ts @@ -2,6 +2,7 @@ import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; export interface UmbMemberItemModel { unique: string; + name: string; // TODO: this is not correct. We need to get it from the variants. This is a temp solution. memberType: { unique: string; icon: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/member-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/member-repository-base.ts new file mode 100644 index 0000000000..be0cfc9bf4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/member-repository-base.ts @@ -0,0 +1,33 @@ +import type { UmbMemberDetailStore } from './detail/member-detail.store.js'; +import { UMB_MEMBER_DETAIL_STORE_CONTEXT } from './detail/member-detail.store.js'; +import type { UmbMemberItemStore } from './item/member-item.store.js'; +import { UMB_MEMBER_ITEM_STORE_CONTEXT } from './item/member-item.store.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbNotificationContext } from '@umbraco-cms/backoffice/notification'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; + +export abstract class UmbMemberRepositoryBase extends UmbRepositoryBase { + protected init; + protected detailStore?: UmbMemberDetailStore; + protected itemStore?: UmbMemberItemStore; + protected notificationContext?: UmbNotificationContext; + + constructor(host: UmbControllerHost) { + super(host); + + this.init = Promise.all([ + this.consumeContext(UMB_MEMBER_DETAIL_STORE_CONTEXT, (instance) => { + this.detailStore = instance; + }).asPromise(), + + this.consumeContext(UMB_MEMBER_ITEM_STORE_CONTEXT, (instance) => { + this.itemStore = instance; + }).asPromise(), + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (instance) => { + this.notificationContext = instance; + }).asPromise(), + ]); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/types.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/types.ts index fdb3308981..fc724221e0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/types.ts @@ -1,5 +1,5 @@ import type { UmbMemberEntityType } from './entity.js'; -import type { UmbVariantModel } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantModel, UmbVariantOptionModel } from '@umbraco-cms/backoffice/variant'; export interface UmbMemberDetailModel { email: string; @@ -21,9 +21,13 @@ export interface UmbMemberDetailModel { variants: Array; } +export interface UmbMemberVariantModel extends UmbVariantModel {} + export interface UmbMemberValueModel { culture: string | null; segment: string | null; alias: string; value: ValueType; } + +export interface UmbMemberVariantOptionModel extends UmbVariantOptionModel {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts index 644e1bce88..0107f6319a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/manifests.ts @@ -1,6 +1,10 @@ import { UMB_MEMBER_ENTITY_TYPE } from '../entity.js'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; -import type { ManifestWorkspace, ManifestWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry'; +import type { + ManifestWorkspace, + ManifestWorkspaceActions, + ManifestWorkspaceView, +} from '@umbraco-cms/backoffice/extension-registry'; export const UMB_MEMBER_WORKSPACE_ALIAS = 'Umb.Workspace.Member'; @@ -14,9 +18,10 @@ const workspace: ManifestWorkspace = { }, }; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.Member.Save', name: 'Save Member Workspace Action', api: UmbSaveWorkspaceAction, @@ -34,4 +39,43 @@ const workspaceActions: Array = [ }, ]; -export const manifests = [workspace, ...workspaceActions]; +export const workspaceViews: Array = [ + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.Member.Content', + name: 'Member Workspace Content View', + js: () => import('./views/content/member-workspace-view-content.element.js'), + weight: 300, + meta: { + label: 'Content', + pathname: 'content', + icon: 'icon-document', + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: UMB_MEMBER_WORKSPACE_ALIAS, + }, + ], + }, + { + type: 'workspaceView', + alias: 'Umb.WorkspaceView.Member.Member', + name: 'Member Workspace Member View', + js: () => import('./views/member/member-workspace-view-member.element.js'), + weight: 200, + meta: { + label: 'Member', + pathname: 'member', + icon: 'icon-user', + }, + conditions: [ + { + alias: 'Umb.Condition.WorkspaceAlias', + match: UMB_MEMBER_WORKSPACE_ALIAS, + }, + ], + }, +]; + +export const manifests = [workspace, ...workspaceActions, ...workspaceViews]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts index 1a7c7faa5d..f841e4d981 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-editor.element.ts @@ -1,55 +1,111 @@ +import type { UmbMemberVariantOptionModel } from '../types.js'; import { UMB_MEMBER_WORKSPACE_CONTEXT } from './member-workspace.context.js'; +import { UmbMemberWorkspaceSplitViewElement } from './member-workspace-split-view.element.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, customElement, property, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { ManifestWorkspace } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbRoute, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; @customElement('umb-member-workspace-editor') export class UmbMemberWorkspaceEditorElement extends UmbLitElement { - @property({ attribute: false }) - manifest?: ManifestWorkspace; - - @state() - private _unique?: string; - @state() - private _email?: string; + // + // TODO: Refactor: when having a split view/variants context token, we can rename the split view/variants component to a generic and make this component generic as well. [NL] + private splitViewElement = new UmbMemberWorkspaceSplitViewElement(); #workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; + @state() + _routes?: Array; + constructor() { super(); - this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { - this.#workspaceContext = workspaceContext; - this.#observeData(); + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (instance) => { + this.#workspaceContext = instance; + this.#observeVariants(); }); } - // Only for CRUD demonstration purposes - #observeData() { + #observeVariants() { if (!this.#workspaceContext) return; - this.observe(this.#workspaceContext.unique, (unique) => { - this._unique = unique; - }); - this.observe(this.#workspaceContext.email, (email) => { - this._email = email; - }); + // TODO: the variantOptions observable is like too broad as this will be triggered then there is any change in the variant options, we need to only update routes when there is a relevant change to them. [NL] + this.observe(this.#workspaceContext.variantOptions, (options) => this._generateRoutes(options), '_observeVariants'); } - // Only for CRUD demonstration purposes - #onChange = (e: Event) => { - const input = e.target as HTMLInputElement; - this.#workspaceContext!.updateData({ email: input.value }); + private _handleVariantFolderPart(index: number, folderPart: string) { + const variantSplit = folderPart.split('_'); + const culture = variantSplit[0]; + const segment = variantSplit[1]; + this.#workspaceContext?.splitView.setActiveVariant(index, culture, segment); + } + + private async _generateRoutes(options: Array) { + if (!options || options.length === 0) return; + + // Generate split view routes for all available routes + const routes: Array = []; + + // Split view routes: + options.forEach((variantA) => { + options.forEach((variantB) => { + routes.push({ + // TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL] + path: variantA.unique + '_&_' + variantB.unique, + component: this.splitViewElement, + setup: (_component, info) => { + // Set split view/active info.. + const variantSplit = info.match.fragments.consumed.split('_&_'); + variantSplit.forEach((part, index) => { + this._handleVariantFolderPart(index, part); + }); + }, + }); + }); + }); + + // Single view: + options.forEach((variant) => { + routes.push({ + // TODO: When implementing Segments, be aware if using the unique is URL Safe... [NL] + path: variant.unique, + component: this.splitViewElement, + setup: (_component, info) => { + // cause we might come from a split-view, we need to reset index 1. + this.#workspaceContext?.splitView.removeActiveVariant(1); + this._handleVariantFolderPart(0, info.match.fragments.consumed); + }, + }); + }); + + if (routes.length !== 0) { + // Using first single view as the default route for now (hence the math below): + routes.push({ + path: '', + redirectTo: routes[options.length * options.length]?.path, + }); + } + + const oldValue = this._routes; + + // is there any differences in the amount ot the paths? [NL] + // TODO: if we make a memorization function as the observer, we can avoid this check and avoid the whole build of routes. [NL] + if (oldValue && oldValue.length === routes.length) { + // is there any differences in the paths? [NL] + const hasDifferences = oldValue.some((route, index) => route.path !== routes[index].path); + if (!hasDifferences) return; + } + this._routes = routes; + this.requestUpdate('_routes', oldValue); + } + + private _gotWorkspaceRoute = (e: UmbRouterSlotInitEvent) => { + this.#workspaceContext?.splitView.setWorkspaceRoute(e.target.absoluteRouterPath); }; render() { - return html` - -
    Unique: ${this._unique}
    - - -
    - `; + return this._routes && this._routes.length > 0 + ? html`` + : ''; } static styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-split-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-split-view.element.ts new file mode 100644 index 0000000000..9258447709 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace-split-view.element.ts @@ -0,0 +1,87 @@ +import { UMB_MEMBER_WORKSPACE_CONTEXT } from './member-workspace.context.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, nothing, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import type { ActiveVariant } from '@umbraco-cms/backoffice/workspace'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-member-workspace-split-view') +export class UmbMemberWorkspaceSplitViewElement extends UmbLitElement { + // TODO: Refactor: use the split view context token: + private _workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; + + @state() + _variants?: Array; + + constructor() { + super(); + + // TODO: Refactor: use a split view workspace context token: + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (context) => { + this._workspaceContext = context; + this._observeActiveVariantInfo(); + }); + } + + private _observeActiveVariantInfo() { + if (!this._workspaceContext) return; + this.observe( + this._workspaceContext.splitView.activeVariantsInfo, + (variants) => { + this._variants = variants; + }, + '_observeActiveVariantsInfo', + ); + } + + render() { + return this._variants + ? html`
    + ${repeat( + this._variants, + (view) => + view.index + '_' + (view.culture ?? '') + '_' + (view.segment ?? '') + '_' + this._variants!.length, + (view) => html` + + `, + )} +
    + + ` + : nothing; + } + + static styles = [ + UmbTextStyles, + css` + :host { + width: 100%; + height: 100%; + + display: flex; + flex: 1; + flex-direction: column; + } + + #splitViews { + display: flex; + width: 100%; + height: calc(100% - var(--umb-footer-layout-height)); + } + + #breadcrumbs { + margin: 0 var(--uui-size-layout-1); + } + `, + ]; +} + +export default UmbMemberWorkspaceSplitViewElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-split-view': UmbMemberWorkspaceSplitViewElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts index eb2fc925c8..4cb8771d30 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member-workspace.context.ts @@ -1,29 +1,102 @@ import { UmbMemberDetailRepository } from '../repository/index.js'; -import type { UmbMemberDetailModel } from '../types.js'; +import type { UmbMemberDetailModel, UmbMemberVariantModel, UmbMemberVariantOptionModel } from '../types.js'; +import { UmbMemberPropertyDataContext } from '../property-dataset-context/member-property-dataset-context.js'; import { UMB_MEMBER_WORKSPACE_ALIAS } from './manifests.js'; -import { - type UmbSaveableWorkspaceContextInterface, - UmbEditableWorkspaceContextBase, +import { UmbMemberTypeDetailRepository } from '@umbraco-cms/backoffice/member-type'; +import { UmbEditableWorkspaceContextBase, UmbWorkspaceSplitViewManager } from '@umbraco-cms/backoffice/workspace'; +import type { + UmbVariantableWorkspaceContextInterface, + UmbSaveableWorkspaceContextInterface, } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { + UmbArrayState, + UmbObjectState, + appendToFrozenArray, + mergeObservables, +} from '@umbraco-cms/backoffice/observable-api'; +import { UmbContentTypePropertyStructureManager } from '@umbraco-cms/backoffice/content-type'; +import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; +import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; type EntityType = UmbMemberDetailModel; export class UmbMemberWorkspaceContext extends UmbEditableWorkspaceContextBase - implements UmbSaveableWorkspaceContextInterface + implements UmbVariantableWorkspaceContextInterface { public readonly repository = new UmbMemberDetailRepository(this); #persistedData = new UmbObjectState(undefined); #currentData = new UmbObjectState(undefined); + #getDataPromise?: Promise; + + // TODo: Optimize this so it uses either a App Language Context? [NL] + #languageRepository = new UmbLanguageCollectionRepository(this); + #languages = new UmbArrayState([], (x) => x.unique); + public readonly languages = this.#languages.asObservable(); + + public isLoaded() { + return this.#getDataPromise; + } + + readonly data = this.#currentData.asObservable(); + readonly name = this.#currentData.asObservablePart((data) => data?.variants[0].name); + readonly createDate = this.#currentData.asObservablePart((data) => data?.variants[0].createDate); + readonly updateDate = this.#currentData.asObservablePart((data) => data?.variants[0].updateDate); + readonly contentTypeUnique = this.#currentData.asObservablePart((data) => data?.memberType.unique); + readonly structure = new UmbContentTypePropertyStructureManager(this, new UmbMemberTypeDetailRepository(this)); + + readonly varies = this.structure.ownerContentTypePart((x) => + x ? x.variesByCulture || x.variesBySegment : undefined, + ); + #varies?: boolean; + + readonly variants = this.#currentData.asObservablePart((data) => data?.variants ?? []); - readonly email = this.#currentData.asObservablePart((data) => data?.email); readonly unique = this.#currentData.asObservablePart((data) => data?.unique); + readonly splitView = new UmbWorkspaceSplitViewManager(); + + readonly variantOptions = mergeObservables( + [this.varies, this.variants, this.languages], + ([varies, variants, languages]) => { + // TODO: When including segments, when be aware about the case of segment varying when not culture varying. [NL] + if (varies === true) { + return languages.map((language) => { + return { + variant: variants.find((x) => x.culture === language.unique), + language, + // TODO: When including segments, this object should be updated to include a object for the segment. [NL] + // TODO: When including segments, the unique should be updated to include the segment as well. [NL] + unique: language.unique, // This must be a variantId string! + culture: language.unique, + segment: null, + } as UmbMemberVariantOptionModel; + }); + } else if (varies === false) { + return [ + { + variant: variants.find((x) => x.culture === null), + language: languages.find((x) => x.isDefault), + culture: null, + segment: null, + unique: UMB_INVARIANT_CULTURE, // This must be a variantId string! + } as UmbMemberVariantOptionModel, + ]; + } + return [] as Array; + }, + ); + constructor(host: UmbControllerHost) { super(host, UMB_MEMBER_WORKSPACE_ALIAS); + + this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique)); + this.observe(this.varies, (varies) => (this.#varies = varies)); + + this.loadLanguages(); } resetState() { @@ -32,30 +105,185 @@ export class UmbMemberWorkspaceContext this.#currentData.setValue(undefined); } + async loadLanguages() { + // TODO: If we don't end up having a Global Context for languages, then we should at least change this into using a asObservable which should be returned from the repository. [Nl] + const { data } = await this.#languageRepository.requestCollection({}); + this.#languages.setValue(data?.items ?? []); + } + async load(unique: string) { this.resetState(); - const { data } = await this.repository.requestByUnique(unique); + this.#getDataPromise = this.repository.requestByUnique(unique); + const { data } = await this.#getDataPromise; + if (!data) return undefined; - if (data) { - this.setIsNew(false); - this.#persistedData.setValue(data); - this.#currentData.setValue(data); - } + this.setIsNew(false); + this.#persistedData.setValue(data); + this.#currentData.setValue(data); + + return data || undefined; } async create(memberTypeUnique: string) { this.resetState(); - const { data } = await this.repository.createScaffold({ - memberType: { unique: memberTypeUnique }, + this.#getDataPromise = this.repository.createScaffold({ + memberType: { + unique: memberTypeUnique, + }, }); + const { data } = await this.#getDataPromise; + if (!data) return undefined; - if (data) { - this.setIsNew(true); - this.#persistedData.setValue(data); - this.#currentData.setValue(data); + this.setIsNew(true); + this.#persistedData.setValue(undefined); + this.#currentData.setValue(data); + return data; + } + + getData() { + return this.#currentData.getValue(); + } + + getUnique() { + return this.getData()?.unique || ''; + } + + getEntityType() { + return 'member'; + } + + getContentTypeId() { + return this.getData()?.memberType.unique; + } + + // TODO: Check if this is used: + getVaries() { + return this.#varies; + } + + variantById(variantId: UmbVariantId) { + return this.#currentData.asObservablePart((data) => data?.variants?.find((x) => variantId.compare(x))); + } + + getVariant(variantId: UmbVariantId) { + return this.#currentData.getValue()?.variants?.find((x) => variantId.compare(x)); + } + + getName(variantId?: UmbVariantId) { + const variants = this.#currentData.getValue()?.variants; + if (!variants) return; + if (variantId) { + return variants.find((x) => variantId.compare(x))?.name; + } else { + return variants[0]?.name; } + } - return { data }; + setName(name: string, variantId?: UmbVariantId) { + /* + const oldVariants = this.#currentData.getValue()?.variants || []; + const variants = partialUpdateFrozenArray( + oldVariants, + { name }, + variantId ? (x) => variantId.compare(x) : () => true, + ); + this.#currentData.update({ variants }); + */ + // TODO: We should move this type of logic to the act of saving [NL] + this.#updateVariantData(variantId ?? UmbVariantId.CreateInvariant(), { name }); + } + + async propertyStructureById(propertyId: string) { + return this.structure.propertyStructureById(propertyId); + } + + async propertyValueByAlias(propertyAlias: string, variantId?: UmbVariantId) { + return this.#currentData.asObservablePart( + (data) => + data?.values?.find((x) => x?.alias === propertyAlias && (variantId ? variantId.compare(x) : true)) + ?.value as PropertyValueType, + ); + } + + /** + * Get the current value of the property with the given alias and variantId. + * @param alias + * @param variantId + * @returns The value or undefined if not set or found. + */ + getPropertyValue(alias: string, variantId?: UmbVariantId) { + const currentData = this.getData(); + if (currentData) { + const newDataSet = currentData.values?.find( + (x) => x.alias === alias && (variantId ? variantId.compare(x) : true), + ); + return newDataSet?.value as ReturnType; + } + return undefined; + } + async setPropertyValue( + alias: string, + value: UmbMemberValueModel, + variantId?: UmbVariantId, + ) { + variantId ??= UmbVariantId.CreateInvariant(); + + const entry = { ...variantId.toObject(), alias, value }; + const currentData = this.getData(); + if (currentData) { + const values = appendToFrozenArray( + currentData.values || [], + entry, + (x) => x.alias === alias && (variantId ? variantId.compare(x) : true), + ); + this.#currentData.update({ values }); + + // TODO: We should move this type of logic to the act of saving [NL] + this.#updateVariantData(variantId); + } + } + + #updateVariantData(variantId: UmbVariantId, update?: Partial) { + const currentData = this.getData(); + if (!currentData) throw new Error('Data is missing'); + if (this.#varies === true) { + // If variant Id is invariant, we don't to have the variant appended to our data. + if (variantId.isInvariant()) return; + const variant = currentData.variants.find((x) => variantId.compare(x)); + const newVariants = appendToFrozenArray( + currentData.variants, + { + name: '', + createDate: null, + updateDate: null, + ...variantId.toObject(), + ...variant, + ...update, + }, + (x) => variantId.compare(x), + ); + this.#currentData.update({ variants: newVariants }); + } else if (this.#varies === false) { + // TODO: Beware about segments, in this case we need to also consider segments, if its allowed to vary by segments. + const invariantVariantId = UmbVariantId.CreateInvariant(); + const variant = currentData.variants.find((x) => invariantVariantId.compare(x)); + // Cause we are invariant, we will just overwrite all variants with this one: + const newVariants = [ + { + state: null, + name: '', + publishDate: null, + createDate: null, + updateDate: null, + ...invariantVariantId.toObject(), + ...variant, + ...update, + }, + ]; + this.#currentData.update({ variants: newVariants }); + } else { + throw new Error('Varies by culture is missing'); + } } async save() { @@ -72,6 +300,31 @@ export class UmbMemberWorkspaceContext this.workspaceComplete(data); } + async delete() { + const id = this.getUnique(); + if (id) { + await this.repository.delete(id); + } + } + + public createPropertyDatasetContext(host: UmbControllerHost, variantId: UmbVariantId) { + return new UmbMemberPropertyDataContext(host, this, variantId); + } + + public destroy(): void { + this.#currentData.destroy(); + super.destroy(); + this.#persistedData.destroy(); + this.#currentData.destroy(); + } + + set( + propertyName: PropertyName, + value: UmbMemberDetailModel[PropertyName], + ) { + this.#currentData.update({ [propertyName]: value }); + } + // Only for CRUD demonstration purposes updateData(data: Partial) { const currentData = this.#currentData.getValue(); @@ -79,22 +332,51 @@ export class UmbMemberWorkspaceContext this.#currentData.setValue({ ...currentData, ...data }); } - getData() { - return this.#currentData.getValue(); + get email() { + return this.#get('email') || ''; } - getUnique() { - return this.getData()?.unique || ''; + get username() { + return this.#get('username') || ''; } - getEntityType() { - return 'member'; + get isLockedOut() { + return this.#get('isLockedOut') || false; } - public destroy(): void { - super.destroy(); - this.#persistedData.destroy(); - this.#currentData.destroy(); + get isTwoFactorEnabled() { + return this.#get('isTwoFactorEnabled') || false; + } + + get isApproved() { + return this.#get('isApproved') || false; + } + + get failedPasswordAttempts() { + return this.#get('failedPasswordAttempts') || 0; + } + + //TODO Use localization for "never" + get lastLockOutDate() { + return this.#get('lastLockoutDate') || 'never'; + } + + get lastLoginDate() { + return this.#get('lastLoginDate') || 'never'; + } + + get lastPasswordChangeDate() { + const date = this.#get('lastPasswordChangeDate'); + if (!date) return 'never'; + return new Date(date).toLocaleString(); + } + + get memberGroups() { + return this.#get('groups') || []; + } + + #get(propertyName: PropertyName) { + return this.#currentData.getValue()?.[propertyName]; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-properties.element.ts new file mode 100644 index 0000000000..0c6fbb52dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-properties.element.ts @@ -0,0 +1,72 @@ +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; +import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbPropertyContainerTypes, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-member-workspace-view-content-properties') +export class UmbMemberWorkspaceViewContentPropertiesElement extends UmbLitElement { + @property({ type: String, attribute: 'container-name', reflect: false }) + public get containerName(): string | undefined { + return this._propertyStructureHelper.getContainerName(); + } + public set containerName(value: string | undefined) { + this._propertyStructureHelper.setContainerName(value); + } + + @property({ type: String, attribute: 'container-type', reflect: false }) + public get containerType(): UmbPropertyContainerTypes | undefined { + return this._propertyStructureHelper.getContainerType(); + } + public set containerType(value: UmbPropertyContainerTypes | undefined) { + this._propertyStructureHelper.setContainerType(value); + } + + _propertyStructureHelper = new UmbContentTypePropertyStructureHelper(this); + + @state() + _propertyStructure: Array = []; + + constructor() { + super(); + + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { + this._propertyStructureHelper.setStructureManager(workspaceContext.structure); + }); + this.observe(this._propertyStructureHelper.propertyStructure, (propertyStructure) => { + this._propertyStructure = propertyStructure; + }); + } + + render() { + return repeat( + this._propertyStructure, + (property) => property.alias, + (property) => + html` `, + ); + } + + static styles = [ + UmbTextStyles, + css` + .property { + border-bottom: 1px solid var(--uui-color-divider); + } + .property:last-child { + border-bottom: 0; + } + `, + ]; +} + +export default UmbMemberWorkspaceViewContentPropertiesElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-content-properties': UmbMemberWorkspaceViewContentPropertiesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-tab.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-tab.element.ts new file mode 100644 index 0000000000..0612fad852 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content-tab.element.ts @@ -0,0 +1,111 @@ +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; +import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/external/backend-api'; + +import './member-workspace-view-content-properties.element.js'; +@customElement('umb-member-workspace-view-content-tab') +export class UmbMemberWorkspaceViewContentTabElement extends UmbLitElement { + private _tabName?: string | undefined; + + @property({ type: String }) + public get tabName(): string | undefined { + return this._groupStructureHelper.getName(); + } + public set tabName(value: string | undefined) { + if (value === this._tabName) return; + const oldValue = this._tabName; + this._tabName = value; + this._groupStructureHelper.setName(value); + this.requestUpdate('tabName', oldValue); + } + + @property({ type: Boolean }) + public get noTabName(): boolean { + return this._groupStructureHelper.getIsRoot(); + } + public set noTabName(value: boolean) { + this._groupStructureHelper.setIsRoot(value); + } + + private _ownerTabId?: string | null; + @property({ type: String }) + public get ownerTabId(): string | null | undefined { + return this._ownerTabId; + } + public set ownerTabId(value: string | null | undefined) { + if (value === this._ownerTabId) return; + this._ownerTabId = value; + this._groupStructureHelper.setOwnerId(value); + } + + _groupStructureHelper = new UmbContentTypeContainerStructureHelper(this); + + @state() + _groups: Array = []; + + @state() + _hasProperties = false; + + constructor() { + super(); + + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { + this._groupStructureHelper.setStructureManager(workspaceContext.structure); + }); + this.observe(this._groupStructureHelper.containers, (groups) => { + this._groups = groups; + }); + this.observe(this._groupStructureHelper.hasProperties, (hasProperties) => { + this._hasProperties = hasProperties; + }); + } + + render() { + return html` + ${this._hasProperties + ? html` + + + + ` + : ''} + ${repeat( + this._groups, + (group) => group.name, + (group) => + html` + + `, + )} + `; + } + + static styles = [ + UmbTextStyles, + css` + uui-box { + --uui-box-default-padding: 0 var(--uui-size-space-5); + } + uui-box:not(:first-child) { + margin-top: var(--uui-size-layout-1); + } + `, + ]; +} + +export default UmbMemberWorkspaceViewContentTabElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-content-tab': UmbMemberWorkspaceViewContentTabElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts new file mode 100644 index 0000000000..05bd3d4a68 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/content/member-workspace-view-content.element.ts @@ -0,0 +1,170 @@ +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; +import type { UmbMemberWorkspaceViewContentTabElement } from './member-workspace-view-content-tab.element.js'; +import { css, html, customElement, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbContentTypeContainerStructureHelper } from '@umbraco-cms/backoffice/content-type'; +import type { UmbRoute, UmbRouterSlotChangeEvent, UmbRouterSlotInitEvent } from '@umbraco-cms/backoffice/router'; +import { encodeFolderName } from '@umbraco-cms/backoffice/router'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { PropertyTypeContainerModelBaseModel } from '@umbraco-cms/backoffice/external/backend-api'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; + +@customElement('umb-member-workspace-view-edit') +export class UmbMemberWorkspaceViewEditElement extends UmbLitElement implements UmbWorkspaceViewElement { + //@state() + //private _hasRootProperties = false; + + @state() + private _hasRootGroups = false; + + @state() + private _routes: UmbRoute[] = []; + + @state() + private _tabs?: Array; + + @state() + private _routerPath?: string; + + @state() + private _activePath = ''; + + private _workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; + + private _tabsStructureHelper = new UmbContentTypeContainerStructureHelper(this); + + constructor() { + super(); + + this._tabsStructureHelper.setIsRoot(true); + this._tabsStructureHelper.setContainerChildType('Tab'); + this.observe(this._tabsStructureHelper.containers, (tabs) => { + this._tabs = tabs; + this._createRoutes(); + }); + + // _hasRootProperties can be gotten via _tabsStructureHelper.hasProperties. But we do not support root properties currently. + + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (workspaceContext) => { + this._workspaceContext = workspaceContext; + this._tabsStructureHelper.setStructureManager(workspaceContext.structure); + this._observeRootGroups(); + }); + } + + private _observeRootGroups() { + if (!this._workspaceContext) return; + + this.observe( + this._workspaceContext.structure.hasRootContainers('Group'), + (hasRootGroups) => { + this._hasRootGroups = hasRootGroups; + this._createRoutes(); + }, + '_observeGroups', + ); + } + + private _createRoutes() { + if (!this._tabs || !this._workspaceContext) return; + const routes: UmbRoute[] = []; + + if (this._tabs.length > 0) { + this._tabs?.forEach((tab) => { + const tabName = tab.name ?? ''; + routes.push({ + path: `tab/${encodeFolderName(tabName).toString()}`, + component: () => import('./member-workspace-view-content-tab.element.js'), + setup: (component) => { + (component as UmbMemberWorkspaceViewContentTabElement).tabName = tabName; + // TODO: Consider if we can link these more simple, and not parse this on. + (component as UmbMemberWorkspaceViewContentTabElement).ownerTabId = + this._tabsStructureHelper.isOwnerContainer(tab.id!) ? tab.id : undefined; + }, + }); + }); + } + + if (this._hasRootGroups) { + routes.push({ + path: '', + component: () => import('./member-workspace-view-content-tab.element.js'), + setup: (component) => { + (component as UmbMemberWorkspaceViewContentTabElement).noTabName = true; + (component as UmbMemberWorkspaceViewContentTabElement).ownerTabId = null; + }, + }); + } + + if (routes.length !== 0) { + routes.push({ + path: '', + redirectTo: routes[0]?.path, + }); + } + + this._routes = routes; + } + + render() { + if (!this._routes || !this._tabs) return nothing; + + return html` + + ${this._routerPath && (this._tabs.length > 1 || (this._tabs.length === 1 && this._hasRootGroups)) + ? html` + ${this._hasRootGroups && this._tabs.length > 0 + ? html` + Content + ` + : ''} + ${repeat( + this._tabs, + (tab) => tab.name, + (tab) => { + const path = this._routerPath + '/tab/' + encodeFolderName(tab.name || ''); + return html`${tab.name}`; + }, + )} + ` + : ''} + + { + this._routerPath = event.target.absoluteRouterPath; + }} + @change=${(event: UmbRouterSlotChangeEvent) => { + this._activePath = event.target.absoluteActiveViewPath || ''; + }}> + + + `; + } + + static styles = [ + UmbTextStyles, + css` + :host { + display: block; + height: 100%; + --uui-tab-background: var(--uui-color-surface); + } + `, + ]; +} + +export default UmbMemberWorkspaceViewEditElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-edit': UmbMemberWorkspaceViewEditElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts new file mode 100644 index 0000000000..1c26c2fd29 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member-info.element.ts @@ -0,0 +1,127 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbMemberWorkspaceContext } from '../../member-workspace.context.js'; +import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; +import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal'; +import { UmbMemberTypeItemRepository } from '@umbraco-cms/backoffice/member-type'; + +@customElement('umb-member-workspace-view-member-info') +export class UmbMemberWorkspaceViewMemberInfoElement extends UmbLitElement implements UmbWorkspaceViewElement { + @state() + private _memberTypeUnique = ''; + @state() + private _memberTypeName = ''; + @state() + private _memberTypeIcon = ''; + + private _workspaceContext?: UmbMemberWorkspaceContext; + private _memberTypeItemRepository: UmbMemberTypeItemRepository = new UmbMemberTypeItemRepository(this); + + @state() + private _editMemberTypePath = ''; + + @state() + private _createDate = 'Unknown'; + @state() + private _updateDate = 'Unknown'; + + constructor() { + super(); + + new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL) + .addAdditionalPath('member-type') + .onSetup(() => { + return { data: { entityType: 'member-type', preset: {} } }; + }) + .observeRouteBuilder((routeBuilder) => { + this._editMemberTypePath = routeBuilder({}); + }); + + this.consumeContext(UMB_WORKSPACE_CONTEXT, async (nodeContext) => { + this._workspaceContext = nodeContext as UmbMemberWorkspaceContext; + this.observe(this._workspaceContext.contentTypeUnique, (unique) => (this._memberTypeUnique = unique || '')); + this.observe(this._workspaceContext.createDate, (date) => (this._createDate = date || 'Unknown')); + this.observe(this._workspaceContext.updateDate, (date) => (this._updateDate = date || 'Unknown')); + + const memberType = (await this._memberTypeItemRepository.requestItems([this._memberTypeUnique])).data?.[0]; + if (!memberType) return; + this._memberTypeName = memberType.name; + this._memberTypeIcon = memberType.icon; + }); + } + + render() { + return this.#renderGeneralSection(); + } + + #renderGeneralSection() { + return html` +
    + + + + +
    +
    + + + + +
    +
    + Member Type +
    + + ${this._memberTypeName} + +
    +
    +
    + + ${this._memberTypeUnique} +
    + `; + } + + static styles = [ + UmbTextStyles, + css` + .member-type-edit { + display: flex; + align-items: center; + } + .member-type-edit uui-icon { + margin-right: var(--uui-size-space-1); + } + .member-type-edit uui-button { + margin-left: auto; + } + + .general-item { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-1); + } + .general-item:not(:last-child) { + margin-bottom: var(--uui-size-space-6); + } + .general-item .headline { + font-weight: bold; + } + `, + ]; +} + +export default UmbMemberWorkspaceViewMemberInfoElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-member-info': UmbMemberWorkspaceViewMemberInfoElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts new file mode 100644 index 0000000000..1f88f1f9b4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/views/member/member-workspace-view-member.element.ts @@ -0,0 +1,294 @@ +// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; +import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context.js'; +import type { UmbMemberDetailModel } from '../../../types.js'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { css, html, customElement, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/extension-registry'; +import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui'; + +import './member-workspace-view-member-info.element.js'; +import type { UmbInputMemberGroupElement } from '@umbraco-cms/backoffice/member-group'; + +@customElement('umb-member-workspace-view-member') +export class UmbMemberWorkspaceViewMemberElement extends UmbLitElement implements UmbWorkspaceViewElement { + private _workspaceContext?: typeof UMB_MEMBER_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_MEMBER_WORKSPACE_CONTEXT, (context) => { + this._workspaceContext = context; + + this.observe(this._workspaceContext.isNew, (isNew) => { + this._isNew = !!isNew; + }); + }); + } + + @state() + private _showChangePasswordForm = false; + + @state() + private _newPasswordError = ''; + + @state() + private _isNew = true; + + #onChange(propertyName: keyof UmbMemberDetailModel, value: UmbMemberDetailModel[keyof UmbMemberDetailModel]) { + if (!this._workspaceContext) return; + + console.log('Setting', propertyName, value); + + this._workspaceContext.set(propertyName, value); + } + + #onGroupsUpdated(event: CustomEvent) { + const uniques = (event.target as UmbInputMemberGroupElement).selectedIds; + + this._workspaceContext?.set('groups', uniques); + } + + #onPasswordUpdate = () => { + const newPassword = this.shadowRoot?.querySelector('uui-input[name="newPassword"]')?.value; + const confirmPassword = this.shadowRoot?.querySelector('uui-input[name="confirmPassword"]') + ?.value; + + if (newPassword !== confirmPassword) { + this._newPasswordError = 'Passwords do not match'; + return; + } + + this._newPasswordError = ''; + + this._workspaceContext?.set('newPassword', newPassword); + }; + + #onNewPasswordCancel = () => { + this._workspaceContext?.set('newPassword', ''); + this._showChangePasswordForm = false; + this._newPasswordError = ''; + }; + + #renderPasswordInput() { + if (this._isNew) { + return html` + + this.#onPasswordUpdate()}> + + + + this.#onPasswordUpdate()}> + + ${when(this._newPasswordError, () => html`

    ${this._newPasswordError}

    `)} + `; + } + + return html` + + ${when( + this._showChangePasswordForm, + () => html` +
    + + this.#onPasswordUpdate()}> + + + this.#onPasswordUpdate()}> + + ${when(this._newPasswordError, () => html`

    ${this._newPasswordError}

    `)} + +
    + `, + () => + html` (this._showChangePasswordForm = true)}>`, + )} +
    + `; + } + + #renderLeftColumn() { + if (!this._workspaceContext) return; + return html`
    + + + this.#onChange('username', (e.target as HTMLInputElement).value)}> + + + + this.#onChange('email', (e.target as HTMLInputElement).value)} + value=${this._workspaceContext.email}> + + + ${this.#renderPasswordInput()} + + + + + + + this.#onChange('isApproved', e.target.checked)}> + + + + + this.#onChange('isLockedOut', e.target.checked)}> + + + + + this.#onChange('isTwoFactorEnabled', e.target.checked)}> + + + +
    `; + } + + #renderRightColumn() { + if (!this._workspaceContext) return; + + return html` +
    + +
    + + ${this._workspaceContext.failedPasswordAttempts} +
    +
    + + ${this._workspaceContext.lastLockOutDate} +
    +
    + + ${this._workspaceContext.lastLoginDate} +
    +
    + + ${this._workspaceContext.lastPasswordChangeDate} +
    +
    + + + + +
    + `; + } + + render() { + if (!this._workspaceContext) { + return html`
    Not found
    `; + } + + return html` +
    ${this.#renderLeftColumn()} ${this.#renderRightColumn()}
    +
    `; + } + + static styles = [ + UmbTextStyles, + css` + uui-input { + width: 100%; + } + #main { + display: flex; + flex-wrap: wrap; + gap: var(--uui-size-space-4); + } + #left-column { + /* Is there a way to make the wrapped right column grow only when wrapped? */ + flex: 9999 1 500px; + } + #right-column { + flex: 1 1 350px; + display: flex; + flex-direction: column; + gap: var(--uui-size-space-4); + } + uui-box { + height: fit-content; + } + umb-property-layout { + padding-block: var(--uui-size-space-4); + } + umb-property-layout:first-child { + padding-top: 0; + } + umb-property-layout:last-child { + padding-bottom: 0; + } + .validation-error { + margin-top: 0; + color: var(--uui-color-danger); + } + + .general-item { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-1); + } + .general-item:not(:last-child) { + margin-bottom: var(--uui-size-space-6); + } + .general-item .headline { + font-weight: bold; + } + `, + ]; +} + +export default UmbMemberWorkspaceViewMemberElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-member-workspace-view-member': UmbMemberWorkspaceViewMemberElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/packages/package-builder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/packages/package-builder/manifests.ts index c995a3b75a..f58f532b7d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/packages/package-builder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/packages/package-builder/manifests.ts @@ -1,6 +1,6 @@ import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -15,6 +15,6 @@ const workspace: ManifestWorkspace = { }; const workspaceViews: Array = []; -const workspaceActions: Array = []; +const workspaceActions: Array = []; export const manifests = [workspace, ...workspaceViews, ...workspaceActions]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/packages/package-repo/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/packages/package-repo/manifests.ts index 6092dc0aeb..6849284193 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/packages/package-repo/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/packages/package-repo/manifests.ts @@ -1,6 +1,6 @@ import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -15,6 +15,6 @@ const workspace: ManifestWorkspace = { }; const workspaceViews: Array = []; -const workspaceActions: Array = []; +const workspaceActions: Array = []; export const manifests = [workspace, ...workspaceViews, ...workspaceActions]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/manifests.ts index 24a35bcf07..41251731fc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/packages/package-section/manifests.ts @@ -50,6 +50,7 @@ const sectionsViews: Array = [ }, ], }, + /* // Temp removed until is is finished { type: 'sectionView', alias: 'Umb.SectionView.Packages.Builder', @@ -68,6 +69,7 @@ const sectionsViews: Array = [ }, ], }, + */ ]; export const manifests = [section, ...sectionsViews]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/packages/package/repository/package.store.ts b/src/Umbraco.Web.UI.Client/src/packages/packages/package/repository/package.store.ts index 84aa4c6a1a..01739bbcef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/packages/package/repository/package.store.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/packages/package/repository/package.store.ts @@ -19,6 +19,7 @@ export class UmbPackageStore extends UmbStoreBase { * Array of packages with extensions * @private */ + // TODO: Revisit this code, to not use RxJS directly: #packages = new ReplaySubject>(1); #extensions = new UmbArrayState([], (e) => e.alias); diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/entity-actions/manifests.ts index 65299fd753..1456a8d0bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/entity-actions/manifests.ts @@ -6,9 +6,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.RelationType.Delete', name: 'Delete RelationType Entity Action', - kind: 'delete', forEntityTypes: [UMB_RELATION_TYPE_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_RELATION_TYPE_REPOSITORY_ALIAS, @@ -17,6 +17,7 @@ const entityActions: Array = [ }, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.RelationType.Create', name: 'Create RelationType Entity Action', weight: 900, @@ -25,7 +26,6 @@ const entityActions: Array = [ meta: { icon: 'icon-add', label: 'Create', - repositoryAlias: UMB_RELATION_TYPE_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/tree/reload-tree-item-children/manifests.ts index 5518777afe..dc6d18eaf3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/tree/reload-tree-item-children/manifests.ts @@ -4,9 +4,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.RelationType.Tree.ReloadChildrenOf', name: 'Reload Relation Type Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', forEntityTypes: [UMB_RELATION_TYPE_ROOT_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/manifests.ts index 3442c72efc..cea3a62b1c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relation-types/workspace/manifests.ts @@ -1,7 +1,7 @@ import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -54,9 +54,10 @@ const workspaceViews: Array = [ }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.RelationType.Save', name: 'Save Relation Type Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/code-editor/code-editor.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/code-editor/code-editor.stories.ts index 55e4876250..cb94bb5bfc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/code-editor/code-editor.stories.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/code-editor/code-editor.stories.ts @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/web-components'; import type { UmbCodeEditorElement } from './code-editor.element.js'; -import type { CodeEditorLanguage} from './code-editor.model.js'; +import type { CodeEditorLanguage } from './code-editor.model.js'; import { CodeEditorTheme } from './code-editor.model.js'; import { html } from '@umbraco-cms/backoffice/external/lit'; @@ -133,18 +133,18 @@ const codeSnippets: Record = { } setName(value: string) { - this.#data.next({ ...this.#data.value, name: value }); + this.#data.setValue({ ...this.#data.value, name: value }); } setContent(value: string) { - this.#data.next({ ...this.#data.value, content: value }); + this.#data.setValue({ ...this.#data.value, content: value }); } async load(entityId: string) { const { data } = await this.repository.requestByKey(entityId); if (data) { this.setIsNew(false); - this.#data.next(data); + this.#data.setValue(data); } } @@ -152,7 +152,7 @@ const codeSnippets: Record = { const { data } = await this.repository.createScaffold(parentId); if (!data) return; this.setIsNew(true); - this.#data.next(data); + this.#data.setValue(data); } }`, json: `{ diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/components/templating-item-menu/templating-item-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/components/templating-item-menu/templating-item-menu.element.ts index 9837e29879..1e015209d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/components/templating-item-menu/templating-item-menu.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/components/templating-item-menu/templating-item-menu.element.ts @@ -6,11 +6,11 @@ import { type UmbTemplatingItemPickerModalValue, } from '../../modals/templating-item-picker/templating-item-picker-modal.token.js'; import { getInsertDictionarySnippet, getInsertPartialSnippet } from '../../utils/index.js'; -import { UmbDictionaryDetailRepository } from '@umbraco-cms/backoffice/dictionary'; +import { UmbDictionaryDetailRepository, UMB_DICTIONARY_PICKER_MODAL } from '@umbraco-cms/backoffice/dictionary'; import { customElement, property, css, html } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; -import { UMB_DICTIONARY_ITEM_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @customElement('umb-templating-insert-menu') @@ -59,9 +59,12 @@ export class UmbTemplatingInsertMenuElement extends UmbLitElement { async #openTemplatingItemPickerModal() { const itemPickerContext = this.#modalContext?.open(this, UMB_TEMPLATING_ITEM_PICKER_MODAL); - await itemPickerContext?.onSubmit(); + const result = await itemPickerContext?.onSubmit().catch(() => undefined); + + if (result === undefined) return; const value = itemPickerContext?.getValue(); + if (!value) return; this.determineInsertValue(value); @@ -69,33 +72,42 @@ export class UmbTemplatingInsertMenuElement extends UmbLitElement { async #openPartialViewPickerModal() { const partialViewPickerContext = this.#modalContext?.open(this, UMB_PARTIAL_VIEW_PICKER_MODAL); - await partialViewPickerContext?.onSubmit(); + const result = await partialViewPickerContext?.onSubmit().catch(() => undefined); - const path = partialViewPickerContext?.getValue().selection[0]; - if (!path) return; + if (result === undefined) return; - this.determineInsertValue({ type: CodeSnippetType.partialView, value: path }); + const value = partialViewPickerContext?.getValue().selection[0]; + + if (!value) return; + + this.determineInsertValue({ type: CodeSnippetType.partialView, value }); } async #openDictionaryItemPickerModal() { - const dictionaryItemPickerContext = this.#modalContext?.open(this, UMB_DICTIONARY_ITEM_PICKER_MODAL); - await dictionaryItemPickerContext?.onSubmit(); + const dictionaryItemPickerContext = this.#modalContext?.open(this, UMB_DICTIONARY_PICKER_MODAL); + const result = await dictionaryItemPickerContext?.onSubmit().catch(() => undefined); - const item = dictionaryItemPickerContext?.getValue().selection[0]; - if (!item) return; + if (result === undefined) return; - this.determineInsertValue({ type: CodeSnippetType.dictionaryItem, value: item }); + const value = dictionaryItemPickerContext?.getValue().selection[0]; + + if (!value) return; + + this.determineInsertValue({ type: CodeSnippetType.dictionaryItem, value }); } async #openPageFieldBuilderModal() { const pageFieldBuilderContext = this.#modalContext?.open(this, UMB_TEMPLATING_PAGE_FIELD_BUILDER_MODAL); - await pageFieldBuilderContext?.onSubmit(); + const result = await pageFieldBuilderContext?.onSubmit().catch(() => undefined); - const output = pageFieldBuilderContext?.getValue().output; - if (!output) return; + if (result === undefined) return; + + const value = pageFieldBuilderContext?.getValue().output; + + if (!value) return; // The output is already built due to the preview in the modal. Can insert it directly now. - this.value = output; + this.value = value; this.#dispatchInsertEvent(); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-item-picker/templating-item-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-item-picker/templating-item-picker-modal.element.ts index 1ff43fae4a..4bf27aa8af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-item-picker/templating-item-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-item-picker/templating-item-picker-modal.element.ts @@ -8,11 +8,8 @@ import type { import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit'; import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; -import { - UMB_MODAL_MANAGER_CONTEXT, - UMB_DICTIONARY_ITEM_PICKER_MODAL, - UmbModalBaseElement, -} from '@umbraco-cms/backoffice/modal'; +import { UMB_MODAL_MANAGER_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UMB_DICTIONARY_PICKER_MODAL } from '@umbraco-cms/backoffice/dictionary'; @customElement('umb-templating-item-picker-modal') export class UmbTemplatingItemPickerModalElement extends UmbModalBaseElement< @@ -34,47 +31,52 @@ export class UmbTemplatingItemPickerModalElement extends UmbModalBaseElement< async #openTemplatingPageFieldModal() { const pageFieldBuilderContext = this.#modalContext?.open(this, UMB_TEMPLATING_PAGE_FIELD_BUILDER_MODAL); - await pageFieldBuilderContext?.onSubmit(); + const result = await pageFieldBuilderContext?.onSubmit().catch(() => undefined); - const output = pageFieldBuilderContext?.getValue().output; + if (result === undefined) return; - if (output) { - this.value = { value: output, type: CodeSnippetType.pageField }; - this.modalContext?.submit(); - } + const value = pageFieldBuilderContext?.getValue().output; + + if (!value) return; + + this.value = { value, type: CodeSnippetType.pageField }; + this.modalContext?.submit(); } async #openPartialViewPickerModal() { const partialViewPickerContext = this.#modalContext?.open(this, UMB_PARTIAL_VIEW_PICKER_MODAL); - await partialViewPickerContext?.onSubmit(); + const result = await partialViewPickerContext?.onSubmit().catch(() => undefined); - const path = partialViewPickerContext?.getValue().selection[0]; + if (result === undefined) return; - if (path) { - const regex = /^%2F|%25dot%25cshtml$/g; - const prettyPath = path.replace(regex, '').replace(/%2F/g, '/'); - this.value = { - value: prettyPath, - type: CodeSnippetType.partialView, - }; - this.modalContext?.submit(); - } + const value = partialViewPickerContext?.getValue().selection[0]; + + if (!value) return; + + const regex = /^%2F|%25dot%25cshtml$/g; + const prettyPath = value.replace(regex, '').replace(/%2F/g, '/'); + this.value = { + value: prettyPath, + type: CodeSnippetType.partialView, + }; + this.modalContext?.submit(); } async #openDictionaryItemPickerModal() { - const dictionaryItemPickerModal = this.#modalContext?.open(this, UMB_DICTIONARY_ITEM_PICKER_MODAL, { + const dictionaryItemPickerModal = this.#modalContext?.open(this, UMB_DICTIONARY_PICKER_MODAL, { data: { pickableFilter: (item) => item.unique !== null, }, }); - await dictionaryItemPickerModal?.onSubmit(); + const result = await dictionaryItemPickerModal?.onSubmit().catch(() => undefined); + if (result === undefined) return; const dictionaryItem = dictionaryItemPickerModal?.getValue().selection[0]; - if (dictionaryItem) { - this.value = { value: dictionaryItem, type: CodeSnippetType.dictionaryItem }; - this.modalContext?.submit(); - } + if (!dictionaryItem) return; + + this.value = { value: dictionaryItem, type: CodeSnippetType.dictionaryItem }; + this.modalContext?.submit(); } render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-item-picker/templating-item-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-item-picker/templating-item-picker-modal.token.ts index 9a0a15e733..46426fc5ee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-item-picker/templating-item-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/modals/templating-item-picker/templating-item-picker-modal.token.ts @@ -1,6 +1,5 @@ import type { CodeSnippetType } from '../../types.js'; -import type { UmbPartialViewPickerModalValue, UmbTemplatingPageFieldBuilderModalValue } from '../index.js'; -import { type UmbDictionaryItemPickerModalValue, UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbTemplatingItemPickerModalData { hidePartialViews?: boolean; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/manifests.ts index 2ebe2e4a28..da6fef2722 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/manifests.ts @@ -1,11 +1,11 @@ import { UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE, UMB_PARTIAL_VIEW_ROOT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_PARTIAL_VIEW_DETAIL_REPOSITORY_ALIAS } from '../../repository/manifests.js'; import { UmbPartialViewCreateOptionsEntityAction } from './create.action.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.PartialView.CreateOptions', name: 'Partial View Create Options Entity Action', weight: 1000, @@ -14,7 +14,6 @@ export const manifests: Array = [ meta: { icon: 'icon-add', label: 'Create...', - repositoryAlias: UMB_PARTIAL_VIEW_DETAIL_REPOSITORY_ALIAS, }, }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts index 1899781d2c..9907230801 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/create/options-modal/partial-view-create-options-modal.element.ts @@ -16,15 +16,15 @@ export class UmbPartialViewCreateOptionsModalElement extends UmbModalBaseElement super.connectedCallback(); if (!this.data?.parent) throw new Error('A parent unique is required to create a folder'); - this.#createFolderAction = new UmbCreateFolderEntityAction( - this, - UMB_PARTIAL_VIEW_FOLDER_REPOSITORY_ALIAS, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: allow null for entity actions. Some actions can be executed on the root item - this.data.parent.unique, - this.data.parent.entityType, - ); + this.#createFolderAction = new UmbCreateFolderEntityAction(this, { + unique: this.data.parent.unique, + entityType: this.data.parent.entityType, + meta: { + icon: 'icon-folder', + label: 'New folder...', + folderRepositoryAlias: UMB_PARTIAL_VIEW_FOLDER_REPOSITORY_ALIAS, + }, + }); } async #onCreateFolderClick(event: PointerEvent) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/manifests.ts index 756f3a6485..beb71e073a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/manifests.ts @@ -8,9 +8,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const partialViewActions: Array = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.PartialView.Delete', name: 'Delete Partial View Entity Action', - kind: 'delete', forEntityTypes: [UMB_PARTIAL_VIEW_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_PARTIAL_VIEW_DETAIL_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/rename/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/rename/manifests.ts index 9db8168c3c..8eefa4af2e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/rename/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/entity-actions/rename/manifests.ts @@ -14,9 +14,9 @@ export const manifests: Array = [ }, { type: 'entityAction', + kind: 'rename', alias: UMB_RENAME_PARTIAL_VIEW_ENTITY_ACTION_ALIAS, name: 'Rename PartialView Entity Action', - kind: 'rename', forEntityTypes: [UMB_PARTIAL_VIEW_ENTITY_TYPE], meta: { renameRepositoryAlias: UMB_RENAME_PARTIAL_VIEW_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/folder/manifests.ts index 5bc73c84ac..a040f428e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/folder/manifests.ts @@ -1,6 +1,5 @@ import { UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE } from '../../entity.js'; import { UmbPartialViewFolderRepository } from './partial-view-folder.repository.js'; -import { UmbDeleteFolderEntityAction } from '@umbraco-cms/backoffice/tree'; import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_PARTIAL_VIEW_FOLDER_REPOSITORY_ALIAS = 'Umb.Repository.PartialView.Folder'; @@ -17,14 +16,12 @@ export const UMB_DELETE_PARTIAL_VIEW_FOLDER_ENTITY_ACTION_ALIAS = 'Umb.EntityAct const entityActions = [ { type: 'entityAction', + kind: 'folderDelete', alias: UMB_DELETE_PARTIAL_VIEW_FOLDER_ENTITY_ACTION_ALIAS, name: 'Delete Partial View folder Entity Action', - api: UmbDeleteFolderEntityAction, forEntityTypes: [UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE], meta: { - icon: 'icon-trash', - label: 'Delete folder...', - repositoryAlias: UMB_PARTIAL_VIEW_FOLDER_REPOSITORY_ALIAS, + folderRepositoryAlias: UMB_PARTIAL_VIEW_FOLDER_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/folder/partial-view-folder.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/folder/partial-view-folder.repository.ts index 7d7b5a5f95..736d5af8ef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/folder/partial-view-folder.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/folder/partial-view-folder.repository.ts @@ -1,32 +1,9 @@ -import { UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import type { UmbPartialViewTreeItemModel } from '../types.js'; -import { UMB_PARTIAL_VIEW_TREE_STORE_CONTEXT } from '../partial-view-tree.store.js'; import { UmbPartialViewFolderServerDataSource } from './partial-view-folder.server.data-source.js'; -import type { UmbFolderModel } from '@umbraco-cms/backoffice/tree'; import { UmbFolderRepositoryBase } from '@umbraco-cms/backoffice/tree'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbPartialViewFolderRepository extends UmbFolderRepositoryBase { +export class UmbPartialViewFolderRepository extends UmbFolderRepositoryBase { constructor(host: UmbControllerHost) { - super( - host, - UmbPartialViewFolderServerDataSource, - UMB_PARTIAL_VIEW_TREE_STORE_CONTEXT, - folderToPartialViewTreeItemFolder, - ); + super(host, UmbPartialViewFolderServerDataSource); } } - -const folderToPartialViewTreeItemFolder = ( - folder: UmbFolderModel, - parentUnique: string | null, -): UmbPartialViewTreeItemModel => { - return { - unique: folder.unique, - parentUnique, - name: folder.name, - entityType: UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE, - isFolder: true, - hasChildren: false, - }; -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/reload-tree-item-children/manifests.ts index e692dc7cbe..3f5626eaf9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/tree/reload-tree-item-children/manifests.ts @@ -1,20 +1,12 @@ -import { - UMB_PARTIAL_VIEW_ROOT_ENTITY_TYPE, - UMB_PARTIAL_VIEW_ENTITY_TYPE, - UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE, -} from '../../entity.js'; +import { UMB_PARTIAL_VIEW_ROOT_ENTITY_TYPE, UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE } from '../../entity.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.PartialView.Tree.ReloadChildrenOf', name: 'Reload Partial View Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', - forEntityTypes: [ - UMB_PARTIAL_VIEW_ROOT_ENTITY_TYPE, - UMB_PARTIAL_VIEW_ENTITY_TYPE, - UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE, - ], + forEntityTypes: [UMB_PARTIAL_VIEW_ROOT_ENTITY_TYPE, UMB_PARTIAL_VIEW_FOLDER_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/manifests.ts index e97c15b560..44fb274234 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/manifests.ts @@ -1,5 +1,5 @@ import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; -import type { ManifestWorkspace, ManifestWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestWorkspace, ManifestWorkspaceActions } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_PARTIAL_VIEW_WORKSPACE_ALIAS = 'Umb.Workspace.PartialView'; @@ -13,9 +13,10 @@ const workspace: ManifestWorkspace = { }, }; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.PartialView.Save', name: 'Save Partial View', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace-editor.element.ts index aa789722ce..69c4476a59 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace-editor.element.ts @@ -79,9 +79,12 @@ export class UmbPartialViewWorkspaceEditorElement extends UmbLitElement { const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); const queryBuilderModal = modalManager.open(this, UMB_TEMPLATE_QUERY_BUILDER_MODAL); - queryBuilderModal?.onSubmit().then((queryBuilderModalValue) => { - if (queryBuilderModalValue.value) this._codeEditor?.insert(getQuerySnippet(queryBuilderModalValue.value)); - }); + queryBuilderModal + ?.onSubmit() + .then((queryBuilderModalValue) => { + if (queryBuilderModalValue.value) this._codeEditor?.insert(getQuerySnippet(queryBuilderModalValue.value)); + }) + .catch(() => undefined); } #renderCodeEditor() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts index 1720214e3b..9d701a8c14 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/partial-views/workspace/partial-view-workspace.context.ts @@ -11,6 +11,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; import { PartialViewResource } from '@umbraco-cms/backoffice/external/backend-api'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; export class UmbPartialViewWorkspaceContext extends UmbEditableWorkspaceContextBase @@ -119,6 +120,14 @@ export class UmbPartialViewWorkspaceContext } else { const { data } = await this.repository.save(this.#data.value); newData = data; + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } if (newData) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/manifests.ts index c053e38477..1c8c5aa5da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/manifests.ts @@ -1,11 +1,11 @@ import { UMB_SCRIPT_FOLDER_ENTITY_TYPE, UMB_SCRIPT_ROOT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_SCRIPT_DETAIL_REPOSITORY_ALIAS } from '../../repository/manifests.js'; import { UmbScriptCreateOptionsEntityAction } from './create.action.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Script.CreateOptions', name: 'Script Create Options Entity Action', weight: 1000, @@ -14,7 +14,6 @@ export const manifests: Array = [ meta: { icon: 'icon-add', label: 'Create...', - repositoryAlias: UMB_SCRIPT_DETAIL_REPOSITORY_ALIAS, }, }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/script-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/script-create-options-modal.element.ts index 1fd849f6cd..ad49f695a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/script-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/entity-actions/create/options-modal/script-create-options-modal.element.ts @@ -22,15 +22,15 @@ export class UmbScriptCreateOptionsModalElement extends UmbModalBaseElement = [ }, { type: 'entityAction', + kind: 'rename', alias: UMB_RENAME_SCRIPT_ENTITY_ACTION_ALIAS, name: 'Rename Script Entity Action', - kind: 'rename', forEntityTypes: [UMB_SCRIPT_ENTITY_TYPE], meta: { renameRepositoryAlias: UMB_RENAME_SCRIPT_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/folder/manifests.ts index 7a2d20f202..6db24d7ad4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/folder/manifests.ts @@ -1,5 +1,4 @@ import { UMB_SCRIPT_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import { UmbDeleteFolderEntityAction } from '@umbraco-cms/backoffice/tree'; import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_SCRIPT_FOLDER_REPOSITORY_ALIAS = 'Umb.Repository.Script.Folder'; @@ -16,14 +15,12 @@ export const UMB_DELETE_SCRIPT_FOLDER_ENTITY_ACTION_ALIAS = 'Umb.EntityAction.Sc const entityActions = [ { type: 'entityAction', + kind: 'folderDelete', alias: UMB_DELETE_SCRIPT_FOLDER_ENTITY_ACTION_ALIAS, name: 'Delete Script folder', - api: UmbDeleteFolderEntityAction, forEntityTypes: [UMB_SCRIPT_FOLDER_ENTITY_TYPE], meta: { - icon: 'icon-trash', - label: 'Delete folder...', - repositoryAlias: UMB_SCRIPT_FOLDER_REPOSITORY_ALIAS, + folderRepositoryAlias: UMB_SCRIPT_FOLDER_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/folder/script-folder.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/folder/script-folder.repository.ts index 34e1110bf8..2838744009 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/folder/script-folder.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/folder/script-folder.repository.ts @@ -1,26 +1,11 @@ -import { UMB_SCRIPT_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import { UMB_SCRIPT_TREE_STORE_CONTEXT } from '../script-tree.store.js'; -import type { UmbScriptTreeItemModel } from '../types.js'; import { UmbScriptFolderServerDataSource } from './script-folder.server.data-source.js'; -import type { UmbFolderModel } from '@umbraco-cms/backoffice/tree'; import { UmbFolderRepositoryBase } from '@umbraco-cms/backoffice/tree'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbScriptFolderRepository extends UmbFolderRepositoryBase { +export class UmbScriptFolderRepository extends UmbFolderRepositoryBase { constructor(host: UmbControllerHost) { - super(host, UmbScriptFolderServerDataSource, UMB_SCRIPT_TREE_STORE_CONTEXT, folderToScriptTreeItemFolder); + super(host, UmbScriptFolderServerDataSource); } } export default UmbScriptFolderRepository; - -const folderToScriptTreeItemFolder = (folder: UmbFolderModel, parentUnique: string | null): UmbScriptTreeItemModel => { - return { - unique: folder.unique, - parentUnique, - name: folder.name, - entityType: UMB_SCRIPT_FOLDER_ENTITY_TYPE, - isFolder: true, - hasChildren: false, - }; -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/reload-tree-item-children/manifests.ts index f9f70b64e1..7f2d44df09 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/tree/reload-tree-item-children/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_SCRIPT_ROOT_ENTITY_TYPE, UMB_SCRIPT_ENTITY_TYPE, UMB_SCRIPT_FOLDER_ENTITY_TYPE } from '../../entity.js'; +import { UMB_SCRIPT_ROOT_ENTITY_TYPE, UMB_SCRIPT_FOLDER_ENTITY_TYPE } from '../../entity.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ @@ -7,6 +7,6 @@ export const manifests: Array = [ alias: 'Umb.EntityAction.Script.Tree.ReloadChildrenOf', name: 'Reload Script Tree Item Children Entity Action', kind: 'reloadTreeItemChildren', - forEntityTypes: [UMB_SCRIPT_ROOT_ENTITY_TYPE, UMB_SCRIPT_ENTITY_TYPE, UMB_SCRIPT_FOLDER_ENTITY_TYPE], + forEntityTypes: [UMB_SCRIPT_ROOT_ENTITY_TYPE, UMB_SCRIPT_FOLDER_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/manifests.ts index 0cf0a7d1d7..341ed9f7ff 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/manifests.ts @@ -1,6 +1,6 @@ import { UMB_SCRIPT_ENTITY_TYPE } from '../entity.js'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; -import type { ManifestWorkspace, ManifestWorkspaceAction } from '@umbraco-cms/backoffice/extension-registry'; +import type { ManifestWorkspace, ManifestWorkspaceActions } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_SCRIPT_WORKSPACE_ALIAS = 'Umb.Workspace.Script'; export const UMB_SAVE_SCRIPT_WORKSPACE_ACTION_ALIAS = 'Umb.WorkspaceAction.Script.Save'; @@ -15,9 +15,10 @@ const workspace: ManifestWorkspace = { }, }; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: UMB_SAVE_SCRIPT_WORKSPACE_ACTION_ALIAS, name: 'Save Script Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts index 1b32266490..a1b4c84438 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/scripts/workspace/script-workspace.context.ts @@ -8,6 +8,7 @@ import { UmbEditableWorkspaceContextBase } from '@umbraco-cms/backoffice/workspa import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; export class UmbScriptWorkspaceContext extends UmbEditableWorkspaceContextBase { public readonly repository = new UmbScriptDetailRepository(this); @@ -104,6 +105,14 @@ export class UmbScriptWorkspaceContext extends UmbEditableWorkspaceContextBase; + private _items: Array = []; #pickerContext = new UmbStylesheetPickerContext(this); @@ -96,10 +99,18 @@ export class UmbStylesheetInputElement extends FormControlMixin(UmbLitElement) { render() { return html` - ${this._items?.map((item) => this._renderItem(item))} - this.#pickerContext.openPicker()} label="open" - >Add + + ${repeat( + this._items, + (item) => item.unique, + (item) => this._renderItem(item), + )} + + this.#pickerContext.openPicker()} + label="Add stylesheet"> `; } @@ -110,9 +121,9 @@ export class UmbStylesheetInputElement extends FormControlMixin(UmbLitElement) { this.#pickerContext.requestRemoveItem(item.unique!)} - label="Remove Data Type ${item.name}" - >Remove + label="Remove Data Type ${item.name}"> + Remove + `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/components/stylesheet-rule-input/stylesheet-rule-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/components/stylesheet-rule-input/stylesheet-rule-input.element.ts index a9d4544675..6e473a408b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/components/stylesheet-rule-input/stylesheet-rule-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/components/stylesheet-rule-input/stylesheet-rule-input.element.ts @@ -31,19 +31,25 @@ export class UmbStylesheetRuleInputElement extends FormControlMixin(UmbLitElemen return modalContext?.onSubmit(); } - #appendRule = async () => { - const { rule: newRule } = await this.#openRuleSettings(null); - if (!newRule) return; - this.rules = [...this.rules, newRule]; - this.dispatchEvent(new UmbChangeEvent()); + #appendRule = () => { + this.#openRuleSettings(null) + .then((value) => { + if (!value.rule) return; + this.rules = [...this.rules, value.rule]; + this.dispatchEvent(new UmbChangeEvent()); + }) + .catch(() => undefined); }; - #editRule = async (rule: UmbStylesheetRule, index: number) => { - const { rule: updatedRule } = await this.#openRuleSettings(rule); - if (!updatedRule) return; - this.rules[index] = updatedRule; - this.dispatchEvent(new UmbChangeEvent()); - this.requestUpdate(); + #editRule = (rule: UmbStylesheetRule, index: number) => { + this.#openRuleSettings(rule) + .then((value) => { + if (!value.rule) return; + this.rules[index] = value.rule; + this.dispatchEvent(new UmbChangeEvent()); + this.requestUpdate(); + }) + .catch(() => undefined); }; #removeRule = (rule: UmbStylesheetRule) => { @@ -67,7 +73,7 @@ export class UmbStylesheetRuleInputElement extends FormControlMixin(UmbLitElemen `, )} - this.#appendRule()}>Add + Add `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts index fe6e10d112..df096e95f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/manifests.ts @@ -5,6 +5,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Stylesheet.CreateOptions', name: 'Stylesheet Create Options Entity Action', weight: 1000, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/options-modal/stylesheet-create-options-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/options-modal/stylesheet-create-options-modal.element.ts index 0e1cfd7d9c..cce8bc8b43 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/options-modal/stylesheet-create-options-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/create/options-modal/stylesheet-create-options-modal.element.ts @@ -15,15 +15,15 @@ export class UmbStylesheetCreateOptionsModalElement extends UmbModalBaseElement< super.connectedCallback(); if (!this.data?.parent) throw new Error('A parent is required to create a folder'); - this.#createFolderAction = new UmbCreateFolderEntityAction( - this, - UMB_STYLESHEET_FOLDER_REPOSITORY_ALIAS, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: allow null for entity actions. Some actions can be executed on the root item - this.data.parent.unique, - this.data.parent.entityType, - ); + this.#createFolderAction = new UmbCreateFolderEntityAction(this, { + unique: this.data.parent.unique, + entityType: this.data.parent.entityType, + meta: { + icon: 'icon-folder', + label: 'New folder...', + folderRepositoryAlias: UMB_STYLESHEET_FOLDER_REPOSITORY_ALIAS, + }, + }); } async #onCreateFolderClick(event: PointerEvent) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/manifests.ts index e54ef0d4b5..0590231d9c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/manifests.ts @@ -7,6 +7,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const stylesheetActions: Array = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.Stylesheet.Delete', name: 'Delete Stylesheet Entity Action', forEntityTypes: [UMB_STYLESHEET_ENTITY_TYPE], diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/rename/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/rename/manifests.ts index 7026723f3f..5d26b4b368 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/rename/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/entity-actions/rename/manifests.ts @@ -14,9 +14,9 @@ export const manifests: Array = [ }, { type: 'entityAction', + kind: 'rename', alias: UMB_RENAME_STYLESHEET_ENTITY_ACTION_ALIAS, name: 'Rename Stylesheet Entity Action', - kind: 'rename', forEntityTypes: [UMB_STYLESHEET_ENTITY_TYPE], meta: { renameRepositoryAlias: UMB_RENAME_STYLESHEET_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/item/stylesheet-item.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/item/stylesheet-item.server.data-source.ts index 115c883b73..9fe4f6e62a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/item/stylesheet-item.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/repository/item/stylesheet-item.server.data-source.ts @@ -37,7 +37,7 @@ export class UmbStylesheetItemServerDataSource implements UmbItemDataSource { const serverPath = this.#serverFilePathUniqueSerializer.toServerPath(unique); - return serverPath ? encodeURI(serverPath) : null; + return serverPath ? serverPath : null; }) .filter((x) => x !== null) as string[]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/folder/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/folder/manifests.ts index aee1ebc696..dbdd787b0f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/folder/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/folder/manifests.ts @@ -1,5 +1,4 @@ import { UMB_STYLESHEET_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import { UmbDeleteFolderEntityAction } from '@umbraco-cms/backoffice/tree'; import type { ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; export const UMB_STYLESHEET_FOLDER_REPOSITORY_ALIAS = 'Umb.Repository.Stylesheet.Folder'; @@ -16,14 +15,12 @@ export const UMB_DELETE_STYLESHEET_FOLDER_ENTITY_ACTION_ALIAS = 'Umb.EntityActio const entityActions = [ { type: 'entityAction', + kind: 'folderDelete', alias: UMB_DELETE_STYLESHEET_FOLDER_ENTITY_ACTION_ALIAS, name: 'Delete Stylesheet folder Entity Action', - api: UmbDeleteFolderEntityAction, forEntityTypes: [UMB_STYLESHEET_FOLDER_ENTITY_TYPE], meta: { - icon: 'icon-trash', - label: 'Delete folder...', - repositoryAlias: UMB_STYLESHEET_FOLDER_REPOSITORY_ALIAS, + folderRepositoryAlias: UMB_STYLESHEET_FOLDER_REPOSITORY_ALIAS, }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/folder/stylesheet-folder.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/folder/stylesheet-folder.repository.ts index 5b023daa00..60dcc298ed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/folder/stylesheet-folder.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/folder/stylesheet-folder.repository.ts @@ -1,34 +1,11 @@ -import { UMB_STYLESHEET_FOLDER_ENTITY_TYPE } from '../../entity.js'; -import type { UmbStylesheetTreeItemModel } from '../types.js'; -import { UMB_STYLESHEET_TREE_STORE_CONTEXT } from '../stylesheet-tree.store.js'; import { UmbStylesheetFolderServerDataSource } from './stylesheet-folder.server.data-source.js'; -import type { UmbFolderModel } from '@umbraco-cms/backoffice/tree'; import { UmbFolderRepositoryBase } from '@umbraco-cms/backoffice/tree'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -export class UmbStylesheetFolderRepository extends UmbFolderRepositoryBase { +export class UmbStylesheetFolderRepository extends UmbFolderRepositoryBase { constructor(host: UmbControllerHost) { - super( - host, - UmbStylesheetFolderServerDataSource, - UMB_STYLESHEET_TREE_STORE_CONTEXT, - folderToStylesheetTreeItemFolder, - ); + super(host, UmbStylesheetFolderServerDataSource); } } export default UmbStylesheetFolderRepository; - -const folderToStylesheetTreeItemFolder = ( - folder: UmbFolderModel, - parentUnique: string | null, -): UmbStylesheetTreeItemModel => { - return { - unique: folder.unique, - parentUnique, - name: folder.name, - entityType: UMB_STYLESHEET_FOLDER_ENTITY_TYPE, - isFolder: true, - hasChildren: false, - }; -}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/reload-tree-item-children/manifests.ts index 2bc7181a3a..d727147aaf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/tree/reload-tree-item-children/manifests.ts @@ -1,6 +1,5 @@ import { UMB_STYLESHEET_ROOT_ENTITY_TYPE, - UMB_STYLESHEET_ENTITY_TYPE, UMB_STYLESHEET_FOLDER_ENTITY_TYPE, } from '../../entity.js'; import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; @@ -8,9 +7,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.Stylesheet.Tree.ReloadChildrenOf', name: 'Reload Stylesheet Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', - forEntityTypes: [UMB_STYLESHEET_ROOT_ENTITY_TYPE, UMB_STYLESHEET_ENTITY_TYPE, UMB_STYLESHEET_FOLDER_ENTITY_TYPE], + forEntityTypes: [UMB_STYLESHEET_ROOT_ENTITY_TYPE, UMB_STYLESHEET_FOLDER_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/utils/stylesheet-rule-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/utils/stylesheet-rule-manager.ts index e5157c77aa..ae966aad42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/utils/stylesheet-rule-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/utils/stylesheet-rule-manager.ts @@ -31,7 +31,6 @@ export class UmbStylesheetRuleManager { */ insertRules(stylesheetContent: string, rules: Array): string { const regex = this.#umbRuleRegex; - if (!stylesheetContent) throw Error('No Stylesheet content'); if (!stylesheetContent && !rules) return ''; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -42,13 +41,13 @@ export class UmbStylesheetRuleManager { ${rules ?.map( (rule) => ` -/**umb_name:${rule.name}*/ -${rule.selector} { - ${rule.styles} +/**umb_name:${rule.name}*/ +${rule.selector} { + ${rule.styles} } `, ) .join('')}`; - return newContent; + return newContent.trim(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts index 11c1c6c820..95bf6786b9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/manifests.ts @@ -1,6 +1,6 @@ import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; @@ -55,9 +55,10 @@ const workspaceViews: Array = [ ], }, ]; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.Stylesheet.Save', name: 'Save Stylesheet Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts index dbf8bf5209..73591fe1c1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/workspace/stylesheet-workspace.context.ts @@ -12,6 +12,7 @@ import { loadCodeEditor } from '@umbraco-cms/backoffice/code-editor'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; export class UmbStylesheetWorkspaceContext extends UmbEditableWorkspaceContextBase @@ -112,10 +113,19 @@ export class UmbStylesheetWorkspaceContext } else { const { data } = await this.repository.save(this.#data.value); newData = data; + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } if (newData) { this.#data.setValue(newData); + this.setIsNew(false); this.workspaceComplete(newData); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/components/input-template/input-template.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/components/input-template/input-template.element.ts index ca24627134..5e373fb475 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/components/input-template/input-template.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/components/input-template/input-template.element.ts @@ -2,10 +2,10 @@ import type { UmbTemplateCardElement } from '../template-card/template-card.elem import '../template-card/template-card.element.js'; import type { UmbTemplateItemModel } from '../../repository/item/index.js'; import { UmbTemplateItemRepository } from '../../repository/item/index.js'; +import { UMB_TEMPLATE_PICKER_MODAL } from '../../modals/index.js'; import { css, html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import { - UMB_TEMPLATE_PICKER_MODAL, UMB_MODAL_MANAGER_CONTEXT, UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/entity-actions/manifests.ts index 1cacd0b368..701e5606e7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/entity-actions/manifests.ts @@ -6,6 +6,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.Template.Create', name: 'Create Template Entity Action', api: UmbCreateEntityAction, @@ -13,14 +14,13 @@ const entityActions: Array = [ meta: { icon: 'icon-add', label: 'Create', - repositoryAlias: UMB_TEMPLATE_DETAIL_REPOSITORY_ALIAS, }, }, { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.Template.Delete', name: 'Delete Template Entity Action', - kind: 'delete', forEntityTypes: [UMB_TEMPLATE_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_TEMPLATE_DETAIL_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/index.ts index f90152eb56..eb260c5068 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/index.ts @@ -1 +1,2 @@ export { UMB_TEMPLATE_QUERY_BUILDER_MODAL } from './query-builder/index.js'; +export { UMB_TEMPLATE_PICKER_MODAL } from './template-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/query-builder/query-builder-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/query-builder/query-builder-modal.element.ts index 41b9ad5e33..da0bdc9672 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/query-builder/query-builder-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/query-builder/query-builder-modal.element.ts @@ -8,16 +8,12 @@ import type { } from './query-builder-modal.token.js'; import type { UUIComboboxListElement } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, state, query, queryAll, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import { - UmbModalBaseElement, - UMB_DOCUMENT_PICKER_MODAL, - UMB_MODAL_MANAGER_CONTEXT, -} from '@umbraco-cms/backoffice/modal'; +import { UmbModalBaseElement, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { TemplateQueryResultResponseModel, TemplateQuerySettingsResponseModel, } from '@umbraco-cms/backoffice/external/backend-api'; -import { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document'; +import { UmbDocumentItemRepository, UMB_DOCUMENT_PICKER_MODAL } from '@umbraco-cms/backoffice/document'; import './query-builder-filter.element.js'; enum SortOrder { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/template-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/template-picker-modal.token.ts similarity index 88% rename from src/Umbraco.Web.UI.Client/src/packages/core/modal/token/template-picker-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/template-picker-modal.token.ts index 7e5273ffdf..dce7a3f9d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/token/template-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/modals/template-picker-modal.token.ts @@ -1,4 +1,4 @@ -import { UmbModalToken } from './modal-token.js'; +import { UmbModalToken } from '../../../core/modal/token/modal-token.js'; import type { UmbPickerModalValue, UmbTreePickerModalData } from '@umbraco-cms/backoffice/modal'; import type { UmbTemplateTreeItemModel } from '@umbraco-cms/backoffice/template'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/reload-tree-item-children/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/reload-tree-item-children/manifests.ts index df8d6f9e92..064e0c422f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/reload-tree-item-children/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/tree/reload-tree-item-children/manifests.ts @@ -4,9 +4,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ { type: 'entityAction', + kind: 'reloadTreeItemChildren', alias: 'Umb.EntityAction.Template.Tree.ReloadChildrenOf', name: 'Reload Template Tree Item Children Entity Action', - kind: 'reloadTreeItemChildren', forEntityTypes: [UMB_TEMPLATE_ROOT_ENTITY_TYPE, UMB_TEMPLATE_ENTITY_TYPE], }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/manifests.ts index 45b7b08d21..50da2717d3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/manifests.ts @@ -1,7 +1,7 @@ import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -19,9 +19,10 @@ const workspace: ManifestWorkspace = { const workspaceViews: Array = []; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.Template.Save', name: 'Save Template', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts index bd1f505327..b4fec1b5af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace-editor.element.ts @@ -8,7 +8,8 @@ import { toCamelCase } from '@umbraco-cms/backoffice/utils'; import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, query, state, nothing, ifDefined } from '@umbraco-cms/backoffice/external/lit'; import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; -import { UMB_MODAL_MANAGER_CONTEXT, UMB_TEMPLATE_PICKER_MODAL } from '@umbraco-cms/backoffice/modal'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UMB_TEMPLATE_PICKER_MODAL } from '@umbraco-cms/backoffice/template'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { Subject, debounceTime } from '@umbraco-cms/backoffice/external/rxjs'; @@ -39,6 +40,7 @@ export class UmbTemplateWorkspaceEditorElement extends UmbLitElement { #masterTemplateUnique: string | null = null; + // TODO: Revisit this code, to not use RxJS directly: private inputQuery$ = new Subject(); constructor() { @@ -110,11 +112,14 @@ export class UmbTemplateWorkspaceEditorElement extends UmbLitElement { #openInsertSectionModal() { const sectionModal = this.#modalContext?.open(this, UMB_TEMPLATING_SECTION_PICKER_MODAL); - sectionModal?.onSubmit().then((insertSectionModalValue) => { - if (insertSectionModalValue?.value) { - this._codeEditor?.insert(insertSectionModalValue.value); - } - }); + sectionModal + ?.onSubmit() + .then((insertSectionModalValue) => { + if (insertSectionModalValue?.value) { + this._codeEditor?.insert(insertSectionModalValue.value); + } + }) + .catch(() => undefined); } #resetMasterTemplate() { @@ -134,20 +139,26 @@ export class UmbTemplateWorkspaceEditorElement extends UmbLitElement { }, }); - modalContext?.onSubmit().then((value) => { - if (!value?.selection) return; - this.#templateWorkspaceContext?.setMasterTemplate(value.selection[0] ?? null); - }); + modalContext + ?.onSubmit() + .then((value) => { + if (!value?.selection) return; + this.#templateWorkspaceContext?.setMasterTemplate(value.selection[0] ?? null); + }) + .catch(() => undefined); } #openQueryBuilder() { const queryBuilderModal = this.#modalContext?.open(this, UMB_TEMPLATE_QUERY_BUILDER_MODAL); - queryBuilderModal?.onSubmit().then((queryBuilderModalValue) => { - if (queryBuilderModalValue?.value) { - this._codeEditor?.insert(getQuerySnippet(queryBuilderModalValue.value)); - } - }); + queryBuilderModal + ?.onSubmit() + .then((queryBuilderModalValue) => { + if (queryBuilderModalValue?.value) { + this._codeEditor?.insert(getQuerySnippet(queryBuilderModalValue.value)); + } + }) + .catch(() => undefined); } #renderMasterTemplatePicker() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts index 0093016c50..d87998cd82 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/workspace/template-workspace.context.ts @@ -10,6 +10,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbReloadTreeItemChildrenRequestEntityActionEvent } from '@umbraco-cms/backoffice/tree'; +import { UmbRequestReloadStructureForEntityEvent } from '@umbraco-cms/backoffice/event'; export class UmbTemplateWorkspaceContext extends UmbEditableWorkspaceContextBase @@ -171,10 +172,19 @@ ${currentContent}`; } else { const { data } = await this.detailRepository.save(this.#data.value); newData = data; + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); } if (newData) { this.#data.setValue(newData); + this.setIsNew(false); this.workspaceComplete(newData); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts index 084dee512b..94518a0814 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts @@ -8,12 +8,10 @@ import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui'; import type { EditorEvent, Editor, RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce'; import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api'; import { type ManifestTinyMcePlugin, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; import { css, customElement, html, property, query, state } from '@umbraco-cms/backoffice/external/lit'; import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; -import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; import { UmbStylesheetDetailRepository, UmbStylesheetRuleManager } from '@umbraco-cms/backoffice/stylesheet'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @@ -57,8 +55,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { #renderEditor?: typeof import('@umbraco-cms/backoffice/external/tinymce').renderEditor; #plugins: Array UmbTinyMcePluginBase> = []; #editorRef?: Editor | null = null; - #stylesheetRepository: UmbStylesheetDetailRepository; - #serverUrl?: string; + #stylesheetRepository = new UmbStylesheetDetailRepository(this); #umbStylesheetRuleManager = new UmbStylesheetRuleManager(); protected getFormElement() { @@ -81,19 +78,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { @query('#editor', true) private _editorElement?: HTMLElement; - constructor() { - super(); - - this.consumeContext(UMB_APP_CONTEXT, (instance) => { - this.#serverUrl = instance.getServerUrl(); - }); - - this.#stylesheetRepository = new UmbStylesheetDetailRepository(this); - } - - protected async firstUpdated(_changedProperties: PropertyValueMap | Map): Promise { - super.firstUpdated(_changedProperties); - + protected async firstUpdated(): Promise { // Here we want to start the loading of everything at first, not one at a time, which is why this code is not using await. const loadEditor = import('@umbraco-cms/backoffice/external/tinymce').then((tinyMce) => { this.#renderEditor = tinyMce.renderEditor; @@ -202,9 +187,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) { // Map the stylesheets with server url const stylesheets = - stylesheetPaths?.map( - (stylesheetPath: string) => `${this.#serverUrl}/css/${stylesheetPath.replace(/\\/g, '/')}`, - ) ?? []; + stylesheetPaths?.map((stylesheetPath: string) => `/css${stylesheetPath.replace(/\\/g, '/')}`) ?? []; stylesheets.push('/umbraco/backoffice/css/rte-content.css'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts index 63c807083d..3897c7be61 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts @@ -3,6 +3,8 @@ import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extensi import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import '../../components/input-tiny-mce/input-tiny-mce.element.js'; + type RichTextEditorValue = { blocks: object; markup: string; @@ -25,6 +27,9 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements public set config(config: UmbPropertyEditorConfigCollection | undefined) { this.#configuration = config; } + public get config() { + return this.#configuration; + } #onChange(event: InputEvent) { this.value = { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts index 1ef4351731..a5de5f04cd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/umbraco-package.ts @@ -1,9 +1,7 @@ -import './index.js'; - -export const name = 'Umbraco.Core.UmbracoNews'; +export const name = 'Umbraco.Core.TinyMce'; export const extensions = [ { - name: 'Umbraco News Bundle', + name: 'TinyMce Bundle', alias: 'Umb.Bundle.TinyMce', type: 'bundle', js: () => import('./manifests.js'), diff --git a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts index e22db293ed..d9a5135dbb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/umbraco-news/umbraco-news-dashboard.element.ts @@ -8,7 +8,7 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement { #currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE; @state() - private name = ''; + private _name = ''; constructor() { super(); @@ -21,22 +21,24 @@ export class UmbUmbracoNewsDashboardElement extends UmbLitElement { #observeCurrentUser(): void { if (!this.#currentUserContext) return; this.observe(this.#currentUserContext.currentUser, (user) => { - this.name = user?.name ?? ''; + this._name = user?.name ?? ''; }); } render() { return html` -

    Welcome, ${this.name}

    +

    + Welcome, ${this._name} +

    - This is a preview version of Umbraco, where you can have a first-hand look at the new Backoffice. + This is the beta version of Umbraco 14, where you can have a first-hand look at the new Backoffice.

    - There is currently very limited functionality.
    Please refer to the - documentation to learn more about - what is possible. + documentation to learn + more about what is possible. Here you will find excellent tutorials, guides, and references to help you get + started extending the Backoffice.

    `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-actions/manifests.ts index f7d0cf4e02..a2eaaa30d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-actions/manifests.ts @@ -5,9 +5,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.UserGroup.Delete', name: 'Delete User Group Entity Action', - kind: 'delete', forEntityTypes: [UMB_USER_GROUP_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_USER_GROUP_DETAIL_REPOSITORY_ALIAS, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/manifests.ts index becb79a51b..15d3ef2d26 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/manifests.ts @@ -1,6 +1,6 @@ import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; @@ -16,9 +16,10 @@ const workspace: ManifestWorkspace = { }; const workspaceViews: Array = []; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.UserGroup.Save', name: 'Save User Group Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace-editor.element.ts index 4f817ae4f3..1ebbfd9a87 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group-workspace-editor.element.ts @@ -7,7 +7,7 @@ import { css, html, nothing, customElement, state } from '@umbraco-cms/backoffic import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbInputDocumentElement } from '@umbraco-cms/backoffice/document'; -import type { UmbInputSectionElement } from '@umbraco-cms/backoffice/components'; +import type { UmbInputSectionElement } from '@umbraco-cms/backoffice/section'; import type { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; import type { UmbInputMediaElement } from '@umbraco-cms/backoffice/media'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/conditions/user-permission.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/conditions/user-permission.condition.ts index f32360bebb..b8074b9fdc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/conditions/user-permission.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/conditions/user-permission.condition.ts @@ -6,13 +6,14 @@ import type { UmbConditionControllerArguments, UmbExtensionCondition, } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbUserPermissionCondition extends UmbConditionBase implements UmbExtensionCondition { - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => { this.observe( diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-action-base.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-action-base.condition.ts index 9275898c3f..7ee7a2de5c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-action-base.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/conditions/user-allow-action-base.condition.ts @@ -1,5 +1,6 @@ import type { UmbUserStateEnum } from '../types.js'; import { UMB_USER_WORKSPACE_CONTEXT } from '../workspace/user-workspace.context.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { isCurrentUser } from '@umbraco-cms/backoffice/current-user'; import type { UmbConditionConfigBase, @@ -15,8 +16,8 @@ export abstract class UmbUserActionConditionBase protected userUnique?: string; protected userState?: UmbUserStateEnum | null; - constructor(args: UmbConditionControllerArguments) { - super(args); + constructor(host: UmbControllerHost, args: UmbConditionControllerArguments) { + super(host, args); this.consumeContext(UMB_USER_WORKSPACE_CONTEXT, (context) => { this.observe( diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts index 4d4d239dcb..879e196811 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/entity-actions/manifests.ts @@ -1,9 +1,4 @@ -import { - UMB_CHANGE_USER_PASSWORD_REPOSITORY_ALIAS, - UMB_UNLOCK_USER_REPOSITORY_ALIAS, - UMB_USER_DETAIL_REPOSITORY_ALIAS, - UMB_USER_ITEM_REPOSITORY_ALIAS, -} from '../repository/index.js'; +import { UMB_USER_DETAIL_REPOSITORY_ALIAS, UMB_USER_ITEM_REPOSITORY_ALIAS } from '../repository/index.js'; import { UMB_USER_ENTITY_TYPE } from '../entity.js'; import { UmbDisableUserEntityAction } from './disable/disable-user.action.js'; import { UmbEnableUserEntityAction } from './enable/enable-user.action.js'; @@ -14,9 +9,9 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'delete', alias: 'Umb.EntityAction.User.Delete', name: 'Delete User Entity Action', - kind: 'delete', forEntityTypes: [UMB_USER_ENTITY_TYPE], meta: { detailRepositoryAlias: UMB_USER_DETAIL_REPOSITORY_ALIAS, @@ -30,6 +25,7 @@ const entityActions: Array = [ }, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.User.Enable', name: 'Enable User Entity Action', weight: 800, @@ -47,6 +43,7 @@ const entityActions: Array = [ }, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.User.Disable', name: 'Disable User Entity Action', weight: 700, @@ -64,6 +61,7 @@ const entityActions: Array = [ }, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.User.ChangePassword', name: 'Change User Password Entity Action', weight: 600, @@ -72,11 +70,11 @@ const entityActions: Array = [ meta: { icon: 'icon-key', label: 'Change Password', - repositoryAlias: UMB_CHANGE_USER_PASSWORD_REPOSITORY_ALIAS, }, }, { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.User.Unlock', name: 'Unlock User Entity Action', weight: 600, @@ -85,7 +83,6 @@ const entityActions: Array = [ meta: { icon: 'icon-unlocked', label: 'Unlock', - repositoryAlias: UMB_UNLOCK_USER_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/entity-action/manifests.ts index 43e8d4feba..bfce6da496 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/invite/entity-action/manifests.ts @@ -6,6 +6,7 @@ import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry'; const entityActions: Array = [ { type: 'entityAction', + kind: 'default', alias: 'Umb.EntityAction.User.ResendInvite', name: 'Resend Invite User Entity Action', weight: 500, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/manifests.ts index 52d3080945..d3dc70aac5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/manifests.ts @@ -2,7 +2,7 @@ import { UMB_USER_ENTITY_TYPE } from '../entity.js'; import { UmbSaveWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; import type { ManifestWorkspace, - ManifestWorkspaceAction, + ManifestWorkspaceActions, ManifestWorkspaceView, } from '@umbraco-cms/backoffice/extension-registry'; @@ -19,9 +19,10 @@ const workspace: ManifestWorkspace = { }; const workspaceViews: Array = []; -const workspaceActions: Array = [ +const workspaceActions: Array = [ { type: 'workspaceAction', + kind: 'default', alias: 'Umb.WorkspaceAction.User.Save', name: 'Save User Workspace Action', api: UmbSaveWorkspaceAction, diff --git a/src/Umbraco.Web.UI.Client/storybook/stories/umb-controller.mdx b/src/Umbraco.Web.UI.Client/storybook/stories/umb-controller.mdx deleted file mode 100644 index c0787a2a02..0000000000 --- a/src/Umbraco.Web.UI.Client/storybook/stories/umb-controller.mdx +++ /dev/null @@ -1,20 +0,0 @@ -import { Meta } from '@storybook/addon-docs'; - - - -# Umbraco Controller - -This class can be used as the base of any class. -This will enable Controllers to be hosted in this class. Additionally it provides few shortcut methods for initializing core Umbraco Controllers. - -```ts -observe(source: Observable, callback: (_value: T) => void, unique?: string): UmbObserverController - -provideContext(alias: string | UmbContextToken, instance: R): UmbContextProviderController - -consumeContext(alias: string | UmbContextToken, callback: UmbContextCallback): UmbContextConsumerController -``` - -Read about the 'observe' method in the [Store-API](?path=/docs/guides-store--docs). - -Read about the 'provideContext' and 'consumeContext' methods in the [Context-API](?path=/docs/guides-context-api--docs). diff --git a/src/Umbraco.Web.UI.Client/storybook/stories/umb-element.mdx b/src/Umbraco.Web.UI.Client/storybook/stories/umb-element.mdx deleted file mode 100644 index 9798bee7f7..0000000000 --- a/src/Umbraco.Web.UI.Client/storybook/stories/umb-element.mdx +++ /dev/null @@ -1,36 +0,0 @@ -import { Meta } from '@storybook/addon-docs'; - - - -# Umbraco Element - -This element can be used as the base of any element. -This will enable Controllers to be hosted at this element. Additionally it provides few shortcut methods for initializing core Umbraco Controllers. - -```ts -observe(source: Observable, callback: (_value: T) => void, unique?: string): UmbObserverController - -provideContext(alias: string | UmbContextToken, instance: R): UmbContextProviderController - -consumeContext(alias: string | UmbContextToken, callback: UmbContextCallback): UmbContextConsumerController -``` - -Use these for an smooth consumption, like this request for a Context API using a simple string context, where the callback value is of an unknown type: - -```ts -this.consumeContext('requestThisContextAlias', (context) => { - // Notice this is a subscription, as context might change or a new one appears. - console.log("I've got the context", context); -}); -``` - -Or use the a Context Token to get a typed context: - -```ts -import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; - -this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => { - // Notice this is a subscription, as context might change or a new one appears, but the value is strongly typed - console.log("I've got the context", context); -}); -``` diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 2a82dbb81e..364077e447 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -62,6 +62,7 @@ "@umbraco-cms/backoffice/lit-element": ["./src/packages/core/lit-element/index.ts"], "@umbraco-cms/backoffice/localization": ["./src/packages/core/localization/index.ts"], "@umbraco-cms/backoffice/log-viewer": ["./src/packages/log-viewer/index.ts"], + "@umbraco-cms/backoffice/markdown-editor": ["./src/packages/markdown-editor/index.ts"], "@umbraco-cms/backoffice/media-type": ["./src/packages/media/media-types/index.ts"], "@umbraco-cms/backoffice/media": ["./src/packages/media/media/index.ts"], "@umbraco-cms/backoffice/member-group": ["./src/packages/members/member-group/index.ts"],