Merge remote-tracking branch 'origin/release/16.0' into v16/merge-16-release-to-main
# Conflicts: # build/nightly-E2E-test-pipelines.yml # src/Umbraco.Web.UI.Client/eslint.config.js # src/Umbraco.Web.UI.Client/package-lock.json # src/Umbraco.Web.UI.Client/package.json # tests/Umbraco.Tests.AcceptanceTest/package-lock.json # tests/Umbraco.Tests.AcceptanceTest/package.json # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/VariantBlockGrid.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/VariantBlockList.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithCheckboxList.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithDropdown.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithRadiobox.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextarea.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithTextstring.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RedirectManagement.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/ContentWithTiptap.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/RichTextEditor/VariantTipTapBlocks.spec.ts # tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Dictionary/Dictionary.spec.ts # version.json
This commit is contained in:
@@ -432,6 +432,33 @@ stages:
|
||||
displayName: Start SQL Server Docker image (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- powershell: |
|
||||
$maxAttempts = 12
|
||||
$attempt = 0
|
||||
$status = ""
|
||||
|
||||
while (($status -ne 'running') -and ($attempt -lt $maxAttempts)) {
|
||||
Start-Sleep -Seconds 5
|
||||
# We use the docker inspect command to check the status of the container. If the container is not running, we wait 5 seconds and try again. And if reaches 12 attempts, we fail the build.
|
||||
$status = docker inspect -f '{{.State.Status}}' mssql
|
||||
|
||||
if ($status -ne 'running') {
|
||||
Write-Host "Waiting for SQL Server to be ready... Attempt $($attempt + 1)"
|
||||
$attempt++
|
||||
}
|
||||
}
|
||||
|
||||
if ($status -eq 'running') {
|
||||
Write-Host "SQL Server container is running"
|
||||
docker ps -a
|
||||
} else {
|
||||
Write-Host "SQL Server did not become ready in time. Last known status: $status"
|
||||
docker logs mssql
|
||||
exit 1
|
||||
}
|
||||
displayName: Wait for SQL Server to be ready (Linux)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
|
||||
|
||||
- pwsh: SqlLocalDB start MSSQLLocalDB
|
||||
displayName: Start SQL Server LocalDB (Windows)
|
||||
condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
@@ -300,17 +300,17 @@ stages:
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
LinuxPart2Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=2/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
LinuxPart3Of3:
|
||||
testCommand: "npm run testSqlite -- --shard=3/3"
|
||||
vmImage: "ubuntu-latest"
|
||||
SA_PASSWORD: $(UMBRACO__CMS__UNATTENDED__UNATTENDEDUSERPASSWORD)
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);TrustServerCertificate=True"
|
||||
CONNECTIONSTRINGS__UMBRACODBDSN: "Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True"
|
||||
WindowsPart1Of3:
|
||||
vmImage: "windows-latest"
|
||||
testCommand: "npm run testSqlite -- --shard=1/3"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -42,12 +42,16 @@ public class CopyDocumentController : DocumentControllerBase
|
||||
Guid id,
|
||||
CopyDocumentRequestModel copyDocumentRequestModel)
|
||||
{
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
AuthorizationResult sourceAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionCopy.ActionLetter, new[] { copyDocumentRequestModel.Target?.Id, id }),
|
||||
ContentPermissionResource.WithKeys(ActionCopy.ActionLetter, [id]),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
AuthorizationResult destinationAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, [copyDocumentRequestModel.Target?.Id]),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (!authorizationResult.Succeeded)
|
||||
if (sourceAuthorizationResult.Succeeded is false || destinationAuthorizationResult.Succeeded is false)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
@@ -28,12 +28,13 @@ public class SearchDocumentItemController : DocumentItemControllerBase
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery] IEnumerable<Guid>? allowedDocumentTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Document, query, parentId, allowedDocumentTypes, trashed, culture, skip, take);
|
||||
var result = new PagedModel<DocumentItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IDocumentEntitySlim>().Select(_documentPresentationFactory.CreateItemResponseModel),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Asp.Versioning;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -39,6 +39,20 @@ public class MoveDocumentController : DocumentControllerBase
|
||||
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Move(CancellationToken cancellationToken, Guid id, MoveDocumentRequestModel moveDocumentRequestModel)
|
||||
{
|
||||
AuthorizationResult sourceAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionMove.ActionLetter, [id]),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
AuthorizationResult destinationAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, [moveDocumentRequestModel.Target?.Id]),
|
||||
AuthorizationPolicies.ContentPermissionByResource);
|
||||
|
||||
if (sourceAuthorizationResult.Succeeded is false || destinationAuthorizationResult.Succeeded is false)
|
||||
{
|
||||
return Forbidden();
|
||||
}
|
||||
|
||||
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
|
||||
User,
|
||||
ContentPermissionResource.WithKeys(ActionMove.ActionLetter, new[] { moveDocumentRequestModel.Target?.Id, id }),
|
||||
|
||||
@@ -21,17 +21,20 @@ public class SearchMediaItemController : MediaItemControllerBase
|
||||
_mediaPresentationFactory = mediaPresentationFactory;
|
||||
}
|
||||
|
||||
[NonAction]
|
||||
[Obsolete("Scheduled to be removed in v16, use the non obsoleted method instead")]
|
||||
public Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
=> SearchFromParentWithAllowedTypes(cancellationToken, query, null, skip, take, parentId);
|
||||
|
||||
[HttpGet("search")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedModel<MediaItemResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(CancellationToken cancellationToken, string query, bool? trashed = null, int skip = 0, int take = 100, Guid? parentId = null, [FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
public async Task<IActionResult> SearchFromParentWithAllowedTypes(
|
||||
CancellationToken cancellationToken,
|
||||
string query,
|
||||
bool? trashed = null,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
Guid? parentId = null,
|
||||
[FromQuery]IEnumerable<Guid>? allowedMediaTypes = null)
|
||||
{
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, skip, take);
|
||||
PagedModel<IEntitySlim> searchResult = await _indexedEntitySearchService.SearchAsync(UmbracoObjectTypes.Media, query, parentId, allowedMediaTypes, trashed, culture, skip, take);
|
||||
var result = new PagedModel<MediaItemResponseModel>
|
||||
{
|
||||
Items = searchResult.Items.OfType<IMediaEntitySlim>().Select(_mediaPresentationFactory.CreateItemResponseModel),
|
||||
|
||||
@@ -14,7 +14,6 @@ using Umbraco.Cms.Web.Common.Authorization;
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.Segment;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[Authorize(Policy = AuthorizationPolicies.SectionAccessSettings)]
|
||||
public class AllSegmentController : SegmentControllerBase
|
||||
{
|
||||
private readonly ISegmentService _segmentService;
|
||||
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Api.Management.NotificationHandlers;
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Api.Management.Services;
|
||||
using Umbraco.Cms.Api.Management.Telemetry;
|
||||
@@ -12,6 +13,7 @@ using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Net;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
@@ -74,6 +76,9 @@ public static partial class UmbracoBuilderExtensions
|
||||
|
||||
services.AddScoped<IBackOfficeExternalLoginService, BackOfficeExternalLoginService>();
|
||||
|
||||
// Register a notification handler to interrogate the registered external login providers at startup.
|
||||
builder.AddNotificationHandler<UmbracoApplicationStartingNotification, ExternalLoginProviderStartupHandler>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using Umbraco.Cms.Api.Management.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates backoffice sessions and clears external logins for removed providers if the external login
|
||||
/// provider setup has changed.
|
||||
/// </summary>
|
||||
internal sealed class ExternalLoginProviderStartupHandler : INotificationHandler<UmbracoApplicationStartingNotification>
|
||||
{
|
||||
private readonly IBackOfficeExternalLoginProviders _backOfficeExternalLoginProviders;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IServerRoleAccessor _serverRoleAccessor;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ExternalLoginProviderStartupHandler"/> class.
|
||||
/// </summary>
|
||||
public ExternalLoginProviderStartupHandler(
|
||||
IBackOfficeExternalLoginProviders backOfficeExternalLoginProviders,
|
||||
IRuntimeState runtimeState,
|
||||
IServerRoleAccessor serverRoleAccessor)
|
||||
{
|
||||
_backOfficeExternalLoginProviders = backOfficeExternalLoginProviders;
|
||||
_runtimeState = runtimeState;
|
||||
_serverRoleAccessor = serverRoleAccessor;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(UmbracoApplicationStartingNotification notification)
|
||||
{
|
||||
if (_runtimeState.Level != RuntimeLevel.Run ||
|
||||
_serverRoleAccessor.CurrentServerRole == ServerRole.Subscriber)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_backOfficeExternalLoginProviders.InvalidateSessionsIfExternalLoginProvidersChanged();
|
||||
}
|
||||
}
|
||||
@@ -10159,6 +10159,13 @@
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "culture",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"in": "query",
|
||||
@@ -15918,6 +15925,13 @@
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "culture",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"in": "query",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Management.Security;
|
||||
|
||||
@@ -8,13 +12,37 @@ public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProvider
|
||||
{
|
||||
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
|
||||
private readonly Dictionary<string, BackOfficeExternalLoginProvider> _externalLogins;
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly IExternalLoginWithKeyService _externalLoginWithKeyService;
|
||||
private readonly ILogger<BackOfficeExternalLoginProviders> _logger;
|
||||
|
||||
private const string ExternalLoginProvidersKey = "Umbraco.Cms.Web.BackOffice.Security.BackOfficeExternalLoginProviders";
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public BackOfficeExternalLoginProviders(
|
||||
IEnumerable<BackOfficeExternalLoginProvider> externalLogins,
|
||||
IAuthenticationSchemeProvider authenticationSchemeProvider)
|
||||
: this(
|
||||
externalLogins,
|
||||
authenticationSchemeProvider,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IKeyValueService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IExternalLoginWithKeyService>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILogger<BackOfficeExternalLoginProviders>>())
|
||||
{
|
||||
}
|
||||
|
||||
public BackOfficeExternalLoginProviders(
|
||||
IEnumerable<BackOfficeExternalLoginProvider> externalLogins,
|
||||
IAuthenticationSchemeProvider authenticationSchemeProvider,
|
||||
IKeyValueService keyValueService,
|
||||
IExternalLoginWithKeyService externalLoginWithKeyService,
|
||||
ILogger<BackOfficeExternalLoginProviders> logger)
|
||||
{
|
||||
_externalLogins = externalLogins.ToDictionary(x => x.AuthenticationType);
|
||||
_authenticationSchemeProvider = authenticationSchemeProvider;
|
||||
_keyValueService = keyValueService;
|
||||
_externalLoginWithKeyService = externalLoginWithKeyService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -60,4 +88,25 @@ public class BackOfficeExternalLoginProviders : IBackOfficeExternalLoginProvider
|
||||
var found = _externalLogins.Values.Where(x => x.Options.DenyLocalLogin).ToList();
|
||||
return found.Count > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InvalidateSessionsIfExternalLoginProvidersChanged()
|
||||
{
|
||||
var previousExternalLoginProvidersValue = _keyValueService.GetValue(ExternalLoginProvidersKey);
|
||||
var currentExternalLoginProvidersValue = string.Join("|", _externalLogins.Keys.OrderBy(key => key));
|
||||
|
||||
if ((previousExternalLoginProvidersValue ?? string.Empty) != currentExternalLoginProvidersValue)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"The configured external login providers have changed. Existing backoffice sessions using the removed providers will be invalidated and external login data removed.");
|
||||
|
||||
_externalLoginWithKeyService.PurgeLoginsForRemovedProviders(_externalLogins.Keys);
|
||||
|
||||
_keyValueService.SetValue(ExternalLoginProvidersKey, currentExternalLoginProvidersValue);
|
||||
}
|
||||
else if (previousExternalLoginProvidersValue is null)
|
||||
{
|
||||
_keyValueService.SetValue(ExternalLoginProvidersKey, string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,4 +23,11 @@ public interface IBackOfficeExternalLoginProviders
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool HasDenyLocalLogin();
|
||||
|
||||
/// <summary>
|
||||
/// Used during startup to see if the configured external login providers is different from the persisted information.
|
||||
/// If they are different, this will invalidate backoffice sessions and clear external logins for removed providers
|
||||
/// if the external login provider setup has changed.
|
||||
/// </summary>
|
||||
void InvalidateSessionsIfExternalLoginProvidersChanged() { }
|
||||
}
|
||||
|
||||
@@ -28,8 +28,8 @@ public class SecuritySettings
|
||||
|
||||
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
internal const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
internal const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
internal const string StaticAuthorizeCallbackPathName = "/umbraco/oauth_complete";
|
||||
internal const string StaticAuthorizeCallbackLogoutPathName = "/umbraco/logout";
|
||||
internal const string StaticAuthorizeCallbackErrorPathName = "/umbraco/error";
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class IntExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Does something 'x' amount of times
|
||||
/// Does something 'x' amount of times.
|
||||
/// </summary>
|
||||
/// <param name="n"></param>
|
||||
/// <param name="action"></param>
|
||||
/// <param name="n">Number of times to execute the action.</param>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
public static void Times(this int n, Action<int> action)
|
||||
{
|
||||
for (var i = 0; i < n; i++)
|
||||
@@ -19,11 +21,11 @@ public static class IntExtensions
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Guid based on an integer value
|
||||
/// Creates a Guid based on an integer value.
|
||||
/// </summary>
|
||||
/// <param name="value"><see cref="int" /> value to convert</param>
|
||||
/// <param name="value">The <see cref="int" /> value to convert.</param>
|
||||
/// <returns>
|
||||
/// <see cref="Guid" />
|
||||
/// The converted <see cref="Guid" />.
|
||||
/// </returns>
|
||||
public static Guid ToGuid(this int value)
|
||||
{
|
||||
@@ -31,4 +33,25 @@ public static class IntExtensions
|
||||
BitConverter.GetBytes(value).CopyTo(bytes);
|
||||
return new Guid(bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores a GUID previously created from an integer value using <see cref="ToGuid" />.
|
||||
/// </summary>
|
||||
/// <param name="value">The <see cref="Guid" /> value to convert.</param>
|
||||
/// <param name="result">The converted <see cref="int" />.</param>
|
||||
/// <returns>
|
||||
/// True if the <see cref="int" /> value could be created, otherwise false.
|
||||
/// </returns>
|
||||
public static bool TryParseFromGuid(Guid value, [NotNullWhen(true)] out int? result)
|
||||
{
|
||||
if (value.ToString().EndsWith("-0000-0000-0000-000000000000") is false)
|
||||
{
|
||||
// We have a proper GUID, not one converted from an integer.
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = BitConverter.ToInt32(value.ToByteArray());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1575,4 +1575,22 @@ public static class StringExtensions
|
||||
// this is by far the fastest way to find string needles in a string haystack
|
||||
public static int CountOccurrences(this string haystack, string needle)
|
||||
=> haystack.Length - haystack.Replace(needle, string.Empty).Length;
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the provided string is a valid culture code and returns it in a consistent casing.
|
||||
/// </summary>
|
||||
/// <param name="culture">Culture code.</param>
|
||||
/// <returns>Culture code in standard casing.</returns>
|
||||
public static string? EnsureCultureCode(this string? culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(culture) || culture == "*")
|
||||
{
|
||||
return culture;
|
||||
}
|
||||
|
||||
// Create as CultureInfo instance from provided name so we can ensure consistent casing of culture code when persisting.
|
||||
// This will accept mixed case but once created have a `Name` property that is consistently and correctly cased.
|
||||
// Will throw in an invalid culture code is provided.
|
||||
return new CultureInfo(culture).Name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Specialized;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Runtime.ConstrainedExecution;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Extensions;
|
||||
@@ -288,6 +290,8 @@ public abstract class ContentBase : TreeEntityBase, IContentBase
|
||||
// set on variant content type
|
||||
if (ContentType.VariesByCulture())
|
||||
{
|
||||
culture = culture.EnsureCultureCode();
|
||||
|
||||
// invariant is ok
|
||||
if (culture.IsNullOrWhiteSpace())
|
||||
{
|
||||
@@ -297,7 +301,7 @@ public abstract class ContentBase : TreeEntityBase, IContentBase
|
||||
// clear
|
||||
else if (name.IsNullOrWhiteSpace())
|
||||
{
|
||||
ClearCultureInfo(culture!);
|
||||
ClearCultureInfo(culture);
|
||||
}
|
||||
|
||||
// set
|
||||
@@ -322,11 +326,6 @@ public abstract class ContentBase : TreeEntityBase, IContentBase
|
||||
|
||||
private void ClearCultureInfo(string culture)
|
||||
{
|
||||
if (culture == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(culture));
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(culture))
|
||||
{
|
||||
throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture));
|
||||
@@ -455,6 +454,7 @@ public abstract class ContentBase : TreeEntityBase, IContentBase
|
||||
$"No PropertyType exists with the supplied alias \"{propertyTypeAlias}\".");
|
||||
}
|
||||
|
||||
culture = culture.EnsureCultureCode();
|
||||
var updated = property.SetValue(value, culture, segment);
|
||||
if (updated)
|
||||
{
|
||||
|
||||
@@ -3,23 +3,29 @@ using Umbraco.Cms.Core.Security;
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for external logins with Guid as key, so it can be shared for members and users
|
||||
/// Repository for external logins with Guid as key, so it can be shared for members and users.
|
||||
/// </summary>
|
||||
public interface IExternalLoginWithKeyRepository : IReadWriteQueryRepository<int, IIdentityUserLogin>,
|
||||
IQueryRepository<IIdentityUserToken>
|
||||
{
|
||||
/// <summary>
|
||||
/// Replaces all external login providers for the user/member key
|
||||
/// Replaces all external login providers for the user/member key.
|
||||
/// </summary>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all external login provider tokens for the providers specified for the user/member key
|
||||
/// Replaces all external login provider tokens for the providers specified for the user/member key.
|
||||
/// </summary>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLoginToken> tokens);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all external logins for the specified the user/member key
|
||||
/// Deletes all external logins for the specified the user/member key.
|
||||
/// </summary>
|
||||
void DeleteUserLogins(Guid userOrMemberKey);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes external logins that aren't associated with the current collection of providers.
|
||||
/// </summary>
|
||||
/// <param name="currentLoginProviders">The names of the currently configured providers.</param>
|
||||
void DeleteUserLoginsForRemovedProviders(IEnumerable<string> currentLoginProviders) { }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Linq.Expressions;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
|
||||
@@ -160,4 +160,10 @@ public interface IUserRepository : IReadWriteQueryRepository<Guid, IUser>
|
||||
bool RemoveClientId(int id, string clientId);
|
||||
|
||||
IUser? GetByClientId(string clientId);
|
||||
|
||||
/// <summary>
|
||||
/// Invalidates sessions for users that aren't associated with the current collection of providers.
|
||||
/// </summary>
|
||||
/// <param name="currentLoginProviders">The names of the currently configured providers.</param>
|
||||
void InvalidateSessionsForRemovedProviders(IEnumerable<string> currentLoginProviders) { }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
|
||||
@@ -12,14 +15,27 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
|
||||
// TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented
|
||||
// by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection
|
||||
|
||||
private readonly ILogger<DataValueReferenceFactoryCollection> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataValueReferenceFactoryCollection" /> class.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items)
|
||||
: base(items)
|
||||
: this(
|
||||
items,
|
||||
StaticServiceProvider.Instance.GetRequiredService<ILogger<DataValueReferenceFactoryCollection>>())
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataValueReferenceFactoryCollection" /> class.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items, ILogger<DataValueReferenceFactoryCollection> logger)
|
||||
: base(items) => _logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Gets all unique references from the specified properties.
|
||||
/// </summary>
|
||||
@@ -33,7 +49,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
|
||||
var references = new HashSet<UmbracoEntityReference>();
|
||||
|
||||
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
|
||||
foreach (var propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values))
|
||||
foreach (IGrouping<string, IReadOnlyCollection<IPropertyValue>> propertyValuesByPropertyEditorAlias in properties.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Values))
|
||||
{
|
||||
if (!propertyEditors.TryGet(propertyValuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
|
||||
{
|
||||
@@ -48,7 +64,7 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
|
||||
values.Add(propertyValue.PublishedValue);
|
||||
}
|
||||
|
||||
references.UnionWith(GetReferences(dataEditor, values));
|
||||
references.UnionWith(GetReferences(dataEditor, values, propertyValuesByPropertyEditorAlias.Key));
|
||||
}
|
||||
|
||||
return references;
|
||||
@@ -74,14 +90,18 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
|
||||
/// The references.
|
||||
/// </returns>
|
||||
public ISet<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, IEnumerable<object?> values) =>
|
||||
GetReferencesEnumerable(dataEditor, values).ToHashSet();
|
||||
private IEnumerable<UmbracoEntityReference> GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable<object?> values)
|
||||
GetReferencesEnumerable(dataEditor, values, null).ToHashSet();
|
||||
|
||||
private ISet<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, IEnumerable<object?> values, string propertyEditorAlias) =>
|
||||
GetReferencesEnumerable(dataEditor, values, propertyEditorAlias).ToHashSet();
|
||||
|
||||
private IEnumerable<UmbracoEntityReference> GetReferencesEnumerable(IDataEditor dataEditor, IEnumerable<object?> values, string? propertyEditorAlias)
|
||||
{
|
||||
// TODO: We will need to change this once we support tracking via variants/segments
|
||||
// for now, we are tracking values from ALL variants
|
||||
if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference)
|
||||
{
|
||||
foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences))
|
||||
foreach (UmbracoEntityReference reference in GetReferencesFromPropertyValues(values, dataValueReference, propertyEditorAlias))
|
||||
{
|
||||
yield return reference;
|
||||
}
|
||||
@@ -107,6 +127,38 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<UmbracoEntityReference> GetReferencesFromPropertyValues(IEnumerable<object?> values, IDataValueReference dataValueReference, string? propertyEditorAlias)
|
||||
{
|
||||
var result = new List<UmbracoEntityReference>();
|
||||
foreach (var value in values)
|
||||
{
|
||||
// When property editors on data types are changed, we could have values that are incompatible with the new editor.
|
||||
// Leading to issues such as:
|
||||
// - https://github.com/umbraco/Umbraco-CMS/issues/17628
|
||||
// - https://github.com/umbraco/Umbraco-CMS/issues/17725
|
||||
// Although some changes like this are not intended to be compatible, we should handle them gracefully and not
|
||||
// error in retrieving references, which would prevent manipulating or deleting the content that uses the data type.
|
||||
try
|
||||
{
|
||||
IEnumerable<UmbracoEntityReference> references = dataValueReference.GetReferences(value);
|
||||
result.AddRange(references);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the exception but don't throw, continue with the next value.
|
||||
_logger.LogError(
|
||||
ex,
|
||||
"Error getting references from value {Value} with data editor {DataEditor} and property editor alias {PropertyEditorAlias}.",
|
||||
value,
|
||||
dataValueReference.GetType().FullName,
|
||||
propertyEditorAlias ?? "n/a");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all relation type aliases that are automatically tracked.
|
||||
/// </summary>
|
||||
@@ -114,6 +166,10 @@ public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataVa
|
||||
/// <returns>
|
||||
/// All relation type aliases that are automatically tracked.
|
||||
/// </returns>
|
||||
[Obsolete("Use GetAllAutomaticRelationTypesAliases. This will be removed in Umbraco 15.")]
|
||||
public ISet<string> GetAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) =>
|
||||
GetAllAutomaticRelationTypesAliases(propertyEditors);
|
||||
|
||||
public ISet<string> GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
// Always add default automatic relation types
|
||||
|
||||
@@ -264,7 +264,16 @@ public sealed class AuditService : RepositoryService, IAuditService
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IAuditEntry Write(int performingUserId, string perfomingDetails, string performingIp, DateTime eventDateUtc, int affectedUserId, string? affectedDetails, string eventType, string eventDetails)
|
||||
[Obsolete("Will be moved to a new service in V17. Scheduled for removal in V18.")]
|
||||
public IAuditEntry Write(
|
||||
int performingUserId,
|
||||
string perfomingDetails,
|
||||
string performingIp,
|
||||
DateTime eventDateUtc,
|
||||
int affectedUserId,
|
||||
string? affectedDetails,
|
||||
string eventType,
|
||||
string eventDetails)
|
||||
{
|
||||
if (performingUserId < 0 && performingUserId != Constants.Security.SuperUserId)
|
||||
{
|
||||
|
||||
@@ -1184,6 +1184,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
throw new ArgumentException("Cultures cannot be null or whitespace", nameof(cultures));
|
||||
}
|
||||
|
||||
cultures = cultures.Select(x => x.EnsureCultureCode()!).ToArray();
|
||||
|
||||
EventMessages evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
// we need to guard against unsaved changes before proceeding; the content will be saved, but we're not firing any saved notifications
|
||||
@@ -1263,7 +1265,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
|
||||
EventMessages evtMsgs = EventMessagesFactory.Get();
|
||||
|
||||
culture = culture?.NullOrWhiteSpaceAsNull();
|
||||
culture = culture?.NullOrWhiteSpaceAsNull().EnsureCultureCode();
|
||||
|
||||
PublishedState publishedState = content.PublishedState;
|
||||
if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished)
|
||||
@@ -2063,7 +2065,7 @@ public class ContentService : RepositoryService, IContentService
|
||||
cultures = ["*"];
|
||||
}
|
||||
|
||||
return cultures;
|
||||
return cultures.Select(x => x.EnsureCultureCode()!).ToArray();
|
||||
}
|
||||
|
||||
private static bool ProvidedCulturesIndicatePublishAll(string[] cultures) => cultures.Length == 0 || (cultures.Length == 1 && cultures[0] == "invariant");
|
||||
|
||||
@@ -5,21 +5,40 @@ using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
public class ExternalLoginService : RepositoryService, IExternalLoginWithKeyService
|
||||
{
|
||||
private readonly IExternalLoginWithKeyRepository _externalLoginRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
public ExternalLoginService(
|
||||
ICoreScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IExternalLoginWithKeyRepository externalLoginRepository)
|
||||
: base(provider, loggerFactory, eventMessagesFactory) =>
|
||||
: this(
|
||||
provider,
|
||||
loggerFactory,
|
||||
eventMessagesFactory,
|
||||
externalLoginRepository,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserRepository>())
|
||||
{
|
||||
}
|
||||
|
||||
public ExternalLoginService(
|
||||
ICoreScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IExternalLoginWithKeyRepository externalLoginRepository,
|
||||
IUserRepository userRepository)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_externalLoginRepository = externalLoginRepository;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public IEnumerable<IIdentityUserLogin> Find(string loginProvider, string providerKey)
|
||||
{
|
||||
@@ -80,4 +99,15 @@ public class ExternalLoginService : RepositoryService, IExternalLoginWithKeyServ
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void PurgeLoginsForRemovedProviders(IEnumerable<string> currentLoginProviders)
|
||||
{
|
||||
using (ICoreScope scope = ScopeProvider.CreateCoreScope())
|
||||
{
|
||||
_userRepository.InvalidateSessionsForRemovedProviders(currentLoginProviders);
|
||||
_externalLoginRepository.DeleteUserLoginsForRemovedProviders(currentLoginProviders);
|
||||
scope.Complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,7 @@ public interface IAuditService : IService
|
||||
/// </example>
|
||||
/// </param>
|
||||
/// <param name="eventDetails">Free-form details about the audited event.</param>
|
||||
[Obsolete("Will be moved to a new service in V17. Scheduled for removal in V18.")]
|
||||
IAuditEntry Write(
|
||||
int performingUserId,
|
||||
string perfomingDetails,
|
||||
|
||||
@@ -5,47 +5,53 @@ namespace Umbraco.Cms.Core.Services;
|
||||
public interface IExternalLoginWithKeyService : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all user logins assigned
|
||||
/// Returns all user logins assigned.
|
||||
/// </summary>
|
||||
IEnumerable<IIdentityUserLogin> GetExternalLogins(Guid userOrMemberKey);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all user login tokens assigned
|
||||
/// Returns all user login tokens assigned.
|
||||
/// </summary>
|
||||
IEnumerable<IIdentityUserToken> GetExternalLoginTokens(Guid userOrMemberKey);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all logins matching the login info - generally there should only be one but in some cases
|
||||
/// there might be more than one depending on if an administrator has been editing/removing members
|
||||
/// there might be more than one depending on if an administrator has been editing/removing members.
|
||||
/// </summary>
|
||||
IEnumerable<IIdentityUserLogin> Find(string loginProvider, string providerKey);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the external logins associated with the user
|
||||
/// Saves the external logins associated with the user.
|
||||
/// </summary>
|
||||
/// <param name="userOrMemberKey">
|
||||
/// The user or member key associated with the logins
|
||||
/// The user or member key associated with the logins.
|
||||
/// </param>
|
||||
/// <param name="logins"></param>
|
||||
/// <remarks>
|
||||
/// This will replace all external login provider information for the user
|
||||
/// This will replace all external login provider information for the user.
|
||||
/// </remarks>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the external login tokens associated with the user
|
||||
/// Saves the external login tokens associated with the user.
|
||||
/// </summary>
|
||||
/// <param name="userOrMemberKey">
|
||||
/// The user or member key associated with the logins
|
||||
/// The user or member key associated with the logins.
|
||||
/// </param>
|
||||
/// <param name="tokens"></param>
|
||||
/// <remarks>
|
||||
/// This will replace all external login tokens for the user
|
||||
/// This will replace all external login tokens for the user.
|
||||
/// </remarks>
|
||||
void Save(Guid userOrMemberKey, IEnumerable<IExternalLoginToken> tokens);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all user logins - normally used when a member is deleted
|
||||
/// Deletes all user logins - normally used when a member is deleted.
|
||||
/// </summary>
|
||||
void DeleteUserLogins(Guid userOrMemberKey);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes external logins and invalidates sessions for users that aren't associated with the current collection of providers.
|
||||
/// </summary>
|
||||
/// <param name="currentLoginProviders">The names of the currently configured providers.</param>
|
||||
void PurgeLoginsForRemovedProviders(IEnumerable<string> currentLoginProviders) { }
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public interface IIndexedEntitySearchService
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> Search(objectType,query, skip, take, ignoreUserStartNodes);
|
||||
=> Search(objectType, query, skip, take, ignoreUserStartNodes);
|
||||
|
||||
Task<PagedModel<IEntitySlim>> SearchAsync(
|
||||
UmbracoObjectTypes objectType,
|
||||
@@ -43,6 +43,7 @@ public interface IIndexedEntitySearchService
|
||||
Guid? parentId,
|
||||
IEnumerable<Guid>? contentTypeIds,
|
||||
bool? trashed,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false);
|
||||
|
||||
@@ -7,7 +7,11 @@ namespace Umbraco.Cms.Core.Templates;
|
||||
public sealed class HtmlImageSourceParser
|
||||
{
|
||||
private static readonly Regex ResolveImgPattern = new(
|
||||
@"(<img[^>]*src="")([^""\?]*)((?:\?[^""]*)?""[^>]*data-udi="")([^""]*)(""[^>]*>)",
|
||||
@"<img[^>]*(data-udi=""([^""]*)"")[^>]*>",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
private static readonly Regex SrcAttributeRegex = new(
|
||||
@"src=""([^""\?]*)(\?[^""]*)?""",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
|
||||
|
||||
private static readonly Regex DataUdiAttributeRegex = new(
|
||||
@@ -61,17 +65,26 @@ public sealed class HtmlImageSourceParser
|
||||
return ResolveImgPattern.Replace(text, match =>
|
||||
{
|
||||
// match groups:
|
||||
// - 1 = from the beginning of the image tag until src attribute value begins
|
||||
// - 2 = the src attribute value excluding the querystring (if present)
|
||||
// - 3 = anything after group 2 and before the data-udi attribute value begins
|
||||
// - 4 = the data-udi attribute value
|
||||
// - 5 = anything after group 4 until the image tag is closed
|
||||
var udi = match.Groups[4].Value;
|
||||
// - 1 = the data-udi attribute
|
||||
// - 2 = the data-udi attribute value
|
||||
var udi = match.Groups[2].Value;
|
||||
if (udi.IsNullOrWhiteSpace() || UdiParser.TryParse<GuidUdi>(udi, out GuidUdi? guidUdi) == false)
|
||||
{
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
// Find the src attribute
|
||||
// src match groups:
|
||||
// - 1 = the src attribute value until the query string
|
||||
// - 2 = the src attribute query string including the '?'
|
||||
Match src = SrcAttributeRegex.Match(match.Value);
|
||||
|
||||
if (src.Success == false)
|
||||
{
|
||||
// the src attribute isn't found, return the original value
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
var mediaUrl = _getMediaUrl(guidUdi.Guid);
|
||||
if (mediaUrl == null)
|
||||
{
|
||||
@@ -80,7 +93,9 @@ public sealed class HtmlImageSourceParser
|
||||
return match.Value;
|
||||
}
|
||||
|
||||
return $"{match.Groups[1].Value}{mediaUrl}{match.Groups[3].Value}{udi}{match.Groups[5].Value}";
|
||||
var newImgTag = match.Value.Replace(src.Value, $"src=\"{mediaUrl}{src.Groups[2].Value}\"");
|
||||
|
||||
return newImgTag;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -91,6 +106,16 @@ public sealed class HtmlImageSourceParser
|
||||
/// <returns></returns>
|
||||
public string RemoveImageSources(string text)
|
||||
|
||||
// see comment in ResolveMediaFromTextString for group reference
|
||||
=> ResolveImgPattern.Replace(text, "$1$3$4$5");
|
||||
// find each ResolveImgPattern match in the text, then find each
|
||||
// SrcAttributeRegex match in the match value, then replace the src
|
||||
// attribute value with an empty string
|
||||
// (see comment in ResolveMediaFromTextString for group reference)
|
||||
=> ResolveImgPattern.Replace(text, match =>
|
||||
{
|
||||
// Find the src attribute
|
||||
Match src = SrcAttributeRegex.Match(match.Value);
|
||||
|
||||
return src.Success == false || string.IsNullOrWhiteSpace(src.Groups[1].Value) ?
|
||||
match.Value : match.Value.Replace(src.Groups[1].Value, string.Empty);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -248,6 +248,8 @@ public static partial class UmbracoBuilderExtensions
|
||||
|
||||
builder.Services.AddSingleton<IRichTextRequiredValidator, RichTextRequiredValidator>();
|
||||
|
||||
builder.Services.AddSingleton<IRichTextRegexValidator, RichTextRegexValidator>();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,12 @@ internal class ExternalLoginRepository : EntityRepositoryBase<int, IIdentityUser
|
||||
public void DeleteUserLogins(Guid userOrMemberKey) =>
|
||||
Database.Delete<ExternalLoginDto>("WHERE userOrMemberKey=@userOrMemberKey", new { userOrMemberKey });
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteUserLoginsForRemovedProviders(IEnumerable<string> currentLoginProviders) =>
|
||||
Database.Execute(Sql()
|
||||
.Delete<ExternalLoginDto>()
|
||||
.WhereNotIn<ExternalLoginDto>(x => x.LoginProvider, currentLoginProviders));
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Save(Guid userOrMemberKey, IEnumerable<IExternalLogin> logins)
|
||||
{
|
||||
|
||||
@@ -12,7 +12,6 @@ using Umbraco.Cms.Core.Models.Entities;
|
||||
using Umbraco.Cms.Core.Models.Membership;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
@@ -21,7 +20,6 @@ using Umbraco.Cms.Infrastructure.Persistence.Mappers;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Querying;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Extensions;
|
||||
using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
/// <summary>
|
||||
@@ -1268,5 +1266,32 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void InvalidateSessionsForRemovedProviders(IEnumerable<string> currentLoginProviders)
|
||||
{
|
||||
// Get all the user or member keys associated with the removed providers.
|
||||
Sql<ISqlContext> idsQuery = SqlContext.Sql()
|
||||
.Select<ExternalLoginDto>(x => x.UserOrMemberKey)
|
||||
.From<ExternalLoginDto>()
|
||||
.WhereNotIn<ExternalLoginDto>(x => x.LoginProvider, currentLoginProviders);
|
||||
List<Guid> userAndMemberKeysAssociatedWithRemovedProviders = Database.Fetch<Guid>(idsQuery);
|
||||
if (userAndMemberKeysAssociatedWithRemovedProviders.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate the security stamps on the users associated with the removed providers.
|
||||
Sql<ISqlContext> updateSecurityStampsQuery = Sql()
|
||||
.Update<UserDto>(u => u.Set(x => x.SecurityStampToken, "0".PadLeft(32, '0')))
|
||||
.WhereIn<UserDto>(x => x.Key, userAndMemberKeysAssociatedWithRemovedProviders);
|
||||
Database.Execute(updateSecurityStampsQuery);
|
||||
|
||||
// Delete the OpenIddict tokens for the users associated with the removed providers.
|
||||
// The following is safe from SQL injection as we are dealing with GUIDs, not strings.
|
||||
var userKeysForInClause = string.Join("','", userAndMemberKeysAssociatedWithRemovedProviders.Select(x => x.ToString()));
|
||||
Database.Execute("DELETE FROM umbracoOpenIddictTokens WHERE Subject IN ('" + userKeysForInClause + "')");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -71,17 +71,17 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
continue;
|
||||
}
|
||||
|
||||
var districtValues = valuesByPropertyEditorAlias.Distinct().ToArray();
|
||||
var distinctValues = valuesByPropertyEditorAlias.Distinct().ToArray();
|
||||
|
||||
if (dataEditor.GetValueEditor() is IDataValueReference reference)
|
||||
{
|
||||
foreach (UmbracoEntityReference value in districtValues.SelectMany(reference.GetReferences))
|
||||
foreach (UmbracoEntityReference value in distinctValues.SelectMany(reference.GetReferences))
|
||||
{
|
||||
result.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerable<UmbracoEntityReference> references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, districtValues);
|
||||
IEnumerable<UmbracoEntityReference> references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, distinctValues);
|
||||
|
||||
foreach (UmbracoEntityReference value in references)
|
||||
{
|
||||
|
||||
@@ -93,6 +93,7 @@ public class RichTextPropertyEditor : DataEditor
|
||||
private readonly RichTextEditorPastedImages _pastedImages;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IRichTextRequiredValidator _richTextRequiredValidator;
|
||||
private readonly IRichTextRegexValidator _richTextRegexValidator;
|
||||
private readonly ILogger<RichTextPropertyValueEditor> _logger;
|
||||
|
||||
public RichTextPropertyValueEditor(
|
||||
@@ -111,6 +112,7 @@ public class RichTextPropertyEditor : DataEditor
|
||||
IPropertyValidationService propertyValidationService,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection,
|
||||
IRichTextRequiredValidator richTextRequiredValidator,
|
||||
IRichTextRegexValidator richTextRegexValidator,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IIOHelper ioHelper)
|
||||
@@ -122,6 +124,7 @@ public class RichTextPropertyEditor : DataEditor
|
||||
_pastedImages = pastedImages;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_richTextRequiredValidator = richTextRequiredValidator;
|
||||
_richTextRegexValidator = richTextRegexValidator;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
|
||||
@@ -131,6 +134,8 @@ public class RichTextPropertyEditor : DataEditor
|
||||
|
||||
public override IValueRequiredValidator RequiredValidator => _richTextRequiredValidator;
|
||||
|
||||
public override IValueFormatValidator FormatValidator => _richTextRegexValidator;
|
||||
|
||||
protected override RichTextBlockValue CreateWithLayout(IEnumerable<RichTextBlockLayoutItem> layout) => new(layout);
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
|
||||
|
||||
internal interface IRichTextRegexValidator : IValueFormatValidator
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
|
||||
|
||||
internal class RichTextRegexValidator : IRichTextRegexValidator
|
||||
{
|
||||
private readonly RegexValidator _regexValidator;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly ILogger<RichTextRegexValidator> _logger;
|
||||
|
||||
public RichTextRegexValidator(
|
||||
IJsonSerializer jsonSerializer,
|
||||
ILogger<RichTextRegexValidator> logger)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_logger = logger;
|
||||
_regexValidator = new RegexValidator();
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> ValidateFormat(object? value, string? valueType, string format) => _regexValidator.ValidateFormat(GetValue(value), valueType, format);
|
||||
|
||||
private object? GetValue(object? value) =>
|
||||
RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue)
|
||||
? richTextEditorValue?.Markup
|
||||
: value;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
=> SearchAsync(objectType, query, parentId, contentTypeIds, trashed, skip, take, ignoreUserStartNodes).GetAwaiter().GetResult();
|
||||
=> SearchAsync(objectType, query, parentId, contentTypeIds, trashed, null, skip, take, ignoreUserStartNodes).GetAwaiter().GetResult();
|
||||
|
||||
public Task<PagedModel<IEntitySlim>> SearchAsync(
|
||||
UmbracoObjectTypes objectType,
|
||||
@@ -81,6 +81,7 @@ internal sealed class IndexedEntitySearchService : IIndexedEntitySearchService
|
||||
Guid? parentId,
|
||||
IEnumerable<Guid>? contentTypeIds,
|
||||
bool? trashed,
|
||||
string? culture = null,
|
||||
int skip = 0,
|
||||
int take = 100,
|
||||
bool ignoreUserStartNodes = false)
|
||||
|
||||
@@ -326,8 +326,9 @@ public class PackagingService : IPackagingService
|
||||
PackageName = group.Key.PackageName,
|
||||
};
|
||||
|
||||
var packageKey = Constants.Conventions.Migrations.KeyValuePrefix + (group.Key.PackageId ?? group.Key.PackageName);
|
||||
var currentState = keyValues?
|
||||
.GetValueOrDefault(Constants.Conventions.Migrations.KeyValuePrefix + group.Key.PackageId);
|
||||
.GetValueOrDefault(packageKey);
|
||||
|
||||
package.PackageMigrationPlans = group
|
||||
.Select(plan => new InstalledPackageMigrationPlans
|
||||
|
||||
@@ -31,7 +31,7 @@ public class ConfigureSecurityStampOptions : IConfigureOptions<SecurityStampVali
|
||||
// Adjust the security stamp validation interval to a shorter duration
|
||||
// when concurrent logins are not allowed and the duration has the default interval value
|
||||
// (currently defaults to 30 minutes), ensuring quicker re-validation.
|
||||
if (securitySettings.AllowConcurrentLogins is false && options.ValidationInterval == TimeSpan.FromMinutes(30))
|
||||
if (securitySettings.AllowConcurrentLogins is false && options.ValidationInterval == new SecurityStampValidatorOptions().ValidationInterval)
|
||||
{
|
||||
options.ValidationInterval = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ export class ExampleBlockCustomView extends UmbElementMixin(LitElement) implemen
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
z-index: 10000;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: red;
|
||||
@@ -38,6 +40,12 @@ export class ExampleBlockCustomView extends UmbElementMixin(LitElement) implemen
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
:host > div {
|
||||
position: relative;
|
||||
display: block;
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -62,8 +62,9 @@ export default {
|
||||
unlock: 'Lås op',
|
||||
createblueprint: 'Opret indholdsskabelon',
|
||||
resendInvite: 'Gensend Invitation',
|
||||
editContent: 'Edit content',
|
||||
chooseWhereToImport: 'Choose where to import',
|
||||
editContent: 'Rediger indhold',
|
||||
chooseWhereToImport: 'Vælg hvor du vil importere',
|
||||
viewActionsFor: (name) => (name ? `Se handlinger for '${name}'` : 'Se handlinger'),
|
||||
},
|
||||
actionCategories: {
|
||||
content: 'Indhold',
|
||||
|
||||
@@ -72,6 +72,7 @@ export default {
|
||||
wasCopiedTo: 'was copied to',
|
||||
wasDeleted: 'was deleted',
|
||||
wasMovedTo: 'was moved to',
|
||||
viewActionsFor: (name) => (name ? `View actions for '${name}'` : 'View actions'),
|
||||
},
|
||||
actionCategories: {
|
||||
content: 'Content',
|
||||
@@ -159,7 +160,7 @@ export default {
|
||||
saveAndPublish: 'Save and publish',
|
||||
saveToPublish: 'Save and send for approval',
|
||||
saveListView: 'Save list view',
|
||||
schedulePublish: 'Schedule',
|
||||
schedulePublish: 'Schedule publish',
|
||||
saveAndPreview: 'Save and preview',
|
||||
showPageDisabled: "Preview is disabled because there's no template assigned",
|
||||
styleChoose: 'Choose style',
|
||||
@@ -271,7 +272,7 @@ export default {
|
||||
publishedPendingChanges: 'Published (pending changes)',
|
||||
publishStatus: 'Publication Status',
|
||||
publishDescendantsHelp:
|
||||
'Publish <strong>%0%</strong> and all content items underneath and thereby making their content publicly available.',
|
||||
'Publish <strong>%0%</strong> and all items underneath and thereby making their content publicly available.',
|
||||
publishDescendantsWithVariantsHelp:
|
||||
'Publish variants and variants of same type underneath and thereby making their content publicly available.',
|
||||
noVariantsToProcess: 'There are no available variants',
|
||||
@@ -319,7 +320,7 @@ export default {
|
||||
addTextBox: 'Add another text box',
|
||||
removeTextBox: 'Remove this text box',
|
||||
contentRoot: 'Content root',
|
||||
includeUnpublished: 'Include unpublished content items.',
|
||||
includeUnpublished: 'Include unpublished items.',
|
||||
isSensitiveValue:
|
||||
'This value is hidden. If you need access to view this value please contact your website administrator.',
|
||||
isSensitiveValue_short: 'This value is hidden.',
|
||||
@@ -348,6 +349,8 @@ export default {
|
||||
variantUnpublishNotAllowed: 'Unpublish is not allowed',
|
||||
selectAllVariants: 'Select all variants',
|
||||
saveModalTitle: 'Save',
|
||||
saveAndPublishModalTitle: 'Save and publish',
|
||||
publishModalTitle: 'Publish',
|
||||
},
|
||||
blueprints: {
|
||||
createBlueprintFrom: "Create a new Document Blueprint from '%0%'",
|
||||
@@ -475,10 +478,11 @@ export default {
|
||||
discardChanges: 'Discard changes',
|
||||
unsavedChanges: 'Discard unsaved changes',
|
||||
unsavedChangesWarning: 'Are you sure you want to navigate away from this page? You have unsaved changes',
|
||||
confirmListViewPublish: 'Publishing will make the selected items visible on the site.',
|
||||
confirmListViewUnpublish: 'Unpublishing will remove the selected items and all their descendants from the site.',
|
||||
confirmPublish: 'Publishing will make this page and all its published descendants visible on the site.',
|
||||
confirmUnpublish: 'Unpublishing will remove this page and all its descendants from the site.',
|
||||
confirmListViewPublish: 'Publishing will make the selected items publicly available.',
|
||||
confirmListViewUnpublish:
|
||||
'Unpublishing will make the selected items and all their descendants publicly unavailable.',
|
||||
confirmPublish: 'Publishing will make this content and all its published descendants publicly available.',
|
||||
confirmUnpublish: 'Unpublishing will make this content publicly unavailable.',
|
||||
doctypeChangeWarning: 'You have unsaved changes. Making changes to the Document Type will discard the changes.',
|
||||
},
|
||||
bulk: {
|
||||
@@ -911,7 +915,7 @@ export default {
|
||||
retrieve: 'Retrieve',
|
||||
retry: 'Retry',
|
||||
rights: 'Permissions',
|
||||
scheduledPublishing: 'Scheduled Publishing',
|
||||
scheduledPublishing: 'Schedule publish',
|
||||
umbracoInfo: 'Umbraco info',
|
||||
search: 'Search',
|
||||
searchNoResult: 'Sorry, we can not find what you are looking for.',
|
||||
@@ -2795,7 +2799,7 @@ export default {
|
||||
modalSource: 'Source',
|
||||
modalManual: 'Manual',
|
||||
modalAnchorValidationMessage:
|
||||
'Please enter an anchor or querystring, or select a published document or media item, or manually configure the URL.',
|
||||
'Please enter an anchor or querystring, select a document or media item, or manually configure the URL.',
|
||||
resetUrlHeadline: 'Reset URL?',
|
||||
resetUrlMessage: 'Are you sure you want to reset this URL?',
|
||||
resetUrlLabel: 'Reset',
|
||||
|
||||
@@ -84,7 +84,7 @@ export class UmbArrayState<T, U = unknown> extends UmbDeepState<T[]> {
|
||||
|
||||
/**
|
||||
* @function remove
|
||||
* @param {unknown[]} uniques - The unique values to remove.
|
||||
* @param {U[]} uniques - The unique values to remove.
|
||||
* @returns {UmbArrayState<T>} Reference to it self.
|
||||
* @description - Remove some new data of this Subject.
|
||||
* @example <caption>Example remove entry with id '1' and '2'</caption>
|
||||
@@ -95,7 +95,7 @@ export class UmbArrayState<T, U = unknown> extends UmbDeepState<T[]> {
|
||||
* const myState = new UmbArrayState(data, (x) => x.id);
|
||||
* myState.remove([1, 2]);
|
||||
*/
|
||||
remove(uniques: unknown[]) {
|
||||
remove(uniques: U[]) {
|
||||
if (this.getUniqueMethod) {
|
||||
let next = this.getValue();
|
||||
if (!next) return this;
|
||||
@@ -114,7 +114,7 @@ export class UmbArrayState<T, U = unknown> extends UmbDeepState<T[]> {
|
||||
|
||||
/**
|
||||
* @function removeOne
|
||||
* @param {unknown} unique - The unique value to remove.
|
||||
* @param {U} unique - The unique value to remove.
|
||||
* @returns {UmbArrayState<T>} Reference to it self.
|
||||
* @description - Remove some new data of this Subject.
|
||||
* @example <caption>Example remove entry with id '1'</caption>
|
||||
@@ -125,7 +125,7 @@ export class UmbArrayState<T, U = unknown> extends UmbDeepState<T[]> {
|
||||
* const myState = new UmbArrayState(data, (x) => x.id);
|
||||
* myState.removeOne(1);
|
||||
*/
|
||||
removeOne(unique: unknown) {
|
||||
removeOne(unique: U) {
|
||||
if (this.getUniqueMethod) {
|
||||
let next = this.getValue();
|
||||
if (!next) return this;
|
||||
@@ -251,7 +251,7 @@ export class UmbArrayState<T, U = unknown> extends UmbDeepState<T[]> {
|
||||
|
||||
/**
|
||||
* @function updateOne
|
||||
* @param {unknown} unique - Unique value to find entry to update.
|
||||
* @param {U} unique - Unique value to find entry to update.
|
||||
* @param {Partial<T>} entry - new data to be added in this Subject.
|
||||
* @returns {UmbArrayState<T>} Reference to it self.
|
||||
* @description - Update a item with some new data, requires the ArrayState to be constructed with a getUnique method.
|
||||
@@ -263,7 +263,7 @@ export class UmbArrayState<T, U = unknown> extends UmbDeepState<T[]> {
|
||||
* const myState = new UmbArrayState(data, (x) => x.key);
|
||||
* myState.updateOne(2, {value: 'updated-bar'});
|
||||
*/
|
||||
updateOne(unique: unknown, entry: Partial<T>) {
|
||||
updateOne(unique: U, entry: Partial<T>) {
|
||||
if (!this.getUniqueMethod) {
|
||||
throw new Error("Can't partial update an ArrayState without a getUnique method provided when constructed.");
|
||||
}
|
||||
|
||||
@@ -427,6 +427,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
#extensionSlotRenderMethod = (ext: UmbExtensionElementInitializer<ManifestBlockEditorCustomView>) => {
|
||||
if (ext.component) {
|
||||
ext.component.classList.add('umb-block-grid__block--view');
|
||||
ext.component.setAttribute('part', 'component');
|
||||
}
|
||||
if (this._exposed) {
|
||||
return ext.component;
|
||||
@@ -641,6 +642,11 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper
|
||||
border-color: var(--uui-color-invalid);
|
||||
}
|
||||
|
||||
umb-extension-slot::part(component) {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
#invalidLocation {
|
||||
position: absolute;
|
||||
top: -1em;
|
||||
|
||||
@@ -346,6 +346,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
};
|
||||
|
||||
#extensionSlotRenderMethod = (ext: UmbExtensionElementInitializer<ManifestBlockEditorCustomView>) => {
|
||||
ext.component?.setAttribute('part', 'component');
|
||||
if (this._exposed) {
|
||||
return ext.component;
|
||||
} else {
|
||||
@@ -511,6 +512,11 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper
|
||||
border-color: var(--uui-color-invalid);
|
||||
}
|
||||
|
||||
umb-extension-slot::part(component) {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
uui-action-bar {
|
||||
position: absolute;
|
||||
top: var(--uui-size-2);
|
||||
|
||||
@@ -243,6 +243,7 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
};
|
||||
|
||||
#extensionSlotRenderMethod = (ext: UmbExtensionElementInitializer<ManifestBlockEditorCustomView>) => {
|
||||
ext.component?.setAttribute('part', 'component');
|
||||
if (this._exposed) {
|
||||
return ext.component;
|
||||
} else {
|
||||
@@ -345,6 +346,12 @@ export class UmbBlockRteEntryElement extends UmbLitElement implements UmbPropert
|
||||
outline: 3px solid var(--uui-color-focus);
|
||||
}
|
||||
}
|
||||
|
||||
umb-extension-slot::part(component) {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
uui-action-bar {
|
||||
position: absolute;
|
||||
top: var(--uui-size-2);
|
||||
|
||||
@@ -39,7 +39,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
|
||||
_dataOwner?: UmbBlockElementManager;
|
||||
|
||||
@state()
|
||||
_variantId?: UmbVariantId;
|
||||
_workspaceVariantId?: UmbVariantId;
|
||||
|
||||
@state()
|
||||
_visibleProperties?: Array<UmbPropertyTypeModel>;
|
||||
@@ -56,7 +56,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
|
||||
this.observe(
|
||||
workspaceContext?.variantId,
|
||||
(variantId) => {
|
||||
this._variantId = variantId;
|
||||
this._workspaceVariantId = variantId;
|
||||
this.#processPropertyStructure();
|
||||
},
|
||||
'observeVariantId',
|
||||
@@ -83,16 +83,19 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#processPropertyStructure() {
|
||||
if (!this._dataOwner || !this.#properties || !this.#propertyStructureHelper) {
|
||||
if (!this._dataOwner || !this.#properties || !this.#propertyStructureHelper || !this._workspaceVariantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyViewGuard = this._dataOwner.propertyViewGuard;
|
||||
|
||||
this.#properties.forEach((property) => {
|
||||
const propertyVariantId = new UmbVariantId(this._variantId?.culture, this._variantId?.segment);
|
||||
const propertyVariantId = new UmbVariantId(
|
||||
property.variesByCulture ? this._workspaceVariantId!.culture : null,
|
||||
property.variesBySegment ? this._workspaceVariantId!.segment : null,
|
||||
);
|
||||
this.observe(
|
||||
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property),
|
||||
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property, this._workspaceVariantId!),
|
||||
(permitted) => {
|
||||
if (permitted) {
|
||||
this.#visiblePropertiesUniques.push(property.unique);
|
||||
@@ -117,7 +120,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
override render() {
|
||||
return this._variantId && this._visibleProperties
|
||||
return this._workspaceVariantId && this._visibleProperties
|
||||
? repeat(
|
||||
this._visibleProperties,
|
||||
(property) => property.alias,
|
||||
@@ -126,7 +129,7 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement {
|
||||
class="property"
|
||||
.ownerContext=${this._dataOwner}
|
||||
.ownerEntityType=${this._ownerEntityType}
|
||||
.variantId=${this._variantId}
|
||||
.variantId=${this._workspaceVariantId}
|
||||
.property=${property}></umb-block-workspace-view-edit-property>`,
|
||||
)
|
||||
: nothing;
|
||||
|
||||
@@ -38,7 +38,11 @@ export class UmbBlockWorkspaceViewEditPropertyElement extends UmbLitElement {
|
||||
})}].value`;
|
||||
|
||||
this.observe(
|
||||
this.ownerContext.propertyWriteGuard.isPermittedForVariantAndProperty(propertyVariantId, this.property),
|
||||
this.ownerContext.propertyWriteGuard.isPermittedForVariantAndProperty(
|
||||
propertyVariantId,
|
||||
this.property,
|
||||
this.variantId,
|
||||
),
|
||||
(write) => {
|
||||
this._writeable = write;
|
||||
},
|
||||
|
||||
@@ -150,7 +150,7 @@ export class UmbClipboardEntryPickerElement extends UmbLitElement {
|
||||
slot="actions"
|
||||
.entityType=${item.entityType}
|
||||
.unique=${item.unique}
|
||||
.label=${item.name}>
|
||||
.label=${this.localize.term('actions_viewActionsFor', [item.name])}>
|
||||
</umb-entity-actions-bundle>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
|
||||
@state()
|
||||
private _usedForInheritance: Array<string> = [];
|
||||
|
||||
@state()
|
||||
private _usedForComposition: Array<string> = [];
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -53,6 +56,7 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
|
||||
|
||||
this._selection = this.data?.selection ?? [];
|
||||
this._usedForInheritance = this.data?.usedForInheritance ?? [];
|
||||
this._usedForComposition = this.data?.usedForComposition ?? [];
|
||||
this.modalContext?.setValue({ selection: this._selection });
|
||||
|
||||
const isNew = this.data!.isNew;
|
||||
@@ -131,7 +135,9 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
|
||||
override render() {
|
||||
return html`
|
||||
<umb-body-layout headline="${this.localize.term('contentTypeEditor_compositions')}">
|
||||
${this._references.length ? this.#renderHasReference() : this.#renderAvailableCompositions()}
|
||||
<uui-box>
|
||||
${this._references.length ? this.#renderHasReference() : this.#renderAvailableCompositions()}
|
||||
</uui-box>
|
||||
<div slot="actions">
|
||||
<uui-button label=${this.localize.term('general_close')} @click=${this._rejectModal}></uui-button>
|
||||
${!this._references.length
|
||||
@@ -213,11 +219,16 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
|
||||
(compositions) => compositions.unique,
|
||||
(compositions) => {
|
||||
const usedForInheritance = this._usedForInheritance.includes(compositions.unique);
|
||||
const usedForComposition = this._usedForComposition.includes(compositions.unique);
|
||||
/* The server will return isCompatible as false if the Doc Type is currently being used in a composition.
|
||||
Therefore, we need to account for this in the "isDisabled" check to ensure it remains enabled.
|
||||
Otherwise, it would become disabled and couldn't be deselected by the user. */
|
||||
const isDisabled = usedForInheritance || (compositions.isCompatible === false && !usedForComposition);
|
||||
return html`
|
||||
<uui-menu-item
|
||||
label=${this.localize.string(compositions.name)}
|
||||
?selectable=${!usedForInheritance}
|
||||
?disabled=${usedForInheritance}
|
||||
?disabled=${isDisabled}
|
||||
@selected=${() => this.#onSelectionAdd(compositions.unique)}
|
||||
@deselected=${() => this.#onSelectionRemove(compositions.unique)}
|
||||
?selected=${this._selection.find((unique) => unique === compositions.unique)}>
|
||||
@@ -251,6 +262,10 @@ export class UmbCompositionPickerModalElement extends UmbModalBaseElement<
|
||||
align-items: center;
|
||||
gap: var(--uui-size-3);
|
||||
}
|
||||
|
||||
.compositions-list {
|
||||
margin-block: var(--uui-size-3);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface UmbCompositionPickerModalData {
|
||||
compositionRepositoryAlias: string;
|
||||
selection: Array<string>;
|
||||
usedForInheritance: Array<string>;
|
||||
usedForComposition: Array<string>;
|
||||
unique: string | null;
|
||||
isElement: boolean;
|
||||
currentPropertyAliases: Array<string>;
|
||||
|
||||
@@ -194,6 +194,7 @@ export class UmbContentTypeContainerStructureHelper<T extends UmbContentTypeMode
|
||||
|
||||
// For that we get the owner containers first (We do not need to observe as this observation will be triggered if one of the owner containers change) [NL]
|
||||
this.#ownerChildContainers = this.#structure!.getOwnerContainers(this.#childType!, this.#containerId!) ?? [];
|
||||
|
||||
this.#childContainers.setValue(rootContainers);
|
||||
},
|
||||
'_observeRootContainers',
|
||||
|
||||
@@ -24,6 +24,7 @@ import { incrementString } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { firstValueFrom } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
|
||||
type UmbPropertyTypeUnique = UmbPropertyTypeModel['unique'];
|
||||
|
||||
@@ -114,12 +115,8 @@ export class UmbContentTypeStructureManager<
|
||||
readonly variesByCulture = createObservablePart(this.ownerContentType, (x) => x?.variesByCulture);
|
||||
readonly variesBySegment = createObservablePart(this.ownerContentType, (x) => x?.variesBySegment);
|
||||
|
||||
#containers: UmbArrayState<UmbPropertyTypeContainerModel> = new UmbArrayState<UmbPropertyTypeContainerModel>(
|
||||
[],
|
||||
(x) => x.id,
|
||||
);
|
||||
containerById(id: string) {
|
||||
return this.#containers.asObservablePart((x) => x.find((y) => y.id === id));
|
||||
return createObservablePart(this.#contentTypeContainers, (x) => x.find((y) => y.id === id));
|
||||
}
|
||||
|
||||
constructor(host: UmbControllerHost, typeRepository: UmbDetailRepository<T> | string) {
|
||||
@@ -143,11 +140,20 @@ export class UmbContentTypeStructureManager<
|
||||
this.#repoManager.entries,
|
||||
(entries) => {
|
||||
// Prevent updating once that are have edited here.
|
||||
entries = entries.filter(
|
||||
const entriesToBeUpdated = entries.filter(
|
||||
(x) => !(this.#editedTypes.getHasOne(x.unique) && this.#contentTypes.getHasOne(x.unique)),
|
||||
);
|
||||
|
||||
this.#contentTypes.append(entries);
|
||||
// Remove entries based on no-longer existing uniques:
|
||||
const entriesToBeRemoved = this.#contentTypes
|
||||
.getValue()
|
||||
.filter((entry) => !entries.some((x) => x.unique === entry.unique))
|
||||
.map((x) => x.unique);
|
||||
|
||||
this.#contentTypes.mute();
|
||||
this.#contentTypes.remove(entriesToBeRemoved);
|
||||
this.#contentTypes.append(entriesToBeUpdated);
|
||||
this.#contentTypes.unmute();
|
||||
},
|
||||
null,
|
||||
);
|
||||
@@ -161,13 +167,6 @@ export class UmbContentTypeStructureManager<
|
||||
},
|
||||
null,
|
||||
);
|
||||
this.observe(
|
||||
this.#contentTypeContainers,
|
||||
(contentTypeContainers) => {
|
||||
this.#containers.setValue(contentTypeContainers);
|
||||
},
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,6 +265,8 @@ export class UmbContentTypeStructureManager<
|
||||
}
|
||||
|
||||
async #loadContentTypeCompositions(contentTypeCompositions: T['compositions'] | undefined) {
|
||||
// Important to wait a JS-cycle, cause this is called by an observation of a state and this results in setting the value for the state(potentially in the same JS-cycle) then we need to make sure we don't trigger a new update before the old subscription chain is completed. [NL]
|
||||
await Promise.resolve();
|
||||
const ownerUnique = this.getOwnerContentTypeUnique();
|
||||
if (!ownerUnique) return;
|
||||
const compositionUniques = contentTypeCompositions?.map((x) => x.contentType.unique) ?? [];
|
||||
@@ -359,7 +360,7 @@ export class UmbContentTypeStructureManager<
|
||||
this.#editedTypes.appendOne(toContentTypeUnique);
|
||||
|
||||
// Find container.
|
||||
const container = this.#containers.getValue().find((x) => x.id === containerId);
|
||||
const container = (await firstValueFrom(this.#contentTypeContainers)).find((x) => x.id === containerId);
|
||||
if (!container) throw new Error('Container to clone was not found');
|
||||
|
||||
const clonedContainer: UmbPropertyTypeContainerModel = {
|
||||
@@ -433,38 +434,37 @@ export class UmbContentTypeStructureManager<
|
||||
sortOrder: sortOrder ?? 0,
|
||||
};
|
||||
|
||||
// Ensure
|
||||
this.ensureContainerNames(contentTypeUnique, type, parentId);
|
||||
|
||||
const contentTypes = this.#contentTypes.getValue();
|
||||
const containers = [...(contentTypes.find((x) => x.unique === contentTypeUnique)?.containers ?? [])];
|
||||
containers.push(container);
|
||||
|
||||
this.#contentTypes.updateOne(contentTypeUnique, { containers } as Partial<T>);
|
||||
|
||||
return container;
|
||||
return this.insertContainer(contentTypeUnique, container);
|
||||
}
|
||||
|
||||
/*async insertContainer(contentTypeUnique: string | null, container: UmbPropertyTypeContainerModel) {
|
||||
async insertContainer(contentTypeUnique: string | null, container: UmbPropertyTypeContainerModel) {
|
||||
await this.#init;
|
||||
contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!;
|
||||
const newContainer = { ...container };
|
||||
const type = newContainer.type;
|
||||
const parentId = newContainer.parent?.id ?? null;
|
||||
|
||||
// If we have a parent, we need to ensure it exists, and then update the parent property with the new container id.
|
||||
if (container.parent) {
|
||||
const parentContainer = await this.ensureContainerOf(container.parent.id, contentTypeUnique);
|
||||
if (newContainer.parent) {
|
||||
const parentContainer = await this.ensureContainerOf(newContainer.parent.id, contentTypeUnique);
|
||||
if (!parentContainer) {
|
||||
throw new Error('Container for inserting property could not be found or created');
|
||||
}
|
||||
container.parent.id = parentContainer.id;
|
||||
newContainer.parent.id = parentContainer.id;
|
||||
}
|
||||
|
||||
// Ensure
|
||||
this.ensureContainerNames(contentTypeUnique, type, parentId);
|
||||
|
||||
const frozenContainers =
|
||||
this.#contentTypes.getValue().find((x) => x.unique === contentTypeUnique)?.containers ?? [];
|
||||
|
||||
const containers = appendToFrozenArray(frozenContainers, container, (x) => x.id === container.id);
|
||||
const containers = appendToFrozenArray(frozenContainers, newContainer, (x) => x.id === newContainer.id);
|
||||
|
||||
this.#contentTypes.updateOne(contentTypeUnique, { containers } as Partial<T>);
|
||||
}*/
|
||||
|
||||
return newContainer;
|
||||
}
|
||||
|
||||
makeEmptyContainerName(
|
||||
containerId: string,
|
||||
@@ -537,7 +537,11 @@ export class UmbContentTypeStructureManager<
|
||||
this.#contentTypes.updateOne(contentTypeUnique, { containers });
|
||||
}
|
||||
|
||||
async removeContainer(contentTypeUnique: string | null, containerId: string | null = null) {
|
||||
async removeContainer(
|
||||
contentTypeUnique: string | null,
|
||||
containerId: string | null = null,
|
||||
args?: { preventRemovingProperties?: boolean },
|
||||
): Promise<void> {
|
||||
await this.#init;
|
||||
contentTypeUnique = contentTypeUnique ?? this.#ownerContentTypeUnique!;
|
||||
this.#editedTypes.appendOne(contentTypeUnique);
|
||||
@@ -552,12 +556,15 @@ export class UmbContentTypeStructureManager<
|
||||
.map((x) => x.id);
|
||||
const containers = frozenContainers.filter((x) => x.id !== containerId && x.parent?.id !== containerId);
|
||||
|
||||
const frozenProperties = contentType.properties;
|
||||
const properties = frozenProperties.filter((x) =>
|
||||
x.container ? !removedContainerIds.some((ids) => ids === x.container?.id) : true,
|
||||
);
|
||||
const updates: Partial<T> = { containers } as Partial<T>;
|
||||
|
||||
this.#contentTypes.updateOne(contentTypeUnique, { containers, properties } as Partial<T>);
|
||||
if (args?.preventRemovingProperties !== true) {
|
||||
updates.properties = contentType.properties.filter((x) =>
|
||||
x.container ? !removedContainerIds.some((ids) => ids === x.container?.id) : true,
|
||||
);
|
||||
}
|
||||
|
||||
this.#contentTypes.updateOne(contentTypeUnique, updates);
|
||||
}
|
||||
|
||||
async insertProperty(contentTypeUnique: string | null, property: UmbPropertyTypeModel) {
|
||||
@@ -655,6 +662,11 @@ export class UmbContentTypeStructureManager<
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async getOwnerPropertyById(propertyUnique: string | null): Promise<UmbPropertyTypeModel | undefined> {
|
||||
await this.#init;
|
||||
return this.getOwnerContentType()?.properties?.find((property) => property.unique === propertyUnique);
|
||||
}
|
||||
|
||||
async getPropertyStructureByAlias(propertyAlias: string) {
|
||||
await this.#init;
|
||||
for (const docType of this.#contentTypes.getValue()) {
|
||||
@@ -695,17 +707,19 @@ export class UmbContentTypeStructureManager<
|
||||
}
|
||||
|
||||
rootContainers(containerType: UmbPropertyContainerTypes) {
|
||||
return this.#containers.asObservablePart((data) => {
|
||||
return createObservablePart(this.#contentTypeContainers, (data) => {
|
||||
return data.filter((x) => x.parent === null && x.type === containerType);
|
||||
});
|
||||
}
|
||||
|
||||
getRootContainers(containerType: UmbPropertyContainerTypes) {
|
||||
return this.#containers.getValue().filter((x) => x.parent === null && x.type === containerType);
|
||||
async getRootContainers(containerType: UmbPropertyContainerTypes) {
|
||||
return (await firstValueFrom(this.#contentTypeContainers)).filter(
|
||||
(x) => x.parent === null && x.type === containerType,
|
||||
);
|
||||
}
|
||||
|
||||
async hasRootContainers(containerType: UmbPropertyContainerTypes) {
|
||||
return this.#containers.asObservablePart((data) => {
|
||||
return createObservablePart(this.#contentTypeContainers, (data) => {
|
||||
return data.filter((x) => x.parent === null && x.type === containerType).length > 0;
|
||||
});
|
||||
}
|
||||
@@ -719,7 +733,14 @@ export class UmbContentTypeStructureManager<
|
||||
);
|
||||
}
|
||||
|
||||
getOwnerContainers(containerType: UmbPropertyContainerTypes, parentId: string | null) {
|
||||
getOwnerContainerById(id: string | null): UmbPropertyTypeContainerModel | undefined {
|
||||
return this.getOwnerContentType()?.containers?.find((x) => x.id === id);
|
||||
}
|
||||
|
||||
getOwnerContainers(
|
||||
containerType: UmbPropertyContainerTypes,
|
||||
parentId: string | null,
|
||||
): Array<UmbPropertyTypeContainerModel> | undefined {
|
||||
return this.getOwnerContentType()?.containers?.filter(
|
||||
(x) => (parentId ? x.parent?.id === parentId : x.parent === null) && x.type === containerType,
|
||||
);
|
||||
@@ -730,14 +751,14 @@ export class UmbContentTypeStructureManager<
|
||||
}
|
||||
|
||||
containersOfParentId(parentId: string, containerType: UmbPropertyContainerTypes) {
|
||||
return this.#containers.asObservablePart((data) => {
|
||||
return createObservablePart(this.#contentTypeContainers, (data) => {
|
||||
return data.filter((x) => x.parent?.id === parentId && x.type === containerType);
|
||||
});
|
||||
}
|
||||
|
||||
// In future this might need to take parentName(parentId lookup) into account as well? otherwise containers that share same name and type will always be merged, but their position might be different and they should not be merged. [NL]
|
||||
containersByNameAndType(name: string, containerType: UmbPropertyContainerTypes) {
|
||||
return this.#containers.asObservablePart((data) => {
|
||||
return createObservablePart(this.#contentTypeContainers, (data) => {
|
||||
return data.filter((x) => x.name === name && x.type === containerType);
|
||||
});
|
||||
}
|
||||
@@ -748,7 +769,7 @@ export class UmbContentTypeStructureManager<
|
||||
parentName: string | null,
|
||||
parentType?: UmbPropertyContainerTypes,
|
||||
) {
|
||||
return this.#containers.asObservablePart((data) => {
|
||||
return createObservablePart(this.#contentTypeContainers, (data) => {
|
||||
return data.filter(
|
||||
(x) =>
|
||||
// Match name and type:
|
||||
@@ -795,17 +816,27 @@ export class UmbContentTypeStructureManager<
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all property aliases for the content type including inherited and composed content types.
|
||||
* @returns {Promise<Array<string>>} - A promise that will be resolved with the list of all content type property aliases.
|
||||
*/
|
||||
async getContentTypePropertyAliases() {
|
||||
return this.#contentTypes
|
||||
.getValue()
|
||||
.flatMap((x) => x.properties?.map((y) => y.alias) ?? [])
|
||||
.filter(UmbFilterDuplicateStrings);
|
||||
}
|
||||
|
||||
#clear() {
|
||||
this.#contentTypeObservers.forEach((observer) => observer.destroy());
|
||||
this.#contentTypeObservers = [];
|
||||
this.#containers.setValue([]);
|
||||
this.#repoManager?.clear();
|
||||
this.#contentTypes.setValue([]);
|
||||
this.#ownerContentTypeUnique = undefined;
|
||||
}
|
||||
|
||||
public override destroy() {
|
||||
this.#contentTypes.destroy();
|
||||
this.#containers.destroy();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export abstract class UmbContentTypeWorkspaceContextBase<
|
||||
): Promise<DetailModelType | undefined> {
|
||||
this.resetState();
|
||||
this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Creating ${this.getEntityType()} scaffold` });
|
||||
this.setParent(args.parent);
|
||||
this._internal_setCreateUnderParent(args.parent);
|
||||
|
||||
const request = this.structure.createScaffold(args.preset);
|
||||
this._getDataPromise = request;
|
||||
|
||||
@@ -97,6 +97,35 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement {
|
||||
i++;
|
||||
}
|
||||
},
|
||||
onRequestDrop: async ({ unique }) => {
|
||||
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
|
||||
if (!context) {
|
||||
throw new Error('Could not get Workspace Context');
|
||||
}
|
||||
return context.structure.getOwnerPropertyById(unique);
|
||||
},
|
||||
requestExternalRemove: async ({ item }) => {
|
||||
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
|
||||
if (!context) {
|
||||
throw new Error('Could not get Workspace Context');
|
||||
}
|
||||
return await context.structure.removeProperty(null, item.unique).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
},
|
||||
requestExternalInsert: async ({ item }) => {
|
||||
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
|
||||
if (!context) {
|
||||
throw new Error('Could not get Workspace Context');
|
||||
}
|
||||
const parent = this._containerId ? { id: this._containerId } : null;
|
||||
const updatedItem = { ...item, parent };
|
||||
return await context.structure.insertProperty(null, updatedItem).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
private _containerId: string | null | undefined;
|
||||
@@ -152,7 +181,7 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.#sorter.disable();
|
||||
//this.#sorter.disable();
|
||||
|
||||
this.consumeContext(UMB_CONTENT_TYPE_DESIGN_EDITOR_CONTEXT, (context) => {
|
||||
this.observe(
|
||||
@@ -160,9 +189,9 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement {
|
||||
(isSorting) => {
|
||||
this._sortModeActive = isSorting;
|
||||
if (isSorting) {
|
||||
this.#sorter.enable();
|
||||
//this.#sorter.enable();
|
||||
} else {
|
||||
this.#sorter.disable();
|
||||
//this.#sorter.disable();
|
||||
}
|
||||
},
|
||||
'_observeIsSorting',
|
||||
@@ -305,6 +334,16 @@ export class UmbContentTypeDesignEditorPropertiesElement extends UmbLitElement {
|
||||
static override styles = [
|
||||
UmbTextStyles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#property-list {
|
||||
/* enables dropping things into this despite it begin empty. */
|
||||
margin-top: -20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
#btn-add {
|
||||
width: 100%;
|
||||
--uui-button-height: var(--uui-size-14);
|
||||
|
||||
@@ -72,6 +72,35 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
|
||||
i++;
|
||||
}
|
||||
},
|
||||
onRequestDrop: async ({ unique }) => {
|
||||
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
|
||||
if (!context) {
|
||||
throw new Error('Could not get Workspace Context');
|
||||
}
|
||||
return context.structure.getOwnerContainerById(unique);
|
||||
},
|
||||
requestExternalRemove: async ({ item }) => {
|
||||
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
|
||||
if (!context) {
|
||||
throw new Error('Could not get Workspace Context');
|
||||
}
|
||||
return await context.structure.removeContainer(null, item.id, { preventRemovingProperties: true }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
},
|
||||
requestExternalInsert: async ({ item }) => {
|
||||
const context = await this.getContext(UMB_CONTENT_TYPE_WORKSPACE_CONTEXT);
|
||||
if (!context) {
|
||||
throw new Error('Could not get Workspace Context');
|
||||
}
|
||||
const parent = this.#containerId ? { id: this.#containerId } : null;
|
||||
const updatedItem = { ...item, parent };
|
||||
return await context.structure.insertContainer(null, updatedItem).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
#workspaceModal?: UmbModalRouteRegistrationController<
|
||||
@@ -231,9 +260,10 @@ export class UmbContentTypeDesignEditorTabElement extends UmbLitElement {
|
||||
.container-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
#convert-to-tab {
|
||||
.container-list #convert-to-tab {
|
||||
margin-bottom: var(--uui-size-layout-1);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@@ -389,17 +389,25 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
|
||||
const currentOwnerCompositionCompositions = currentOwnerCompositions.filter(
|
||||
(composition) => composition.compositionType === CompositionTypeModel.COMPOSITION,
|
||||
);
|
||||
|
||||
const currentOwnerCompositionCompositionUniques = currentOwnerCompositionCompositions.map(
|
||||
(composition) => composition.contentType.unique,
|
||||
);
|
||||
|
||||
const currentOwnerInheritanceCompositions = currentOwnerCompositions.filter(
|
||||
(composition) => composition.compositionType === CompositionTypeModel.INHERITANCE,
|
||||
);
|
||||
|
||||
const currentPropertyAliases = await this.#workspaceContext.structure.getContentTypePropertyAliases();
|
||||
|
||||
const compositionConfiguration = {
|
||||
compositionRepositoryAlias: this._compositionRepositoryAlias,
|
||||
unique: unique,
|
||||
selection: currentOwnerCompositionCompositions.map((composition) => composition.contentType.unique),
|
||||
selection: currentOwnerCompositionCompositionUniques,
|
||||
usedForInheritance: currentInheritanceCompositions.map((composition) => composition.contentType.unique),
|
||||
usedForComposition: currentOwnerCompositionCompositionUniques,
|
||||
isElement: ownerContentType.isElement,
|
||||
currentPropertyAliases: [],
|
||||
currentPropertyAliases,
|
||||
isNew: this.#workspaceContext.getIsNew()!,
|
||||
};
|
||||
|
||||
@@ -423,6 +431,12 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
|
||||
]);
|
||||
}
|
||||
|
||||
#onDragOver(event: DragEvent, path: string) {
|
||||
if (this._activePath === path) return;
|
||||
event.preventDefault();
|
||||
window.history.replaceState(null, '', path);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<umb-body-layout header-fit-height>
|
||||
@@ -499,8 +513,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
|
||||
}
|
||||
|
||||
renderRootTab() {
|
||||
const rootTabPath = this._routerPath + '/root';
|
||||
const rootTabActive = rootTabPath === this._activePath;
|
||||
const path = this._routerPath + '/root';
|
||||
const rootTabActive = path === this._activePath;
|
||||
if (!this._hasRootGroups && !this._sortModeActive) {
|
||||
// If we don't have any root groups and we are not in sort mode, then we don't want to render the root tab.
|
||||
return nothing;
|
||||
@@ -512,7 +526,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
|
||||
class=${this._hasRootGroups || rootTabActive ? '' : 'content-tab-is-empty'}
|
||||
label=${this.localize.term('general_generic')}
|
||||
.active=${rootTabActive}
|
||||
href=${rootTabPath}>
|
||||
href=${path}
|
||||
@dragover=${(event: DragEvent) => this.#onDragOver(event, path)}>
|
||||
${this.localize.term('general_generic')}
|
||||
</uui-tab>
|
||||
`;
|
||||
@@ -529,7 +544,8 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements
|
||||
href=${path}
|
||||
data-umb-tab-id=${ifDefined(tab.id)}
|
||||
data-mark="tab:${tab.name}"
|
||||
?sortable=${ownedTab}>
|
||||
?sortable=${ownedTab}
|
||||
@dragover=${(event: DragEvent) => this.#onDragOver(event, path)}>
|
||||
${this.renderTabInner(tab, tabActive, ownedTab)}
|
||||
</uui-tab>`;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
createObservablePart,
|
||||
mergeObservables,
|
||||
} from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
import { UmbVariantContext, UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
import type { UmbContentTypeModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type';
|
||||
import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
@@ -47,6 +47,8 @@ export abstract class UmbElementPropertyDatasetContext<
|
||||
protected _readOnly = new UmbBooleanState(false);
|
||||
public readOnly = this._readOnly.asObservable();
|
||||
|
||||
#variantContext = new UmbVariantContext(this).inherit();
|
||||
|
||||
getEntityType(): string {
|
||||
return this._dataOwner.getEntityType();
|
||||
}
|
||||
@@ -64,6 +66,7 @@ export abstract class UmbElementPropertyDatasetContext<
|
||||
super(host, UMB_PROPERTY_DATASET_CONTEXT);
|
||||
this._dataOwner = dataOwner;
|
||||
this.#variantId = variantId ?? UmbVariantId.CreateInvariant();
|
||||
this.#variantContext.setVariantId(this.#variantId);
|
||||
|
||||
this.#propertyVariantIdPromise = new Promise((resolve) => {
|
||||
this.#propertyVariantIdPromiseResolver = resolve as any;
|
||||
|
||||
@@ -714,6 +714,10 @@ export abstract class UmbContentDetailWorkspaceContextBase<
|
||||
*/
|
||||
public async runMandatoryValidationForSaveData(saveData: DetailModelType, variantIds: Array<UmbVariantId> = []) {
|
||||
// Check that the data is valid before we save it.
|
||||
// If we vary by culture then we do not want to validate the invariant variant.
|
||||
if (this.getVariesByCulture()) {
|
||||
variantIds = variantIds.filter((variant) => !variant.isCultureInvariant());
|
||||
}
|
||||
const missingVariants = variantIds.filter((variant) => {
|
||||
return !saveData.variants.some((y) => variant.compare(y));
|
||||
});
|
||||
@@ -754,7 +758,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
|
||||
|
||||
// We ask the server first to get a concatenated set of validation messages. So we see both front-end and back-end validation messages [NL]
|
||||
if (this.getIsNew()) {
|
||||
const parent = this.getParent();
|
||||
const parent = this._internal_getCreateUnderParent();
|
||||
if (!parent) throw new Error('Parent is not set');
|
||||
await this.#serverValidation.askServerForValidation(
|
||||
saveData,
|
||||
@@ -885,7 +889,7 @@ export abstract class UmbContentDetailWorkspaceContextBase<
|
||||
async #create(variantIds: Array<UmbVariantId>, saveData: DetailModelType) {
|
||||
if (!this._detailRepository) throw new Error('Detail repository is not set');
|
||||
|
||||
const parent = this.getParent();
|
||||
const parent = this._internal_getCreateUnderParent();
|
||||
if (!parent) throw new Error('Parent is not set');
|
||||
|
||||
const { data, error } = await this._detailRepository.create(saveData, parent.unique);
|
||||
|
||||
@@ -29,7 +29,7 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
|
||||
}
|
||||
|
||||
@state()
|
||||
_variantId?: UmbVariantId;
|
||||
_datasetVariantId?: UmbVariantId;
|
||||
|
||||
@state()
|
||||
_visibleProperties?: Array<UmbPropertyTypeModel>;
|
||||
@@ -38,7 +38,7 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
|
||||
super();
|
||||
|
||||
this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => {
|
||||
this._variantId = datasetContext?.getVariantId();
|
||||
this._datasetVariantId = datasetContext?.getVariantId();
|
||||
this.#processPropertyStructure();
|
||||
});
|
||||
|
||||
@@ -61,16 +61,19 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
|
||||
}
|
||||
|
||||
#processPropertyStructure() {
|
||||
if (!this.#workspaceContext || !this.#properties || !this.#propertyStructureHelper) {
|
||||
if (!this.#workspaceContext || !this.#properties || !this.#propertyStructureHelper || !this._datasetVariantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyViewGuard = this.#workspaceContext.propertyViewGuard;
|
||||
|
||||
this.#properties.forEach((property) => {
|
||||
const propertyVariantId = new UmbVariantId(this._variantId?.culture, this._variantId?.segment);
|
||||
const propertyVariantId = new UmbVariantId(
|
||||
property.variesByCulture ? this._datasetVariantId?.culture : null,
|
||||
property.variesBySegment ? this._datasetVariantId?.segment : null,
|
||||
);
|
||||
this.observe(
|
||||
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property),
|
||||
propertyViewGuard.isPermittedForVariantAndProperty(propertyVariantId, property, this._datasetVariantId!),
|
||||
(permitted) => {
|
||||
if (permitted) {
|
||||
this.#visiblePropertiesUniques.push(property.unique);
|
||||
@@ -95,14 +98,14 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement
|
||||
}
|
||||
|
||||
override render() {
|
||||
return this._variantId && this._visibleProperties
|
||||
return this._datasetVariantId && this._visibleProperties
|
||||
? repeat(
|
||||
this._visibleProperties,
|
||||
(property) => property.alias,
|
||||
(property) =>
|
||||
html`<umb-content-workspace-view-edit-property
|
||||
class="property"
|
||||
.variantId=${this._variantId}
|
||||
.variantId=${this._datasetVariantId}
|
||||
.property=${property}></umb-content-workspace-view-edit-property>`,
|
||||
)
|
||||
: nothing;
|
||||
|
||||
@@ -46,7 +46,11 @@ export class UmbContentWorkspaceViewEditPropertyElement extends UmbLitElement {
|
||||
})}].value`;
|
||||
|
||||
this.observe(
|
||||
this._context.propertyWriteGuard.isPermittedForVariantAndProperty(propertyVariantId, this.property),
|
||||
this._context.propertyWriteGuard.isPermittedForVariantAndProperty(
|
||||
propertyVariantId,
|
||||
this.property,
|
||||
this.variantId,
|
||||
),
|
||||
(write) => {
|
||||
this._writeable = write;
|
||||
},
|
||||
|
||||
@@ -6700,6 +6700,7 @@ export type GetItemDocumentSearchData = {
|
||||
query?: {
|
||||
query?: string;
|
||||
trashed?: boolean;
|
||||
culture?: string;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
parentId?: string;
|
||||
@@ -8991,6 +8992,7 @@ export type GetItemMediaSearchData = {
|
||||
query?: {
|
||||
query?: string;
|
||||
trashed?: boolean;
|
||||
culture?: string;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
parentId?: string;
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '@umbraco-cms/backoffice/entity-action';
|
||||
import type { UmbActionEventContext } from '@umbraco-cms/backoffice/action';
|
||||
import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';
|
||||
import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity';
|
||||
import { UMB_ENTITY_CONTEXT, UmbParentEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import { UMB_WORKSPACE_MODAL } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UmbModalRouteRegistrationController, type UmbModalRouteBuilder } from '@umbraco-cms/backoffice/router';
|
||||
|
||||
@@ -83,6 +83,7 @@ export class UmbDefaultCollectionContext<
|
||||
});
|
||||
|
||||
#actionEventContext: UmbActionEventContext | undefined;
|
||||
#parentEntityContext = new UmbParentEntityContext(this);
|
||||
|
||||
constructor(host: UmbControllerHost, defaultViewAlias: string, defaultFilter: Partial<FilterModelType> = {}) {
|
||||
super(host, UMB_COLLECTION_CONTEXT);
|
||||
@@ -92,6 +93,23 @@ export class UmbDefaultCollectionContext<
|
||||
|
||||
this.pagination.addEventListener(UmbChangeEvent.TYPE, this.#onPageChange);
|
||||
this.#listenToEntityEvents();
|
||||
|
||||
// The parent entity context is used to get the parent entity for the collection items
|
||||
// All items in the collection are children of the current entity context
|
||||
this.consumeContext(UMB_ENTITY_CONTEXT, (context) => {
|
||||
const currentEntityUnique = context?.getUnique();
|
||||
const currentEntityType = context?.getEntityType();
|
||||
|
||||
const parent: UmbEntityModel | undefined =
|
||||
currentEntityUnique && currentEntityType
|
||||
? {
|
||||
unique: currentEntityUnique,
|
||||
entityType: currentEntityType,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
this.#parentEntityContext?.setParent(parent);
|
||||
});
|
||||
}
|
||||
|
||||
setupView(viewElement: UmbControllerHost) {
|
||||
@@ -225,6 +243,10 @@ export class UmbDefaultCollectionContext<
|
||||
return this._manifest;
|
||||
}
|
||||
|
||||
public getEmptyLabel(): string {
|
||||
return this.manifest?.meta.noItemsLabel ?? this.#config?.noItemsLabel ?? '#collection_noItemsTitle';
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the collection from the repository.
|
||||
* @returns {*}
|
||||
@@ -261,6 +283,10 @@ export class UmbDefaultCollectionContext<
|
||||
this.requestCollection();
|
||||
}
|
||||
|
||||
public updateFilter(filter: Partial<FilterModelType>) {
|
||||
this._filter.setValue({ ...this._filter.getValue(), ...filter });
|
||||
}
|
||||
|
||||
public getLastSelectedView(unique: string | undefined): string | undefined {
|
||||
if (!unique) return;
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ umbExtensionsRegistry.register(manifest);
|
||||
|
||||
@customElement('umb-collection-default')
|
||||
export class UmbCollectionDefaultElement extends UmbLitElement {
|
||||
//
|
||||
#collectionContext?: UmbDefaultCollectionContext;
|
||||
|
||||
@state()
|
||||
private _routes: Array<UmbRoute> = [];
|
||||
|
||||
@@ -32,7 +35,8 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
|
||||
@state()
|
||||
private _isDoneLoading = false;
|
||||
|
||||
#collectionContext?: UmbDefaultCollectionContext<any, any>;
|
||||
@state()
|
||||
private _emptyLabel?: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -40,6 +44,7 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
|
||||
this.#collectionContext = context;
|
||||
this.#observeCollectionRoutes();
|
||||
this.#observeTotalItems();
|
||||
this.#getEmptyStateLabel();
|
||||
await this.#collectionContext?.requestCollection();
|
||||
this._isDoneLoading = true;
|
||||
});
|
||||
@@ -69,6 +74,10 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
|
||||
);
|
||||
}
|
||||
|
||||
#getEmptyStateLabel() {
|
||||
this._emptyLabel = this.#collectionContext?.getEmptyLabel();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return this._routes
|
||||
? html`
|
||||
@@ -98,9 +107,10 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
|
||||
|
||||
#renderEmptyState() {
|
||||
if (!this._isDoneLoading) return nothing;
|
||||
|
||||
return html`
|
||||
<div id="empty-state" class="uui-text">
|
||||
<h4><umb-localize key="collection_noItemsTitle"></umb-localize></h4>
|
||||
<h4>${this.localize.string(this._emptyLabel)}</h4>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ManifestCollection
|
||||
|
||||
export interface MetaCollection {
|
||||
repositoryAlias: string;
|
||||
noItemsLabel?: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface UmbCollectionConfiguration {
|
||||
orderBy?: string;
|
||||
orderDirection?: string;
|
||||
pageSize?: number;
|
||||
noItemsLabel?: string;
|
||||
userDefinedProperties?: Array<UmbCollectionColumnConfiguration>;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,11 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
// TODO: maybe move this to UI Library.
|
||||
@customElement('umb-dropdown')
|
||||
export class UmbDropdownElement extends UmbLitElement {
|
||||
@query('#dropdown-popover')
|
||||
popoverContainerElement?: UUIPopoverContainerElement;
|
||||
@property({ type: Boolean, reflect: true })
|
||||
open = false;
|
||||
|
||||
@property()
|
||||
label = '';
|
||||
label?: string;
|
||||
|
||||
@property()
|
||||
look: UUIInterfaceLook = 'default';
|
||||
@@ -35,19 +33,16 @@ export class UmbDropdownElement extends UmbLitElement {
|
||||
@property({ type: Boolean, attribute: 'hide-expand' })
|
||||
hideExpand = false;
|
||||
|
||||
@query('#dropdown-popover')
|
||||
popoverContainerElement?: UUIPopoverContainerElement;
|
||||
|
||||
protected override updated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.updated(_changedProperties);
|
||||
if (_changedProperties.has('open') && this.popoverContainerElement) {
|
||||
if (this.open) {
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.popoverContainerElement.showPopover();
|
||||
this.openDropdown();
|
||||
} else {
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.popoverContainerElement.hidePopover();
|
||||
this.closeDropdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,14 +54,29 @@ export class UmbDropdownElement extends UmbLitElement {
|
||||
this.open = event.newState === 'open';
|
||||
}
|
||||
|
||||
openDropdown() {
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.popoverContainerElement?.showPopover();
|
||||
}
|
||||
|
||||
closeDropdown() {
|
||||
// TODO: This ignorer is just needed for JSON SCHEMA TO WORK, As its not updated with latest TS jet.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.popoverContainerElement?.hidePopover();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<uui-button
|
||||
id="dropdown-button"
|
||||
popovertarget="dropdown-popover"
|
||||
data-mark="open-dropdown"
|
||||
.look=${this.look}
|
||||
.color=${this.color}
|
||||
.label=${this.label}
|
||||
.label=${this.label ?? ''}
|
||||
.compact=${this.compact}>
|
||||
<slot name="label"></slot>
|
||||
${when(
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { UmbEntityContext } from '../../entity/entity.context.js';
|
||||
import type { UmbDropdownElement } from '../dropdown/index.js';
|
||||
import type { UmbEntityAction, ManifestEntityActionDefaultKind } from '@umbraco-cms/backoffice/entity-action';
|
||||
import type { PropertyValueMap } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { html, nothing, customElement, property, state, ifDefined, css } from '@umbraco-cms/backoffice/external/lit';
|
||||
import {
|
||||
html,
|
||||
nothing,
|
||||
customElement,
|
||||
property,
|
||||
state,
|
||||
ifDefined,
|
||||
css,
|
||||
query,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbExtensionsManifestInitializer, createExtensionApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
@@ -29,8 +39,8 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
|
||||
@state()
|
||||
private _firstActionHref?: string;
|
||||
|
||||
@state()
|
||||
_dropdownIsOpen = false;
|
||||
@query('#action-modal')
|
||||
private _dropdownElement?: UmbDropdownElement;
|
||||
|
||||
// TODO: provide the entity context on a higher level, like the root element of this entity, tree-item/workspace/... [NL]
|
||||
#entityContext = new UmbEntityContext(this);
|
||||
@@ -79,7 +89,7 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#onActionExecuted() {
|
||||
this._dropdownIsOpen = false;
|
||||
this._dropdownElement?.closeDropdown();
|
||||
}
|
||||
|
||||
#onDropdownClick(event: Event) {
|
||||
@@ -95,25 +105,27 @@ export class UmbEntityActionsBundleElement extends UmbLitElement {
|
||||
if (this._numberOfActions === 1) return nothing;
|
||||
|
||||
return html`
|
||||
<umb-dropdown id="action-modal" .open=${this._dropdownIsOpen} @click=${this.#onDropdownClick} compact hide-expand>
|
||||
<uui-symbol-more slot="label" label="Open actions menu"></uui-symbol-more>
|
||||
<umb-dropdown id="action-modal" @click=${this.#onDropdownClick} .label=${this.label} compact hide-expand>
|
||||
<uui-symbol-more slot="label" .label=${this.label}></uui-symbol-more>
|
||||
<uui-scroll-container>
|
||||
<umb-entity-action-list
|
||||
@action-executed=${this.#onActionExecuted}
|
||||
.entityType=${this.entityType}
|
||||
.unique=${this.unique}></umb-entity-action-list>
|
||||
.unique=${this.unique}
|
||||
.label=${this.label}></umb-entity-action-list>
|
||||
</uui-scroll-container>
|
||||
</umb-dropdown>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderFirstAction() {
|
||||
if (!this._firstActionApi) return nothing;
|
||||
if (!this._firstActionApi || !this._firstActionManifest) return nothing;
|
||||
return html`<uui-button
|
||||
label=${ifDefined(this._firstActionManifest?.meta.label)}
|
||||
label=${this.localize.string(this._firstActionManifest.meta.label)}
|
||||
data-mark=${'entity-action:' + this._firstActionManifest.alias}
|
||||
@click=${this.#onFirstActionClick}
|
||||
href="${ifDefined(this._firstActionHref)}">
|
||||
<uui-icon name=${ifDefined(this._firstActionManifest?.meta.icon)}></uui-icon>
|
||||
<uui-icon name=${ifDefined(this._firstActionManifest.meta.icon)}></uui-icon>
|
||||
</uui-button>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -66,17 +66,18 @@ export class UmbEntityActionDefaultElement<
|
||||
}
|
||||
|
||||
override render() {
|
||||
const label = this.manifest?.meta.label ? this.localize.string(this.manifest.meta.label) : this.manifest?.name;
|
||||
if (!this.manifest) return nothing;
|
||||
|
||||
const label = this.manifest.meta.label ? this.localize.string(this.manifest.meta.label) : this.manifest.name;
|
||||
|
||||
return html`
|
||||
<uui-menu-item
|
||||
data-mark=${'entity-action:' + this.manifest?.alias}
|
||||
label=${ifDefined(this.manifest?.meta.additionalOptions ? label + '…' : label)}
|
||||
href=${ifDefined(this._href)}
|
||||
@click-label=${this.#onClickLabel}
|
||||
@click=${this.#onClick}>
|
||||
${this.manifest?.meta.icon
|
||||
? html`<umb-icon slot="icon" name="${this.manifest?.meta.icon}"></umb-icon>`
|
||||
: nothing}
|
||||
${this.manifest.meta.icon ? html`<umb-icon slot="icon" name="${this.manifest.meta.icon}"></umb-icon>` : nothing}
|
||||
</uui-menu-item>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import type { UmbEntityModel, UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import { html, nothing, customElement, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
|
||||
@customElement('umb-entity-actions-table-column-view')
|
||||
export class UmbEntityActionsTableColumnViewElement extends UmbLitElement {
|
||||
@property({ attribute: false })
|
||||
value?: UmbEntityModel;
|
||||
value?: UmbEntityModel | UmbNamedEntityModel;
|
||||
|
||||
override render() {
|
||||
if (!this.value) return nothing;
|
||||
|
||||
return html`
|
||||
<umb-entity-actions-bundle .entityType=${this.value.entityType} .unique=${this.value.unique}>
|
||||
<umb-entity-actions-bundle
|
||||
.entityType=${this.value.entityType}
|
||||
.unique=${this.value.unique}
|
||||
.label=${this.localize.term('actions_viewActionsFor', [(this.value as any).name])}>
|
||||
</umb-entity-actions-bundle>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router';
|
||||
import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
import './default-item-ref.element.js';
|
||||
import { UmbDeselectedEvent, UmbSelectedEvent } from '@umbraco-cms/backoffice/event';
|
||||
|
||||
@customElement('umb-entity-item-ref')
|
||||
export class UmbEntityItemRefElement extends UmbLitElement {
|
||||
@@ -41,7 +42,7 @@ export class UmbEntityItemRefElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#readonly = false;
|
||||
@property({ type: Boolean, attribute: 'readonly' })
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public get readonly() {
|
||||
return this.#readonly;
|
||||
}
|
||||
@@ -54,7 +55,7 @@ export class UmbEntityItemRefElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#standalone = false;
|
||||
@property({ type: Boolean, attribute: 'standalone' })
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public get standalone() {
|
||||
return this.#standalone;
|
||||
}
|
||||
@@ -66,8 +67,74 @@ export class UmbEntityItemRefElement extends UmbLitElement {
|
||||
}
|
||||
}
|
||||
|
||||
#selectOnly = false;
|
||||
@property({ type: Boolean, attribute: 'select-only', reflect: true })
|
||||
public get selectOnly() {
|
||||
return this.#selectOnly;
|
||||
}
|
||||
public set selectOnly(value) {
|
||||
this.#selectOnly = value;
|
||||
|
||||
if (this._component) {
|
||||
this._component.selectOnly = this.#selectOnly;
|
||||
}
|
||||
}
|
||||
|
||||
#selectable = false;
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public get selectable() {
|
||||
return this.#selectable;
|
||||
}
|
||||
public set selectable(value) {
|
||||
this.#selectable = value;
|
||||
|
||||
if (this._component) {
|
||||
this._component.selectable = this.#selectable;
|
||||
}
|
||||
}
|
||||
|
||||
#selected = false;
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public get selected() {
|
||||
return this.#selected;
|
||||
}
|
||||
public set selected(value) {
|
||||
this.#selected = value;
|
||||
|
||||
if (this._component) {
|
||||
this._component.selected = this.#selected;
|
||||
}
|
||||
}
|
||||
|
||||
#disabled = false;
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public get disabled() {
|
||||
return this.#disabled;
|
||||
}
|
||||
public set disabled(value) {
|
||||
this.#disabled = value;
|
||||
|
||||
if (this._component) {
|
||||
this._component.disabled = this.#disabled;
|
||||
}
|
||||
}
|
||||
|
||||
#pathAddendum = new UmbRoutePathAddendumContext(this);
|
||||
|
||||
#onSelected(event: UmbSelectedEvent) {
|
||||
event.stopPropagation();
|
||||
const unique = this.#item?.unique;
|
||||
if (!unique) throw new Error('No unique id found for item');
|
||||
this.dispatchEvent(new UmbSelectedEvent(unique));
|
||||
}
|
||||
|
||||
#onDeselected(event: UmbDeselectedEvent) {
|
||||
event.stopPropagation();
|
||||
const unique = this.#item?.unique;
|
||||
if (!unique) throw new Error('No unique id found for item');
|
||||
this.dispatchEvent(new UmbDeselectedEvent(unique));
|
||||
}
|
||||
|
||||
protected override firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
|
||||
super.firstUpdated(_changedProperties);
|
||||
this.setAttribute(UMB_MARK_ATTRIBUTE_NAME, 'entity-item-ref');
|
||||
@@ -91,6 +158,13 @@ export class UmbEntityItemRefElement extends UmbLitElement {
|
||||
component.item = this.#item;
|
||||
component.readonly = this.readonly;
|
||||
component.standalone = this.standalone;
|
||||
component.selectOnly = this.selectOnly;
|
||||
component.selectable = this.selectable;
|
||||
component.selected = this.selected;
|
||||
component.disabled = this.disabled;
|
||||
|
||||
component.addEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this));
|
||||
component.addEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this));
|
||||
|
||||
// Proxy the actions slot to the component
|
||||
const slotElement = document.createElement('slot');
|
||||
@@ -110,6 +184,12 @@ export class UmbEntityItemRefElement extends UmbLitElement {
|
||||
return html`${this._component}`;
|
||||
}
|
||||
|
||||
override destroy(): void {
|
||||
this._component?.removeEventListener(UmbSelectedEvent.TYPE, this.#onSelected.bind(this));
|
||||
this._component?.removeEventListener(UmbDeselectedEvent.TYPE, this.#onDeselected.bind(this));
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
css`
|
||||
:host {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './item-data-api-get-request-controller/index.js';
|
||||
export * from './entity-item-ref/index.js';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './item-data-api-get-request.controller.js';
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { UmbItemDataApiGetRequestControllerArgs } from './types.js';
|
||||
import {
|
||||
batchTryExecute,
|
||||
tryExecute,
|
||||
UmbError,
|
||||
type UmbApiError,
|
||||
type UmbCancelError,
|
||||
type UmbDataApiResponse,
|
||||
} from '@umbraco-cms/backoffice/resources';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { batchArray } from '@umbraco-cms/backoffice/utils';
|
||||
import { umbPeekError } from '@umbraco-cms/backoffice/notification';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
|
||||
export class UmbItemDataApiGetRequestController<
|
||||
ResponseModelType extends UmbDataApiResponse,
|
||||
> extends UmbControllerBase {
|
||||
#apiCallback: (args: { uniques: Array<string> }) => Promise<ResponseModelType>;
|
||||
#uniques: Array<string>;
|
||||
#batchSize: number = 40;
|
||||
|
||||
constructor(host: UmbControllerHost, args: UmbItemDataApiGetRequestControllerArgs<ResponseModelType>) {
|
||||
super(host);
|
||||
this.#apiCallback = args.api;
|
||||
this.#uniques = args.uniques;
|
||||
}
|
||||
|
||||
async request() {
|
||||
if (!this.#uniques) throw new Error('Uniques are missing');
|
||||
|
||||
let data: ResponseModelType['data'] | undefined;
|
||||
let error: UmbError | UmbApiError | UmbCancelError | Error | undefined;
|
||||
|
||||
if (this.#uniques.length > this.#batchSize) {
|
||||
const chunks = batchArray<string>(this.#uniques, this.#batchSize);
|
||||
const results = await batchTryExecute(this, chunks, (chunk) => this.#apiCallback({ uniques: chunk }));
|
||||
|
||||
const errors = results.filter((promiseResult) => promiseResult.status === 'rejected');
|
||||
|
||||
if (errors.length > 0) {
|
||||
error = await this.#getAndHandleErrorResult(errors);
|
||||
}
|
||||
|
||||
data = results
|
||||
.filter((promiseResult) => promiseResult.status === 'fulfilled')
|
||||
.flatMap((promiseResult) => promiseResult.value.data);
|
||||
} else {
|
||||
const result = await tryExecute(this, this.#apiCallback({ uniques: this.#uniques }));
|
||||
data = result.data;
|
||||
error = result.error;
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
}
|
||||
|
||||
async #getAndHandleErrorResult(errors: Array<PromiseRejectedResult>) {
|
||||
// TODO: We currently expect all the errors to be the same, but we should handle this better in the future.
|
||||
const error = errors[0];
|
||||
await umbPeekError(this, {
|
||||
headline: 'Error fetching items',
|
||||
message: 'An error occurred while fetching items.',
|
||||
});
|
||||
|
||||
return new UmbError(error.reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { UmbDataApiResponse } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
export interface UmbItemDataApiGetRequestControllerArgs<ResponseModelType extends UmbDataApiResponse> {
|
||||
api: (args: { uniques: Array<string> }) => Promise<ResponseModelType>;
|
||||
uniques: Array<string>;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
export type * from './item-data-api-get-request-controller/types.js';
|
||||
|
||||
export interface UmbDefaultItemModel extends UmbNamedEntityModel {
|
||||
icon?: string;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './contexts/ancestors/constants.js';
|
||||
export * from './contexts/parent/constants.js';
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { UMB_PARENT_ENTITY_CONTEXT } from './parent.entity-context-token.js';
|
||||
@@ -0,0 +1 @@
|
||||
export { UmbParentEntityContext } from './parent.entity-context.js';
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { UmbParentEntityContext } from './parent.entity-context.js';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
export const UMB_PARENT_ENTITY_CONTEXT = new UmbContextToken<UmbParentEntityContext>('UmbParentEntityContext');
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { UmbEntityModel } from '../../types.js';
|
||||
import { UMB_PARENT_ENTITY_CONTEXT } from './parent.entity-context-token.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
|
||||
/**
|
||||
* A entity context for the parent
|
||||
* @class UmbParentEntityContext
|
||||
* @augments {UmbContextBase}
|
||||
* @implements {UmbParentEntityContext}
|
||||
*/
|
||||
export class UmbParentEntityContext extends UmbContextBase {
|
||||
#parent = new UmbObjectState<UmbEntityModel | undefined>(undefined);
|
||||
parent = this.#parent.asObservable();
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host, UMB_PARENT_ENTITY_CONTEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parent state
|
||||
* @returns {UmbEntityModel | undefined} - The parent state
|
||||
* @memberof UmbParentEntityContext
|
||||
*/
|
||||
getParent(): UmbEntityModel | undefined {
|
||||
return this.#parent.getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parent state
|
||||
* @param {UmbEntityModel | undefined} parent - The parent state
|
||||
* @memberof UmbParentEntityContext
|
||||
*/
|
||||
setParent(parent: UmbEntityModel | undefined): void {
|
||||
this.#parent.setValue(parent);
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,6 @@ export { UMB_ENTITY_CONTEXT } from './entity.context-token.js';
|
||||
export { UmbEntityContext } from './entity.context.js';
|
||||
export * from './constants.js';
|
||||
export * from './contexts/ancestors/index.js';
|
||||
export * from './contexts/parent/index.js';
|
||||
|
||||
export type * from './types.js';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { html, customElement, property, ifDefined, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { ensureSlash } from '@umbraco-cms/backoffice/router';
|
||||
import { debounce } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
/**
|
||||
@@ -62,8 +63,12 @@ export class UmbMenuItemLayoutElement extends UmbLitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = window.location.pathname;
|
||||
this._isActive = location.includes(this.href);
|
||||
/* Check if the current location includes the href of this menu item
|
||||
We ensure that the paths ends with a slash to avoid collisions with paths like /path-1 and /path-1-2 where /path is in both.
|
||||
Instead we compare /path-1/ with /path-1-2/ which wont collide.*/
|
||||
const location = ensureSlash(window.location.pathname);
|
||||
const compareHref = ensureSlash(this.href);
|
||||
this._isActive = location.includes(compareHref);
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -80,7 +85,7 @@ export class UmbMenuItemLayoutElement extends UmbLitElement {
|
||||
slot="actions"
|
||||
.entityType=${this.entityType}
|
||||
.unique=${null}
|
||||
.label=${this.label}>
|
||||
.label=${this.localize.term('actions_viewActionsFor', [this.label])}>
|
||||
</umb-entity-actions-bundle>`
|
||||
: ''}
|
||||
<slot></slot>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './components/index.js';
|
||||
export * from './menu-tree-structure-workspace-context-base.js';
|
||||
export * from './menu-structure-workspace-context.context-token.js';
|
||||
export * from './menu-variant-structure-workspace-context.context-token.js';
|
||||
export * from './menu-variant-tree-structure-workspace-context-base.js';
|
||||
|
||||
export type * from './types.js';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { UmbMenuStructureWorkspaceContext } from './menu-structure-workspace-context.interface.js';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
export const UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT = new UmbContextToken<UmbMenuStructureWorkspaceContext>(
|
||||
'UmbWorkspaceContext',
|
||||
'UmbMenuStructure',
|
||||
);
|
||||
@@ -1,32 +1,41 @@
|
||||
import type { UmbStructureItemModel } from './types.js';
|
||||
import { UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT } from './menu-structure-workspace-context.context-token.js';
|
||||
import type { UmbTreeRepository, UmbTreeItemModel, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
|
||||
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UMB_SUBMITTABLE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbAncestorsEntityContext, UmbParentEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
interface UmbMenuTreeStructureWorkspaceContextBaseArgs {
|
||||
treeRepositoryAlias: string;
|
||||
}
|
||||
|
||||
// TODO: introduce base class for all menu structure workspaces to handle ancestors and parent
|
||||
export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContextBase {
|
||||
#workspaceContext?: typeof UMB_SUBMITTABLE_WORKSPACE_CONTEXT.TYPE;
|
||||
#workspaceContext?: typeof UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT.TYPE;
|
||||
#args: UmbMenuTreeStructureWorkspaceContextBaseArgs;
|
||||
|
||||
#structure = new UmbArrayState<UmbStructureItemModel>([], (x) => x.unique);
|
||||
public readonly structure = this.#structure.asObservable();
|
||||
|
||||
#parent = new UmbObjectState<UmbStructureItemModel | undefined>(undefined);
|
||||
/**
|
||||
* @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead.
|
||||
*/
|
||||
public readonly parent = this.#parent.asObservable();
|
||||
|
||||
#parentContext = new UmbParentEntityContext(this);
|
||||
#ancestorContext = new UmbAncestorsEntityContext(this);
|
||||
|
||||
constructor(host: UmbControllerHost, args: UmbMenuTreeStructureWorkspaceContextBaseArgs) {
|
||||
// TODO: set up context token
|
||||
super(host, 'UmbMenuStructureWorkspaceContext');
|
||||
super(host, UMB_MENU_STRUCTURE_WORKSPACE_CONTEXT);
|
||||
// 'UmbMenuStructureWorkspaceContext' is Obsolete, will be removed in v.18
|
||||
this.provideContext('UmbMenuStructureWorkspaceContext', this);
|
||||
this.#args = args;
|
||||
|
||||
// TODO: set up context token that supports parentEntityType, parentUnique, entityType.
|
||||
this.consumeContext(UMB_SUBMITTABLE_WORKSPACE_CONTEXT, (instance) => {
|
||||
this.consumeContext(UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT, (instance) => {
|
||||
this.#workspaceContext = instance;
|
||||
this.observe(this.#workspaceContext?.unique, (value) => {
|
||||
if (!value) return;
|
||||
@@ -59,14 +68,16 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex
|
||||
const isNew = this.#workspaceContext?.getIsNew();
|
||||
|
||||
const entityTypeObservable = isNew
|
||||
? (this.#workspaceContext as any)?.parentEntityType
|
||||
: (this.#workspaceContext as any).entityType;
|
||||
? this.#workspaceContext?._internal_createUnderParentEntityType
|
||||
: this.#workspaceContext?.entityType;
|
||||
const entityType = (await this.observe(entityTypeObservable, () => {})?.asPromise()) as string;
|
||||
if (!entityType) throw new Error('Entity type is not available');
|
||||
|
||||
// If the entity type is different from the root entity type, then we can request the ancestors.
|
||||
if (entityType !== root?.entityType) {
|
||||
const uniqueObservable = isNew ? (this.#workspaceContext as any)?.parentUnique : this.#workspaceContext?.unique;
|
||||
const uniqueObservable = isNew
|
||||
? this.#workspaceContext?._internal_createUnderParentEntityUnique
|
||||
: this.#workspaceContext?.unique;
|
||||
const unique = (await this.observe(uniqueObservable, () => {})?.asPromise()) as string;
|
||||
if (!unique) throw new Error('Unique is not available');
|
||||
|
||||
@@ -83,11 +94,49 @@ export abstract class UmbMenuTreeStructureWorkspaceContextBase extends UmbContex
|
||||
});
|
||||
|
||||
structureItems.push(...ancestorItems);
|
||||
|
||||
this.#structure.setValue(structureItems);
|
||||
this.#setParentData(structureItems);
|
||||
this.#setAncestorData(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parent = structureItems[structureItems.length - 2];
|
||||
#setParentData(structureItems: Array<UmbStructureItemModel>) {
|
||||
/* If the item is not new, the current item is the last item in the array.
|
||||
We filter out the current item unique to handle any case where it could show up */
|
||||
const parent = structureItems.filter((item) => item.unique !== this.#workspaceContext?.getUnique()).pop();
|
||||
|
||||
// TODO: remove this when the parent gets removed from the structure interface
|
||||
this.#parent.setValue(parent);
|
||||
this.#structure.setValue(structureItems);
|
||||
|
||||
const parentEntity = parent
|
||||
? {
|
||||
unique: parent.unique,
|
||||
entityType: parent.entityType,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
this.#parentContext.setParent(parentEntity);
|
||||
}
|
||||
|
||||
/* Notice: ancestors are based on the server "data" ancestors and are not based on the full Menu (UI) structure.
|
||||
This will mean that any item placed in the data root will not have any ancestors. But will have a parent based on the UI structure.
|
||||
*/
|
||||
#setAncestorData(ancestors: Array<UmbTreeItemModel>) {
|
||||
const ancestorEntities = ancestors
|
||||
.map((treeItem) => {
|
||||
const entity: UmbEntityModel = {
|
||||
unique: treeItem.unique,
|
||||
entityType: treeItem.entityType,
|
||||
};
|
||||
|
||||
return entity;
|
||||
})
|
||||
/* If the item is not new, the current item is the last item in the array.
|
||||
We filter out the current item unique to handle any case where it could show up */
|
||||
.filter((item) => item.unique !== this.#workspaceContext?.getUnique());
|
||||
|
||||
this.#ancestorContext.setAncestors(ancestorEntities);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { UmbMenuVariantStructureWorkspaceContext } from './menu-variant-structure-workspace-context.interface.js';
|
||||
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
|
||||
|
||||
export const UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT =
|
||||
new UmbContextToken<UmbMenuVariantStructureWorkspaceContext>(
|
||||
'UmbWorkspaceContext',
|
||||
'UmbMenuStructure',
|
||||
(context): context is UmbMenuVariantStructureWorkspaceContext =>
|
||||
'IS_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT' in context,
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { UmbVariantStructureItemModel } from './types.js';
|
||||
import type { UmbContext } from '@umbraco-cms/backoffice/class-api';
|
||||
import type { Observable } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
|
||||
export interface UmbMenuVariantStructureWorkspaceContext extends UmbContext {
|
||||
structure: Observable<UmbVariantStructureItemModel[]>;
|
||||
}
|
||||
@@ -1,36 +1,44 @@
|
||||
import type { UmbVariantStructureItemModel } from './types.js';
|
||||
import type { UmbTreeRepository, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
|
||||
import { UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT } from './menu-variant-structure-workspace-context.context-token.js';
|
||||
import type { UmbTreeItemModel, UmbTreeRepository, UmbTreeRootModel } from '@umbraco-cms/backoffice/tree';
|
||||
import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UMB_VARIANT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
import { UmbArrayState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbAncestorsEntityContext } from '@umbraco-cms/backoffice/entity';
|
||||
import { UmbAncestorsEntityContext, UmbParentEntityContext, type UmbEntityModel } from '@umbraco-cms/backoffice/entity';
|
||||
import { UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace';
|
||||
|
||||
interface UmbMenuVariantTreeStructureWorkspaceContextBaseArgs {
|
||||
treeRepositoryAlias: string;
|
||||
}
|
||||
|
||||
// TODO: introduce base class for all menu structure workspaces to handle ancestors and parent
|
||||
export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends UmbContextBase {
|
||||
// TODO: add correct interface
|
||||
#workspaceContext?: typeof UMB_VARIANT_WORKSPACE_CONTEXT.TYPE;
|
||||
//
|
||||
#workspaceContext?: typeof UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT.TYPE;
|
||||
#args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs;
|
||||
|
||||
#structure = new UmbArrayState<UmbVariantStructureItemModel>([], (x) => x.unique);
|
||||
public readonly structure = this.#structure.asObservable();
|
||||
|
||||
#parent = new UmbObjectState<UmbVariantStructureItemModel | undefined>(undefined);
|
||||
/**
|
||||
* @deprecated Will be removed in v.18: Use UMB_PARENT_ENTITY_CONTEXT instead.
|
||||
*/
|
||||
public readonly parent = this.#parent.asObservable();
|
||||
|
||||
#parentContext = new UmbParentEntityContext(this);
|
||||
#ancestorContext = new UmbAncestorsEntityContext(this);
|
||||
|
||||
public readonly IS_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT = true;
|
||||
|
||||
constructor(host: UmbControllerHost, args: UmbMenuVariantTreeStructureWorkspaceContextBaseArgs) {
|
||||
// TODO: set up context token
|
||||
super(host, 'UmbMenuStructureWorkspaceContext');
|
||||
super(host, UMB_MENU_VARIANT_STRUCTURE_WORKSPACE_CONTEXT);
|
||||
// 'UmbMenuStructureWorkspaceContext' is Obsolete, will be removed in v.18
|
||||
this.provideContext('UmbMenuStructureWorkspaceContext', this);
|
||||
this.#args = args;
|
||||
|
||||
// TODO: Implement a Context Token that supports parentUnique, parentEntityType, entityType
|
||||
this.consumeContext(UMB_VARIANT_WORKSPACE_CONTEXT, (instance) => {
|
||||
this.consumeContext(UMB_SUBMITTABLE_TREE_ENTITY_WORKSPACE_CONTEXT, (instance) => {
|
||||
this.#workspaceContext = instance;
|
||||
this.observe(
|
||||
this.#workspaceContext?.unique,
|
||||
@@ -45,10 +53,12 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
|
||||
|
||||
async #requestStructure() {
|
||||
const isNew = this.#workspaceContext?.getIsNew();
|
||||
const uniqueObservable = isNew ? (this.#workspaceContext as any)?.parentUnique : this.#workspaceContext?.unique;
|
||||
const uniqueObservable = isNew
|
||||
? this.#workspaceContext?._internal_createUnderParentEntityType
|
||||
: this.#workspaceContext?.unique;
|
||||
const entityTypeObservable = isNew
|
||||
? (this.#workspaceContext as any)?.parentEntityType
|
||||
: (this.#workspaceContext as any)?.entityType;
|
||||
? this.#workspaceContext?._internal_createUnderParentEntityUnique
|
||||
: this.#workspaceContext?.entityType;
|
||||
|
||||
let structureItems: Array<UmbVariantStructureItemModel> = [];
|
||||
|
||||
@@ -58,7 +68,7 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
|
||||
const entityType = (await this.observe(entityTypeObservable, () => {})?.asPromise()) as string;
|
||||
if (!entityType) throw new Error('Entity type is not available');
|
||||
|
||||
// TODO: add correct tree variant item model
|
||||
// TODO: introduce variant tree item model
|
||||
const treeRepository = await createExtensionApiByAlias<UmbTreeRepository<any, UmbTreeRootModel>>(
|
||||
this,
|
||||
this.#args.treeRepositoryAlias,
|
||||
@@ -79,7 +89,7 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
|
||||
const { data } = await treeRepository.requestTreeItemAncestors({ treeItem: { unique, entityType } });
|
||||
|
||||
if (data) {
|
||||
const ancestorItems = data.map((treeItem) => {
|
||||
const treeItemAncestors = data.map((treeItem) => {
|
||||
return {
|
||||
unique: treeItem.unique,
|
||||
entityType: treeItem.entityType,
|
||||
@@ -93,20 +103,49 @@ export abstract class UmbMenuVariantTreeStructureWorkspaceContextBase extends Um
|
||||
};
|
||||
});
|
||||
|
||||
const ancestorEntities = data.map((treeItem) => {
|
||||
return {
|
||||
structureItems.push(...treeItemAncestors);
|
||||
|
||||
this.#structure.setValue(structureItems);
|
||||
this.#setParentData(structureItems);
|
||||
this.#setAncestorData(data);
|
||||
}
|
||||
}
|
||||
|
||||
#setParentData(structureItems: Array<UmbVariantStructureItemModel>) {
|
||||
/* If the item is not new, the current item is the last item in the array.
|
||||
We filter out the current item unique to handle any case where it could show up */
|
||||
const parent = structureItems.filter((item) => item.unique !== this.#workspaceContext?.getUnique()).pop();
|
||||
|
||||
// TODO: remove this when the parent gets removed from the structure interface
|
||||
this.#parent.setValue(parent);
|
||||
|
||||
const parentEntity = parent
|
||||
? {
|
||||
unique: parent.unique,
|
||||
entityType: parent.entityType,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
this.#parentContext.setParent(parentEntity);
|
||||
}
|
||||
|
||||
/* Notice: ancestors are based on the server "data" ancestors and are not based on the full Menu (UI) structure.
|
||||
This will mean that any item placed in the data root will not have any ancestors. But will have a parent based on the UI structure.
|
||||
*/
|
||||
#setAncestorData(ancestors: Array<UmbTreeItemModel>) {
|
||||
const ancestorEntities = ancestors
|
||||
.map((treeItem) => {
|
||||
const entity: UmbEntityModel = {
|
||||
unique: treeItem.unique,
|
||||
entityType: treeItem.entityType,
|
||||
};
|
||||
});
|
||||
|
||||
this.#ancestorContext.setAncestors(ancestorEntities);
|
||||
return entity;
|
||||
})
|
||||
/* If the item is not new, the current item is the last item in the array.
|
||||
We filter out the current item unique to handle any case where it could show up */
|
||||
.filter((item) => item.unique !== this.#workspaceContext?.getUnique());
|
||||
|
||||
structureItems.push(...ancestorItems);
|
||||
|
||||
const parent = structureItems[structureItems.length - 2];
|
||||
this.#parent.setValue(parent);
|
||||
this.#structure.setValue(structureItems);
|
||||
}
|
||||
this.#ancestorContext.setAncestors(ancestorEntities);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { UmbSectionSidebarMenuElement } from '../section-sidebar-menu/section-sidebar-menu.element.js';
|
||||
import type { ManifestSectionSidebarAppMenuWithEntityActionsKind } from '../section-sidebar-menu/types.js';
|
||||
import { css, html, customElement } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, html, customElement, type PropertyValues, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbParentEntityContext } from '@umbraco-cms/backoffice/entity';
|
||||
|
||||
const manifestWithEntityActions: UmbExtensionManifestKind = {
|
||||
type: 'kind',
|
||||
@@ -18,15 +19,30 @@ umbExtensionsRegistry.register(manifestWithEntityActions);
|
||||
|
||||
@customElement('umb-section-sidebar-menu-with-entity-actions')
|
||||
export class UmbSectionSidebarMenuWithEntityActionsElement extends UmbSectionSidebarMenuElement<ManifestSectionSidebarAppMenuWithEntityActionsKind> {
|
||||
@state()
|
||||
_unique = null;
|
||||
|
||||
@state()
|
||||
_entityType?: string | null;
|
||||
|
||||
#parentContext = new UmbParentEntityContext(this);
|
||||
|
||||
protected override updated(_changedProperties: PropertyValues<this>): void {
|
||||
if (_changedProperties.has('manifest')) {
|
||||
const entityType = this.manifest?.meta.entityType;
|
||||
this.#parentContext.setParent(entityType ? { unique: this._unique, entityType } : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
override renderHeader() {
|
||||
return html`
|
||||
<div id="header">
|
||||
<h3>${this.localize.string(this.manifest?.meta?.label ?? '')}</h3>
|
||||
<umb-entity-actions-bundle
|
||||
slot="actions"
|
||||
.unique=${null}
|
||||
.unique=${this._unique}
|
||||
.entityType=${this.manifest?.meta.entityType}
|
||||
.label=${this.manifest?.meta.label}>
|
||||
.label=${this.localize.term('actions_viewActionsFor', [this.manifest?.meta.label])}>
|
||||
</umb-entity-actions-bundle>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -68,7 +68,6 @@ export class UmbPickerSearchResultElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
#renderResultItem(item: UmbEntityModel) {
|
||||
console.log('pickableFilter', this.pickableFilter(item));
|
||||
return html`
|
||||
<umb-extension-with-api-slot
|
||||
type="pickerSearchResultItem"
|
||||
|
||||
@@ -44,7 +44,12 @@ export class UmbPropertyActionMenuElement extends UmbLitElement {
|
||||
override render() {
|
||||
if (!this._actions?.length) return nothing;
|
||||
return html`
|
||||
<uui-button id="popover-trigger" popovertarget="property-action-popover" label="Open actions menu" compact>
|
||||
<uui-button
|
||||
id="popover-trigger"
|
||||
popovertarget="property-action-popover"
|
||||
data-mark="open-property-actions"
|
||||
label=${this.localize.term('actions_viewActionsFor')}
|
||||
compact>
|
||||
<uui-symbol-more id="more-symbol"></uui-symbol-more>
|
||||
</uui-button>
|
||||
<uui-popover-container id="property-action-popover">
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { UmbNameablePropertyDatasetContext } from './nameable-property-data
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import { UmbArrayState, UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
import { UmbVariantContext, UmbVariantId } from '@umbraco-cms/backoffice/variant';
|
||||
|
||||
/**
|
||||
* A base property dataset context implementation.
|
||||
@@ -32,26 +32,34 @@ export class UmbPropertyDatasetContextBase
|
||||
#readOnly = new UmbBooleanState(false);
|
||||
public readOnly = this.#readOnly.asObservable();
|
||||
|
||||
#variantId: UmbVariantId = UmbVariantId.CreateInvariant();
|
||||
#variantContext = new UmbVariantContext(this).inherit();
|
||||
|
||||
getEntityType() {
|
||||
return this._entityType;
|
||||
}
|
||||
|
||||
getUnique() {
|
||||
return this._unique;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return this.#name.getValue();
|
||||
}
|
||||
|
||||
setName(name: string | undefined) {
|
||||
this.#name.setValue(name);
|
||||
}
|
||||
|
||||
getVariantId() {
|
||||
return UmbVariantId.CreateInvariant();
|
||||
return this.#variantId;
|
||||
}
|
||||
// variant id for a specific property?
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
// The controller alias, is a very generic name cause we want only one of these for this controller host.
|
||||
super(host, UMB_PROPERTY_DATASET_CONTEXT);
|
||||
this.#variantContext.setVariantId(this.getVariantId());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
|
||||
it('is not permitted for a variant when no states', (done) => {
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(invariantVariant, propB)
|
||||
.isPermittedForVariantAndProperty(invariantVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -82,7 +82,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(ruleEn);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.true;
|
||||
done();
|
||||
@@ -94,7 +94,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(ruleInv);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -106,7 +106,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(statePropAInv);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -117,7 +117,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(statePropAInv);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(invariantVariant, propB)
|
||||
.isPermittedForVariantAndProperty(invariantVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -129,7 +129,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(statePropAInv);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propA)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propA, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -141,7 +141,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(rulePlain);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.true;
|
||||
done();
|
||||
@@ -154,7 +154,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(ruleNoEn);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -166,7 +166,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(ruleNoPlain);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -179,7 +179,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(ruleEn);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -193,7 +193,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(ruleNoEn);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -206,7 +206,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(rulePlain);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB)
|
||||
.isPermittedForVariantAndProperty(englishVariant, propB, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -220,7 +220,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(statePropAInv);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(invariantVariant, propA)
|
||||
.isPermittedForVariantAndProperty(invariantVariant, propA, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
@@ -234,7 +234,7 @@ describe('UmbVariantPropertyGuardManager', () => {
|
||||
manager.addRule(stateNoPropAInv);
|
||||
|
||||
manager
|
||||
.isPermittedForVariantAndProperty(invariantVariant, propA)
|
||||
.isPermittedForVariantAndProperty(invariantVariant, propA, invariantVariant)
|
||||
.subscribe((value) => {
|
||||
expect(value).to.be.false;
|
||||
done();
|
||||
|
||||
@@ -5,21 +5,39 @@ import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models';
|
||||
import { UmbGuardManagerBase } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
export interface UmbVariantPropertyGuardRule extends UmbPropertyGuardRule {
|
||||
/**
|
||||
* @description - The variant id of the property.
|
||||
* @type {UmbVariantId}
|
||||
* @memberof UmbVariantPropertyGuardRule
|
||||
*/
|
||||
variantId?: UmbVariantId;
|
||||
|
||||
/**
|
||||
* @description - The variant id of the dataset. This is used to determine if the rule applies to the current dataset.
|
||||
* @type {UmbVariantId}
|
||||
* @memberof UmbVariantPropertyGuardRule
|
||||
*/
|
||||
datasetVariantId?: UmbVariantId;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rule
|
||||
* @param variantId
|
||||
* @param propertyType
|
||||
* @param {UmbVariantPropertyGuardRule} rule - The rule to check.
|
||||
* @param {UmbVariantId} variantId - The property variant id to check.
|
||||
* @param {UmbReferenceByUnique} propertyType - The property type to check.
|
||||
* @param {UmbVariantId} datasetVariantId - The variant id of the dataset. This is used to determine if the rule applies to the current dataset.
|
||||
* @returns {boolean} - Returns true if the rule applies to the given conditions.
|
||||
*/
|
||||
function findRule(rule: UmbVariantPropertyGuardRule, variantId: UmbVariantId, propertyType: UmbReferenceByUnique) {
|
||||
function findRule(
|
||||
rule: UmbVariantPropertyGuardRule,
|
||||
variantId: UmbVariantId,
|
||||
propertyType: UmbReferenceByUnique,
|
||||
datasetVariantId: UmbVariantId,
|
||||
) {
|
||||
return (
|
||||
(rule.variantId?.compare(variantId) && rule.propertyType?.unique === propertyType.unique) ||
|
||||
(rule.variantId === undefined && rule.propertyType?.unique === propertyType.unique) ||
|
||||
(rule.variantId?.compare(variantId) && rule.propertyType === undefined) ||
|
||||
(rule.variantId === undefined && rule.propertyType === undefined)
|
||||
(rule.variantId === undefined || rule.variantId.culture === variantId.culture) &&
|
||||
(rule.propertyType === undefined || rule.propertyType.unique === propertyType.unique) &&
|
||||
(rule.datasetVariantId === undefined || rule.datasetVariantId.culture === datasetVariantId.culture)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,33 +52,54 @@ export class UmbVariantPropertyGuardManager extends UmbGuardManagerBase<UmbVaria
|
||||
* Checks if the variant and propertyType is permitted.
|
||||
* @param {UmbVariantId} variantId - The variant id to check.
|
||||
* @param {UmbReferenceByUnique} propertyType - The property type to check.
|
||||
* @param {UmbVariantId} datasetVariantId - The dataset variant id to check.
|
||||
* @returns {Observable<boolean>} - Returns an observable that emits true if the variant and propertyType is permitted, false otherwise.
|
||||
* @memberof UmbVariantPropertyGuardManager
|
||||
*/
|
||||
isPermittedForVariantAndProperty(variantId: UmbVariantId, propertyType: UmbReferenceByUnique): Observable<boolean> {
|
||||
return this._rules.asObservablePart((rules) => this.#resolvePermission(rules, variantId, propertyType));
|
||||
isPermittedForVariantAndProperty(
|
||||
variantId: UmbVariantId,
|
||||
propertyType: UmbReferenceByUnique,
|
||||
datasetVariantId: UmbVariantId,
|
||||
): Observable<boolean> {
|
||||
return this._rules.asObservablePart((rules) =>
|
||||
this.#resolvePermission(rules, variantId, propertyType, datasetVariantId),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the variant and propertyType is permitted.
|
||||
* @param {UmbVariantId} variantId - The variant id to check.
|
||||
* @param {UmbReferenceByUnique} propertyType - The property type to check.
|
||||
* @param {UmbVariantId} datasetVariantId - The dataset variant id to check.
|
||||
* @returns {boolean} - Returns true if the variant and propertyType is permitted, false otherwise.
|
||||
* @memberof UmbVariantPropertyGuardManager
|
||||
*/
|
||||
getIsPermittedForVariantAndProperty(variantId: UmbVariantId, propertyType: UmbReferenceByUnique): boolean {
|
||||
return this.#resolvePermission(this._rules.getValue(), variantId, propertyType);
|
||||
getIsPermittedForVariantAndProperty(
|
||||
variantId: UmbVariantId,
|
||||
propertyType: UmbReferenceByUnique,
|
||||
datasetVariantId: UmbVariantId,
|
||||
): boolean {
|
||||
return this.#resolvePermission(this._rules.getValue(), variantId, propertyType, datasetVariantId);
|
||||
}
|
||||
|
||||
#resolvePermission(
|
||||
rules: UmbVariantPropertyGuardRule[],
|
||||
variantId: UmbVariantId,
|
||||
propertyType: UmbReferenceByUnique,
|
||||
datasetVariantId: UmbVariantId,
|
||||
) {
|
||||
if (rules.filter((x) => x.permitted === false).some((rule) => findRule(rule, variantId, propertyType))) {
|
||||
if (
|
||||
rules
|
||||
.filter((x) => x.permitted === false)
|
||||
.some((rule) => findRule(rule, variantId, propertyType, datasetVariantId))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (rules.filter((x) => x.permitted === true).some((rule) => findRule(rule, variantId, propertyType))) {
|
||||
if (
|
||||
rules
|
||||
.filter((x) => x.permitted === true)
|
||||
.some((rule) => findRule(rule, variantId, propertyType, datasetVariantId))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return this._fallback;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import type { UmbDataSourceResponse } from '../data-source-response.interface.js';
|
||||
import type { UmbItemDataSource } from './item-data-source.interface.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
import { tryExecute } from '@umbraco-cms/backoffice/resources';
|
||||
|
||||
export interface UmbItemServerDataSourceBaseArgs<ServerItemType, ClientItemType extends { unique: string }> {
|
||||
getItems: (uniques: Array<string>) => Promise<UmbDataSourceResponse<Array<ServerItemType>>>;
|
||||
getItems?: (uniques: Array<string>) => Promise<UmbDataSourceResponse<Array<ServerItemType>>>;
|
||||
mapper: (item: ServerItemType) => ClientItemType;
|
||||
}
|
||||
|
||||
@@ -14,10 +15,10 @@ export interface UmbItemServerDataSourceBaseArgs<ServerItemType, ClientItemType
|
||||
* @implements {DocumentTreeDataSource}
|
||||
*/
|
||||
export abstract class UmbItemServerDataSourceBase<ServerItemType, ClientItemType extends { unique: string }>
|
||||
extends UmbControllerBase
|
||||
implements UmbItemDataSource<ClientItemType>
|
||||
{
|
||||
#host: UmbControllerHost;
|
||||
#getItems: (uniques: Array<string>) => Promise<UmbDataSourceResponse<Array<ServerItemType>>>;
|
||||
#getItems?: (uniques: Array<string>) => Promise<UmbDataSourceResponse<Array<ServerItemType>>>;
|
||||
#mapper: (item: ServerItemType) => ClientItemType;
|
||||
|
||||
/**
|
||||
@@ -27,7 +28,7 @@ export abstract class UmbItemServerDataSourceBase<ServerItemType, ClientItemType
|
||||
* @memberof UmbItemServerDataSourceBase
|
||||
*/
|
||||
constructor(host: UmbControllerHost, args: UmbItemServerDataSourceBaseArgs<ServerItemType, ClientItemType>) {
|
||||
this.#host = host;
|
||||
super(host);
|
||||
this.#getItems = args.getItems;
|
||||
this.#mapper = args.mapper;
|
||||
}
|
||||
@@ -39,14 +40,17 @@ export abstract class UmbItemServerDataSourceBase<ServerItemType, ClientItemType
|
||||
* @memberof UmbItemServerDataSourceBase
|
||||
*/
|
||||
async getItems(uniques: Array<string>) {
|
||||
if (!this.#getItems) throw new Error('getItems is not implemented');
|
||||
if (!uniques) throw new Error('Uniques are missing');
|
||||
const { data, error } = await tryExecute(this.#host, this.#getItems(uniques));
|
||||
|
||||
if (data) {
|
||||
const items = data.map((item) => this.#mapper(item));
|
||||
return { data: items };
|
||||
}
|
||||
const { data, error } = await tryExecute(this, this.#getItems(uniques));
|
||||
|
||||
return { error };
|
||||
return { data: this._getMappedItems(data), error };
|
||||
}
|
||||
|
||||
protected _getMappedItems(items: Array<ServerItemType> | undefined): Array<ClientItemType> | undefined {
|
||||
if (!items) return undefined;
|
||||
if (!this.#mapper) throw new Error('Mapper is not implemented');
|
||||
return items.map((item) => this.#mapper(item));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ export class UmbRepositoryDetailsManager<DetailType extends { unique: string }>
|
||||
return this.#init;
|
||||
}
|
||||
|
||||
#uniques = new UmbArrayState<string>([], (x) => x);
|
||||
#uniques = new UmbArrayState<DetailType['unique']>([], (x) => x);
|
||||
uniques = this.#uniques.asObservable();
|
||||
|
||||
#entries = new UmbArrayState<DetailType>([], (x) => x.unique);
|
||||
#entries = new UmbArrayState<DetailType, DetailType['unique']>([], (x) => x.unique);
|
||||
entries = this.#entries.asObservable();
|
||||
|
||||
#statuses = new UmbArrayState<UmbRepositoryRequestStatus>([], (x) => x.unique);
|
||||
@@ -77,11 +77,15 @@ export class UmbRepositoryDetailsManager<DetailType extends { unique: string }>
|
||||
this.uniques,
|
||||
(uniques) => {
|
||||
// remove entries based on no-longer existing uniques:
|
||||
const removedEntries = this.#entries.getValue().filter((entry) => !uniques.includes(entry.unique));
|
||||
this.#entries.remove(removedEntries);
|
||||
const removedEntries = this.#entries
|
||||
.getValue()
|
||||
.filter((entry) => !uniques.includes(entry.unique))
|
||||
.map((x) => x.unique);
|
||||
|
||||
this.#statuses.remove(removedEntries);
|
||||
this.#entries.remove(removedEntries);
|
||||
removedEntries.forEach((entry) => {
|
||||
this.removeUmbControllerByAlias('observeEntry_' + entry.unique);
|
||||
this.removeUmbControllerByAlias('observeEntry_' + entry);
|
||||
});
|
||||
|
||||
this.#requestNewDetails();
|
||||
|
||||
@@ -16,6 +16,7 @@ export class UmbApiInterceptorController extends UmbControllerBase {
|
||||
this.addAuthResponseInterceptor(client);
|
||||
this.addUmbGeneratedResourceInterceptor(client);
|
||||
this.addUmbNotificationsInterceptor(client);
|
||||
this.addForbiddenResponseInterceptor(client);
|
||||
this.addErrorInterceptor(client);
|
||||
}
|
||||
|
||||
@@ -38,6 +39,24 @@ export class UmbApiInterceptorController extends UmbControllerBase {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Interceptor which checks responses for 403 errors and displays them as a notification.
|
||||
* @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors.
|
||||
* @internal
|
||||
*/
|
||||
addForbiddenResponseInterceptor(client: typeof umbHttpClient) {
|
||||
client.interceptors.response.use(async (response: Response) => {
|
||||
if (response.status === 403) {
|
||||
const headline = 'Permission Denied';
|
||||
const message = 'You do not have the necessary permissions to complete the requested action. If you believe this is in error, please reach out to your administrator.';
|
||||
|
||||
this.#peekError(headline, message, null);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Interceptor which checks responses for the Umb-Generated-Resource header and replaces the value into the response body.
|
||||
* @param {umbHttpClient} client The OpenAPI client to add the interceptor to. It can be any client supporting Response and Request interceptors.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface UmbDataApiResponse<ResponseType extends { data: unknown } = { data: unknown }> {
|
||||
data: ResponseType['data'];
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
export * from './api-interceptor.controller.js';
|
||||
export * from './resource.controller.js';
|
||||
export * from './try-execute.controller.js';
|
||||
export * from './tryExecute.function.js';
|
||||
export * from './tryExecuteAndNotify.function.js';
|
||||
export * from './tryXhrRequest.function.js';
|
||||
export * from './extractUmbNotificationColor.function.js';
|
||||
export * from './extractUmbColorVariable.function.js';
|
||||
export * from './isUmbNotifications.function.js';
|
||||
export * from './apiTypeValidators.function.js';
|
||||
export * from './extractUmbColorVariable.function.js';
|
||||
export * from './extractUmbNotificationColor.function.js';
|
||||
export * from './isUmbNotifications.function.js';
|
||||
export * from './resource.controller.js';
|
||||
export * from './try-execute/index.js';
|
||||
export * from './umb-error.js';
|
||||
export type * from './types.js';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { tryExecute } from './tryExecute.function.js';
|
||||
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
|
||||
|
||||
/**
|
||||
* Batches promises and returns a promise that resolves to an array of results
|
||||
* @param {UmbControllerHost} host - The host to use for the request and where notifications will be shown
|
||||
* @param {Array<Array<BatchEntryType>>} chunks - The array of chunks to process
|
||||
* @param {(chunk: Array<BatchEntryType>) => Promise<PromiseResult>} callback - The function to call for each chunk
|
||||
* @returns {Promise<PromiseSettledResult<PromiseResult>[]>} - A promise that resolves to an array of results
|
||||
*/
|
||||
export function batchTryExecute<BatchEntryType, PromiseResult>(
|
||||
host: UmbControllerHost,
|
||||
chunks: Array<Array<BatchEntryType>>,
|
||||
callback: (chunk: Array<BatchEntryType>) => Promise<PromiseResult>,
|
||||
): Promise<PromiseSettledResult<PromiseResult>[]> {
|
||||
return Promise.allSettled(chunks.map((chunk) => tryExecute(host, callback(chunk), { disableNotifications: true })));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './batch-try-execute.function.js';
|
||||
export * from './try-execute.controller.js';
|
||||
export * from './tryExecute.function.js';
|
||||
export * from './tryExecuteAndNotify.function.js';
|
||||
export * from './tryXhrRequest.function.js';
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isProblemDetailsLike } from './apiTypeValidators.function.js';
|
||||
import { UmbResourceController } from './resource.controller.js';
|
||||
import type { UmbApiResponse, UmbTryExecuteOptions } from './types.js';
|
||||
import { UmbApiError, UmbCancelError } from './umb-error.js';
|
||||
import { isProblemDetailsLike } from '../apiTypeValidators.function.js';
|
||||
import { UmbResourceController } from '../resource.controller.js';
|
||||
import type { UmbApiResponse, UmbTryExecuteOptions } from '../types.js';
|
||||
import { UmbApiError, UmbCancelError } from '../umb-error.js';
|
||||
|
||||
export class UmbTryExecuteController<T> extends UmbResourceController<T> {
|
||||
#abortSignal?: AbortSignal;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user