From 2d69eb66efec236057b4c4d57acbf0dc9402a94f Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 18 Nov 2024 15:03:15 +0100 Subject: [PATCH] Feature: Content Type Workspace Context Base (#17542) * Create content-type-workspace-context-base.ts * make detail model with entityType * allow repository alias * export base class * fix type check * add method to get unpersisted changes * remove duplicate code * remove duplicate code * remove duplicate code * wip porting code to the base class * improve extendability * clean up * clean up * move logic to base * allow to preset the scaffold * pass preset * add public tag * clean up * simplify the number of places we store the entity type * add js docs * rename private method to clear * remove debugger * use flag instead of a data state * set persisted data after create + update * Update entity-detail-workspace-base.ts * add js docs * add protected tag * call super * make linter happy * add comment * type casting * no need create observables for unique and entityType it is already handled * add null check --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- .../content-type-structure-manager.class.ts | 62 ++++- .../src/packages/core/content-type/types.ts | 12 +- .../content-type-workspace-context-base.ts | 249 +++++++++++++++++ .../core/content-type/workspace/index.ts | 1 + .../content-detail-workspace-base.ts | 2 - .../entity-detail-workspace-base.ts | 78 ++++-- .../document-type-workspace.context.ts | 255 ++++-------------- ...ument-workspace-view-info-links.element.ts | 1 + .../workspace/media-type-workspace.context.ts | 198 ++------------ .../member-type-workspace.context.ts | 203 +++----------- 10 files changed, 477 insertions(+), 584 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts index eb0826f824..8aa1fe7517 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts @@ -17,6 +17,8 @@ import { } from '@umbraco-cms/backoffice/observable-api'; import { incrementString } from '@umbraco-cms/backoffice/utils'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; type UmbPropertyTypeId = UmbPropertyTypeModel['id']; @@ -35,7 +37,16 @@ export class UmbContentTypeStructureManager< > extends UmbControllerBase { #init!: Promise; - #repository: UmbDetailRepository; + #repository?: UmbDetailRepository; + #initRepositoryResolver?: () => void; + + #initRepository = new Promise((resolve) => { + if (this.#repository) { + resolve(); + } else { + this.#initRepositoryResolver = resolve; + } + }); #ownerContentTypeUnique?: string; #contentTypeObservers = new Array(); @@ -84,9 +95,15 @@ export class UmbContentTypeStructureManager< return this.#containers.asObservablePart((x) => x.find((y) => y.id === id)); } - constructor(host: UmbControllerHost, typeRepository: UmbDetailRepository) { + constructor(host: UmbControllerHost, typeRepository: UmbDetailRepository | string) { super(host); - this.#repository = typeRepository; + + if (typeof typeRepository === 'string') { + this.#observeRepository(typeRepository); + } else { + this.#repository = typeRepository; + this.#initRepositoryResolver?.(); + } // Observe owner content type compositions, as we only allow one level of compositions at this moment. [NL] // But, we could support more, we would just need to flatMap all compositions and make sure the entries are unique and then base the observation on that. [NL] @@ -107,7 +124,7 @@ export class UmbContentTypeStructureManager< public async loadType(unique?: string) { //if (!unique) return; //if (this.#ownerContentTypeUnique === unique) return; - this._reset(); + this.#clear(); this.#ownerContentTypeUnique = unique; @@ -117,10 +134,11 @@ export class UmbContentTypeStructureManager< return promise; } - public async createScaffold() { - this._reset(); + public async createScaffold(preset?: Partial) { + await this.#initRepository; + this.#clear(); - const { data } = await this.#repository.createScaffold(); + const { data } = await this.#repository!.createScaffold(preset); if (!data) return {}; this.#ownerContentTypeUnique = data.unique; @@ -135,10 +153,11 @@ export class UmbContentTypeStructureManager< * @returns {Promise} - A promise that will be resolved when the content type is saved. */ public async save() { + await this.#initRepository; const contentType = this.getOwnerContentType(); if (!contentType || !contentType.unique) throw new Error('Could not find the Content Type to save'); - const { error, data } = await this.#repository.save(contentType); + const { error, data } = await this.#repository!.save(contentType); if (error || !data) { throw error?.message ?? 'Repository did not return data after save.'; } @@ -155,12 +174,13 @@ export class UmbContentTypeStructureManager< * @returns {Promise} - a promise that is resolved when the content type has been created. */ public async create(parentUnique: string | null) { + await this.#initRepository; const contentType = this.getOwnerContentType(); if (!contentType || !contentType.unique) { throw new Error('Could not find the Content Type to create'); } - const { data } = await this.#repository.create(contentType, parentUnique); + const { data } = await this.#repository!.create(contentType, parentUnique); if (!data) return Promise.reject(); // Update state with latest version: @@ -200,9 +220,10 @@ export class UmbContentTypeStructureManager< async #loadType(unique?: string) { if (!unique) return {}; + await this.#initRepository; // Lets initiate the content type: - const { data, asObservable } = await this.#repository.requestByUnique(unique); + const { data, asObservable } = await this.#repository!.requestByUnique(unique); if (!data) return {}; await this.#observeContentType(data); @@ -211,12 +232,13 @@ export class UmbContentTypeStructureManager< async #observeContentType(data: T) { if (!data.unique) return; + await this.#initRepository; // Notice we do not store the content type in the store here, cause it will happen shortly after when the observations gets its first initial callback. [NL] const ctrl = this.observe( // Then lets start observation of the content type: - await this.#repository.byUnique(data.unique), + await this.#repository!.byUnique(data.unique), (docType) => { if (docType) { this.#contentTypes.appendOne(docType); @@ -725,13 +747,29 @@ export class UmbContentTypeStructureManager< ); } - private _reset() { + #observeRepository(repositoryAlias: string) { + if (!repositoryAlias) throw new Error('Content Type structure manager must have a repository alias.'); + + new UmbExtensionApiInitializer>>( + this, + umbExtensionsRegistry, + repositoryAlias, + [this._host], + (permitted, ctrl) => { + this.#repository = permitted ? ctrl.api : undefined; + this.#initRepositoryResolver?.(); + }, + ); + } + + #clear() { this.#contentTypes.setValue([]); this.#contentTypeObservers.forEach((observer) => observer.destroy()); this.#contentTypeObservers = []; this.#contentTypes.setValue([]); this.#containers.setValue([]); } + public override destroy() { this.#contentTypes.destroy(); this.#containers.destroy(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/types.ts index 3d76913002..857c3b0705 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/types.ts @@ -12,7 +12,13 @@ export interface UmbPropertyTypeContainerModel { type: UmbPropertyContainerTypes; sortOrder: number; } - +/** + * + * @deprecated + * This model is deprecated and will be removed in version 17. Please use the UmbContentTypeDetailModel instead. + * @export + * @interface UmbContentTypeModel + */ export interface UmbContentTypeModel { unique: string; name: string; @@ -30,6 +36,10 @@ export interface UmbContentTypeModel { collection: UmbReferenceByUnique | null; } +export interface UmbContentTypeDetailModel extends UmbContentTypeModel { + entityType: string; +} + export interface UmbPropertyTypeScaffoldModel extends Omit { dataType?: UmbPropertyTypeModel['dataType']; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts new file mode 100644 index 0000000000..88088bd72f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts @@ -0,0 +1,249 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; +import { + UmbEntityDetailWorkspaceContextBase, + type UmbEntityDetailWorkspaceContextArgs, + type UmbEntityDetailWorkspaceContextCreateArgs, + type UmbRoutableWorkspaceContext, +} from '@umbraco-cms/backoffice/workspace'; +import type { UmbContentTypeWorkspaceContext } from './content-type-workspace-context.interface.js'; +import type { UmbContentTypeCompositionModel, UmbContentTypeDetailModel, UmbContentTypeSortModel } from '../types.js'; +import { UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { UmbContentTypeStructureManager } from '../structure/index.js'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import { jsonStringComparison, type Observable } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { + UmbRequestReloadChildrenOfEntityEvent, + UmbRequestReloadStructureForEntityEvent, +} from '@umbraco-cms/backoffice/entity-action'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface UmbContentTypeWorkspaceContextArgs extends UmbEntityDetailWorkspaceContextArgs {} + +export abstract class UmbContentTypeWorkspaceContextBase< + DetailModelType extends UmbContentTypeDetailModel = UmbContentTypeDetailModel, + DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository, + > + extends UmbEntityDetailWorkspaceContextBase + implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext +{ + public readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT = true; + + public readonly name: Observable; + public readonly alias: Observable; + public readonly description: Observable; + public readonly icon: Observable; + + public readonly allowedAtRoot: Observable; + public readonly variesByCulture: Observable; + public readonly variesBySegment: Observable; + public readonly isElement: Observable; + public readonly allowedContentTypes: Observable | undefined>; + public readonly compositions: Observable | undefined>; + public readonly collection: Observable; + + public readonly structure: UmbContentTypeStructureManager; + + constructor(host: UmbControllerHost, args: UmbContentTypeWorkspaceContextArgs) { + super(host, args); + + this.structure = new UmbContentTypeStructureManager(this, args.detailRepositoryAlias); + + this.addValidationContext(new UmbValidationContext(this)); + + 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); + this.collection = this.structure.ownerContentTypeObservablePart((data) => data?.collection); + } + + /** + * Creates a new scaffold + * @param { UmbEntityDetailWorkspaceContextCreateArgs } args The arguments for creating a new scaffold + * @returns { Promise } The new scaffold + */ + public override async createScaffold( + args: UmbEntityDetailWorkspaceContextCreateArgs, + ): Promise { + this.resetState(); + this.setParent(args.parent); + + const request = this.structure.createScaffold(args.preset); + this._getDataPromise = request; + let { data } = await request; + if (!data) return undefined; + + this.setUnique(data.unique); + + if (this.modalContext) { + data = { ...data, ...this.modalContext.data.preset }; + } + + this.setIsNew(true); + this._data.setPersisted(data); + + return data; + } + + /** + * Loads the data for the workspace + * @param { string } unique The unique identifier of the data to load + * @returns { Promise } The loaded data + */ + override async load(unique: string) { + this.resetState(); + this.setUnique(unique); + this._getDataPromise = this.structure.loadType(unique); + const response = await this._getDataPromise; + const data = response.data; + + if (data) { + this._data.setPersisted(data); + this.setIsNew(false); + } + + return response; + } + + /** + * Creates the Content Type + * @param { DetailModelType } currentData The current data + * @param { UmbEntityModel } parent The parent entity + * @memberof UmbContentTypeWorkspaceContextBase + */ + override async _create(currentData: DetailModelType, parent: UmbEntityModel) { + try { + await this.structure.create(parent?.unique); + + this._data.setPersisted(this.structure.getOwnerContentType()); + + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadChildrenOfEntityEvent({ + entityType: parent.entityType, + unique: parent.unique, + }); + eventContext.dispatchEvent(event); + + this.setIsNew(false); + } catch (error) { + console.error(error); + } + } + + /** + * Updates the content type for the workspace + * @memberof UmbContentTypeWorkspaceContextBase + */ + override async _update() { + try { + await this.structure.save(); + + this._data.setPersisted(this.structure.getOwnerContentType()); + + const actionEventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + const event = new UmbRequestReloadStructureForEntityEvent({ + unique: this.getUnique()!, + entityType: this.getEntityType(), + }); + + actionEventContext.dispatchEvent(event); + } catch (error) { + console.error(error); + } + } + + /** + * Gets the name of the content type + * @returns { string | undefined } The name of the content type + */ + public getName(): string | undefined { + return this.structure.getOwnerContentType()?.name; + } + + /** + * Sets the name of the content type + * @param { string } name The name of the content type + */ + public setName(name: string) { + this.structure.updateOwnerContentType({ name } as Partial); + } + + /** + * Gets the alias of the content type + * @returns { string | undefined } The alias of the content type + */ + public getAlias(): string | undefined { + return this.structure.getOwnerContentType()?.alias; + } + + /** + * Sets the alias of the content type + * @param { string } alias The alias of the content type + */ + public setAlias(alias: string) { + this.structure.updateOwnerContentType({ alias } as Partial); + } + + /** + * Gets the description of the content type + * @returns { string | undefined } The description of the content type + */ + public getDescription(): string | undefined { + return this.structure.getOwnerContentType()?.description; + } + + /** + * Sets the description of the content type + * @param { string } description The description of the content type + */ + public setDescription(description: string) { + this.structure.updateOwnerContentType({ description } as Partial); + } + + /** + * Gets the compositions of the content type + * @returns { string | undefined } The icon of the content type + */ + public getCompositions(): Array | undefined { + return this.structure.getOwnerContentType()?.compositions; + } + + /** + * Sets the compositions of the content type + * @param { string } compositions The compositions of the content type + * @returns { void } + * + */ + public setCompositions(compositions: Array) { + this.structure.updateOwnerContentType({ compositions } as Partial); + } + + // TODO: manage setting icon color alias? + public setIcon(icon: string) { + this.structure.updateOwnerContentType({ icon } as Partial); + } + + public override getData() { + return this.structure.getOwnerContentType(); + } + + protected override _getHasUnpersistedChanges(): boolean { + const currentData = this.structure.getOwnerContentType(); + const persistedData = this._data.getPersisted(); + return jsonStringComparison(persistedData, currentData) === false; + } + + public override destroy(): void { + this.structure.destroy(); + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/index.ts index 352572934d..176e98907a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/index.ts @@ -1,3 +1,4 @@ export type * from './content-type-workspace-context.interface.js'; export * from './content-type-workspace.context-token.js'; export * from './views/design/content-type-design-editor-property.context-token.js'; +export * from './content-type-workspace-context-base.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts index d4504f8ee1..bd3b2f9517 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts @@ -78,8 +78,6 @@ export abstract class UmbContentDetailWorkspaceContextBase< /* Content Data */ protected override readonly _data = new UmbContentWorkspaceDataManager(this); - public override readonly entityType = this._data.createObservablePartOfCurrent((data) => data?.entityType); - public override readonly unique = this._data.createObservablePartOfCurrent((data) => data?.unique); public readonly values = this._data.createObservablePartOfCurrent((data) => data?.values); public readonly variants = this._data.createObservablePartOfCurrent((data) => data?.variants ?? []); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts index 855370f93a..b6af4f2728 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts @@ -43,18 +43,19 @@ export abstract class UmbEntityDetailWorkspaceContextBase< protected readonly _data = new UmbEntityWorkspaceDataManager(this); public readonly data = this._data.current; - public readonly entityType = this._data.createObservablePartOfCurrent((data) => data?.entityType); - public readonly unique = this._data.createObservablePartOfCurrent((data) => data?.unique); protected _getDataPromise?: Promise; protected _detailRepository?: DetailRepositoryType; #entityContext = new UmbEntityContext(this); - #entityType: string; + public readonly entityType = this.#entityContext.entityType; + public readonly unique = this.#entityContext.unique; #parent = new UmbObjectState<{ entityType: string; unique: UmbEntityUnique } | undefined>(undefined); - readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); - readonly parentEntityType = this.#parent.asObservablePart((parent) => (parent ? parent.entityType : undefined)); + public readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); + public readonly parentEntityType = this.#parent.asObservablePart((parent) => + parent ? parent.entityType : undefined, + ); #initResolver?: () => void; #initialized = false; @@ -67,10 +68,9 @@ export abstract class UmbEntityDetailWorkspaceContextBase< } }); - constructor(host: UmbControllerHost, args: UmbEntityWorkspaceContextArgs) { + constructor(host: UmbControllerHost, args: UmbEntityDetailWorkspaceContextArgs) { super(host, args.workspaceAlias); - this.#entityType = args.entityType; - this.#entityContext.setEntityType(this.#entityType); + this.#entityContext.setEntityType(args.entityType); window.addEventListener('willchangestate', this.#onWillNavigate); this.#observeRepository(args.detailRepositoryAlias); } @@ -80,7 +80,9 @@ export abstract class UmbEntityDetailWorkspaceContextBase< * @returns { string } The entity type */ getEntityType(): string { - return this.#entityType; + const entityType = this.#entityContext.getEntityType(); + if (!entityType) throw new Error('Entity type is not set'); + return entityType; } /** @@ -96,7 +98,11 @@ export abstract class UmbEntityDetailWorkspaceContextBase< * @returns { string | undefined } The unique identifier */ getUnique(): UmbEntityUnique | undefined { - return this._data.getCurrent()?.unique; + return this.getData()?.unique; + } + + setUnique(unique: string) { + this.#entityContext.setUnique(unique); } /** @@ -107,6 +113,10 @@ export abstract class UmbEntityDetailWorkspaceContextBase< return this.#parent.getValue(); } + setParent(parent: UmbEntityModel) { + this.#parent.setValue(parent); + } + /** * Get the parent unique * @returns { string | undefined } The parent unique identifier @@ -120,7 +130,6 @@ export abstract class UmbEntityDetailWorkspaceContextBase< } async load(unique: string) { - this.#entityContext.setEntityType(this.#entityType); this.#entityContext.setUnique(unique); await this.#init; this.resetState(); @@ -156,21 +165,22 @@ export abstract class UmbEntityDetailWorkspaceContextBase< * @param {Partial} args.preset The preset data. * @returns { Promise | undefined } The data of the scaffold. */ - async createScaffold(args: CreateArgsType) { + public async createScaffold(args: CreateArgsType) { await this.#init; this.resetState(); - this.#parent.setValue(args.parent); + this.setParent(args.parent); + const request = this._detailRepository!.createScaffold(args.preset); this._getDataPromise = request; let { data } = await request; if (!data) return undefined; - this.#entityContext.setEntityType(this.#entityType); this.#entityContext.setUnique(data.unique); if (this.modalContext) { data = { ...data, ...this.modalContext.data.preset }; } + this.setIsNew(true); this._data.setPersisted(data); this._data.setCurrent(data); @@ -180,7 +190,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< async submit() { await this.#init; - const currentData = this._data.getCurrent(); + const currentData = this.getData(); if (!currentData) { throw new Error('Data is not set'); @@ -191,9 +201,12 @@ export abstract class UmbEntityDetailWorkspaceContextBase< } if (this.getIsNew()) { - await this.#create(currentData); + const parent = this.#parent.getValue(); + if (parent?.unique === undefined) throw new Error('Parent unique is missing'); + if (!parent.entityType) throw new Error('Parent entity type is missing'); + await this._create(currentData, parent); } else { - await this.#update(currentData); + await this._update(currentData); } } @@ -217,12 +230,9 @@ export abstract class UmbEntityDetailWorkspaceContextBase< return !newUrl.includes(this.routes.getActiveLocalPath()); } - async #create(currentData: DetailModelType) { + protected async _create(currentData: DetailModelType, parent: UmbEntityModel) { if (!this._detailRepository) throw new Error('Detail repository is not set'); - const parent = this.#parent.getValue(); - if (!parent) throw new Error('Parent is not set'); - const { error, data } = await this._detailRepository.create(currentData, parent.unique); if (error || !data) { throw error?.message ?? 'Repository did not return data after create.'; @@ -240,7 +250,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< this.setIsNew(false); } - async #update(currentData: DetailModelType) { + protected async _update(currentData: DetailModelType) { const { error, data } = await this._detailRepository!.save(currentData); if (error || !data) { throw error?.message ?? 'Repository did not return data after create.'; @@ -258,16 +268,26 @@ export abstract class UmbEntityDetailWorkspaceContextBase< actionEventContext.dispatchEvent(event); } + #allowNavigateAway = false; + #onWillNavigate = async (e: CustomEvent) => { const newUrl = e.detail.url; + if (this.#allowNavigateAway) { + return true; + } + /* TODO: temp removal of discard changes in workspace modals. The modal closes before the discard changes dialog is resolved.*/ if (newUrl.includes('/modal/umb-modal-workspace/')) { return true; } - if (this._checkWillNavigateAway(newUrl) && this._data.getHasUnpersistedChanges()) { + if (this._checkWillNavigateAway(newUrl) && this._getHasUnpersistedChanges()) { + /* Since ours modals are async while events are synchronous, we need to prevent the default behavior of the event, even if the modal hasn’t been resolved yet. + Once the modal is resolved (the user accepted to discard the changes and navigate away from the route), we will push a new history state. + This push will make the "willchangestate" event happen again and due to this somewhat "backward" behavior, + we set an "allowNavigateAway"-flag to prevent the "discard-changes" functionality from running in a loop.*/ e.preventDefault(); const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); const modal = modalManager.open(this, UMB_DISCARD_CHANGES_MODAL); @@ -275,8 +295,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< try { // navigate to the new url when discarding changes await modal.onSubmit(); - // Reset the current data so we don't end in a endless loop of asking to discard changes. - this._data.resetCurrent(); + this.#allowNavigateAway = true; history.pushState({}, '', e.detail.url); return true; } catch { @@ -287,9 +306,18 @@ export abstract class UmbEntityDetailWorkspaceContextBase< return true; }; + /** + * Check if there are unpersisted changes. + * @returns { boolean } true if there are unpersisted changes. + */ + protected _getHasUnpersistedChanges(): boolean { + return this._data.getHasUnpersistedChanges(); + } + override resetState() { super.resetState(); this._data.clear(); + this.#allowNavigateAway = false; } #checkIfInitialized() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace.context.ts index 150015820a..2c1c805d2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace.context.ts @@ -7,99 +7,42 @@ import { } from '../../paths.js'; import type { UmbDocumentTypeDetailModel } from '../../types.js'; import { UMB_DOCUMENT_TYPE_ENTITY_TYPE } from '../../entity.js'; -import { UmbDocumentTypeDetailRepository } from '../../repository/detail/document-type-detail.repository.js'; import { UmbDocumentTypeWorkspaceEditorElement } from './document-type-workspace-editor.element.js'; -import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContentTypeWorkspaceContextBase } from '@umbraco-cms/backoffice/content-type'; import { - UmbRequestReloadChildrenOfEntityEvent, - UmbRequestReloadStructureForEntityEvent, -} from '@umbraco-cms/backoffice/entity-action'; -import { - UmbSubmittableWorkspaceContextBase, UmbWorkspaceIsNewRedirectController, UmbWorkspaceIsNewRedirectControllerAlias, } from '@umbraco-cms/backoffice/workspace'; -import { UmbTemplateDetailRepository } from '@umbraco-cms/backoffice/template'; -import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; -import type { - UmbContentTypeCompositionModel, - UmbContentTypeSortModel, - UmbContentTypeWorkspaceContext, -} from '@umbraco-cms/backoffice/content-type'; +import type { UmbContentTypeSortModel, UmbContentTypeWorkspaceContext } from '@umbraco-cms/backoffice/content-type'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; import type { UmbRoutableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import type { UmbPathPatternTypeAsEncodedParamsType } from '@umbraco-cms/backoffice/router'; -import { UmbValidationContext } from '@umbraco-cms/backoffice/validation'; +import { UMB_DOCUMENT_TYPE_WORKSPACE_ALIAS } from './constants.js'; +import { UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS } from '../../repository/index.js'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UmbTemplateDetailRepository } from '@umbraco-cms/backoffice/template'; -type EntityType = UmbDocumentTypeDetailModel; +type DetailModelType = UmbDocumentTypeDetailModel; export class UmbDocumentTypeWorkspaceContext - extends UmbSubmittableWorkspaceContextBase - implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext + extends UmbContentTypeWorkspaceContextBase + implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext { - readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT = true; - // - readonly repository = new UmbDocumentTypeDetailRepository(this); - // Data/Draft is located in structure manager - - #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); - readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); - readonly parentEntityType = this.#parent.asObservablePart((parent) => (parent ? parent.entityType : undefined)); - - #persistedData = new UmbObjectState(undefined); - - // General for content types: - //readonly data; - readonly unique; - readonly entityType; - readonly name; - getName(): string | undefined { - return this.structure.getOwnerContentType()?.name; - } - readonly alias; - readonly description; - readonly icon; - - readonly allowedAtRoot; - readonly variesByCulture; - readonly variesBySegment; - readonly isElement; - readonly allowedContentTypes; - readonly compositions; - readonly collection; - // Document type specific: readonly allowedTemplateIds; readonly defaultTemplate; readonly cleanup; - readonly structure = new UmbContentTypeStructureManager(this, this.repository); - createTemplateMode: boolean = false; + #templateRepository = new UmbTemplateDetailRepository(this); + constructor(host: UmbControllerHost) { - super(host, 'Umb.Workspace.DocumentType'); - - this.addValidationContext(new UmbValidationContext(this)); - - // General for content types: - //this.data = this.structure.ownerContentType; - - this.unique = this.structure.ownerContentTypeObservablePart((data) => data?.unique); - this.entityType = this.structure.ownerContentTypeObservablePart((data) => data?.entityType); - - this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name); - this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias); - this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description); - 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); - this.collection = this.structure.ownerContentTypeObservablePart((data) => data?.collection); + super(host, { + workspaceAlias: UMB_DOCUMENT_TYPE_WORKSPACE_ALIAS, + entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE, + detailRepositoryAlias: UMB_DOCUMENT_TYPE_DETAIL_REPOSITORY_ALIAS, + }); // Document type specific: this.allowedTemplateIds = this.structure.ownerContentTypeObservablePart((data) => data?.allowedTemplates); @@ -117,10 +60,12 @@ export class UmbDocumentTypeWorkspaceContext const parentEntityType = params.parentEntityType; const parentUnique = params.parentUnique === 'null' ? null : params.parentUnique; const presetAlias = params.presetAlias === 'null' ? null : (params.presetAlias ?? null); + if (parentUnique === undefined) { throw new Error('ParentUnique url parameter is required to create a document type'); } - await this.create({ entityType: parentEntityType, unique: parentUnique }, presetAlias); + + await this.#onScaffoldSetup({ entityType: parentEntityType, unique: parentUnique }, presetAlias); new UmbWorkspaceIsNewRedirectController( this, @@ -141,40 +86,6 @@ export class UmbDocumentTypeWorkspaceContext ]); } - protected override resetState(): void { - super.resetState(); - this.#persistedData.setValue(undefined); - } - - getData() { - return this.structure.getOwnerContentType(); - } - - getUnique() { - return this.getData()?.unique; - } - - getEntityType() { - return UMB_DOCUMENT_TYPE_ENTITY_TYPE; - } - - setName(name: string) { - this.structure.updateOwnerContentType({ name }); - } - - setAlias(alias: string) { - this.structure.updateOwnerContentType({ alias }); - } - - setDescription(description: string) { - this.structure.updateOwnerContentType({ description }); - } - - // TODO: manage setting icon color alias? - setIcon(icon: string) { - this.structure.updateOwnerContentType({ icon }); - } - setAllowedAtRoot(allowedAtRoot: boolean) { this.structure.updateOwnerContentType({ allowedAtRoot }); } @@ -199,10 +110,6 @@ export class UmbDocumentTypeWorkspaceContext this.structure.updateOwnerContentType({ cleanup }); } - setCompositions(compositions: Array) { - this.structure.updateOwnerContentType({ compositions }); - } - setCollection(collection: UmbReferenceByUnique) { this.structure.updateOwnerContentType({ collection }); } @@ -220,115 +127,69 @@ export class UmbDocumentTypeWorkspaceContext this.structure.updateOwnerContentType({ defaultTemplate }); } - async create(parent: { entityType: string; unique: string | null }, presetAlias: string | null) { - this.resetState(); - this.#parent.setValue(parent); - const { data } = await this.structure.createScaffold(); - if (!data) return undefined; + async #onScaffoldSetup(parent: UmbEntityModel, presetAlias: string | null) { + let preset: Partial | undefined = undefined; switch (presetAlias) { case UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PRESET_TEMPLATE satisfies UmbCreateDocumentTypeWorkspacePresetType: { - this.setIcon('icon-document-html'); + preset = { + icon: 'icon-document-html', + }; this.createTemplateMode = true; break; } case UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PRESET_ELEMENT satisfies UmbCreateDocumentTypeWorkspacePresetType: { - this.setIcon('icon-plugin'); - this.setIsElement(true); + preset = { + icon: 'icon-plugin', + isElement: true, + }; break; } default: break; } - this.setIsNew(true); - - this.#persistedData.setValue(this.structure.getOwnerContentType()); - - return data; + this.createScaffold({ parent, preset }); } - async load(unique: string) { - this.resetState(); - const { data, asObservable } = await this.structure.loadType(unique); - - if (data) { - this.setIsNew(false); - this.#persistedData.update(data); + override async _create(currentData: DetailModelType, parent: UmbEntityModel) { + // TODO: move this responsibility to the template package + if (this.createTemplateMode) { + await this.#createAndAssignTemplate(); } - if (asObservable) { - this.observe(asObservable(), (entity) => this.#onStoreChange(entity), 'umbDocumentTypeStoreObserver'); + try { + super._create(currentData, parent); + this.createTemplateMode = false; + } catch (error) { + console.log(error); } } - #onStoreChange(entity: EntityType | undefined) { - if (!entity) { - //TODO: This solution is alright for now. But reconsider when we introduce signal-r - history.pushState(null, '', 'section/settings/workspace/document-type-root'); - } + // TODO: move this responsibility to the template package + async #createAndAssignTemplate() { + const { data: templateScaffold } = await this.#templateRepository.createScaffold({ + name: this.getName(), + alias: this.getAlias(), + }); + + if (!templateScaffold) throw new Error('Could not create template scaffold'); + const { data: template } = await this.#templateRepository.create(templateScaffold, null); + if (!template) throw new Error('Could not create template'); + + const templateEntity = { id: template.unique }; + const allowedTemplates = this.getAllowedTemplateIds() ?? []; + this.setAllowedTemplateIds([templateEntity, ...allowedTemplates]); + this.setDefaultTemplate(templateEntity); } /** - * Save or creates the document type, based on wether its a new one or existing. + * @deprecated Use the createScaffold method instead. Will be removed in 17. + * @param {UmbEntityModel} parent + * @memberof UmbMediaTypeWorkspaceContext */ - async submit() { - const data = this.getData(); - if (data === undefined) { - throw new Error('Cannot save, no data'); - } - - if (this.getIsNew()) { - const parent = this.#parent.getValue(); - if (!parent) throw new Error('Parent is not set'); - - if (this.createTemplateMode) { - const repo = new UmbTemplateDetailRepository(this); - const { data: templateScaffold } = await repo.createScaffold(); - if (!templateScaffold) throw new Error('Could not create template scaffold'); - - templateScaffold.name = data.name; - templateScaffold.alias = data.alias; - - const { data: template } = await repo.create(templateScaffold, null); - if (!template) throw new Error('Could not create template'); - - const templateEntity = { id: template.unique }; - const allowedTemplates = this.getAllowedTemplateIds() ?? []; - this.setAllowedTemplateIds([templateEntity, ...allowedTemplates]); - this.setDefaultTemplate(templateEntity); - } - - await this.structure.create(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); - const event = new UmbRequestReloadChildrenOfEntityEvent({ - entityType: parent.entityType, - unique: parent.unique, - }); - eventContext.dispatchEvent(event); - - this.setIsNew(false); - this.createTemplateMode = false; - } 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); - } - } - - public override destroy(): void { - this.#persistedData.destroy(); - this.structure.destroy(); - this.repository.destroy(); - super.destroy(); + async create(parent: UmbEntityModel, presetAlias: string | null) { + this.#onScaffoldSetup(parent, presetAlias); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-links.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-links.element.ts index ed31704173..7165a5a786 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-links.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/views/info/document-workspace-view-info-links.element.ts @@ -37,6 +37,7 @@ export class UmbDocumentWorkspaceViewInfoLinksElement extends UmbLitElement { this.observe( observeMultiple([context.isNew, context.unique, context.variantOptions]), ([isNew, unique, variantOptions]) => { + if (!unique) return; this._isNew = isNew === true; this._unique = unique; this._variantOptions = variantOptions; 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 ae9c12ae11..b0b50cd9bb 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 @@ -1,90 +1,39 @@ -import { UmbMediaTypeDetailRepository } from '../repository/detail/media-type-detail.repository.js'; import { UMB_MEDIA_TYPE_ENTITY_TYPE } from '../entity.js'; import type { UmbMediaTypeDetailModel } from '../types.js'; import { UmbMediaTypeWorkspaceEditorElement } from './media-type-workspace-editor.element.js'; import { - UmbSubmittableWorkspaceContextBase, type UmbRoutableWorkspaceContext, UmbWorkspaceIsNewRedirectController, } from '@umbraco-cms/backoffice/workspace'; -import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; -import type { - UmbContentTypeCompositionModel, - UmbContentTypeSortModel, - UmbContentTypeWorkspaceContext, -} from '@umbraco-cms/backoffice/content-type'; +import { UmbContentTypeWorkspaceContextBase } from '@umbraco-cms/backoffice/content-type'; +import type { UmbContentTypeSortModel, UmbContentTypeWorkspaceContext } from '@umbraco-cms/backoffice/content-type'; 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 { - UmbRequestReloadChildrenOfEntityEvent, - UmbRequestReloadStructureForEntityEvent, -} from '@umbraco-cms/backoffice/entity-action'; +import { UMB_MEDIA_TYPE_WORKSPACE_ALIAS } from './constants.js'; +import { UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; -type EntityType = UmbMediaTypeDetailModel; +type DetailModelType = UmbMediaTypeDetailModel; export class UmbMediaTypeWorkspaceContext - extends UmbSubmittableWorkspaceContextBase - implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext + extends UmbContentTypeWorkspaceContextBase + implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext { - readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT = true; - // - public readonly repository: UmbMediaTypeDetailRepository = new UmbMediaTypeDetailRepository(this); - // Draft is located in structure manager - - #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); - readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); - readonly parentEntityType = this.#parent.asObservablePart((parent) => (parent ? parent.entityType : undefined)); - - #persistedData = new UmbObjectState(undefined); - - // General for content types: - readonly data; - readonly unique; - readonly entityType; - readonly name; - getName(): string | undefined { - return this.structure.getOwnerContentType()?.name; - } - readonly alias; - readonly description; - readonly icon; - - readonly allowedAtRoot; - readonly variesByCulture; - readonly variesBySegment; - readonly allowedContentTypes; - readonly compositions; - readonly collection; - - readonly structure = new UmbContentTypeStructureManager(this, this.repository); - constructor(host: UmbControllerHost) { - super(host, 'Umb.Workspace.MediaType'); - - // General for content types: - this.data = this.structure.ownerContentType; - this.unique = this.structure.ownerContentTypeObservablePart((data) => data?.unique); - this.entityType = this.structure.ownerContentTypeObservablePart((data) => data?.entityType); - this.name = this.structure.ownerContentTypeObservablePart((data) => data?.name); - this.alias = this.structure.ownerContentTypeObservablePart((data) => data?.alias); - this.description = this.structure.ownerContentTypeObservablePart((data) => data?.description); - 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.allowedContentTypes = this.structure.ownerContentTypeObservablePart((data) => data?.allowedContentTypes); - this.compositions = this.structure.ownerContentTypeObservablePart((data) => data?.compositions); - this.collection = this.structure.ownerContentTypeObservablePart((data) => data?.collection); + super(host, { + workspaceAlias: UMB_MEDIA_TYPE_WORKSPACE_ALIAS, + entityType: UMB_MEDIA_TYPE_ENTITY_TYPE, + detailRepositoryAlias: UMB_MEDIA_TYPE_DETAIL_REPOSITORY_ALIAS, + }); this.routes.setRoutes([ { - path: 'create/parent/:entityType/:parentUnique', + path: 'create/parent/:parentEntityType/:parentUnique', component: UmbMediaTypeWorkspaceEditorElement, setup: async (_component, info) => { - const parentEntityType = info.match.params.entityType; + const parentEntityType = info.match.params.parentEntityType; const parentUnique = info.match.params.parentUnique === 'null' ? null : info.match.params.parentUnique; - await this.create({ entityType: parentEntityType, unique: parentUnique }); + const parent: UmbEntityModel = { entityType: parentEntityType, unique: parentUnique }; + await this.createScaffold({ parent }); new UmbWorkspaceIsNewRedirectController( this, @@ -104,40 +53,6 @@ export class UmbMediaTypeWorkspaceContext ]); } - protected override resetState(): void { - super.resetState(); - this.#persistedData.setValue(undefined); - } - - getData() { - return this.structure.getOwnerContentType(); - } - - getUnique() { - return this.getData()?.unique; - } - - getEntityType() { - return UMB_MEDIA_TYPE_ENTITY_TYPE; - } - - setName(name: string) { - this.structure.updateOwnerContentType({ name }); - } - - setAlias(alias: string) { - this.structure.updateOwnerContentType({ alias }); - } - - setDescription(description: string) { - this.structure.updateOwnerContentType({ description }); - } - - // TODO: manage setting icon color alias? - setIcon(icon: string) { - this.structure.updateOwnerContentType({ icon }); - } - setAllowedAtRoot(allowedAtRoot: boolean) { this.structure.updateOwnerContentType({ allowedAtRoot }); } @@ -158,86 +73,17 @@ export class UmbMediaTypeWorkspaceContext this.structure.updateOwnerContentType({ allowedContentTypes }); } - setCompositions(compositions: Array) { - this.structure.updateOwnerContentType({ compositions }); - } - setCollection(collection: UmbReferenceByUnique) { this.structure.updateOwnerContentType({ collection }); } - async create(parent: { entityType: string; unique: string | null }) { - this.resetState(); - this.#parent.setValue(parent); - const { data } = await this.structure.createScaffold(); - if (!data) return undefined; - - this.setIsNew(true); - this.#persistedData.setValue(data); - return data; - } - - async load(unique: string) { - this.resetState(); - const { data, asObservable } = await this.structure.loadType(unique); - - if (data) { - this.setIsNew(false); - this.#persistedData.update(data); - } - - if (asObservable) { - this.observe(asObservable(), (entity) => this.#onStoreChange(entity), 'umbMediaTypeStoreObserver'); - } - } - - #onStoreChange(entity: EntityType | undefined) { - if (!entity) { - //TODO: This solution is alright for now. But reconsider when we introduce signal-r - history.pushState(null, '', 'section/settings/workspace/media-type-root'); - } - } - /** - * Save or creates the media type, based on wether its a new one or existing. + * @deprecated Use the createScaffold method instead. Will be removed in 17. + * @param {UmbEntityModel} parent + * @memberof UmbMediaTypeWorkspaceContext */ - async submit() { - const data = this.getData(); - if (!data) { - throw new Error('Something went wrong, there is no data for media type you want to save...'); - } - - if (this.getIsNew()) { - const parent = this.#parent.getValue(); - if (!parent) throw new Error('Parent is not set'); - - await this.structure.create(parent.unique); - - const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); - const event = new UmbRequestReloadChildrenOfEntityEvent({ - entityType: parent.entityType, - unique: parent.unique, - }); - eventContext.dispatchEvent(event); - this.setIsNew(false); - } 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); - } - } - - public override destroy(): void { - this.#persistedData.destroy(); - this.structure.destroy(); - this.repository.destroy(); - super.destroy(); + async create(parent: UmbEntityModel) { + this.createScaffold({ parent }); } } 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 5cb6063562..a32e6ff96d 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 @@ -1,86 +1,40 @@ -import { UmbMemberTypeDetailRepository } from '../repository/detail/index.js'; +import { UMB_MEMBER_TYPE_DETAIL_REPOSITORY_ALIAS } from '../repository/detail/index.js'; import type { UmbMemberTypeDetailModel } from '../types.js'; import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '../index.js'; import { UmbMemberTypeWorkspaceEditorElement } from './member-type-workspace-editor.element.js'; import { - UmbSubmittableWorkspaceContextBase, type UmbRoutableWorkspaceContext, UmbWorkspaceIsNewRedirectController, } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { - type UmbContentTypeCompositionModel, - UmbContentTypeStructureManager, type UmbContentTypeWorkspaceContext, + UmbContentTypeWorkspaceContextBase, } from '@umbraco-cms/backoffice/content-type'; -import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; -import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; -import { - UmbRequestReloadChildrenOfEntityEvent, - UmbRequestReloadStructureForEntityEvent, -} from '@umbraco-cms/backoffice/entity-action'; +import { UMB_MEMBER_TYPE_WORKSPACE_ALIAS } from './manifests.js'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; -type EntityType = UmbMemberTypeDetailModel; +type EntityDetailModel = UmbMemberTypeDetailModel; export class UmbMemberTypeWorkspaceContext - extends UmbSubmittableWorkspaceContextBase - implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext + extends UmbContentTypeWorkspaceContextBase + implements UmbContentTypeWorkspaceContext, UmbRoutableWorkspaceContext { - readonly IS_CONTENT_TYPE_WORKSPACE_CONTEXT = true; - - public readonly repository = new UmbMemberTypeDetailRepository(this); - - #parent = new UmbObjectState<{ entityType: string; unique: string | null } | undefined>(undefined); - readonly parentUnique = this.#parent.asObservablePart((parent) => (parent ? parent.unique : undefined)); - readonly parentEntityType = this.#parent.asObservablePart((parent) => (parent ? parent.entityType : undefined)); - - #persistedData = new UmbObjectState(undefined); - - // General for content types: - readonly data; - readonly unique; - readonly name; - getName(): string | undefined { - return this.structure.getOwnerContentType()?.name; - } - readonly alias; - readonly description; - readonly icon; - - readonly allowedAtRoot; - readonly variesByCulture; - readonly variesBySegment; - readonly isElement; - readonly allowedContentTypes; - readonly compositions; - - readonly structure = new UmbContentTypeStructureManager(this, this.repository); - constructor(host: UmbControllerHost) { - super(host, 'Umb.Workspace.MemberType'); - - // General for content types: - this.data = this.structure.ownerContentType; - - this.unique = this.structure.ownerContentTypeObservablePart((data) => data?.unique); - 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); + super(host, { + workspaceAlias: UMB_MEMBER_TYPE_WORKSPACE_ALIAS, + entityType: UMB_MEMBER_TYPE_ENTITY_TYPE, + detailRepositoryAlias: UMB_MEMBER_TYPE_DETAIL_REPOSITORY_ALIAS, + }); this.routes.setRoutes([ { - path: 'create/parent/:entityType/:parentUnique', + path: 'create/parent/:parentEntityType/:parentUnique', component: UmbMemberTypeWorkspaceEditorElement, setup: async (_component, info) => { - const parentEntityType = info.match.params.entityType; + const parentEntityType = info.match.params.parentEntityType; const parentUnique = info.match.params.parentUnique === 'null' ? null : info.match.params.parentUnique; - await this.create({ entityType: parentEntityType, unique: parentUnique }); + const parent: UmbEntityModel = { entityType: parentEntityType, unique: parentUnique }; + await this.createScaffold({ parent }); new UmbWorkspaceIsNewRedirectController( this, @@ -100,120 +54,27 @@ export class UmbMemberTypeWorkspaceContext ]); } - set(propertyName: PropertyName, value: EntityType[PropertyName]) { + /** + * @deprecated Use the individual set methods instead. Will be removed in 17. + * @template PropertyName + * @param {PropertyName} propertyName + * @param {EntityDetailModel[PropertyName]} value + * @memberof UmbMemberTypeWorkspaceContext + */ + set( + propertyName: PropertyName, + value: EntityDetailModel[PropertyName], + ) { this.structure.updateOwnerContentType({ [propertyName]: value }); } - protected override resetState(): void { - super.resetState(); - this.#persistedData.setValue(undefined); - } - - getData() { - return this.structure.getOwnerContentType(); - } - - getUnique() { - return this.getData()?.unique; - } - - getEntityType() { - return UMB_MEMBER_TYPE_ENTITY_TYPE; - } - - setName(name: string) { - this.structure.updateOwnerContentType({ name }); - } - - setAlias(alias: string) { - this.structure.updateOwnerContentType({ alias }); - } - - setDescription(description: string) { - this.structure.updateOwnerContentType({ description }); - } - - // TODO: manage setting icon color alias? - setIcon(icon: string) { - this.structure.updateOwnerContentType({ icon }); - } - - setCompositions(compositions: Array) { - this.structure.updateOwnerContentType({ compositions }); - } - - async create(parent: { entityType: string; unique: string | null }) { - this.resetState(); - this.#parent.setValue(parent); - const { data } = await this.structure.createScaffold(); - if (!data) return undefined; - - this.setIsNew(true); - this.#persistedData.setValue(data); - return data; - } - - async load(unique: string) { - this.resetState(); - const { data, asObservable } = await this.structure.loadType(unique); - - if (data) { - this.setIsNew(false); - this.#persistedData.update(data); - } - - if (asObservable) { - this.observe(asObservable(), (entity) => this.#onStoreChange(entity), 'umbMemberTypeStoreObserver'); - } - } - - #onStoreChange(entity: EntityType | undefined) { - if (!entity) { - //TODO: This solution is alright for now. But reconsider when we introduce signal-r - history.pushState(null, '', 'section/settings/workspace/member-type-root'); - } - } - /** - * Save or creates the member type, based on wether its a new one or existing. + * @deprecated Use the createScaffold method instead. Will be removed in 17. + * @param {UmbEntityModel} parent + * @memberof UmbMemberTypeWorkspaceContext */ - async submit() { - const data = this.getData(); - if (!data) { - throw new Error('Something went wrong, there is no data for media type you want to save...'); - } - - if (this.getIsNew()) { - const parent = this.#parent.getValue(); - if (!parent) throw new Error('Parent is not set'); - - await this.structure.create(parent.unique); - - const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); - const event = new UmbRequestReloadChildrenOfEntityEvent({ - entityType: parent.entityType, - unique: parent.unique, - }); - eventContext.dispatchEvent(event); - this.setIsNew(false); - } 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); - } - } - - public override destroy(): void { - this.#persistedData.destroy(); - this.structure.destroy(); - this.repository.destroy(); - super.destroy(); + async create(parent: UmbEntityModel) { + this.createScaffold({ parent }); } }