From 2802bfbcefe46d3b965491d94a31d40e165e869c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 14 Apr 2025 20:51:49 +0200 Subject: [PATCH 01/23] content type nesting --- .../ContentTypeEditingPresentationFactory.cs | 20 ++- .../DocumentTypeEditingPresentationFactory.cs | 14 +- .../MediaTypeEditingPresentationFactory.cs | 15 ++- .../ContentType/ContentTypeMapDefinition.cs | 25 +--- .../DocumentType/DocumentTypeMapDefinition.cs | 2 +- .../MediaType/MediaTypeMapDefinition.cs | 2 +- .../MemberType/MemberTypeMapDefinition.cs | 2 +- .../ContentTypeEditingServiceBase.cs | 9 ++ .../entity-actions/create/manifests.ts | 6 +- ...ument-type-create-options-modal.element.ts | 15 ++- .../document-type-workspace.context.ts | 13 ++ .../ContentTypeEditingServiceTests.Create.cs | 57 +++++++- .../ContentTypeEditingServiceTests.Update.cs | 125 ++++++++++++++++++ 13 files changed, 261 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Factories/ContentTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/ContentTypeEditingPresentationFactory.cs index f979c17649ff..2b78847dc369 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/ContentTypeEditingPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/ContentTypeEditingPresentationFactory.cs @@ -1,4 +1,5 @@ -using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Extensions; using ContentTypeEditingModels = Umbraco.Cms.Core.Models.ContentTypeEditing; @@ -38,12 +39,27 @@ protected TContentTypeEditingModel MapContentTypeEditingModel< VariesByCulture = viewModel.VariesByCulture, VariesBySegment = viewModel.VariesBySegment, Containers = MapContainers(viewModel.Containers), - Properties = MapProperties(viewModel.Properties) + Properties = MapProperties(viewModel.Properties), }; return editingModel; } + protected Guid? CalculateCreateContainerKey(ReferenceByIdModel? parent, IDictionary compositions) + { + // special case: + // the API is somewhat confusing when it comes to inheritance. the parent denotes a container (folder), but it + // is easily confused with the parent for inheritance. + // if the request model contains the same key for container and "inheritance composition", we'll be lenient and + // allow it - just remove the container, inheritance takes precedence as intent. + Guid? parentId = parent?.Id; + return parentId.HasValue + && compositions.TryGetValue(parentId.Value, out ContentTypeViewModels.CompositionType compositionType) + && compositionType is ContentTypeViewModels.CompositionType.Inheritance + ? null + : parentId; + } + protected T MapCompositionModel(ContentTypeAvailableCompositionsResult compositionResult) where T : ContentTypeViewModels.AvailableContentTypeCompositionResponseModelBase, new() { diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentTypeEditingPresentationFactory.cs index d0fbc9317276..27b4ef8b4510 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentTypeEditingPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentTypeEditingPresentationFactory.cs @@ -25,12 +25,14 @@ public ContentTypeCreateModel MapCreateModel(CreateDocumentTypeRequestModel requ MapCleanup(createModel, requestModel.Cleanup); createModel.Key = requestModel.Id; - createModel.ContainerKey = requestModel.Parent?.Id; createModel.AllowedTemplateKeys = requestModel.AllowedTemplates.Select(reference => reference.Id).ToArray(); createModel.DefaultTemplateKey = requestModel.DefaultTemplate?.Id; createModel.ListView = requestModel.Collection?.Id; createModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedDocumentTypes); - createModel.Compositions = MapCompositions(requestModel.Compositions); + + IDictionary compositionTypesByKey = CompositionTypesByKey(requestModel.Compositions); + createModel.Compositions = MapCompositions(compositionTypesByKey); + createModel.ContainerKey = CalculateCreateContainerKey(requestModel.Parent, compositionTypesByKey); return createModel; } @@ -51,7 +53,7 @@ public ContentTypeUpdateModel MapUpdateModel(UpdateDocumentTypeRequestModel requ updateModel.DefaultTemplateKey = requestModel.DefaultTemplate?.Id; updateModel.ListView = requestModel.Collection?.Id; updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedDocumentTypes); - updateModel.Compositions = MapCompositions(requestModel.Compositions); + updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions)); return updateModel; } @@ -72,8 +74,8 @@ private IEnumerable MapAllowedContentTypes(IEnumerable t.DocumentType.Id) .ToDictionary(t => t.DocumentType.Id, t => t.SortOrder)); - private IEnumerable MapCompositions(IEnumerable documentTypeCompositions) - => MapCompositions(documentTypeCompositions + private IDictionary CompositionTypesByKey(IEnumerable documentTypeCompositions) + => documentTypeCompositions .DistinctBy(c => c.DocumentType.Id) - .ToDictionary(c => c.DocumentType.Id, c => c.CompositionType)); + .ToDictionary(c => c.DocumentType.Id, c => c.CompositionType); } diff --git a/src/Umbraco.Cms.Api.Management/Factories/MediaTypeEditingPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/MediaTypeEditingPresentationFactory.cs index c5cd70ed4995..db08e47fd60b 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/MediaTypeEditingPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/MediaTypeEditingPresentationFactory.cs @@ -23,11 +23,13 @@ public MediaTypeCreateModel MapCreateModel(CreateMediaTypeRequestModel requestMo >(requestModel); createModel.Key = requestModel.Id; - createModel.ContainerKey = requestModel.Parent?.Id; createModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes); - createModel.Compositions = MapCompositions(requestModel.Compositions); createModel.ListView = requestModel.Collection?.Id; + IDictionary compositionTypesByKey = CompositionTypesByKey(requestModel.Compositions); + createModel.Compositions = MapCompositions(compositionTypesByKey); + createModel.ContainerKey = CalculateCreateContainerKey(requestModel.Parent, compositionTypesByKey); + return createModel; } @@ -42,7 +44,8 @@ public MediaTypeUpdateModel MapUpdateModel(UpdateMediaTypeRequestModel requestMo >(requestModel); updateModel.AllowedContentTypes = MapAllowedContentTypes(requestModel.AllowedMediaTypes); - updateModel.Compositions = MapCompositions(requestModel.Compositions); + updateModel.Compositions = MapCompositions(CompositionTypesByKey(requestModel.Compositions)); + updateModel.ListView = requestModel.Collection?.Id; return updateModel; @@ -56,8 +59,8 @@ private IEnumerable MapAllowedContentTypes(IEnumerable t.MediaType.Id) .ToDictionary(t => t.MediaType.Id, t => t.SortOrder)); - private IEnumerable MapCompositions(IEnumerable documentTypeCompositions) - => MapCompositions(documentTypeCompositions + private IDictionary CompositionTypesByKey(IEnumerable documentTypeCompositions) + => documentTypeCompositions .DistinctBy(c => c.MediaType.Id) - .ToDictionary(c => c.MediaType.Id, c => c.CompositionType)); + .ToDictionary(c => c.MediaType.Id, c => c.CompositionType); } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/ContentType/ContentTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/ContentType/ContentTypeMapDefinition.cs index 7cc15e2a7645..acfd8e6c5e91 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/ContentType/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/ContentType/ContentTypeMapDefinition.cs @@ -83,24 +83,9 @@ protected static CompositionType CalculateCompositionType(int contentTypeParentI ? CompositionType.Inheritance : CompositionType.Composition; - protected static IEnumerable MapNestedCompositions(IEnumerable directCompositions, int contentTypeParentId, Func contentTypeCompositionFactory) - { - var allCompositions = new List(); - - foreach (var composition in directCompositions) - { - CompositionType compositionType = CalculateCompositionType(contentTypeParentId, composition); - T contentTypeComposition = contentTypeCompositionFactory(new ReferenceByIdModel(composition.Key), compositionType); - allCompositions.Add(contentTypeComposition); - - // When we have composition inheritance, we have to find all ancestor compositions recursively - if (compositionType == CompositionType.Inheritance && composition.ContentTypeComposition.Any()) - { - var nestedCompositions = MapNestedCompositions(composition.ContentTypeComposition, composition.ParentId, contentTypeCompositionFactory); - allCompositions.AddRange(nestedCompositions); - } - } - - return allCompositions; - } + protected static IEnumerable MapCompositions(IEnumerable directCompositions, int contentTypeParentId, Func contentTypeCompositionFactory) + => directCompositions + .Select(composition => contentTypeCompositionFactory( + new ReferenceByIdModel(composition.Key), + CalculateCompositionType(contentTypeParentId, composition))).ToArray(); } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs index 958dff64e424..0e9aeba492f5 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/DocumentType/DocumentTypeMapDefinition.cs @@ -41,7 +41,7 @@ private void Map(IContentType source, DocumentTypeResponseModel target, MapperCo new DocumentTypeSort { DocumentType = new ReferenceByIdModel(ct.Key), SortOrder = ct.SortOrder }) .OrderBy(ct => ct.SortOrder) .ToArray() ?? Enumerable.Empty(); - target.Compositions = MapNestedCompositions( + target.Compositions = MapCompositions( source.ContentTypeComposition, source.ParentId, (referenceByIdModel, compositionType) => new DocumentTypeComposition diff --git a/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs index 32fdf427b865..af1a0fff090d 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/MediaType/MediaTypeMapDefinition.cs @@ -40,7 +40,7 @@ private void Map(IMediaType source, MediaTypeResponseModel target, MapperContext target.AllowedMediaTypes = source.AllowedContentTypes?.Select(ct => new MediaTypeSort { MediaType = new ReferenceByIdModel(ct.Key), SortOrder = ct.SortOrder }) .ToArray() ?? Enumerable.Empty(); - target.Compositions = MapNestedCompositions( + target.Compositions = MapCompositions( source.ContentTypeComposition, source.ParentId, (referenceByIdModel, compositionType) => new MediaTypeComposition diff --git a/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeMapDefinition.cs index 7e19a2e45a11..8192c6b585f9 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/MemberType/MemberTypeMapDefinition.cs @@ -32,7 +32,7 @@ private void Map(IMemberType source, MemberTypeResponseModel target, MapperConte target.IsElement = source.IsElement; target.Containers = MapPropertyTypeContainers(source); target.Properties = MapPropertyTypes(source); - target.Compositions = MapNestedCompositions( + target.Compositions = MapCompositions( source.ContentTypeComposition, source.ParentId, (referenceByIdModel, compositionType) => new MemberTypeComposition diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs index a86d9186e0b4..fdf160ef4f6b 100644 --- a/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs +++ b/src/Umbraco.Core/Services/ContentTypeEditing/ContentTypeEditingServiceBase.cs @@ -322,6 +322,15 @@ private ContentTypeOperationStatus ValidateCompositions(TContentType? contentTyp // get the content type keys we want to use for compositions Guid[] compositionKeys = KeysForCompositionTypes(model, CompositionType.Composition); + // if the content type keys are already set as compositions, don't perform any additional validation + // - this covers an edge case where compositions are configured for a content type before child content types are created + if (contentType is not null && contentType.ContentTypeComposition + .Select(c => c.Key) + .ContainsAll(compositionKeys)) + { + return ContentTypeOperationStatus.Success; + } + // verify that all compositions keys are allowed Guid[] allowedCompositionKeys = _contentTypeService.GetAvailableCompositeContentTypes(contentType, allContentTypeCompositions, isElement: model.IsElement) .Results 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 09ad63c51eda..f0a18c7cb745 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 @@ -10,7 +10,11 @@ export const manifests: Array = alias: 'Umb.EntityAction.DocumentType.Create', name: 'Create Document Type Entity Action', weight: 1200, - forEntityTypes: [UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE, UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE], + forEntityTypes: [ + UMB_DOCUMENT_TYPE_ENTITY_TYPE, + UMB_DOCUMENT_TYPE_ROOT_ENTITY_TYPE, + UMB_DOCUMENT_TYPE_FOLDER_ENTITY_TYPE, + ], meta: { icon: 'icon-add', label: '#actions_create', 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 941c581e24e6..a80df1de7054 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 @@ -1,6 +1,8 @@ import { UMB_DOCUMENT_TYPE_FOLDER_REPOSITORY_ALIAS } from '../../../tree/index.js'; import { UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN, + UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PRESET_ELEMENT, + UMB_CREATE_DOCUMENT_TYPE_WORKSPACE_PRESET_TEMPLATE, type UmbCreateDocumentTypeWorkspacePresetType, } from '../../../paths.js'; import type { UmbDocumentTypeEntityTypeUnion } from '../../../entity.js'; @@ -9,8 +11,10 @@ import { html, customElement, map } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbCreateFolderEntityAction } from '@umbraco-cms/backoffice/tree'; +const CREATE_FOLDER_PRESET = 'folder'; + // Include the types from the DocumentTypeWorkspacePresetType + folder. -type OptionsPresetType = UmbCreateDocumentTypeWorkspacePresetType | 'folder' | null; +type OptionsPresetType = UmbCreateDocumentTypeWorkspacePresetType | typeof CREATE_FOLDER_PRESET | null; /** @deprecated No longer used internally. This will be removed in Umbraco 17. [LK] */ @customElement('umb-document-type-create-options-modal') @@ -30,13 +34,13 @@ export class UmbDataTypeCreateOptionsModalElement extends UmbModalBaseElement + { + Assert.AreEqual("Child", childContentType.Name); + Assert.AreEqual(1, childContentType.ContentTypeComposition.Count()); + Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key); + }); + } + [Test] public async Task Cannot_Be_Both_Parent_And_Composition() { @@ -691,6 +720,32 @@ public async Task Cannot_Mix_Inheritance_And_ParentKey() Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status); } + [Test] + public async Task Cannot_Have_Same_Key_For_Inheritance_And_Parent() + { + var parentModel = ContentTypeCreateModel("Parent"); + var parent = (await ContentTypeEditingService.CreateAsync(parentModel, Constants.Security.SuperUserKey)).Result; + Assert.IsNotNull(parent); + + Composition[] composition = + { + new() + { + CompositionType = CompositionType.Inheritance, Key = parent.Key, + } + }; + + var childModel = ContentTypeCreateModel( + "Child", + containerKey: parent.Key, + compositions: composition); + + var result = await ContentTypeEditingService.CreateAsync(childModel, Constants.Security.SuperUserKey); + + Assert.IsFalse(result.Success); + Assert.AreEqual(ContentTypeOperationStatus.InvalidParent, result.Status); + } + [Test] public async Task Cannot_Use_As_ParentKey() { @@ -796,7 +851,7 @@ public async Task Cannot_Use_Empty_Name_For_PropertyType_Container() [TestCase(".")] [TestCase("-")] [TestCase("!\"#¤%&/()=)?`")] - [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { "System"})] + [TestCaseSource(nameof(DifferentCapitalizedAlias), new object[] { "System" })] public async Task Cannot_Use_Invalid_Alias(string contentTypeAlias) { var createModel = ContentTypeCreateModel("Test", contentTypeAlias); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs index 00c9cb262308..89f8b126bf6f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentTypeEditingServiceTests.Update.cs @@ -601,6 +601,91 @@ public async Task Can_Update_History_Cleanup() Assert.AreEqual(567, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays); } + [Test] + public async Task Can_Reapply_Compositions_For_Content_Type_With_Children() + { + var compositionContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Composition"), Constants.Security.SuperUserKey)).Result!; + var parentContentType = (await ContentTypeEditingService.CreateAsync( + ContentTypeCreateModel( + "Parent", + compositions: [new Composition { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]), + Constants.Security.SuperUserKey)).Result!; + var childContentType = (await ContentTypeEditingService.CreateAsync( + ContentTypeCreateModel( + "Child", + compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]), + Constants.Security.SuperUserKey)).Result!; + + var updateModel = ContentTypeUpdateModel( + "Parent Updated", + compositions: [new() { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]); + + var result = await ContentTypeEditingService.UpdateAsync(parentContentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + parentContentType = await ContentTypeService.GetAsync(parentContentType.Key); + + Assert.IsNotNull(parentContentType); + Assert.Multiple(() => + { + Assert.AreEqual("Parent Updated", parentContentType.Name); + Assert.AreEqual(1, parentContentType.ContentTypeComposition.Count()); + Assert.AreEqual(compositionContentType.Key, parentContentType.ContentTypeComposition.Single().Key); + }); + + childContentType = await ContentTypeService.GetAsync(childContentType.Key); + + Assert.IsNotNull(childContentType); + Assert.Multiple(() => + { + Assert.AreEqual("Child", childContentType.Name); + Assert.AreEqual(1, childContentType.ContentTypeComposition.Count()); + Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key); + }); + } + + [Test] + public async Task Can_Remove_Compositions_For_Content_Type_With_Children() + { + var compositionContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Composition"), Constants.Security.SuperUserKey)).Result!; + var parentContentType = (await ContentTypeEditingService.CreateAsync( + ContentTypeCreateModel( + "Parent", + compositions: [new Composition { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]), + Constants.Security.SuperUserKey)).Result!; + var childContentType = (await ContentTypeEditingService.CreateAsync( + ContentTypeCreateModel( + "Child", + compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]), + Constants.Security.SuperUserKey)).Result!; + + var updateModel = ContentTypeUpdateModel("Parent Updated", compositions: []); + + var result = await ContentTypeEditingService.UpdateAsync(parentContentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + // Ensure it's actually persisted + parentContentType = await ContentTypeService.GetAsync(parentContentType.Key); + + Assert.IsNotNull(parentContentType); + Assert.Multiple(() => + { + Assert.AreEqual("Parent Updated", parentContentType.Name); + Assert.IsEmpty(parentContentType.ContentTypeComposition); + }); + + childContentType = await ContentTypeService.GetAsync(childContentType.Key); + + Assert.IsNotNull(childContentType); + Assert.Multiple(() => + { + Assert.AreEqual("Child", childContentType.Name); + Assert.AreEqual(1, childContentType.ContentTypeComposition.Count()); + Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key); + }); + } + [TestCase(false)] [TestCase(true)] public async Task Cannot_Move_Properties_To_Non_Existing_Containers(bool isElement) @@ -826,4 +911,44 @@ public async Task Cannot_Update_Container_Types_To_Unknown_Types(string containe Assert.IsFalse(result.Success); Assert.AreEqual(ContentTypeOperationStatus.InvalidContainerType, result.Status); } + + + [Test] + public async Task Cannot_Add_Compositions_For_Content_Type_With_Children() + { + var compositionContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Composition"), Constants.Security.SuperUserKey)).Result!; + var parentContentType = (await ContentTypeEditingService.CreateAsync(ContentTypeCreateModel("Parent"), Constants.Security.SuperUserKey)).Result!; + var childContentType = (await ContentTypeEditingService.CreateAsync( + ContentTypeCreateModel( + "Child", + compositions: [new Composition { CompositionType = CompositionType.Inheritance, Key = parentContentType.Key }]), + Constants.Security.SuperUserKey)).Result!; + + var updateModel = ContentTypeUpdateModel( + "Parent Updated", + compositions: [new() { CompositionType = CompositionType.Composition, Key = compositionContentType.Key }]); + + var result = await ContentTypeEditingService.UpdateAsync(parentContentType, updateModel, Constants.Security.SuperUserKey); + Assert.IsFalse(result.Success); + + // Ensure nothing was persisted + parentContentType = await ContentTypeService.GetAsync(parentContentType.Key); + + Assert.IsNotNull(parentContentType); + Assert.Multiple(() => + { + Assert.AreEqual("Parent", parentContentType.Name); + Assert.AreEqual(0, parentContentType.ContentTypeComposition.Count()); + }); + + childContentType = await ContentTypeService.GetAsync(childContentType.Key); + + Assert.IsNotNull(childContentType); + Assert.Multiple(() => + { + Assert.AreEqual("Child", childContentType.Name); + Assert.AreEqual(1, childContentType.ContentTypeComposition.Count()); + Assert.AreEqual(parentContentType.Key, childContentType.ContentTypeComposition.Single().Key); + }); + } } From 12f1685e9ac485d623ef70398428c1119cec7ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 14 Apr 2025 21:27:14 +0200 Subject: [PATCH 02/23] TODOs --- .../structure/content-type-structure-manager.class.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index fee663e92577..7213f10580e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -118,6 +118,7 @@ export class UmbContentTypeStructureManager< // 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] + // TODO: Do something like above ^^ this.observe(this.ownerContentTypeCompositions, (ownerContentTypeCompositions) => { this.#loadContentTypeCompositions(ownerContentTypeCompositions); }); @@ -236,6 +237,8 @@ export class UmbContentTypeStructureManager< if (!unique) return {}; await this.#initRepository; + // TODO: Some way to know who are already in loading state... + // Lets initiate the content type: const { data, asObservable } = await this.#repository!.requestByUnique(unique); if (!data) return {}; From 54c7e50c0f594b4b1bfd39adbd9386949840a445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 15 Apr 2025 10:59:59 +0200 Subject: [PATCH 03/23] repository detail manager --- .../src/packages/core/repository/index.ts | 1 + .../repository/repository-details.manager.ts | 223 ++++++++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts 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 a344a151a986..8ccfa5a3e6fc 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 @@ -3,6 +3,7 @@ export * from './data-mapper/index.js'; export * from './detail/index.js'; export * from './item/index.js'; export * from './repository-base.js'; +export * from './repository-details.manager.js'; export * from './repository-items.manager.js'; export type { UmbDataSourceResponse, UmbDataSourceErrorResponse } from './data-source-response.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts new file mode 100644 index 000000000000..79e2dc773b2c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts @@ -0,0 +1,223 @@ +import type { UmbDetailRepository } from './detail/detail-repository.interface.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { type ManifestRepository, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { UmbEntityUpdatedEvent } from '@umbraco-cms/backoffice/entity-action'; + +interface UmbRepositoryStatus { + state: { + type: 'success' | 'error' | 'loading'; + error?: string; + }; + unique: string; +} + +export class UmbRepositoryDetailsManager extends UmbControllerBase { + // + repository?: UmbDetailRepository; + + #init: Promise; + #eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; + + // the init promise is used externally for recognizing when the manager is ready. + public get init() { + return this.#init; + } + + #uniques = new UmbArrayState([], (x) => x); + uniques = this.#uniques.asObservable(); + + #entries = new UmbArrayState([], (x) => x.unique); + entries = this.#entries.asObservable(); + + #statuses = new UmbArrayState([], (x) => x.unique); + statuses = this.#statuses.asObservable(); + + /** + * Creates an instance of UmbRepositoryItemsManager. + * @param {UmbControllerHost} host - The host for the controller. + * @param {string} repository - The alias of the repository to use. + * @memberof UmbRepositoryItemsManager + */ + constructor(host: UmbControllerHost, repository: UmbDetailRepository | string) { + super(host); + + this.#entries.sortBy((a, b) => { + const uniques = this.getUniques(); + const aIndex = uniques.indexOf(a.unique); + const bIndex = uniques.indexOf(b.unique); + return aIndex - bIndex; + }); + + if (typeof repository === 'string') { + this.#init = new UmbExtensionApiInitializer>>( + this, + umbExtensionsRegistry, + repository, + [this], + (permitted, repository) => { + this.repository = permitted ? repository.api : undefined; + }, + ).asPromise(); + } else { + this.repository = repository; + this.#init = Promise.resolve(); + } + + this.observe( + this.uniques, + (uniques) => { + // remove entries based on no-longer existing uniques: + const removedEntries = this.#entries.getValue().filter((entry) => !uniques.includes(entry.unique)); + removedEntries.forEach((entry) => { + const unique = entry.unique; + if (unique) { + this.#entries.removeOne(unique); + this.removeUmbControllerByAlias('observeEntry_' + unique); + } + }); + + this.#requestNewItems(); + }, + null, + ); + + this.consumeContext(UMB_ACTION_EVENT_CONTEXT, (context) => { + this.#eventContext?.removeEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); + + this.#eventContext = context; + this.#eventContext.addEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); + }); + } + + getUniques(): Array { + return this.#uniques.getValue(); + } + + setUniques(uniques: string[] | undefined): void { + this.#uniques.setValue(uniques ?? []); + } + + getItems(): Array { + return this.#entries.getValue(); + } + + itemByUnique(unique: string) { + return this.#entries.asObservablePart((items) => items.find((item) => item.unique === unique)); + } + + async getItemByUnique(unique: string) { + // TODO: Make an observeOnce feature, to avoid this amount of code: [NL] + const ctrl = this.observe(this.itemByUnique(unique)); + const result = await ctrl.asPromise(); + ctrl.destroy(); + return result; + } + + async #requestNewItems(): Promise { + await this.#init; + if (!this.repository) throw new Error('Repository is not initialized'); + + const requestedUniques = this.getUniques(); + + const newRequestedUniques = requestedUniques.filter((unique) => { + const item = this.#statuses.getValue().find((status) => status.unique === unique); + return !item; + }); + + newRequestedUniques.forEach((unique) => { + this.#requestItem(unique); + }); + } + + async #reloadItem(unique: string): Promise { + return await this.#requestItem(unique); + } + + async #requestItem(unique: string): Promise { + await this.#init; + if (!this.repository) throw new Error('Repository is not initialized'); + + this.#statuses.appendOne({ + state: { + type: 'loading', + }, + unique, + }); + + const { data, error, asObservable } = await this.repository.requestByUnique(unique); + + if (error) { + this.#statuses.appendOne({ + state: { + type: 'error', + error: '#general_notFound', + }, + unique, + } as UmbRepositoryStatus); + this.#entries.removeOne(unique); + this.removeUmbControllerByAlias('observeEntry_' + unique); + } + + if (data) { + //Check it still exists in uniques: + const uniques = this.getUniques(); + if (!uniques.includes(unique)) { + this.#statuses.removeOne(unique); + return; + } + this.#entries.appendOne(data); + + this.#statuses.appendOne({ + state: { + type: 'success', + }, + unique, + }); + + if (asObservable) { + this.observe( + asObservable(), + (data) => { + if (data) { + this.#entries.updateOne(unique, data); + } else { + this.#entries.removeOne(unique); + } + }, + 'observeEntry_' + unique, + ); + } + } + } + + #onEntityUpdatedEvent = (event: UmbEntityUpdatedEvent) => { + const eventUnique = event.getUnique(); + + const items = this.getItems(); + if (items.length === 0) return; + + // Ignore events if the entity is not in the list of items. + const item = items.find((item) => item.unique === eventUnique); + if (!item) return; + + this.#reloadItem(item.unique); + }; + + override destroy(): void { + this.#eventContext?.removeEventListener( + UmbEntityUpdatedEvent.TYPE, + this.#onEntityUpdatedEvent as unknown as EventListener, + ); + super.destroy(); + } +} From 40bf0d011c798bdc8fd2453cb3ad8efba88ac9c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 15 Apr 2025 11:01:09 +0200 Subject: [PATCH 04/23] todo --- .../src/packages/core/repository/repository-items.manager.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts index 16f4e60bfaf2..24f8d84fe8dd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts @@ -41,11 +41,12 @@ export class UmbRepositoryItemsManager exte #statuses = new UmbArrayState([], (x) => x.unique); statuses = this.#statuses.asObservable(); + // TODO: Align with the other manager(details), and make a generic type/base for these. v.17.0 [NL] /** * Creates an instance of UmbRepositoryItemsManager. * @param {UmbControllerHost} host - The host for the controller. * @param {string} repositoryAlias - The alias of the repository to use. - * @param {((entry: ItemType) => string | undefined)} [getUniqueMethod] - DEPRECATED since 15.3. Will be removed in v. 17: A method to get the unique key from the item. + * @param {((entry: ItemType) => string | undefined)} [getUniqueMethod] - DEPRECATED since 15.3. Will be removed in v.17.0: A method to get the unique key from the item. * @memberof UmbRepositoryItemsManager */ constructor( From eb1fcb297130b0dc609d0b49e71e746e3fef87ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 15 Apr 2025 14:07:12 +0200 Subject: [PATCH 05/23] implement unlimited compositions --- .../content-type-structure-manager.class.ts | 160 +++++++----------- .../content-type-workspace-context-base.ts | 17 +- .../content-type-design-editor.element.ts | 4 +- .../repository/repository-details.manager.ts | 27 ++- .../entity-detail-workspace-base.ts | 21 ++- .../workspace/user/user-workspace.context.ts | 7 +- 6 files changed, 113 insertions(+), 123 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 7213f10580e1..063985575320 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -4,7 +4,12 @@ import type { UmbPropertyTypeContainerModel, UmbPropertyTypeModel, } from '../types.js'; -import type { UmbDetailRepository, UmbRepositoryResponse } from '@umbraco-cms/backoffice/repository'; +import { + UmbRepositoryDetailsManager, + type UmbDetailRepository, + type UmbRepositoryResponse, + type UmbRepositoryResponseWithAsObservable, +} from '@umbraco-cms/backoffice/repository'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbControllerHost, UmbController } from '@umbraco-cms/backoffice/controller-api'; import type { MappingFunction } from '@umbraco-cms/backoffice/observable-api'; @@ -35,22 +40,24 @@ const UmbFilterDuplicateStrings = (value: string, index: number, array: Array extends UmbControllerBase { - #initResolver?: (respoonse: UmbRepositoryResponse) => void; - #init = new Promise>((resolve) => { + #initResolver?: (result: T) => void; + #init = new Promise((resolve) => { this.#initResolver = resolve; }); #repository?: UmbDetailRepository; - #initRepositoryResolver?: () => void; + #initRepositoryResolver?: (repo: UmbDetailRepository) => void; - #initRepository = new Promise((resolve) => { + #initRepository = new Promise>((resolve) => { if (this.#repository) { - resolve(); + resolve(this.#repository); } else { this.#initRepositoryResolver = resolve; } }); + #repoManager?: UmbRepositoryDetailsManager; + async whenLoaded() { await this.#init; return true; @@ -68,25 +75,24 @@ export class UmbContentTypeStructureManager< readonly ownerContentTypeName = createObservablePart(this.ownerContentType, (x) => x?.name); readonly ownerContentTypeCompositions = createObservablePart(this.ownerContentType, (x) => x?.compositions); + readonly contentTypeCompositions = this.#contentTypes.asObservablePart((contentTypes) => { + return contentTypes.flatMap((x) => x.compositions ?? []); + }); readonly #contentTypeContainers = this.#contentTypes.asObservablePart((contentTypes) => { - // Notice this may need to use getValue to avoid resetting it self. [NL] return contentTypes.flatMap((x) => x.containers ?? []); }); readonly contentTypeProperties = this.#contentTypes.asObservablePart((contentTypes) => { - // Notice this may need to use getValue to avoid resetting it self. [NL] return contentTypes.flatMap((x) => x.properties ?? []); }); async getContentTypeProperties() { return await this.observe(this.contentTypeProperties).asPromise(); } readonly contentTypeDataTypeUniques = this.#contentTypes.asObservablePart((contentTypes) => { - // Notice this may need to use getValue to avoid resetting it self. [NL] return contentTypes .flatMap((x) => x.properties?.map((p) => p.dataType.unique) ?? []) .filter(UmbFilterDuplicateStrings); }); readonly contentTypeHasProperties = this.#contentTypes.asObservablePart((contentTypes) => { - // Notice this may need to use getValue to avoid resetting it self. [NL] return contentTypes.some((x) => x.properties.length > 0); }); readonly contentTypePropertyAliases = createObservablePart(this.contentTypeProperties, (properties) => @@ -113,14 +119,22 @@ export class UmbContentTypeStructureManager< this.#observeRepository(typeRepository); } else { this.#repository = typeRepository; - this.#initRepositoryResolver?.(); + this.#initRepositoryResolver?.(typeRepository); } - // 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] - // TODO: Do something like above ^^ - this.observe(this.ownerContentTypeCompositions, (ownerContentTypeCompositions) => { - this.#loadContentTypeCompositions(ownerContentTypeCompositions); + this.#initRepository.then(() => { + if (!this.#repository) { + throw new Error( + 'Content Type Structure Manager failed cause it could not initialize or receive the Content Type Detail Repository.', + ); + } + this.#repoManager = new UmbRepositoryDetailsManager(this, typeRepository); + this.observe(this.#repoManager.entries, (entries) => this.#contentTypes.append(entries), null); + }); + + // Observe all Content Types compositions: [NL] + this.observe(this.contentTypeCompositions, (contentTypeCompositions) => { + this.#loadContentTypeCompositions(contentTypeCompositions); }); this.observe(this.#contentTypeContainers, (contentTypeContainers) => { this.#containers.setValue(contentTypeContainers); @@ -133,32 +147,36 @@ export class UmbContentTypeStructureManager< * @param {string} unique - The unique of the ContentType to load. * @returns {Promise} - Promise resolved */ - public async loadType(unique?: string) { + public async loadType(unique?: string): Promise> { if (this.#ownerContentTypeUnique === unique) { // Its the same, but we do not know if its done loading jet, so we will wait for the load promise to finish. [NL] await this.#init; - return; + return { data: this.getOwnerContentType(), asObservable: () => this.ownerContentType }; } + await this.#initRepository; this.#clear(); this.#ownerContentTypeUnique = unique; - if (!unique) return; - const result = await this.#loadType(unique); + if (!unique) return Promise.reject(); + this.#repoManager!.setUniques([unique]); + const result = await this.observe(this.#repoManager!.entryByUnique(unique)).asPromise(); this.#initResolver?.(result); - return result; + await this.#init; + return { data: result, asObservable: () => this.ownerContentType }; } - public async createScaffold(preset?: Partial) { + public async createScaffold(preset?: Partial): Promise> { await this.#initRepository; this.#clear(); const repsonse = await this.#repository!.createScaffold(preset); - if (!repsonse.data) return {}; + const { data } = repsonse; + if (!data) return { error: repsonse.error }; - this.#ownerContentTypeUnique = repsonse.data.unique; + this.#ownerContentTypeUnique = data.unique; // Add the new content type to the list of content types, this holds our draft state of this scaffold. - this.#contentTypes.appendOne(repsonse.data); - this.#initResolver?.(repsonse); + this.#contentTypes.appendOne(data); + this.#initResolver?.(data); return repsonse; } @@ -166,7 +184,7 @@ export class UmbContentTypeStructureManager< * Save the owner content type. Notice this is for a Content Type that is already stored on the server. * @returns {Promise} - A promise that will be resolved when the content type is saved. */ - public async save() { + public async save(): Promise { await this.#initRepository; const contentType = this.getOwnerContentType(); if (!contentType || !contentType.unique) throw new Error('Could not find the Content Type to save'); @@ -187,88 +205,32 @@ export class UmbContentTypeStructureManager< * @param {string | null} parentUnique - The unique of the parent content type * @returns {Promise} - a promise that is resolved when the content type has been created. */ - public async create(parentUnique: string | null) { + public async create(parentUnique: string | null): Promise { 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); - if (!data) return Promise.reject(); + const { error, data } = await this.#repository!.create(contentType, parentUnique); + if (error || !data) { + throw error?.message ?? 'Repository did not return data after create.'; + } // Update state with latest version: this.#contentTypes.updateOne(contentType.unique, data); - // Start observe the new content type in the store, as we did not do that when it was a scaffold/local-version. - this.#observeContentType(data); + // Let the repo manager know about this new unique, so it can be loaded: + this.#repoManager!.addEntry(data); + return data; } - async #loadContentTypeCompositions(ownerContentTypeCompositions: T['compositions'] | undefined) { - if (!ownerContentTypeCompositions) { - // Owner content type was undefined, so we cannot load compositions. - // But to clean up existing compositions, we set the array to empty to still be able to execute the clean-up code. - ownerContentTypeCompositions = []; - } - + async #loadContentTypeCompositions(contentTypeCompositions: T['compositions'] | undefined) { const ownerUnique = this.getOwnerContentTypeUnique(); - // Remove content types that does not exist as compositions anymore: - this.#contentTypes.getValue().forEach((x) => { - if ( - x.unique !== ownerUnique && - !ownerContentTypeCompositions.find((comp) => comp.contentType.unique === x.unique) - ) { - this.#contentTypeObservers.find((y) => y.controllerAlias === 'observeContentType_' + x.unique)?.destroy(); - this.#contentTypes.removeOne(x.unique); - } - }); - ownerContentTypeCompositions.forEach((composition) => { - this.#ensureType(composition.contentType.unique); - }); - } - - async #ensureType(unique?: string) { - if (!unique) return; - if (this.#contentTypes.getValue().find((x) => x.unique === unique)) return; - await this.#loadType(unique); - } - - async #loadType(unique?: string) { - if (!unique) return {}; - await this.#initRepository; - - // TODO: Some way to know who are already in loading state... - - // Lets initiate the content type: - const { data, asObservable } = await this.#repository!.requestByUnique(unique); - if (!data) return {}; - - await this.#observeContentType(data); - return { data, asObservable }; - } - - 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), - (docType) => { - if (docType) { - this.#contentTypes.appendOne(docType); - } else { - // Remove the content type from the store, if it does not exist anymore. - this.#contentTypes.removeOne(data.unique); - } - }, - 'observeContentType_' + data.unique, - // Controller Alias is used to stop observation when no longer needed. [NL] - ); - - this.#contentTypeObservers.push(ctrl); + if (!ownerUnique) return; + const compositionUniques = contentTypeCompositions?.map((x) => x.contentType.unique) ?? []; + const newUniques = [ownerUnique, ...compositionUniques]; + this.#contentTypes.filter((x) => newUniques.includes(x.unique)); + this.#repoManager?.setUniques(newUniques); } /** Public methods for consuming structure: */ @@ -793,7 +755,9 @@ export class UmbContentTypeStructureManager< [this._host], (permitted, ctrl) => { this.#repository = permitted ? ctrl.api : undefined; - this.#initRepositoryResolver?.(); + if (this.#repository) { + this.#initRepositoryResolver?.(this.#repository); + } }, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts index 030b2cefc2a6..a2b0cd669f4c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/content-type-workspace-context-base.ts @@ -2,7 +2,11 @@ import type { UmbContentTypeCompositionModel, UmbContentTypeDetailModel, UmbCont import { UmbContentTypeStructureManager } from '../structure/index.js'; import type { UmbContentTypeWorkspaceContext } from './content-type-workspace-context.interface.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; +import type { + UmbDetailRepository, + UmbRepositoryResponse, + UmbRepositoryResponseWithAsObservable, +} from '@umbraco-cms/backoffice/repository'; import { UmbEntityDetailWorkspaceContextBase, type UmbEntityDetailWorkspaceContextArgs, @@ -105,11 +109,13 @@ export abstract class UmbContentTypeWorkspaceContextBase< /** * Loads the data for the workspace * @param { string } unique The unique identifier of the data to load - * @returns { Promise } The loaded data + * @returns { Promise | UmbRepositoryResponseWithAsObservable> } The loaded data */ - override async load(unique: string) { + override async load( + unique: string, + ): Promise | UmbRepositoryResponseWithAsObservable> { if (unique === this.getUnique() && this._getDataPromise) { - return (await this._getDataPromise) as any; + return await this._getDataPromise; } this.resetState(); @@ -118,13 +124,12 @@ export abstract class UmbContentTypeWorkspaceContextBase< this._getDataPromise = this.structure.loadType(unique); const response = await this._getDataPromise; const data = response.data; - if (data) { this._data.setPersisted(data); this.setIsNew(false); this.observe( - response.asObservable(), + this.structure.ownerContentType, (entity: any) => this.#onDetailStoreChange(entity), 'umbContentTypeDetailStoreObserver', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts index e92baa60f32a..e8f79456c16e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -127,11 +127,9 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements this.#sorter.disable(); } }, - '_observeIsSorting', + null, ); - //TODO: We need to differentiate between local and composition tabs (and hybrids) - this.#tabsStructureHelper.setContainerChildType('Tab'); this.#tabsStructureHelper.setIsRoot(true); this.observe(this.#tabsStructureHelper.mergedContainers, (tabs) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts index 79e2dc773b2c..c430a6ddda29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts @@ -32,6 +32,9 @@ export class UmbRepositoryDetailsManager #entries = new UmbArrayState([], (x) => x.unique); entries = this.#entries.asObservable(); + entryByUnique(unique: string) { + return this.#entries.asObservablePart((items) => items.find((item) => item.unique === unique)); + } #statuses = new UmbArrayState([], (x) => x.unique); statuses = this.#statuses.asObservable(); @@ -107,6 +110,22 @@ export class UmbRepositoryDetailsManager this.#uniques.setValue(uniques ?? []); } + addUnique(unique: string): void { + this.#uniques.appendOne(unique); + } + + addEntry(data: DetailType): void { + const unique = data.unique; + this.#statuses.appendOne({ + state: { + type: 'success', + }, + unique, + }); + this.#entries.appendOne(data); + this.#uniques.appendOne(unique); + } + getItems(): Array { return this.#entries.getValue(); } @@ -115,14 +134,6 @@ export class UmbRepositoryDetailsManager return this.#entries.asObservablePart((items) => items.find((item) => item.unique === unique)); } - async getItemByUnique(unique: string) { - // TODO: Make an observeOnce feature, to avoid this amount of code: [NL] - const ctrl = this.observe(this.itemByUnique(unique)); - const result = await ctrl.asPromise(); - ctrl.destroy(); - return result; - } - async #requestNewItems(): Promise { await this.#init; if (!this.repository) throw new Error('Repository is not initialized'); 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 6ff40b9e3709..9b4b33b05acf 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 @@ -13,7 +13,11 @@ import { } from '@umbraco-cms/backoffice/entity-action'; import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; -import type { UmbDetailRepository } from '@umbraco-cms/backoffice/repository'; +import type { + UmbDetailRepository, + UmbRepositoryResponse, + UmbRepositoryResponseWithAsObservable, +} from '@umbraco-cms/backoffice/repository'; import { UmbDeprecation, UmbStateManager } from '@umbraco-cms/backoffice/utils'; import { UmbValidationContext } from '@umbraco-cms/backoffice/validation'; import { UmbId } from '@umbraco-cms/backoffice/id'; @@ -44,7 +48,9 @@ export abstract class UmbEntityDetailWorkspaceContextBase< public readonly persistedData = this._data.persisted; public readonly loading = new UmbStateManager(this); - protected _getDataPromise?: Promise; + protected _getDataPromise?: Promise< + UmbRepositoryResponse | UmbRepositoryResponseWithAsObservable + >; protected _detailRepository?: DetailRepositoryType; #eventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; @@ -166,9 +172,11 @@ export abstract class UmbEntityDetailWorkspaceContextBase< return this.#parent.getValue()?.entityType; } - async load(unique: string) { + async load( + unique: string, + ): Promise | UmbRepositoryResponseWithAsObservable> { if (unique === this.getUnique() && this._getDataPromise) { - return (await this._getDataPromise) as GetDataType; + return await this._getDataPromise; } this.resetState(); this.setIsNew(false); @@ -176,8 +184,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Loading ${this.getEntityType()} Details` }); await this.#init; this._getDataPromise = this._detailRepository!.requestByUnique(unique); - type GetDataType = Awaited['requestByUnique']>>; - const response = (await this._getDataPromise) as GetDataType; + const response = await this._getDataPromise; const data = response.data; if (data) { @@ -185,7 +192,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< this._data.setCurrent(data); this.observe( - response.asObservable(), + (response as UmbRepositoryResponseWithAsObservable).asObservable?.(), (entity) => this.#onDetailStoreChange(entity), 'umbEntityDetailTypeStoreObserver', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace.context.ts index 99f6625512ab..dc74e0f923d2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/workspace/user/user-workspace.context.ts @@ -10,6 +10,7 @@ import type { UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/wor import { UmbEntityDetailWorkspaceContextBase } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbRepositoryResponseWithAsObservable } from '@umbraco-cms/backoffice/repository'; type EntityType = UmbUserDetailModel; @@ -60,7 +61,11 @@ export class UmbUserWorkspaceContext override async load(unique: string) { const response = await super.load(unique); - this.observe(response.asObservable?.(), (user) => this.onUserStoreChanges(user), 'umbUserStoreObserver'); + this.observe( + (response as UmbRepositoryResponseWithAsObservable).asObservable?.(), + (user) => this.onUserStoreChanges(user), + 'umbUserStoreObserver', + ); if (!this._detailRepository) { throw new Error('Detail repository is missing'); From 3140b92309f182e510995d8eacee39e0a2d984fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 15 Apr 2025 14:14:19 +0200 Subject: [PATCH 06/23] a little refactor --- .../content-type-structure-manager.class.ts | 47 ++++++------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 063985575320..551ad2605d1b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -25,7 +25,7 @@ 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']; +type UmbPropertyTypeUnique = UmbPropertyTypeModel['unique']; const UmbFilterDuplicateStrings = (value: string, index: number, array: Array) => array.indexOf(value) === index; @@ -342,10 +342,7 @@ export class UmbContentTypeStructureManager< containers.push(clonedContainer); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: fix TS partial complaint [NL] - this.#contentTypes.updateOne(toContentTypeUnique, { containers }); + this.#contentTypes.updateOne(toContentTypeUnique, { containers } as Partial); return clonedContainer; } @@ -398,10 +395,7 @@ export class UmbContentTypeStructureManager< const containers = [...(contentTypes.find((x) => x.unique === contentTypeUnique)?.containers ?? [])]; containers.push(container); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: fix TS partial complaint - this.#contentTypes.updateOne(contentTypeUnique, { containers }); + this.#contentTypes.updateOne(contentTypeUnique, { containers } as Partial); return container; } @@ -424,10 +418,7 @@ export class UmbContentTypeStructureManager< const containers = appendToFrozenArray(frozenContainers, container, (x) => x.id === container.id); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: fix TS partial complaint - this.#contentTypes.updateOne(contentTypeUnique, { containers }); + this.#contentTypes.updateOne(contentTypeUnique, { containers } as Partial); }*/ makeEmptyContainerName( @@ -488,7 +479,11 @@ export class UmbContentTypeStructureManager< ); } - const containers = partialUpdateFrozenArray(frozenContainers, partialUpdate, (x) => x.id === containerId); + const containers: UmbPropertyTypeContainerModel[] = partialUpdateFrozenArray( + frozenContainers, + partialUpdate, + (x) => x.id === containerId, + ); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -515,10 +510,7 @@ export class UmbContentTypeStructureManager< x.container ? !removedContainerIds.some((ids) => ids === x.container?.id) : true, ); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: fix TS partial complaint - this.#contentTypes.updateOne(contentTypeUnique, { containers, properties }); + this.#contentTypes.updateOne(contentTypeUnique, { containers, properties } as Partial); } async insertProperty(contentTypeUnique: string | null, property: UmbPropertyTypeModel) { @@ -546,10 +538,7 @@ export class UmbContentTypeStructureManager< const properties = appendToFrozenArray(frozenProperties, property, (x) => x.id === property.id); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: fix TS partial complaint - this.#contentTypes.updateOne(contentTypeUnique, { properties }); + this.#contentTypes.updateOne(contentTypeUnique, { properties } as Partial); } async removeProperty(contentTypeUnique: string | null, propertyId: string) { @@ -561,10 +550,7 @@ export class UmbContentTypeStructureManager< const properties = filterFrozenArray(frozenProperties, (x) => x.id !== propertyId); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: fix TS partial complaint - this.#contentTypes.updateOne(contentTypeUnique, { properties }); + this.#contentTypes.updateOne(contentTypeUnique, { properties } as Partial); } async updateProperty( @@ -579,10 +565,7 @@ export class UmbContentTypeStructureManager< this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique)?.properties ?? []; const properties = partialUpdateFrozenArray(frozenProperties, partialUpdate, (x) => x.id === propertyId); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // TODO: fix TS partial complaint - this.#contentTypes.updateOne(contentTypeUnique, { properties }); + this.#contentTypes.updateOne(contentTypeUnique, { properties } as Partial); } // TODO: Refactor: These property methods, should maybe be named without structure in their name. @@ -739,9 +722,9 @@ export class UmbContentTypeStructureManager< .find((contentType) => contentType.containers.some((c) => c.id === containerId)); } - contentTypeOfProperty(propertyId: UmbPropertyTypeId) { + contentTypeOfProperty(propertyId: UmbPropertyTypeUnique) { return this.#contentTypes.asObservablePart((contentTypes) => - contentTypes.find((contentType) => contentType.properties.some((p) => p.id === propertyId)), + contentTypes.find((contentType) => contentType.properties.some((p) => p.unique === propertyId)), ); } From 2a21b967d0541d4b501cd683b1ee56de8f228603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 15 Apr 2025 14:22:24 +0200 Subject: [PATCH 07/23] warn --- .../workspace/document-type/document-type-workspace.context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d808ab0e8a38..f91f2c55c9c3 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 @@ -174,7 +174,7 @@ export class UmbDocumentTypeWorkspaceContext super._create(currentData, parent); this.createTemplateMode = false; } catch (error) { - console.log(error); + console.warn(error); } } From 990682de2dde3ac9ddac8cdff2d57fcc75ce5c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 15 Apr 2025 14:28:08 +0200 Subject: [PATCH 08/23] clear state --- .../content-type-structure-manager.class.ts | 1 + .../core/repository/repository-details.manager.ts | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 551ad2605d1b..5673faf35c61 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -749,6 +749,7 @@ export class UmbContentTypeStructureManager< this.#init = new Promise((resolve) => { this.#initResolver = resolve; }); + this.#repoManager?.clear(); this.#contentTypes.setValue([]); this.#contentTypeObservers.forEach((observer) => observer.destroy()); this.#contentTypeObservers = []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts index c430a6ddda29..16d36644a967 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts @@ -75,12 +75,10 @@ export class UmbRepositoryDetailsManager (uniques) => { // remove entries based on no-longer existing uniques: const removedEntries = this.#entries.getValue().filter((entry) => !uniques.includes(entry.unique)); + this.#entries.remove(removedEntries); + this.#statuses.remove(removedEntries); removedEntries.forEach((entry) => { - const unique = entry.unique; - if (unique) { - this.#entries.removeOne(unique); - this.removeUmbControllerByAlias('observeEntry_' + unique); - } + this.removeUmbControllerByAlias('observeEntry_' + entry.unique); }); this.#requestNewItems(); @@ -102,6 +100,12 @@ export class UmbRepositoryDetailsManager }); } + clear(): void { + this.#uniques.setValue([]); + this.#entries.setValue([]); + this.#statuses.setValue([]); + } + getUniques(): Array { return this.#uniques.getValue(); } From cb54166a5c771a9edd1f80695b37624d2a06ebab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 15 Apr 2025 15:42:05 +0200 Subject: [PATCH 09/23] refactor to use unique --- ...nt-type-property-structure-helper.class.ts | 18 ++++---- .../content-type-structure-manager.class.ts | 18 ++++---- ...t-type-design-editor-properties.element.ts | 14 +++---- ...ent-type-design-editor-property.element.ts | 26 +++++++----- .../property-type-workspace.context.ts | 42 ++++++++++--------- 5 files changed, 64 insertions(+), 54 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts index 6c2a98b60e5d..6ff9acab953c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-property-structure-helper.class.ts @@ -9,7 +9,7 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbArrayState, mergeObservables } from '@umbraco-cms/backoffice/observable-api'; -type UmbPropertyTypeId = UmbPropertyTypeModel['id']; +type UmbPropertyTypeUnique = UmbPropertyTypeModel['unique']; /** * This class is a helper class for managing the structure of containers in a content type. @@ -24,7 +24,7 @@ export class UmbContentTypePropertyStructureHelper([], (x) => x.id); + #propertyStructure = new UmbArrayState([], (x) => x.unique); readonly propertyStructure = this.#propertyStructure.asObservable(); constructor(host: UmbControllerHost) { @@ -164,18 +164,20 @@ export class UmbContentTypePropertyStructureHelper x?.properties.some((y) => y.id === propertyId)); + return this.#structure.ownerContentTypeObservablePart((x) => + x?.properties.some((y) => y.unique === propertyUnique), + ); } - async contentTypeOfProperty(propertyId: UmbPropertyTypeId) { + async contentTypeOfProperty(propertyUnique: UmbPropertyTypeUnique) { await this.#init; if (!this.#structure) return; - return this.#structure.contentTypeOfProperty(propertyId); + return this.#structure.contentTypeOfProperty(propertyUnique); } // TODO: consider moving this to another class, to separate 'viewer' from 'manipulator': @@ -194,11 +196,11 @@ export class UmbContentTypePropertyStructureHelper x.unique === contentTypeUnique)?.properties ?? []; - const properties = appendToFrozenArray(frozenProperties, property, (x) => x.id === property.id); + const properties = appendToFrozenArray(frozenProperties, property, (x) => x.unique === property.unique); this.#contentTypes.updateOne(contentTypeUnique, { properties } as Partial); } - async removeProperty(contentTypeUnique: string | null, propertyId: string) { + async removeProperty(contentTypeUnique: string | null, propertyUnique: string) { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; const frozenProperties = this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique)?.properties ?? []; - const properties = filterFrozenArray(frozenProperties, (x) => x.id !== propertyId); + const properties = filterFrozenArray(frozenProperties, (x) => x.unique !== propertyUnique); this.#contentTypes.updateOne(contentTypeUnique, { properties } as Partial); } async updateProperty( contentTypeUnique: string | null, - propertyId: string, + propertyUnique: string, partialUpdate: Partial, ) { await this.#init; @@ -563,17 +563,17 @@ export class UmbContentTypeStructureManager< const frozenProperties = this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique)?.properties ?? []; - const properties = partialUpdateFrozenArray(frozenProperties, partialUpdate, (x) => x.id === propertyId); + const properties = partialUpdateFrozenArray(frozenProperties, partialUpdate, (x) => x.unique === propertyUnique); this.#contentTypes.updateOne(contentTypeUnique, { properties } as Partial); } // TODO: Refactor: These property methods, should maybe be named without structure in their name. - async propertyStructureById(propertyId: string) { + async propertyStructureById(propertyUnique: string) { await this.#init; return this.#contentTypes.asObservablePart((docTypes) => { for (const docType of docTypes) { - const foundProp = docType.properties?.find((property) => property.id === propertyId); + const foundProp = docType.properties?.find((property) => property.unique === propertyUnique); if (foundProp) { return foundProp; } @@ -594,10 +594,10 @@ export class UmbContentTypeStructureManager< }); } - async getPropertyStructureById(propertyId: string) { + async getPropertyStructureById(propertyUnique: string) { await this.#init; for (const docType of this.#contentTypes.getValue()) { - const foundProp = docType.properties?.find((property) => property.id === propertyId); + const foundProp = docType.properties?.find((property) => property.unique === propertyUnique); if (foundProp) { return foundProp; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts index a495bbab4778..7fe61dbb1066 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor-properties.element.ts @@ -29,7 +29,7 @@ const SORTER_CONFIG: UmbSorterConfig { - return modelEntry.id; + return modelEntry.unique; }, identifier: 'content-type-property-sorter', itemSelector: 'umb-content-type-design-editor-property', @@ -52,7 +52,7 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement { if (this._containerId === undefined) { throw new Error('ContainerId is not set'); } - this.#propertyStructureHelper.partialUpdateProperty(item.id, { + this.#propertyStructureHelper.partialUpdateProperty(item.unique, { container: this._containerId ? { id: this._containerId } : null, }); }, @@ -66,7 +66,7 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement { * the overlap if true, which may cause another overlap, so we loop through them till no more overlaps... */ const model = this._properties; - const newIndex = model.findIndex((entry) => entry.id === item.id); + const newIndex = model.findIndex((entry) => entry.unique === item.unique); // Doesn't exist in model if (newIndex === -1) return; @@ -80,7 +80,7 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement { } // increase the prevSortOrder and use it for the moved item, - this.#propertyStructureHelper.partialUpdateProperty(item.id, { + this.#propertyStructureHelper.partialUpdateProperty(item.unique, { sortOrder: ++prevSortOrder, }); @@ -90,7 +90,7 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement { // As long as there is an item with the index & the sortOrder is less or equal to the prevSortOrder, we will update the sortOrder: while ((entry = model[i]) !== undefined && entry.sortOrder <= prevSortOrder) { // Increase the prevSortOrder and use it for the item: - this.#propertyStructureHelper.partialUpdateProperty(entry.id, { + this.#propertyStructureHelper.partialUpdateProperty(entry.unique, { sortOrder: ++prevSortOrder, }); @@ -257,11 +257,11 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement {
${repeat( this._properties, - (property) => property.id, + (property) => property.unique, (property) => { return html` { this._inherited = this._propertyStructureHelper?.getStructureManager()?.getOwnerContentTypeUnique() !== contentType?.unique; @@ -112,7 +112,7 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { #partialUpdate(partialObject: UmbPropertyTypeModel) { if (!this._property || !this._propertyStructureHelper) return; - this._propertyStructureHelper.partialUpdateProperty(this._property.id, partialObject); + this._propertyStructureHelper.partialUpdateProperty(this._property.unique, partialObject); } #singleValueUpdate( @@ -122,7 +122,7 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { if (!this._property || !this._propertyStructureHelper) return; const partialObject: Partial = {}; partialObject[propertyName] = value === null ? undefined : value; - this._propertyStructureHelper.partialUpdateProperty(this._property.id, partialObject); + this._propertyStructureHelper.partialUpdateProperty(this._property.unique, partialObject); } #onToggleAliasLock(event: CustomEvent) { @@ -152,17 +152,19 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { async #requestRemove(e: Event) { e.preventDefault(); e.stopImmediatePropagation(); - if (!this._property || !this._property.id) return; + if (!this._property || !this._property.unique) return; + + const unique = this._property.unique; // TODO: Do proper localization here: [NL] 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}
`, + content: html`Are you sure you want to delete the property ${this._property.name ?? unique}`, confirmLabel: this.localize.term('actions_delete'), color: 'danger', }); - this._propertyStructureHelper?.removeProperty(this._property.id); + this._propertyStructureHelper?.removeProperty(unique); } #onAliasChanged(event: UUIInputEvent) { @@ -247,7 +249,7 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { look="outline" label=${this.localize.term('contentTypeEditor_editorSettings')} href=${this.editPropertyTypePath + - UMB_EDIT_PROPERTY_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique: this.property.id })}> + UMB_EDIT_PROPERTY_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique: this.property.unique })}> ${this.renderPropertyTags()} @@ -259,6 +261,9 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { } } + #onPropertyOrderChanged = (e: UUIInputEvent) => + this.#partialUpdate({ sortOrder: parseInt(e.target.value as string) ?? 0 } as UmbPropertyTypeModel); + renderSortableProperty() { if (!this.property) return; return html` @@ -271,9 +276,8 @@ export class UmbContentTypeDesignEditorPropertyElement extends UmbLitElement { type="number" ?disabled=${this._inherited} label="sort order" - @change=${(e: UUIInputEvent) => - this.#partialUpdate({ sortOrder: parseInt(e.target.value as string) ?? 0 } as UmbPropertyTypeModel)} - .value=${this.property.sortOrder ?? 0}> + @change=${this.#onPropertyOrderChanged} + .value=${(this.property.sortOrder ?? 0).toString()}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/property-type-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/property-type-workspace.context.ts index 25add74dbc56..d3c218dc2834 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/property-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/property-type-workspace.context.ts @@ -16,14 +16,16 @@ import { } from '@umbraco-cms/backoffice/workspace'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import type { UmbPropertyTypeScaffoldModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content-type'; import { UmbId } from '@umbraco-cms/backoffice/id'; import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs'; import { UmbValidationContext } from '@umbraco-cms/backoffice/validation'; -export class UmbPropertyTypeWorkspaceContext - extends UmbSubmittableWorkspaceContextBase +type PropertyTypeDataModel = UmbPropertyTypeScaffoldModel; + +export class UmbPropertyTypeWorkspaceContext + extends UmbSubmittableWorkspaceContextBase implements UmbInvariantDatasetWorkspaceContext, UmbRoutableWorkspaceContext { // Just for context token safety: @@ -34,11 +36,11 @@ export class UmbPropertyTypeWorkspaceContext(undefined); + #data = new UmbObjectState(undefined); readonly data = this.#data.asObservable(); readonly name = this.#data.asObservablePart((data) => data?.name); @@ -57,12 +59,12 @@ export class UmbPropertyTypeWorkspaceContext { if (unique) { - this.validationgContext.setDataPath(UmbDataPathPropertyTypeQuery({ id: unique })); + this.validationContext.setDataPath(UmbDataPathPropertyTypeQuery({ id: unique })); } }); @@ -118,7 +120,7 @@ export class UmbPropertyTypeWorkspaceContext { if (property) { - this.#data.setValue(property as PropertyTypeData); + this.#data.setValue(property as PropertyTypeDataModel); //this.#persistedData.setValue(property); //this.#currentData.setValue(property); @@ -135,8 +137,10 @@ export class UmbPropertyTypeWorkspaceContext) { + updateData(partialData: Partial) { this.#data?.update(partialData); } @@ -194,12 +198,12 @@ export class UmbPropertyTypeWorkspaceContext | undefined>} * @description Get an Observable for the value of this property. */ - async propertyValueByAlias(propertyAlias: string) { - return this.#data.asObservablePart((data) => data?.[propertyAlias as keyof PropertyTypeData] as ReturnType); + async propertyValueByAlias(propertyAlias: keyof PropertyTypeDataModel) { + return this.#data.asObservablePart((data) => data?.[propertyAlias] as ReturnType); } - getPropertyValue(propertyAlias: string) { - return this.#data.getValue()?.[propertyAlias as keyof PropertyTypeData] as ReturnType; + getPropertyValue(propertyAlias: keyof PropertyTypeDataModel) { + return this.#data.getValue()?.[propertyAlias] as ReturnType; } /** @@ -230,8 +234,8 @@ export class UmbPropertyTypeWorkspaceContext Date: Tue, 15 Apr 2025 15:48:34 +0200 Subject: [PATCH 10/23] note --- .../src/packages/core/repository/repository-details.manager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts index 16d36644a967..048948641ab5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts @@ -128,6 +128,7 @@ export class UmbRepositoryDetailsManager }); this.#entries.appendOne(data); this.#uniques.appendOne(unique); + // Notice in this case we do not have a observable from the repo, but it should maybe be fine that we just listen for ACTION EVENTS. } getItems(): Array { From 1fccfc1d40051ae4972f831200cc173505f8f97b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 15 Apr 2025 16:01:37 +0200 Subject: [PATCH 11/23] code corrections to match with types --- .../block/block/workspace/block-element-manager.ts | 6 +++++- .../content-type-structure-manager.class.ts | 12 ++++++++++-- .../property-workspace-view-settings.element.ts | 6 +++--- .../document-blueprint-workspace.context.ts | 10 +++++++++- .../workspace/document-workspace.context.ts | 10 +++++++++- .../media/media/workspace/media-workspace.context.ts | 10 +++++++++- .../workspace/member/member-workspace.context.ts | 10 +++++++++- 7 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts index ea180f72b730..dbf3edb45f9a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts @@ -73,7 +73,11 @@ export class UmbBlockElementManager this.structure.loadType(id)); + this.observe(this.contentTypeId, (id) => { + if (id) { + this.structure.loadType(id); + } + }); this.observe(this.unique, (key) => { if (key) { this.validation.setDataPath('$.' + dataPathPropertyName + `[?(@.key == '${key}')]`); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index cf7f3ed34314..6ae551eff056 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -147,7 +147,7 @@ export class UmbContentTypeStructureManager< * @param {string} unique - The unique of the ContentType to load. * @returns {Promise} - Promise resolved */ - public async loadType(unique?: string): Promise> { + public async loadType(unique: string): Promise> { if (this.#ownerContentTypeUnique === unique) { // Its the same, but we do not know if its done loading jet, so we will wait for the load promise to finish. [NL] await this.#init; @@ -156,7 +156,11 @@ export class UmbContentTypeStructureManager< await this.#initRepository; this.#clear(); this.#ownerContentTypeUnique = unique; - if (!unique) return Promise.reject(); + if (!unique) { + return Promise.reject( + new Error('The unique identifier is missing. A valid unique identifier is required to load the content type.'), + ); + } this.#repoManager!.setUniques([unique]); const result = await this.observe(this.#repoManager!.entryByUnique(unique)).asPromise(); this.#initResolver?.(result); @@ -176,6 +180,8 @@ export class UmbContentTypeStructureManager< // Add the new content type to the list of content types, this holds our draft state of this scaffold. this.#contentTypes.appendOne(data); + // Make a entry in the repo manager: + this.#repoManager!.addEntry(data); this.#initResolver?.(data); return repsonse; } @@ -197,6 +203,8 @@ export class UmbContentTypeStructureManager< // Update state with latest version: this.#contentTypes.updateOne(contentType.unique, data); + // Update entry in the repo manager: + this.#repoManager!.addEntry(data); return data; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/views/settings/property-workspace-view-settings.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/views/settings/property-workspace-view-settings.element.ts index 8c069f12d1e3..1b9a248ec81d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/views/settings/property-workspace-view-settings.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/property-type/workspace/views/settings/property-workspace-view-settings.element.ts @@ -5,7 +5,7 @@ import { umbBindToValidation } from '@umbraco-cms/backoffice/validation'; import { UmbLitElement, umbFocus } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content-type'; -import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import type { UmbPropertyTypeScaffoldModel } from '@umbraco-cms/backoffice/content-type'; import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; import type { UUIBooleanInputEvent, @@ -44,7 +44,7 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i ]; @state() - private _data?: UmbPropertyTypeModel; + private _data?: UmbPropertyTypeScaffoldModel; @state() private _aliasLocked = true; @@ -89,7 +89,7 @@ export class UmbPropertyTypeWorkspaceViewSettingsElement extends UmbLitElement i }).passContextAliasMatches(); } - updateValue(partialValue: Partial) { + updateValue(partialValue: Partial) { this.#context?.updateData(partialValue); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts index aacaad252d7e..ab33e43fa53e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/workspace/document-blueprint-workspace.context.ts @@ -47,7 +47,15 @@ export class UmbDocumentBlueprintWorkspaceContext contentTypePropertyName: 'documentType', }); - this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); + this.observe( + this.contentTypeUnique, + (unique) => { + if (unique) { + this.structure.loadType(unique); + } + }, + null, + ); this.routes.setRoutes([ { 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 5407aeb4a1f5..f9c8c6933457 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 @@ -97,7 +97,15 @@ export class UmbDocumentWorkspaceContext } }); - this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); + this.observe( + this.contentTypeUnique, + (unique) => { + if (unique) { + this.structure.loadType(unique); + } + }, + null, + ); // TODO: Remove this in v17 as we have moved the publishing methods to the UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT. this.consumeContext(UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT, (context) => { 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 029aae91fa72..7412ca8b7ae3 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 @@ -54,7 +54,15 @@ export class UmbMediaWorkspaceContext contentTypePropertyName: 'mediaType', }); - this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); + this.observe( + this.contentTypeUnique, + (unique) => { + if (unique) { + this.structure.loadType(unique); + } + }, + null, + ); this.propertyViewGuard.fallbackToPermitted(); this.propertyWriteGuard.fallbackToPermitted(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts index 426a039c9509..19b0327095ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts @@ -44,7 +44,15 @@ export class UmbMemberWorkspaceContext contentTypePropertyName: 'memberType', }); - this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); + this.observe( + this.contentTypeUnique, + (unique) => { + if (unique) { + this.structure.loadType(unique); + } + }, + null, + ); this.propertyViewGuard.fallbackToPermitted(); this.propertyWriteGuard.fallbackToPermitted(); From 5d9174ad219053851a187ac65aa367d451a67bab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 22 Apr 2025 15:47:18 +0200 Subject: [PATCH 12/23] unique type for Array State --- .../libs/observable-api/states/array-state.ts | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) 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 579efc0e3c17..e43bc298e997 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 @@ -11,11 +11,11 @@ import { UmbDeepState } from './deep-state.js'; * * The ArrayState provides methods to append data when the data is an Object. */ -export class UmbArrayState extends UmbDeepState { - readonly getUniqueMethod: (entry: T) => unknown; +export class UmbArrayState extends UmbDeepState { + readonly getUniqueMethod: (entry: T) => U; #sortMethod?: (a: T, b: T) => number; - constructor(initialData: T[], getUniqueOfEntryMethod: (entry: T) => unknown) { + constructor(initialData: T[], getUniqueOfEntryMethod: (entry: T) => U) { super(initialData); this.getUniqueMethod = getUniqueOfEntryMethod; } @@ -42,6 +42,8 @@ export class UmbArrayState extends UmbDeepState { return this; } + debug = false; + /** * @function setValue * @param value @@ -61,6 +63,27 @@ export class UmbArrayState extends UmbDeepState { } } + /** + * @function hasOne + * @param {U} unique - the unique value to compare with. + * @returns {boolean} Wether it existed + * @description - Check if a unique value exists in the current data of this Subject. + * @example Example check for key to exist. + * const data = [ + * { key: 1, value: 'foo'}, + * { key: 2, value: 'bar'} + * ]; + * const myState = new UmbArrayState(data, (x) => x.key); + * myState.hasOne(1); + */ + hasOne(unique: U): boolean { + if (this.getUniqueMethod) { + return this.getValue().some((x) => this.getUniqueMethod(x) === unique); + } else { + throw new Error('Cannot use hasOne when no unique method provided to check for uniqueness'); + } + } + /** * @function remove * @param {unknown[]} uniques - The unique values to remove. From 0738ea0d1b1dc7a5dc923cfbf265632dad7c0e4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 22 Apr 2025 15:48:07 +0200 Subject: [PATCH 13/23] implement usedForInheritance and editedTypes for Structure Manager and Compositions --- .../composition-picker-modal.element.ts | 29 ++++++++++----- .../composition-picker-modal.token.ts | 1 + .../content-type-structure-manager.class.ts | 29 ++++++++++++++- .../content-type-design-editor.element.ts | 37 +++++++++++++++---- ...member-type-workspace-view-info.element.ts | 1 - ...mber-workspace-view-member-info.element.ts | 1 - 6 files changed, 78 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.element.ts index 24da1d903204..e0cf37273d21 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.element.ts @@ -35,6 +35,10 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< @state() private _selection: Array = []; + + @state() + private _usedForInheritance: Array = []; + override connectedCallback() { super.connectedCallback(); @@ -48,6 +52,7 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< } this._selection = this.data?.selection ?? []; + this._usedForInheritance = this.data?.usedForInheritance ?? []; this.modalContext?.setValue({ selection: this._selection }); const isNew = this.data!.isNew; @@ -206,16 +211,20 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement< return repeat( compositionsList, (compositions) => compositions.unique, - (compositions) => html` - this.#onSelectionAdd(compositions.unique)} - @deselected=${() => this.#onSelectionRemove(compositions.unique)} - ?selected=${this._selection.find((unique) => unique === compositions.unique)}> - - - `, + (compositions) => { + const usedForInheritance = this._usedForInheritance.includes(compositions.unique); + return html` + this.#onSelectionAdd(compositions.unique)} + @deselected=${() => this.#onSelectionRemove(compositions.unique)} + ?selected=${this._selection.find((unique) => unique === compositions.unique)}> + + + `; + }, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.token.ts index ec4e047e50f9..a711e4e4df9e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/modals/composition-picker/composition-picker-modal.token.ts @@ -4,6 +4,7 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbCompositionPickerModalData { compositionRepositoryAlias: string; selection: Array; + usedForInheritance: Array; unique: string | null; isElement: boolean; currentPropertyAliases: Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 6ae551eff056..05c265c40201 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -45,6 +45,8 @@ export class UmbContentTypeStructureManager< this.#initResolver = resolve; }); + #editedTypes = new UmbArrayState([], (x) => x); + #repository?: UmbDetailRepository; #initRepositoryResolver?: (repo: UmbDetailRepository) => void; @@ -78,6 +80,12 @@ export class UmbContentTypeStructureManager< readonly contentTypeCompositions = this.#contentTypes.asObservablePart((contentTypes) => { return contentTypes.flatMap((x) => x.compositions ?? []); }); + async getContentTypeCompositions() { + return await this.observe(this.contentTypeCompositions).asPromise(); + } + async getOwnerContentTypeCompositions() { + return await this.observe(this.ownerContentTypeCompositions).asPromise(); + } readonly #contentTypeContainers = this.#contentTypes.asObservablePart((contentTypes) => { return contentTypes.flatMap((x) => x.containers ?? []); }); @@ -129,7 +137,16 @@ export class UmbContentTypeStructureManager< ); } this.#repoManager = new UmbRepositoryDetailsManager(this, typeRepository); - this.observe(this.#repoManager.entries, (entries) => this.#contentTypes.append(entries), null); + this.observe( + this.#repoManager.entries, + (entries) => { + // Prevent updating once that are have edited here. + entries = entries.filter((x) => !(this.#editedTypes.hasOne(x.unique) && this.#contentTypes.hasOne(x.unique))); + + this.#contentTypes.append(entries); + }, + null, + ); }); // Observe all Content Types compositions: [NL] @@ -238,6 +255,7 @@ export class UmbContentTypeStructureManager< const compositionUniques = contentTypeCompositions?.map((x) => x.contentType.unique) ?? []; const newUniques = [ownerUnique, ...compositionUniques]; this.#contentTypes.filter((x) => newUniques.includes(x.unique)); + await Promise.resolve(); this.#repoManager?.setUniques(newUniques); } @@ -273,6 +291,7 @@ export class UmbContentTypeStructureManager< } updateOwnerContentType(entry: Partial) { + this.#editedTypes.appendOne(this.#ownerContentTypeUnique!); this.#contentTypes.updateOne(this.#ownerContentTypeUnique, entry); } @@ -322,6 +341,7 @@ export class UmbContentTypeStructureManager< ): Promise { await this.#init; toContentTypeUnique = toContentTypeUnique ?? this.#ownerContentTypeUnique!; + this.#editedTypes.appendOne(toContentTypeUnique); // Find container. const container = this.#containers.getValue().find((x) => x.id === containerId); @@ -364,6 +384,7 @@ export class UmbContentTypeStructureManager< this.getOwnerContainers(type, parentId)?.forEach((container) => { if (container.name === '') { const newName = 'Unnamed'; + this.#editedTypes.appendOne(contentTypeUnique); this.updateContainer(null, container.id, { name: this.makeContainerNameUniqueForOwnerContentType(container.id, newName, type, parentId) ?? newName, }); @@ -379,6 +400,7 @@ export class UmbContentTypeStructureManager< ): Promise { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; + this.#editedTypes.appendOne(contentTypeUnique); if (parentId) { const duplicatedParentContainer = await this.ensureContainerOf(parentId, contentTypeUnique); @@ -464,6 +486,7 @@ export class UmbContentTypeStructureManager< ) { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; + this.#editedTypes.appendOne(contentTypeUnique); /* // If we have a container, we need to ensure it exists, and then update the container with the new parent id. @@ -502,6 +525,7 @@ export class UmbContentTypeStructureManager< async removeContainer(contentTypeUnique: string | null, containerId: string | null = null) { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; + this.#editedTypes.appendOne(contentTypeUnique); const contentType = this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique); if (!contentType) { @@ -524,6 +548,7 @@ export class UmbContentTypeStructureManager< async insertProperty(contentTypeUnique: string | null, property: UmbPropertyTypeModel) { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; + this.#editedTypes.appendOne(contentTypeUnique); // If we have a container, we need to ensure it exists, and then update the container with the new parent id. [NL] if (property.container) { @@ -552,6 +577,7 @@ export class UmbContentTypeStructureManager< async removeProperty(contentTypeUnique: string | null, propertyUnique: string) { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; + this.#editedTypes.appendOne(contentTypeUnique); const frozenProperties = this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique)?.properties ?? []; @@ -568,6 +594,7 @@ export class UmbContentTypeStructureManager< ) { await this.#init; contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!; + this.#editedTypes.appendOne(contentTypeUnique); const frozenProperties = this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique)?.properties ?? []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts index e8f79456c16e..39022db71d4e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -1,5 +1,9 @@ import { UMB_CONTENT_TYPE_WORKSPACE_CONTEXT } from '../../content-type-workspace.context-token.js'; -import type { UmbContentTypeModel, UmbPropertyTypeContainerModel } from '../../../types.js'; +import type { + UmbContentTypeCompositionModel, + UmbContentTypeModel, + UmbPropertyTypeContainerModel, +} from '../../../types.js'; import { UmbContentTypeContainerStructureHelper, UmbContentTypeMoveRootGroupsIntoFirstTabHelper, @@ -371,17 +375,29 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements if (!unique) { throw new Error('Content Type unique is undefined'); } - const contentTypes = this.#workspaceContext.structure.getContentTypes(); const ownerContentType = this.#workspaceContext.structure.getOwnerContentType(); if (!ownerContentType) { throw new Error('Owner Content Type not found'); } + const currentCompositions = await this.#workspaceContext.structure.getContentTypeCompositions(); + const currentInheritanceCompositions = currentCompositions.filter( + (composition) => composition.compositionType === CompositionTypeModel.INHERITANCE, + ); + + const currentOwnerCompositions = await this.#workspaceContext.structure.getOwnerContentTypeCompositions(); + const currentOwnerCompositionCompositions = currentOwnerCompositions.filter( + (composition) => composition.compositionType === CompositionTypeModel.COMPOSITION, + ); + const currentOwnerInheritanceCompositions = currentOwnerCompositions.filter( + (composition) => composition.compositionType === CompositionTypeModel.INHERITANCE, + ); + const compositionConfiguration = { compositionRepositoryAlias: this._compositionRepositoryAlias, unique: unique, - // Here we use the loaded content types to declare what we already inherit. That puts a pressure on cleaning up, but thats a good thing. [NL] - selection: contentTypes.map((contentType) => contentType.unique).filter((id) => id !== unique), + selection: currentOwnerCompositionCompositions.map((composition) => composition.contentType.unique), + usedForInheritance: currentInheritanceCompositions.map((composition) => composition.contentType.unique), isElement: ownerContentType.isElement, currentPropertyAliases: [], isNew: this.#workspaceContext.getIsNew()!, @@ -395,9 +411,16 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements const compositionIds = value.selection; - this.#workspaceContext?.setCompositions( - compositionIds.map((unique) => ({ contentType: { unique }, compositionType: CompositionTypeModel.COMPOSITION })), - ); + this.#workspaceContext.setCompositions([ + ...currentOwnerInheritanceCompositions, + ...compositionIds.map( + (unique) => + ({ + contentType: { unique }, + compositionType: CompositionTypeModel.COMPOSITION, + }) as UmbContentTypeCompositionModel, + ), + ]); } override render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group/views/info/member-type-workspace-view-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group/views/info/member-type-workspace-view-info.element.ts index d56c718f6178..5abc7a6fa8e9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group/views/info/member-type-workspace-view-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-group/workspace/member-group/views/info/member-type-workspace-view-info.element.ts @@ -1,4 +1,3 @@ -// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; import { UMB_MEMBER_GROUP_WORKSPACE_CONTEXT } from '../../member-group-workspace.context-token.js'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/views/member/member-workspace-view-member-info.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/views/member/member-workspace-view-member-info.element.ts index 4f6e625a1cdc..712896f81209 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/views/member/member-workspace-view-member-info.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/views/member/member-workspace-view-member-info.element.ts @@ -1,4 +1,3 @@ -// import { UMB_COMPOSITION_PICKER_MODAL, type UmbCompositionPickerModalData } from '../../../modals/index.js'; import { UMB_MEMBER_WORKSPACE_CONTEXT } from '../../member-workspace.context-token.js'; import { UmbMemberKind, type UmbMemberKindType } from '../../../../utils/index.js'; import { TimeFormatOptions } from './utils.js'; From 7da6258805d1236dcd9d5fb1a29aa1967f2a81e1 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 23 Apr 2025 11:17:03 +0200 Subject: [PATCH 14/23] rename method --- .../src/libs/observable-api/states/array-state.ts | 4 ++-- .../structure/content-type-structure-manager.class.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) 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 e43bc298e997..c36a66fe4c49 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 @@ -64,7 +64,7 @@ export class UmbArrayState extends UmbDeepState { } /** - * @function hasOne + * @function getHasOne * @param {U} unique - the unique value to compare with. * @returns {boolean} Wether it existed * @description - Check if a unique value exists in the current data of this Subject. @@ -76,7 +76,7 @@ export class UmbArrayState extends UmbDeepState { * const myState = new UmbArrayState(data, (x) => x.key); * myState.hasOne(1); */ - hasOne(unique: U): boolean { + getHasOne(unique: U): boolean { if (this.getUniqueMethod) { return this.getValue().some((x) => this.getUniqueMethod(x) === unique); } else { diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 05c265c40201..e0e121135358 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -141,7 +141,9 @@ export class UmbContentTypeStructureManager< this.#repoManager.entries, (entries) => { // Prevent updating once that are have edited here. - entries = entries.filter((x) => !(this.#editedTypes.hasOne(x.unique) && this.#contentTypes.hasOne(x.unique))); + entries = entries.filter( + (x) => !(this.#editedTypes.getHasOne(x.unique) && this.#contentTypes.getHasOne(x.unique)), + ); this.#contentTypes.append(entries); }, From c69ad1941dd6ef16e77d19cb3fde5fe9097403ea Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 23 Apr 2025 11:18:58 +0200 Subject: [PATCH 15/23] Update repository-details.manager.ts --- .../packages/core/repository/repository-details.manager.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts index 048948641ab5..54441fe3dea5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts @@ -7,7 +7,7 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbEntityUpdatedEvent } from '@umbraco-cms/backoffice/entity-action'; -interface UmbRepositoryStatus { +interface UmbRepositoryRequestStatus { state: { type: 'success' | 'error' | 'loading'; error?: string; @@ -36,7 +36,7 @@ export class UmbRepositoryDetailsManager return this.#entries.asObservablePart((items) => items.find((item) => item.unique === unique)); } - #statuses = new UmbArrayState([], (x) => x.unique); + #statuses = new UmbArrayState([], (x) => x.unique); statuses = this.#statuses.asObservable(); /** @@ -179,7 +179,7 @@ export class UmbRepositoryDetailsManager error: '#general_notFound', }, unique, - } as UmbRepositoryStatus); + } as UmbRepositoryRequestStatus); this.#entries.removeOne(unique); this.removeUmbControllerByAlias('observeEntry_' + unique); } From 9cff888133cbcbd3fe6c1d9e5c8288544c58b602 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 23 Apr 2025 11:22:32 +0200 Subject: [PATCH 16/23] avoid type casting --- .../design/content-type-design-editor.element.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts index 39022db71d4e..35bf37453c4a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -413,13 +413,13 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements this.#workspaceContext.setCompositions([ ...currentOwnerInheritanceCompositions, - ...compositionIds.map( - (unique) => - ({ - contentType: { unique }, - compositionType: CompositionTypeModel.COMPOSITION, - }) as UmbContentTypeCompositionModel, - ), + ...compositionIds.map((unique) => { + const model: UmbContentTypeCompositionModel = { + contentType: { unique }, + compositionType: CompositionTypeModel.COMPOSITION, + }; + return model; + }), ]); } From 21a791d7ef35b463d954bfbcde5bc6b58b4cdade Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 23 Apr 2025 12:11:27 +0200 Subject: [PATCH 17/23] align naming --- .../repository/repository-details.manager.ts | 72 ++++++++++++++----- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts index 54441fe3dea5..fb58c28815b4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-details.manager.ts @@ -1,6 +1,6 @@ import type { UmbDetailRepository } from './detail/detail-repository.interface.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observable-api'; import { type ManifestRepository, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; @@ -15,6 +15,12 @@ interface UmbRepositoryRequestStatus { unique: string; } +/** + * @export + * @class UmbRepositoryDetailsManager + * @augments {UmbControllerBase} + * @template DetailType + */ export class UmbRepositoryDetailsManager extends UmbControllerBase { // repository?: UmbDetailRepository; @@ -32,18 +38,15 @@ export class UmbRepositoryDetailsManager #entries = new UmbArrayState([], (x) => x.unique); entries = this.#entries.asObservable(); - entryByUnique(unique: string) { - return this.#entries.asObservablePart((items) => items.find((item) => item.unique === unique)); - } #statuses = new UmbArrayState([], (x) => x.unique); statuses = this.#statuses.asObservable(); /** - * Creates an instance of UmbRepositoryItemsManager. + * Creates an instance of UmbRepositoryDetailsManager. * @param {UmbControllerHost} host - The host for the controller. * @param {string} repository - The alias of the repository to use. - * @memberof UmbRepositoryItemsManager + * @memberof UmbRepositoryDetailsManager */ constructor(host: UmbControllerHost, repository: UmbDetailRepository | string) { super(host); @@ -81,7 +84,7 @@ export class UmbRepositoryDetailsManager this.removeUmbControllerByAlias('observeEntry_' + entry.unique); }); - this.#requestNewItems(); + this.#requestNewDetails(); }, null, ); @@ -100,24 +103,48 @@ export class UmbRepositoryDetailsManager }); } + /** + * Clear the manager + * @memberof UmbRepositoryDetailsManager + */ clear(): void { this.#uniques.setValue([]); this.#entries.setValue([]); this.#statuses.setValue([]); } + /** + * Get the uniques in the manager + * @returns {Array} - The uniques in the manager. + * @memberof UmbRepositoryDetailsManager + */ getUniques(): Array { return this.#uniques.getValue(); } - setUniques(uniques: string[] | undefined): void { + /** + * Set the uniques in the manager + * @param {(string[] | undefined)} uniques + * @memberof UmbRepositoryDetailsManager + */ + setUniques(uniques: Array | undefined): void { this.#uniques.setValue(uniques ?? []); } + /** + * Add a unique to the manager + * @param {string} unique + * @memberof UmbRepositoryDetailsManager + */ addUnique(unique: string): void { this.#uniques.appendOne(unique); } + /** + * Add an entry to the manager + * @param {DetailType} data + * @memberof UmbRepositoryDetailsManager + */ addEntry(data: DetailType): void { const unique = data.unique; this.#statuses.appendOne({ @@ -131,15 +158,26 @@ export class UmbRepositoryDetailsManager // Notice in this case we do not have a observable from the repo, but it should maybe be fine that we just listen for ACTION EVENTS. } - getItems(): Array { + /** + * Get all entries in the manager + * @returns {Array} - The entries in the manager. + * @memberof UmbRepositoryDetailsManager + */ + getEntries(): Array { return this.#entries.getValue(); } - itemByUnique(unique: string) { + /** + * Get an entry observable by unique + * @param {string} unique + * @returns {Observable} - The entry observable. + * @memberof UmbRepositoryDetailsManager + */ + entryByUnique(unique: string): Observable { return this.#entries.asObservablePart((items) => items.find((item) => item.unique === unique)); } - async #requestNewItems(): Promise { + async #requestNewDetails(): Promise { await this.#init; if (!this.repository) throw new Error('Repository is not initialized'); @@ -151,15 +189,15 @@ export class UmbRepositoryDetailsManager }); newRequestedUniques.forEach((unique) => { - this.#requestItem(unique); + this.#requestDetails(unique); }); } - async #reloadItem(unique: string): Promise { - return await this.#requestItem(unique); + async #reloadDetails(unique: string): Promise { + return await this.#requestDetails(unique); } - async #requestItem(unique: string): Promise { + async #requestDetails(unique: string): Promise { await this.#init; if (!this.repository) throw new Error('Repository is not initialized'); @@ -219,14 +257,14 @@ export class UmbRepositoryDetailsManager #onEntityUpdatedEvent = (event: UmbEntityUpdatedEvent) => { const eventUnique = event.getUnique(); - const items = this.getItems(); + const items = this.getEntries(); if (items.length === 0) return; // Ignore events if the entity is not in the list of items. const item = items.find((item) => item.unique === eventUnique); if (!item) return; - this.#reloadItem(item.unique); + this.#reloadDetails(item.unique); }; override destroy(): void { From b011e573be5413776da4548e6c909f38dd99720f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 23 Apr 2025 13:25:26 +0200 Subject: [PATCH 18/23] do not await --- .../content-type-structure-manager.class.ts | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index e0e121135358..124dcbee23f0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -152,12 +152,20 @@ export class UmbContentTypeStructureManager< }); // Observe all Content Types compositions: [NL] - this.observe(this.contentTypeCompositions, (contentTypeCompositions) => { - this.#loadContentTypeCompositions(contentTypeCompositions); - }); - this.observe(this.#contentTypeContainers, (contentTypeContainers) => { - this.#containers.setValue(contentTypeContainers); - }); + this.observe( + this.contentTypeCompositions, + (contentTypeCompositions) => { + this.#loadContentTypeCompositions(contentTypeCompositions); + }, + null, + ); + this.observe( + this.#contentTypeContainers, + (contentTypeContainers) => { + this.#containers.setValue(contentTypeContainers); + }, + null, + ); } /** @@ -257,7 +265,6 @@ export class UmbContentTypeStructureManager< const compositionUniques = contentTypeCompositions?.map((x) => x.contentType.unique) ?? []; const newUniques = [ownerUnique, ...compositionUniques]; this.#contentTypes.filter((x) => newUniques.includes(x.unique)); - await Promise.resolve(); this.#repoManager?.setUniques(newUniques); } @@ -786,12 +793,12 @@ export class UmbContentTypeStructureManager< this.#init = new Promise((resolve) => { this.#initResolver = resolve; }); - this.#repoManager?.clear(); this.#contentTypes.setValue([]); this.#contentTypeObservers.forEach((observer) => observer.destroy()); this.#contentTypeObservers = []; this.#contentTypes.setValue([]); this.#containers.setValue([]); + this.#repoManager?.clear(); } public override destroy() { From 21b0faa3c47ed0c9345f05af21969d2320113593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 23 Apr 2025 13:29:49 +0200 Subject: [PATCH 19/23] fix race condition when switching document types fast --- .../structure/content-type-structure-manager.class.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 124dcbee23f0..aaa20a0626a2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -265,7 +265,7 @@ export class UmbContentTypeStructureManager< const compositionUniques = contentTypeCompositions?.map((x) => x.contentType.unique) ?? []; const newUniques = [ownerUnique, ...compositionUniques]; this.#contentTypes.filter((x) => newUniques.includes(x.unique)); - this.#repoManager?.setUniques(newUniques); + this.#repoManager!.setUniques(newUniques); } /** Public methods for consuming structure: */ @@ -793,12 +793,11 @@ export class UmbContentTypeStructureManager< this.#init = new Promise((resolve) => { this.#initResolver = resolve; }); - this.#contentTypes.setValue([]); this.#contentTypeObservers.forEach((observer) => observer.destroy()); this.#contentTypeObservers = []; - this.#contentTypes.setValue([]); this.#containers.setValue([]); this.#repoManager?.clear(); + this.#contentTypes.setValue([]); } public override destroy() { From e813ec2f2afd899b0ddac2af1d78ba752e02573f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 23 Apr 2025 13:32:19 +0200 Subject: [PATCH 20/23] remove test prop --- .../src/libs/observable-api/states/array-state.ts | 2 -- 1 file changed, 2 deletions(-) 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 c36a66fe4c49..3fdf1b10b055 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 @@ -42,8 +42,6 @@ export class UmbArrayState extends UmbDeepState { return this; } - debug = false; - /** * @function setValue * @param value From 6a62cbc1220573d47af98d6d173baf5898bd2b89 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 23 Apr 2025 13:38:31 +0200 Subject: [PATCH 21/23] Update manifests.ts --- .../entity-actions/create/default/manifests.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/default/manifests.ts index 72db1ed5b2a0..fe22736b14bc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/default/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/create/default/manifests.ts @@ -1,4 +1,8 @@ -import { UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE, UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE } from '../../../entity.js'; +import { + UMB_MEDIA_TYPE_ENTITY_TYPE, + UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE, + UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, +} from '../../../entity.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ @@ -8,7 +12,7 @@ export const manifests: Array = name: 'Default Media Type Entity Create Option Action', weight: 1200, api: () => import('./default-media-type-create-option-action.js'), - forEntityTypes: [UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE], + forEntityTypes: [UMB_MEDIA_TYPE_ROOT_ENTITY_TYPE, UMB_MEDIA_TYPE_ENTITY_TYPE, UMB_MEDIA_TYPE_FOLDER_ENTITY_TYPE], meta: { icon: 'icon-picture', label: '#content_mediatype', From d5a5e32e3a7dba3d8d327c5165b9288ad1459e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 23 Apr 2025 13:39:12 +0200 Subject: [PATCH 22/23] UmbMediaTypeWorkspaceContext Routes for inheritance --- .../workspace/media-type-workspace.context.ts | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) 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 3e4d2ad86312..38eb65be5f46 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace.context.ts @@ -33,7 +33,8 @@ export class UmbMediaTypeWorkspaceContext const parentEntityType = info.match.params.parentEntityType; const parentUnique = info.match.params.parentUnique === 'null' ? null : info.match.params.parentUnique; const parent: UmbEntityModel = { entityType: parentEntityType, unique: parentUnique }; - await this.createScaffold({ parent }); + + await this.#onScaffoldSetup(parent); new UmbWorkspaceIsNewRedirectController( this, @@ -83,8 +84,27 @@ export class UmbMediaTypeWorkspaceContext * @memberof UmbMediaTypeWorkspaceContext */ async create(parent: UmbEntityModel) { + console.warn('create() is deprecated. Use createScaffold() instead.'); this.createScaffold({ parent }); } + + async #onScaffoldSetup(parent: UmbEntityModel) { + let preset: Partial | undefined = undefined; + + if (parent.unique && parent.entityType === UMB_MEDIA_TYPE_ENTITY_TYPE) { + preset = { + ...preset, + compositions: [ + { + contentType: { unique: parent.unique }, + compositionType: CompositionTypeModel.INHERITANCE, + }, + ], + }; + } + + this.createScaffold({ parent, preset }); + } } export { UmbMediaTypeWorkspaceContext as api }; From a36cdf2d36b520c0531a6424de3360c5b20d0aeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 23 Apr 2025 13:41:02 +0200 Subject: [PATCH 23/23] import --- .../media/media-types/workspace/media-type-workspace.context.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 38eb65be5f46..b13b52dceab6 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 @@ -12,6 +12,7 @@ import type { UmbContentTypeSortModel, UmbContentTypeWorkspaceContext } from '@u import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { CompositionTypeModel } from '@umbraco-cms/backoffice/external/backend-api'; type DetailModelType = UmbMediaTypeDetailModel; export class UmbMediaTypeWorkspaceContext @@ -93,7 +94,6 @@ export class UmbMediaTypeWorkspaceContext if (parent.unique && parent.entityType === UMB_MEDIA_TYPE_ENTITY_TYPE) { preset = { - ...preset, compositions: [ { contentType: { unique: parent.unique },