From 30633fe7283878579e6c743ef86379ad5f267354 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 24 Mar 2025 12:02:50 +0100 Subject: [PATCH 01/19] Only validate for duplicate member email address when configured to do so (#18747) * Only validate for duplicate member email address when configured to do so. * Lookup member after creation by user name rather than email, as only the former is guaranteed to be unique. --- .../Services/MemberEditingService.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/MemberEditingService.cs b/src/Umbraco.Infrastructure/Services/MemberEditingService.cs index f7cd7060c3..4b6a50fae0 100644 --- a/src/Umbraco.Infrastructure/Services/MemberEditingService.cs +++ b/src/Umbraco.Infrastructure/Services/MemberEditingService.cs @@ -116,8 +116,8 @@ internal sealed class MemberEditingService : IMemberEditingService return IdentityMemberCreationFailed(createResult, status); } - IMember member = _memberService.GetByEmail(createModel.Email) - ?? throw new InvalidOperationException("Member creation succeeded, but member could not be found by email."); + IMember member = _memberService.GetByUsername(createModel.Username) + ?? throw new InvalidOperationException("Member creation succeeded, but member could not be found by username."); var updateRolesResult = await UpdateRoles(createModel.Roles, identityMember); if (updateRolesResult is false) @@ -283,10 +283,13 @@ internal sealed class MemberEditingService : IMemberEditingService return MemberEditingOperationStatus.DuplicateUsername; } - IMember? byEmail = _memberService.GetByEmail(model.Email); - if (byEmail is not null && byEmail.Key != memberKey) + if (_securitySettings.MemberRequireUniqueEmail) { - return MemberEditingOperationStatus.DuplicateEmail; + IMember? byEmail = _memberService.GetByEmail(model.Email); + if (byEmail is not null && byEmail.Key != memberKey) + { + return MemberEditingOperationStatus.DuplicateEmail; + } } return MemberEditingOperationStatus.Success; From 39cad5b2ea74daac6fb37a2e097e731f383af54f Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Mon, 24 Mar 2025 12:10:30 +0100 Subject: [PATCH 02/19] Add variancy information to reference response model (#18645) * Made variant info available on DocumentReferenceResponseModel * Fix scope issue * PR Feedback + correct scoping --- .../RelationTypePresentationFactory.cs | 71 ++++++++++++++++--- ...TrackedReferenceViewModelsMapDefinition.cs | 2 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 15 +++- .../DocumentReferenceResponseModel.cs | 6 +- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs index 3ce445a125..0c718bf663 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs @@ -1,7 +1,12 @@ -using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.Factories; @@ -9,21 +14,69 @@ namespace Umbraco.Cms.Api.Management.Factories; public class RelationTypePresentationFactory : IRelationTypePresentationFactory { private readonly IUmbracoMapper _umbracoMapper; + private readonly IEntityRepository _entityRepository; + private readonly IDocumentPresentationFactory _documentPresentationFactory; + private readonly IScopeProvider _scopeProvider; + [Obsolete("Please use the non obsoleted constructor. Scheduled for removal in v17")] public RelationTypePresentationFactory(IUmbracoMapper umbracoMapper) + : this( + umbracoMapper, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { - _umbracoMapper = umbracoMapper; } - public async Task> CreateReferenceResponseModelsAsync(IEnumerable relationItemModels) + public RelationTypePresentationFactory( + IUmbracoMapper umbracoMapper, + IEntityRepository entityRepository, + IDocumentPresentationFactory documentPresentationFactory, + IScopeProvider scopeProvider) { - IReferenceResponseModel[] result = relationItemModels.Select(relationItemModel => relationItemModel.NodeType switch - { - Constants.UdiEntityType.Document => _umbracoMapper.Map(relationItemModel), - Constants.UdiEntityType.Media => _umbracoMapper.Map(relationItemModel), - _ => _umbracoMapper.Map(relationItemModel) as IReferenceResponseModel, - }).WhereNotNull().ToArray(); + _umbracoMapper = umbracoMapper; + _entityRepository = entityRepository; + _documentPresentationFactory = documentPresentationFactory; + _scopeProvider = scopeProvider; + } + + public async Task> CreateReferenceResponseModelsAsync( + IEnumerable relationItemModels) + { + IReadOnlyCollection relationItemModelsCollection = relationItemModels.ToArray(); + + Guid[] documentKeys = relationItemModelsCollection + .Where(item => item.NodeType is Constants.UdiEntityType.Document) + .Select(item => item.NodeKey).Distinct().ToArray(); + + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + + var slimEntities = _entityRepository.GetAll(Constants.ObjectTypes.Document, documentKeys).ToList(); + + IReferenceResponseModel[] result = relationItemModelsCollection.Select(relationItemModel => + relationItemModel.NodeType switch + { + Constants.UdiEntityType.Document => MapDocumentReference(relationItemModel, slimEntities), + Constants.UdiEntityType.Media => _umbracoMapper.Map(relationItemModel), + _ => _umbracoMapper.Map(relationItemModel), + }).WhereNotNull().ToArray(); return await Task.FromResult(result); } + + private IReferenceResponseModel? MapDocumentReference(RelationItemModel relationItemModel, + List slimEntities) + { + DocumentReferenceResponseModel? documentReferenceResponseModel = + _umbracoMapper.Map(relationItemModel); + if (documentReferenceResponseModel is not null + && slimEntities.FirstOrDefault(e => e.Key == relationItemModel.NodeKey) is DocumentEntitySlim + matchingSlimDocument) + { + documentReferenceResponseModel.Variants = + _documentPresentationFactory.CreateVariantsItemResponseModels(matchingSlimDocument); + } + + return documentReferenceResponseModel; + } } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs index 6a4fe40c62..7a3874271c 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs @@ -16,7 +16,7 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition mapper.Define((source, context) => new ReferenceByIdModel(), Map); } - // Umbraco.Code.MapAll + // Umbraco.Code.MapAll -Variants private void Map(RelationItemModel source, DocumentReferenceResponseModel target, MapperContext context) { target.Id = source.NodeKey; diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index fd63df8194..ebecf3e9cb 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -37126,7 +37126,8 @@ "required": [ "$type", "documentType", - "id" + "id", + "variants" ], "type": "object", "properties": { @@ -37151,6 +37152,16 @@ "$ref": "#/components/schemas/TrackedReferenceDocumentTypeModel" } ] + }, + "variants": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/DocumentVariantItemResponseModel" + } + ] + } } }, "additionalProperties": false, @@ -46510,4 +46521,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentReferenceResponseModel.cs index 77e1df1d7f..e2bb767f99 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentReferenceResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentReferenceResponseModel.cs @@ -1,4 +1,6 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; +using Umbraco.Cms.Api.Management.ViewModels.Document; + +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; public class DocumentReferenceResponseModel : IReferenceResponseModel { @@ -9,4 +11,6 @@ public class DocumentReferenceResponseModel : IReferenceResponseModel public bool? Published { get; set; } public TrackedReferenceDocumentType DocumentType { get; set; } = new(); + + public IEnumerable Variants { get; set; } = Enumerable.Empty(); } From 63113c45522f22b7a2af5273ae561034394e23be Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 24 Mar 2025 14:17:48 +0100 Subject: [PATCH 03/19] V15: New dropzone component available for the Backoffice (#18753) * create a symlink between local Client .vscode snippets and global snippets for ease of use * fix: no need to specify `Element` in the snippet as that is pulled from the filename Because of our convention with `x.element.ts` you would have ended up with `UmbXElementElement` * feat: adds new component `umb-input-dropzone` * docs(storybook): more stories * feat: construct the temporary files centrally along with an `AbortController` and use its signal * feat: makes UmbInputDropzone form aware * feat: introduces a change event * chore: temporary changes before changing upload field * feat: adds default slot * docs: adds jsdocs * feat: adds more properties * feat: adds dashed styling * feat: adds multiple support * feat: allows to cancel file * feat: separate **cancel** and **remove** * fix stylibg * move dropzone element * move input-dropzone into dropzone package * feat: introduces a 'dropzone' package * import for backward compatibility * remove ambigious export * reexport everything from dropzone * fix import * cleanup test files * use correct import paths * test: make sure folder exists before writing to it * adds export for modals * adds entrypoint for dropzone package * use the AbortController directly on the temporary file object * uses correct icon name * feat: adds ability to remove all files and cancel the request * feat: adds styling for the uploader and enables it to work in multiple mode with classes over id's * do not let the content exceed its boundaries * feat: formats progress with 2 decimals * feat: formats with 0 decimals * fix: returns cancel error * fix: maps cancel errors back to the uploadable item * fix: do not proceed with media items if the request was cancelled * chore: mark exports from media <- dropzone as deprecated * fix: use correct attribute and remove a todo with localizations * fix: use correct attribute and remove a todo with localizations * fix: allow to specify parent through attribute * feat: align attribute `disableFolderUpload` between dropzone components --- .vscode/lit.code-snippets | 1 + .../.vscode/lit.code-snippets | 6 +- .../devops/generate-check-const-test/index.js | 10 +- src/Umbraco.Web.UI.Client/package.json | 1 + .../core/resources/resource.controller.ts | 2 +- .../temporary-file-manager.class.ts | 15 +- .../src/packages/core/temporary-file/types.ts | 9 + .../document-type-import-modal.element.ts | 15 +- .../components}/dropzone.element.ts | 18 +- .../media/dropzone/components/index.ts | 2 + .../input-dropzone/input-dropzone.element.ts | 306 ++++++++++ .../input-dropzone/input-dropzone.stories.ts | 55 ++ .../src/packages/media/dropzone/constants.ts | 9 + .../media/dropzone/dropzone-change.event.ts | 15 + .../dropzone/dropzone-manager.class.ts | 74 ++- .../dropzone/dropzone-submitted.event.ts | 0 .../src/packages/media/dropzone/index.ts | 7 + .../src/packages/media/dropzone/manifests.ts | 3 + ...ropzone-media-type-picker-modal.element.ts | 0 .../dropzone-media-type-picker-modal.token.ts | 0 .../dropzone-media-type-picker/index.ts | 0 .../{media => }/dropzone/modals/index.ts | 0 .../{media => }/dropzone/modals/manifests.ts | 0 .../media/{media => }/dropzone/types.ts | 9 +- .../src/packages/media/manifests.ts | 2 + .../modal/media-type-import-modal.element.ts | 14 +- .../collection/media-collection.context.ts | 2 +- .../collection/media-collection.element.ts | 3 +- .../packages/media/media/collection/types.ts | 2 +- .../media-grid-collection-view.element.ts | 2 +- .../input-rich-media.element.ts | 2 +- .../input-upload-field.element.ts | 31 +- .../src/packages/media/media/constants.ts | 2 - .../media/media/dropzone/constants.ts | 1 - .../packages/media/media/dropzone/index.ts | 3 - .../media/media/dropzone/manifests.ts | 1 - .../src/packages/media/media/index.ts | 6 +- .../src/packages/media/media/manifests.ts | 2 - .../media-picker-modal.element.ts | 2 +- .../src/packages/media/media/types.ts | 1 - .../src/packages/media/vite.config.ts | 1 + src/Umbraco.Web.UI.Client/tsconfig.json | 1 + .../utils/all-umb-consts/imports.ts | 530 ------------------ 43 files changed, 546 insertions(+), 619 deletions(-) create mode 120000 .vscode/lit.code-snippets rename src/Umbraco.Web.UI.Client/src/packages/media/{media/dropzone => dropzone/components}/dropzone.element.ts (92%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.stories.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/dropzone/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-change.event.ts rename src/Umbraco.Web.UI.Client/src/packages/media/{media => }/dropzone/dropzone-manager.class.ts (86%) rename src/Umbraco.Web.UI.Client/src/packages/media/{media => }/dropzone/dropzone-submitted.event.ts (100%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/dropzone/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/dropzone/manifests.ts rename src/Umbraco.Web.UI.Client/src/packages/media/{media => }/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/{media => }/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/{media => }/dropzone/modals/dropzone-media-type-picker/index.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/{media => }/dropzone/modals/index.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/{media => }/dropzone/modals/manifests.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/{media => }/dropzone/types.ts (87%) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/constants.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts delete mode 100644 src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts diff --git a/.vscode/lit.code-snippets b/.vscode/lit.code-snippets new file mode 120000 index 0000000000..aa55c3049d --- /dev/null +++ b/.vscode/lit.code-snippets @@ -0,0 +1 @@ +../src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets b/src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets index 4920d8011b..d6e2e5ea16 100644 --- a/src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets +++ b/src/Umbraco.Web.UI.Client/.vscode/lit.code-snippets @@ -8,7 +8,7 @@ "import { UmbTextStyles } from '@umbraco-cms/backoffice/style';", "", "@customElement('umb-${TM_FILENAME_BASE/(.*)\\..+$/$1/}')", - "export class Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/}Element extends UmbLitElement {", + "export class Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/} extends UmbLitElement {", "\toverride render() {", "\t\treturn html`$0`;", "\t}", @@ -16,11 +16,11 @@ "\tstatic override readonly styles = [UmbTextStyles, css``];", "}", "", - "export { Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/}Element as element };", + "export { Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/} as element };", "", "declare global {", "\tinterface HTMLElementTagNameMap {", - "\t\t'umb-${TM_FILENAME_BASE/(.*)\\..+$/$1/}': Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/}Element;", + "\t\t'umb-${TM_FILENAME_BASE/(.*)\\..+$/$1/}': Umb${TM_FILENAME_BASE/(.*)$/${1:/pascalcase}/};", "\t}", "}", "", diff --git a/src/Umbraco.Web.UI.Client/devops/generate-check-const-test/index.js b/src/Umbraco.Web.UI.Client/devops/generate-check-const-test/index.js index 56757ff716..f78147612e 100644 --- a/src/Umbraco.Web.UI.Client/devops/generate-check-const-test/index.js +++ b/src/Umbraco.Web.UI.Client/devops/generate-check-const-test/index.js @@ -91,8 +91,9 @@ export async function findUmbConstExports() { const content = `export const foundConsts = [${foundConsts.join(',\n')}];`; - const outputPath = path.join(projectRoot, './utils/all-umb-consts/index.ts'); - fs.writeFileSync(outputPath, content); + const outputPath = path.join(projectRoot, './utils/all-umb-consts'); + fs.mkdirSync(outputPath, { recursive: true }); + fs.writeFileSync(path.join(outputPath, 'index.ts'), content, {}); generatetestImportFile(projectRoot); @@ -149,8 +150,9 @@ function generatetestImportFile(projectRoot) { ]; ` - const outputPath = path.join(projectRoot, './utils/all-umb-consts/imports.ts'); - fs.writeFileSync(outputPath, content); + const outputPath = path.join(projectRoot, './utils/all-umb-consts'); + fs.mkdirSync(outputPath, { recursive: true }); + fs.writeFileSync(path.join(outputPath, 'imports.ts'), content); } diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 2898815f08..73b930d3ae 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -41,6 +41,7 @@ "./document-blueprint": "./dist-cms/packages/documents/document-blueprints/index.js", "./document-type": "./dist-cms/packages/documents/document-types/index.js", "./document": "./dist-cms/packages/documents/documents/index.js", + "./dropzone": "./dist-cms/packages/media/dropzone/index.js", "./entity-action": "./dist-cms/packages/core/entity-action/index.js", "./entity-bulk-action": "./dist-cms/packages/core/entity-bulk-action/index.js", "./entity-create-option-action": "./dist-cms/packages/core/entity-create-option-action/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts index d96a843293..f37ce13fdf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/resources/resource.controller.ts @@ -71,7 +71,7 @@ export class UmbResourceController extends UmbControllerBase { */ if (isCancelError(error)) { // Cancelled - do nothing - return {}; + return { error }; } else { console.groupCollapsed('ApiError caught in UmbResourceController'); console.error('Request failed', error.request); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts index bc6bf5f340..0f4a8ab972 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/temporary-file-manager.class.ts @@ -11,6 +11,7 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; import { formatBytes } from '@umbraco-cms/backoffice/utils'; +import { isCancelError } from '@umbraco-cms/backoffice/resources'; export class UmbTemporaryFileManager< UploadableItem extends UmbTemporaryFileModel = UmbTemporaryFileModel, @@ -62,6 +63,10 @@ export class UmbTemporaryFileManager< this.#queue.remove(uniques); } + removeAll() { + this.#queue.setValue([]); + } + async #handleQueue(options?: UmbUploadOptions): Promise> { const filesCompleted: Array = []; const queue = this.#queue.getValue(); @@ -152,9 +157,15 @@ export class UmbTemporaryFileManager< // Update progress in percent if a callback is provided if (item.onProgress) item.onProgress((evt.loaded / evt.total) * 100); }, - item.abortSignal, + item.abortController?.signal ?? item.abortSignal, ); - const status = error ? TemporaryFileStatus.ERROR : TemporaryFileStatus.SUCCESS; + let status = TemporaryFileStatus.SUCCESS; + if (error) { + status = TemporaryFileStatus.ERROR; + if (isCancelError(error)) { + status = TemporaryFileStatus.CANCELLED; + } + } this.#queue.updateOne(item.temporaryUnique, { ...item, status }); return { ...item, status }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts index b471f033f0..821a770766 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/temporary-file/types.ts @@ -4,6 +4,7 @@ export enum TemporaryFileStatus { SUCCESS = 'success', WAITING = 'waiting', ERROR = 'error', + CANCELLED = 'cancelled', } export interface UmbTemporaryFileModel { @@ -11,7 +12,15 @@ export interface UmbTemporaryFileModel { temporaryUnique: string; status?: TemporaryFileStatus; onProgress?: (progress: number) => void; + /** + * The abort signal used to cancel the upload. + * @deprecated Use {@link abortController} instead. + */ abortSignal?: AbortSignal; + /** + * The abort controller used to cancel the upload. + */ + abortController?: AbortController; } export type UmbQueueHandlerCallback = (item: TItem) => Promise; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts index 7bd699489b..97758c9fc8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts @@ -6,7 +6,7 @@ import type { import { css, html, customElement, query, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import type { UmbDropzoneElement } from '@umbraco-cms/backoffice/media'; +import type { UmbDropzoneElement } from '@umbraco-cms/backoffice/dropzone'; interface UmbDocumentTypePreview { unique: string; @@ -134,13 +134,18 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement< () => /**TODO Add localizations */ html`
- Drag and drop your file here - + Drag and drop your file(s) into the area + + + create-as-temporary + @complete=${this.#onUploadComplete}>
`, )} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/dropzone.element.ts similarity index 92% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/dropzone.element.ts index f90199879d..e12a41488b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/dropzone.element.ts @@ -1,16 +1,17 @@ -import { UmbDropzoneManager } from './dropzone-manager.class.js'; -import { UmbDropzoneSubmittedEvent } from './dropzone-submitted.event.js'; -import { UmbFileDropzoneItemStatus, type UmbUploadableItem } from './types.js'; +import { UmbDropzoneManager } from '../dropzone-manager.class.js'; +import { UmbDropzoneSubmittedEvent } from '../dropzone-submitted.event.js'; +import type { UmbUploadableItem } from '../types.js'; +import { UmbFileDropzoneItemStatus } from '../constants.js'; import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-dropzone') export class UmbDropzoneElement extends UmbLitElement { - @property({ attribute: false }) + @property({ attribute: 'parent-unique' }) parentUnique: string | null = null; - @property({ type: Boolean }) + @property({ type: Boolean, attribute: 'create-as-temporary' }) createAsTemporary: boolean = false; @property({ type: String }) @@ -23,12 +24,12 @@ export class UmbDropzoneElement extends UmbLitElement { disabled = false; @property({ type: Boolean, attribute: 'disable-folder-upload', reflect: true }) - public get disableFolderUpload() { - return this._disableFolderUpload; - } public set disableFolderUpload(isAllowed: boolean) { this.#dropzoneManager.setIsFoldersAllowed(!isAllowed); } + public get disableFolderUpload() { + return this._disableFolderUpload; + } private readonly _disableFolderUpload = false; @state() @@ -130,6 +131,7 @@ export class UmbDropzoneElement extends UmbLitElement { id="dropzone" accept=${ifDefined(this.accept)} ?multiple=${this.multiple} + ?disallowFolderUpload=${this.disableFolderUpload} @change=${this.#onDropFiles} label=${this.localize.term('media_dragAndDropYourFilesIntoTheArea')}>`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/index.ts new file mode 100644 index 0000000000..008143e170 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/index.ts @@ -0,0 +1,2 @@ +export * from './input-dropzone/input-dropzone.element.js'; +export * from './dropzone.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts new file mode 100644 index 0000000000..c8de1fe129 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts @@ -0,0 +1,306 @@ +import { UmbDropzoneChangeEvent, UmbDropzoneManager, UmbDropzoneSubmittedEvent } from '../../index.js'; +import type { UmbUploadableItem } from '../../types.js'; +import { UmbFileDropzoneItemStatus } from '../../constants.js'; +import { + css, + customElement, + html, + ifDefined, + nothing, + property, + query, + repeat, + state, + when, +} from '@umbraco-cms/backoffice/external/lit'; +import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { formatBytes } from '@umbraco-cms/backoffice/utils'; +import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; + +/** + * @element umb-input-dropzone + * @fires ProgressEvent When the progress of the upload changes. + * @fires UmbDropzoneChangeEvent When the upload is complete. + * @fires UmbDropzoneSubmittedEvent When the upload is submitted. + * @slot - The default slot. + */ +@customElement('umb-input-dropzone') +export class UmbInputDropzoneElement extends UmbFormControlMixin( + UmbLitElement, +) { + /** + * Comma-separated list of accepted mime types or file extensions. + */ + @property({ type: String }) + accept?: string; + + /** + * Determines if the dropzone should create temporary files or media items directly. + */ + @property({ type: Boolean, attribute: 'create-as-temporary' }) + createAsTemporary: boolean = false; + + /** + * Disable folder uploads. + */ + @property({ type: Boolean, attribute: 'disable-folder-upload', reflect: true }) + public set disableFolderUpload(isAllowed: boolean) { + this.#manager.setIsFoldersAllowed(!isAllowed); + } + public get disableFolderUpload() { + return this._disableFolderUpload; + } + private readonly _disableFolderUpload = false; + + /** + * Create the media item below this parent. + * @description This is only used when `createAsTemporary` is `false`. + */ + @property({ type: String, attribute: 'parent-unique' }) + parentUnique: string | null = null; + + /** + * Disables the dropzone. + * @description The dropzone will not accept any uploads. + */ + @property({ type: Boolean, reflect: true }) + disabled: boolean = false; + + /** + * Determines if the dropzone should accept multiple files. + */ + @property({ type: Boolean }) + multiple: boolean = false; + + /** + * The label for the dropzone. + */ + @property({ type: String }) + label = 'dropzone'; + + @query('#dropzone', true) + private _dropzone?: UUIFileDropzoneElement; + + @state() + private _progressItems?: Array; + + #manager = new UmbDropzoneManager(this); + + constructor() { + super(); + + this.observe( + this.#manager.progress, + (progress) => + this.dispatchEvent(new ProgressEvent('progress', { loaded: progress.completed, total: progress.total })), + '_observeProgress', + ); + + this.observe( + this.#manager.progressItems, + (progressItems) => { + this._progressItems = [...progressItems]; + const waiting = this._progressItems.find((item) => item.status === UmbFileDropzoneItemStatus.WAITING); + if (this._progressItems.length && !waiting) { + this.value = [...this._progressItems]; + this.dispatchEvent(new UmbDropzoneChangeEvent(this._progressItems)); + } + }, + '_observeProgressItems', + ); + } + + override render() { + return html` + + + + + + ${this.#renderUploader()} + `; + } + + #renderUploader() { + if (this.disabled) return nothing; + if (!this._progressItems?.length) return nothing; + + return html` +
+ ${repeat( + this._progressItems, + (item) => item.unique, + (item) => this.#renderPlaceholder(item), + )} + + ${this.localize.term('content_uploadClear')} + +
+ `; + } + + #renderPlaceholder(item: UmbUploadableItem) { + const file = item.temporaryFile?.file; + return html` +
+
+ ${when( + item.status === UmbFileDropzoneItemStatus.COMPLETE, + () => html``, + )} + ${when( + item.status === UmbFileDropzoneItemStatus.ERROR || + item.status === UmbFileDropzoneItemStatus.CANCELLED || + item.status === UmbFileDropzoneItemStatus.NOT_ALLOWED, + () => html``, + )} +
+
+
${file?.name ?? ''}
+
+ ${formatBytes(file?.size ?? 0, { decimals: 2 })}: + ${this.localize.number(item.progress, { maximumFractionDigits: 0 })}% +
+ ${when( + item.status === UmbFileDropzoneItemStatus.WAITING, + () => html`
`, + )} + ${when( + item.status === UmbFileDropzoneItemStatus.ERROR, + () => html`
An error occured
`, + )} + ${when(item.status === UmbFileDropzoneItemStatus.CANCELLED, () => html`
Cancelled
`)} + ${when( + item.status === UmbFileDropzoneItemStatus.NOT_ALLOWED, + () => html`
File type not allowed
`, + )} +
+
+ ${when( + item.status === UmbFileDropzoneItemStatus.WAITING, + () => html` + this.#handleCancel(item)} + label=${this.localize.term('general_cancel')}> + ${this.localize.term('general_cancel')} + + `, + )} +
+
+ `; + } + + #handleBrowse(e: Event) { + if (!this._dropzone) return; + e.stopImmediatePropagation(); + this._dropzone.browse(); + } + + #handleCancel(item: UmbUploadableItem) { + item.temporaryFile?.abortController?.abort(); + } + + #handleRemove() { + this.#manager.removeAll(); + } + + async #onUpload(e: UUIFileDropzoneEvent) { + e.stopImmediatePropagation(); + + if (this.disabled) return; + if (!e.detail.files.length && !e.detail.folders.length) return; + + if (this.createAsTemporary) { + const uploadables = this.#manager.createTemporaryFiles(e.detail.files); + this.dispatchEvent(new UmbDropzoneSubmittedEvent(await uploadables)); + } else { + const uploadables = this.#manager.createMediaItems(e.detail, null); + this.dispatchEvent(new UmbDropzoneSubmittedEvent(uploadables)); + } + } + + static override readonly styles = [ + UmbTextStyles, + css` + :host([disabled]) #dropzone { + opacity: 0.5; + pointer-events: none; + } + + #dropzone { + inset: 0; + backdrop-filter: opacity(1); /* Removes the built in blur effect */ + overflow: clip; + } + + #uploader { + display: flex; + flex-direction: column; + flex-wrap: wrap; + align-items: center; + gap: var(--uui-size-space-3); + + .placeholder { + display: grid; + grid-template-columns: 30px 200px 1fr; + max-width: fit-content; + padding: var(--uui-size-space-3); + border: 1px dashed var(--uui-color-divider-emphasis); + } + + .fileIcon, + .fileActions { + place-self: center center; + } + + .fileName { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: var(--uui-size-5); + } + + .fileSize { + font-size: var(--uui-font-size-small); + color: var(--uui-color-text-alt); + } + + .error { + color: var(--uui-color-danger); + } + } + `, + ]; +} + +export const UmbInputDropzoneDashedStyles = css` + umb-input-dropzone { + position: relative; + display: block; + inset: 0; + cursor: pointer; + border: 1px dashed var(--uui-color-divider-emphasis); + } +`; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-dropzone': UmbInputDropzoneElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.stories.ts new file mode 100644 index 0000000000..346097c6ff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.stories.ts @@ -0,0 +1,55 @@ +import type { UmbInputDropzoneElement } from './input-dropzone.element.js'; +import type { Meta, StoryObj } from '@storybook/web-components'; +import { html } from '@umbraco-cms/backoffice/external/lit'; + +import './input-dropzone.element.js'; + +const meta: Meta = { + id: 'umb-input-dropzone', + title: 'Components/Inputs/Dropzone', + component: 'umb-input-dropzone', + args: { + disabled: false, + accept: '', + createAsTemporary: true, + }, + decorators: [(Story) => html`
${Story()}
`], + parameters: { + layout: 'centered', + actions: { + handles: ['submitted', 'change'], + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Overview: Story = {}; + +export const WithDisabled: Story = { + args: { + disabled: true, + }, +}; + +export const WithAccept: Story = { + args: { + accept: 'jpg,png', + }, + parameters: { + docs: { + description: { + story: 'This is a dropzone with an accept attribute set to "jpg,png".', + }, + }, + }, +}; + +export const WithDefaultSlot: Story = { + render: () => + html` +
Custom slot
+
`, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/constants.ts new file mode 100644 index 0000000000..df1c3a9d8b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/constants.ts @@ -0,0 +1,9 @@ +export { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js'; + +export enum UmbFileDropzoneItemStatus { + WAITING = 'waiting', + COMPLETE = 'complete', + NOT_ALLOWED = 'not allowed', + CANCELLED = 'cancelled', + ERROR = 'error', +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-change.event.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-change.event.ts new file mode 100644 index 0000000000..1ba53bf4c4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-change.event.ts @@ -0,0 +1,15 @@ +import type { UmbUploadableItem } from './types.js'; + +export class UmbDropzoneChangeEvent extends Event { + public static readonly TYPE = 'change'; + + /** + * An array of resolved uploadable items. + */ + public items; + + public constructor(items: Array, args?: EventInit) { + super(UmbDropzoneChangeEvent.TYPE, { bubbles: false, composed: false, cancelable: false, ...args }); + this.items = items; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts similarity index 86% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts index b58069b537..1a01cf86ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts @@ -1,6 +1,6 @@ -import { UmbMediaDetailRepository } from '../repository/index.js'; -import type { UmbMediaDetailModel, UmbMediaValueModel } from '../types.js'; -import { UmbFileDropzoneItemStatus } from './types.js'; +import { UmbMediaDetailRepository } from '../media/repository/index.js'; +import type { UmbMediaDetailModel, UmbMediaValueModel } from '../media/types.js'; +import { UmbFileDropzoneItemStatus } from './constants.js'; import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/index.js'; import type { UmbUploadableFile, @@ -11,7 +11,11 @@ import type { UmbAllowedMediaTypesOfExtension, UmbAllowedChildrenOfMediaType, } from './types.js'; -import { TemporaryFileStatus, UmbTemporaryFileManager } from '@umbraco-cms/backoffice/temporary-file'; +import { + TemporaryFileStatus, + UmbTemporaryFileManager, + type UmbTemporaryFileModel, +} from '@umbraco-cms/backoffice/temporary-file'; import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; @@ -111,11 +115,7 @@ export class UmbDropzoneManager extends UmbControllerBase { for (const item of uploadableItems) { // Upload as temp file - const uploaded = await this.#tempFileManager.uploadOne({ - temporaryUnique: item.temporaryFile.temporaryUnique, - file: item.temporaryFile.file, - onProgress: (progress) => this.#updateProgress(item, progress), - }); + const uploaded = await this.#tempFileManager.uploadOne(item.temporaryFile); // Update progress if (uploaded.status === TemporaryFileStatus.SUCCESS) { @@ -131,6 +131,35 @@ export class UmbDropzoneManager extends UmbControllerBase { return uploadedItems; } + public removeOne(item: UmbUploadableItem) { + item.temporaryFile?.abortController?.abort(); + this.#progressItems.removeOne(item.unique); + if (item.temporaryFile) { + this.#tempFileManager.removeOne(item.temporaryFile.temporaryUnique); + } + } + + public remove(items: Array) { + const uniques: string[] = []; + for (const item of items) { + item.temporaryFile?.abortController?.abort(); + if (item.temporaryFile) { + uniques.push(item.temporaryFile.temporaryUnique); + } + } + this.#progressItems.remove(uniques); + const temporaryUniques = items.map((x) => x.temporaryFile?.temporaryUnique).filter((x): x is string => !!x); + this.#tempFileManager.remove(temporaryUniques); + } + + public removeAll() { + for (const item of this.#progressItems.getValue()) { + item.temporaryFile?.abortController?.abort(); + } + this.#progressItems.setValue([]); + this.#tempFileManager.removeAll(); + } + async #showDialogMediaTypePicker(options: Array) { const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); const modalContext = modalManager.open(this.#host, UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL, { data: { options } }); @@ -188,6 +217,10 @@ export class UmbDropzoneManager extends UmbControllerBase { async #handleFile(item: UmbUploadableFile, mediaTypeUnique: string) { // Upload the file as a temporary file and update progress. const temporaryFile = await this.#uploadAsTemporaryFile(item); + if (temporaryFile.status === TemporaryFileStatus.CANCELLED) { + this.#updateStatus(item, UmbFileDropzoneItemStatus.CANCELLED); + return; + } if (temporaryFile.status !== TemporaryFileStatus.SUCCESS) { this.#updateStatus(item, UmbFileDropzoneItemStatus.ERROR); return; @@ -215,11 +248,7 @@ export class UmbDropzoneManager extends UmbControllerBase { } #uploadAsTemporaryFile(item: UmbUploadableFile) { - return this.#tempFileManager.uploadOne({ - temporaryUnique: item.temporaryFile.temporaryUnique, - file: item.temporaryFile.file, - onProgress: (progress) => this.#updateProgress(item, progress), - }); + return this.#tempFileManager.uploadOne(item.temporaryFile); } // Media types @@ -322,13 +351,26 @@ export class UmbDropzoneManager extends UmbControllerBase { const items: Array = []; for (const file of files) { - items.push({ + const temporaryFile: UmbTemporaryFileModel = { + file, + temporaryUnique: UmbId.new(), + abortController: new AbortController(), + onProgress: (progress) => this.#updateProgress(uploadableItem, progress), + }; + + const uploadableItem: UmbUploadableFile = { unique: UmbId.new(), parentUnique, status: UmbFileDropzoneItemStatus.WAITING, progress: 0, - temporaryFile: { file, temporaryUnique: UmbId.new() }, + temporaryFile, + }; + + temporaryFile.abortController?.signal.addEventListener('abort', () => { + this.#updateStatus(uploadableItem, UmbFileDropzoneItemStatus.CANCELLED); }); + + items.push(uploadableItem); } for (const subfolder of folders) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-submitted.event.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-submitted.event.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-submitted.event.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-submitted.event.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/index.ts new file mode 100644 index 0000000000..9a15a46dc6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/index.ts @@ -0,0 +1,7 @@ +export * from './constants.js'; +export * from './components/index.js'; +export * from './modals/index.js'; +export * from './dropzone-manager.class.js'; +export * from './dropzone-submitted.event.js'; +export * from './dropzone-change.event.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/manifests.ts new file mode 100644 index 0000000000..6e046c0210 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/manifests.ts @@ -0,0 +1,3 @@ +import { manifests as modalManifests } from './modals/manifests.js'; + +export const manifests: Array = [...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/dropzone-media-type-picker/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/dropzone-media-type-picker/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/dropzone-media-type-picker/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/modals/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/modals/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/types.ts similarity index 87% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/dropzone/types.ts index f1b90a32e8..24ed77b094 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/types.ts @@ -1,3 +1,4 @@ +import type { UmbFileDropzoneItemStatus } from './constants.js'; import type { UUIFileFolder } from '@umbraco-cms/backoffice/external/uui'; import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-type'; import type { UmbTemporaryFileModel } from '@umbraco-cms/backoffice/temporary-file'; @@ -38,11 +39,3 @@ export interface UmbFileDropzoneProgress { total: number; completed: number; } - -export enum UmbFileDropzoneItemStatus { - WAITING = 'waiting', - COMPLETE = 'complete', - NOT_ALLOWED = 'not allowed', - CANCELLED = 'cancelled', - ERROR = 'error', -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/manifests.ts index bfb213f1b4..b4f6bc4c69 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/manifests.ts @@ -2,6 +2,7 @@ import { manifests as mediaManifests } from './media/manifests.js'; import { manifests as mediaSectionManifests } from './media-section/manifests.js'; import { manifests as mediaTypesManifests } from './media-types/manifests.js'; import { manifests as imagingManifests } from './imaging/manifests.js'; +import { manifests as dropzoneManifests } from './dropzone/manifests.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ @@ -9,4 +10,5 @@ export const manifests: Array = ...mediaManifests, ...mediaTypesManifests, ...imagingManifests, + ...dropzoneManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts index 06973dd3a2..6bf7494ea3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts @@ -125,15 +125,19 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< label=${this.localize.term('general_remove')}> `, () => - /**TODO Add localizations */ html`
- Drag and drop your file here - + Drag and drop your file(s) into the area + + + create-as-temporary + @complete=${this.#onUploadCompleted}>
`, )} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts index 8353b3579e..486dada1ec 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.context.ts @@ -1,7 +1,7 @@ import { UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE } from '../entity.js'; -import type { UmbFileDropzoneItemStatus } from '../dropzone/types.js'; import { UMB_MEDIA_GRID_COLLECTION_VIEW_ALIAS } from './views/constants.js'; import type { UmbMediaCollectionFilterModel, UmbMediaCollectionItemModel } from './types.js'; +import type { UmbFileDropzoneItemStatus } from '@umbraco-cms/backoffice/dropzone'; import { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts index e5deb42fa9..ffc8e3fbcd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts @@ -1,12 +1,11 @@ import { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE } from '../entity.js'; import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../workspace/media-workspace.context-token.js'; -import type { UmbDropzoneSubmittedEvent } from '../dropzone/dropzone-submitted.event.js'; -import type { UmbDropzoneElement } from '../dropzone/dropzone.element.js'; import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js'; import { customElement, html, ref, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import type { UmbDropzoneElement, UmbDropzoneSubmittedEvent } from '@umbraco-cms/backoffice/dropzone'; @customElement('umb-media-collection') export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts index 78d0b0a566..06712e08d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/types.ts @@ -1,4 +1,4 @@ -import type { UmbFileDropzoneItemStatus } from '../dropzone/types.js'; +import type { UmbFileDropzoneItemStatus } from '@umbraco-cms/backoffice/dropzone'; import type { UmbCollectionFilterModel } from '@umbraco-cms/backoffice/collection'; export interface UmbMediaCollectionFilterModel extends UmbCollectionFilterModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts index 3a5e187522..ac9709081d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/views/grid/media-grid-collection-view.element.ts @@ -2,11 +2,11 @@ import { UMB_EDIT_MEDIA_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; import type { UmbMediaCollectionItemModel } from '../../types.js'; import type { UmbMediaCollectionContext } from '../../media-collection.context.js'; import { UMB_MEDIA_COLLECTION_CONTEXT } from '../../media-collection.context-token.js'; -import { UmbFileDropzoneItemStatus } from '../../../dropzone/types.js'; import { UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE } from '../../../entity.js'; import { css, customElement, html, ifDefined, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbFileDropzoneItemStatus } from '@umbraco-cms/backoffice/dropzone'; import '@umbraco-cms/backoffice/imaging'; import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts index d65a643e28..848bde04eb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts @@ -1,6 +1,6 @@ import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js'; import type { UmbMediaItemModel, UmbCropModel, UmbMediaPickerPropertyValueEntry } from '../../types.js'; -import type { UmbUploadableItem } from '../../dropzone/types.js'; +import type { UmbUploadableItem } from '@umbraco-cms/backoffice/dropzone'; import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { umbConfirmModal, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/input-upload-field.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/input-upload-field.element.ts index 50c283acec..7c7cd25841 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/input-upload-field.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-upload-field/input-upload-field.element.ts @@ -69,8 +69,6 @@ export class UmbInputUploadFieldElement extends UmbLitElement { #manifests: Array = []; - #uploadAbort?: AbortController; - override updated(changedProperties: PropertyValueMap | Map) { super.updated(changedProperties); @@ -154,22 +152,19 @@ export class UmbInputUploadFieldElement extends UmbLitElement { } async #onUpload(e: UUIFileDropzoneEvent) { - //Property Editor for Upload field will always only have one file. - this.temporaryFile = { - temporaryUnique: UmbId.new(), - status: TemporaryFileStatus.WAITING, - file: e.detail.files[0], - }; - try { - this.#uploadAbort = new AbortController(); - const uploaded = await this.#manager.uploadOne({ - ...this.temporaryFile, + //Property Editor for Upload field will always only have one file. + this.temporaryFile = { + temporaryUnique: UmbId.new(), + status: TemporaryFileStatus.WAITING, + file: e.detail.files[0], onProgress: (p) => { this._progress = Math.ceil(p); }, - abortSignal: this.#uploadAbort.signal, - }); + abortController: new AbortController(), + }; + + const uploaded = await this.#manager.uploadOne(this.temporaryFile); if (uploaded.status === TemporaryFileStatus.SUCCESS) { this.temporaryFile.status = TemporaryFileStatus.SUCCESS; @@ -190,8 +185,6 @@ export class UmbInputUploadFieldElement extends UmbLitElement { } // If the error was caused by the upload being aborted, do not show an error message. - } finally { - this.#uploadAbort = undefined; } } @@ -292,13 +285,13 @@ export class UmbInputUploadFieldElement extends UmbLitElement { } #handleRemove() { + // If the upload promise happens to be in progress, cancel it. + this.temporaryFile?.abortController?.abort(); + this.value = { src: undefined }; this.temporaryFile = undefined; this._progress = 0; this.dispatchEvent(new UmbChangeEvent()); - - // If the upload promise happens to be in progress, cancel it. - this.#uploadAbort?.abort(); } static override readonly styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts index 5b8f281fe2..7c28975d0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts @@ -1,5 +1,4 @@ export * from './collection/constants.js'; -export * from './dropzone/constants.js'; export * from './entity-actions/constants.js'; export * from './entity-bulk-actions/constants.js'; export * from './menu/constants.js'; @@ -11,7 +10,6 @@ export * from './search/constants.js'; export * from './tree/constants.js'; export * from './url/constants.js'; export * from './workspace/constants.js'; - export * from './paths.js'; export { UMB_MEDIA_VARIANT_CONTEXT } from './property-dataset-context/media-property-dataset-context.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/constants.ts deleted file mode 100644 index 4f9dd69361..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/dropzone-media-type-picker/dropzone-media-type-picker-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts deleted file mode 100644 index c965489395..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './dropzone.element.js'; -export * from './dropzone-manager.class.js'; -export * from './dropzone-submitted.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts deleted file mode 100644 index c777b79000..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/manifests.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './modals/manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts index f9957ebb0d..9b7f13cc62 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts @@ -1,6 +1,5 @@ export * from './components/index.js'; export * from './constants.js'; -export * from './dropzone/index.js'; export * from './reference/index.js'; export * from './repository/index.js'; export * from './search/index.js'; @@ -10,3 +9,8 @@ export * from './utils/index.js'; export { UmbMediaAuditLogRepository } from './audit-log/index.js'; export type * from './types.js'; + +/** + * @deprecated Please import directly from the `@umbraco-cms/backoffice/dropzone` package instead. This package will be removed in Umbraco 18. + */ +export * from '@umbraco-cms/backoffice/dropzone'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts index 69738473e5..45d54199b1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/manifests.ts @@ -1,6 +1,5 @@ import { manifests as auditLogManifests } from './audit-log/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; -import { manifests as dropzoneManifests } from './dropzone/manifests.js'; import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionsManifests } from './entity-bulk-actions/manifests.js'; import { manifests as fileUploadPreviewManifests } from './components/input-upload-field/manifests.js'; @@ -20,7 +19,6 @@ import { manifests as workspaceManifests } from './workspace/manifests.js'; export const manifests: Array = [ ...auditLogManifests, ...collectionManifests, - ...dropzoneManifests, ...entityActionsManifests, ...entityBulkActionsManifests, ...fileUploadPreviewManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index fdd8bb21d4..3e12c0901b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -1,12 +1,12 @@ import { UmbMediaItemRepository } from '../../repository/index.js'; import { UmbMediaTreeRepository } from '../../tree/media-tree.repository.js'; import { UMB_MEDIA_ROOT_ENTITY_TYPE } from '../../entity.js'; -import type { UmbDropzoneElement } from '../../dropzone/dropzone.element.js'; import type { UmbMediaTreeItemModel, UmbMediaSearchItemModel, UmbMediaItemModel } from '../../types.js'; import { UmbMediaSearchProvider } from '../../search/index.js'; import type { UmbMediaPathModel } from './types.js'; import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; +import type { UmbDropzoneElement } from '@umbraco-cms/backoffice/dropzone'; import { css, html, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts index c9545de5a0..9905007aba 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/types.ts @@ -5,7 +5,6 @@ import type { UmbContentDetailModel, UmbElementValueModel } from '@umbraco-cms/b export type * from './audit-log/types.js'; export type * from './collection/types.js'; -export type * from './dropzone/types.js'; export type * from './modals/types.js'; export type * from './recycle-bin/types.js'; export type * from './repository/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/media/vite.config.ts index 03e52c2baa..c14c673090 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/vite.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ entry: { 'entry-point': 'entry-point.ts', 'imaging/index': 'imaging/index.ts', + 'dropzone/index': 'dropzone/index.ts', 'media-types/index': 'media-types/index.ts', 'media/index': 'media/index.ts', 'umbraco-package': 'umbraco-package.ts', diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 512ce70e9a..9d85ebd8e6 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -68,6 +68,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "@umbraco-cms/backoffice/document-blueprint": ["./src/packages/documents/document-blueprints/index.ts"], "@umbraco-cms/backoffice/document-type": ["./src/packages/documents/document-types/index.ts"], "@umbraco-cms/backoffice/document": ["./src/packages/documents/documents/index.ts"], + "@umbraco-cms/backoffice/dropzone": ["./src/packages/media/dropzone/index.ts"], "@umbraco-cms/backoffice/entity-action": ["./src/packages/core/entity-action/index.ts"], "@umbraco-cms/backoffice/entity-bulk-action": ["./src/packages/core/entity-bulk-action/index.ts"], "@umbraco-cms/backoffice/entity-create-option-action": [ diff --git a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts deleted file mode 100644 index 0cd1f38783..0000000000 --- a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts +++ /dev/null @@ -1,530 +0,0 @@ - - import * as import0 from '@umbraco-cms/backoffice/app'; -import * as import1 from '@umbraco-cms/backoffice/class-api'; -import * as import2 from '@umbraco-cms/backoffice/context-api'; -import * as import3 from '@umbraco-cms/backoffice/controller-api'; -import * as import4 from '@umbraco-cms/backoffice/element-api'; -import * as import5 from '@umbraco-cms/backoffice/embedded-media'; -import * as import6 from '@umbraco-cms/backoffice/extension-api'; -import * as import7 from '@umbraco-cms/backoffice/formatting-api'; -import * as import8 from '@umbraco-cms/backoffice/localization-api'; -import * as import9 from '@umbraco-cms/backoffice/observable-api'; -import * as import10 from '@umbraco-cms/backoffice/action'; -import * as import11 from '@umbraco-cms/backoffice/audit-log'; -import * as import12 from '@umbraco-cms/backoffice/auth'; -import * as import13 from '@umbraco-cms/backoffice/block-custom-view'; -import * as import14 from '@umbraco-cms/backoffice/block-grid'; -import * as import15 from '@umbraco-cms/backoffice/block-list'; -import * as import16 from '@umbraco-cms/backoffice/block-rte'; -import * as import17 from '@umbraco-cms/backoffice/block-type'; -import * as import18 from '@umbraco-cms/backoffice/block'; -import * as import19 from '@umbraco-cms/backoffice/clipboard'; -import * as import20 from '@umbraco-cms/backoffice/code-editor'; -import * as import21 from '@umbraco-cms/backoffice/collection'; -import * as import22 from '@umbraco-cms/backoffice/components'; -import * as import23 from '@umbraco-cms/backoffice/const'; -import * as import24 from '@umbraco-cms/backoffice/content-type'; -import * as import25 from '@umbraco-cms/backoffice/content'; -import * as import26 from '@umbraco-cms/backoffice/culture'; -import * as import27 from '@umbraco-cms/backoffice/current-user'; -import * as import28 from '@umbraco-cms/backoffice/dashboard'; -import * as import29 from '@umbraco-cms/backoffice/data-type'; -import * as import30 from '@umbraco-cms/backoffice/debug'; -import * as import31 from '@umbraco-cms/backoffice/dictionary'; -import * as import32 from '@umbraco-cms/backoffice/document-blueprint'; -import * as import33 from '@umbraco-cms/backoffice/document-type'; -import * as import34 from '@umbraco-cms/backoffice/document'; -import * as import35 from '@umbraco-cms/backoffice/entity-action'; -import * as import36 from '@umbraco-cms/backoffice/entity-bulk-action'; -import * as import37 from '@umbraco-cms/backoffice/entity-create-option-action'; -import * as import38 from '@umbraco-cms/backoffice/entity'; -import * as import39 from '@umbraco-cms/backoffice/entity-item'; -import * as import40 from '@umbraco-cms/backoffice/event'; -import * as import41 from '@umbraco-cms/backoffice/extension-registry'; -import * as import42 from '@umbraco-cms/backoffice/health-check'; -import * as import43 from '@umbraco-cms/backoffice/help'; -import * as import44 from '@umbraco-cms/backoffice/icon'; -import * as import45 from '@umbraco-cms/backoffice/id'; -import * as import46 from '@umbraco-cms/backoffice/imaging'; -import * as import47 from '@umbraco-cms/backoffice/language'; -import * as import48 from '@umbraco-cms/backoffice/lit-element'; -import * as import49 from '@umbraco-cms/backoffice/localization'; -import * as import50 from '@umbraco-cms/backoffice/log-viewer'; -import * as import51 from '@umbraco-cms/backoffice/media-type'; -import * as import52 from '@umbraco-cms/backoffice/media'; -import * as import53 from '@umbraco-cms/backoffice/member-group'; -import * as import54 from '@umbraco-cms/backoffice/member-type'; -import * as import55 from '@umbraco-cms/backoffice/member'; -import * as import56 from '@umbraco-cms/backoffice/menu'; -import * as import57 from '@umbraco-cms/backoffice/modal'; -import * as import58 from '@umbraco-cms/backoffice/multi-url-picker'; -import * as import59 from '@umbraco-cms/backoffice/notification'; -import * as import60 from '@umbraco-cms/backoffice/object-type'; -import * as import61 from '@umbraco-cms/backoffice/package'; -import * as import62 from '@umbraco-cms/backoffice/partial-view'; -import * as import63 from '@umbraco-cms/backoffice/picker-input'; -import * as import64 from '@umbraco-cms/backoffice/picker'; -import * as import65 from '@umbraco-cms/backoffice/property-action'; -import * as import66 from '@umbraco-cms/backoffice/property-editor'; -import * as import67 from '@umbraco-cms/backoffice/property-type'; -import * as import68 from '@umbraco-cms/backoffice/property'; -import * as import69 from '@umbraco-cms/backoffice/recycle-bin'; -import * as import70 from '@umbraco-cms/backoffice/relation-type'; -import * as import71 from '@umbraco-cms/backoffice/relations'; -import * as import72 from '@umbraco-cms/backoffice/repository'; -import * as import73 from '@umbraco-cms/backoffice/resources'; -import * as import74 from '@umbraco-cms/backoffice/router'; -import * as import75 from '@umbraco-cms/backoffice/rte'; -import * as import76 from '@umbraco-cms/backoffice/script'; -import * as import77 from '@umbraco-cms/backoffice/search'; -import * as import78 from '@umbraco-cms/backoffice/section'; -import * as import79 from '@umbraco-cms/backoffice/server-file-system'; -import * as import80 from '@umbraco-cms/backoffice/settings'; -import * as import81 from '@umbraco-cms/backoffice/sorter'; -import * as import82 from '@umbraco-cms/backoffice/static-file'; -import * as import83 from '@umbraco-cms/backoffice/store'; -import * as import84 from '@umbraco-cms/backoffice/style'; -import * as import85 from '@umbraco-cms/backoffice/stylesheet'; -import * as import86 from '@umbraco-cms/backoffice/sysinfo'; -import * as import87 from '@umbraco-cms/backoffice/tags'; -import * as import88 from '@umbraco-cms/backoffice/template'; -import * as import89 from '@umbraco-cms/backoffice/temporary-file'; -import * as import90 from '@umbraco-cms/backoffice/themes'; -import * as import91 from '@umbraco-cms/backoffice/tiny-mce'; -import * as import92 from '@umbraco-cms/backoffice/tiptap'; -import * as import93 from '@umbraco-cms/backoffice/translation'; -import * as import94 from '@umbraco-cms/backoffice/tree'; -import * as import95 from '@umbraco-cms/backoffice/ufm'; -import * as import96 from '@umbraco-cms/backoffice/user-change-password'; -import * as import97 from '@umbraco-cms/backoffice/user-group'; -import * as import98 from '@umbraco-cms/backoffice/user-permission'; -import * as import99 from '@umbraco-cms/backoffice/user'; -import * as import100 from '@umbraco-cms/backoffice/utils'; -import * as import101 from '@umbraco-cms/backoffice/validation'; -import * as import102 from '@umbraco-cms/backoffice/variant'; -import * as import103 from '@umbraco-cms/backoffice/webhook'; -import * as import104 from '@umbraco-cms/backoffice/workspace'; - - export const imports = [ - { - path: '@umbraco-cms/backoffice/app', - package: import0 - }, -{ - path: '@umbraco-cms/backoffice/class-api', - package: import1 - }, -{ - path: '@umbraco-cms/backoffice/context-api', - package: import2 - }, -{ - path: '@umbraco-cms/backoffice/controller-api', - package: import3 - }, -{ - path: '@umbraco-cms/backoffice/element-api', - package: import4 - }, -{ - path: '@umbraco-cms/backoffice/embedded-media', - package: import5 - }, -{ - path: '@umbraco-cms/backoffice/extension-api', - package: import6 - }, -{ - path: '@umbraco-cms/backoffice/formatting-api', - package: import7 - }, -{ - path: '@umbraco-cms/backoffice/localization-api', - package: import8 - }, -{ - path: '@umbraco-cms/backoffice/observable-api', - package: import9 - }, -{ - path: '@umbraco-cms/backoffice/action', - package: import10 - }, -{ - path: '@umbraco-cms/backoffice/audit-log', - package: import11 - }, -{ - path: '@umbraco-cms/backoffice/auth', - package: import12 - }, -{ - path: '@umbraco-cms/backoffice/block-custom-view', - package: import13 - }, -{ - path: '@umbraco-cms/backoffice/block-grid', - package: import14 - }, -{ - path: '@umbraco-cms/backoffice/block-list', - package: import15 - }, -{ - path: '@umbraco-cms/backoffice/block-rte', - package: import16 - }, -{ - path: '@umbraco-cms/backoffice/block-type', - package: import17 - }, -{ - path: '@umbraco-cms/backoffice/block', - package: import18 - }, -{ - path: '@umbraco-cms/backoffice/clipboard', - package: import19 - }, -{ - path: '@umbraco-cms/backoffice/code-editor', - package: import20 - }, -{ - path: '@umbraco-cms/backoffice/collection', - package: import21 - }, -{ - path: '@umbraco-cms/backoffice/components', - package: import22 - }, -{ - path: '@umbraco-cms/backoffice/const', - package: import23 - }, -{ - path: '@umbraco-cms/backoffice/content-type', - package: import24 - }, -{ - path: '@umbraco-cms/backoffice/content', - package: import25 - }, -{ - path: '@umbraco-cms/backoffice/culture', - package: import26 - }, -{ - path: '@umbraco-cms/backoffice/current-user', - package: import27 - }, -{ - path: '@umbraco-cms/backoffice/dashboard', - package: import28 - }, -{ - path: '@umbraco-cms/backoffice/data-type', - package: import29 - }, -{ - path: '@umbraco-cms/backoffice/debug', - package: import30 - }, -{ - path: '@umbraco-cms/backoffice/dictionary', - package: import31 - }, -{ - path: '@umbraco-cms/backoffice/document-blueprint', - package: import32 - }, -{ - path: '@umbraco-cms/backoffice/document-type', - package: import33 - }, -{ - path: '@umbraco-cms/backoffice/document', - package: import34 - }, -{ - path: '@umbraco-cms/backoffice/entity-action', - package: import35 - }, -{ - path: '@umbraco-cms/backoffice/entity-bulk-action', - package: import36 - }, -{ - path: '@umbraco-cms/backoffice/entity-create-option-action', - package: import37 - }, -{ - path: '@umbraco-cms/backoffice/entity', - package: import38 - }, -{ - path: '@umbraco-cms/backoffice/entity-item', - package: import39 - }, -{ - path: '@umbraco-cms/backoffice/event', - package: import40 - }, -{ - path: '@umbraco-cms/backoffice/extension-registry', - package: import41 - }, -{ - path: '@umbraco-cms/backoffice/health-check', - package: import42 - }, -{ - path: '@umbraco-cms/backoffice/help', - package: import43 - }, -{ - path: '@umbraco-cms/backoffice/icon', - package: import44 - }, -{ - path: '@umbraco-cms/backoffice/id', - package: import45 - }, -{ - path: '@umbraco-cms/backoffice/imaging', - package: import46 - }, -{ - path: '@umbraco-cms/backoffice/language', - package: import47 - }, -{ - path: '@umbraco-cms/backoffice/lit-element', - package: import48 - }, -{ - path: '@umbraco-cms/backoffice/localization', - package: import49 - }, -{ - path: '@umbraco-cms/backoffice/log-viewer', - package: import50 - }, -{ - path: '@umbraco-cms/backoffice/media-type', - package: import51 - }, -{ - path: '@umbraco-cms/backoffice/media', - package: import52 - }, -{ - path: '@umbraco-cms/backoffice/member-group', - package: import53 - }, -{ - path: '@umbraco-cms/backoffice/member-type', - package: import54 - }, -{ - path: '@umbraco-cms/backoffice/member', - package: import55 - }, -{ - path: '@umbraco-cms/backoffice/menu', - package: import56 - }, -{ - path: '@umbraco-cms/backoffice/modal', - package: import57 - }, -{ - path: '@umbraco-cms/backoffice/multi-url-picker', - package: import58 - }, -{ - path: '@umbraco-cms/backoffice/notification', - package: import59 - }, -{ - path: '@umbraco-cms/backoffice/object-type', - package: import60 - }, -{ - path: '@umbraco-cms/backoffice/package', - package: import61 - }, -{ - path: '@umbraco-cms/backoffice/partial-view', - package: import62 - }, -{ - path: '@umbraco-cms/backoffice/picker-input', - package: import63 - }, -{ - path: '@umbraco-cms/backoffice/picker', - package: import64 - }, -{ - path: '@umbraco-cms/backoffice/property-action', - package: import65 - }, -{ - path: '@umbraco-cms/backoffice/property-editor', - package: import66 - }, -{ - path: '@umbraco-cms/backoffice/property-type', - package: import67 - }, -{ - path: '@umbraco-cms/backoffice/property', - package: import68 - }, -{ - path: '@umbraco-cms/backoffice/recycle-bin', - package: import69 - }, -{ - path: '@umbraco-cms/backoffice/relation-type', - package: import70 - }, -{ - path: '@umbraco-cms/backoffice/relations', - package: import71 - }, -{ - path: '@umbraco-cms/backoffice/repository', - package: import72 - }, -{ - path: '@umbraco-cms/backoffice/resources', - package: import73 - }, -{ - path: '@umbraco-cms/backoffice/router', - package: import74 - }, -{ - path: '@umbraco-cms/backoffice/rte', - package: import75 - }, -{ - path: '@umbraco-cms/backoffice/script', - package: import76 - }, -{ - path: '@umbraco-cms/backoffice/search', - package: import77 - }, -{ - path: '@umbraco-cms/backoffice/section', - package: import78 - }, -{ - path: '@umbraco-cms/backoffice/server-file-system', - package: import79 - }, -{ - path: '@umbraco-cms/backoffice/settings', - package: import80 - }, -{ - path: '@umbraco-cms/backoffice/sorter', - package: import81 - }, -{ - path: '@umbraco-cms/backoffice/static-file', - package: import82 - }, -{ - path: '@umbraco-cms/backoffice/store', - package: import83 - }, -{ - path: '@umbraco-cms/backoffice/style', - package: import84 - }, -{ - path: '@umbraco-cms/backoffice/stylesheet', - package: import85 - }, -{ - path: '@umbraco-cms/backoffice/sysinfo', - package: import86 - }, -{ - path: '@umbraco-cms/backoffice/tags', - package: import87 - }, -{ - path: '@umbraco-cms/backoffice/template', - package: import88 - }, -{ - path: '@umbraco-cms/backoffice/temporary-file', - package: import89 - }, -{ - path: '@umbraco-cms/backoffice/themes', - package: import90 - }, -{ - path: '@umbraco-cms/backoffice/tiny-mce', - package: import91 - }, -{ - path: '@umbraco-cms/backoffice/tiptap', - package: import92 - }, -{ - path: '@umbraco-cms/backoffice/translation', - package: import93 - }, -{ - path: '@umbraco-cms/backoffice/tree', - package: import94 - }, -{ - path: '@umbraco-cms/backoffice/ufm', - package: import95 - }, -{ - path: '@umbraco-cms/backoffice/user-change-password', - package: import96 - }, -{ - path: '@umbraco-cms/backoffice/user-group', - package: import97 - }, -{ - path: '@umbraco-cms/backoffice/user-permission', - package: import98 - }, -{ - path: '@umbraco-cms/backoffice/user', - package: import99 - }, -{ - path: '@umbraco-cms/backoffice/utils', - package: import100 - }, -{ - path: '@umbraco-cms/backoffice/validation', - package: import101 - }, -{ - path: '@umbraco-cms/backoffice/variant', - package: import102 - }, -{ - path: '@umbraco-cms/backoffice/webhook', - package: import103 - }, -{ - path: '@umbraco-cms/backoffice/workspace', - package: import104 - } - ]; - \ No newline at end of file From 384b9bdb6501918f9e6f5f5291deed826de92f91 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 14:31:40 +0100 Subject: [PATCH 04/19] Update error-viewer-modal.token.ts (#18776) --- .../core/modal/common/error-viewer/error-viewer-modal.token.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/error-viewer/error-viewer-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/error-viewer/error-viewer-modal.token.ts index 311f1b1c67..18178addd4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/error-viewer/error-viewer-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/modal/common/error-viewer/error-viewer-modal.token.ts @@ -1,5 +1,5 @@ import type { UmbPeekErrorArgs } from '../../../notification/types.js'; -import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import { UmbModalToken } from '../../token/index.js'; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface UmbErrorViewerModalData extends UmbPeekErrorArgs {} From ee496d2730373d8c720ac529a22d2436c0971625 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 14:32:00 +0100 Subject: [PATCH 05/19] fix workspace circular (#18775) --- .../components/workspace-editor/workspace-editor.element.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts index d40612ba90..845b2248af 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts @@ -1,9 +1,10 @@ +import type { ManifestWorkspaceView } from '../../extensions/types.js'; +import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { createExtensionElement, UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { UMB_WORKSPACE_VIEW_PATH_PATTERN, type ManifestWorkspaceView } from '@umbraco-cms/backoffice/workspace'; import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; /** From 0b6fe91fd3511ee1a6017e4c6566b687fd10ebe9 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 14:39:18 +0100 Subject: [PATCH 06/19] Validation: Fixes circular dependencies in the validation module (#18774) * fixes circular dependencies in the validation module * Update bind-to-validation.lit-directive.ts --- .../validation/context/server-model-validator.context.ts | 6 +++--- .../directives/bind-to-validation.lit-directive.ts | 4 ++-- .../validation-path-translator-base.controller.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validator.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validator.context.ts index e8f3073049..1cecd8d39b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validator.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/context/server-model-validator.context.ts @@ -1,14 +1,14 @@ import type { UmbValidator } from '../interfaces/validator.interface.js'; +import type { UmbValidationPathTranslator } from '../types.js'; +import { UmbValidationPathTranslationController } from '../controllers/validation-path-translation/index.js'; import { UMB_VALIDATION_CONTEXT } from './validation.context-token.js'; import { UMB_SERVER_MODEL_VALIDATOR_CONTEXT } from './server-model-validator.context-token.js'; +import type { UmbValidationMessage } from './validation-messages.manager.js'; import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbDataSourceResponse } from '@umbraco-cms/backoffice/repository'; -import type { UmbValidationMessage } from './validation-messages.manager.js'; -import type { UmbValidationPathTranslator } from '../types.js'; import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; -import { UmbValidationPathTranslationController } from '../index.js'; /** This should ideally be generated by the server, but we currently don't generate error-model-types. */ interface ValidateErrorResponseBodyModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/directives/bind-to-validation.lit-directive.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/directives/bind-to-validation.lit-directive.ts index 01f484ae35..248c5b4a88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/directives/bind-to-validation.lit-directive.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/directives/bind-to-validation.lit-directive.ts @@ -1,7 +1,7 @@ +import type { UmbFormControlMixinInterface } from '../mixins/index.js'; +import { UmbBindServerValidationToFormControl, UmbFormControlValidator } from '../controllers/index.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { AsyncDirective, directive, nothing, type ElementPart } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbFormControlMixinInterface } from '@umbraco-cms/backoffice/validation'; -import { UmbBindServerValidationToFormControl, UmbFormControlValidator } from '@umbraco-cms/backoffice/validation'; /** * The `bind to validation` directive connects the Form Control Element to closets Validation Context. diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/validation-path-translator-base.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/validation-path-translator-base.controller.ts index b9a9bacb8e..9e84e00794 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/validation-path-translator-base.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/translators/validation-path-translator-base.controller.ts @@ -1,4 +1,4 @@ -import { UMB_VALIDATION_CONTEXT } from '../index.js'; +import { UMB_VALIDATION_CONTEXT } from '../context/index.js'; import type { UmbValidationMessageTranslator } from './validation-message-path-translator.interface.js'; import type { UmbControllerAlias, UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; From ad443c7b13567085ac592f88a1fbb35520257f30 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 14:47:04 +0100 Subject: [PATCH 07/19] fix circular (#18773) --- .../repository/detail/user-detail.server.data-source.ts | 2 +- .../src/packages/user/user/utils/index.ts | 8 +------- .../src/packages/user/user/utils/user-kind.ts | 6 ++++++ 3 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user/utils/user-kind.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/detail/user-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/detail/user-detail.server.data-source.ts index 072ae05901..6dfb702237 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/detail/user-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/repository/detail/user-detail.server.data-source.ts @@ -1,6 +1,6 @@ import type { UmbUserDetailModel, UmbUserStartNodesModel } from '../../types.js'; import { UMB_USER_ENTITY_TYPE } from '../../entity.js'; -import { UmbUserKind } from '../../utils/index.js'; +import { UmbUserKind } from '../../utils/user-kind.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository'; import type { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/index.ts index 4115ca2bbc..35243911c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/index.ts @@ -1,8 +1,2 @@ export * from './is-user.function.js'; - -export type UmbUserKindType = 'Default' | 'Api'; - -export const UmbUserKind = Object.freeze({ - DEFAULT: 'Default', - API: 'Api', -}); +export * from './user-kind.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/user-kind.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/user-kind.ts new file mode 100644 index 0000000000..278826cf3b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user/utils/user-kind.ts @@ -0,0 +1,6 @@ +export type UmbUserKindType = 'Default' | 'Api'; + +export const UmbUserKind = Object.freeze({ + DEFAULT: 'Default', + API: 'Api', +}); From 36c66177fcaa6b992e084005fbf0c4ce63bca573 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 14:49:09 +0100 Subject: [PATCH 08/19] Feature: Tree expansion state (#18227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implement tree expansion logic * wip test example * support complex expansion * extend entity * extend with model * Update tree-item-context.interface.ts * use expansion model to observe open state * clean up * fall back to tree context * Update default-tree.context.ts * Update default-tree.context.ts * Update default-tree.context.ts * clean up * simplify model and state * refactor to manager * remove test data * Update default-tree.context.ts * rename * add get method * rename to collapse * all collapse all method * fix collapse logic * add js docs * add tests for expansion manager * do not load children if the item is already open * Update tree-item-element-base.ts * config to expand tree root in pickers * expand tree root for duplicate to * Update tree-expansion-manager.test.ts * make methods async * use array state * add isExpanded helper * refactor to use isExpanded helper * fix type issues --------- Co-authored-by: Niels Lyngsø Co-authored-by: Niels Lyngsø --- .../core/tree/default/default-tree.context.ts | 60 ++++++++++ .../core/tree/default/default-tree.element.ts | 19 ++++ .../modal/duplicate-to-modal.element.ts | 1 + .../entity-actions/move/move-to.action.ts | 1 + .../core/tree/expansion-manager/index.ts | 1 + .../tree-expansion-manager.test.ts | 107 ++++++++++++++++++ .../tree-expansion-manager.ts | 81 +++++++++++++ .../core/tree/expansion-manager/types.ts | 3 + .../tree-item-base/tree-item-context-base.ts | 67 ++++++++++- .../tree-item-base/tree-item-element-base.ts | 18 ++- .../tree-item/tree-item-context.interface.ts | 4 + .../tree-picker-modal.element.ts | 1 + .../tree-picker-modal.token.ts | 1 + .../modal/duplicate-document-modal.element.ts | 5 +- 14 files changed, 364 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.test.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts index 71a82a8ba8..79a5eba6c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.context.ts @@ -3,6 +3,8 @@ import type { UmbTreeRepository } from '../data/tree-repository.interface.js'; import type { UmbTreeContext } from '../tree-context.interface.js'; import type { UmbTreeRootItemsRequestArgs } from '../data/types.js'; import type { ManifestTree } from '../extensions/types.js'; +import { UmbTreeExpansionManager } from '../expansion-manager/index.js'; +import type { UmbTreeExpansionModel } from '../expansion-manager/types.js'; import { UMB_TREE_CONTEXT } from './default-tree.context-token.js'; import { type UmbActionEventContext, UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { type ManifestRepository, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; @@ -38,10 +40,14 @@ export class UmbDefaultTreeContext< public filter?: (item: TreeItemType) => boolean = () => true; public readonly selection = new UmbSelectionManager(this._host); public readonly pagination = new UmbPaginationManager(); + public readonly expansion = new UmbTreeExpansionManager(this._host); #hideTreeRoot = new UmbBooleanState(false); hideTreeRoot = this.#hideTreeRoot.asObservable(); + #expandTreeRoot = new UmbBooleanState(undefined); + expandTreeRoot = this.#expandTreeRoot.asObservable(); + #startNode = new UmbObjectState(undefined); startNode = this.#startNode.asObservable(); @@ -156,6 +162,10 @@ export class UmbDefaultTreeContext< if (data) { this.#treeRoot.setValue(data); this.pagination.setTotalItems(1); + + if (this.getExpandTreeRoot()) { + this.#toggleTreeRootExpansion(true); + } } } @@ -277,6 +287,56 @@ export class UmbDefaultTreeContext< return this.#additionalRequestArgs.getValue(); } + /** + * Sets the expansion state + * @param {UmbTreeExpansionModel} data - The expansion state + * @returns {void} + * @memberof UmbDefaultTreeContext + */ + setExpansion(data: UmbTreeExpansionModel): void { + this.expansion.setExpansion(data); + } + + /** + * Gets the expansion state + * @returns {UmbTreeExpansionModel} - The expansion state + * @memberof UmbDefaultTreeContext + */ + getExpansion(): UmbTreeExpansionModel { + return this.expansion.getExpansion(); + } + + /** + * Sets the expandTreeRoot config + * @param {boolean} expandTreeRoot - Whether to expand the tree root + * @memberof UmbDefaultTreeContext + */ + setExpandTreeRoot(expandTreeRoot: boolean) { + this.#expandTreeRoot.setValue(expandTreeRoot); + this.#toggleTreeRootExpansion(expandTreeRoot); + } + + /** + * Gets the expandTreeRoot config + * @returns {boolean | undefined} - Whether to expand the tree root + * @memberof UmbDefaultTreeContext + */ + getExpandTreeRoot(): boolean | undefined { + return this.#expandTreeRoot.getValue(); + } + + #toggleTreeRootExpansion(expand: boolean) { + const treeRoot = this.#treeRoot.getValue(); + if (!treeRoot) return; + const treeRootEntity = { entityType: treeRoot.entityType, unique: treeRoot.unique }; + + if (expand) { + this.expansion.expandItem(treeRootEntity); + } else { + this.expansion.collapseItem(treeRootEntity); + } + } + #resetTree() { this.#treeRoot.setValue(undefined); this.#rootItems.setValue([]); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts index 329e9dfb3d..3ad8bf25c8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/default/default-tree.element.ts @@ -5,6 +5,7 @@ import type { UmbTreeSelectionConfiguration, UmbTreeStartNode, } from '../types.js'; +import type { UmbTreeExpansionModel } from '../expansion-manager/types.js'; import type { UmbDefaultTreeContext } from './default-tree.context.js'; import { UMB_TREE_CONTEXT } from './default-tree.context-token.js'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; @@ -28,6 +29,9 @@ export class UmbDefaultTreeElement extends UmbLitElement { @property({ type: Boolean, attribute: false }) hideTreeRoot: boolean = false; + @property({ type: Boolean, attribute: false }) + expandTreeRoot: boolean = false; + @property({ type: Object, attribute: false }) startNode?: UmbTreeStartNode; @@ -40,6 +44,9 @@ export class UmbDefaultTreeElement extends UmbLitElement { @property({ attribute: false }) filter: (item: UmbTreeItemModelBase) => boolean = () => true; + @property({ attribute: false }) + expansion: UmbTreeExpansionModel = []; + @state() private _rootItems: UmbTreeItemModel[] = []; @@ -92,6 +99,10 @@ export class UmbDefaultTreeElement extends UmbLitElement { this.#treeContext!.setHideTreeRoot(this.hideTreeRoot); } + if (_changedProperties.has('expandTreeRoot')) { + this.#treeContext!.setExpandTreeRoot(this.expandTreeRoot); + } + if (_changedProperties.has('foldersOnly')) { this.#treeContext!.setFoldersOnly(this.foldersOnly ?? false); } @@ -103,12 +114,20 @@ export class UmbDefaultTreeElement extends UmbLitElement { if (_changedProperties.has('filter')) { this.#treeContext!.filter = this.filter; } + + if (_changedProperties.has('expansion')) { + this.#treeContext!.setExpansion(this.expansion); + } } getSelection() { return this.#treeContext?.selection.getSelection(); } + getExpansion() { + return this.#treeContext?.expansion.getExpansion(); + } + override render() { return html` ${this.#renderTreeRoot()} ${this.#renderRootItems()}`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/duplicate-to/modal/duplicate-to-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/duplicate-to/modal/duplicate-to-modal.element.ts index cc37ba5d74..ef129d8762 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/duplicate-to/modal/duplicate-to-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/duplicate-to/modal/duplicate-to-modal.element.ts @@ -32,6 +32,7 @@ export class UmbDuplicateToModalElement extends UmbModalBaseElement diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/move/move-to.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/move/move-to.action.ts index 239ddb10b3..370dd982bc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/move/move-to.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/entity-actions/move/move-to.action.ts @@ -16,6 +16,7 @@ export class UmbMoveToEntityAction extends UmbEntityActionBase treeItem.unique !== this.args.unique, }, }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/index.ts new file mode 100644 index 0000000000..11111f15fb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/index.ts @@ -0,0 +1 @@ +export * from './tree-expansion-manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.test.ts new file mode 100644 index 0000000000..9f9adb5d7a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.test.ts @@ -0,0 +1,107 @@ +import { UmbTreeExpansionManager } from './tree-expansion-manager.js'; +import { expect } from '@open-wc/testing'; +import { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; + +@customElement('test-my-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +describe('UmbTreeExpansionManager', () => { + let manager: UmbTreeExpansionManager; + const item = { entityType: 'test', unique: '123' }; + const item2 = { entityType: 'test', unique: '456' }; + + beforeEach(() => { + const hostElement = new UmbTestControllerHostElement(); + manager = new UmbTreeExpansionManager(hostElement); + }); + + describe('Public API', () => { + describe('properties', () => { + it('has an expansion property', () => { + expect(manager).to.have.property('expansion').to.be.an.instanceOf(Observable); + }); + }); + + describe('methods', () => { + it('has an isExpanded method', () => { + expect(manager).to.have.property('isExpanded').that.is.a('function'); + }); + + it('has a setExpansion method', () => { + expect(manager).to.have.property('setExpansion').that.is.a('function'); + }); + + it('has a getExpansion method', () => { + expect(manager).to.have.property('getExpansion').that.is.a('function'); + }); + + it('has a expandItem method', () => { + expect(manager).to.have.property('expandItem').that.is.a('function'); + }); + + it('has a collapseItem method', () => { + expect(manager).to.have.property('collapseItem').that.is.a('function'); + }); + + it('has a collapseAll method', () => { + expect(manager).to.have.property('collapseAll').that.is.a('function'); + }); + }); + }); + + describe('isExpanded', () => { + it('checks if an item is expanded', (done) => { + manager.setExpansion([item]); + const isExpanded = manager.isExpanded(item); + expect(isExpanded).to.be.an.instanceOf(Observable); + manager.isExpanded(item).subscribe((value) => { + console.log('VALUE', value); + expect(value).to.be.true; + done(); + }); + }); + }); + + describe('setExpansion', () => { + it('sets the expansion state', () => { + const expansion = [item]; + manager.setExpansion(expansion); + expect(manager.getExpansion()).to.deep.equal(expansion); + }); + }); + + describe('getExpansion', () => { + it('gets the expansion state', () => { + const expansion = [item]; + manager.setExpansion(expansion); + expect(manager.getExpansion()).to.deep.equal(expansion); + }); + }); + + describe('expandItem', () => { + it('expands an item', async () => { + await manager.expandItem(item); + expect(manager.getExpansion()).to.deep.equal([item]); + }); + }); + + describe('collapseItem', () => { + it('collapses an item', async () => { + await manager.expandItem(item); + expect(manager.getExpansion()).to.deep.equal([item]); + manager.collapseItem(item); + expect(manager.getExpansion()).to.deep.equal([]); + }); + }); + + describe('collapseAll', () => { + it('collapses all items', () => { + manager.setExpansion([item, item2]); + expect(manager.getExpansion()).to.deep.equal([item, item2]); + manager.collapseAll(); + expect(manager.getExpansion()).to.deep.equal([]); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.ts new file mode 100644 index 0000000000..a2144f8c1c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/tree-expansion-manager.ts @@ -0,0 +1,81 @@ +import type { UmbTreeExpansionModel } from './types.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { UmbArrayState, type Observable } from '@umbraco-cms/backoffice/observable-api'; + +/** + * Manages the expansion state of a tree + * @exports + * @class UmbTreeExpansionManager + * @augments {UmbControllerBase} + */ +export class UmbTreeExpansionManager extends UmbControllerBase { + #expansion = new UmbArrayState([], (x) => x.unique); + expansion = this.#expansion.asObservable(); + + /** + * Checks if an entity is expanded + * @param {UmbEntityModel} entity The entity to check + * @param {string} entity.entityType The entity type + * @param {string} entity.unique The unique key + * @returns {Observable} True if the entity is expanded + * @memberof UmbTreeExpansionManager + */ + isExpanded(entity: UmbEntityModel): Observable { + return this.#expansion.asObservablePart((entries) => + entries?.some((entry) => entry.entityType === entity.entityType && entry.unique === entity.unique), + ); + } + + /** + * Sets the expansion state + * @param {UmbTreeExpansionModel | undefined} expansion The expansion state + * @memberof UmbTreeExpansionManager + * @returns {void} + */ + setExpansion(expansion: UmbTreeExpansionModel): void { + this.#expansion.setValue(expansion); + } + + /** + * Gets the expansion state + * @memberof UmbTreeExpansionManager + * @returns {UmbTreeExpansionModel} The expansion state + */ + getExpansion(): UmbTreeExpansionModel { + return this.#expansion.getValue(); + } + + /** + * Opens a child tree item + * @param {UmbEntityModel} entity The entity to open + * @param {string} entity.entityType The entity type + * @param {string} entity.unique The unique key + * @memberof UmbTreeExpansionManager + * @returns {Promise} + */ + public async expandItem(entity: UmbEntityModel): Promise { + this.#expansion.appendOne(entity); + } + + /** + * Closes a child tree item + * @param {UmbEntityModel} entity The entity to close + * @param {string} entity.entityType The entity type + * @param {string} entity.unique The unique key + * @memberof UmbTreeExpansionManager + * @returns {Promise} + */ + public async collapseItem(entity: UmbEntityModel): Promise { + this.#expansion.filter((x) => x.entityType !== entity.entityType || x.unique !== entity.unique); + } + + /** + * Closes all child tree items + * @memberof UmbTreeExpansionManager + * @returns {Promise} + */ + public async collapseAll(): Promise { + this.#expansion.setValue([]); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/types.ts new file mode 100644 index 0000000000..7d5c050029 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/expansion-manager/types.ts @@ -0,0 +1,3 @@ +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export type UmbTreeExpansionModel = Array; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts index 93844ec277..00b8782314 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-context-base.ts @@ -19,6 +19,7 @@ import { import type { UmbEntityActionEvent } from '@umbraco-cms/backoffice/entity-action'; import { UmbPaginationManager, debounce } from '@umbraco-cms/backoffice/utils'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; +import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; export abstract class UmbTreeItemContextBase< TreeItemType extends UmbTreeItemModel, @@ -28,7 +29,7 @@ export abstract class UmbTreeItemContextBase< extends UmbContextBase> implements UmbTreeItemContext { - public unique?: string | null; + public unique?: UmbEntityUnique; public entityType?: string; public readonly pagination = new UmbPaginationManager(); @@ -66,6 +67,9 @@ export abstract class UmbTreeItemContextBase< #path = new UmbStringState(''); readonly path = this.#path.asObservable(); + #isOpen = new UmbBooleanState(false); + isOpen = this.#isOpen.asObservable(); + #foldersOnly = new UmbBooleanState(false); readonly foldersOnly = this.#foldersOnly.asObservable(); @@ -212,16 +216,57 @@ export abstract class UmbTreeItemContextBase< }); } + /** + * Selects the tree item + * @memberof UmbTreeItemContextBase + * @returns {void} + */ public select() { if (this.unique === undefined) throw new Error('Could not select. Unique is missing'); this.treeContext?.selection.select(this.unique); } + /** + * Deselects the tree item + * @memberof UmbTreeItemContextBase + * @returns {void} + */ public deselect() { if (this.unique === undefined) throw new Error('Could not deselect. Unique is missing'); this.treeContext?.selection.deselect(this.unique); } + public showChildren() { + const entityType = this.entityType; + const unique = this.unique; + + if (!entityType) { + throw new Error('Could not show children, entity type is missing'); + } + + if (unique === undefined) { + throw new Error('Could not show children, unique is missing'); + } + + // It is the tree that keeps track of the open children. We tell the tree to open this child + this.treeContext?.expansion.expandItem({ entityType, unique }); + } + + public hideChildren() { + const entityType = this.entityType; + const unique = this.unique; + + if (!entityType) { + throw new Error('Could not show children, entity type is missing'); + } + + if (unique === undefined) { + throw new Error('Could not show children, unique is missing'); + } + + this.treeContext?.expansion.collapseItem({ entityType, unique }); + } + async #consumeContexts() { this.consumeContext(UMB_SECTION_CONTEXT, (instance) => { this.#sectionContext = instance; @@ -239,6 +284,7 @@ export abstract class UmbTreeItemContextBase< this.#observeIsSelectable(); this.#observeIsSelected(); this.#observeFoldersOnly(); + this.#observeExpansion(); }); this.consumeContext(UMB_TREE_ITEM_CONTEXT, (instance) => { @@ -339,6 +385,25 @@ export abstract class UmbTreeItemContextBase< ); } + #observeExpansion() { + if (this.unique === undefined) return; + if (!this.entityType) return; + if (!this.treeContext) return; + + this.observe( + this.treeContext.expansion.isExpanded({ entityType: this.entityType, unique: this.unique }), + (isExpanded) => { + // If this item has children, load them + if (isExpanded && this.#hasChildren.getValue() && this.#isOpen.getValue() === false) { + this.loadChildren(); + } + + this.#isOpen.setValue(isExpanded); + }, + 'observeExpansion', + ); + } + #onReloadRequest = (event: UmbEntityActionEvent) => { if (event.getUnique() !== this.unique) return; if (event.getEntityType() !== this.entityType) return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts index 8cee44081d..aa5814edfc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-base/tree-item-element-base.ts @@ -2,6 +2,7 @@ import type { UmbTreeItemContext } from '../index.js'; import type { UmbTreeItemModel } from '../../types.js'; import { html, nothing, state, ifDefined, repeat, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { UUIMenuItemEvent } from '@umbraco-cms/backoffice/external/uui'; export abstract class UmbTreeItemElementBase< TreeItemModelType extends UmbTreeItemModel, @@ -32,6 +33,7 @@ export abstract class UmbTreeItemElementBase< this.observe(this.#api.childItems, (value) => (this._childItems = value)); this.observe(this.#api.hasChildren, (value) => (this._hasChildren = value)); this.observe(this.#api.isActive, (value) => (this._isActive = value)); + this.observe(this.#api.isOpen, (value) => (this._isOpen = value)); this.observe(this.#api.isLoading, (value) => (this._isLoading = value)); this.observe(this.#api.isSelectableContext, (value) => (this._isSelectableContext = value)); this.observe(this.#api.isSelectable, (value) => (this._isSelectable = value)); @@ -70,6 +72,9 @@ export abstract class UmbTreeItemElementBase< @state() private _hasChildren = false; + @state() + private _isOpen = false; + @state() private _iconSlotHasChildren = false; @@ -95,9 +100,14 @@ export abstract class UmbTreeItemElementBase< this.#api?.deselect(); } - // TODO: do we want to catch and emit a backoffice event here? - private _onShowChildren() { - this.#api?.loadChildren(); + private _onShowChildren(event: UUIMenuItemEvent) { + event.stopPropagation(); + this.#api?.showChildren(); + } + + private _onHideChildren(event: UUIMenuItemEvent) { + event.stopPropagation(); + this.#api?.hideChildren(); } #onLoadMoreClick = (event: any) => { @@ -113,6 +123,7 @@ export abstract class UmbTreeItemElementBase< return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-context.interface.ts index d1829dc149..a9255fa94a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-item/tree-item-context.interface.ts @@ -14,6 +14,7 @@ export interface UmbTreeItemContext exten isSelectable: Observable; isSelected: Observable; isActive: Observable; + isOpen: Observable; hasActions: Observable; path: Observable; pagination: UmbPaginationManager; @@ -24,4 +25,7 @@ export interface UmbTreeItemContext exten select(): void; deselect(): void; constructPath(pathname: string, entityType: string, unique: string): string; + loadChildren(): void; + showChildren(): void; + hideChildren(): void; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts index a7b47cd0b6..4bceebff8f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/tree/tree-picker-modal/tree-picker-modal.element.ts @@ -182,6 +182,7 @@ export class UmbTreePickerModalElement extends UmbPickerModalData { hideTreeRoot?: boolean; + expandTreeRoot?: boolean; treeAlias?: string; // Consider if it makes sense to move this into the UmbPickerModalData interface, but for now this is a TreePicker feature. [NL] createAction?: UmbTreePickerModalCreateActionData; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/modal/duplicate-document-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/modal/duplicate-document-modal.element.ts index 55b95dd45f..27fe5f948c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/modal/duplicate-document-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/modal/duplicate-document-modal.element.ts @@ -46,7 +46,10 @@ export class UmbDocumentDuplicateToModalElement extends UmbModalBaseElement< return html` - + Date: Mon, 24 Mar 2025 14:59:10 +0100 Subject: [PATCH 09/19] Update 01_bug_report.yml (#18513) --- .github/ISSUE_TEMPLATE/01_bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml index 47f6fe038d..54cfaca84e 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -6,8 +6,8 @@ body: - type: input id: "version" attributes: - label: "Which Umbraco version are you using? (Please write the *exact* version, example: 10.1.0)" - description: "Use the help icon in the Umbraco backoffice to find the version you're using" + label: "Which Umbraco version are you using?" + description: "Please write the *exact* version, example: `10.1.0`. Use the help icon in the Umbraco backoffice to find the version you're using" validations: required: true - type: textarea From 4c679a5f4c1f0921216296e3f9a215e0f24dfd33 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 15:10:13 +0100 Subject: [PATCH 10/19] Update input-rich-media.element.ts (#18772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø --- .../components/input-rich-media/input-rich-media.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts index 848bde04eb..2d1bf10dcf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts @@ -1,6 +1,7 @@ import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js'; import type { UmbMediaItemModel, UmbCropModel, UmbMediaPickerPropertyValueEntry } from '../../types.js'; import type { UmbUploadableItem } from '@umbraco-cms/backoffice/dropzone'; +import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/constants.js'; import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { umbConfirmModal, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; @@ -13,7 +14,6 @@ import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbTreeStartNode } from '@umbraco-cms/backoffice/tree'; import { UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; import { UmbRepositoryItemsManager } from '@umbraco-cms/backoffice/repository'; -import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '@umbraco-cms/backoffice/media'; import '@umbraco-cms/backoffice/imaging'; From 34649db5c9a1d48c7a116899c2d7a5b240520e4a Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 15:46:34 +0100 Subject: [PATCH 11/19] Block Grid: Fix circular dependencies (#18782) * Update input-rich-media.element.ts * fix circular * fixes circular dependencies in the validation module * Update bind-to-validation.lit-directive.ts * fix workspace circular * Update error-viewer-modal.token.ts * move contexts next to component * fix import errors * import local components from one file * lint * import in property editor file * fix import * remove duplicate import --- .../block-grid-manager.context-token.ts | 0 .../block-grid-manager.context.ts | 2 +- .../block-grid-manager/constants.ts | 1 + .../block-grid/block-grid-manager/index.ts | 1 + ...grid-to-grid-block-copy-translator.test.ts | 2 +- .../block-grid-area-config-entry.element.ts | 2 -- .../block-grid-area-config-entry/index.ts | 1 - .../block-grid-area.element.ts | 4 +-- .../components/block-grid-area/index.ts | 1 - .../block-grid-areas-container.element.ts | 5 ++-- .../block-grid-areas-container/index.ts | 1 - .../block-grid-block-inline.element.ts | 6 ++-- .../block-grid-block-inline/index.ts | 1 - .../block-grid-block-unsupported.element.ts | 3 -- .../block-grid-block-unsupported/index.ts | 1 - .../block-grid-block.element.ts | 2 -- .../components/block-grid-block/index.ts | 1 - .../block-grid-entries.context-token.ts | 0 .../block-grid-entries.context.ts | 30 ++++++++++--------- .../block-grid-entries.element.ts | 5 ++-- .../block-grid-entries/constants.ts | 1 + .../components/block-grid-entries/index.ts | 1 - .../block-grid-entry.context-token.ts | 0 .../block-grid-entry.context.ts | 15 ++++++---- .../block-grid-entry.element.ts | 7 +---- .../components/block-grid-entry/constants.ts | 1 + .../components/block-grid-entry/index.ts | 1 - .../components/block-scale-handler/index.ts | 1 - .../block/block-grid/components/constants.ts | 1 + .../components/ref-grid-block/index.ts | 1 - .../packages/block/block-grid/constants.ts | 3 +- .../block/block-grid/context/constants.ts | 3 -- .../block/block-grid/context/index.ts | 2 -- .../src/packages/block/block-grid/index.ts | 1 - .../block/block-grid/local-components.ts | 8 +++++ ...itor-ui-block-grid-areas-config.element.ts | 2 +- ...ditor-ui-block-grid-column-span.element.ts | 6 ++-- .../property-editor-ui-block-grid.element.ts | 5 ++-- .../input-rich-media.element.ts | 2 +- 39 files changed, 59 insertions(+), 71 deletions(-) rename src/Umbraco.Web.UI.Client/src/packages/block/block-grid/{context => block-grid-manager}/block-grid-manager.context-token.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/block/block-grid/{context => block-grid-manager}/block-grid-manager.context.ts (99%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-areas-container/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-unsupported/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/index.ts rename src/Umbraco.Web.UI.Client/src/packages/block/block-grid/{context => components/block-grid-entries}/block-grid-entries.context-token.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/block/block-grid/{context => components/block-grid-entries}/block-grid-entries.context.ts (96%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/constants.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/index.ts rename src/Umbraco.Web.UI.Client/src/packages/block/block-grid/{context => components/block-grid-entry}/block-grid-entry.context-token.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/block/block-grid/{context => components/block-grid-entry}/block-grid-entry.context.ts (95%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/constants.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/ref-grid-block/index.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/block/block-grid/local-components.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context-token.ts rename to src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context-token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context.ts similarity index 99% rename from src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts rename to src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context.ts index 7fb16cd573..6b6a74b924 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/block-grid-manager.context.ts @@ -1,6 +1,6 @@ import type { UmbBlockGridLayoutModel, UmbBlockGridTypeModel } from '../types.js'; import type { UmbBlockGridWorkspaceOriginData } from '../index.js'; -import { UMB_BLOCK_GRID_DEFAULT_LAYOUT_STYLESHEET } from './constants.js'; +import { UMB_BLOCK_GRID_DEFAULT_LAYOUT_STYLESHEET } from '../context/constants.js'; import { appendToFrozenArray, pushAtToUniqueArray, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/constants.ts new file mode 100644 index 0000000000..ec1c2a3f28 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/constants.ts @@ -0,0 +1 @@ +export { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../block-grid-manager/block-grid-manager.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/index.ts new file mode 100644 index 0000000000..155af5e738 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/block-grid-manager/index.ts @@ -0,0 +1 @@ +export * from './block-grid-manager.context.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.test.ts index 6129436f82..48e584d2d2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.test.ts @@ -4,7 +4,7 @@ import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controlle import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS } from '../../../property-editors/constants.js'; import type { UmbBlockGridValueModel, UmbGridBlockClipboardEntryValueModel } from '../../../types.js'; import { UmbBlockGridToGridBlockClipboardCopyPropertyValueTranslator } from './block-grid-to-grid-block-copy-translator.js'; -import { UmbBlockGridManagerContext } from '../../../context/block-grid-manager.context.js'; +import { UmbBlockGridManagerContext } from '../../../block-grid-manager/block-grid-manager.context.js'; @customElement('test-controller-host') class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/block-grid-area-config-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/block-grid-area-config-entry.element.ts index 7f9d977ac2..ce094fbe31 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/block-grid-area-config-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/block-grid-area-config-entry.element.ts @@ -2,8 +2,6 @@ import { UmbBlockGridAreaConfigEntryContext } from './block-grid-area-config-ent import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { html, css, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; -import '../block-grid-block/index.js'; -import '../block-scale-handler/index.js'; /** * @element umb-block-area-config-entry diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/index.ts deleted file mode 100644 index 1b48acd409..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area-config-entry/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-grid-area-config-entry.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area/block-grid-area.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area/block-grid-area.element.ts index a12676647e..a0087d9d20 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area/block-grid-area.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area/block-grid-area.element.ts @@ -1,5 +1,5 @@ -import { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from '../../context/block-grid-entries.context-token.js'; -import { UmbBlockGridEntriesElement } from '../block-grid-entries/index.js'; +import { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from '../block-grid-entries/constants.js'; +import { UmbBlockGridEntriesElement } from '../block-grid-entries/block-grid-entries.element.js'; import { customElement } from '@umbraco-cms/backoffice/external/lit'; /** diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area/index.ts deleted file mode 100644 index c81f975a43..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-area/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-grid-area.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-areas-container/block-grid-areas-container.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-areas-container/block-grid-areas-container.element.ts index 1eb1befcbf..c99c89e157 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-areas-container/block-grid-areas-container.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-areas-container/block-grid-areas-container.element.ts @@ -1,10 +1,9 @@ import type { UmbBlockGridTypeAreaType } from '../../types.js'; -import { UMB_BLOCK_GRID_ENTRY_CONTEXT } from '../../context/index.js'; -import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../context/block-grid-manager.context-token.js'; +import { UMB_BLOCK_GRID_ENTRY_CONTEXT } from '../block-grid-entry/constants.js'; +import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../block-grid-manager/constants.js'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { css, customElement, html, repeat, state } from '@umbraco-cms/backoffice/external/lit'; -import '../block-grid-entries/index.js'; /** * @description * This element is used to render the block grid areas. diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-areas-container/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-areas-container/index.ts deleted file mode 100644 index 72d5cd24dc..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-areas-container/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-grid-areas-container.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts index d277b126de..ba4745337a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/block-grid-block-inline.element.ts @@ -1,11 +1,9 @@ -import { UMB_BLOCK_GRID_ENTRY_CONTEXT } from '../../context/block-grid-entry.context-token.js'; +import { UMB_BLOCK_GRID_ENTRY_CONTEXT } from '../block-grid-entry/constants.js'; import type { UmbBlockGridWorkspaceOriginData } from '../../workspace/block-grid-workspace.modal-token.js'; -import { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from '../../context/block-grid-entries.context-token.js'; +import { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from '../block-grid-entries/constants.js'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { css, customElement, html, nothing, property, state } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; -import '../block-grid-areas-container/index.js'; -import '../ref-grid-block/index.js'; import type { UmbBlockEditorCustomViewConfiguration } from '@umbraco-cms/backoffice/block-custom-view'; import { type UMB_BLOCK_WORKSPACE_CONTEXT, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/index.ts deleted file mode 100644 index 3654873d8d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-inline/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-grid-block-inline.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-unsupported/block-grid-block-unsupported.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-unsupported/block-grid-block-unsupported.element.ts index c4d9b6de84..7e37f1244d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-unsupported/block-grid-block-unsupported.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-unsupported/block-grid-block-unsupported.element.ts @@ -2,9 +2,6 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import '../block-grid-areas-container/index.js'; -import '../ref-grid-block/index.js'; - @customElement('umb-block-grid-block-unsupported') export class UmbBlockGridBlockUnsupportedElement extends UmbLitElement { override render() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-unsupported/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-unsupported/index.ts deleted file mode 100644 index 7ac6576c7d..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block-unsupported/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-grid-block-unsupported.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts index 389cdb5080..acbab8698e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/block-grid-block.element.ts @@ -4,8 +4,6 @@ import type { UmbBlockDataType } from '@umbraco-cms/backoffice/block'; import type { UmbBlockEditorCustomViewConfiguration } from '@umbraco-cms/backoffice/block-custom-view'; import '@umbraco-cms/backoffice/ufm'; -import '../block-grid-areas-container/index.js'; -import '../ref-grid-block/index.js'; @customElement('umb-block-grid-block') export class UmbBlockGridBlockElement extends UmbLitElement { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/index.ts deleted file mode 100644 index 8afd0511df..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-block/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-grid-block.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context-token.ts rename to src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context-token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts similarity index 96% rename from src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts rename to src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts index e410f894d0..67468a8ade 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entries.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.context.ts @@ -1,22 +1,19 @@ -import type { UmbBlockDataModel } from '../../block/index.js'; -import { UMB_BLOCK_CATALOGUE_MODAL, UmbBlockEntriesContext } from '../../block/index.js'; -import { - UMB_BLOCK_GRID_ENTRY_CONTEXT, - UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, - UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS, - UMB_BLOCK_GRID_WORKSPACE_MODAL, - type UmbBlockGridWorkspaceOriginData, -} from '../index.js'; import type { UmbBlockGridLayoutModel, UmbBlockGridTypeAreaType, UmbBlockGridTypeModel, UmbBlockGridValueModel, -} from '../types.js'; -import { forEachBlockLayoutEntryOf } from '../utils/index.js'; -import type { UmbBlockGridPropertyEditorConfig } from '../property-editors/block-grid-editor/types.js'; -import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js'; -import type { UmbBlockGridScalableContainerContext } from './block-grid-scale-manager/block-grid-scale-manager.controller.js'; +} from '../../types.js'; +import { forEachBlockLayoutEntryOf } from '../../utils/index.js'; +import type { UmbBlockGridPropertyEditorConfig } from '../../property-editors/block-grid-editor/types.js'; +import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../block-grid-manager/constants.js'; +import { UMB_BLOCK_GRID_WORKSPACE_MODAL, type UmbBlockGridWorkspaceOriginData } from '../../workspace/index.js'; +import type { UmbBlockGridScalableContainerContext } from '../../context/block-grid-scale-manager/block-grid-scale-manager.controller.js'; +import { UMB_BLOCK_GRID_ENTRY_CONTEXT } from '../block-grid-entry/constants.js'; +import { + UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, + UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS, +} from '../../property-editors/block-grid-editor/constants.js'; import { UmbArrayState, UmbBooleanState, @@ -32,6 +29,11 @@ import { UmbClipboardPastePropertyValueTranslatorValueResolver, } from '@umbraco-cms/backoffice/clipboard'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; +import { + UMB_BLOCK_CATALOGUE_MODAL, + UmbBlockEntriesContext, + type UmbBlockDataModel, +} from '@umbraco-cms/backoffice/block'; interface UmbBlockGridAreaTypeInvalidRuleType { groupKey?: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts index 776f189afb..290129dc4a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts @@ -1,6 +1,6 @@ -import { UmbBlockGridEntriesContext } from '../../context/block-grid-entries.context.js'; -import type { UmbBlockGridEntryElement } from '../block-grid-entry/index.js'; +import type { UmbBlockGridEntryElement } from '../block-grid-entry/block-grid-entry.element.js'; import type { UmbBlockGridLayoutModel } from '../../types.js'; +import { UmbBlockGridEntriesContext } from './block-grid-entries.context.js'; import { getAccumulatedValueOfIndex, getInterpolatedIndexOfPositionInWeightMap, @@ -9,7 +9,6 @@ import { import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { html, customElement, state, repeat, css, property, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import '../block-grid-entry/index.js'; import { UmbSorterController, type UmbSorterConfig, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/constants.ts new file mode 100644 index 0000000000..1bb1d47079 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/constants.ts @@ -0,0 +1 @@ +export { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from './block-grid-entries.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/index.ts deleted file mode 100644 index 1ccc1acaa8..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-grid-entries.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context-token.ts rename to src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context-token.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context.ts similarity index 95% rename from src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts rename to src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context.ts index de3b11d474..76e13b3ca0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/block-grid-entry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.context.ts @@ -1,12 +1,15 @@ -import { closestColumnSpanOption, forEachBlockLayoutEntryOf } from '../utils/index.js'; -import type { UmbBlockGridValueModel } from '../types.js'; -import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS } from '../constants.js'; -import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js'; -import { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from './block-grid-entries.context-token.js'; +import { closestColumnSpanOption, forEachBlockLayoutEntryOf } from '../../utils/index.js'; +import type { UmbBlockGridValueModel } from '../../types.js'; +import { + UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS, + UMB_BLOCK_GRID_PROPERTY_EDITOR_UI_ALIAS, +} from '../../constants.js'; +import { UMB_BLOCK_GRID_MANAGER_CONTEXT } from '../../block-grid-manager/block-grid-manager.context-token.js'; +import { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from '../block-grid-entries/block-grid-entries.context-token.js'; import { type UmbBlockGridScalableContext, UmbBlockGridScaleManager, -} from './block-grid-scale-manager/block-grid-scale-manager.controller.js'; +} from '../../context/block-grid-scale-manager/block-grid-scale-manager.controller.js'; import { UmbArrayState, UmbBooleanState, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts index 80807a87b1..03a7da86ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts @@ -1,16 +1,11 @@ -import { UmbBlockGridEntryContext } from '../../context/block-grid-entry.context.js'; import type { UmbBlockGridLayoutModel } from '../../types.js'; import { UMB_BLOCK_GRID } from '../../constants.js'; +import { UmbBlockGridEntryContext } from './block-grid-entry.context.js'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { html, css, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; import { stringOrStringArrayContains } from '@umbraco-cms/backoffice/utils'; - -import '../block-grid-block-inline/index.js'; -import '../block-grid-block-unsupported/index.js'; -import '../block-grid-block/index.js'; -import '../block-scale-handler/index.js'; import { UmbObserveValidationStateController } from '@umbraco-cms/backoffice/validation'; import { UmbDataPathBlockElementDataQuery } from '@umbraco-cms/backoffice/block'; import type { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/constants.ts new file mode 100644 index 0000000000..16b71ef819 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/constants.ts @@ -0,0 +1 @@ +export { UMB_BLOCK_GRID_ENTRY_CONTEXT } from './block-grid-entry.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/index.ts deleted file mode 100644 index 4b7b635ce7..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-grid-entry.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/index.ts deleted file mode 100644 index 3d6b5076ae..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-scale-handler/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './block-scale-handler.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/constants.ts index b05ba11d99..85bbbe7b56 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/constants.ts @@ -1,2 +1,3 @@ export { UMB_BLOCK_GRID_AREA_CONFIG_ENTRY_CONTEXT } from './block-grid-area-config-entry/block-grid-area-config-entry.context-token.js'; export * from './block-grid-area-config-entry/constants.js'; +export * from './block-grid-entries/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/ref-grid-block/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/ref-grid-block/index.ts deleted file mode 100644 index 192623405a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/ref-grid-block/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ref-grid-block.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/constants.ts index 1905a7d399..70dc5b769f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/constants.ts @@ -1,7 +1,8 @@ +export * from './block-grid-manager/constants.js'; +export * from './clipboard/constants.js'; export * from './components/constants.js'; export * from './context/constants.js'; export * from './property-editors/constants.js'; -export * from './clipboard/constants.js'; export const UMB_BLOCK_GRID_TYPE = 'block-grid-type'; export const UMB_BLOCK_GRID = 'block-grid'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/constants.ts index 0f3681aa05..32b3acc60d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/constants.ts @@ -1,4 +1 @@ export const UMB_BLOCK_GRID_DEFAULT_LAYOUT_STYLESHEET = '/umbraco/backoffice/css/umbraco-blockgridlayout.css'; -export { UMB_BLOCK_GRID_ENTRIES_CONTEXT } from './block-grid-entries.context-token.js'; -export { UMB_BLOCK_GRID_ENTRY_CONTEXT } from './block-grid-entry.context-token.js'; -export { UMB_BLOCK_GRID_MANAGER_CONTEXT } from './block-grid-manager.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/index.ts deleted file mode 100644 index f652a039d7..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/context/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './block-grid-entries.context-token.js'; -export * from './block-grid-entry.context-token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/index.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/index.ts index 79d559f5c7..7a20184c1a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/index.ts @@ -1,4 +1,3 @@ export * from './constants.js'; -export * from './context/index.js'; export * from './workspace/index.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/local-components.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/local-components.ts new file mode 100644 index 0000000000..eaaec9e3f2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/local-components.ts @@ -0,0 +1,8 @@ +import './components/block-grid-areas-container/block-grid-areas-container.element.js'; +import './components/block-grid-block-inline/block-grid-block-inline.element.js'; +import './components/block-grid-block-unsupported/block-grid-block-unsupported.element.js'; +import './components/block-grid-block/block-grid-block.element.js'; +import './components/block-grid-entries/block-grid-entries.element.js'; +import './components/block-grid-entry/block-grid-entry.element.js'; +import './components/block-scale-handler/block-scale-handler.element.js'; +import './components/ref-grid-block/ref-grid-block.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts index ebadbed801..93d2fd7b05 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-areas-config/property-editor-ui-block-grid-areas-config.element.ts @@ -11,7 +11,7 @@ import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; import { UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/router'; import { incrementString } from '@umbraco-cms/backoffice/utils'; -import '../../components/block-grid-area-config-entry/index.js'; +import '../../components/block-grid-area-config-entry/block-grid-area-config-entry.element.js'; @customElement('umb-property-editor-ui-block-grid-areas-config') export class UmbPropertyEditorUIBlockGridAreasConfigElement extends UmbLitElement diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-column-span/property-editor-ui-block-grid-column-span.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-column-span/property-editor-ui-block-grid-column-span.element.ts index 6cc9d81072..8e55eb2f67 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-column-span/property-editor-ui-block-grid-column-span.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-column-span/property-editor-ui-block-grid-column-span.element.ts @@ -1,8 +1,8 @@ import type { UmbBlockGridTypeColumnSpanOption } from '../../types.js'; import { html, customElement, property, css, state, repeat } from '@umbraco-cms/backoffice/external/lit'; -import { - type UmbPropertyEditorUiElement, - type UmbPropertyEditorConfigCollection, +import type { + UmbPropertyEditorUiElement, + UmbPropertyEditorConfigCollection, } from '@umbraco-cms/backoffice/property-editor'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts index 1079083b44..eaf8481af9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts @@ -1,4 +1,4 @@ -import { UmbBlockGridManagerContext } from '../../context/block-grid-manager.context.js'; +import { UmbBlockGridManagerContext } from '../../block-grid-manager/index.js'; import { UMB_BLOCK_GRID_PROPERTY_EDITOR_SCHEMA_ALIAS } from './constants.js'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { @@ -22,7 +22,8 @@ import type { UmbBlockTypeGroup } from '@umbraco-cms/backoffice/block-type'; import type { UmbBlockGridTypeModel, UmbBlockGridValueModel } from '@umbraco-cms/backoffice/block-grid'; import { debounceTime } from '@umbraco-cms/backoffice/external/rxjs'; -import '../../components/block-grid-entries/index.js'; +// TODO: consider moving the components to the property editor folder as they are only used here +import '../../local-components.js'; /** * @element umb-property-editor-ui-block-grid diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts index 2d1bf10dcf..4e74a0a6d6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts @@ -1,7 +1,7 @@ import { UMB_IMAGE_CROPPER_EDITOR_MODAL, UMB_MEDIA_PICKER_MODAL } from '../../modals/index.js'; import type { UmbMediaItemModel, UmbCropModel, UmbMediaPickerPropertyValueEntry } from '../../types.js'; -import type { UmbUploadableItem } from '@umbraco-cms/backoffice/dropzone'; import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../repository/constants.js'; +import type { UmbUploadableItem } from '@umbraco-cms/backoffice/dropzone'; import { css, customElement, html, nothing, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { umbConfirmModal, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; From 3be766fe2a2f60c64a9f786f0730766658177825 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 16:04:44 +0100 Subject: [PATCH 12/19] fix build --- .../src/packages/block/block-grid/components/constants.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/constants.ts index 85bbbe7b56..e558f49a32 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/constants.ts @@ -1,3 +1,4 @@ export { UMB_BLOCK_GRID_AREA_CONFIG_ENTRY_CONTEXT } from './block-grid-area-config-entry/block-grid-area-config-entry.context-token.js'; export * from './block-grid-area-config-entry/constants.js'; export * from './block-grid-entries/constants.js'; +export * from './block-grid-entry/constants.js'; From 25837e66fc27fa933dcb76ccab00063491c7d6b0 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Mar 2025 15:59:57 +0100 Subject: [PATCH 13/19] fix dropzone circular --- .../components/input-dropzone/input-dropzone.element.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts index c8de1fe129..e75f279822 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts @@ -1,6 +1,6 @@ -import { UmbDropzoneChangeEvent, UmbDropzoneManager, UmbDropzoneSubmittedEvent } from '../../index.js'; import type { UmbUploadableItem } from '../../types.js'; import { UmbFileDropzoneItemStatus } from '../../constants.js'; +import { UmbDropzoneManager } from '../../dropzone-manager.class.js'; import { css, customElement, @@ -18,6 +18,8 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { formatBytes } from '@umbraco-cms/backoffice/utils'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; +import { UmbDropzoneChangeEvent } from '../../dropzone-change.event.js'; +import { UmbDropzoneSubmittedEvent } from '../../dropzone-submitted.event.js'; /** * @element umb-input-dropzone From 80e092069b1b1ff22d576e90f7075c0f1d946c48 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Tue, 25 Mar 2025 11:52:27 +0000 Subject: [PATCH 14/19] Tiptap RTE: Statusbar extension type (#18789) --- src/Umbraco.Web.UI.Client/package-lock.json | 15 + src/Umbraco.Web.UI.Client/package.json | 1 + .../src/assets/lang/en.ts | 3 + .../src/external/tiptap/index.ts | 1 + .../mocks/data/data-type/data-type.data.ts | 3 + .../character-map-modal.element.ts | 4 + .../input-tiptap/input-tiptap.element.ts | 70 +++- .../input-tiptap/tiptap-statusbar.element.ts | 125 ++++++ .../input-tiptap/tiptap-toolbar.element.ts | 29 +- .../src/packages/tiptap/components/types.ts | 2 + .../extensions/core/word-count.tiptap-api.ts | 9 + .../packages/tiptap/extensions/manifests.ts | 15 +- .../element-path.tiptap-statusbar-element.ts | 88 ++++ .../tiptap/extensions/statusbar/manifests.ts | 25 ++ .../word-count.tiptap-statusbar-element.ts | 56 +++ .../extensions/tiptap-statusbar.extension.ts | 22 + .../src/packages/tiptap/extensions/types.ts | 1 + ...-tiptap-statusbar-configuration.element.ts | 395 ++++++++++++++++++ ...ui-tiptap-toolbar-configuration.element.ts | 12 +- .../tiptap-statusbar-configuration.context.ts | 171 ++++++++ .../tiptap-toolbar-configuration.context.ts | 4 +- .../property-editors/tiptap/manifests.ts | 38 +- .../property-editor-ui-tiptap.element.ts | 2 +- .../tiptap/property-editors/tiptap/types.ts | 19 +- 24 files changed, 1052 insertions(+), 58 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-statusbar.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/word-count.tiptap-api.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/element-path.tiptap-statusbar-element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/word-count.tiptap-statusbar-element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-statusbar.extension.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-statusbar-configuration.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-statusbar-configuration.context.ts diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 361b144d0c..d0ba73c6bb 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -13,6 +13,7 @@ ], "dependencies": { "@tiptap/core": "2.11.5", + "@tiptap/extension-character-count": "^2.11.5", "@tiptap/extension-image": "2.11.5", "@tiptap/extension-link": "2.11.5", "@tiptap/extension-placeholder": "2.11.5", @@ -2584,6 +2585,20 @@ "@tiptap/core": "^2.7.0" } }, + "node_modules/@tiptap/extension-character-count": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-character-count/-/extension-character-count-2.11.5.tgz", + "integrity": "sha512-Da2VGb7ClmKwXdQdQC2735qylYD8/MQAPA0skPEcHxcDTDuI8ibyIDnMPnczgS/hR5g0TYE2DQp/dkhJXeovkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, "node_modules/@tiptap/extension-code": { "version": "2.11.5", "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.11.5.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 73b930d3ae..f17919f040 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -203,6 +203,7 @@ }, "dependencies": { "@tiptap/core": "2.11.5", + "@tiptap/extension-character-count": "^2.11.5", "@tiptap/extension-image": "2.11.5", "@tiptap/extension-link": "2.11.5", "@tiptap/extension-placeholder": "2.11.5", diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 9f012e2b05..4f29dcb7f1 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2740,12 +2740,15 @@ export default { anchor_input: 'Enter an anchor ID', config_dimensions_description: 'Set the maximum width and height of the editor. This excludes the toolbar height.', config_extensions: 'Capabilities', + config_statusbar: 'Statusbar', config_toolbar: 'Toolbar', extGroup_formatting: 'Text formatting', extGroup_interactive: 'Interactive elements', extGroup_media: 'Embeds and media', extGroup_structure: 'Content structure', extGroup_unknown: 'Uncategorized', + statusbar_availableItems: 'Available statuses', + statusbar_availableItemsEmpty: 'There are no statusbar extensions to show', toobar_availableItems: 'Available actions', toobar_availableItemsEmpty: 'There are no toolbar extensions to show', toolbar_designer: 'Toolbar designer', diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts index 607acb91f8..9bb1aedd28 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -8,6 +8,7 @@ export { TextStyle } from '@tiptap/extension-text-style'; export { Blockquote } from '@tiptap/extension-blockquote'; export { Bold } from '@tiptap/extension-bold'; export { BulletList } from '@tiptap/extension-bullet-list'; +export { CharacterCount } from '@tiptap/extension-character-count'; export { Code } from '@tiptap/extension-code'; export { CodeBlock } from '@tiptap/extension-code-block'; export { Heading } from '@tiptap/extension-heading'; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 35e2968842..0567c880a5 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1035,6 +1035,7 @@ export const data: Array = [ 'Umb.Tiptap.TextDirection', 'Umb.Tiptap.TextIndent', 'Umb.Tiptap.Underline', + 'Umb.Tiptap.WordCount', ], }, { @@ -1061,6 +1062,7 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.TextAlignRight', ], ['Umb.Tiptap.Toolbar.Subscript', 'Umb.Tiptap.Toolbar.Superscript'], + ['Umb.Tiptap.Toolbar.CodeBlock'], [ 'Umb.Tiptap.Toolbar.CharacterMap', 'Umb.Tiptap.Toolbar.TextDirectionRtl', @@ -1078,6 +1080,7 @@ export const data: Array = [ ], ], }, + { alias: 'statusbar', value: [['Umb.Tiptap.Statusbar.ElementPath'], ['Umb.Tiptap.Statusbar.WordCount']] }, { alias: 'stylesheets', value: ['/rte-styles.css'] }, { alias: 'dimensions', value: { height: 500 } }, { alias: 'maxImageSize', value: 500 }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.element.ts index 4056251ee2..6f344a20f4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/character-map/character-map-modal.element.ts @@ -431,6 +431,10 @@ export class UmbCharacterMapModalElement extends UmbModalBaseElement< gap: var(--uui-size-space-4); } + uui-input { + align-items: baseline; + } + uui-scroll-container { height: 300px; width: calc(450px + var(--uui-size-layout-1)); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index 459e01e2ab..12078ec55a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -1,5 +1,5 @@ import type { UmbTiptapExtensionApi } from '../../extensions/types.js'; -import type { UmbTiptapToolbarValue } from '../types.js'; +import type { UmbTiptapStatusbarValue, UmbTiptapToolbarValue } from '../types.js'; import { css, customElement, html, map, property, state, unsafeCSS, when } from '@umbraco-cms/backoffice/external/lit'; import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; @@ -12,6 +12,7 @@ import type { Extensions } from '@umbraco-cms/backoffice/external/tiptap'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import './tiptap-toolbar.element.js'; +import './tiptap-statusbar.element.js'; const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials'; @@ -63,7 +64,10 @@ export class UmbInputTiptapElement extends UmbFormControlMixin = []; @state() - _toolbar: UmbTiptapToolbarValue = [[[]]]; + private _toolbar: UmbTiptapToolbarValue = [[[]]]; + + @state() + private _statusbar: UmbTiptapStatusbarValue = [[], []]; constructor() { super(); @@ -129,6 +133,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin('toolbar') ?? [[[]]]; + this._statusbar = this.configuration?.getValueByAlias('statusbar') ?? []; const tiptapExtensions: Extensions = []; @@ -165,21 +170,12 @@ export class UmbInputTiptapElement extends UmbFormControlMixin html`
`, - () => html` - ${this.#renderStyles()} - - - `, - )} + ${when(loading, () => html`
`)} + ${when(!loading, () => html`${this.#renderStyles()}${this.#renderToolbar()}`)}
+ ${when(!loading, () => this.#renderStatusbar())} `; } @@ -193,6 +189,32 @@ export class UmbInputTiptapElement extends UmbFormControlMixin + + `; + } + + #renderStatusbar() { + if (!this._statusbar.flat().length) return; + return html` + + + `; + } + static override readonly styles = [ css` :host { @@ -230,9 +252,6 @@ export class UmbInputTiptapElement extends UmbFormControlMixin; + + @property({ type: Boolean, reflect: true }) + readonly = false; + + @property({ attribute: false }) + editor?: Editor; + + @property({ attribute: false }) + configuration?: UmbPropertyEditorConfigCollection; + + @property({ attribute: false }) + public set statusbar(value: UmbTiptapStatusbarValue) { + if (typeof value === 'string') { + value = [[], [value]]; + } else if (Array.isArray(value) && value.length === 1) { + value = [[], value[0]]; + } + + this.#statusbar = value; + } + public get statusbar(): UmbTiptapStatusbarValue { + return this.#statusbar; + } + #statusbar: UmbTiptapStatusbarValue = [[], []]; + + override connectedCallback() { + super.connectedCallback(); + this.#attached = true; + this.#observeExtensions(); + } + + override disconnectedCallback() { + this.#attached = false; + this.#extensionsController?.destroy(); + this.#extensionsController = undefined; + super.disconnectedCallback(); + } + + #observeExtensions() { + if (!this.#attached) return; + this.#extensionsController?.destroy(); + + this.#extensionsController = new UmbExtensionsElementInitializer( + this, + umbExtensionsRegistry, + 'tiptapStatusbarExtension', + (manifest) => this.statusbar.flat().includes(manifest.alias), + (extensionControllers) => { + this._lookup = new Map( + extensionControllers.map((ext) => { + (ext.component as HTMLElement)?.setAttribute('data-mark', `action:tiptap-statusbar:${ext.alias}`); + return [ext.alias, ext.component]; + }), + ); + }, + ); + + this.#extensionsController.properties = { editor: this.editor, configuration: this.configuration }; + } + + override render() { + if (!this.statusbar.flat().length) return nothing; + return map( + this.statusbar, + (area) => html`
${map(area, (alias) => this._lookup?.get(alias) ?? nothing)}
`, + ); + } + + static override readonly styles = css` + :host([readonly]) { + display: none; + } + + :host { + --uui-button-height: var(--uui-size-layout-2); + + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + + border-radius: var(--uui-border-radius); + border: 1px solid var(--uui-color-border); + border-top-left-radius: 0; + border-top-right-radius: 0; + border-top: 0; + + min-height: var(--uui-size-layout-1); + max-height: var(--uui-size-layout-2); + + padding: 0 var(--uui-size-3); + + > p { + margin: 0; + } + + .area { + display: inline-flex; + flex-wrap: wrap; + align-items: stretch; + } + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-statusbar': UmbTiptapStatusbarElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts index 8338b7bf43..33fc3116cf 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts @@ -30,11 +30,10 @@ export class UmbTiptapToolbarElement extends UmbLitElement { override connectedCallback(): void { super.connectedCallback(); - this.setAttribute('data-mark', 'tiptap-toolbar'); - this.#attached = true; this.#observeExtensions(); } + override disconnectedCallback(): void { this.#attached = false; this.#extensionsController?.destroy(); @@ -70,19 +69,19 @@ export class UmbTiptapToolbarElement extends UmbLitElement { } override render() { - return html` - ${map( - this.toolbar, - (row) => html` -
- ${map( - row, - (group) => html`
${map(group, (alias) => this._lookup?.get(alias) ?? nothing)}
`, - )} -
- `, - )} - `; + if (!this.toolbar.flat(2).length) return nothing; + + return map( + this.toolbar, + (row) => html` +
+ ${map( + row, + (group) => html`
${map(group, (alias) => this._lookup?.get(alias) ?? nothing)}
`, + )} +
+ `, + ); } static override readonly styles = css` diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/types.ts index 5167150c48..51ee93a45f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/types.ts @@ -1 +1,3 @@ export type UmbTiptapToolbarValue = Array>>; + +export type UmbTiptapStatusbarValue = Array>; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/word-count.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/word-count.tiptap-api.ts new file mode 100644 index 0000000000..6ff0bbcde8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/word-count.tiptap-api.ts @@ -0,0 +1,9 @@ +import { UmbTiptapExtensionApiBase } from '../base.js'; +import { css } from '@umbraco-cms/backoffice/external/lit'; +import { CharacterCount } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapWordCountExtensionApi extends UmbTiptapExtensionApiBase { + getTiptapExtensions = () => [CharacterCount]; + + override getStyles = () => css``; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 57b67924fe..cb3b5c79ea 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -1,6 +1,7 @@ import { manifests as blockExtensions } from './block/manifests.js'; import { manifests as styleSelectExtensions } from './style-select/manifests.js'; import { manifests as tableExtensions } from './table/manifests.js'; +import { manifests as statusbarExtensions } from './statusbar/manifests.js'; import type { ManifestTiptapExtension } from './tiptap.extension.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; @@ -167,11 +168,22 @@ const coreExtensions: Array = [ name: 'Text Indent Tiptap Extension', api: () => import('./core/text-indent.tiptap-api.js'), meta: { - icon: 'icon-science', + icon: 'icon-indent', label: 'Text Indent', group: '#tiptap_extGroup_formatting', }, }, + { + type: 'tiptapExtension', + alias: 'Umb.Tiptap.WordCount', + name: 'Word Count Tiptap Extension', + api: () => import('./core/word-count.tiptap-api.js'), + meta: { + icon: 'icon-speed-gauge', + label: 'Word Count', + group: '#tiptap_extGroup_interactive', + }, + }, ]; const toolbarExtensions: Array = [ @@ -648,6 +660,7 @@ const toolbarExtensions: Array = [ export const manifests = [ ...kinds, ...coreExtensions, + ...statusbarExtensions, ...toolbarExtensions, ...blockExtensions, ...styleSelectExtensions, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/element-path.tiptap-statusbar-element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/element-path.tiptap-statusbar-element.ts new file mode 100644 index 0000000000..181e5c2267 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/element-path.tiptap-statusbar-element.ts @@ -0,0 +1,88 @@ +import { css, customElement, html, map, nothing, state } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +@customElement('umb-tiptap-statusbar-element-path') +export class UmbTiptapStatusbarElementPathElement extends UmbLitElement { + @state() + private _path?: Array; + + public editor?: Editor; + + override connectedCallback() { + super.connectedCallback(); + + if (this.editor) { + this.editor.on('selectionUpdate', this.#onEditorSelectionUpdate); + this.#onEditorSelectionUpdate(); + } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + + if (this.editor) { + this.editor.off('selectionUpdate', this.#onEditorSelectionUpdate); + } + } + + readonly #onEditorSelectionUpdate = () => { + let dom = this.editor?.view.domAtPos(this.editor!.state.selection.from).node; + + this._path = []; + + while (dom) { + if (!this.editor?.view.dom.contains(dom)) break; + + if (dom.nodeType === dom.ELEMENT_NODE && dom instanceof HTMLElement) { + let tagName = dom.nodeName.toLocaleLowerCase(); + + if (dom.id) { + tagName += `#${dom.id}`; + } + + if (dom.classList.length) { + tagName += `${['', ...dom.classList].join('.')}`; + } + + this._path.push(tagName); + } + + if (!dom.parentElement) break; + + dom = dom.parentElement; + } + + this._path.reverse().shift(); + }; + + override render() { + if (!this._path) return nothing; + return map(this._path, (item) => html`${item}`); + } + + static override readonly styles = [ + css` + :host { + display: flex; + gap: 0.5rem; + + font-size: var(--uui-type-small-size); + color: var(--uui-color-text-alt); + } + + code:not(:last-of-type)::after { + content: '>'; + margin-left: 0.5rem; + } + `, + ]; +} + +export { UmbTiptapStatusbarElementPathElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-statusbar-element-path': UmbTiptapStatusbarElementPathElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/manifests.ts new file mode 100644 index 0000000000..40825436c9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/manifests.ts @@ -0,0 +1,25 @@ +export const manifests: Array = [ + { + type: 'tiptapStatusbarExtension', + alias: 'Umb.Tiptap.Statusbar.WordCount', + name: 'Word Count Tiptap Statusbar Extension', + element: () => import('./word-count.tiptap-statusbar-element.js'), + forExtensions: ['Umb.Tiptap.WordCount'], + meta: { + alias: 'wordCount', + icon: 'icon-speed-gauge', + label: 'Word Count', + }, + }, + { + type: 'tiptapStatusbarExtension', + alias: 'Umb.Tiptap.Statusbar.ElementPath', + name: 'Element Path Tiptap Statusbar Extension', + element: () => import('./element-path.tiptap-statusbar-element.js'), + meta: { + alias: 'elementPath', + icon: 'icon-map-alt', + label: 'Element Path', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/word-count.tiptap-statusbar-element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/word-count.tiptap-statusbar-element.ts new file mode 100644 index 0000000000..f18a268ce3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/statusbar/word-count.tiptap-statusbar-element.ts @@ -0,0 +1,56 @@ +import { customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-tiptap-statusbar-word-count') +export class UmbTiptapStatusbarWordCountElement extends UmbLitElement { + @state() + private _characters = 0; + + @state() + private _words = 0; + + @state() + private _showCharacters = false; + + public editor?: Editor; + + override connectedCallback() { + super.connectedCallback(); + + if (this.editor) { + this.editor.on('update', this.#onEditorUpdate); + this.#onEditorUpdate(); + } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + + if (this.editor) { + this.editor.off('update', this.#onEditorUpdate); + } + } + + readonly #onEditorUpdate = () => { + this._characters = this.editor?.storage.characterCount.characters() ?? 0; + this._words = this.editor?.storage.characterCount.words() ?? 0; + }; + + readonly #onClick = () => (this._showCharacters = !this._showCharacters); + + override render() { + const label = this._showCharacters + ? this._characters.toLocaleString() + ' ' + (this._characters === 1 ? 'character' : 'characters') + : this._words.toLocaleString() + ' ' + (this._words === 1 ? 'word' : 'words'); + return html``; + } +} + +export { UmbTiptapStatusbarWordCountElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-statusbar-word-count': UmbTiptapStatusbarWordCountElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-statusbar.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-statusbar.extension.ts new file mode 100644 index 0000000000..b3dd8a9311 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-statusbar.extension.ts @@ -0,0 +1,22 @@ +import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; +import type { ManifestElement } from '@umbraco-cms/backoffice/extension-api'; + +export interface ManifestTiptapStatusbarExtension< + MetaType extends MetaTiptapStatusbarExtension = MetaTiptapStatusbarExtension, +> extends ManifestElement { + type: 'tiptapStatusbarExtension'; + forExtensions?: Array; + meta: MetaType; +} + +export interface MetaTiptapStatusbarExtension { + alias: string; + icon: string; + label: string; +} + +declare global { + interface UmbExtensionManifestMap { + umbTiptapStatusbarExtension: ManifestTiptapStatusbarExtension; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts index 7177fcd940..7b2f40033c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts @@ -6,6 +6,7 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; export type * from './tiptap.extension.js'; +export type * from './tiptap-statusbar.extension.js'; export type * from './tiptap-toolbar.extension.js'; export interface UmbTiptapExtensionApi extends UmbApi { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-statusbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-statusbar-configuration.element.ts new file mode 100644 index 0000000000..14a7be8f35 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-statusbar-configuration.element.ts @@ -0,0 +1,395 @@ +import { UmbTiptapStatusbarConfigurationContext } from '../contexts/tiptap-statusbar-configuration.context.js'; +import type { UmbTiptapStatusbarValue } from '../../../components/types.js'; +import type { UmbTiptapStatusbarViewModel, UmbTiptapStatusbarExtension } from '../types.js'; +import { customElement, css, html, property, when, repeat, nothing, state } from '@umbraco-cms/backoffice/external/lit'; +import { debounce } from '@umbraco-cms/backoffice/utils'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; + +@customElement('umb-property-editor-ui-tiptap-statusbar-configuration') +export class UmbPropertyEditorUiTiptapStatusbarConfigurationElement + extends UmbLitElement + implements UmbPropertyEditorUiElement +{ + #context = new UmbTiptapStatusbarConfigurationContext(this); + + #currentDragItem?: { alias: string; fromPos?: [number, number] }; + + #debouncedFilter = debounce((query: string) => { + this._availableExtensions = this.#context.filterExtensions(query); + }, 250); + + @state() + private _availableExtensions: Array = []; + + @state() + private _statusbar: Array = []; + + @property({ attribute: false }) + set value(value: UmbTiptapStatusbarValue | undefined) { + if (!value) value = [[], []]; + if (value === this.#value) return; + this.#value = value; + } + get value(): UmbTiptapStatusbarValue | undefined { + return this.#context.cloneStatusbarValue(this.#value); + } + #value?: UmbTiptapStatusbarValue; + + constructor() { + super(); + + this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => { + this.observe(this.#context.extensions, (extensions) => { + this._availableExtensions = extensions; + }); + + this.observe(this.#context.reload, (reload) => { + if (reload) { + this.requestUpdate(); + } + }); + + this.observe(this.#context.statusbar, (statusbar) => { + if (!statusbar.length) return; + this._statusbar = statusbar; + this.#value = statusbar.map((area) => [...area.data]); + propertyContext.setValue(this.#value); + }); + }); + } + + protected override firstUpdated() { + this.#context.setStatusbar(this.#value); + } + + #onClick(item: UmbTiptapStatusbarExtension) { + const lastArea = (this.#value?.length ?? 1) - 1; + const lastItem = this.#value?.[lastArea].length ?? 0; + this.#context.insertStatusbarItem(item.alias, [lastArea, lastItem]); + } + + #onDragStart(event: DragEvent, alias: string, fromPos?: [number, number]) { + event.dataTransfer!.effectAllowed = 'move'; + this.#currentDragItem = { alias, fromPos }; + } + + #onDragOver(event: DragEvent) { + event.preventDefault(); + event.dataTransfer!.dropEffect = 'move'; + } + + #onDragEnd(event: DragEvent) { + event.preventDefault(); + if (event.dataTransfer?.dropEffect === 'none') { + const { fromPos } = this.#currentDragItem ?? {}; + if (!fromPos) return; + + this.#context.removeStatusbarItem(fromPos); + } + } + + #onDrop(event: DragEvent, toPos?: [number, number]) { + event.preventDefault(); + const { alias, fromPos } = this.#currentDragItem ?? {}; + + // Remove item if no destination position is provided + if (fromPos && !toPos) { + this.#context.removeStatusbarItem(fromPos); + return; + } + + // Move item if both source and destination positions are available + if (fromPos && toPos) { + this.#context.moveStatusbarItem(fromPos, toPos); + return; + } + + // Insert item if an alias and a destination position are provided + if (alias && toPos) { + this.#context.insertStatusbarItem(alias, toPos); + } + } + + #onFilterInput(event: InputEvent & { target: HTMLInputElement }) { + const query = (event.target.value ?? '').toLocaleLowerCase(); + this.#debouncedFilter(query); + } + + override render() { + return html`${this.#renderDesigner()} ${this.#renderAvailableItems()}`; + } + + #renderAvailableItems() { + return html` + +
+ +
+ +
+
+
+ +
+ ${when( + this._availableExtensions.length === 0, + () => + html`There are no statusbar extensions to show`, + () => repeat(this._availableExtensions, (item) => this.#renderAvailableItem(item)), + )} +
+
+
+ `; + } + + #renderAvailableItem(item: UmbTiptapStatusbarExtension) { + const forbidden = !this.#context.isExtensionEnabled(item.alias); + const inUse = this.#context.isExtensionInUse(item.alias); + if (inUse || forbidden) return nothing; + return html` + this.#onClick(item)} + @dragstart=${(e: DragEvent) => this.#onDragStart(e, item.alias)} + @dragend=${this.#onDragEnd}> +
+ ${when(item.icon, () => html``)} + ${this.localize.string(item.label)} +
+
+ `; + } + + #renderDesigner() { + return html` +
+
+ ${repeat( + this._statusbar, + (area) => area.unique, + (area, idx) => this.#renderArea(area, idx), + )} +
+
+ `; + } + + #renderArea(area?: UmbTiptapStatusbarViewModel, areaIndex = 0) { + if (!area) return nothing; + return html` +
this.#onDrop(e, [areaIndex, area.data.length - 1])}> +
+ ${when( + area?.data.length === 0, + () => html`Empty`, + () => html`${area!.data.map((alias, idx) => this.#renderItem(alias, areaIndex, idx))}`, + )} +
+
+ `; + } + + #renderItem(alias: string, areaIndex = 0, itemIndex = 0) { + const item = this.#context?.getExtensionByAlias(alias); + if (!item) return nothing; + + const forbidden = !this.#context?.isExtensionEnabled(item.alias); + + return html` + this.#context.removeStatusbarItem([areaIndex, itemIndex])} + @dragend=${this.#onDragEnd} + @dragstart=${(e: DragEvent) => this.#onDragStart(e, alias, [areaIndex, itemIndex])}> +
+ ${when(item.icon, (icon) => html``)} + ${this.localize.string(item.label)} +
+
+ `; + } + + static override readonly styles = [ + css` + :host { + display: flex; + flex-direction: column; + gap: var(--uui-size-space-4); + flex-grow: 1; + } + + @media (min-width: 1400px) { + :host { + flex-direction: row; + } + #toolbox { + width: 500px; + max-width: 33%; + flex-grow: 1; + } + + #statusbar { + flex-grow: 100; + } + } + + #toolbox { + border: 1px solid var(--uui-color-border); + } + + uui-box { + [slot='header-actions'] { + margin-bottom: var(--uui-size-2); + + uui-input { + align-items: baseline; + } + + uui-icon { + color: var(--uui-color-border); + } + } + } + + uui-scroll-container { + max-height: 350px; + } + + .available-items { + display: flex; + flex-wrap: wrap; + gap: var(--uui-size-3); + + uui-button { + --uui-button-font-weight: normal; + + &[draggable='true'], + &[draggable='true'] > .inner { + cursor: move; + } + + &[disabled], + &[disabled] > .inner { + cursor: not-allowed; + } + + &.forbidden { + --color: var(--uui-color-danger); + --color-standalone: var(--uui-color-danger-standalone); + --color-emphasis: var(--uui-color-danger-emphasis); + --color-contrast: var(--uui-color-danger); + --uui-button-contrast-disabled: var(--uui-color-danger); + --uui-button-border-color-disabled: var(--uui-color-danger); + } + + div { + display: flex; + gap: var(--uui-size-1); + } + } + } + + #areas { + display: flex; + gap: var(--uui-size-1); + justify-content: space-between; + align-items: center; + + .area { + flex: 1; + display: flex; + align-items: flex-start; + justify-content: space-between; + + border: 1px dashed transparent; + padding: var(--uui-size-3); + + &:last-child { + justify-content: flex-end; + } + + &:focus-within, + &:hover { + border-color: var(--uui-color-border-standalone); + } + + .items { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: var(--uui-size-1); + + uui-button { + --uui-button-font-weight: normal; + + &[draggable='true'], + &[draggable='true'] > .inner { + cursor: move; + } + + &[disabled], + &[disabled] > .inner { + cursor: not-allowed; + } + + &.forbidden { + --color: var(--uui-color-danger); + --color-standalone: var(--uui-color-danger-standalone); + --color-emphasis: var(--uui-color-danger-emphasis); + --color-contrast: var(--uui-color-danger); + --uui-button-contrast-disabled: var(--uui-color-danger); + --uui-button-border-color-disabled: var(--uui-color-danger); + } + + div { + display: flex; + gap: var(--uui-size-1); + } + } + } + } + } + + #btnAddRow { + display: block; + margin-top: var(--uui-size-1); + } + + .handle { + cursor: move; + } + `, + ]; +} + +export { UmbPropertyEditorUiTiptapStatusbarConfigurationElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-tiptap-statusbar-configuration': UmbPropertyEditorUiTiptapStatusbarConfigurationElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts index 159a040f77..820ff25915 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/components/property-editor-ui-tiptap-toolbar-configuration.element.ts @@ -10,7 +10,6 @@ import { debounce } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; import type { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; @customElement('umb-property-editor-ui-tiptap-toolbar-configuration') export class UmbPropertyEditorUiTiptapToolbarConfigurationElement @@ -325,7 +324,6 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement } static override readonly styles = [ - UmbTextStyles, css` :host { display: flex; @@ -353,14 +351,14 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement border: 1px solid var(--uui-color-border); } - uui-box.minimal { - --uui-box-header-padding: 0; - --uui-box-default-padding: var(--uui-size-2) 0; - --uui-box-box-shadow: none; - + uui-box { [slot='header-actions'] { margin-bottom: var(--uui-size-2); + uui-input { + align-items: baseline; + } + uui-icon { color: var(--uui-color-border); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-statusbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-statusbar-configuration.context.ts new file mode 100644 index 0000000000..90ccc26f80 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-statusbar-configuration.context.ts @@ -0,0 +1,171 @@ +import type { UmbTiptapStatusbarExtension, UmbTiptapStatusbarViewModel } from '../types.js'; +import type { UmbTiptapStatusbarValue } from '../../../components/types.js'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbId } from '@umbraco-cms/backoffice/id'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export class UmbTiptapStatusbarConfigurationContext extends UmbContextBase { + #extensions = new UmbArrayState([], (x) => x.alias); + public readonly extensions = this.#extensions.asObservable(); + + #reload = new UmbBooleanState(false); + public readonly reload = this.#reload.asObservable(); + + #extensionsEnabled = new Set(); + + #extensionsInUse = new Set(); + + #lookup?: Map; + + #statusbar = new UmbArrayState([], (x) => x.unique); + public readonly statusbar = this.#statusbar.asObservable(); + + constructor(host: UmbControllerHost) { + super(host, 'UmbTiptapStatusbarConfigurationContext'); + + this.observe(umbExtensionsRegistry.byType('tiptapStatusbarExtension'), (extensions) => { + const _extensions = extensions + .sort((a, b) => a.alias.localeCompare(b.alias)) + .map((ext) => ({ + alias: ext.alias, + label: ext.meta.label, + icon: ext.meta.icon, + dependencies: ext.forExtensions, + })); + + this.#extensions.setValue(_extensions); + + this.#lookup = new Map(_extensions.map((ext) => [ext.alias, ext])); + }); + + this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, async (dataset) => { + this.observe( + await dataset.propertyValueByAlias>('extensions'), + (extensions) => { + if (extensions) { + this.#extensionsEnabled.clear(); + this.#reload.setValue(false); + + this.#extensions + .getValue() + .filter((x) => !x.dependencies || x.dependencies.every((z) => extensions.includes(z))) + .map((x) => x.alias) + .forEach((alias) => this.#extensionsEnabled.add(alias)); + + this.#reload.setValue(true); + } + }, + '_observeExtensions', + ); + }); + } + + public cloneStatusbarValue(value?: UmbTiptapStatusbarValue | null): UmbTiptapStatusbarValue { + if (!this.isValidStatusbarValue(value)) return [[], []]; + return value.map((area) => [...area]); + } + + public filterExtensions(query: string): Array { + return this.#extensions + .getValue() + .filter((ext) => ext.alias?.toLowerCase().includes(query) || ext.label?.toLowerCase().includes(query)); + } + + public getExtensionByAlias(alias: string): UmbTiptapStatusbarExtension | undefined { + return this.#lookup?.get(alias); + } + + public isExtensionEnabled(alias: string): boolean { + return this.#extensionsEnabled.has(alias); + } + + public isExtensionInUse(alias: string): boolean { + return this.#extensionsInUse.has(alias); + } + + public isValidStatusbarValue(value: unknown): value is UmbTiptapStatusbarValue { + if (!Array.isArray(value)) return false; + for (const area of value) { + if (!Array.isArray(area)) return false; + for (const alias of area) { + if (typeof alias !== 'string') return false; + } + } + return true; + } + + public insertStatusbarItem(alias: string, to: [number, number]) { + const statusbar = [...this.#statusbar.getValue()]; + + const [areaIndex, itemIndex] = to; + + const area = statusbar[areaIndex]; + const items = [...area.data]; + + items.splice(itemIndex, 0, alias); + this.#extensionsInUse.add(alias); + + statusbar[areaIndex] = { unique: area.unique, data: items }; + + this.#statusbar.setValue(statusbar); + } + + public moveStatusbarItem(from: [number, number], to: [number, number]) { + const [fromAreaIndex, fromItemIndex] = from; + const [toAreaIndex, toItemIndex] = to; + + const statusbar = [...this.#statusbar.getValue()]; + + const fromArea = statusbar[fromAreaIndex]; + const fromItems = [...fromArea.data]; + + const toBeMoved = fromItems.splice(fromItemIndex, 1); + + statusbar[fromAreaIndex] = { unique: fromArea.unique, data: fromItems }; + + const toArea = statusbar[toAreaIndex]; + const toItems = [...toArea.data]; + + toItems.splice(toItemIndex, 0, toBeMoved[0]); + + statusbar[toAreaIndex] = { unique: toArea.unique, data: toItems }; + + this.#statusbar.setValue(statusbar); + } + + public removeStatusbarItem(from: [number, number]) { + const [areaIndex, itemIndex] = from; + + const statusbar = [...this.#statusbar.getValue()]; + + const area = statusbar[areaIndex]; + const items = [...area.data]; + + const removed = items.splice(itemIndex, 1); + removed.forEach((alias) => this.#extensionsInUse.delete(alias)); + + statusbar[areaIndex] = { unique: area.unique, data: items }; + + this.#statusbar.setValue(statusbar); + } + + public setStatusbar(value?: UmbTiptapStatusbarValue | null) { + if (!this.isValidStatusbarValue(value)) { + value = [[], []]; + } + + if (value.length === 1) { + value = [[], value[0]]; + } + + this.#extensionsInUse.clear(); + value.forEach((area) => area.forEach((alias) => this.#extensionsInUse.add(alias))); + + const statusbar = value.map((area) => ({ unique: UmbId.new(), data: area })); + + this.#statusbar.setValue(statusbar); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts index 2b8b19e043..a068ee7162 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts @@ -54,8 +54,8 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase = [ { alias: 'extensions', label: '#tiptap_config_extensions', - description: `Choose which Tiptap extensions to enable + description: `Choose which Tiptap extensions to enable. -_Once enabled, the related actions will be available for the toolbar._`, +_Once enabled, the related actions will be available for the toolbar and statusbar._`, propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ExtensionsConfiguration', weight: 10, }, { alias: 'toolbar', label: '#tiptap_config_toolbar', - description: `Design the available actions + description: `Design the available actions. _Drag and drop the available actions onto the toolbar._`, propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration', weight: 15, }, + { + alias: 'statusbar', + label: '#tiptap_config_statusbar', + description: `Design the available statuses. + +_Drag and drop the available actions onto the statusbar areas._`, + propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.StatusbarConfiguration', + weight: 18, + }, { alias: 'stylesheets', label: '#treeHeaders_stylesheets', - description: 'Pick the stylesheets whose editor styles should be available when editing!!!', + description: 'Pick the stylesheets whose editor styles should be available when editing.', propertyEditorUiAlias: 'Umb.PropertyEditorUi.StylesheetPicker', weight: 20, }, @@ -85,11 +94,22 @@ _Drag and drop the available actions onto the toolbar._`, }, }, }, + { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.Tiptap.ExtensionsConfiguration', + name: 'Tiptap Extensions Property Editor UI', + element: () => import('./components/property-editor-ui-tiptap-extensions-configuration.element.js'), + meta: { + label: 'Tiptap Extensions Configuration', + icon: 'icon-autofill', + group: 'common', + }, + }, { type: 'propertyEditorUi', alias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration', name: 'Tiptap Toolbar Property Editor UI', - js: () => import('./components/property-editor-ui-tiptap-toolbar-configuration.element.js'), + element: () => import('./components/property-editor-ui-tiptap-toolbar-configuration.element.js'), meta: { label: 'Tiptap Toolbar Configuration', icon: 'icon-autofill', @@ -98,11 +118,11 @@ _Drag and drop the available actions onto the toolbar._`, }, { type: 'propertyEditorUi', - alias: 'Umb.PropertyEditorUi.Tiptap.ExtensionsConfiguration', - name: 'Tiptap Extensions Property Editor UI', - js: () => import('./components/property-editor-ui-tiptap-extensions-configuration.element.js'), + alias: 'Umb.PropertyEditorUi.Tiptap.StatusbarConfiguration', + name: 'Tiptap Statusbar Property Editor UI', + element: () => import('./components/property-editor-ui-tiptap-statusbar-configuration.element.js'), meta: { - label: 'Tiptap Extensions Configuration', + label: 'Tiptap Statusbar Configuration', icon: 'icon-autofill', group: 'common', }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts index 3323f33f06..fb28b8a1ae 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts @@ -67,10 +67,10 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem return html` `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts index 448accfa2c..c7c80b8c23 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/types.ts @@ -1,11 +1,20 @@ -export type UmbTiptapToolbarExtension = { - kind?: string; +export type UmbTiptapSortableViewModel = { unique: string; data: T }; + +export type UmbTiptapStatusbarExtension = { alias: string; label: string; icon: string; dependencies?: Array; }; -export type UmbTiptapToolbarSortableViewModel = { unique: string; data: T }; -export type UmbTiptapToolbarRowViewModel = UmbTiptapToolbarSortableViewModel>; -export type UmbTiptapToolbarGroupViewModel = UmbTiptapToolbarSortableViewModel>; +export type UmbTiptapStatusbarViewModel = UmbTiptapSortableViewModel>; + +export type UmbTiptapToolbarExtension = UmbTiptapStatusbarExtension & { + kind?: string; +}; + +export type UmbTiptapToolbarRowViewModel = UmbTiptapSortableViewModel>; +export type UmbTiptapToolbarGroupViewModel = UmbTiptapSortableViewModel>; + +/** @deprecated No longer used internally. This will be removed in Umbraco 17. [LK] */ +export type UmbTiptapToolbarSortableViewModel = UmbTiptapSortableViewModel; From 2711ac07ac323a9ac37923435f60da71aa1e883e Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 25 Mar 2025 12:58:01 +0100 Subject: [PATCH 15/19] Only validate invariant properties when strictly necessary (#18729) --- .../Services/PropertyValidationService.cs | 39 +++++++++ ...stElementLevelVariationTests.Validation.cs | 63 ++++++++++++--- .../ContentPublishingServiceTests.Publish.cs | 79 +++++++++++++++++++ .../Services/ContentPublishingServiceTests.cs | 42 +++++++++- .../Umbraco.Core/Models/VariationTests.cs | 6 +- .../PropertyValidationServiceTests.cs | 11 ++- 6 files changed, 225 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index 1bed040ba1..d1cc509110 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.Models; @@ -17,6 +19,8 @@ public class PropertyValidationService : IPropertyValidationService private readonly PropertyEditorCollection _propertyEditors; private readonly IValueEditorCache _valueEditorCache; private readonly ICultureDictionary _cultureDictionary; + private readonly ILanguageService _languageService; + private readonly ContentSettings _contentSettings; [Obsolete("Use the constructor that accepts ICultureDictionary. Will be removed in V15.")] public PropertyValidationService( @@ -28,18 +32,40 @@ public class PropertyValidationService : IPropertyValidationService { } + [Obsolete("Use the constructor that accepts ILanguageService and ContentSettings options. Will be removed in V17.")] public PropertyValidationService( PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService, IValueEditorCache valueEditorCache, ICultureDictionary cultureDictionary) + : this( + propertyEditors, + dataTypeService, + textService, + valueEditorCache, + cultureDictionary, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>()) + { + } + + public PropertyValidationService( + PropertyEditorCollection propertyEditors, + IDataTypeService dataTypeService, + ILocalizedTextService textService, + IValueEditorCache valueEditorCache, + ICultureDictionary cultureDictionary, + ILanguageService languageService, + IOptions contentSettings) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _textService = textService; _valueEditorCache = valueEditorCache; _cultureDictionary = cultureDictionary; + _languageService = languageService; + _contentSettings = contentSettings.Value; } /// @@ -66,6 +92,19 @@ public class PropertyValidationService : IPropertyValidationService propertyType.PropertyEditorAlias); } + // only validate culture invariant properties if + // - AllowEditInvariantFromNonDefault is true, or + // - the default language is being validated, or + // - the underlying data editor supports partial property value merging (e.g. block level variance) + var defaultCulture = _languageService.GetDefaultIsoCodeAsync().GetAwaiter().GetResult(); + if (propertyType.VariesByCulture() is false + && _contentSettings.AllowEditInvariantFromNonDefault is false + && validationContext.CulturesBeingValidated.InvariantContains(defaultCulture) is false + && dataEditor.CanMergePartialPropertyValues(propertyType) is false) + { + return []; + } + return ValidatePropertyValue(dataEditor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage, validationContext); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs index 8e8878e9bc..ea2e680293 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Integration.Attributes; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; @@ -172,7 +173,15 @@ internal partial class BlockListElementLevelVariationTests } [Test] - public async Task Can_Validate_Invalid_Properties_Specific_Culture_Only() + [ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))] + public async Task Can_Validate_Invalid_Properties_Specific_Culture_Only_With_AllowEditInvariantFromNonDefault() + => await Can_Validate_Invalid_Properties_Specific_Culture_Only(); + + [Test] + public async Task Can_Validate_Invalid_Properties_Specific_Culture_Only_Without_AllowEditInvariantFromNonDefault() + => await Can_Validate_Invalid_Properties_Specific_Culture_Only(); + + private async Task Can_Validate_Invalid_Properties_Specific_Culture_Only() { var elementType = CreateElementTypeWithValidation(); var blockListDataType = await CreateBlockListDataType(elementType); @@ -214,6 +223,8 @@ internal partial class BlockListElementLevelVariationTests contentType, new[] { "en-US" }); + // NOTE: since the default culture is being validated, we expect the same result regardless + // of the AllowEditInvariantFromNonDefault configuration var errors = result.ValidationErrors.ToArray(); Assert.Multiple(() => { @@ -338,7 +349,15 @@ internal partial class BlockListElementLevelVariationTests } [Test] - public async Task Can_Validate_Missing_Properties_Nested_Blocks_Specific_Culture_Only() + [ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))] + public async Task Can_Validate_Missing_Properties_Nested_Blocks_Specific_Culture_Only_With_AllowEditInvariantFromNonDefault() + => Can_Validate_Missing_Properties_Nested_Blocks_Specific_Culture_Only(true); + + [Test] + public async Task Can_Validate_Missing_Properties_Nested_Blocks_Specific_Culture_Only_Without_AllowEditInvariantFromNonDefault() + => Can_Validate_Missing_Properties_Nested_Blocks_Specific_Culture_Only(false); + + private async Task Can_Validate_Missing_Properties_Nested_Blocks_Specific_Culture_Only(bool expectedInvariantValidationErrors) { var (rootElementType, nestedElementType) = await CreateElementTypeWithValidationAndNestedBlocksAsync(); var rootBlockListDataType = await CreateBlockListDataType(rootElementType); @@ -448,19 +467,39 @@ internal partial class BlockListElementLevelVariationTests new[] { "da-DK" }); var errors = result.ValidationErrors.ToArray(); - Assert.Multiple(() => + + // NOTE: since the default culture is not being validated, we expect different results depending + // on the AllowEditInvariantFromNonDefault configuration + + if (expectedInvariantValidationErrors) { - Assert.AreEqual(6, errors.Length); - Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); + Assert.Multiple(() => + { + Assert.AreEqual(6, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); - Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); - Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); - Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); - Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); - Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); - Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); - }); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + }); + } + else + { + Assert.Multiple(() => + { + Assert.AreEqual(3, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == null && error.Segment == null)); + + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[0].value.contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value.settingsData[0].values[?(@.alias == 'variantText' && @.culture == 'da-DK' && @.segment == null)].value")); + }); + } } [Test] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs index a8abd95f98..5951af2bb5 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.Publish.cs @@ -2,8 +2,11 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Integration.Attributes; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -325,6 +328,82 @@ public partial class ContentPublishingServiceTests Assert.AreEqual(2, content.PublishedCultures.Count()); } + [TestCase(true, "da-DK")] + [TestCase(false, "en-US")] + [TestCase(false, "en-US", "da-DK")] + public async Task Publish_Invalid_Invariant_Property_WithoutAllowEditInvariantFromNonDefault(bool expectedSuccess, params string[] culturesToRepublish) + => await Publish_Invalid_Invariant_Property(expectedSuccess, culturesToRepublish); + + [TestCase(false, "da-DK")] + [TestCase(false, "en-US")] + [TestCase(false, "en-US", "da-DK")] + [ConfigureBuilder(ActionName = nameof(ConfigureAllowEditInvariantFromNonDefaultTrue))] + public async Task Publish_Invalid_Invariant_Property_WithAllowEditInvariantFromNonDefault(bool expectedSuccess, params string[] culturesToRepublish) + => await Publish_Invalid_Invariant_Property(expectedSuccess, culturesToRepublish); + + private async Task Publish_Invalid_Invariant_Property(bool expectedSuccess, params string[] culturesToRepublish) + { + var contentType = await SetupVariantInvariantTest(); + + IContent content = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", "EN") + .WithCultureName("da-DK", "DA") + .Build(); + content.SetValue("variantValue", "EN value", culture: "en-US"); + content.SetValue("variantValue", "DA value", culture: "da-DK"); + content.SetValue("invariantValue", "Invariant value"); + ContentService.Save(content); + + var result = await ContentPublishingService.PublishAsync(content.Key, MakeModel(new HashSet { "en-US", "da-DK" }), Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + + content = ContentService.GetById(content.Key)!; + content.SetValue("variantValue", "EN value updated", culture: "en-US"); + content.SetValue("variantValue", "DA value updated", culture: "da-DK"); + content.SetValue("invariantValue", null); + ContentService.Save(content); + + result = await ContentPublishingService.PublishAsync(content.Key, MakeModel(new HashSet(culturesToRepublish)), Constants.Security.SuperUserKey); + + content = ContentService.GetById(content.Key)!; + + Assert.Multiple(() => + { + Assert.AreEqual(null, content.GetValue("invariantValue", published: false)); + Assert.AreEqual("EN value updated", content.GetValue("variantValue", culture: "en-US", published: false)); + Assert.AreEqual("DA value updated", content.GetValue("variantValue", culture: "da-DK", published: false)); + + Assert.AreEqual("Invariant value", content.GetValue("invariantValue", published: true)); + }); + + if (expectedSuccess) + { + Assert.Multiple(() => + { + Assert.IsTrue(result.Success); + + var expectedPublishedEnglishValue = culturesToRepublish.Contains("en-US") + ? "EN value updated" + : "EN value"; + var expectedPublishedDanishValue = culturesToRepublish.Contains("da-DK") + ? "DA value updated" + : "DA value"; + Assert.AreEqual(expectedPublishedEnglishValue, content.GetValue("variantValue", culture: "en-US", published: true)); + Assert.AreEqual(expectedPublishedDanishValue, content.GetValue("variantValue", culture: "da-DK", published: true)); + }); + } + else + { + Assert.Multiple(() => + { + Assert.IsFalse(result.Success); + Assert.AreEqual("EN value", content.GetValue("variantValue", culture: "en-US", published: true)); + Assert.AreEqual("DA value", content.GetValue("variantValue", culture: "da-DK", published: true)); + }); + } + } + [Test] public async Task Cannot_Publish_Non_Existing_Content() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.cs index ab44a3131f..6e5cf55fc7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentPublishingServiceTests.cs @@ -1,5 +1,7 @@ -using NUnit.Framework; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentPublishing; @@ -109,6 +111,41 @@ public partial class ContentPublishingServiceTests : UmbracoIntegrationTestWithC return (langEn, langDa, contentType); } + private async Task SetupVariantInvariantTest() + { + var langDa = new LanguageBuilder() + .WithCultureInfo("da-DK") + .Build(); + await LanguageService.CreateAsync(langDa, Constants.Security.SuperUserKey); + + var key = Guid.NewGuid(); + var contentType = new ContentTypeBuilder() + .WithAlias("variantInvariantContent") + .WithName("Variant Invariant Content") + .WithKey(key) + .WithContentVariation(ContentVariation.Culture) + .AddAllowedContentType() + .WithKey(key) + .WithAlias("variantInvariantContent") + .Done() + .AddPropertyType() + .WithAlias("variantValue") + .WithVariations(ContentVariation.Culture) + .WithMandatory(true) + .Done() + .AddPropertyType() + .WithAlias("invariantValue") + .WithVariations(ContentVariation.Nothing) + .WithMandatory(true) + .Done() + .Build(); + + contentType.AllowedAsRoot = true; + await ContentTypeService.SaveAsync(contentType, Constants.Security.SuperUserKey); + + return contentType; + } + protected override void CustomTestSetup(IUmbracoBuilder builder) => builder .AddNotificationHandler() @@ -132,4 +169,7 @@ public partial class ContentPublishingServiceTests : UmbracoIntegrationTestWithC public void Handle(ContentUnpublishingNotification notification) => UnpublishingContent?.Invoke(notification); } + + public static void ConfigureAllowEditInvariantFromNonDefaultTrue(IUmbracoBuilder builder) + => builder.Services.Configure(config => config.AllowEditInvariantFromNonDefault = true); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs index ef8b8733c7..e6cb811f01 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs @@ -1,10 +1,12 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -654,6 +656,8 @@ public class VariationTests dataTypeService, Mock.Of(), new ValueEditorCache(), - Mock.Of()); + Mock.Of(), + Mock.Of(), + Mock.Of>()); } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs index 097a0495e5..a5273d31fc 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Services/PropertyValidationServiceTests.cs @@ -1,10 +1,12 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Dictionary; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; @@ -44,7 +46,14 @@ public class PropertyValidationServiceTests var propEditors = new PropertyEditorCollection(new DataEditorCollection(() => new[] { dataEditor })); - validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of(),new ValueEditorCache(), Mock.Of()); + validationService = new PropertyValidationService( + propEditors, + dataTypeService.Object, + Mock.Of(), + new ValueEditorCache(), + Mock.Of(), + Mock.Of(), + Mock.Of>()); } [Test] From 10f37494b6a8a06d297beb66249f4d236dea75fd Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:09:41 +0100 Subject: [PATCH 16/19] V15: umb-dropzone extends umb-input-dropzone (#18784) * feat: umb-dropzone should extend umb-input-dropzone moves the umb-dropzone back into the media package and extends the umb-input-dropzone for common logic * feat: adds a UmbDropzoneMediaManager class to handle media specifically * chore: sort imports * feat: adds a browse() method * removes unused export * use correct import * fix: document and media type import should use other dropzone * docs(storybook): remove old argument * feat: adds umb-dropzone-media element and deprecates umb-dropzone element * feat: use umb-dropzone-media instead * adds export for dropzone * feat: adds a slot to show an additional text above the dropzone graphics * feat: the dropzone should fill out its host component * feat: adds back a text to describe where to drop files * remove unused import * fix: removes overflow to allow full border --- .../document-type-import-modal.element.ts | 51 +++---- .../media/dropzone/components/index.ts | 1 - .../input-dropzone/input-dropzone.element.ts | 83 ++++++----- .../input-dropzone/input-dropzone.stories.ts | 5 +- .../media/dropzone/dropzone-manager.class.ts | 1 + .../modal/media-type-import-modal.element.ts | 48 +++---- .../collection/media-collection.element.ts | 9 +- .../input-rich-media.element.ts | 4 +- .../dropzone/dropzone-media-manager.class.ts | 21 +++ .../dropzone/dropzone-media.element.ts} | 135 +++++++----------- .../media/media/dropzone/dropzone.element.ts | 21 +++ .../packages/media/media/dropzone/index.ts | 3 + .../src/packages/media/media/index.ts | 1 + .../media-picker-modal.element.ts | 10 +- 14 files changed, 202 insertions(+), 191 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-media-manager.class.ts rename src/Umbraco.Web.UI.Client/src/packages/media/{dropzone/components/dropzone.element.ts => media/dropzone/dropzone-media.element.ts} (53%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts index 97758c9fc8..57473a2735 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/entity-actions/import/modal/document-type-import-modal.element.ts @@ -3,10 +3,10 @@ import type { UmbDocumentTypeImportModalData, UmbDocumentTypeImportModalValue, } from './document-type-import-modal.token.js'; -import { css, html, customElement, query, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import type { UmbDropzoneElement } from '@umbraco-cms/backoffice/dropzone'; +import type { UmbDropzoneChangeEvent, UmbDropzoneMediaElement } from '@umbraco-cms/backoffice/media'; interface UmbDocumentTypePreview { unique: string; @@ -27,9 +27,6 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement< @state() private _fileContent: Array = []; - @query('#dropzone') - private dropzone?: UmbDropzoneElement; - constructor() { super(); this.#fileReader = new FileReader(); @@ -43,12 +40,18 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement< }; } - #onUploadComplete() { - const data = this.dropzone?.getItems()[0]; - if (!data?.temporaryFile) return; + #onUploadComplete(evt: UmbDropzoneChangeEvent) { + evt.preventDefault(); + const target = evt.target as UmbDropzoneMediaElement; + const data = target.value; + if (!data?.length) return; - this.#temporaryUnique = data.temporaryFile.temporaryUnique; - this.#fileReader.readAsText(data.temporaryFile.file); + const file = data[0]; + + if (file.temporaryFile) { + this.#temporaryUnique = file.temporaryFile.temporaryUnique; + this.#fileReader.readAsText(file.temporaryFile.file); + } } async #onFileImport() { @@ -93,10 +96,6 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement< this.#temporaryUnique = undefined; } - async #onBrowse() { - this.dropzone?.browse(); - } - override render() { return html` ${this.#renderUploadZone()} @@ -132,26 +131,18 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement< label=${this.localize.term('general_remove')}> `, () => - /**TODO Add localizations */ html`
- Drag and drop your file(s) into the area - - - + Drag and drop your file(s) into the area +
`, )} `; } - static override styles = [ + static override readonly styles = [ UmbTextStyles, css` #wrapper { @@ -166,6 +157,10 @@ export class UmbDocumentTypeImportModalLayout extends UmbModalBaseElement< padding: var(--uui-size-space-6); } + #dropzone { + width: 100%; + } + #import { margin-top: var(--uui-size-space-6); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/index.ts index 008143e170..9c89425588 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/index.ts @@ -1,2 +1 @@ export * from './input-dropzone/input-dropzone.element.js'; -export * from './dropzone.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts index e75f279822..a4049b995a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/input-dropzone/input-dropzone.element.ts @@ -1,6 +1,8 @@ import type { UmbUploadableItem } from '../../types.js'; import { UmbFileDropzoneItemStatus } from '../../constants.js'; import { UmbDropzoneManager } from '../../dropzone-manager.class.js'; +import { UmbDropzoneChangeEvent } from '../../dropzone-change.event.js'; +import { UmbDropzoneSubmittedEvent } from '../../dropzone-submitted.event.js'; import { css, customElement, @@ -18,15 +20,16 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { formatBytes } from '@umbraco-cms/backoffice/utils'; import { UmbFormControlMixin } from '@umbraco-cms/backoffice/validation'; -import { UmbDropzoneChangeEvent } from '../../dropzone-change.event.js'; -import { UmbDropzoneSubmittedEvent } from '../../dropzone-submitted.event.js'; /** + * A dropzone for uploading files and folders. + * The files will be uploaded to the server as temporary files and can be used in the backoffice. * @element umb-input-dropzone * @fires ProgressEvent When the progress of the upload changes. * @fires UmbDropzoneChangeEvent When the upload is complete. * @fires UmbDropzoneSubmittedEvent When the upload is submitted. * @slot - The default slot. + * @slot text - A text shown above the dropzone graphic. */ @customElement('umb-input-dropzone') export class UmbInputDropzoneElement extends UmbFormControlMixin( @@ -38,12 +41,6 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin; + protected _progressItems: Array = []; #manager = new UmbDropzoneManager(this); @@ -114,8 +104,22 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin - ${this.#renderUploader()} + ${this.renderUploader()} `; } - #renderUploader() { + protected renderUploader() { if (this.disabled) return nothing; if (!this._progressItems?.length) return nothing; @@ -142,7 +146,7 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin item.unique, - (item) => this.#renderPlaceholder(item), + (item) => this.renderPlaceholder(item), )} @@ -208,6 +212,16 @@ export class UmbInputDropzoneElement extends UmbFormControlMixin = { args: { disabled: false, accept: '', - createAsTemporary: true, }, decorators: [(Story) => html`
${Story()}
`], parameters: { @@ -53,3 +52,7 @@ export const WithDefaultSlot: Story = {
Custom slot
`, }; + +export const WithTextSlot: Story = { + render: () => html`
Drop your files here
`, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts index 1a01cf86ad..81ea3bad58 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts @@ -80,6 +80,7 @@ export class UmbDropzoneManager extends UmbControllerBase { /** * Uploads files and folders to the server and creates the media items with corresponding media type.\ * Allows the user to pick a media type option if multiple types are allowed. + * @deprecated Use the {@link UmbDropzoneMediaManager} class instead. This will be removed in Umbraco 18. * @param {UmbFileDropzoneDroppedItems} items - The files and folders to upload. * @param {string | null} parentUnique - Where the items should be uploaded. * @returns {Array} - The items about to be uploaded. diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts index 6bf7494ea3..46d4c64ba3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/entity-actions/import/modal/media-type-import-modal.element.ts @@ -1,9 +1,9 @@ import { UmbMediaTypeImportRepository } from '../repository/media-type-import.repository.js'; import type { UmbMediaTypeImportModalData, UmbMediaTypeImportModalValue } from './media-type-import-modal.token.js'; -import { css, html, customElement, query, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; -import type { UmbDropzoneElement } from '@umbraco-cms/backoffice/media'; +import type { UmbDropzoneChangeEvent, UmbDropzoneMediaElement } from '@umbraco-cms/backoffice/media'; interface UmbMediaTypePreview { unique: string; @@ -24,9 +24,6 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< @state() private _fileContent: Array = []; - @query('#dropzone') - private dropzone?: UmbDropzoneElement; - constructor() { super(); this.#fileReader = new FileReader(); @@ -40,12 +37,18 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< }; } - #onUploadCompleted() { - const data = this.dropzone?.getItems()[0]; - if (!data?.temporaryFile) return; + #onUploadComplete(evt: UmbDropzoneChangeEvent) { + evt.preventDefault(); + const target = evt.target as UmbDropzoneMediaElement; + const data = target.value; + if (!data?.length) return; - this.#temporaryUnique = data.temporaryFile.temporaryUnique; - this.#fileReader.readAsText(data.temporaryFile.file); + const file = data[0]; + + if (file.temporaryFile) { + this.#temporaryUnique = file.temporaryFile.temporaryUnique; + this.#fileReader.readAsText(file.temporaryFile.file); + } } async #onFileImport() { @@ -90,10 +93,6 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< this.#temporaryUnique = undefined; } - async #onBrowse() { - this.dropzone?.browse(); - } - override render() { return html` ${this.#renderUploadZone()} @@ -126,18 +125,11 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< `, () => html`
- Drag and drop your file(s) into the area - - - + Drag and drop your file(s) into the area +
`, )} `; @@ -158,6 +150,10 @@ export class UmbMediaTypeImportModalLayout extends UmbModalBaseElement< padding: var(--uui-size-space-6); } + #dropzone { + width: 100%; + } + #import { margin-top: var(--uui-size-space-6); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts index ffc8e3fbcd..116ca9f028 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/collection/media-collection.element.ts @@ -1,11 +1,12 @@ import { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE } from '../entity.js'; import { UMB_MEDIA_WORKSPACE_CONTEXT } from '../workspace/media-workspace.context-token.js'; +import type { UmbDropzoneMediaElement } from '../dropzone/index.js'; import { UMB_MEDIA_COLLECTION_CONTEXT } from './media-collection.context-token.js'; import { customElement, html, ref, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbCollectionDefaultElement } from '@umbraco-cms/backoffice/collection'; import { UmbRequestReloadChildrenOfEntityEvent } from '@umbraco-cms/backoffice/entity-action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; -import type { UmbDropzoneElement, UmbDropzoneSubmittedEvent } from '@umbraco-cms/backoffice/dropzone'; +import type { UmbDropzoneSubmittedEvent } from '@umbraco-cms/backoffice/dropzone'; @customElement('umb-media-collection') export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { @@ -34,7 +35,7 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { #observeProgressItems(dropzone?: Element) { if (!dropzone) return; this.observe( - (dropzone as UmbDropzoneElement).progressItems(), + (dropzone as UmbDropzoneMediaElement).progressItems(), (progressItems) => { progressItems.forEach((item) => { // We do not update folders as it may have children still being uploaded. @@ -84,14 +85,14 @@ export class UmbMediaCollectionElement extends UmbCollectionDefaultElement { ${when(this._progress >= 0, () => html``)} - + @progress=${this.#onProgress}> `; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts index 4e74a0a6d6..73793d947e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/components/input-rich-media/input-rich-media.element.ts @@ -354,7 +354,9 @@ export class UmbInputRichMediaElement extends UmbFormControlMixin< #renderDropzone() { if (this.readonly) return nothing; if (this._cards && this._cards.length >= this.max) return; - return html` 1} @complete=${this.#onUploadCompleted}>`; + return html` 1} + @complete=${this.#onUploadCompleted}>`; } #renderItems() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-media-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-media-manager.class.ts new file mode 100644 index 0000000000..b49dca7188 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-media-manager.class.ts @@ -0,0 +1,21 @@ +import { + UmbDropzoneManager, + type UmbFileDropzoneDroppedItems, + type UmbUploadableItem, +} from '@umbraco-cms/backoffice/dropzone'; + +export class UmbDropzoneMediaManager extends UmbDropzoneManager { + /** + * Uploads files and folders to the server and creates the media items with corresponding media type.\ + * Allows the user to pick a media type option if multiple types are allowed. + * @param {UmbFileDropzoneDroppedItems} items - The files and folders to upload. + * @param {string | null} parentUnique - Where the items should be uploaded. + * @returns {Array} - The items about to be uploaded. + */ + public override createMediaItems( + items: UmbFileDropzoneDroppedItems, + parentUnique: string | null, + ): Array { + return super.createMediaItems(items, parentUnique); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-media.element.ts similarity index 53% rename from src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/dropzone.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-media.element.ts index e12a41488b..912a51780e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/components/dropzone.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone-media.element.ts @@ -1,81 +1,63 @@ -import { UmbDropzoneManager } from '../dropzone-manager.class.js'; -import { UmbDropzoneSubmittedEvent } from '../dropzone-submitted.event.js'; -import type { UmbUploadableItem } from '../types.js'; -import { UmbFileDropzoneItemStatus } from '../constants.js'; -import { css, customElement, html, ifDefined, property, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { UUIFileDropzoneElement, UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; +import { UmbDropzoneMediaManager } from './dropzone-media-manager.class.js'; +import { + UmbInputDropzoneElement, + UmbFileDropzoneItemStatus, + UmbDropzoneSubmittedEvent, + type UmbUploadableItem, +} from '@umbraco-cms/backoffice/dropzone'; +import { css, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIFileDropzoneEvent } from '@umbraco-cms/backoffice/external/uui'; -@customElement('umb-dropzone') -export class UmbDropzoneElement extends UmbLitElement { +/** + * A dropzone for uploading files and folders as media items. It is hidden by default and will be shown when dragging files over the window. + * @element umb-dropzone-media + * @fires ProgressEvent When the progress of the upload changes. + * @fires UmbDropzoneSubmittedEvent When the upload is submitted. + * @fires UmbDropzoneChangeEvent When any upload changes. + * @fires CustomEvent<'complete'> When all uploads are complete (deprecated: use {@link UmbDropzoneChangeEvent} instead). + * @slot - The default slot. + */ +@customElement('umb-dropzone-media') +export class UmbDropzoneMediaElement extends UmbInputDropzoneElement { @property({ attribute: 'parent-unique' }) parentUnique: string | null = null; + /** + * Determines if the dropzone should create temporary files or media items directly. + * @deprecated Use the {@link UmbInputDropzoneElement} instead. + */ @property({ type: Boolean, attribute: 'create-as-temporary' }) createAsTemporary: boolean = false; - @property({ type: String }) - accept?: string; - - @property({ type: Boolean, reflect: true }) - multiple: boolean = false; - - @property({ type: Boolean, reflect: true }) - disabled = false; - - @property({ type: Boolean, attribute: 'disable-folder-upload', reflect: true }) - public set disableFolderUpload(isAllowed: boolean) { - this.#dropzoneManager.setIsFoldersAllowed(!isAllowed); - } - public get disableFolderUpload() { - return this._disableFolderUpload; - } - private readonly _disableFolderUpload = false; - - @state() - private _progressItems: Array = []; - - #dropzoneManager: UmbDropzoneManager; + #dropzoneManager = new UmbDropzoneMediaManager(this); /** * @deprecated Please use `getItems()` instead; this method will be removed in Umbraco 17. * @returns {Array} An array of uploadable items. */ - public getFiles() { - return this.getItems(); - } + public getFiles = this.getItems; - public getItems() { + /** + * Gets the current value of the uploaded items. + * @returns {Array} An array of uploadable items. + */ + public getItems(): Array { return this._progressItems; } public progressItems = () => this.#dropzoneManager.progressItems; public progress = () => this.#dropzoneManager.progress; - public browse() { - if (this.disabled) return; - const element = this.shadowRoot?.querySelector('#dropzone') as UUIFileDropzoneElement; - return element.browse(); - } - constructor() { super(); - this.#dropzoneManager = new UmbDropzoneManager(this); + document.addEventListener('dragenter', this.#handleDragEnter.bind(this)); document.addEventListener('dragleave', this.#handleDragLeave.bind(this)); document.addEventListener('drop', this.#handleDrop.bind(this)); - this.observe( - this.#dropzoneManager.progress, - (progress) => - this.dispatchEvent(new ProgressEvent('progress', { loaded: progress.completed, total: progress.total })), - '_observeProgress', - ); - this.observe( this.#dropzoneManager.progressItems, (progressItems: Array) => { - this._progressItems = progressItems; const waiting = progressItems.find((item) => item.status === UmbFileDropzoneItemStatus.WAITING); if (progressItems.length && !waiting) { this.dispatchEvent(new CustomEvent('complete', { detail: progressItems })); @@ -93,6 +75,19 @@ export class UmbDropzoneElement extends UmbLitElement { document.removeEventListener('drop', this.#handleDrop.bind(this)); } + override async onUpload(event: UUIFileDropzoneEvent) { + if (this.disabled) return; + if (!event.detail.files.length && !event.detail.folders.length) return; + + if (this.createAsTemporary) { + const uploadable = this.#dropzoneManager.createTemporaryFiles(event.detail.files); + this.dispatchEvent(new UmbDropzoneSubmittedEvent(await uploadable)); + } else { + const uploadable = this.#dropzoneManager.createMediaItems(event.detail, this.parentUnique); + this.dispatchEvent(new UmbDropzoneSubmittedEvent(uploadable)); + } + } + #handleDragEnter(e: DragEvent) { if (this.disabled) return; // Avoid collision with UmbSorterController @@ -113,30 +108,8 @@ export class UmbDropzoneElement extends UmbLitElement { this.toggleAttribute('dragging', false); } - async #onDropFiles(event: UUIFileDropzoneEvent) { - if (this.disabled) return; - if (!event.detail.files.length && !event.detail.folders.length) return; - - if (this.createAsTemporary) { - const uploadable = this.#dropzoneManager.createTemporaryFiles(event.detail.files); - this.dispatchEvent(new UmbDropzoneSubmittedEvent(await uploadable)); - } else { - const uploadable = this.#dropzoneManager.createMediaItems(event.detail, this.parentUnique); - this.dispatchEvent(new UmbDropzoneSubmittedEvent(uploadable)); - } - } - - override render() { - return html``; - } - static override styles = [ + ...UmbInputDropzoneElement.styles, css` :host(:not([disabled])[dragging]) #dropzone { opacity: 1; @@ -154,30 +127,18 @@ export class UmbDropzoneElement extends UmbLitElement { align-items: center; justify-content: center; position: absolute; - inset: 0px; z-index: 100; - backdrop-filter: opacity(1); /* Removes the built in blur effect */ border-radius: var(--uui-border-radius); - overflow: clip; border: 1px solid var(--uui-color-focus); } - #dropzone:after { - content: ''; - display: block; - position: absolute; - inset: 0; - border-radius: var(--uui-border-radius); - background-color: var(--uui-color-focus); - opacity: 0.2; - } `, ]; } -export default UmbDropzoneElement; +export default UmbDropzoneMediaElement; declare global { interface HTMLElementTagNameMap { - 'umb-dropzone': UmbDropzoneElement; + 'umb-dropzone-media': UmbDropzoneMediaElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts new file mode 100644 index 0000000000..b58b0a6ccc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/dropzone.element.ts @@ -0,0 +1,21 @@ +import { UmbDropzoneMediaElement } from './dropzone-media.element.js'; +import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; + +const DEPRECATION_MESSAGE = new UmbDeprecation({ + deprecated: '', + removeInVersion: '18', + solution: 'Use for media items and for all other files and folders.', +}); + +/** + * @inheritdoc + * @deprecated Use {@link UmbDropzoneMediaElement} for media items instead, and {@link UmbInputDropzoneElement} for all other files and folders. This will be removed in Umbraco 18. + */ +@customElement('umb-dropzone') +export default class UmbDropzoneElement extends UmbDropzoneMediaElement { + override connectedCallback(): void { + super.connectedCallback(); + DEPRECATION_MESSAGE.warn(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts new file mode 100644 index 0000000000..a48fad8436 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/dropzone/index.ts @@ -0,0 +1,3 @@ +export * from './dropzone-media-manager.class.js'; +export * from './dropzone-media.element.js'; +export * from './dropzone.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts index 9b7f13cc62..4a12b45770 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/index.ts @@ -1,5 +1,6 @@ export * from './components/index.js'; export * from './constants.js'; +export * from './dropzone/index.js'; export * from './reference/index.js'; export * from './repository/index.js'; export * from './search/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts index 3e12c0901b..5f709abe61 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/modals/media-picker/media-picker-modal.element.ts @@ -6,7 +6,7 @@ import { UmbMediaSearchProvider } from '../../search/index.js'; import type { UmbMediaPathModel } from './types.js'; import type { UmbMediaPickerFolderPathElement } from './components/media-picker-folder-path.element.js'; import type { UmbMediaPickerModalData, UmbMediaPickerModalValue } from './media-picker-modal.token.js'; -import type { UmbDropzoneElement } from '@umbraco-cms/backoffice/dropzone'; +import type { UmbDropzoneMediaElement } from '@umbraco-cms/backoffice/media'; import { css, html, @@ -72,7 +72,7 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement(); @@ -287,11 +287,11 @@ export class UmbMediaPickerModalElement extends UmbModalBaseElement this.#loadChildrenOfCurrentMediaItem()} - .parentUnique=${this._currentMediaEntity.unique}> + @complete=${this.#loadChildrenOfCurrentMediaItem} + .parentUnique=${this._currentMediaEntity.unique}> ${this._searchQuery ? this.#renderSearchResult() : this.#renderCurrentChildren()} `; } From 0dd4443b754dc2f578841082aa8292860a283923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 25 Mar 2025 15:48:07 +0100 Subject: [PATCH 17/19] Feature: validation synchronization as opt in (#18798) * allow for this word * getMessages * split inherit and sync method * sync feature * rename sync report * auto report + impl * remove log * double inheritance test * one more test --- .vscode/settings.json | 5 + .../property-editor-ui-block-grid.element.ts | 1 + .../property-editor-ui-block-list.element.ts | 1 + .../workspace/block-workspace.context.ts | 7 + .../content-detail-workspace-base.ts | 1 + .../context/validation-messages.manager.ts | 12 +- .../controllers/validation.controller.test.ts | 270 +++++++++++++++++- .../controllers/validation.controller.ts | 158 ++++++---- .../submittable-workspace-context-base.ts | 2 +- .../rte/components/rte-base.element.ts | 1 + 10 files changed, 392 insertions(+), 66 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..3119ae6509 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "unprovide" + ] +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts index eaf8481af9..b156099843 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts @@ -95,6 +95,7 @@ export class UmbPropertyEditorUIBlockGridElement if (dataPath) { // Set the data path for the local validation context: this.#validationContext.setDataPath(dataPath); + this.#validationContext.autoReport(); } }, 'observeDataPath', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index 00c9ef6204..e916852946 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -261,6 +261,7 @@ export class UmbPropertyEditorUIBlockListElement if (dataPath) { // Set the data path for the local validation context: this.#validationContext.setDataPath(dataPath); + this.#validationContext.autoReport(); } }, 'observeDataPath', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index b565d4de48..8f34ef7cd5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -486,6 +486,13 @@ export class UmbBlockWorkspaceContext (this.#filter ? msgs.filter(this.#filter) : msgs)); - getFilteredMessages(): Array { + getNotFilteredMessages(): Array { + return this.#messages.getValue(); + } + + getMessages(): Array { const msgs = this.#messages.getValue(); return this.#filter ? msgs.filter(this.#filter) : msgs; } @@ -68,12 +72,12 @@ export class UmbValidationMessagesManager { } getHasAnyMessages(): boolean { - return this.getFilteredMessages().length !== 0; + return this.getMessages().length !== 0; } getMessagesOfPathAndDescendant(path: string): Array { //path = path.toLowerCase(); - return this.getFilteredMessages().filter((x) => MatchPathOrDescendantPath(x.path, path)); + return this.getMessages().filter((x) => MatchPathOrDescendantPath(x.path, path)); } messagesOfPathAndDescendant(path: string): Observable> { @@ -99,7 +103,7 @@ export class UmbValidationMessagesManager { } getHasMessagesOfPathAndDescendant(path: string): boolean { //path = path.toLowerCase(); - return this.getFilteredMessages().some( + return this.getMessages().some( (x) => x.path.indexOf(path) === 0 && (x.path.length === path.length || x.path[path.length] === '.' || x.path[path.length] === '['), diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.test.ts index 8258b8f498..ef8542916f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.test.ts @@ -17,16 +17,24 @@ describe('UmbValidationController', () => { ctrl = new UmbValidationController(host); }); + afterEach(() => { + host.destroy(); + }); + describe('Basics', () => { + it('is invalid when holding messages', async () => { + ctrl.messages.addMessage('server', '$.test', 'test'); + + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.false; + }); + it('is valid when holding no messages', async () => { await ctrl.validate().catch(() => undefined); expect(ctrl.isValid).to.be.true; }); - it('is invalid when holding messages', async () => { - ctrl.messages.addMessage('server', '$.test', 'test'); - - await ctrl.validate().catch(() => undefined); + it('is not valid in its initial state', async () => { expect(ctrl.isValid).to.be.false; }); }); @@ -70,14 +78,24 @@ describe('UmbValidationController', () => { beforeEach(() => { child = new UmbValidationController(host); }); + afterEach(() => { + child.destroy(); + }); - it('is invalid when not inherited a message', async () => { + it('is valid despite a child begin created', async () => { + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.true; + expect(ctrl.messages.getHasAnyMessages()).to.be.false; + }); + + it('is valid when not inherited a message', async () => { ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-other')].value.test", 'test'); child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); await Promise.resolve(); await ctrl.validate().catch(() => undefined); + await child.validate().catch(() => undefined); expect(child.isValid).to.be.true; expect(child.messages.getHasAnyMessages()).to.be.false; }); @@ -89,22 +107,97 @@ describe('UmbValidationController', () => { await ctrl.validate().catch(() => undefined); expect(child.isValid).to.be.false; expect(child.messages.getHasAnyMessages()).to.be.true; - expect(child.messages.getFilteredMessages()?.[0].body).to.be.equal('test-body'); + expect(child.messages.getMessages()?.[0].body).to.be.equal('test-body'); }); - it('is invalid base on a message bubbling up', async () => { + it('is invalid bases on a message from a parent', async () => { ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property')].value.test", 'test-body'); child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.autoReport(); await ctrl.validate().catch(() => undefined); expect(ctrl.isValid).to.be.false; expect(ctrl.messages.getHasAnyMessages()).to.be.true; - expect(ctrl.messages.getFilteredMessages()?.[0].body).to.be.equal('test-body'); + expect(ctrl.messages.getMessages()?.[0].body).to.be.equal('test-body'); + }); + + it('is invalid based on a synced message from a child', async () => { + child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.messages.addMessage('server', '$.test', 'test-body'); + child.autoReport(); + + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(ctrl.messages.getMessages()?.[0].body).to.be.equal('test-body'); + }); + + it('is invalid based on a syncOnce message from a child', async () => { + child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.messages.addMessage('server', '$.test', 'test-body'); + child.report(); + + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(ctrl.messages.getMessages()?.[0].body).to.be.equal('test-body'); + }); + + it('is invalid based on a syncOnce message from a child who later got the message removed.', async () => { + child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.messages.addMessage('server', '$.test', 'test-body', 'test-msg-key'); + child.report(); + child.messages.removeMessageByKey('test-msg-key'); + + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(ctrl.messages.getMessages()?.[0].body).to.be.equal('test-body'); + }); + + it('is valid based on a syncOnce message from a child who later got removed and syncOnce.', async () => { + child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.messages.addMessage('server', '$.test', 'test-body', 'test-msg-key'); + child.report(); + child.messages.removeMessageByKey('test-msg-key'); + child.report(); + + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.true; + expect(ctrl.messages.getHasAnyMessages()).to.be.false; + }); + + it('is valid despite child previously had a syncOnce executed', async () => { + child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.report(); + child.messages.addMessage('server', '$.test', 'test-body'); + + expect(child.isValid).to.be.false; + expect(child.messages.getHasAnyMessages()).to.be.true; + expect(child.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body'); + + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.true; + expect(ctrl.messages.getHasAnyMessages()).to.be.false; + }); + + it('is still valid despite non-synchronizing child is invalid', async () => { + child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.messages.addMessage('server', '$.test', 'test-body'); + + await ctrl.validate().catch(() => undefined); + await child.validate().catch(() => undefined); + expect(child.isValid).to.be.false; + expect(child.messages.getHasAnyMessages()).to.be.true; + expect(child.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body'); + expect(ctrl.isValid).to.be.true; + expect(ctrl.messages.getHasAnyMessages()).to.be.false; }); it('is valid when a message has been removed from a child context', async () => { ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property')].value.test", 'test-body'); child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.autoReport(); // First they are invalid: await ctrl.validate().catch(() => undefined); @@ -123,13 +216,27 @@ describe('UmbValidationController', () => { expect(ctrl.messages.getHasAnyMessages()).to.be.false; }); + it('is still invalid despite a message has been removed from a non-synchronizing child context', async () => { + ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property')].value.test", 'test-body'); + child.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child.messages.removeMessagesByPath('$.test'); + + // After the removal they are valid: + await child.validate().catch(() => undefined); + expect(child.isValid).to.be.true; + expect(child.messages.getHasAnyMessages()).to.be.false; + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + }); + describe('Inheritance + Variant Filter', () => { - it('is invalid when not inherited a message', async () => { + it('is valid when not inherited a message', async () => { child.setVariantId(new UmbVariantId('en-us')); child.inheritFrom( ctrl, "$.values[?(@.alias == 'my-property' && @.culture == 'en-us' && @.segment == null)].value", ); + child.autoReport(); ctrl.messages.addMessage( 'server', @@ -168,6 +275,7 @@ describe('UmbValidationController', () => { 'test-body', ); child.inheritFrom(ctrl, '$'); + child.autoReport(); // First they are invalid: await ctrl.validate().catch(() => undefined); @@ -189,4 +297,148 @@ describe('UmbValidationController', () => { }); }); }); + + describe('Double inheritance', () => { + let child1: UmbValidationController; + let child2: UmbValidationController; + + beforeEach(() => { + child1 = new UmbValidationController(host); + child2 = new UmbValidationController(host); + }); + afterEach(() => { + child1.destroy(); + child2.destroy(); + }); + + it('is auto reporting from two sub contexts', async () => { + ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property-1')].value.test", 'test-body-1'); + ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property-2')].value.test", 'test-body-2'); + child1.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property-1')].value"); + child2.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property-2')].value"); + child1.autoReport(); + child2.autoReport(); + + // First they are invalid: + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(child1.isValid).to.be.false; + expect(child1.messages.getHasAnyMessages()).to.be.true; + expect(child1.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-1'); + expect(child2.isValid).to.be.false; + expect(child2.messages.getHasAnyMessages()).to.be.true; + expect(child2.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-2'); + + child1.messages.removeMessagesByPath('$.test'); + await child1.validate().catch(() => undefined); + + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(child1.isValid).to.be.true; + expect(child1.messages.getHasAnyMessages()).to.be.false; + expect(child2.isValid).to.be.false; + expect(child2.messages.getHasAnyMessages()).to.be.true; + + child2.messages.removeMessagesByPath('$.test'); + await child2.validate().catch(() => undefined); + + expect(child1.isValid).to.be.true; + expect(child1.messages.getHasAnyMessages()).to.be.false; + expect(child2.isValid).to.be.true; + expect(child2.messages.getHasAnyMessages()).to.be.false; + + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid, 'root context to be valid').to.be.true; + expect(ctrl.messages.getHasAnyMessages(), 'root context have no messages').to.be.false; + }); + + it('is reporting between two sub context', async () => { + ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property')].value.test1", 'test-body-1'); + ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property')].value.test2", 'test-body-2'); + child1.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child2.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child1.autoReport(); + child2.autoReport(); + + await Promise.resolve(); + // First they are invalid: + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(child1.isValid).to.be.false; + expect(child1.messages.getHasAnyMessages()).to.be.true; + expect(child1.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-1'); + expect(child1.messages.getNotFilteredMessages()?.[1].body).to.be.equal('test-body-2'); + expect(child2.isValid).to.be.false; + expect(child2.messages.getHasAnyMessages()).to.be.true; + expect(child2.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-1'); + expect(child2.messages.getNotFilteredMessages()?.[1].body).to.be.equal('test-body-2'); + + child1.messages.removeMessagesByPath('$.test1'); + + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(child1.isValid).to.be.false; + expect(child1.messages.getHasAnyMessages()).to.be.true; + expect(child1.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-2'); + expect(child2.isValid).to.be.false; + expect(child2.messages.getHasAnyMessages()).to.be.true; + expect(child2.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-2'); + + child2.messages.removeMessagesByPath('$.test2'); + + // Only need to validate the root, because the other controllers are auto reporting. + await ctrl.validate().catch(() => undefined); + + expect(ctrl.isValid, 'root context is valid').to.be.true; + expect(child1.isValid, 'child1 context is valid').to.be.true; + expect(child2.isValid, 'child2 context is valid').to.be.true; + }); + + it('is reporting between two sub context', async () => { + ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property')].value.test1", 'test-body-1'); + ctrl.messages.addMessage('server', "$.values[?(@.alias == 'my-property')].value.test2", 'test-body-2'); + child1.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + child2.inheritFrom(ctrl, "$.values[?(@.alias == 'my-property')].value"); + + await Promise.resolve(); + // First they are invalid: + await ctrl.validate().catch(() => undefined); + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(child1.isValid).to.be.false; + expect(child1.messages.getHasAnyMessages()).to.be.true; + expect(child1.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-1'); + expect(child1.messages.getNotFilteredMessages()?.[1].body).to.be.equal('test-body-2'); + expect(child2.isValid).to.be.false; + expect(child2.messages.getHasAnyMessages()).to.be.true; + expect(child2.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-1'); + expect(child2.messages.getNotFilteredMessages()?.[1].body).to.be.equal('test-body-2'); + + child1.messages.removeMessagesByPath('$.test1'); + child1.report(); + + expect(ctrl.isValid).to.be.false; + expect(ctrl.messages.getHasAnyMessages()).to.be.true; + expect(child1.isValid).to.be.false; + expect(child1.messages.getHasAnyMessages()).to.be.true; + expect(child1.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-2'); + expect(child2.isValid).to.be.false; + expect(child2.messages.getHasAnyMessages()).to.be.true; + expect(child2.messages.getNotFilteredMessages()?.[0].body).to.be.equal('test-body-2'); + + child2.messages.removeMessagesByPath('$.test2'); + child2.report(); + + // We need to validate to the not auto reporting validation controllers updating their isValid state. + await ctrl.validate().catch(() => undefined); + await child1.validate().catch(() => undefined); + await child2.validate().catch(() => undefined); + + expect(ctrl.isValid, 'root context is valid').to.be.true; + expect(child1.isValid, 'child1 context is valid').to.be.true; + expect(child2.isValid, 'child2 context is valid').to.be.true; + }); + }); }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts index fe2460a23d..e9e4d3721f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/validation/controllers/validation.controller.ts @@ -9,6 +9,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; import { ReplaceStartOfPath } from '../utils/replace-start-of-path.function.js'; import type { UmbVariantId } from '../../variant/variant-id.class.js'; +import { UmbDeprecation } from '../../utils/deprecation/deprecation.js'; const Regex = /@\.culture == ('[^']*'|null) *&& *@\.segment == ('[^']*'|null)/g; @@ -26,15 +27,31 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal >; #inUnprovidingState: boolean = false; + // @reprecated - Will be removed in v.17 // Local version of the data send to the server, only use-case is for translation. #translationData = new UmbObjectState(undefined); + /** + * @deprecated Use extension type 'propertyValidationPathTranslator' instead. Will be removed in v.17 + */ translationDataOf(path: string): any { return this.#translationData.asObservablePart((data) => GetValueByJsonPath(data, path)); } + /** + * @deprecated Use extension type 'propertyValidationPathTranslator' instead. Will be removed in v.17 + */ setTranslationData(data: any): void { this.#translationData.setValue(data); } + /** + * @deprecated Use extension type 'propertyValidationPathTranslator' instead. Will be removed in v.17 + */ getTranslationData(): any { + new UmbDeprecation({ + removeInVersion: '17', + deprecated: 'getTranslationData', + solution: 'getTranslationData is deprecated.', + }).warn(); + return this.#translationData.getValue(); } @@ -43,6 +60,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal #isValid: boolean = false; #parent?: UmbValidationController; + #sync?: boolean; #parentMessages?: Array; #localMessages?: Array; #baseDataPath?: string; @@ -135,8 +153,9 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal /** * Define a specific data path for this validation context. * This will turn this validation context into a sub-context of the parent validation context. - * This means that a two-way binding for messages will be established between the parent and the sub-context. - * And it will inherit the Translation Data from its parent. + * This will make this context inherit the messages from the parent validation context. + * @see {@link report} Call `report()` to propagate changes to the parent context. + * @see {@link autoReport} Call `autoReport()` to continuously synchronize changes to the parent context. * * messages and data will be localizes accordingly to the given data path. * @param dataPath {string} - The data path to bind this validation context to. @@ -176,12 +195,14 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal this.#parent.removeValidator(this); } this.#parent = parent; - parent.addValidator(this); + this.#readyToSync(); this.messages.clear(); + this.#localMessages = undefined; this.#baseDataPath = dataPath; + // @deprecated - Will be removed in v.17 this.observe( parent.translationDataOf(dataPath), (data) => { @@ -214,63 +235,90 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal this.messages.addMessage(msg.type, path, msg.body, msg.key); }); } + + this.#localMessages = this.messages.getNotFilteredMessages(); this.messages.finishChange(); }, 'observeParentMessages', ); - - this.observe( - this.messages.messages, - (msgs) => { - if (!this.#parent) return; - - this.#parent!.messages.initiateChange(); - - if (this.#localMessages) { - // Remove the parent messages that does not exist locally anymore: - const toRemove = this.#localMessages.filter((msg) => !msgs.find((m) => m.key === msg.key)); - this.#parent!.messages.removeMessageByKeys(toRemove.map((msg) => msg.key)); - } - this.#localMessages = msgs; - if (this.#baseDataPath === '$') { - this.#parent!.messages.addMessageObjects(msgs); - } else { - msgs.forEach((msg) => { - // replace this.#baseDataPath (if it starts with it) with $ in the path, so it becomes relative to the parent context - const path = ReplaceStartOfPath(msg.path, '$', this.#baseDataPath!); - if (path === undefined) { - throw new Error( - 'Path was not transformed correctly and can therefor not be synced with parent messages.', - ); - } - // Notice, the parent message uses the same key. [NL] - this.#parent!.messages.addMessage(msg.type, path, msg.body, msg.key); - }); - } - - this.#parent!.messages.finishChange(); - }, - 'observeLocalMessages', - ); } #stopInheritance(): void { + this.removeUmbControllerByAlias('observeTranslationData'); + this.removeUmbControllerByAlias('observeParentMessages'); + if (this.#parent) { this.#parent.removeValidator(this); } this.messages.clear(); + this.#localMessages = undefined; this.setTranslationData(undefined); + } - this.removeUmbControllerByAlias('observeTranslationData'); - this.removeUmbControllerByAlias('observeParentMessages'); + #readyToSync() { + if (this.#sync && this.#parent) { + this.#parent.addValidator(this); + } + } + + /** + * Continuously synchronize the messages from this context to the parent context. + */ + autoReport() { + this.#sync = true; + this.#readyToSync(); + this.observe(this.messages.messages, this.#transferMessages, 'observeLocalMessages'); + } + + // no need for this method at this movement. [NL] + /* + #stopSync() { this.removeUmbControllerByAlias('observeLocalMessages'); } + */ + + /** + * Perform a one time transfer of the messages from this context to the parent context. + */ + report(): void { + if (!this.#parent) return; + + if (!this.#sync) { + this.#transferMessages(this.messages.getNotFilteredMessages()); + } + } + + #transferMessages = (msgs: Array) => { + if (!this.#parent) return; + + this.#parent!.messages.initiateChange(); + + if (this.#localMessages) { + // Remove the parent messages that does not exist locally anymore: + const toRemove = this.#localMessages.filter((msg) => !msgs.find((m) => m.key === msg.key)); + this.#parent!.messages.removeMessageByKeys(toRemove.map((msg) => msg.key)); + } + + if (this.#baseDataPath === '$') { + this.#parent!.messages.addMessageObjects(msgs); + } else { + msgs.forEach((msg) => { + // replace this.#baseDataPath (if it starts with it) with $ in the path, so it becomes relative to the parent context + const path = ReplaceStartOfPath(msg.path, '$', this.#baseDataPath!); + if (path === undefined) { + throw new Error('Path was not transformed correctly and can therefor not be synced with parent messages.'); + } + // Notice, the parent message uses the same key. [NL] + this.#parent!.messages.addMessage(msg.type, path, msg.body, msg.key); + }); + } + + this.#parent!.messages.finishChange(); + }; override hostConnected(): void { super.hostConnected(); - if (this.#parent) { - this.#parent.addValidator(this); - } + this.#readyToSync(); } override hostDisconnected(): void { super.hostDisconnected(); @@ -317,7 +365,7 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal this.#validators.splice(index, 1); // If we are in validation mode then we should re-validate to focus next invalid element: if (this.#validationMode) { - this.validate(); + this.validate().catch(() => undefined); } } } @@ -328,23 +376,26 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal * @returns succeed {Promise} - Returns a promise that resolves to true if the validation succeeded. */ async validate(): Promise { - // TODO: clear server messages here?, well maybe only if we know we will get new server messages? Do the server messages hook into the system like another validator? this.#validationMode = true; - const resultsStatus = await Promise.all(this.#validators.map((v) => v.validate())).then( - () => true, - () => false, - ); + const resultsStatus = + this.#validators.length === 0 + ? true + : await Promise.all(this.#validators.map((v) => v.validate())).then( + () => true, + () => false, + ); if (this.#validators.length === 0 && resultsStatus === false) { throw new Error('No validators to validate, but validation failed'); } if (this.messages === undefined) { - // This Context has been destroyed while is was validating, so we should not continue. + // This Context has been destroyed while is was validating, so we should not continue. [NL] return Promise.reject(); } + // We need to ask again for messages, as they might have been added during the validation process. [NL] const hasMessages = this.messages.getHasAnyMessages(); // If we have any messages then we are not valid, otherwise lets check the validation results: [NL] @@ -398,17 +449,20 @@ export class UmbValidationController extends UmbControllerBase implements UmbVal } override destroy(): void { + this.#validationMode = false; if (this.#inUnprovidingState === true) { return; } + this.#destroyValidators(); this.unprovide(); + this.messages?.destroy(); + (this.messages as unknown) = undefined; if (this.#parent) { this.#parent.removeValidator(this); } + this.#localMessages = undefined; + this.#parentMessages = undefined; this.#parent = undefined; - this.#destroyValidators(); - this.messages?.destroy(); - (this.messages as unknown) = undefined; super.destroy(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts index 1e988153e2..f018231dd1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts @@ -103,7 +103,7 @@ export abstract class UmbSubmittableWorkspaceContextBase // TODO: Implement developer-mode logging here. [NL] console.warn( 'Validation failed because of these validation messages still begin present: ', - this.#validationContexts.flatMap((x) => x.messages.getFilteredMessages()), + this.#validationContexts.flatMap((x) => x.messages.getMessages()), ); onInvalid(error).then(this.#resolveSubmit, this.#rejectSubmit); }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts index f9f8a9d5b0..0749d8fbd2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts @@ -145,6 +145,7 @@ export abstract class UmbPropertyEditorUiRteElementBase if (dataPath) { // Set the data path for the local validation context: this.#validationContext.setDataPath(dataPath + '.blocks'); + this.#validationContext.autoReport(); } }, 'observeDataPath', From b5bffdfadae452ec913ab722fd8195cbe6804973 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Tue, 25 Mar 2025 17:28:24 +0100 Subject: [PATCH 18/19] Adjusting some of the colors for the dark theme so the ui looks a bit better when using dark theme (#18792) --- src/Umbraco.Web.UI.Client/src/css/dark.theme.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/css/dark.theme.css b/src/Umbraco.Web.UI.Client/src/css/dark.theme.css index 7c2823052d..5b3cb0429e 100644 --- a/src/Umbraco.Web.UI.Client/src/css/dark.theme.css +++ b/src/Umbraco.Web.UI.Client/src/css/dark.theme.css @@ -16,7 +16,7 @@ --uui-color-focus: #316dca; --uui-color-surface: #2d333b; --uui-color-surface-alt: #373e47; - --uui-color-surface-emphasis: #434c56; + --uui-color-surface-emphasis: #333a42; --uui-color-background: #21262e; --uui-color-text: #eeeeef; --uui-color-text-alt: #eeeeef; @@ -29,8 +29,8 @@ --uui-color-divider-standalone: #434c56; --uui-color-divider-emphasis: #545d68; --uui-color-default: #316dca; - --uui-color-default-emphasis: #316dca; - --uui-color-default-standalone: #316dca; + --uui-color-default-emphasis: #5387d5; + --uui-color-default-standalone: #eeeeef; --uui-color-default-contrast: #eeeeef; --uui-color-warning: #af7c12; --uui-color-warning-emphasis: #af7c12; From ccc70fc786cf3fb443f3007d0e594320d4e54381 Mon Sep 17 00:00:00 2001 From: Mathias Helsengren Date: Tue, 25 Mar 2025 17:28:53 +0100 Subject: [PATCH 19/19] Changes for the debug tag that makes it look better in dark mode (#18791) --- .../views/search/components/log-viewer-level-tag.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-level-tag.element.ts b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-level-tag.element.ts index 8b84e93388..8e42602bef 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-level-tag.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/log-viewer/workspace/views/search/components/log-viewer-level-tag.element.ts @@ -16,8 +16,8 @@ export class UmbLogViewerLevelTagElement extends LitElement { levelMap: Record = { Verbose: { look: 'secondary' }, Debug: { - look: 'default', - style: 'background-color: var(--umb-log-viewer-debug-color); color: var(--uui-color-surface)', + look: 'primary', + style: 'background-color: var(--umb-log-viewer-debug-color)', }, Information: { look: 'primary', color: 'positive' }, Warning: { look: 'primary', color: 'warning' },