diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b438a0027b..ed34279ab9 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -94,11 +94,18 @@ The solution contains 30 C# projects organized as follows: ## Common Tasks -### Frontend Development -For frontend-only changes: -1. Configure backend for frontend development: - ```json - +### Running Umbraco in Different Modes + +**Production Mode (Standard Development)** +Use this for backend development, testing full builds, or when you don't need hot reloading: +1. Build frontend assets: `cd src/Umbraco.Web.UI.Client && npm run build:for:cms` +2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build` +3. Access backoffice: `https://localhost:44339/umbraco` +4. Application uses compiled frontend from `wwwroot/umbraco/backoffice/` + +**Vite Dev Server Mode (Frontend Development with Hot Reload)** +Use this for frontend-only development with hot module reloading: +1. Configure backend for frontend development - Add to `src/Umbraco.Web.UI/appsettings.json` under `Umbraco:CMS:Security`: ```json "BackOfficeHost": "http://localhost:5173", "AuthorizeCallbackPathName": "/oauth_complete", @@ -107,6 +114,10 @@ For frontend-only changes: ``` 2. Run backend: `cd src/Umbraco.Web.UI && dotnet run --no-build` 3. Run frontend dev server: `cd src/Umbraco.Web.UI.Client && npm run dev:server` +4. Access backoffice: `http://localhost:5173/` (no `/umbraco` prefix) +5. Changes to TypeScript/Lit files hot reload automatically + +**Important:** Remove the `BackOfficeHost` configuration before committing or switching back to production mode. ### Backend-Only Development For backend-only changes, disable frontend builds: diff --git a/.vscode/launch.json b/.vscode/launch.json index ef4677989e..f4d47c3dab 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -101,6 +101,7 @@ "env": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:44339", + "UMBRACO__CMS__WEBROUTING__UMBRACOAPPLICATIONURL": "https://localhost:44339", "UMBRACO__CMS__SECURITY__BACKOFFICEHOST": "http://localhost:5173", "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKPATHNAME": "/oauth_complete", "UMBRACO__CMS__SECURITY__AUTHORIZECALLBACKLOGOUTPATHNAME": "/logout", diff --git a/src/Umbraco.Cms.Api.Common/Accessors/RequestContextOutputExpansionStrategyAccessor.cs b/src/Umbraco.Cms.Api.Common/Accessors/RequestContextOutputExpansionStrategyAccessor.cs new file mode 100644 index 0000000000..cb375857b7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Accessors/RequestContextOutputExpansionStrategyAccessor.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core.DeliveryApi; + +namespace Umbraco.Cms.Api.Common.Accessors; + +public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase, IOutputExpansionStrategyAccessor +{ + public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor) + : base(httpContextAccessor) + { + } +} diff --git a/src/Umbraco.Cms.Api.Common/Accessors/RequestContextServiceAccessorBase.cs b/src/Umbraco.Cms.Api.Common/Accessors/RequestContextServiceAccessorBase.cs new file mode 100644 index 0000000000..2748746a96 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Accessors/RequestContextServiceAccessorBase.cs @@ -0,0 +1,20 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Umbraco.Cms.Api.Common.Accessors; + +public abstract class RequestContextServiceAccessorBase + where T : class +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor) + => _httpContextAccessor = httpContextAccessor; + + public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService) + { + requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService(); + return requestStartNodeService is not null; + } +} diff --git a/src/Umbraco.Cms.Api.Common/Rendering/ElementOnlyOutputExpansionStrategy.cs b/src/Umbraco.Cms.Api.Common/Rendering/ElementOnlyOutputExpansionStrategy.cs new file mode 100644 index 0000000000..a5f113c7c7 --- /dev/null +++ b/src/Umbraco.Cms.Api.Common/Rendering/ElementOnlyOutputExpansionStrategy.cs @@ -0,0 +1,148 @@ +using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Common.Rendering; + +public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy +{ + protected const string All = "$all"; + protected const string None = ""; + protected const string ExpandParameterName = "expand"; + protected const string FieldsParameterName = "fields"; + + private readonly IApiPropertyRenderer _propertyRenderer; + + protected Stack ExpandProperties { get; } = new(); + + protected Stack IncludeProperties { get; } = new(); + + public ElementOnlyOutputExpansionStrategy( + IApiPropertyRenderer propertyRenderer) + { + _propertyRenderer = propertyRenderer; + } + + public virtual IDictionary MapContentProperties(IPublishedContent content) + => content.ItemType == PublishedItemType.Content + ? MapProperties(content.Properties) + : throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}"); + + public virtual IDictionary MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true) + { + if (media.ItemType != PublishedItemType.Media) + { + throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}"); + } + + IPublishedProperty[] properties = media + .Properties + .Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false) + .ToArray(); + + return properties.Any() + ? MapProperties(properties) + : new Dictionary(); + } + + public virtual IDictionary MapElementProperties(IPublishedElement element) + => MapProperties(element.Properties, true); + + private IDictionary MapProperties(IEnumerable properties, bool forceExpandProperties = false) + { + Node? currentExpandProperties = ExpandProperties.Count > 0 ? ExpandProperties.Peek() : null; + if (ExpandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false) + { + return new Dictionary(); + } + + Node? currentIncludeProperties = IncludeProperties.Count > 0 ? IncludeProperties.Peek() : null; + var result = new Dictionary(); + foreach (IPublishedProperty property in properties) + { + Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias); + if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null) + { + continue; + } + + Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias); + + IncludeProperties.Push(nextIncludeProperties); + ExpandProperties.Push(nextExpandProperties); + + result[property.Alias] = GetPropertyValue(property); + + ExpandProperties.Pop(); + IncludeProperties.Pop(); + } + + return result; + } + + private Node? GetNextProperties(Node? currentProperties, string propertyAlias) + => currentProperties?.Items.FirstOrDefault(i => i.Key == All) + ?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias); + + private object? GetPropertyValue(IPublishedProperty property) + => _propertyRenderer.GetPropertyValue(property, ExpandProperties.Peek() is not null); + + protected sealed class Node + { + public string Key { get; private set; } = string.Empty; + + public List Items { get; } = new(); + + public static Node Parse(string value) + { + // verify that there are as many start brackets as there are end brackets + if (value.CountOccurrences("[") != value.CountOccurrences("]")) + { + throw new ArgumentException("Value did not contain an equal number of start and end brackets"); + } + + // verify that the value does not start with a start bracket + if (value.StartsWith("[")) + { + throw new ArgumentException("Value cannot start with a bracket"); + } + + // verify that there are no empty brackets + if (value.Contains("[]")) + { + throw new ArgumentException("Value cannot contain empty brackets"); + } + + var stack = new Stack(); + var root = new Node { Key = "root" }; + stack.Push(root); + + var currentNode = new Node(); + root.Items.Add(currentNode); + + foreach (char c in value) + { + switch (c) + { + case '[': // Start a new node, child of the current node + stack.Push(currentNode); + currentNode = new Node(); + stack.Peek().Items.Add(currentNode); + break; + case ',': // Start a new node, but at the same level of the current node + currentNode = new Node(); + stack.Peek().Items.Add(currentNode); + break; + case ']': // Back to parent of the current node + currentNode = stack.Pop(); + break; + default: // Add char to current node key + currentNode.Key += c; + break; + } + } + + return root; + } + } +} diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs index 7d20039897..a0860a3422 100644 --- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs @@ -35,28 +35,35 @@ public static class UmbracoBuilderExtensions builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); - builder.Services.AddScoped(provider => - { - HttpContext? httpContext = provider.GetRequiredService().HttpContext; - ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion(); - if (apiVersion is null) - { - return provider.GetRequiredService(); - } - // V1 of the Delivery API uses a different expansion strategy than V2+ - return apiVersion.MajorVersion == 1 - ? provider.GetRequiredService() - : provider.GetRequiredService(); - }); + builder.Services.AddUnique( + provider => + { + HttpContext? httpContext = provider.GetRequiredService().HttpContext; + ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion(); + if (apiVersion is null) + { + return provider.GetRequiredService(); + } + + // V1 of the Delivery API uses a different expansion strategy than V2+ + return apiVersion.MajorVersion == 1 + ? provider.GetRequiredService() + : provider.GetRequiredService(); + }, + ServiceLifetime.Scoped); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + + // Webooks register a more basic implementation, remove it. + builder.Services.AddUnique(ServiceLifetime.Singleton); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs index e1a29b3ec7..779ed31083 100644 --- a/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs +++ b/src/Umbraco.Cms.Api.Delivery/Rendering/RequestContextOutputExpansionStrategyV2.cs @@ -1,62 +1,25 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using Umbraco.Cms.Api.Common.Rendering; using Umbraco.Cms.Core.DeliveryApi; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Extensions; namespace Umbraco.Cms.Api.Delivery.Rendering; -internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy +internal sealed class RequestContextOutputExpansionStrategyV2 : ElementOnlyOutputExpansionStrategy, IOutputExpansionStrategy { - private const string All = "$all"; - private const string None = ""; - private const string ExpandParameterName = "expand"; - private const string FieldsParameterName = "fields"; - - private readonly IApiPropertyRenderer _propertyRenderer; private readonly ILogger _logger; - private readonly Stack _expandProperties; - private readonly Stack _includeProperties; - public RequestContextOutputExpansionStrategyV2( IHttpContextAccessor httpContextAccessor, IApiPropertyRenderer propertyRenderer, ILogger logger) + : base(propertyRenderer) { - _propertyRenderer = propertyRenderer; _logger = logger; - _expandProperties = new Stack(); - _includeProperties = new Stack(); InitializeExpandAndInclude(httpContextAccessor); } - public IDictionary MapContentProperties(IPublishedContent content) - => content.ItemType == PublishedItemType.Content - ? MapProperties(content.Properties) - : throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}"); - - public IDictionary MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true) - { - if (media.ItemType != PublishedItemType.Media) - { - throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}"); - } - - IPublishedProperty[] properties = media - .Properties - .Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false) - .ToArray(); - - return properties.Any() - ? MapProperties(properties) - : new Dictionary(); - } - - public IDictionary MapElementProperties(IPublishedElement element) - => MapProperties(element.Properties, true); - private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor) { string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key]; @@ -66,7 +29,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion try { - _expandProperties.Push(Node.Parse(toExpand)); + ExpandProperties.Push(Node.Parse(toExpand)); } catch (ArgumentException ex) { @@ -76,7 +39,7 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion try { - _includeProperties.Push(Node.Parse(toInclude)); + IncludeProperties.Push(Node.Parse(toInclude)); } catch (ArgumentException ex) { @@ -84,102 +47,4 @@ internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansion throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}"); } } - - private IDictionary MapProperties(IEnumerable properties, bool forceExpandProperties = false) - { - Node? currentExpandProperties = _expandProperties.Peek(); - if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false) - { - return new Dictionary(); - } - - Node? currentIncludeProperties = _includeProperties.Peek(); - var result = new Dictionary(); - foreach (IPublishedProperty property in properties) - { - Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias); - if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null) - { - continue; - } - - Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias); - - _includeProperties.Push(nextIncludeProperties); - _expandProperties.Push(nextExpandProperties); - - result[property.Alias] = GetPropertyValue(property); - - _expandProperties.Pop(); - _includeProperties.Pop(); - } - - return result; - } - - private Node? GetNextProperties(Node? currentProperties, string propertyAlias) - => currentProperties?.Items.FirstOrDefault(i => i.Key == All) - ?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias); - - private object? GetPropertyValue(IPublishedProperty property) - => _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null); - - private sealed class Node - { - public string Key { get; private set; } = string.Empty; - - public List Items { get; } = new(); - - public static Node Parse(string value) - { - // verify that there are as many start brackets as there are end brackets - if (value.CountOccurrences("[") != value.CountOccurrences("]")) - { - throw new ArgumentException("Value did not contain an equal number of start and end brackets"); - } - - // verify that the value does not start with a start bracket - if (value.StartsWith("[")) - { - throw new ArgumentException("Value cannot start with a bracket"); - } - - // verify that there are no empty brackets - if (value.Contains("[]")) - { - throw new ArgumentException("Value cannot contain empty brackets"); - } - - var stack = new Stack(); - var root = new Node { Key = "root" }; - stack.Push(root); - - var currentNode = new Node(); - root.Items.Add(currentNode); - - foreach (char c in value) - { - switch (c) - { - case '[': // Start a new node, child of the current node - stack.Push(currentNode); - currentNode = new Node(); - stack.Peek().Items.Add(currentNode); - break; - case ',': // Start a new node, but at the same level of the current node - currentNode = new Node(); - stack.Peek().Items.Add(currentNode); - break; - case ']': // Back to parent of the current node - currentNode = stack.Pop(); - break; - default: // Add char to current node key - currentNode.Key += c; - break; - } - } - - return root; - } - } } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs index 8d2d20a1d6..81d764f697 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/WebhooksBuilderExtensions.cs @@ -1,5 +1,9 @@ -using Umbraco.Cms.Api.Management.Factories; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.Accessors; +using Umbraco.Cms.Api.Common.Rendering; +using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Mapping.Webhook; +using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Extensions; @@ -12,6 +16,10 @@ internal static class WebhooksBuilderExtensions builder.Services.AddUnique(); builder.AddMapDefinition(); + // deliveryApi will overwrite these more basic ones. + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + return builder; } } diff --git a/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs b/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs index 07baedc473..f242ef20a9 100644 --- a/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs +++ b/src/Umbraco.Core/Notifications/ContentPublishedNotification.cs @@ -24,6 +24,7 @@ public sealed class ContentPublishedNotification : EnumerableObjectNotification< public ContentPublishedNotification(IEnumerable target, EventMessages messages, bool includeDescendants) : base(target, messages) => IncludeDescendants = includeDescendants; + /// /// Gets a enumeration of which are being published. /// diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 884eac7dd7..eebf54e503 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -2214,7 +2214,7 @@ public class ContentService : RepositoryService, IContentService variesByCulture ? culturesPublished.IsCollectionEmpty() ? null : culturesPublished : ["*"], null, eventMessages)); - scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages).WithState(notificationState)); + scope.Notifications.Publish(new ContentPublishedNotification(publishedDocuments, eventMessages, true).WithState(notificationState)); scope.Complete(); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs index efa48f00f2..71c824f357 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs @@ -6,7 +6,9 @@ using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_3_0; @@ -69,7 +71,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase private void IfNotExistsCreateBytesLabel() { - if (Database.Exists(Constants.DataTypes.LabelBytes)) + if (NodeExists(_labelBytesDataTypeKey)) { return; } @@ -89,7 +91,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase CreateDate = DateTime.Now, }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); var dataTypeDto = new DataTypeDto { @@ -100,12 +102,12 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase Configuration = "{\"umbracoDataValueType\":\"BIGINT\", \"labelTemplate\":\"{=value | bytes}\"}", }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); } private void IfNotExistsCreatePixelsLabel() { - if (Database.Exists(Constants.DataTypes.LabelPixels)) + if (NodeExists(_labelPixelsDataTypeKey)) { return; } @@ -125,7 +127,7 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase CreateDate = DateTime.Now, }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); var dataTypeDto = new DataTypeDto { @@ -136,7 +138,16 @@ public class MigrateMediaTypeLabelProperties : AsyncMigrationBase Configuration = "{\"umbracoDataValueType\":\"INT\", \"labelTemplate\":\"{=value}px\"}", }; - _ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + + private bool NodeExists(Guid uniqueId) + { + Sql sql = Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .Where(x => x.UniqueId == uniqueId); + return Database.FirstOrDefault(sql) is not null; } private async Task MigrateMediaTypeLabels() diff --git a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts index b987fc3a64..dff606e519 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/dictionary/workspace/views/workspace-view-dictionary-editor.element.ts @@ -1,7 +1,6 @@ import { UMB_DICTIONARY_WORKSPACE_CONTEXT } from '../dictionary-workspace.context-token.js'; import type { UmbDictionaryDetailModel } from '../../types.js'; import type { UUITextareaElement } from '@umbraco-cms/backoffice/external/uui'; -import { UUITextareaEvent } from '@umbraco-cms/backoffice/external/uui'; import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; @@ -72,13 +71,11 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { } #onTextareaChange(e: Event) { - if (e instanceof UUITextareaEvent) { - const target = e.composedPath()[0] as UUITextareaElement; - const translation = (target.value as string).toString(); - const isoCode = target.getAttribute('name')!; + const target = e.composedPath()[0] as UUITextareaElement; + const translation = (target.value as string).toString(); + const isoCode = target.getAttribute('name')!; - this.#workspaceContext?.setPropertyValue(isoCode, translation); - } + this.#workspaceContext?.setPropertyValue(isoCode, translation); } override render() { @@ -104,7 +101,7 @@ export class UmbWorkspaceViewDictionaryEditorElement extends UmbLitElement { slot="editor" name=${language.unique} label="translation" - @change=${this.#onTextareaChange} + @input=${this.#onTextareaChange} .value=${translation?.translation ?? ''} ?readonly=${this.#isReadOnly(language.unique)}> `; diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts index a62f7b004e..5b1e1b150d 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithImageCropper.spec.ts @@ -60,6 +60,8 @@ test('can publish content with the image cropper data type', {tag: '@smoke'}, as // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.uploadFile(imageFilePath); + // Wait for the upload to complete + await umbracoUi.waitForTimeout(1000); await umbracoUi.content.clickSaveAndPublishButton(); // Assert