diff --git a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupControllerBase.cs index a5691932b1aa..0112df812797 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/UserGroup/UserGroupControllerBase.cs @@ -54,8 +54,12 @@ protected IActionResult UserGroupOperationStatusResult(UserGroupOperationStatus .WithDetail("The assigned media start node does not exists.") .Build()), UserGroupOperationStatus.DocumentPermissionKeyNotFound => NotFound(new ProblemDetailsBuilder() - .WithTitle("A document permission key not found") - .WithDetail("A assigned document permission not exists.") + .WithTitle("Document permission key not found") + .WithDetail("An assigned document permission does not reference an existing document.") + .Build()), + UserGroupOperationStatus.DocumentTypePermissionKeyNotFound => NotFound(new ProblemDetailsBuilder() + .WithTitle("Document type permission key not found") + .WithDetail("An assigned document type permission does not reference an existing document type.") .Build()), UserGroupOperationStatus.LanguageNotFound => NotFound(problemDetailsBuilder .WithTitle("Language not found") diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs index b7902e490a8c..525215157392 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/UserGroupsBuilderExtensions.cs @@ -13,10 +13,11 @@ internal static IUmbracoBuilder AddUserGroups(this IUmbracoBuilder builder) builder.Services.AddTransient(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(x=>x.GetRequiredService()); - builder.Services.AddSingleton(x=>x.GetRequiredService()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); return builder; } diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPropertyValuePermissionMapper.cs b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPropertyValuePermissionMapper.cs new file mode 100644 index 000000000000..ed0964abdaf2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Mapping/Permissions/DocumentPropertyValuePermissionMapper.cs @@ -0,0 +1,69 @@ +using Umbraco.Cms.Api.Management.ViewModels; +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; +using Umbraco.Cms.Core.Models.Membership.Permissions; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Persistence.Mappers; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Mapping.Permissions; + +public class DocumentPropertyValuePermissionMapper : IPermissionPresentationMapper, IPermissionMapper +{ + public string Context => DocumentPropertyValueGranularPermission.ContextType; + + public IGranularPermission MapFromDto(UserGroup2GranularPermissionDto dto) => + new DocumentPropertyValueGranularPermission() + { + Key = dto.UniqueId!.Value, + Permission = dto.Permission, + }; + + public Type PresentationModelToHandle => typeof(DocumentPropertyValuePermissionPresentationModel); + + public IEnumerable MapManyAsync(IEnumerable granularPermissions) + { + var intermediate = granularPermissions.Where(p => p.Key.HasValue).Select(p => + { + var parts = p.Permission.Split('|'); + return parts.Length == 2 && Guid.TryParse(parts[0], out Guid propertyTypeId) + ? new { DocumentTypeId = p.Key!.Value, PropertyTypeId = propertyTypeId, Verb = parts[1] } + : null; + }) + .WhereNotNull() + .ToArray(); + + var intermediateByDocumentType = intermediate.GroupBy(x => x.DocumentTypeId); + foreach (var documentTypeGroup in intermediateByDocumentType) + { + foreach (var propertyTypeGroup in documentTypeGroup.GroupBy(x => x.PropertyTypeId)) + { + yield return new DocumentPropertyValuePermissionPresentationModel + { + DocumentType = new ReferenceByIdModel(documentTypeGroup.Key), + PropertyType = new ReferenceByIdModel(propertyTypeGroup.Key), + Verbs = propertyTypeGroup + .Select(x => x.Verb) + .Where(verb => verb.IsNullOrWhiteSpace() is false) + .ToHashSet(), + }; + } + } + } + + public IEnumerable MapToGranularPermissions(IPermissionPresentationModel permissionViewModel) + { + if (permissionViewModel is not DocumentPropertyValuePermissionPresentationModel documentTypePermissionPresentationModel) + { + yield break; + } + + foreach (var verb in documentTypePermissionPresentationModel.Verbs.Distinct().DefaultIfEmpty(string.Empty)) + { + yield return new DocumentPropertyValueGranularPermission + { + Key = documentTypePermissionPresentationModel.DocumentType.Id, + Permission = $"{documentTypePermissionPresentationModel.PropertyType.Id}|{verb}" + }; + } + } +} diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index e4fabc31c8a0..5fcf8f314331 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -36405,6 +36405,9 @@ { "$ref": "#/components/schemas/DocumentPermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -36693,6 +36696,9 @@ { "$ref": "#/components/schemas/DocumentPermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -37568,6 +37574,48 @@ } } }, + "DocumentPropertyValuePermissionPresentationModel": { + "required": [ + "$type", + "documentType", + "propertyType", + "verbs" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "documentType": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + }, + "propertyType": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + }, + "verbs": { + "uniqueItems": true, + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DocumentPropertyValuePermissionPresentationModel": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" + } + } + }, "DocumentRecycleBinItemResponseModel": { "required": [ "createDate", @@ -46169,6 +46217,9 @@ { "$ref": "#/components/schemas/DocumentPermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -46592,6 +46643,9 @@ { "$ref": "#/components/schemas/DocumentPermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/DocumentPropertyValuePermissionPresentationModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/DocumentPropertyValuePermissionPresentationModel.cs new file mode 100644 index 000000000000..dc12a56bd70f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/Permissions/DocumentPropertyValuePermissionPresentationModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; + +public class DocumentPropertyValuePermissionPresentationModel : IPermissionPresentationModel +{ + public required ReferenceByIdModel DocumentType { get; set; } + + public required ReferenceByIdModel PropertyType { get; set; } + + public required ISet Verbs { get; set; } +} diff --git a/src/Umbraco.Core/Actions/ActionDocumentPropertyRead.cs b/src/Umbraco.Core/Actions/ActionDocumentPropertyRead.cs new file mode 100644 index 000000000000..bf5515b97a97 --- /dev/null +++ b/src/Umbraco.Core/Actions/ActionDocumentPropertyRead.cs @@ -0,0 +1,29 @@ +namespace Umbraco.Cms.Core.Actions; + +public class ActionDocumentPropertyRead : IAction +{ + /// + public const string ActionLetter = "Umb.Document.PropertyValue.Read"; + + /// + public const string ActionAlias = "documentpropertyread"; + + /// + public string Letter => ActionLetter; + + /// + public string Alias => ActionAlias; + + /// + public bool ShowInNotifier => false; + + /// + public bool CanBePermissionAssigned => true; + + /// + public string Icon => string.Empty; + + /// + public string Category => Constants.Conventions.PermissionCategories.OtherCategory; +} + diff --git a/src/Umbraco.Core/Actions/ActionDocumentPropertyWrite.cs b/src/Umbraco.Core/Actions/ActionDocumentPropertyWrite.cs new file mode 100644 index 000000000000..dec8ad0e3294 --- /dev/null +++ b/src/Umbraco.Core/Actions/ActionDocumentPropertyWrite.cs @@ -0,0 +1,29 @@ +namespace Umbraco.Cms.Core.Actions; + +public class ActionDocumentPropertyWrite : IAction +{ + /// + public const string ActionLetter = "Umb.Document.PropertyValue.Write"; + + /// + public const string ActionAlias = "documentpropertywrite"; + + /// + public string Letter => ActionLetter; + + /// + public string Alias => ActionAlias; + + /// + public bool ShowInNotifier => false; + + /// + public bool CanBePermissionAssigned => true; + + /// + public string Icon => string.Empty; + + /// + public string Category => Constants.Conventions.PermissionCategories.OtherCategory; +} + diff --git a/src/Umbraco.Core/Models/Membership/Permissions/DocumentPropertyValueGranularPermission.cs b/src/Umbraco.Core/Models/Membership/Permissions/DocumentPropertyValueGranularPermission.cs new file mode 100644 index 000000000000..0f386ea6d11e --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/Permissions/DocumentPropertyValueGranularPermission.cs @@ -0,0 +1,36 @@ +namespace Umbraco.Cms.Core.Models.Membership.Permissions; + +public class DocumentPropertyValueGranularPermission : INodeGranularPermission +{ + public const string ContextType = "DocumentTypeProperty"; + + public required Guid Key { get; set; } + + public string Context => ContextType; + + public required string Permission { get; set; } + + protected bool Equals(DocumentPropertyValueGranularPermission other) => Key.Equals(other.Key) && Permission == other.Permission; + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + return Equals((DocumentPropertyValueGranularPermission)obj); + } + + public override int GetHashCode() => HashCode.Combine(Key, Permission); +} diff --git a/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs b/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs index 7870429e4cc1..d658e07565e0 100644 --- a/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs +++ b/src/Umbraco.Core/Services/OperationStatus/UserGroupOperationStatus.cs @@ -14,6 +14,7 @@ public enum UserGroupOperationStatus MediaStartNodeKeyNotFound, DocumentStartNodeKeyNotFound, DocumentPermissionKeyNotFound, + DocumentTypePermissionKeyNotFound, LanguageNotFound, NameTooLong, AliasTooLong, diff --git a/src/Umbraco.Core/Services/UserGroupService.cs b/src/Umbraco.Core/Services/UserGroupService.cs index 1bf0a2870d00..ac7d1a40aaab 100644 --- a/src/Umbraco.Core/Services/UserGroupService.cs +++ b/src/Umbraco.Core/Services/UserGroupService.cs @@ -612,20 +612,26 @@ private UserGroupOperationStatus ValidateStartNodesExists(IUserGroup userGroup) private UserGroupOperationStatus ValidateGranularPermissionsExists(IUserGroup userGroup) { - IEnumerable documentKeys = userGroup.GranularPermissions.Select(granularPermission => - { - if (granularPermission is DocumentGranularPermission nodeGranularPermission) - { - return (Guid?)nodeGranularPermission.Key; - } + Guid[] documentKeys = userGroup.GranularPermissions + .OfType() + .Select(p => p.Key) + .ToArray(); - return null; - }).Where(x => x.HasValue).Cast().ToArray(); if (documentKeys.Any() && _entityService.Exists(documentKeys) is false) { return UserGroupOperationStatus.DocumentPermissionKeyNotFound; } + Guid[] documentTypeKeys = userGroup.GranularPermissions + .OfType() + .Select(p => p.Key) + .ToArray(); + + if (documentTypeKeys.Any() && _entityService.Exists(documentTypeKeys) is false) + { + return UserGroupOperationStatus.DocumentTypePermissionKeyNotFound; + } + return UserGroupOperationStatus.Success; } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 68209ee1559a..2ab912c7f606 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -195,10 +195,10 @@ private void CreateUserGroup2PermissionData() { var userGroupKeyToPermissions = new Dictionary>() { - [Constants.Security.AdminGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T"], - [Constants.Security.EditorGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T"], - [Constants.Security.WriterGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":" ], - [Constants.Security.TranslatorGroupKey] = [ActionUpdate.ActionLetter, ActionBrowse.ActionLetter], + [Constants.Security.AdminGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T", ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], + [Constants.Security.EditorGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T", ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], + [Constants.Security.WriterGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":" , ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], + [Constants.Security.TranslatorGroupKey] = [ActionUpdate.ActionLetter, ActionBrowse.ActionLetter, ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], }; var i = 1; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 395504cfdc9c..2afd3554a047 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -114,6 +114,7 @@ protected virtual void DefinePlan() // To 15.4.0 To("{A9E72794-4036-4563-B543-1717C73B8879}"); + To("{D1568C33-A697-455F-8D16-48060CB954A1}"); To("{33D62294-D0DE-4A86-A830-991EB36B96DA}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_4_0/AddDocumentPropertyPermissions.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_4_0/AddDocumentPropertyPermissions.cs new file mode 100644 index 000000000000..916788272338 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_4_0/AddDocumentPropertyPermissions.cs @@ -0,0 +1,47 @@ +using Umbraco.Cms.Core.Actions; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_4_0; + +[Obsolete("Remove in Umbraco 18.")] +internal class AddDocumentPropertyPermissions : MigrationBase +{ + public AddDocumentPropertyPermissions(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + List? userGroups = Database.Fetch(); + + foreach (UserGroupDto userGroupDto in userGroups ?? []) + { + List? currentPermissions = Database.Fetch( + "WHERE userGroupKey = @UserGroupKey", + new { UserGroupKey = userGroupDto.Key }); + + if (currentPermissions is null || currentPermissions.Count is 0) + { + continue; + } + + if (currentPermissions.Any(p => p.Permission == ActionDocumentPropertyRead.ActionLetter)) + { + return; + } + + Database.InsertBulk( + [ + new UserGroup2PermissionDto + { + UserGroupKey = userGroupDto.Key, Permission = ActionDocumentPropertyRead.ActionLetter + }, + new UserGroup2PermissionDto + { + UserGroupKey = userGroupDto.Key, Permission = ActionDocumentPropertyWrite.ActionLetter + } + ]); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs index cd9f104bc085..f437c0cd0d1d 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -220,6 +220,9 @@ protected override void PersistDeletedItem(IContentType entity) // Delete all PropertyData where propertytypeid EXISTS in the subquery above Database.Execute(SqlSyntax.GetDeleteSubquery(Constants.DatabaseSchema.Tables.PropertyData, "propertytypeid", sql)); + // delete all granular permissions for this content type + Database.Delete(Sql().Where(dto => dto.UniqueId == entity.Key)); + base.PersistDeletedItem(entity); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index 7a6b2132913f..1eb3fa47a7dc 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -447,7 +447,6 @@ protected override IEnumerable GetDeleteClauses() "DELETE FROM umbracoUserGroup2App WHERE userGroupId = @id", "DELETE FROM umbracoUserGroup2Permission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", "DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", - "DELETE FROM umbracoUserGroup2GranularPermission WHERE userGroupKey IN (SELECT [umbracoUserGroup].[Key] FROM umbracoUserGroup WHERE Id = @id)", "DELETE FROM umbracoUserGroup WHERE id = @id", }; return list; diff --git a/src/Umbraco.Web.UI.Client/.vscode/settings.json b/src/Umbraco.Web.UI.Client/.vscode/settings.json index 8fc1bce8e6c8..4b30430f590d 100644 --- a/src/Umbraco.Web.UI.Client/.vscode/settings.json +++ b/src/Umbraco.Web.UI.Client/.vscode/settings.json @@ -28,7 +28,9 @@ "uninitialize", "unprovide", "unpublishing", - "variantable" + "variantable", + "viewability", + "writability" ], "exportall.config.folderListener": [], "exportall.config.relExclusion": [], diff --git a/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/README.md b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/README.md new file mode 100644 index 000000000000..8ba6aee4cd45 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/README.md @@ -0,0 +1,5 @@ +# Manipulate Document Property Value Permissions + +This example demonstrates the essence of the Document Property Value Permissions by using three Workspace Actions. + +Use the toggles to change rules for the Rich Text Editor in the 'All Properties' Document. diff --git a/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/index.ts b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/index.ts new file mode 100644 index 000000000000..d43f3431c3e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/index.ts @@ -0,0 +1,80 @@ +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; + +export const manifests: Array = [ + { + type: 'workspaceAction', + kind: 'default', + name: 'Example Manipulate Document Property Value Write Permissions Workspace Action', + alias: 'example.workspaceAction.manipulate.write', + weight: 1000, + api: () => import('./manipulate-property-write-permissions-action.js'), + meta: { + label: 'Toggle write RTE', + look: 'primary', + color: 'danger', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], + }, + { + type: 'workspaceAction', + kind: 'default', + name: 'Example Manipulate Document Property Value Readonly Permissions Workspace Action', + alias: 'example.workspaceAction.manipulate.readonly', + weight: 1000, + api: () => import('./manipulate-property-readonly-permissions-action.js'), + meta: { + label: 'Toggle readonly RTE', + look: 'primary', + color: 'danger', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], + }, + { + type: 'workspaceAction', + kind: 'default', + name: 'Example Manipulate Document Property Value View Permissions Workspace Action', + alias: 'example.workspaceAction.manipulate.view', + weight: 1000, + api: () => import('./manipulate-property-view-permissions-action.js'), + meta: { + label: 'Toggle view RTE', + look: 'primary', + color: 'danger', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], + }, + { + type: 'workspaceAction', + kind: 'default', + name: 'Example Manipulate Document Property Value Write Complex Permissions Workspace Action', + alias: 'example.workspaceAction.manipulate.writeComplex', + weight: 1000, + api: () => import('./manipulate-property-write-complex-permissions-action.js'), + meta: { + label: 'Toggle composed write RTE', + look: 'primary', + color: 'danger', + }, + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: 'Umb.Workspace.Document', + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-readonly-permissions-action.ts b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-readonly-permissions-action.ts new file mode 100644 index 000000000000..04744b3ca716 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-readonly-permissions-action.ts @@ -0,0 +1,31 @@ +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbWorkspaceActionBase, type UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; + +// The Example Incrementor Workspace Action Controller: +export class ExampleWorkspaceActionManipulateReadonlyPermission + extends UmbWorkspaceActionBase + implements UmbWorkspaceAction +{ + #isOn = false; + + // This method is executed + override async execute() { + const context = await this.getContext(UMB_CONTENT_WORKSPACE_CONTEXT); + if (this.#isOn) { + context.propertyReadonlyGuard.removeRule('exampleRule'); + } else { + context.propertyReadonlyGuard.addRule({ + unique: 'exampleRule', + propertyType: { unique: '1_tipTap' }, // Notice ID is very short here as this is the mock data. Real Property type IDs are GUIDs. + //variantId: UmbVariantId.Create({ culture: 'en-US', segment: null }), + permitted: true, + }); + } + + this.#isOn = !this.#isOn; + } +} + +// Declare a api export, so Extension Registry can initialize this class: +export const api = ExampleWorkspaceActionManipulateReadonlyPermission; diff --git a/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-view-permissions-action.ts b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-view-permissions-action.ts new file mode 100644 index 000000000000..308a62258cc8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-view-permissions-action.ts @@ -0,0 +1,31 @@ +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbWorkspaceActionBase, type UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; + +// The Example Incrementor Workspace Action Controller: +export class ExampleWorkspaceActionManipulateWiewPermission + extends UmbWorkspaceActionBase + implements UmbWorkspaceAction +{ + #isOn = false; + + // This method is executed + override async execute() { + const context = await this.getContext(UMB_CONTENT_WORKSPACE_CONTEXT); + if (this.#isOn) { + context.propertyViewGuard.removeRule('exampleRule'); + } else { + context.propertyViewGuard.addRule({ + unique: 'exampleRule', + propertyType: { unique: '1_tipTap' }, // Notice ID is very short here as this is the mock data. Real Property type IDs are GUIDs. + //variantId: UmbVariantId.Create({ culture: 'en-US', segment: null }), + permitted: false, + }); + } + + this.#isOn = !this.#isOn; + } +} + +// Declare a api export, so Extension Registry can initialize this class: +export const api = ExampleWorkspaceActionManipulateWiewPermission; diff --git a/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-write-complex-permissions-action.ts b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-write-complex-permissions-action.ts new file mode 100644 index 000000000000..6890365600f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-write-complex-permissions-action.ts @@ -0,0 +1,38 @@ +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbWorkspaceActionBase, type UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; + +// The Example Incrementor Workspace Action Controller: +export class ExampleWorkspaceActionManipulateWriteComplexPermission + extends UmbWorkspaceActionBase + implements UmbWorkspaceAction +{ + #isOn = false; + + // This method is executed + override async execute() { + const context = await this.getContext(UMB_CONTENT_WORKSPACE_CONTEXT); + if (this.#isOn) { + context.propertyWriteGuard.removeRule('exampleRule'); + context.propertyWriteGuard.removeRule('exampleRule2'); + } else { + context.propertyWriteGuard.addRule({ + unique: 'exampleRule', + propertyType: { unique: '1_tipTap' }, // Notice ID is very short here as this is the mock data. Real Property type IDs are GUIDs. + //variantId: UmbVariantId.Create({ culture: 'en-US', segment: null }), + permitted: false, + }); + context.propertyWriteGuard.addRule({ + unique: 'exampleRule2', + propertyType: { unique: '1_tipTap' }, // Notice ID is very short here as this is the mock data. Real Property type IDs are GUIDs. + variantId: UmbVariantId.Create({ culture: 'en-US', segment: null }), + permitted: true, + }); + } + + this.#isOn = !this.#isOn; + } +} + +// Declare a api export, so Extension Registry can initialize this class: +export const api = ExampleWorkspaceActionManipulateWriteComplexPermission; diff --git a/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-write-permissions-action.ts b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-write-permissions-action.ts new file mode 100644 index 000000000000..601749ea8212 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/examples/manipulate-document-property-value-permissions/manipulate-property-write-permissions-action.ts @@ -0,0 +1,37 @@ +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbWorkspaceActionBase, type UmbWorkspaceAction } from '@umbraco-cms/backoffice/workspace'; + +// The Example Incrementor Workspace Action Controller: +export class ExampleWorkspaceActionManipulateWritePermission + extends UmbWorkspaceActionBase + implements UmbWorkspaceAction +{ + #isOn = false; + + // This method is executed + override async execute() { + const context = await this.getContext(UMB_CONTENT_WORKSPACE_CONTEXT); + if (this.#isOn) { + context.propertyWriteGuard.removeRule('exampleRule'); + } else { + context.propertyWriteGuard.addRule({ + unique: 'exampleRule', + propertyType: { unique: '1_tipTap' }, // Notice ID is very short here as this is the mock data. Real Property type IDs are GUIDs. + //variantId: UmbVariantId.Create({ culture: 'en-US', segment: null }), + permitted: false, + }); + context.propertyWriteGuard.addRule({ + unique: 'exampleRule2', + propertyType: { unique: '1_tipTap' }, // Notice ID is very short here as this is the mock data. Real Property type IDs are GUIDs. + variantId: UmbVariantId.Create({ culture: 'en-US', segment: null }), + permitted: true, + }); + } + + this.#isOn = !this.#isOn; + } +} + +// Declare a api export, so Extension Registry can initialize this class: +export const api = ExampleWorkspaceActionManipulateWritePermission; diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 79a42ed9e561..48a4550d6ea2 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -1973,9 +1973,6 @@ export default { permissionsGranularHelp: 'Set permissions for specific nodes', granularRightsLabel: 'Documents', granularRightsDescription: 'Assign permissions to specific documents', - permissionsEntityGroup_document: 'Content', - permissionsEntityGroup_media: 'Media', - permissionsEntityGroup_member: 'Member', profile: 'Profile', searchAllChildren: 'Search all children', languagesHelp: 'Limit the languages users have access to edit', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 7c9603f4022d..ff9d4b2081a3 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2051,9 +2051,11 @@ export default { permissionsGranularHelp: 'Set permissions for specific nodes', granularRightsLabel: 'Documents', granularRightsDescription: 'Assign permissions to specific documents', - permissionsEntityGroup_document: 'Content', + permissionsEntityGroup_document: 'Document', permissionsEntityGroup_media: 'Media', permissionsEntityGroup_member: 'Member', + 'permissionsEntityGroup_document-property-value': 'Document Property Value', + permissionNoVerbs: 'No allowed permissions', profile: 'Profile', searchAllChildren: 'Search all children', languagesHelp: 'Limit the languages users have access to edit', diff --git a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts index aadb2351cf8f..faa546c04f5e 100644 --- a/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/external/backend-api/src/types.gen.ts @@ -408,7 +408,7 @@ export type CreateUserGroupRequestModel = { mediaStartNode?: ((ReferenceByIdModel) | null); mediaRootAccess: boolean; fallbackPermissions: Array<(string)>; - permissions: Array<(DocumentPermissionPresentationModel | UnknownTypePermissionPresentationModel)>; + permissions: Array<(DocumentPermissionPresentationModel | DocumentPropertyValuePermissionPresentationModel | UnknownTypePermissionPresentationModel)>; id?: (string) | null; }; @@ -460,7 +460,7 @@ export type CurrentUserResponseModel = { hasAccessToAllLanguages: boolean; hasAccessToSensitiveData: boolean; fallbackPermissions: Array<(string)>; - permissions: Array<(DocumentPermissionPresentationModel | UnknownTypePermissionPresentationModel)>; + permissions: Array<(DocumentPermissionPresentationModel | DocumentPropertyValuePermissionPresentationModel | UnknownTypePermissionPresentationModel)>; allowedSections: Array<(string)>; isAdmin: boolean; }; @@ -680,6 +680,13 @@ export type DocumentPermissionPresentationModel = { verbs: Array<(string)>; }; +export type DocumentPropertyValuePermissionPresentationModel = { + $type: string; + documentType: (ReferenceByIdModel); + propertyType: (ReferenceByIdModel); + verbs: Array<(string)>; +}; + export type DocumentRecycleBinItemResponseModel = { id: string; createDate: string; @@ -2707,7 +2714,7 @@ export type UpdateUserGroupRequestModel = { mediaStartNode?: ((ReferenceByIdModel) | null); mediaRootAccess: boolean; fallbackPermissions: Array<(string)>; - permissions: Array<(DocumentPermissionPresentationModel | UnknownTypePermissionPresentationModel)>; + permissions: Array<(DocumentPermissionPresentationModel | DocumentPropertyValuePermissionPresentationModel | UnknownTypePermissionPresentationModel)>; }; export type UpdateUserGroupsOnUserRequestModel = { @@ -2807,7 +2814,7 @@ export type UserGroupResponseModel = { mediaStartNode?: ((ReferenceByIdModel) | null); mediaRootAccess: boolean; fallbackPermissions: Array<(string)>; - permissions: Array<(DocumentPermissionPresentationModel | UnknownTypePermissionPresentationModel)>; + permissions: Array<(DocumentPermissionPresentationModel | DocumentPropertyValuePermissionPresentationModel | UnknownTypePermissionPresentationModel)>; id: string; isDeletable: boolean; aliasCanBeChanged: boolean; diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts index 540384e57c31..ce7b0faabcf7 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document-type/document-type.data.ts @@ -82,7 +82,7 @@ export const data: Array = [ isFolder: false, properties: [ { - id: '1', + id: '1_tipTap', container: { id: 'all-properties-group-key', }, @@ -92,7 +92,7 @@ export const data: Array = [ dataType: { id: 'dt-richTextEditorTiptap', }, - variesByCulture: false, + variesByCulture: true, variesBySegment: false, sortOrder: 0, validation: { diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts index d2f7f94e8f68..ab62454c9ed7 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts @@ -1,5 +1,24 @@ import type { UserGroupItemResponseModel, UserGroupResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UMB_CONTENT_SECTION_ALIAS } from '@umbraco-cms/backoffice/content'; +import { + UMB_USER_PERMISSION_DOCUMENT_CREATE, + UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT, + UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES, + UMB_USER_PERMISSION_DOCUMENT_DELETE, + UMB_USER_PERMISSION_DOCUMENT_DUPLICATE, + UMB_USER_PERMISSION_DOCUMENT_MOVE, + UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS, + UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, + UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_READ, + UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_WRITE, + UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS, + UMB_USER_PERMISSION_DOCUMENT_PUBLISH, + UMB_USER_PERMISSION_DOCUMENT_READ, + UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, + UMB_USER_PERMISSION_DOCUMENT_SORT, + UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, + UMB_USER_PERMISSION_DOCUMENT_UPDATE, +} from '@umbraco-cms/backoffice/document'; export type UmbMockUserGroupModel = UserGroupResponseModel & UserGroupItemResponseModel; @@ -10,21 +29,23 @@ export const data: Array = [ alias: 'admin', icon: 'icon-medal', fallbackPermissions: [ - 'Umb.Document.Read', - 'Umb.Document.Create', - 'Umb.Document.Update', - 'Umb.Document.Delete', - 'Umb.Document.CreateBlueprint', - 'Umb.Document.Notifications', - 'Umb.Document.Publish', - 'Umb.Document.Permissions', - 'Umb.Document.Unpublish', - 'Umb.Document.Duplicate', - 'Umb.Document.Move', - 'Umb.Document.Sort', - 'Umb.Document.CultureAndHostnames', - 'Umb.Document.PublicAccess', - 'Umb.Document.Rollback', + UMB_USER_PERMISSION_DOCUMENT_READ, + UMB_USER_PERMISSION_DOCUMENT_CREATE, + UMB_USER_PERMISSION_DOCUMENT_UPDATE, + UMB_USER_PERMISSION_DOCUMENT_DELETE, + UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT, + UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS, + UMB_USER_PERMISSION_DOCUMENT_PUBLISH, + UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, + UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, + UMB_USER_PERMISSION_DOCUMENT_DUPLICATE, + UMB_USER_PERMISSION_DOCUMENT_MOVE, + UMB_USER_PERMISSION_DOCUMENT_SORT, + UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES, + UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS, + UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, + UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_READ, + UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_WRITE, ], permissions: [ { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.test.ts index 8f34c4aa0f55..ab9ef567f496 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/copy/block-grid-to-block-copy-translator.test.ts @@ -25,6 +25,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => { alias: 'headline', editorAlias: 'Umbraco.TextBox', value: 'Headline value', + entityType: '', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.test.ts index 571720619f55..d0609714d26b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/block/paste/block-to-block-grid-paste-translator.test.ts @@ -26,6 +26,7 @@ describe('UmbBlockToBlockGridClipboardPastePropertyValueTranslator', () => { alias: 'headline', editorAlias: 'Umbraco.TextBox', value: 'Headline value', + entityType: '', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.test.ts index 48e584d2d286..9bb6aed26aac 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/copy/block-grid-to-grid-block-copy-translator.test.ts @@ -30,6 +30,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => { alias: 'headline', editorAlias: 'Umbraco.TextBox', value: 'Headline value', + entityType: '', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/paste/grid-block-to-block-grid-paste-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/paste/grid-block-to-block-grid-paste-translator.test.ts index 105cfa9e18a7..ace3d0d20c54 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/paste/grid-block-to-block-grid-paste-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/clipboard/grid-block/paste/grid-block-to-block-grid-paste-translator.test.ts @@ -24,6 +24,7 @@ describe('UmbGridBlockToBlockGridClipboardPastePropertyValueTranslator', () => { alias: 'headline', editorAlias: 'Umbraco.TextBox', value: 'Headline value', + entityType: '', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts index d721602a662b..3c0df0f3ae0c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entries/block-grid-entries.element.ts @@ -260,7 +260,7 @@ export class UmbBlockGridEntriesElement extends UmbFormControlMixin(UmbLitElemen ); this.observe( - manager.readOnlyState.isReadOnly, + manager.readOnlyState.permitted, (isReadOnly) => (this._isReadOnly = isReadOnly), 'observeIsReadOnly', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts index edf7e97a2aff..7cca1d8ff68c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/components/block-grid-entry/block-grid-entry.element.ts @@ -286,7 +286,7 @@ export class UmbBlockGridEntryElement extends UmbLitElement implements UmbProper ); this.observe( - this.#context.readOnlyState.isReadOnly, + this.#context.readOnlyGuard.permitted, (isReadOnly) => (this._isReadOnly = isReadOnly), 'umbReadonlyObserver', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts index 3434ca228605..df6996cbc4da 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-grid/property-editors/block-grid-editor/property-editor-ui-block-grid.element.ts @@ -59,6 +59,25 @@ export class UmbPropertyEditorUIBlockGridElement this.#managerContext.setEditorConfiguration(config); } + /** + * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. + * @type {boolean} + * @default false + */ + public set readonly(value) { + this.#readonly = value; + + if (this.#readonly) { + this.#managerContext.readOnlyState.fallbackToPermitted(); + } else { + this.#managerContext.readOnlyState.fallbackToDisallowed(); + } + } + public get readonly() { + return this.#readonly; + } + #readonly = false; + @state() private _layoutColumns?: number; @@ -186,28 +205,6 @@ export class UmbPropertyEditorUIBlockGridElement }, 'observePropertyAlias', ); - - // If the current property is readonly all inner block content should also be readonly. - this.observe( - observeMultiple([propertyContext.isReadOnly, propertyContext.variantId]), - ([isReadOnly, variantId]) => { - const unique = 'UMB_PROPERTY_EDITOR_UI'; - if (variantId === undefined) return; - - if (isReadOnly) { - const state = { - unique, - variantId, - message: '', - }; - - this.#managerContext.readOnlyState.addState(state); - } else { - this.#managerContext.readOnlyState.removeState(unique); - } - }, - 'observeIsReadOnly', - ); }); this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (context) => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/copy/block-list-to-block-copy-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/copy/block-list-to-block-copy-translator.test.ts index bc71ec139f9c..e96719af56e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/copy/block-list-to-block-copy-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/copy/block-list-to-block-copy-translator.test.ts @@ -25,6 +25,7 @@ describe('UmbBlockListToBlockClipboardCopyPropertyValueTranslator', () => { alias: 'headline', editorAlias: 'Umbraco.TextBox', value: 'Headline value', + entityType: '', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.test.ts index 56fb74df4221..dab83b27b64a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/clipboard/block/paste/block-to-block-list-paste-translator.test.ts @@ -25,6 +25,7 @@ describe('UmbBlockToBlockListClipboardPastePropertyValueTranslator', () => { alias: 'headline', editorAlias: 'Umbraco.TextBox', value: 'Headline value', + entityType: '', }, ], }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts index 49ef9333a094..04a1b836f2b1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/components/block-list-entry/block-list-entry.element.ts @@ -231,7 +231,7 @@ export class UmbBlockListEntryElement extends UmbLitElement implements UmbProper null, ); this.observe( - this.#context.readOnlyState.isReadOnly, + this.#context.readOnlyGuard.permitted, (isReadOnly) => (this._isReadOnly = isReadOnly), 'umbReadonlyObserver', ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index b00c145c9e32..47203d13c093 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -120,16 +120,17 @@ export class UmbPropertyEditorUIBlockListElement /** * Sets the input to readonly mode, meaning value cannot be changed but still able to read and select its content. * @type {boolean} - * @default + * @default false */ - @property({ type: Boolean, reflect: true }) public set readonly(value) { this.#readonly = value; if (this.#readonly) { this.#sorter.disable(); + this.#managerContext.readOnlyState.fallbackToPermitted(); } else { this.#sorter.enable(); + this.#managerContext.readOnlyState.fallbackToDisallowed(); } } public get readonly() { @@ -351,28 +352,6 @@ export class UmbPropertyEditorUIBlockListElement }, 'motherObserver', ); - - // If the current property is readonly all inner block content should also be readonly. - this.observe( - observeMultiple([context.isReadOnly, context.variantId]), - ([isReadOnly, variantId]) => { - const unique = 'UMB_PROPERTY_EDITOR_UI'; - if (variantId === undefined) return; - - if (isReadOnly) { - const state = { - unique, - variantId, - message: '', - }; - - this.#managerContext.readOnlyState.addState(state); - } else { - this.#managerContext.readOnlyState.removeState(unique); - } - }, - 'observeIsReadOnly', - ); } protected override getFormElement() { diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts index 7bea42e67b3e..701e79d448c2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-entry.context.ts @@ -17,7 +17,7 @@ import { mergeObservables, observeMultiple, } from '@umbraco-cms/backoffice/observable-api'; -import { encodeFilePath, UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils'; +import { encodeFilePath, UmbReadonlyVariantGuardManager } from '@umbraco-cms/backoffice/utils'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; @@ -65,7 +65,7 @@ export abstract class UmbBlockEntryContext< #hasExpose = new UmbBooleanState(undefined); readonly hasExpose = this.#hasExpose.asObservable(); - public readonly readOnlyState = new UmbReadOnlyVariantStateManager(this); + public readonly readOnlyGuard = new UmbReadonlyVariantGuardManager(this); // Workspace alike methods, to enables editing of data without the need of a workspace (Custom views and block grid inline editing mode for example). getEntityType() { @@ -573,22 +573,23 @@ export abstract class UmbBlockEntryContext< #observeReadOnlyState() { if (!this._manager) return; + // TODO: Here is a potential future issue. This is parsing on the read only state of the variant that this is opened from, that is problematic when we enable switching variant within a Block. [NL] + // TODO: This could benefit from a more dynamic approach, where we inherit all non-variant and variant scoped states. [NL] this.observe( - observeMultiple([this._manager.readOnlyState.isReadOnly, this._manager.variantId]), - ([isReadOnly, variantId]) => { + // TODO: Instead transfer all variant states. + this._manager.readOnlyState.permittedForVariantObservable(this._variantId), + (isReadOnly) => { const unique = 'UMB_BLOCK_MANAGER_CONTEXT'; - if (variantId === undefined) return; if (isReadOnly) { - const state = { + const rule = { unique, - variantId, - message: '', + variantId: this.#variantId.getValue(), }; - this.readOnlyState?.addState(state); + this.readOnlyGuard?.addRule(rule); } else { - this.readOnlyState?.removeState(unique); + this.readOnlyGuard?.removeRule(unique); } }, 'observeIsReadOnly', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts index 4bc15730a530..b9e82d5e880a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/context/block-manager.context.ts @@ -17,7 +17,7 @@ import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbBlockTypeBaseModel } from '@umbraco-cms/backoffice/block-type'; -import { UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils'; +import { UmbReadonlyVariantGuardManager } from '@umbraco-cms/backoffice/utils'; import { UmbPropertyValuePresetVariantBuilderController, type UmbPropertyTypePresetModel, @@ -77,7 +77,8 @@ export abstract class UmbBlockManagerContext< readonly #settings = new UmbArrayState(>[], (x) => x.key); public readonly settings = this.#settings.asObservable(); - public readonly readOnlyState = new UmbReadOnlyVariantStateManager(this); + // TODO: This is a bad seperation of concerns, this should be self initializing, not defined from the outside. [NL] + public readonly readOnlyState = new UmbReadonlyVariantGuardManager(this); readonly #exposes = new UmbArrayState( >[], diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts index 028a3ed887cc..dca8328fa2ca 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-element-manager.ts @@ -15,9 +15,10 @@ import { UmbDocumentTypeDetailRepository } from '@umbraco-cms/backoffice/documen import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbValidationController } from '@umbraco-cms/backoffice/validation'; import { UmbElementWorkspaceDataManager, type UmbElementPropertyDataOwner } from '@umbraco-cms/backoffice/content'; -import { UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils'; +import { UmbReadonlyVariantGuardManager } from '@umbraco-cms/backoffice/utils'; import { UmbDataTypeItemRepositoryManager } from '@umbraco-cms/backoffice/data-type'; +import { UmbVariantPropertyGuardManager } from '@umbraco-cms/backoffice/property'; export class UmbBlockElementManager extends UmbControllerBase @@ -33,7 +34,8 @@ export class UmbBlockElementManager void; - public readonly readOnlyState = new UmbReadOnlyVariantStateManager(this); + // TODO: who is controlling this? We need to be aware about seperation of concerns. [NL] + public readonly readonlyGuard = new UmbReadonlyVariantGuardManager(this); #variantId = new UmbClassState(undefined); readonly variantId = this.#variantId.asObservable(); @@ -56,6 +58,10 @@ export class UmbBlockElementManager, dataPathPropertyName: string) { @@ -65,6 +71,9 @@ export class UmbBlockElementManager this.structure.loadType(id)); this.observe(this.unique, (key) => { if (key) { @@ -99,6 +108,12 @@ export class UmbBlockElementManager { this.observe( - workspace.readOnlyState.states, - (states) => { - const isReadOnly = states.some((state) => state.variantId.equal(elementManager.getVariantId())); + workspace.readOnlyGuard.permittedForVariant(elementManager.getVariantId()), + (isReadOnly) => { this._readOnly.setValue(isReadOnly); }, 'umbObserveReadOnlyStates', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts index f3ffe0b53153..fd0df55fb7c9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/block-workspace.context.ts @@ -17,7 +17,7 @@ import { } from '@umbraco-cms/backoffice/observable-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UMB_DISCARD_CHANGES_MODAL, UMB_MODAL_CONTEXT, UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; -import { decodeFilePath, UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils'; +import { decodeFilePath, UmbReadonlyVariantGuardManager } from '@umbraco-cms/backoffice/utils'; import { UMB_BLOCK_ENTRIES_CONTEXT, UMB_BLOCK_MANAGER_CONTEXT, @@ -74,7 +74,7 @@ export class UmbBlockWorkspaceContext(undefined); readonly exposed = this.#exposed.asObservable(); - public readonly readOnlyState = new UmbReadOnlyVariantStateManager(this); + public readonly readOnlyGuard = new UmbReadonlyVariantGuardManager(this); constructor(host: UmbControllerHost, workspaceArgs: { manifest: ManifestWorkspace }) { super(host, workspaceArgs.manifest.alias); @@ -188,21 +188,20 @@ export class UmbBlockWorkspaceContext { + // TODO: Again we need to parse on all variants.... + manager.readOnlyState.permittedForVariantObservable(this.variantId), + (isReadOnly) => { const unique = 'UMB_BLOCK_MANAGER_CONTEXT'; - if (variantId === undefined) return; if (isReadOnly) { - const state = { + const rule = { unique, - variantId, - message: '', + variantId: this.#variantId.getValue(), }; - this.readOnlyState?.addState(state); + this.readOnlyGuard?.addRule(rule); } else { - this.readOnlyState?.removeState(unique); + this.readOnlyGuard?.removeRule(unique); } }, 'observeIsReadOnly', diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-properties.element.ts index 1b1acc7425cf..50e0e73e4807 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-properties.element.ts @@ -1,18 +1,22 @@ import type { UmbBlockWorkspaceElementManagerNames } from '../../block-workspace.context.js'; import { UMB_BLOCK_WORKSPACE_CONTEXT } from '../../block-workspace.context-token.js'; -import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbContentTypeModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type'; -import { UmbLitElement, umbDestroyOnDisconnect } from '@umbraco-cms/backoffice/lit-element'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; -import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; + +import './block-workspace-view-edit-property.element.js'; +import type UmbBlockElementManager from '../../block-element-manager.js'; @customElement('umb-block-workspace-view-edit-properties') export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { #managerName?: UmbBlockWorkspaceElementManagerNames; - #blockWorkspace?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; + #workspaceContext?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; #propertyStructureHelper = new UmbContentTypePropertyStructureHelper(this); + #properties?: Array; + #visiblePropertiesUniques: Array = []; @property({ attribute: false }) public get managerName(): UmbBlockWorkspaceElementManagerNames | undefined { @@ -32,27 +36,28 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { } @state() - _propertyStructure: Array = []; + _dataOwner?: UmbBlockElementManager; @state() - _dataPaths?: Array; + _variantId?: UmbVariantId; @state() - private _ownerEntityType?: string; + _visibleProperties?: Array; - #variantId?: UmbVariantId; + @state() + private _ownerEntityType?: string; constructor() { super(); this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, (workspaceContext) => { - this.#blockWorkspace = workspaceContext; - this._ownerEntityType = this.#blockWorkspace.getEntityType(); + this.#workspaceContext = workspaceContext; + this._ownerEntityType = this.#workspaceContext.getEntityType(); this.observe( workspaceContext.variantId, (variantId) => { - this.#variantId = variantId; - this.#generatePropertyDataPath(); + this._variantId = variantId; + this.#processPropertyStructure(); }, 'observeVariantId', ); @@ -61,49 +66,66 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { } #setStructureManager() { - if (!this.#blockWorkspace || !this.#managerName) return; - this.#propertyStructureHelper.setStructureManager(this.#blockWorkspace[this.#managerName].structure); + if (!this.#workspaceContext || !this.#managerName) return; + + this._dataOwner = this.#workspaceContext[this.#managerName]; + const structureManager = this._dataOwner.structure; + + this.#propertyStructureHelper.setStructureManager(structureManager); this.observe( this.#propertyStructureHelper.propertyStructure, - (propertyStructure) => { - this._propertyStructure = propertyStructure; - this.#generatePropertyDataPath(); + (properties) => { + this.#properties = properties; + this.#processPropertyStructure(); }, 'observePropertyStructure', ); } - /* - #generatePropertyDataPath() { - if (!this._propertyStructure) return; - this._dataPaths = this._propertyStructure.map((property) => `$.${property.alias}`); + #processPropertyStructure() { + if (!this._dataOwner || !this.#properties || !this.#propertyStructureHelper) { + return; + } + + const propertyViewGuard = this._dataOwner.propertyViewGuard; + + this.#properties.forEach((property) => { + const propertyVariantId = new UmbVariantId(this._variantId?.culture, this._variantId?.segment); + this.observe(propertyViewGuard.permittedForVariantAndProperty(propertyVariantId, property), (permitted) => { + if (permitted) { + this.#visiblePropertiesUniques.push(property.unique); + this.#calculateVisaibleProperties(); + } else { + const index = this.#visiblePropertiesUniques.indexOf(property.unique); + if (index !== -1) { + this.#visiblePropertiesUniques.splice(index, 1); + this.#calculateVisaibleProperties(); + } + } + }); + }); } - */ - - #generatePropertyDataPath() { - if (!this.#variantId || !this._propertyStructure) return; - this._dataPaths = this._propertyStructure.map( - (property) => - `$.values[${UmbDataPathPropertyValueQuery({ - alias: property.alias, - culture: property.variesByCulture ? this.#variantId!.culture : null, - segment: property.variesBySegment ? this.#variantId!.segment : null, - })}].value`, + + #calculateVisaibleProperties() { + this._visibleProperties = this.#properties!.filter((property) => + this.#visiblePropertiesUniques.includes(property.unique), ); } override render() { - return repeat( - this._propertyStructure, - (property) => property.alias, - (property, index) => - html``, - ); + return this._variantId && this._visibleProperties + ? repeat( + this._visibleProperties, + (property) => property.alias, + (property) => + html``, + ) + : nothing; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-property.element.ts new file mode 100644 index 000000000000..d8e92ad1af77 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block/workspace/views/edit/block-workspace-view-edit-property.element.ts @@ -0,0 +1,71 @@ +import { html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import type UmbBlockElementManager from '../../block-element-manager'; + +@customElement('umb-block-workspace-view-edit-property') +export class UmbBlockWorkspaceViewEditPropertyElement extends UmbLitElement { + // + @property({ attribute: false }) + variantId?: UmbVariantId; + + @property({ attribute: false }) + property?: UmbPropertyTypeModel; + + @state() + _dataPath?: string; + + @state() + _writeable?: boolean; + + @property({ attribute: false }) + ownerContext?: UmbBlockElementManager; + + override willUpdate(changedProperties: Map) { + super.willUpdate(changedProperties); + if (changedProperties.has('type') || changedProperties.has('variantId') || changedProperties.has('ownerContext')) { + if (this.variantId && this.property && this.ownerContext) { + const propertyVariantId = new UmbVariantId( + this.property.variesByCulture ? this.variantId.culture : null, + this.property.variesBySegment ? this.variantId.segment : null, + ); + this._dataPath = `$.values[${UmbDataPathPropertyValueQuery({ + alias: this.property.alias, + culture: propertyVariantId.culture, + segment: propertyVariantId.segment, + })}].value`; + + this.observe( + observeMultiple([ + this.ownerContext.propertyReadonlyGuard.permittedForVariantAndProperty(propertyVariantId, this.property), + this.ownerContext.propertyWriteGuard.permittedForVariantAndProperty(propertyVariantId, this.property), + ]), + ([readonly, write]) => { + this._writeable = !readonly && write; + }, + 'observeView', + ); + } + } + } + + override render() { + if (!this._dataPath || this._writeable === undefined) return nothing; + + return html``; + } +} + +export default UmbBlockWorkspaceViewEditPropertyElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-block-workspace-view-edit-property': UmbBlockWorkspaceViewEditPropertyElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/types.ts index 857c3b0705de..8622e3fd9d0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/types.ts @@ -46,7 +46,14 @@ export interface UmbPropertyTypeScaffoldModel extends Omit + .validation=${this._property.validation} + ?readonly=${this.readonly}> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/controller/merge-content-variant-data.controller.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/controller/merge-content-variant-data.controller.test.ts index d2ee6658955f..53008678a729 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/controller/merge-content-variant-data.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/controller/merge-content-variant-data.controller.test.ts @@ -74,6 +74,7 @@ describe('UmbMergeContentVariantDataController', () => { alias: 'test', culture: null, segment: null, + entityType: '', value: { nestedValue: { editorAlias: 'some-editor', @@ -94,6 +95,7 @@ describe('UmbMergeContentVariantDataController', () => { alias: 'test', culture: null, segment: null, + entityType: '', value: { nestedValue: { editorAlias: 'some-editor', @@ -123,6 +125,7 @@ describe('UmbMergeContentVariantDataController', () => { alias: 'test', culture: null, segment: null, + entityType: '', value: { nestedValue: { editorAlias: 'some-editor', @@ -143,6 +146,7 @@ describe('UmbMergeContentVariantDataController', () => { alias: 'test', culture: null, segment: null, + entityType: '', value: { nestedValue: { editorAlias: 'some-editor', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/element-property-data-owner.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/element-property-data-owner.interface.ts index 142f7172de6a..ada7c9017184 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/element-property-data-owner.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/element-property-data-owner.interface.ts @@ -4,7 +4,7 @@ import type { UmbContentTypeModel, UmbContentTypeStructureManager } from '@umbra import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; -import type { UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils'; +import type { UmbReadonlyVariantGuardManager } from '@umbraco-cms/backoffice/utils'; /** * The data supplier for a Element Property Dataset @@ -21,7 +21,7 @@ export interface UmbElementPropertyDataOwner< getValues(): ContentModel['values'] | undefined; isLoaded(): Promise | undefined; - readonly readOnlyState: UmbReadOnlyVariantStateManager; + readonly readonlyGuard: UmbReadonlyVariantGuardManager; // Same as from UmbVariantDatasetWorkspaceContext, could be refactored later [NL] propertyValueByAlias( diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/element-property-dataset.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/element-property-dataset.context.ts index d40ba486d903..faab1dbc8989 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/element-property-dataset.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/property-dataset-context/element-property-dataset.context.ts @@ -70,9 +70,8 @@ export abstract class UmbElementPropertyDatasetContext< }); this.observe( - this._dataOwner.readOnlyState.states, - (states) => { - const isReadOnly = states.some((state) => state.variantId.equal(this.#variantId)); + this._dataOwner.readonlyGuard.permittedForVariant(this.#variantId), + (isReadOnly) => { this._readOnly.setValue(isReadOnly); }, null, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/types.ts index 4cded9d86cb2..0a3e945fca9f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/types.ts @@ -8,8 +8,9 @@ export interface UmbElementDetailModel { } export interface UmbElementValueModel extends UmbPropertyValueData { - editorAlias: string; culture: string | null; + editorAlias: string; + entityType: string; segment: string | null; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-validation-path-translator.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-validation-path-translator.test.ts index 6e88784da4ad..f2df7ff95b28 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-validation-path-translator.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-validation-path-translator.test.ts @@ -33,6 +33,7 @@ describe('UmbValidationPropertyPathTranslationController', () => { value: 'value1', culture: null, segment: null, + entityType: 'document-property-value', }, ], variants: [], diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts index f33ea7e40921..13fd0e30bbb2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-detail-workspace-base.ts @@ -25,7 +25,7 @@ import { type UmbEntityVariantModel, type UmbEntityVariantOptionModel, } from '@umbraco-cms/backoffice/variant'; -import { UmbDeprecation, UmbReadOnlyVariantStateManager } from '@umbraco-cms/backoffice/utils'; +import { UmbDeprecation, UmbReadonlyVariantGuardManager } from '@umbraco-cms/backoffice/utils'; import { UmbDataTypeDetailRepository, UmbDataTypeItemRepositoryManager } from '@umbraco-cms/backoffice/data-type'; import { appendToFrozenArray, mergeObservables, UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; @@ -49,6 +49,7 @@ import { import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api'; import { UmbPropertyValuePresetVariantBuilderController, + UmbVariantPropertyGuardManager, type UmbPropertyTypePresetModel, type UmbPropertyTypePresetModelTypeModel, } from '@umbraco-cms/backoffice/property'; @@ -100,7 +101,11 @@ export abstract class UmbContentDetailWorkspaceContextBase< { public readonly IS_CONTENT_WORKSPACE_CONTEXT = true as const; - public readonly readOnlyState = new UmbReadOnlyVariantStateManager(this); + public readonly readonlyGuard = new UmbReadonlyVariantGuardManager(this); + + public readonly propertyViewGuard = new UmbVariantPropertyGuardManager(this); + public readonly propertyWriteGuard = new UmbVariantPropertyGuardManager(this); + public readonly propertyReadonlyGuard = new UmbVariantPropertyGuardManager(this); /* Content Data */ protected override readonly _data = new UmbContentWorkspaceDataManager(this); @@ -138,6 +143,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< /** * @private * @description - Should not be used by external code. + * @internal */ public readonly languages = this.#languages.asObservable(); @@ -171,6 +177,9 @@ export abstract class UmbContentDetailWorkspaceContextBase< ) { super(host, args); + this.propertyViewGuard.fallbackToPermitted(); + this.propertyWriteGuard.fallbackToPermitted(); + this.#serverValidation.addPathTranslator(UmbContentDetailValidationPathTranslator); this._data.setVariantScaffold(args.contentVariantScaffold); @@ -524,7 +533,15 @@ export abstract class UmbContentDetailWorkspaceContextBase< } // Notice the order of the properties is important for our JSON String Compare function. [NL] - const entry = { editorAlias, ...variantId.toObject(), alias, value } as UmbElementValueModel; + const entry: UmbElementValueModel = { + editorAlias, + // Be aware that this solution is a bit magical, and based on a naming convention. + // We might want to make this more flexible at some point and get the entityType from somewhere instead of constructing it here. + entityType: `${this.getEntityType()}-property-value`, + ...variantId.toObject(), + alias, + value, + }; const currentData = this.getData(); if (currentData) { @@ -573,10 +590,12 @@ export abstract class UmbContentDetailWorkspaceContextBase< const changedVariantIds = this._data.getChangedVariants(); const selectedVariantIds = activeVariantIds.concat(changedVariantIds); + const writableSelectedVariantIds = selectedVariantIds.filter( + (x) => this.readonlyGuard.getPermittedForVariant(x) === false, + ); + // Selected can contain entries that are not part of the options, therefor the modal filters selection based on options. - const readOnlyCultures = this.readOnlyState.getStates().map((s) => s.variantId.culture); - let selected = selectedVariantIds.map((x) => x.toString()).filter((v, i, a) => a.indexOf(v) === i); - selected = selected.filter((x) => readOnlyCultures.includes(x) === false); + let selected = writableSelectedVariantIds.map((x) => x.toString()).filter((v, i, a) => a.indexOf(v) === i); return { options, @@ -585,8 +604,7 @@ export abstract class UmbContentDetailWorkspaceContextBase< } protected _saveableVariantsFilter = (option: VariantOptionModelType) => { - const readOnlyCultures = this.readOnlyState.getStates().map((s) => s.variantId.culture); - return readOnlyCultures.includes(option.culture) === false; + return this.readonlyGuard.getPermittedForVariant(UmbVariantId.Create(option)) === false; }; /* validation */ @@ -862,7 +880,13 @@ export abstract class UmbContentDetailWorkspaceContextBase< override resetState() { super.resetState(); - this.readOnlyState.clear(); + this.readonlyGuard.clear(); + this.propertyViewGuard.clear(); + this.propertyWriteGuard.clear(); + this.propertyReadonlyGuard.clear(); + // default: + this.propertyViewGuard.fallbackToPermitted(); + this.propertyWriteGuard.fallbackToPermitted(); } abstract getContentTypeUnique(): string | undefined; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-workspace-context.interface.ts index ba1b17c59c58..7cda46922dee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/content-workspace-context.interface.ts @@ -8,6 +8,7 @@ import type { UmbRoutableWorkspaceContext, UmbVariantDatasetWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; +import type { UmbVariantPropertyGuardManager } from '@umbraco-cms/backoffice/property'; export interface UmbContentWorkspaceContext< ContentModel extends UmbContentDetailModel = UmbContentDetailModel, @@ -27,6 +28,10 @@ export interface UmbContentWorkspaceContext< isLoaded(): Promise | undefined; variantById(variantId: UmbVariantId): Observable; + readonly propertyViewGuard: UmbVariantPropertyGuardManager; + readonly propertyWriteGuard: UmbVariantPropertyGuardManager; + readonly propertyReadonlyGuard: UmbVariantPropertyGuardManager; + //initiatePropertyValueChange(): void; //finishPropertyValueChange(): void; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-properties.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-properties.element.ts index 139982020193..79d8f56dc679 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-properties.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-properties.element.ts @@ -1,4 +1,4 @@ -import { css, html, customElement, property, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { css, html, customElement, property, state, repeat, nothing } from '@umbraco-cms/backoffice/external/lit'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import type { UmbContentTypeModel, @@ -7,13 +7,18 @@ import type { } from '@umbraco-cms/backoffice/content-type'; import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/content-type'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation'; -import { UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/workspace'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import './content-editor-property.element.js'; +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; @customElement('umb-content-workspace-view-edit-properties') export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement { + #workspaceContext?: typeof UMB_CONTENT_WORKSPACE_CONTEXT.TYPE; + #propertyStructureHelper = new UmbContentTypePropertyStructureHelper(this); + #properties?: Array; + #visiblePropertiesUniques: Array = []; + @property({ type: String, attribute: 'container-id', reflect: false }) public get containerId(): string | null | undefined { return this.#propertyStructureHelper.getContainerId(); @@ -22,62 +27,85 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement this.#propertyStructureHelper.setContainerId(value); } - #propertyStructureHelper = new UmbContentTypePropertyStructureHelper(this); - #variantId?: UmbVariantId; - @state() - _propertyStructure?: Array; + _variantId?: UmbVariantId; @state() - _dataPaths?: Array; + _visibleProperties?: Array; constructor() { super(); - this.consumeContext(UMB_PROPERTY_STRUCTURE_WORKSPACE_CONTEXT, (workspaceContext) => { + this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => { + this._variantId = datasetContext.getVariantId(); + this.#processPropertyStructure(); + }); + + this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (workspaceContext) => { + this.#workspaceContext = workspaceContext; + this.#propertyStructureHelper.setStructureManager( // Assuming its the same content model type that we are working with here... [NL] workspaceContext.structure as unknown as UmbContentTypeStructureManager, ); + + this.observe( + this.#propertyStructureHelper.propertyStructure, + (properties) => { + this.#properties = properties; + this.#processPropertyStructure(); + }, + 'observePropertyStructure', + ); }); - this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => { - this.#variantId = datasetContext.getVariantId(); - this.#generatePropertyDataPath(); + } + + #processPropertyStructure() { + if (!this.#workspaceContext || !this.#properties || !this.#propertyStructureHelper) { + return; + } + + const propertyViewGuard = this.#workspaceContext.propertyViewGuard; + + this.#properties.forEach((property) => { + const propertyVariantId = new UmbVariantId(this._variantId?.culture, this._variantId?.segment); + this.observe( + propertyViewGuard.permittedForVariantAndProperty(propertyVariantId, property), + (permitted) => { + if (permitted) { + this.#visiblePropertiesUniques.push(property.unique); + this.#calculateVisaibleProperties(); + } else { + const index = this.#visiblePropertiesUniques.indexOf(property.unique); + if (index !== -1) { + this.#visiblePropertiesUniques.splice(index, 1); + this.#calculateVisaibleProperties(); + } + } + }, + `propertyViewGuard-permittedForVariantAndProperty-${property.unique}`, + ); }); - this.observe( - this.#propertyStructureHelper.propertyStructure, - (propertyStructure) => { - this._propertyStructure = propertyStructure; - this.#generatePropertyDataPath(); - }, - null, - ); } - #generatePropertyDataPath() { - if (!this.#variantId || !this._propertyStructure) return; - this._dataPaths = this._propertyStructure.map( - (property) => - `$.values[${UmbDataPathPropertyValueQuery({ - alias: property.alias, - culture: property.variesByCulture ? this.#variantId!.culture : null, - segment: property.variesBySegment ? this.#variantId!.segment : null, - })}].value`, + #calculateVisaibleProperties() { + this._visibleProperties = this.#properties!.filter((property) => + this.#visiblePropertiesUniques.includes(property.unique), ); } override render() { - return this._propertyStructure && this._dataPaths + return this._variantId && this._visibleProperties ? repeat( - this._propertyStructure, + this._visibleProperties, (property) => property.alias, - (property, index) => - html` + html` `, + .variantId=${this._variantId} + .property=${property}>`, ) - : ''; + : nothing; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-property.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-property.element.ts new file mode 100644 index 000000000000..d54220e6e7cf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/workspace/views/edit/content-editor-property.element.ts @@ -0,0 +1,79 @@ +import { html, customElement, property, state, nothing } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; + +@customElement('umb-content-workspace-view-edit-property') +export class UmbContentWorkspaceViewEditPropertyElement extends UmbLitElement { + // + @property({ attribute: false }) + variantId?: UmbVariantId; + + @property({ attribute: false }) + property?: UmbPropertyTypeModel; + + @state() + _dataPath?: string; + + @state() + _writeable?: boolean; + + @state() + _context?: typeof UMB_CONTENT_WORKSPACE_CONTEXT.TYPE; + + constructor() { + super(); + + this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, (context) => { + this._context = context; + }); + } + + override willUpdate(changedProperties: Map) { + super.willUpdate(changedProperties); + if (changedProperties.has('type') || changedProperties.has('variantId') || changedProperties.has('_context')) { + if (this.variantId && this.property && this._context) { + const propertyVariantId = new UmbVariantId( + this.property.variesByCulture ? this.variantId.culture : null, + this.property.variesBySegment ? this.variantId.segment : null, + ); + this._dataPath = `$.values[${UmbDataPathPropertyValueQuery({ + alias: this.property.alias, + culture: propertyVariantId.culture, + segment: propertyVariantId.segment, + })}].value`; + + this.observe( + observeMultiple([ + this._context.propertyReadonlyGuard.permittedForVariantAndProperty(propertyVariantId, this.property), + this._context.propertyWriteGuard.permittedForVariantAndProperty(propertyVariantId, this.property), + ]), + ([readonly, write]) => { + this._writeable = !readonly && write; + }, + 'observeView', + ); + } + } + } + + override render() { + if (!this._dataPath || this._writeable === undefined) return nothing; + + return html``; + } +} + +export default UmbContentWorkspaceViewEditPropertyElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-content-workspace-view-edit-property': UmbContentWorkspaceViewEditPropertyElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts index c992d6388f2e..804ef8093476 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/models/index.ts @@ -19,6 +19,10 @@ export interface UmbReferenceByUnique { unique: string; } +export interface UmbReferenceByAlias { + alias: string; +} + export interface UmbReferenceByUniqueAndType { type: string; unique: string; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor-ui-element.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor-ui-element.interface.ts index 58163db2d927..234a1f99ccee 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor-ui-element.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property-editor/extensions/property-editor-ui-element.interface.ts @@ -4,6 +4,7 @@ export interface UmbPropertyEditorUiElement extends HTMLElement { name?: string; value?: unknown; config?: UmbPropertyEditorConfigCollection; + readonly?: boolean; mandatory?: boolean; mandatoryMessage?: string; destroy?: () => void; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts index 1a2608931b6d..b82d952f0e71 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts @@ -4,7 +4,6 @@ import { UmbContextBase } from '@umbraco-cms/backoffice/class-api'; import { UmbArrayState, UmbBasicState, - UmbBooleanState, UmbClassState, UmbDeepState, UmbObjectState, @@ -22,6 +21,7 @@ import type { UmbPropertyTypeAppearanceModel, UmbPropertyTypeValidationModel, } from '@umbraco-cms/backoffice/content-type'; +import { UmbReadOnlyStateManager } from '@umbraco-cms/backoffice/utils'; export class UmbPropertyContext extends UmbContextBase> { #alias = new UmbStringState(undefined); @@ -60,8 +60,8 @@ export class UmbPropertyContext extends UmbContextBase(undefined); public readonly editorManifest = this.#editorManifest.asObservable(); - #isReadOnly = new UmbBooleanState(false); - public readonly isReadOnly = this.#isReadOnly.asObservable(); + public readonly readonlyState = new UmbReadOnlyStateManager(this); + public readonly isReadOnly = this.readonlyState.isOn; /** * Set the property editor UI element for this property. @@ -160,7 +160,15 @@ export class UmbPropertyContext extends UmbContextBase { - this.#isReadOnly.setValue(value); + const unique = 'UMB_DATASET'; + + if (value) { + this.readonlyState.addState({ + unique, + }); + } else { + this.readonlyState.removeState(unique); + } }); } @@ -334,7 +342,7 @@ export class UmbPropertyContext extends UmbContextBase extends UmbContextBase { - this._isReadOnly = value; - this._element?.toggleAttribute('readonly', value); + this._isReadonly = value; + if (this._element) { + this._element.readonly = value; + this._element.toggleAttribute('readonly', value); + } }, null, ); @@ -285,7 +312,7 @@ export class UmbPropertyElement extends UmbLitElement { } const el = await createExtensionElement(manifest); - this._supportsReadOnly = manifest.meta.supportsReadOnly || false; + this._supportsReadonly = manifest.meta.supportsReadOnly || false; if (el) { const oldElement = this._element; @@ -355,7 +382,9 @@ export class UmbPropertyElement extends UmbLitElement { } } - this._element.toggleAttribute('readonly', this._isReadOnly); + this._element.readonly = this._isReadonly; + this._element.toggleAttribute('readonly', this._isReadonly); + this.#createController(manifest); } @@ -412,7 +441,7 @@ export class UmbPropertyElement extends UmbLitElement { #renderPropertyEditor() { return html`
- ${this._isReadOnly && this._supportsReadOnly === false ? html`
` : nothing} + ${this._isReadonly && this._supportsReadonly === false ? html`
` : nothing} ${this._element}
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/index.ts index e5d236cee9be..dc09da0afb75 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/property/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/index.ts @@ -1,6 +1,8 @@ export * from './components/index.js'; export * from './conditions/index.js'; export * from './property-dataset/index.js'; +export * from './property-guard-manager/index.js'; export * from './property-value-cloner/property-value-clone.controller.js'; export * from './property-value-preset/index.js'; + export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/index.ts new file mode 100644 index 000000000000..483a62696ab1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/index.ts @@ -0,0 +1,2 @@ +export * from './property-guard.manager.js'; +export * from './variant-property-guard.manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/property-guard.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/property-guard.manager.test.ts new file mode 100644 index 000000000000..7692f9c8e006 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/property-guard.manager.test.ts @@ -0,0 +1,144 @@ +import { expect } from '@open-wc/testing'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import { UmbPropertyGuardManager } from './property-guard.manager.js'; + +@customElement('test-my-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +describe('UmbPropertyGuardManager', () => { + let manager: UmbPropertyGuardManager; + const propA = { unique: 'propA' }; + const propB = { unique: 'propB' }; + const rulePropA = { unique: '1', message: 'State 1', permitted: true, propertyType: propA }; + const rulePropB = { unique: '2', message: 'State 2', permitted: true, propertyType: propB }; + const rulePlain = { unique: '3', message: 'State 3', permitted: true }; + const ruleNoPropA = { unique: '01', message: 'State 01', permitted: false, propertyType: propA }; + const ruleNoPropB = { unique: '02', message: 'State 02', permitted: false, propertyType: propB }; + const ruleNoPlain = { unique: '03', message: 'State 03', permitted: false }; + + beforeEach(() => { + const hostElement = new UmbTestControllerHostElement(); + manager = new UmbPropertyGuardManager(hostElement); + }); + + describe('propertyTypes based states', () => { + it('works with propertyTypes in the state data.', () => { + manager.addRule(rulePropA); + manager.addRule(rulePropB); + expect(manager.getRules()[0].propertyType?.unique).to.be.equal(propA.unique); + expect(manager.getRules()[1].propertyType?.unique).to.be.equal(propB.unique); + }); + + it('is not on for a variant when no states', (done) => { + manager + .isPermittedForProperty(propA) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is on for present variant', (done) => { + manager.addRule(rulePropB); + + manager + .isPermittedForProperty(propB) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('is not on for incompatible variant', (done) => { + manager.addRule(rulePropA); + + manager + .isPermittedForProperty(propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is on by generic state', (done) => { + manager.addRule(rulePlain); + + manager + .isPermittedForProperty(propB) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('is not on when specific state states false', (done) => { + manager.addRule(rulePlain); + manager.addRule(ruleNoPropB); + + manager + .isPermittedForProperty(propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is not on when generic state states false', (done) => { + manager.addRule(ruleNoPlain); + + manager + .isPermittedForProperty(propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is not on when specific state states true', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(rulePropB); + + manager + .isPermittedForProperty(propB) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('a negative specific state wins', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(rulePropB); + manager.addRule(ruleNoPropB); + + manager + .isPermittedForProperty(propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('a negative general state wins', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(rulePlain); + + manager + .isPermittedForProperty(propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/property-guard.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/property-guard.manager.ts new file mode 100644 index 000000000000..f1169458282f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/property-guard.manager.ts @@ -0,0 +1,34 @@ +import { type Observable } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import { UmbGuardManagerBase, type UmbGuardRule } from '@umbraco-cms/backoffice/utils'; + +export interface UmbPropertyGuardRule extends UmbGuardRule { + propertyType?: UmbReferenceByUnique; +} + +function ComparePropertyRefWithStates(rules: UmbPropertyGuardRule[], propertyType: UmbReferenceByUnique) { + // any specific states for the propertyType? + const propertyState = rules.find((s) => s.propertyType?.unique === propertyType.unique); + if (propertyState) { + return propertyState.permitted; + } + + // any state without variant: + const nonVariantState = rules.find((s) => s.propertyType === undefined); + if (nonVariantState) { + return nonVariantState.permitted; + } + + return false; +} + +export class UmbPropertyGuardManager extends UmbGuardManagerBase { + // + isPermittedForProperty(propertyType: UmbReferenceByUnique): Observable { + return this._rules.asObservablePart((rules) => ComparePropertyRefWithStates(rules, propertyType)); + } + + getPermittedForVariant(propertyType: UmbReferenceByUnique): boolean { + return ComparePropertyRefWithStates(this.getRules(), propertyType); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.test.ts new file mode 100644 index 000000000000..c077610a77f1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.test.ts @@ -0,0 +1,245 @@ +import { expect } from '@open-wc/testing'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import { UmbVariantPropertyGuardManager } from './variant-property-guard.manager.js'; +import { UmbVariantId } from '../../variant/variant-id.class.js'; + +@customElement('test-my-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +describe('UmbVariantPropertyGuardManager', () => { + let manager: UmbVariantPropertyGuardManager; + + const invariantVariant = UmbVariantId.CreateInvariant(); + const englishVariant = UmbVariantId.Create({ culture: 'en', segment: null }); + const propA = { unique: 'propA' }; + const propB = { unique: 'propB' }; + + const ruleInv = { unique: '1', message: 'State 1', permitted: true, variantId: invariantVariant }; + const ruleEn = { unique: '2', message: 'State 2', permitted: true, variantId: englishVariant }; + const rulePlain = { unique: '3', message: 'State 3', permitted: true }; + const ruleNoInv = { unique: '01', message: 'State 01', permitted: false, variantId: invariantVariant }; + const ruleNoEn = { unique: '02', message: 'State 02', permitted: false, variantId: englishVariant }; + const ruleNoPlain = { unique: '03', message: 'State 03', permitted: false }; + + const statePropAInv = { + unique: 'a1', + message: 'State 1', + permitted: true, + variantId: invariantVariant, + propertyType: propA, + }; + const statePropBEn = { + unique: 'b2', + message: 'State 2', + permitted: true, + variantId: englishVariant, + propertyType: propB, + }; + const statePropAPlain = { unique: 'a3', message: 'State 3', permitted: true, propertyType: propA }; + const stateNoPropAInv = { + unique: 'a01', + message: 'State 01', + permitted: false, + variantId: invariantVariant, + propertyType: propA, + }; + const stateNoPropBEn = { + unique: 'b02', + message: 'State 02', + permitted: false, + variantId: englishVariant, + propertyType: propB, + }; + const stateNoPropAPlain = { unique: 'a03', message: 'State 03', permitted: false, propertyType: propA }; + + beforeEach(() => { + const hostElement = new UmbTestControllerHostElement(); + manager = new UmbVariantPropertyGuardManager(hostElement); + }); + + describe('VariantIds based states', () => { + it('works with variantIds class instances in the state data.', () => { + manager.addRule(ruleInv); + manager.addRule(ruleEn); + expect(manager.getRules()[0].variantId?.compare(invariantVariant)).to.be.true; + expect(manager.getRules()[0].variantId?.compare(englishVariant)).to.be.false; + expect(manager.getRules()[1].variantId?.compare(englishVariant)).to.be.true; + expect(manager.getRules()[1].variantId?.compare(invariantVariant)).to.be.false; + }); + + it('is not on for a variant when no states', (done) => { + manager + .permittedForVariantAndProperty(invariantVariant, propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is on for present variant', (done) => { + manager.addRule(ruleEn); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('is not on for incompatible variant', (done) => { + manager.addRule(ruleInv); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is not on for incompatible variant and incompatible property', (done) => { + manager.addRule(statePropAInv); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + it('is not on for compatible variant with incompatible property', (done) => { + manager.addRule(statePropAInv); + + manager + .permittedForVariantAndProperty(invariantVariant, propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is not on for incompatible variant with compatible property', (done) => { + manager.addRule(statePropAInv); + + manager + .permittedForVariantAndProperty(englishVariant, propA) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is on by generic state', (done) => { + manager.addRule(rulePlain); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('is not on when specific variant states false', (done) => { + manager.addRule(rulePlain); + manager.addRule(ruleNoEn); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is not on when generic variant states false', (done) => { + manager.addRule(ruleNoPlain); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is not on when specific state states true', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(ruleEn); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('a negative specific state wins', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(ruleEn); + manager.addRule(ruleNoEn); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('a negative general state wins', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(rulePlain); + + manager + .permittedForVariantAndProperty(englishVariant, propB) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('a specific state wins over general states', (done) => { + manager.addRule(stateNoPropAPlain); + manager.addRule(statePropAPlain); + manager.addRule(statePropAInv); + + manager + .permittedForVariantAndProperty(invariantVariant, propA) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('a specific negative state wins over general states', (done) => { + manager.addRule(stateNoPropAPlain); + manager.addRule(statePropAPlain); + manager.addRule(stateNoPropAInv); + + manager + .permittedForVariantAndProperty(invariantVariant, propA) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.ts new file mode 100644 index 000000000000..ef37ab810934 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-guard-manager/variant-property-guard.manager.ts @@ -0,0 +1,69 @@ +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { type Observable } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import type { UmbPropertyGuardRule } from './property-guard.manager'; +import { UmbGuardManagerBase } from '../../utils/guard-manager'; + +export interface UmbVariantPropertyGuardRule extends UmbPropertyGuardRule { + variantId?: UmbVariantId; +} + +function CompareVariantAndPropertyWithStates( + rules: UmbVariantPropertyGuardRule[], + variantId: UmbVariantId, + propertyType: UmbReferenceByUnique, +) { + // any specific states for the variant and propertyType? + const variantAndPropertyState = rules.find( + (s) => s.variantId?.compare(variantId) && s.propertyType?.unique === propertyType.unique, + ); + if (variantAndPropertyState) { + return variantAndPropertyState.permitted; + } + + // any specific states for the propertyType? + const propertyState = rules.find((s) => s.variantId === undefined && s.propertyType?.unique === propertyType.unique); + if (propertyState) { + return propertyState.permitted; + } + + // any specific states for the variant? + const variantState = rules.find((s) => s.variantId?.compare(variantId) && s.propertyType === undefined); + if (variantState) { + return variantState.permitted; + } + + // any state without variant: + const nonVariantState = rules.find((s) => s.variantId === undefined && s.propertyType === undefined); + if (nonVariantState) { + return nonVariantState.permitted; + } + + return false; +} + +export class UmbVariantPropertyGuardManager extends UmbGuardManagerBase { + // + permittedForVariantAndProperty(variantId: UmbVariantId, propertyType: UmbReferenceByUnique): Observable { + return this._rules.asObservablePart((rules) => CompareVariantAndPropertyWithStates(rules, variantId, propertyType)); + } + + /* + isPermittedForVariantObservableAndProperty( + variantId: Observable, + propertyType: UmbReferenceByUnique, + ): Observable { + return mergeObservables([this._rulesObservable, variantId], ([states, variantId]) => { + if (!variantId) { + // Or should we know about the fallback state here? [NL] + return false; + } + return CompareVariantAndPropertyWithStates(states, variantId, propertyType); + }); + } + */ + + getPermittedForVariant(variantId: UmbVariantId, propertyType: UmbReferenceByUnique): boolean { + return CompareVariantAndPropertyWithStates(this._rules.getValue(), variantId, propertyType); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/data-mapper.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/data-mapper.ts index 88f0fbd27450..2022d024df03 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/data-mapper.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/data-mapper.ts @@ -13,21 +13,33 @@ export class UmbDataSourceDataMapper) { if (!args.forDataSource) { - throw new Error('data source identifier is required'); + const message = 'data source identifier is required'; + console.error(message); + throw new Error(message); } - if (!args.forDataModel) { - throw new Error('data identifier is required'); + if (!args.data) { + const message = 'data is required'; + console.error(message); + throw new Error(message); } - if (!args.data) { - throw new Error('data is required'); + if (!args.forDataModel && !args.fallback) { + const message = 'forDataModel is missing and no fallback provided.'; + console.error(message); + throw new Error(message); + } + + if (!args.forDataModel && args.fallback) { + return args.fallback(args.data); } const dataMapping = await this.#dataMappingResolver.resolve(args.forDataSource, args.forDataModel); if (!dataMapping && !args.fallback) { - throw new Error('Data mapping not found and no fallback provided.'); + const message = 'Data mapping not found and no fallback provided.'; + console.error(message); + throw new Error(message); } if (!dataMapping && args.fallback) { @@ -35,7 +47,9 @@ export class UmbDataSourceDataMapper { + let manager: UmbGuardManagerBase; + const rule1: UmbGuardIncomingRuleBase = { unique: '1', message: 'Rule 1', permitted: true }; + const rule2: UmbGuardIncomingRuleBase = { unique: '2', message: 'Rule 2', permitted: true }; + const ruleFalse: UmbGuardIncomingRuleBase = { unique: '-1', message: 'Rule -1', permitted: false }; + + beforeEach(() => { + const hostElement = new UmbTestControllerHostElement(); + manager = new UmbGuardManagerBase(hostElement); + }); + + describe('Public API', () => { + describe('methods', () => { + it('has a fallbackToDisallowed method', () => { + expect(manager).to.have.property('fallbackToDisallowed').that.is.a('function'); + }); + + it('has a fallbackToPermitted method', () => { + expect(manager).to.have.property('fallbackToPermitted').that.is.a('function'); + }); + + it('has a addRule method', () => { + expect(manager).to.have.property('addRule').that.is.a('function'); + }); + + it('has a addRules method', () => { + expect(manager).to.have.property('addRules').that.is.a('function'); + }); + + it('has a removeRule method', () => { + expect(manager).to.have.property('removeRule').that.is.a('function'); + }); + + it('has a clear method', () => { + expect(manager).to.have.property('clear').that.is.a('function'); + }); + }); + }); + + describe('Add Rule', () => { + it('adds a single state to the states array', () => { + manager.addRule(rule1); + expect(manager.getRules()).to.deep.equal([rule1]); + }); + + it('adding a rule without permitted defined will default to true', () => { + manager.addRule({ ...rule1, permitted: undefined }); + expect(manager.getRules()).to.deep.equal([rule1]); + }); + + it('adds multiple states to the states array', () => { + manager.addRules([rule1, rule2]); + expect(manager.getRules()).to.deep.equal([rule1, rule2]); + }); + + it('updates the observable', (done) => { + manager.addRule(rule1); + + manager.rules + .subscribe((value) => { + expect(value[0]).to.deep.equal(rule1); + done(); + }) + .unsubscribe(); + }); + + it('sort negative states first', () => { + manager.addRules([rule1, ruleFalse, rule2]); + expect(manager.getRules()).to.deep.equal([ruleFalse, rule1, rule2]); + }); + + it('sort default states last', () => { + manager.fallbackToDisallowed(); + manager.addRules([rule1, ruleFalse, rule2]); + const rules = manager.getRules(); + expect(rules[0]).to.deep.equal(ruleFalse); + expect(rules[1]).to.deep.equal(rule1); + expect(rules[2]).to.deep.equal(rule2); + expect(rules[3].permitted).to.be.false; + + manager.fallbackToPermitted(); + const rules2 = manager.getRules(); + expect(rules2[0]).to.deep.equal(ruleFalse); + expect(rules2[1]).to.deep.equal(rule1); + expect(rules2[2]).to.deep.equal(rule2); + expect(rules2[3].permitted).to.be.true; + }); + }); + + describe('Remove State', () => { + beforeEach(() => { + manager.addRules([rule1, rule2]); + }); + + it('removes a single state from the states array', () => { + manager.removeRule('1'); + expect(manager.getRules()).to.deep.equal([rule2]); + }); + + it('removes multiple states from the states array', () => { + manager.removeRules(['1', '2']); + expect(manager.getRules()).to.deep.equal([]); + }); + + it('updates the observable', (done) => { + manager.removeRule('1'); + + manager.rules + .subscribe((value) => { + expect(value).to.deep.equal([rule2]); + done(); + }) + .unsubscribe(); + }); + }); + + describe('Get States', () => { + it('returns all states', () => { + manager.addRules([rule1, rule2]); + expect(manager.getRules()).to.deep.equal([rule1, rule2]); + }); + }); + + describe('Clear', () => { + beforeEach(() => { + manager.addRules([rule1, rule2]); + }); + + it('clears all states', () => { + manager.clear(); + expect(manager.getRules()).to.deep.equal([]); + }); + + it('updates the observable', (done) => { + manager.clear(); + + manager.rules + .subscribe((value) => { + expect(value).to.deep.equal([]); + done(); + }) + .unsubscribe(); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/guard.manager.base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/guard.manager.base.ts new file mode 100644 index 000000000000..c5606e5e36ed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/guard.manager.base.ts @@ -0,0 +1,100 @@ +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbPartialSome } from '../type'; + +export interface UmbGuardIncomingRuleBase { + unique?: string | symbol; + permitted?: boolean; + message?: string; +} + +export interface UmbGuardRule extends UmbGuardIncomingRuleBase { + unique: string | symbol; + permitted: boolean; +} + +const DefaultRuleUnique = Symbol(); + +export class UmbGuardManagerBase< + RuleType extends UmbGuardRule = UmbGuardRule, + IncomingRuleType extends UmbGuardIncomingRuleBase = UmbPartialSome, +> extends UmbControllerBase { + // + protected readonly _rules = new UmbArrayState([], (x) => x.unique).sortBy((a, b) => { + // Ensure DefaultRuleUnique always comes last: + if (a.unique === DefaultRuleUnique) return 1; + if (b.unique === DefaultRuleUnique) return -1; + // Otherwise disallowed first and permitted last: + return a.permitted === b.permitted ? 0 : a.permitted ? 1 : -1; + }); + public readonly rules = this._rules.asObservable(); + public readonly hasRules = this._rules.asObservablePart((x) => x.length > 0); + + public fallbackToDisallowed() { + this._rules.appendOne({ unique: DefaultRuleUnique, permitted: false } as RuleType); + } + + public fallbackToPermitted() { + this._rules.appendOne({ unique: DefaultRuleUnique, permitted: true } as RuleType); + } + + /** + * Add a new rule + * @param {RuleType} rule + */ + addRule(rule: IncomingRuleType) { + const newRule = { ...rule } as unknown as RuleType; + newRule.unique ??= Symbol(); + if (newRule.permitted === undefined) { + newRule.permitted = true; + } + this._rules.appendOne(newRule); + return rule.unique; + } + + /** + * Add multiple rules + * @param {RuleType[]} rules + */ + addRules(rules: IncomingRuleType[]) { + this._rules.mute(); + rules.forEach((rule) => this.addRule(rule)); + this._rules.unmute(); + } + + /** + * Remove a rule + * @param {RuleType['unique']} unique Unique value of the rule to remove + */ + removeRule(unique: RuleType['unique']) { + this._rules.removeOne(unique); + } + + /** + * Remove multiple rules + * @param {RuleType['unique'][]} uniques Array of unique values to remove + */ + removeRules(uniques: RuleType['unique'][]) { + this._rules.remove(uniques); + } + + /** + * Get all rules + * @returns {RuleType[]} Array of rules + */ + getRules(): RuleType[] { + return this._rules.getValue(); + } + + /** + * Clear all rules + */ + clear(): void { + this._rules.setValue([]); + } + + override destroy() { + this._rules.destroy(); + super.destroy(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/index.ts new file mode 100644 index 000000000000..edc5d7292b31 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/index.ts @@ -0,0 +1,3 @@ +export * from './guard.manager.base.js'; +export * from './readonly-guard.manager.js'; +export * from './readonly-variant-guard.manager.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-guard.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-guard.manager.ts new file mode 100644 index 000000000000..0d085c236f2e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-guard.manager.ts @@ -0,0 +1,21 @@ +import { UmbGuardManagerBase, type UmbGuardRule } from './guard.manager.base.js'; + +function CompareRules(rules: Array): boolean { + const firstState = rules[0]; + if (firstState) { + return firstState.permitted; + } + + return false; +} + +// TODO: Check the need for this one. +export class UmbReadonlyGuardManager extends UmbGuardManagerBase { + public readonly permitted = this._rules.asObservablePart((rules) => { + return CompareRules(rules); + }); + + getPermitted(): boolean { + return CompareRules(this.getRules()); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-variant-guard.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-variant-guard.manager.test.ts new file mode 100644 index 000000000000..b9b1a00f410c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-variant-guard.manager.test.ts @@ -0,0 +1,147 @@ +import { expect } from '@open-wc/testing'; +import { customElement } from '@umbraco-cms/backoffice/external/lit'; +import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api'; +import { UmbReadonlyVariantGuardManager } from './readonly-variant-guard.manager.js'; +import { UmbVariantId } from '../../variant/variant-id.class.js'; + +@customElement('test-my-controller-host') +class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} + +describe('UmbReadonlyVariantGuardManager', () => { + let manager: UmbReadonlyVariantGuardManager; + const invariantVariant = UmbVariantId.CreateInvariant(); + const englishVariant = UmbVariantId.Create({ culture: 'en', segment: null }); + const ruleInv = { unique: '1', message: 'State 1', permitted: true, variantId: invariantVariant }; + const ruleEn = { unique: '2', message: 'State 2', permitted: true, variantId: englishVariant }; + const rulePlain = { unique: '3', message: 'State 3', permitted: true }; + const ruleNoInv = { unique: '01', message: 'State 01', permitted: false, variantId: invariantVariant }; + const ruleNoEn = { unique: '02', message: 'State 02', permitted: false, variantId: englishVariant }; + const ruleNoPlain = { unique: '03', message: 'State 03', permitted: false }; + + beforeEach(() => { + const hostElement = new UmbTestControllerHostElement(); + manager = new UmbReadonlyVariantGuardManager(hostElement); + }); + + describe('VariantIds based rules', () => { + it('works with variantIds class instances in the rule data.', () => { + manager.addRule(ruleInv); + manager.addRule(ruleEn); + expect(manager.getRules()[0].variantId?.compare(invariantVariant)).to.be.true; + expect(manager.getRules()[0].variantId?.compare(englishVariant)).to.be.false; + expect(manager.getRules()[1].variantId?.compare(englishVariant)).to.be.true; + expect(manager.getRules()[1].variantId?.compare(invariantVariant)).to.be.false; + }); + + it('is not on for a variant when no rules', (done) => { + manager + .permittedForVariant(invariantVariant) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is on for present variant', (done) => { + manager.addRule(ruleEn); + + manager + .permittedForVariant(englishVariant) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('is not on for incompatible variant', (done) => { + manager.addRule(ruleInv); + + manager + .permittedForVariant(englishVariant) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is on by generic rule', (done) => { + manager.addRule(rulePlain); + + manager + .permittedForVariant(englishVariant) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('is not on when specific permitted does not permit', (done) => { + manager.addRule(rulePlain); + manager.addRule(ruleNoEn); + + manager + .permittedForVariant(englishVariant) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is not on when generic permitted does not permit', (done) => { + manager.addRule(ruleNoPlain); + + manager + .permittedForVariant(englishVariant) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('is not on when specific rule permits', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(ruleEn); + + manager + .permittedForVariant(englishVariant) + .subscribe((value) => { + expect(value).to.be.true; + done(); + }) + .unsubscribe(); + }); + + it('a negative specific rule wins', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(ruleEn); + manager.addRule(ruleNoEn); + + manager + .permittedForVariant(englishVariant) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + + it('a negative general rule wins', (done) => { + manager.addRule(ruleNoPlain); + manager.addRule(rulePlain); + + manager + .permittedForVariant(englishVariant) + .subscribe((value) => { + expect(value).to.be.false; + done(); + }) + .unsubscribe(); + }); + }); +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-variant-guard.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-variant-guard.manager.ts new file mode 100644 index 000000000000..d1b86b92dac2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/guard-manager/readonly-variant-guard.manager.ts @@ -0,0 +1,48 @@ +import { UmbReadonlyGuardManager } from './readonly-guard.manager.js'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { mergeObservables, type Observable } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbGuardRule } from './guard.manager.base.js'; + +export interface UmbVariantGuardRule extends UmbGuardRule { + variantId?: UmbVariantId; +} + +function CompareStateAndVariantId(rules: Array, variantId: UmbVariantId): boolean { + // any specific states for the variant? + const variantState = rules.find((s) => s.variantId?.compare(variantId)); + if (variantState) { + return variantState.permitted; + } + + // any state without variant: + const nonVariantState = rules.find((s) => s.variantId === undefined); + if (nonVariantState) { + return nonVariantState.permitted; + } + + return false; +} + +// TODO: Check the need for this one. +export class UmbReadonlyVariantGuardManager extends UmbReadonlyGuardManager { + // + permittedForVariant(variantId: UmbVariantId): Observable { + return this._rules.asObservablePart((states) => { + return CompareStateAndVariantId(states, variantId); + }); + } + + permittedForVariantObservable(variantId: Observable): Observable { + return mergeObservables([this.rules, variantId], ([states, variantId]) => { + if (!variantId) { + // Or should we know about the fallback state here? [NL] + return false; + } + return CompareStateAndVariantId(states, variantId); + }); + } + + getPermittedForVariant(variantId: UmbVariantId): boolean { + return CompareStateAndVariantId(this.getRules(), variantId); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts index e6aabbc8d175..eaf83a2c1e11 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/index.ts @@ -1,9 +1,11 @@ export * from './bytes/bytes.function.js'; export * from './debounce/debounce.function.js'; +export * from './deprecation/index.js'; export * from './direction/index.js'; export * from './download/blob-download.function.js'; export * from './get-guid-from-udi.function.js'; export * from './get-processed-image-url.function.js'; +export * from './guard-manager/index.js'; export * from './math/math.js'; export * from './media/image-size.function.js'; export * from './object/deep-merge.function.js'; @@ -25,5 +27,4 @@ export * from './sanitize/sanitize-html.function.js'; export * from './selection-manager/selection.manager.js'; export * from './state-manager/index.js'; export * from './string/index.js'; -export * from './deprecation/index.js'; export type * from './type/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.ts index 4c163593df02..a18357d4bb7d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.ts @@ -4,7 +4,7 @@ import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; export interface UmbState { unique: string; - message: string; + message?: string; } export class UmbStateManager extends UmbControllerBase { @@ -64,7 +64,7 @@ export class UmbStateManager extends UmbC * @memberof UmbStateManager */ removeState(unique: StateType['unique']) { - this._states.setValue(this._states.getValue().filter((x) => x.unique !== unique)); + this._states.removeOne(unique); } /** @@ -73,7 +73,7 @@ export class UmbStateManager extends UmbC * @memberof UmbStateManager */ removeStates(uniques: StateType['unique'][]) { - this._states.setValue(this._states.getValue().filter((x) => !uniques.includes(x.unique))); + this._states.remove(uniques); } /** @@ -85,6 +85,14 @@ export class UmbStateManager extends UmbC return this._states.getValue(); } + getIsOn(): boolean { + return this._states.getValue().length > 0; + } + + getIsOff(): boolean { + return this._states.getValue().length === 0; + } + /** * Clear all states from the state manager * @memberof UmbStateManager diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts index 5a6c69cbf2ec..d190db617d65 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/index.ts @@ -1,3 +1,4 @@ export * from './variant-id.class.js'; export * from './variant-object-compare.function.js'; + export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts index 016eb39d21f9..9bc28d1dd05c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view-variant-selector.element.ts @@ -9,7 +9,7 @@ import { UMB_PROPERTY_DATASET_CONTEXT, isNameablePropertyDatasetContext } from ' import { UUIInputEvent } from '@umbraco-cms/backoffice/external/uui'; import type { UmbContentWorkspaceContext } from '@umbraco-cms/backoffice/content'; import type { UmbEntityVariantModel, UmbEntityVariantOptionModel } from '@umbraco-cms/backoffice/variant'; -import type { UmbVariantState } from '@umbraco-cms/backoffice/utils'; +import type { UmbVariantGuardRule } from '@umbraco-cms/backoffice/utils'; import type { UUIInputElement, UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-workspace-split-view-variant-selector') @@ -24,7 +24,7 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< private _variantOptions: Array = []; @state() - private _readOnlyStates: Array = []; + private _readOnlyStates: Array = []; @state() _activeVariants: Array = []; @@ -66,7 +66,6 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< this.#observeVariants(workspaceContext); this.#observeActiveVariants(workspaceContext); - this.#observeReadOnlyStates(workspaceContext); this.#observeCurrentVariant(); }); @@ -82,23 +81,12 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< workspaceContext.variantOptions, (variantOptions) => { this._variantOptions = (variantOptions as Array).sort(this._variantSorter); - this.#setReadOnlyCultures(); + this.#setReadOnlyCultures(workspaceContext); }, '_observeVariantOptions', ); } - async #observeReadOnlyStates(workspaceContext: UmbContentWorkspaceContext) { - this.observe( - workspaceContext.readOnlyState.states, - (states) => { - this._readOnlyStates = states; - this.#setReadOnlyCultures(); - }, - 'umbObserveReadOnlyStates', - ); - } - async #observeActiveVariants(workspaceContext: UmbContentWorkspaceContext) { this.observe( workspaceContext.splitView.activeVariantsInfo, @@ -178,9 +166,9 @@ export class UmbWorkspaceSplitViewVariantSelectorElement< return this._variantOptions?.length > 1; } - #setReadOnlyCultures() { + #setReadOnlyCultures(workspaceContext: UmbContentWorkspaceContext) { this._readOnlyCultures = this._variantOptions - .filter((variant) => this._readOnlyStates.some((state) => state.variantId.compare(variant))) + .filter((variant) => workspaceContext.readonlyGuard.getPermittedForVariant(UmbVariantId.Create(variant))) .map((variant) => variant.culture); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/index.ts index 4c28e6c8da47..a5e20e96523c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/index.ts @@ -1,4 +1,5 @@ export * from './constants.js'; export * from './repository/index.js'; +export * from './entity.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts index b823c9bfe3e6..4301d43f9fad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-blueprints/repository/detail/document-blueprint-detail.server.data-source.ts @@ -9,6 +9,7 @@ import type { import { DocumentBlueprintService } from '@umbraco-cms/backoffice/external/backend-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE } from '@umbraco-cms/backoffice/document'; /** * A data source for the Document that fetches data from the server @@ -93,6 +94,7 @@ export class UmbDocumentBlueprintServerDataSource implements UmbDetailDataSource values: data.values.map((value) => { return { editorAlias: value.editorAlias, + entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, culture: value.culture || null, segment: value.segment || null, alias: value.alias, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/index.ts index 42ee1a2779d0..429c4a3bfa08 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/index.ts @@ -5,4 +5,5 @@ export * from './constants.js'; export * from './modals/index.js'; export * from './repository/index.js'; export * from './workspace/index.js'; + export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.server.data-source.ts index 77f3ede54807..4d14c6ef6ba1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/repository/detail/document-type-detail.server.data-source.ts @@ -98,6 +98,7 @@ export class UmbDocumentTypeDetailServerDataSource implements UmbDetailDataSourc properties: data.properties.map((property) => { return { id: property.id, + unique: property.id, container: property.container, sortOrder: property.sortOrder, alias: property.alias, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/block/document-block.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/block/document-block.workspace-context.ts new file mode 100644 index 000000000000..239fe02ea61d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/block/document-block.workspace-context.ts @@ -0,0 +1,29 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UMB_BLOCK_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/block'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../workspace/constants.js'; + +/** + * Document Block Workspace Context + * Extension to configure the workspace context for a block in a document. + * @export + * @class UmbDocumentBlockWorkspaceContext + * @extends {UmbControllerBase} + */ +export class UmbDocumentBlockWorkspaceContext extends UmbControllerBase { + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, async (context) => { + // TODO: revisit this when getContext supports passContextAliasMatches + const consumer = await this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, () => {}).passContextAliasMatches(); + const documentWorkspaceContext = consumer.asPromise().catch(() => { + throw new Error('Could not find document workspace context'); + }); + + // TODO: Revist this code, is there a need to hook in here to set fallbackToDisallowed? [NL] + }); + } +} + +export { UmbDocumentBlockWorkspaceContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/block/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/block/manifests.ts new file mode 100644 index 000000000000..1aa08184dfc9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/block/manifests.ts @@ -0,0 +1,18 @@ +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_BLOCK_WORKSPACE_ALIAS } from '@umbraco-cms/backoffice/block'; + +export const manifests: Array = [ + { + type: 'workspaceContext', + name: 'Document Block Workspace Context', + alias: 'Umb.WorkspaceContext.Block.Document', + api: () => import('./document-block.workspace-context.js'), + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_BLOCK_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create-blueprint/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create-blueprint/manifests.ts index 12c47060b23c..80e2fcddc45d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create-blueprint/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create-blueprint/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT } from '../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT } from '../../user-permissions/document/constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; export const manifests: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts index 121cf7a77c92..1cdd48b96d9b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/create/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_ROOT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_CREATE } from '../../user-permissions/index.js'; +import { UMB_USER_PERMISSION_DOCUMENT_CREATE } from '../../user-permissions/document/constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; export const manifests: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/manifests.ts index 1e5c788b21b7..f62616ada41b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/culture-and-hostnames/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES } from '../../user-permissions/index.js'; +import { UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES } from '../../user-permissions/document/constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; export const manifests: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/manifests.ts index 019e1ed39e23..a435844c3013 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/duplicate/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_DUPLICATE } from '../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_DUPLICATE } from '../../user-permissions/document/constants.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as modalManifests } from './modal/manifests.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts index ecbb1a7e5c24..2959afa4655f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/manifests.ts @@ -1,7 +1,7 @@ import { UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS } from '../repository/index.js'; import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../item/constants.js'; import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_DELETE } from '../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_DELETE } from '../user-permissions/document/constants.js'; import { UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS } from '../reference/constants.js'; import { manifests as createBlueprintManifests } from './create-blueprint/manifests.js'; import { manifests as createManifests } from './create/manifests.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/move-to/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/move-to/manifests.ts index 422421304cde..8b01534fd594 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/move-to/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/move-to/manifests.ts @@ -1,6 +1,6 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; import { UMB_DOCUMENT_TREE_ALIAS, UMB_DOCUMENT_TREE_REPOSITORY_ALIAS } from '../../tree/index.js'; -import { UMB_USER_PERMISSION_DOCUMENT_MOVE } from '../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_MOVE } from '../../user-permissions/document/constants.js'; import { UMB_MOVE_DOCUMENT_REPOSITORY_ALIAS } from './repository/index.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/notifications/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/notifications/manifests.ts index b45851616ca3..9c4cb83b5d61 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/notifications/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/notifications/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS } from '../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS } from '../../user-permissions/document/constants.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as modalManifests } from './modal/manifests.js'; import type { ManifestEntityAction } from '@umbraco-cms/backoffice/entity-action'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/public-access/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/public-access/manifests.ts index 354dbf2f57d5..124335ac9abd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/public-access/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/public-access/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS } from '../../user-permissions/index.js'; +import { UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS } from '../../user-permissions/document/constants.js'; import { UMB_SECTION_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/section'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/sort-children-of/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/sort-children-of/manifests.ts index 39f00ca0f828..2eb5289fc5e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/sort-children-of/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/sort-children-of/manifests.ts @@ -1,7 +1,7 @@ import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_ROOT_ENTITY_TYPE } from '../../entity.js'; import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../item/constants.js'; import { UMB_DOCUMENT_TREE_REPOSITORY_ALIAS } from '../../tree/index.js'; -import { UMB_USER_PERMISSION_DOCUMENT_SORT } from '../../user-permissions/index.js'; +import { UMB_USER_PERMISSION_DOCUMENT_SORT } from '../../user-permissions/document/constants.js'; import { UMB_SORT_CHILDREN_OF_DOCUMENT_REPOSITORY_ALIAS } from './repository/constants.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/duplicate-to/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/duplicate-to/manifests.ts index 6acd5a69c6f0..cd6babebd346 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/duplicate-to/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/duplicate-to/manifests.ts @@ -1,7 +1,7 @@ import { UMB_DOCUMENT_COLLECTION_ALIAS } from '../../collection/constants.js'; import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; import { UMB_DOCUMENT_TREE_ALIAS } from '../../tree/manifests.js'; -import { UMB_USER_PERMISSION_DOCUMENT_DUPLICATE } from '../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_DUPLICATE } from '../../user-permissions/document/constants.js'; import { UMB_BULK_DUPLICATE_DOCUMENT_REPOSITORY_ALIAS } from './repository/constants.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/move-to/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/move-to/manifests.ts index 57e351c8f578..835beba3486d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/move-to/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/move-to/manifests.ts @@ -1,7 +1,7 @@ import { UMB_DOCUMENT_COLLECTION_ALIAS } from '../../collection/constants.js'; import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; import { UMB_DOCUMENT_TREE_ALIAS } from '../../tree/manifests.js'; -import { UMB_USER_PERMISSION_DOCUMENT_MOVE } from '../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_MOVE } from '../../user-permissions/document/constants.js'; import { UMB_BULK_MOVE_DOCUMENT_REPOSITORY_ALIAS } from './repository/constants.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity.ts index 13602ef85d70..e805d28f3f8e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity.ts @@ -5,3 +5,7 @@ export type UmbDocumentEntityType = typeof UMB_DOCUMENT_ENTITY_TYPE; export type UmbDocumentRootEntityType = typeof UMB_DOCUMENT_ROOT_ENTITY_TYPE; export type UmbDocumentEntityTypeUnion = UmbDocumentEntityType | UmbDocumentRootEntityType; + +// TODO: move this to a better location inside the document module +export const UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE = `${UMB_DOCUMENT_ENTITY_TYPE}-property-value`; +export type UmbDocumentPropertyValueEntityType = typeof UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-configuration.context.ts index 8829599ee039..6f4454f64f30 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/global-contexts/document-configuration.context.ts @@ -8,7 +8,7 @@ import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; // TODO: Turn this into a Repository with a Store that holds the cache [NL] /** * A context for fetching and caching the document configuration. - * @deprecated Do not use this one, it will have ot change in near future. + * @internal Do not use this one, it is only for internal usage. */ export class UmbDocumentConfigurationContext extends UmbContextBase @@ -46,7 +46,7 @@ export class UmbDocumentConfigurationContext export default UmbDocumentConfigurationContext; /** - * @deprecated Do not use this one, it will have ot change in near future. + * @internal Do not use this one, it is only for internal usage. */ export const UMB_DOCUMENT_CONFIGURATION_CONTEXT = new UmbContextToken( 'UmbDocumentConfigurationContext', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts index a14bf00b99e1..317a4ffda72f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/index.ts @@ -4,6 +4,7 @@ export * from './audit-log/index.js'; export * from './components/index.js'; export * from './constants.js'; export * from './entity-actions/index.js'; +export * from './entity.js'; export * from './global-contexts/index.js'; export * from './item/index.js'; export * from './modals/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts index c7910bc1b5c3..0817b8ab977d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/manifests.ts @@ -1,4 +1,5 @@ import { manifests as auditLogManifests } from './audit-log/manifests.js'; +import { manifests as blockManifests } from './block/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as entityActionManifests } from './entity-actions/manifests.js'; import { manifests as entityBulkActionManifests } from './entity-bulk-actions/manifests.js'; @@ -23,6 +24,7 @@ import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension export const manifests: Array = [ ...auditLogManifests, + ...blockManifests, ...collectionManifests, ...entityActionManifests, ...entityBulkActionManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/pending-changes/document-published-pending-changes.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/pending-changes/document-published-pending-changes.manager.test.ts index a3257a21e856..7df1d7e55692 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/pending-changes/document-published-pending-changes.manager.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/pending-changes/document-published-pending-changes.manager.test.ts @@ -5,7 +5,7 @@ import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controlle import { UmbDocumentPublishedPendingChangesManager } from './document-published-pending-changes.manager.js'; import { DocumentVariantStateModel } from '@umbraco-cms/backoffice/external/backend-api'; import { type UmbDocumentDetailModel } from '../../types.js'; -import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; +import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE } from '../../entity.js'; @customElement('test-my-controller-host') class UmbTestControllerHostElement extends UmbControllerHostElementMixin(HTMLElement) {} @@ -77,6 +77,7 @@ describe('UmbSelectionManager', () => { values: [ { editorAlias: 'Umbraco.TextBox', + entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, alias: 'prop1', culture: null, segment: null, @@ -158,6 +159,7 @@ describe('UmbSelectionManager', () => { values: [ { editorAlias: 'Umbraco.TextBox', + entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, alias: 'prop1', culture: 'en-US', segment: null, @@ -165,6 +167,7 @@ describe('UmbSelectionManager', () => { }, { editorAlias: 'Umbraco.TextBox', + entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, alias: 'prop1', culture: 'da-DK', segment: null, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/workspace-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/workspace-action/manifests.ts index 75854fc50e22..d66d02909ef0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/workspace-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/workspace-action/manifests.ts @@ -2,7 +2,7 @@ import { UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, UMB_USER_PERMISSION_DOCUMENT_PUBLISH, UMB_USER_PERMISSION_DOCUMENT_UPDATE, -} from '../../../user-permissions/constants.js'; +} from '../../../user-permissions/document/constants.js'; import { UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/manifests.ts index f501f4a7f9a5..7fd3ec4b660d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-action/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_PUBLISH } from '../../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_PUBLISH } from '../../../user-permissions/document/constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; export const manifests: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/manifests.ts index 9b50bc1ed379..017228b8c5ed 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/entity-bulk-action/manifests.ts @@ -1,6 +1,6 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../../entity.js'; import { UMB_DOCUMENT_COLLECTION_ALIAS } from '../../../collection/constants.js'; -import { UMB_USER_PERMISSION_DOCUMENT_PUBLISH } from '../../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_PUBLISH } from '../../../user-permissions/document/constants.js'; import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; export const manifests: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/workspace-action/save-and-publish.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/workspace-action/save-and-publish.action.ts index 0aa52c0b8b56..9e6dfadf5025 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/workspace-action/save-and-publish.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish/workspace-action/save-and-publish.action.ts @@ -1,8 +1,8 @@ -import { UmbDocumentUserPermissionCondition } from '../../../user-permissions/conditions/document-user-permission.condition.js'; +import { UmbDocumentUserPermissionCondition } from '../../../user-permissions/document/conditions/document-user-permission.condition.js'; import { UMB_USER_PERMISSION_DOCUMENT_PUBLISH, UMB_USER_PERMISSION_DOCUMENT_UPDATE, -} from '../../../user-permissions/constants.js'; +} from '../../../user-permissions/document/constants.js'; import { UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT } from '../../workspace-context/constants.js'; import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../../constants.js'; import { UmbWorkspaceActionBase, type UmbWorkspaceActionArgs } from '@umbraco-cms/backoffice/workspace'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts index 36ab0bad3c1d..20c24f8584d8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/repository/document-publishing.server.data-source.ts @@ -1,5 +1,5 @@ import type { UmbDocumentDetailModel, UmbDocumentVariantPublishModel } from '../../types.js'; -import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; +import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE } from '../../entity.js'; import type { CultureAndScheduleRequestModel, PublishDocumentRequestModel, @@ -162,6 +162,7 @@ export class UmbDocumentPublishingServerDataSource { values: data.values.map((value) => { return { editorAlias: value.editorAlias, + entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, culture: value.culture || null, segment: value.segment || null, alias: value.alias, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/manifests.ts index 436dff320a34..b89880060d80 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-action/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH } from '../../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH } from '../../../user-permissions/document/constants.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; export const manifests: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/manifests.ts index e473c143633d..0bd4d3396cd4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/entity-bulk-action/manifests.ts @@ -1,6 +1,6 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../../../entity.js'; import { UMB_DOCUMENT_COLLECTION_ALIAS } from '../../../collection/constants.js'; -import { UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH } from '../../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH } from '../../../user-permissions/document/constants.js'; import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; export const manifests: Array = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/workspace-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/workspace-action/manifests.ts index 432a1eb33483..f67a92aebf42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/workspace-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/unpublish/workspace-action/manifests.ts @@ -1,7 +1,7 @@ import { UMB_DOCUMENT_USER_PERMISSION_CONDITION_ALIAS, UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, -} from '../../../user-permissions/constants.js'; +} from '../../../user-permissions/document/constants.js'; import { UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts index fee74f7c7436..4a4a49b8ff6d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/workspace-context/document-publishing.workspace-context.ts @@ -370,9 +370,7 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase { - const readOnlyCultures = - this.#documentWorkspaceContext?.readOnlyState.getStates().map((s) => s.variantId.culture) ?? []; - return readOnlyCultures.includes(option.culture) === false; + return this.#documentWorkspaceContext!.readonlyGuard.getPermittedForVariant(UmbVariantId.Create(option)); }; async #determineVariantOptions(): Promise<{ @@ -394,8 +392,9 @@ export class UmbDocumentPublishingWorkspaceContext extends UmbContextBase s.variantId.culture); - selected = selected.filter((x) => readOnlyCultures.includes(x) === false); + selected = selected.filter( + (x) => this.#documentWorkspaceContext!.readonlyGuard.getPermittedForVariant(new UmbVariantId(x)) === false, + ); return { options, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/manifests.ts index 2ac56f94f52d..4ef514fef747 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/manifests.ts @@ -1,4 +1,4 @@ -import { UMB_USER_PERMISSION_DOCUMENT_DELETE } from '../../../user-permissions/constants.js'; +import { UMB_USER_PERMISSION_DOCUMENT_DELETE } from '../../../user-permissions/document/constants.js'; import { UMB_DOCUMENT_ENTITY_TYPE } from '../../../entity.js'; import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../../item/constants.js'; import { UMB_DOCUMENT_RECYCLE_BIN_REPOSITORY_ALIAS } from '../../repository/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts index 1b96b586c547..2938f7709e9e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/repository/detail/document-detail.server.data-source.ts @@ -1,5 +1,5 @@ import type { UmbDocumentDetailModel } from '../../types.js'; -import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; +import { UMB_DOCUMENT_ENTITY_TYPE, UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE } from '../../entity.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository'; import type { @@ -94,6 +94,7 @@ export class UmbDocumentServerDataSource implements UmbDetailDataSource { return { editorAlias: value.editorAlias, + entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, culture: value.culture || null, segment: value.segment || null, alias: value.alias, diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/constants.ts index bb0fa17977ac..0fc894b76f39 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/constants.ts @@ -1,18 +1,2 @@ -export const UMB_USER_PERMISSION_DOCUMENT_CREATE = 'Umb.Document.Create'; -export const UMB_USER_PERMISSION_DOCUMENT_READ = 'Umb.Document.Read'; -export const UMB_USER_PERMISSION_DOCUMENT_UPDATE = 'Umb.Document.Update'; -export const UMB_USER_PERMISSION_DOCUMENT_DELETE = 'Umb.Document.Delete'; -export const UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT = 'Umb.Document.CreateBlueprint'; -export const UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS = 'Umb.Document.Notifications'; -export const UMB_USER_PERMISSION_DOCUMENT_PUBLISH = 'Umb.Document.Publish'; -export const UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS = 'Umb.Document.Permissions'; -export const UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH = 'Umb.Document.Unpublish'; -export const UMB_USER_PERMISSION_DOCUMENT_DUPLICATE = 'Umb.Document.Duplicate'; -export const UMB_USER_PERMISSION_DOCUMENT_MOVE = 'Umb.Document.Move'; -export const UMB_USER_PERMISSION_DOCUMENT_SORT = 'Umb.Document.Sort'; -export const UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES = 'Umb.Document.CultureAndHostnames'; -export const UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS = 'Umb.Document.PublicAccess'; -export const UMB_USER_PERMISSION_DOCUMENT_ROLLBACK = 'Umb.Document.Rollback'; - -export * from './conditions/constants.js'; -export * from './repository/constants.js'; +export * from './document/constants.js'; +export * from './document-property-value/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/constants.ts new file mode 100644 index 000000000000..bff1ca601d35 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/constants.ts @@ -0,0 +1,2 @@ +export const UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_CONDITION_ALIAS = + 'Umb.Condition.UserPermission.Document.PropertyValue'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/document-property-value-user-permission.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/document-property-value-user-permission.condition.ts new file mode 100644 index 000000000000..2332d0d03bff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/document-property-value-user-permission.condition.ts @@ -0,0 +1,105 @@ +import type { UmbDocumentPropertyValueUserPermissionModel } from '../types.js'; +import type { UmbDocumentPropertyValueUserPermissionConditionConfig } from './types.js'; +import { isDocumentPropertyValueUserPermission } from './utils.js'; +import { UMB_CURRENT_USER_CONTEXT } from '@umbraco-cms/backoffice/current-user'; +import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +// Do not export - for internal use only +type UmbOnChangeCallbackType = (permitted: boolean) => void; + +export class UmbDocumentPropertyValueUserPermissionCondition + extends UmbControllerBase + implements UmbExtensionCondition +{ + config: UmbDocumentPropertyValueUserPermissionConditionConfig; + permitted = false; + + #documentPropertyValuePermissions: Array = []; + #fallbackPermissions: string[] = []; + #onChange: UmbOnChangeCallbackType; + + constructor( + host: UmbControllerHost, + args: UmbConditionControllerArguments< + UmbDocumentPropertyValueUserPermissionConditionConfig, + UmbOnChangeCallbackType + >, + ) { + super(host); + this.config = args.config; + this.#onChange = args.onChange; + + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => { + this.observe( + context.currentUser, + (currentUser) => { + this.#documentPropertyValuePermissions = + currentUser?.permissions?.filter(isDocumentPropertyValueUserPermission) || []; + this.#fallbackPermissions = currentUser?.fallbackPermissions || []; + this.#checkPermissions(); + }, + 'umbDocumentPropertyValueUserPermissionConditionObserver', + ); + }); + } + + #checkPermissions() { + const hasDocumentPropertyValuePermissions = this.#documentPropertyValuePermissions.length > 0; + + // if there is no permissions for any documents we use the fallback permissions + if (!hasDocumentPropertyValuePermissions) { + this.#check(this.#fallbackPermissions); + return; + } + + /* If there are document permission we check if there are permissions for the current document property value + If there aren't we use the fallback permissions */ + if (hasDocumentPropertyValuePermissions) { + const permissionsForCurrentDocumentPropertyValue = this.#documentPropertyValuePermissions.find( + (permission) => permission.propertyType.unique === this.config.match.propertyType.unique, + ); + + // no permissions for the current document property value - use the fallback permissions + if (!permissionsForCurrentDocumentPropertyValue) { + this.#check(this.#fallbackPermissions); + return; + } + + // we found permissions for the current document property value - check them + this.#check(permissionsForCurrentDocumentPropertyValue.verbs); + } + } + + #check(verbs: Array) { + /* we default to true se we don't require both allOf and oneOf to be defined + but they can be combined for more complex scenarios */ + let allOfPermitted = true; + let oneOfPermitted = true; + + // check if all of the verbs are present + if (this.config.allOf?.length) { + allOfPermitted = this.config.allOf.every((verb) => verbs.includes(verb)); + } + + // check if at least one of the verbs is present + if (this.config.oneOf?.length) { + oneOfPermitted = this.config.oneOf.some((verb) => verbs.includes(verb)); + } + + // if neither allOf or oneOf is defined we default to false + if (!allOfPermitted && !oneOfPermitted) { + allOfPermitted = false; + oneOfPermitted = false; + } + + const permitted = allOfPermitted && oneOfPermitted; + if (permitted === this.permitted) return; + + this.permitted = permitted; + this.#onChange(permitted); + } +} + +export { UmbDocumentPropertyValueUserPermissionCondition as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/manifests.ts new file mode 100644 index 000000000000..527dc8dcc200 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/manifests.ts @@ -0,0 +1,10 @@ +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_CONDITION_ALIAS } from './constants.js'; + +export const manifests: Array = [ + { + type: 'condition', + name: 'Document Property Value User Permission Condition', + alias: UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_CONDITION_ALIAS, + api: () => import('./document-property-value-user-permission.condition.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/types.ts new file mode 100644 index 000000000000..99e208ff01c5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/types.ts @@ -0,0 +1,30 @@ +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; + +export type UmbDocumentPropertyValueUserPermissionConditionConfig = + UmbConditionConfigBase<'Umb.Condition.UserPermission.Document.PropertyValue'> & { + /** + * The user must have all of the permissions in this array for the condition to be met. + * @example + * ["Umb.Document.PropertyValue.Read", "Umb.Document.PropertyValue.Write"] + */ + allOf?: Array; + + /** + * The user must have at least one of the permissions in this array for the condition to be met. + * @example + * ["Umb.Document.PropertyValue.Read", "Umb.Document.PropertyValue.Write"] + */ + oneOf?: Array; + + match: { + propertyType: { + unique: string; + }; + }; + }; + +declare global { + interface UmbExtensionConditionConfigMap { + umbDocumentPropertyValueUserPermissionConditionConfig: UmbDocumentPropertyValueUserPermissionConditionConfig; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/utils.ts new file mode 100644 index 000000000000..04e05d6156ef --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/conditions/utils.ts @@ -0,0 +1,16 @@ +import type { UmbDocumentPropertyValueUserPermissionModel } from '../types.js'; +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE } from '../user-permission.js'; + +/** + * + * @param permission + * @returns {boolean} True if the permission is a permission for document property values + */ +export function isDocumentPropertyValueUserPermission( + permission: unknown, +): permission is UmbDocumentPropertyValueUserPermissionModel { + return ( + (permission as UmbDocumentPropertyValueUserPermissionModel).userPermissionType === + UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE + ); +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/constants.ts new file mode 100644 index 000000000000..c6eda198592f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/constants.ts @@ -0,0 +1,6 @@ +export * from './conditions/constants.js'; +export * from './document-property-value-permission-flow-modal/constants.js'; +export { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE } from './user-permission.js'; + +export const UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_READ = 'Umb.Document.PropertyValue.Read'; +export const UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_WRITE = 'Umb.Document.PropertyValue.Write'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/from-server.management-api.mapping.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/from-server.management-api.mapping.ts new file mode 100644 index 000000000000..903cd8215f1f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/from-server.management-api.mapping.ts @@ -0,0 +1,32 @@ +import type { UmbDocumentPropertyValueUserPermissionModel } from '../types.js'; +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE } from '../user-permission.js'; +import type { UmbDataSourceDataMapping } from '@umbraco-cms/backoffice/repository'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { DocumentPropertyValuePermissionPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; + +export class UmbDocumentPropertyValueUserPermissionFromManagementApiDataMapping + extends UmbControllerBase + implements + UmbDataSourceDataMapping< + DocumentPropertyValuePermissionPresentationModel, + UmbDocumentPropertyValueUserPermissionModel + > +{ + async map( + data: DocumentPropertyValuePermissionPresentationModel, + ): Promise { + return { + $type: data.$type, + userPermissionType: UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE, + documentType: { + unique: data.documentType.id, + }, + propertyType: { + unique: data.propertyType.id, + }, + verbs: data.verbs, + }; + } +} + +export { UmbDocumentPropertyValueUserPermissionFromManagementApiDataMapping as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/manifests.ts new file mode 100644 index 000000000000..afde9143747a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/manifests.ts @@ -0,0 +1,22 @@ +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE } from '../user-permission.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS } from '@umbraco-cms/backoffice/repository'; + +export const manifests: Array = [ + { + type: 'dataSourceDataMapping', + alias: 'Umb.DataSourceDataMapping.ManagementApi.To.DocumentPropertyValuePermissionPresentationModel', + name: 'Document Property Value Permission To Management Api Data Mapping', + api: () => import('./to-server.management-api.mapping.js'), + forDataSource: UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS, + forDataModel: UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE, + }, + { + type: 'dataSourceDataMapping', + alias: 'Umb.DataSourceDataMapping.ManagementApi.From.DocumentPropertyValuePermissionPresentationModel', + name: 'Document Property Value Permission From Management Api Data Mapping', + api: () => import('./from-server.management-api.mapping.js'), + forDataSource: UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS, + forDataModel: 'DocumentPropertyValuePermissionPresentationModel', + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/to-server.management-api.mapping.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/to-server.management-api.mapping.ts new file mode 100644 index 000000000000..8ca9e5c58e2f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/data/to-server.management-api.mapping.ts @@ -0,0 +1,30 @@ +import type { UmbDocumentPropertyValueUserPermissionModel } from '../types.js'; +import type { UmbDataSourceDataMapping } from '@umbraco-cms/backoffice/repository'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import type { DocumentPropertyValuePermissionPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; + +export class UmbDocumentPropertyValueUserPermissionToManagementApiDataMapping + extends UmbControllerBase + implements + UmbDataSourceDataMapping< + UmbDocumentPropertyValueUserPermissionModel, + DocumentPropertyValuePermissionPresentationModel + > +{ + async map( + data: UmbDocumentPropertyValueUserPermissionModel, + ): Promise { + return { + $type: 'DocumentPropertyValuePermissionPresentationModel', + documentType: { + id: data.documentType.unique, + }, + propertyType: { + id: data.propertyType.unique, + }, + verbs: data.verbs, + }; + } +} + +export { UmbDocumentPropertyValueUserPermissionToManagementApiDataMapping as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/constants.ts new file mode 100644 index 000000000000..5dac179a5893 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/constants.ts @@ -0,0 +1,2 @@ +export { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_MODAL } from './document-property-value-permission-flow-modal.token.js'; +export * from './property-type-modal/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/document-property-value-permission-flow-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/document-property-value-permission-flow-modal.element.ts new file mode 100644 index 000000000000..d9c8e9b90c48 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/document-property-value-permission-flow-modal.element.ts @@ -0,0 +1,95 @@ +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL } from './property-type-modal/property-type-modal.token.js'; +import type { + UmbDocumentPropertyValueUserPermissionFlowModalData, + UmbDocumentPropertyValueUserPermissionFlowModalValue, +} from './document-property-value-permission-flow-modal.token.js'; +import { html, customElement, state } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_MODAL_MANAGER_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { UMB_DOCUMENT_TYPE_TREE_ALIAS } from '@umbraco-cms/backoffice/document-type'; +import type { UmbSelectionChangeEvent } from '@umbraco-cms/backoffice/event'; +import type { UmbTreeElement } from '@umbraco-cms/backoffice/tree'; + +@customElement('umb-document-property-value-user-permission-flow-modal') +export class UmbDocumentPropertyValueUserPermissionFlowModalElement extends UmbModalBaseElement< + UmbDocumentPropertyValueUserPermissionFlowModalData, + UmbDocumentPropertyValueUserPermissionFlowModalValue +> { + @state() + _selection: Array = []; + + async #next() { + if (this._selection.length === 0) { + throw new Error('No document type selected'); + } + + const documentType = { unique: this._selection[0] }; + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + if (!modalManager) { + throw new Error('Could not get modal manager context'); + } + + const modal = modalManager.open(this, UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL, { + data: { + documentType, + preset: { + verbs: this.data?.preset?.verbs, + }, + pickableFilter: this.data?.pickablePropertyTypeFilter, + }, + }); + + try { + const value = await modal.onSubmit(); + + this.updateValue({ + documentType, + propertyType: value.propertyType, + verbs: value.verbs, + }); + + this._submitModal(); + } catch (err) { + console.error(err); + } + } + + #onTreeSelectionChange(event: UmbSelectionChangeEvent) { + const target = event.target as UmbTreeElement; + const selection = target.getSelection(); + this._selection = [...selection]; + } + + override render() { + return html` + + + + +
+ + +
+
+ `; + } +} + +export { UmbDocumentPropertyValueUserPermissionFlowModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-property-value-user-permission-flow-modal': UmbDocumentPropertyValueUserPermissionFlowModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/document-property-value-permission-flow-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/document-property-value-permission-flow-modal.token.ts new file mode 100644 index 000000000000..be0a2f26984a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/document-property-value-permission-flow-modal.token.ts @@ -0,0 +1,27 @@ +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDocumentPropertyValueUserPermissionFlowModalData { + preset?: Partial; + pickablePropertyTypeFilter?: (propertyType: UmbPropertyTypeModel) => boolean; +} + +export interface UmbDocumentPropertyValueUserPermissionFlowModalValue { + documentType: { + unique: string; + }; + propertyType: { + unique: string; + }; + verbs: Array; +} + +export const UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_MODAL = new UmbModalToken< + UmbDocumentPropertyValueUserPermissionFlowModalData, + UmbDocumentPropertyValueUserPermissionFlowModalValue +>('Umb.Modal.DocumentPropertyValueUserPermissionFlow', { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/index.ts new file mode 100644 index 000000000000..6913b07b66e7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/index.ts @@ -0,0 +1 @@ +export { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_MODAL } from './document-property-value-permission-flow-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/manifests.ts new file mode 100644 index 000000000000..eaa896c9a446 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/manifests.ts @@ -0,0 +1,12 @@ +import { manifests as propertyTypeModalManifests } from './property-type-modal/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.DocumentPropertyValueUserPermissionFlow', + name: 'Document Property Value User Permission Flow Modal', + element: () => import('./document-property-value-permission-flow-modal.element.js'), + }, + ...propertyTypeModalManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/constants.ts new file mode 100644 index 000000000000..b30d1cbca3d6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/constants.ts @@ -0,0 +1,2 @@ +export { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL_ALIAS } from './manifests.js'; +export { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL } from './property-type-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/manifests.ts new file mode 100644 index 000000000000..7ddec3db6683 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/manifests.ts @@ -0,0 +1,13 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL_ALIAS = + 'Umb.Modal.DocumentPropertyValueUserPermissionFlow.PropertyType'; + +export const manifests: Array = [ + { + type: 'modal', + alias: UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL_ALIAS, + name: 'Document Property Value User Permission Flow Property Type Modal', + element: () => import('./property-type-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/property-type-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/property-type-modal.element.ts new file mode 100644 index 000000000000..cccfe38107a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/property-type-modal.element.ts @@ -0,0 +1,144 @@ +import { UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE } from '../../../../entity.js'; +import type { + UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalData, + UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalValue, +} from './property-type-modal.token.js'; +import { html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit'; +import { UMB_MODAL_MANAGER_CONTEXT, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbDocumentTypeDetailRepository } from '@umbraco-cms/backoffice/document-type'; +import { UMB_ENTITY_USER_PERMISSION_MODAL } from '@umbraco-cms/backoffice/user-permission'; + +@customElement('umb-document-property-value-user-permission-flow-property-type-modal') +export class UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalElement extends UmbModalBaseElement< + UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalData, + UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalValue +> { + @state() + private _documentTypeProperties: Array = []; + + @state() + private _documentTypeName?: string; + + @state() + _selectedItem: UmbPropertyTypeModel | null = null; + + @state() + _pickableFilter: (propertyType: UmbPropertyTypeModel) => boolean = () => true; + + #detailRepository = new UmbDocumentTypeDetailRepository(this); + + #onItemSelected(event: CustomEvent, item: UmbPropertyTypeModel) { + event.stopPropagation(); + this._selectedItem = item; + } + + #onItemDeselected(event: CustomEvent) { + event.stopPropagation(); + this._selectedItem = null; + } + + override async firstUpdated() { + if (!this.data?.documentType?.unique) { + throw new Error('Document type unique is required'); + } + + this._pickableFilter = this.data.pickableFilter ?? this._pickableFilter; + + const { data } = await this.#detailRepository.requestByUnique(this.data.documentType.unique); + this._documentTypeProperties = data?.properties ?? []; + this._documentTypeName = data?.name; + } + + #getItemDetail(item: UmbPropertyTypeModel): string { + const isMandatory = item.validation?.mandatory ? ' - Mandatory' : ''; + const variesByCulture = item.variesByCulture ? ' - Varies by culture' : ''; + const variesBySegment = item.variesBySegment ? ' - Varies by segment' : ''; + return `${item.alias} ${isMandatory} ${variesByCulture} ${variesBySegment}`; + } + + #next() { + if (!this._selectedItem) { + throw new Error('Could not proceed, no property was selected'); + } + + this.#selectEntityUserPermissionsForProperty(); + } + + async #selectEntityUserPermissionsForProperty() { + if (!this._selectedItem) { + throw new Error('Could not open permissions modal, no property was provided'); + } + + const headline = `Permissions for ${this._documentTypeName}: ${this._selectedItem.name}`; + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + if (!modalManager) { + throw new Error('Could not open permissions modal, modal manager is not available'); + } + + const modal = modalManager.open(this, UMB_ENTITY_USER_PERMISSION_MODAL, { + data: { + entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, + headline, + preset: { + allowedVerbs: this.data?.preset?.verbs ?? [], + }, + }, + }); + + try { + const value = await modal.onSubmit(); + this.updateValue({ + propertyType: { unique: this._selectedItem.unique }, + verbs: value.allowedVerbs, + }); + this._submitModal(); + } catch (error) { + console.log(error); + } + } + + override render() { + return html` + + ${this._documentTypeProperties.length > 0 + ? repeat( + this._documentTypeProperties, + (item) => item.unique, + (item) => html` + this.#onItemSelected(event, item)} + @deselected=${(event: CustomEvent) => this.#onItemDeselected(event)} + ?selected=${this._selectedItem?.unique === item.unique} + ?disabled=${!this._pickableFilter(item)}> + + + `, + ) + : html`There are no properties to choose from.`} + +
+ + +
+
`; + } +} + +export { UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-document-property-value-user-permission-flow-property-type-modal': UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/property-type-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/property-type-modal.token.ts new file mode 100644 index 000000000000..9ea7fbf02247 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/document-property-value-permission-flow-modal/property-type-modal/property-type-modal.token.ts @@ -0,0 +1,28 @@ +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL_ALIAS } from './manifests.js'; +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalData { + documentType: { + unique: string; + }; + preset?: Partial; + pickableFilter?: (propertyType: UmbPropertyTypeModel) => boolean; +} + +export interface UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalValue { + propertyType: { + unique: string; + }; + verbs: Array; +} + +export const UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL = new UmbModalToken< + UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalData, + UmbDocumentPropertyValueUserPermissionFlowPropertyTypeModalValue +>(UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_PROPERTY_TYPE_MODAL_ALIAS, { + modal: { + type: 'sidebar', + size: 'small', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/input-document-property-value-user-permission/input-document-property-value-user-permission.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/input-document-property-value-user-permission/input-document-property-value-user-permission.element.ts new file mode 100644 index 000000000000..f2548a45b7b8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/input-document-property-value-user-permission/input-document-property-value-user-permission.element.ts @@ -0,0 +1,264 @@ +import type { UmbDocumentPropertyValueUserPermissionModel as UmbDocumentPropertyValueUserPermissionModel } from '../types.js'; +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_MODAL } from '../document-property-value-permission-flow-modal/index.js'; +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE } from '../user-permission.js'; +import { UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE } from '../../../entity.js'; +import { + css, + customElement, + html, + ifDefined, + nothing, + property, + repeat, + state, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UUIFormControlMixin } from '@umbraco-cms/backoffice/external/uui'; +import { + UMB_ENTITY_USER_PERMISSION_MODAL, + type ManifestEntityUserPermission, +} from '@umbraco-cms/backoffice/user-permission'; +import { + UmbDocumentTypeDetailRepository, + type UmbDocumentTypeDetailModel, +} from '@umbraco-cms/backoffice/document-type'; +import { UmbChangeEvent } from '@umbraco-cms/backoffice/event'; + +@customElement('umb-input-document-property-value-user-permission') +export class UmbInputDocumentPropertyValueUserPermissionElement extends UUIFormControlMixin(UmbLitElement, '') { + _permissions: Array = []; + public get permissions(): Array { + return this._permissions; + } + public set permissions(value: Array) { + this._permissions = value; + const uniques = value.map((item) => item.documentType.unique); + this.#observePickedDocumentTypes(uniques); + } + + @property({ type: Array, attribute: false }) + fallbackPermissions: Array = []; + + @state() + private _documentTypes?: Array; + + #documentTypeDetailRepository = new UmbDocumentTypeDetailRepository(this); + + protected override getFormElement() { + return undefined; + } + + async #observePickedDocumentTypes(uniques: Array) { + const promises = uniques.map((unique) => this.#documentTypeDetailRepository.requestByUnique(unique)); + const responses = await Promise.allSettled(promises); + + // TODO: handle errors + this._documentTypes = responses + .filter((response) => response.status === 'fulfilled') + .map((response) => response.value.data) + .filter((item) => item) as Array; + } + + async #addPermission() { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + if (!modalManager) { + throw new Error('Could not open modal, no modal manager found'); + } + + const modal = modalManager.open(this, UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_FLOW_MODAL, { + data: { + preset: { + verbs: this.#getFallbackPermissionVerbsForEntityType(UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE), + }, + pickablePropertyTypeFilter: (propertyType) => + !this._permissions.some((permission) => permission.propertyType.unique === propertyType.unique), + }, + }); + + try { + const value = await modal?.onSubmit(); + if (!value) throw new Error('No result from modal'); + + const permissionItem: UmbDocumentPropertyValueUserPermissionModel = { + $type: 'DocumentPropertyValuePermissionPresentationModel', + userPermissionType: UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE, + documentType: value.documentType, + propertyType: value.propertyType, + verbs: value.verbs, + }; + + this.permissions = [...this._permissions, permissionItem]; + this.dispatchEvent(new UmbChangeEvent()); + } catch (error) { + console.error(error); + } + } + + async #editPermission(currentPermission: UmbDocumentPropertyValueUserPermissionModel) { + if (!currentPermission) { + throw new Error('Could not open permissions modal, no item was provided'); + } + + const documentType = this._documentTypes?.find((item) => item.unique === currentPermission.documentType.unique); + + if (!documentType) { + throw new Error('Could not open permissions modal, no document type was found'); + } + + // TODO: show document type and property type name + const headline = `Permissions`; + + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + if (!modalManager) { + throw new Error('Could not open permissions modal, modal manager is not available'); + } + + const modal = modalManager.open(this, UMB_ENTITY_USER_PERMISSION_MODAL, { + data: { + entityType: UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE, + headline, + preset: { + allowedVerbs: currentPermission.verbs, + }, + }, + }); + + try { + const value = await modal.onSubmit(); + + // don't do anything if the verbs have not been updated + if (JSON.stringify(value.allowedVerbs) === JSON.stringify(currentPermission.verbs)) return; + + // update permission with new verbs + this.permissions = this._permissions.map((permission) => { + if (permission.propertyType.unique === currentPermission.propertyType.unique) { + return { + ...permission, + verbs: value.allowedVerbs, + }; + } + return permission; + }); + + this.dispatchEvent(new UmbChangeEvent()); + } catch (error) { + console.log(error); + } + } + + #removePermission(permission: UmbDocumentPropertyValueUserPermissionModel) { + this.permissions = this._permissions.filter((v) => JSON.stringify(v) !== JSON.stringify(permission)); + this.dispatchEvent(new UmbChangeEvent()); + } + + #getVerbNamesForPermission(permission: UmbDocumentPropertyValueUserPermissionModel) { + if (!permission) { + throw new Error('Could not find permission for property type'); + } + + if (permission.verbs.length === 0) { + return this.localize.term('user_permissionNoVerbs'); + } + + return umbExtensionsRegistry + .getByTypeAndFilter('entityUserPermission', (manifest) => + manifest.meta.verbs.every((verb) => permission.verbs.includes(verb)), + ) + .map((m) => { + const manifest = m as ManifestEntityUserPermission; + return manifest.meta.label ? this.localize.string(manifest.meta.label) : manifest.name; + }) + .join(', '); + } + + #getFallbackPermissionVerbsForEntityType(entityType: string) { + // get all permissions that are allowed for the entity type and have at least one of the fallback permissions + // this is used to determine the default permissions for a document + const verbs = umbExtensionsRegistry + .getByTypeAndFilter( + 'entityUserPermission', + (manifest) => + manifest.forEntityTypes.includes(entityType) && + this.fallbackPermissions.map((verb) => manifest.meta.verbs.includes(verb)).includes(true), + ) + .flatMap((permission) => permission.meta.verbs); + + // ensure that the verbs are unique + return [...new Set([...verbs])]; + } + + override render() { + return html`${this.#renderItems()} ${this.#renderAddButton()}`; + } + + #renderItems() { + if (!this.permissions) return; + return html` + + ${repeat( + this.permissions, + (item) => item.propertyType.unique, + (item) => this.#renderRef(item), + )} + + `; + } + + #renderAddButton() { + return html``; + } + + #renderRef(permission: UmbDocumentPropertyValueUserPermissionModel) { + if (!permission.propertyType.unique) { + throw new Error('Property type unique is required'); + } + + const documentType = this._documentTypes?.find((item) => item.unique === permission.documentType.unique); + const propertyType = documentType?.properties.find((item) => item.unique === permission.propertyType.unique); + const permissionName = `${documentType?.name}: ${propertyType?.name} (${propertyType?.alias})`; + const verbNames = this.#getVerbNamesForPermission(permission); + + return html` + + ${documentType?.icon ? html`` : nothing} + ${this.#renderEditButton(permission)} ${this.#renderRemoveButton(permission)} + + `; + } + + #renderEditButton(permission: UmbDocumentPropertyValueUserPermissionModel) { + return html` this.#editPermission(permission)} + label=${this.localize.term('general_edit')}>`; + } + + #renderRemoveButton(permission: UmbDocumentPropertyValueUserPermissionModel) { + return html` this.#removePermission(permission)} + label=${this.localize.term('general_remove')}>`; + } + + static override styles = [ + css` + #btn-add { + width: 100%; + } + `, + ]; +} + +export { UmbInputDocumentPropertyValueUserPermissionElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-input-document-property-value-user-permission': UmbInputDocumentPropertyValueUserPermissionElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/manifests.ts new file mode 100644 index 000000000000..a12076297d1c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/manifests.ts @@ -0,0 +1,56 @@ +import { UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE } from '../../entity.js'; +import { + UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_READ, + UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_WRITE, +} from './constants.js'; +import { manifests as documentPropertyValueUserPermissionFlowModalManifests } from './document-property-value-permission-flow-modal/manifests.js'; +import { manifests as conditionManifests } from './conditions/manifests.js'; +import { manifests as dataManifests } from './data/manifests.js'; +import { manifests as workspaceContextManifests } from './workspace-context/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'entityUserPermission', + alias: 'Umb.EntityUserPermission.Document.PropertyValue.Read', + name: 'Read Document Property Value User Permission', + forEntityTypes: [UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE], + weight: 200, + meta: { + verbs: [UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_READ], + label: 'Read', + description: 'Read Document property values', + }, + }, + { + type: 'entityUserPermission', + alias: 'Umb.EntityUserPermission.DocumentPropertyValue.Write', + name: 'Write Document Property Value User Permission', + forEntityTypes: [UMB_DOCUMENT_PROPERTY_VALUE_ENTITY_TYPE], + weight: 200, + meta: { + verbs: [UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_WRITE], + label: 'Write', + description: 'Write Document property values', + }, + }, + { + type: 'userGranularPermission', + alias: 'Umb.UserGranularPermission.Document.PropertyValue', + name: 'Document Property Values Granular User Permission', + weight: 950, + element: () => + import( + './input-document-property-value-user-permission/input-document-property-value-user-permission.element.js' + ), + meta: { + schemaType: 'DocumentPropertyValuePermissionPresentationModel', + label: 'Document Property Values', + description: 'Assign Permissions to Document property values', + }, + }, + ...conditionManifests, + ...dataManifests, + ...documentPropertyValueUserPermissionFlowModalManifests, + ...workspaceContextManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/types.ts new file mode 100644 index 000000000000..d5334bb7572f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/types.ts @@ -0,0 +1,9 @@ +import type { UmbDocumentPropertyValueUserPermissionType } from './user-permission.js'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import type { UmbUserPermissionModel } from '@umbraco-cms/backoffice/user-permission'; + +export interface UmbDocumentPropertyValueUserPermissionModel extends UmbUserPermissionModel { + userPermissionType: UmbDocumentPropertyValueUserPermissionType; + documentType: UmbReferenceByUnique; + propertyType: UmbReferenceByUnique; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/user-permission.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/user-permission.ts new file mode 100644 index 000000000000..f261a0c8ef61 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/user-permission.ts @@ -0,0 +1,3 @@ +export const UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE = 'document-property-value'; + +export type UmbDocumentPropertyValueUserPermissionType = typeof UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/document-block-property-value-user-permission.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/document-block-property-value-user-permission.workspace-context.ts new file mode 100644 index 000000000000..7037e4fb5fb2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/document-block-property-value-user-permission.workspace-context.ts @@ -0,0 +1,57 @@ +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../../workspace/constants.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UMB_BLOCK_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/block'; +import { UmbPropertyValueUserPermissionWorkspaceContextBase } from './property-value-user-permission-workspace-context-base.js'; +import { UMB_CONTENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/content'; +import { UMB_DOCUMENT_ENTITY_TYPE } from '../../../entity.js'; + +export class UmbDocumentBlockPropertyValueUserPermissionWorkspaceContext extends UmbPropertyValueUserPermissionWorkspaceContextBase { + #blockWorkspaceContext?: typeof UMB_BLOCK_WORKSPACE_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_BLOCK_WORKSPACE_CONTEXT, async (context) => { + this.#blockWorkspaceContext = context; + + // We only want to apply the permission logic if the block is in a document + // TODO: revisit this when getContext supports passContextAliasMatches + const contentWorkspaceContext = await this.consumeContext(UMB_CONTENT_WORKSPACE_CONTEXT, () => {}) + .passContextAliasMatches() + .asPromise() + .catch(() => undefined); + + if (contentWorkspaceContext?.getEntityType() === UMB_DOCUMENT_ENTITY_TYPE) { + this.#observeDocumentBlockProperties(); + } + }); + } + + async #observeDocumentBlockProperties() { + if (!this.#blockWorkspaceContext) return; + + const ownerContent = this.#blockWorkspaceContext.content; + + this.observe(ownerContent.structure.contentTypeProperties, (properties) => { + // TODO: If zero properties I guess we should then clear the state? [NL] + if (properties.length === 0) return; + + ownerContent.propertyViewGuard.fallbackToDisallowed(); + ownerContent.propertyWriteGuard.fallbackToDisallowed(); + this._setPermissions(properties, ownerContent.propertyViewGuard, ownerContent.propertyWriteGuard); + }); + + const ownerSettings = this.#blockWorkspaceContext.settings; + + this.observe(ownerSettings.structure.contentTypeProperties, (properties) => { + // TODO: If zero properties I guess we should then clear the state? [NL] + if (properties.length === 0) return; + + ownerSettings.propertyViewGuard.fallbackToDisallowed(); + ownerSettings.propertyWriteGuard.fallbackToDisallowed(); + this._setPermissions(properties, ownerSettings.propertyViewGuard, ownerSettings.propertyWriteGuard); + }); + } +} + +export { UmbDocumentBlockPropertyValueUserPermissionWorkspaceContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/document-property-value-user-permission.workspace-context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/document-property-value-user-permission.workspace-context.ts new file mode 100644 index 000000000000..ec5a7eacd075 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/document-property-value-user-permission.workspace-context.ts @@ -0,0 +1,34 @@ +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../../workspace/constants.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbPropertyValueUserPermissionWorkspaceContextBase } from './property-value-user-permission-workspace-context-base.js'; + +export class UmbDocumentPropertyValueUserPermissionWorkspaceContext extends UmbPropertyValueUserPermissionWorkspaceContextBase { + #documentWorkspaceContext?: typeof UMB_DOCUMENT_WORKSPACE_CONTEXT.TYPE; + + constructor(host: UmbControllerHost) { + super(host); + + this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (context) => { + this.#documentWorkspaceContext = context; + + this.#observeDocumentProperties(); + }); + } + + #observeDocumentProperties() { + if (!this.#documentWorkspaceContext) return; + + const owner = this.#documentWorkspaceContext; + + this.observe(this.#documentWorkspaceContext.structure.contentTypeProperties, (properties) => { + // TODO: If zero properties I guess we should then clear the state? [NL] + if (properties.length === 0) return; + + this.#documentWorkspaceContext!.propertyViewGuard.fallbackToDisallowed(); + this.#documentWorkspaceContext!.propertyWriteGuard.fallbackToDisallowed(); + this._setPermissions(properties, owner.propertyViewGuard, owner.propertyWriteGuard); + }); + } +} + +export { UmbDocumentPropertyValueUserPermissionWorkspaceContext as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/manifests.ts new file mode 100644 index 000000000000..78a7953b7d3b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/manifests.ts @@ -0,0 +1,31 @@ +import { UMB_DOCUMENT_WORKSPACE_ALIAS } from '../../../workspace/constants.js'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_BLOCK_WORKSPACE_ALIAS } from '@umbraco-cms/backoffice/block'; + +export const manifests: Array = [ + { + type: 'workspaceContext', + name: 'Document Property Value User Permission Document Workspace Context', + alias: 'Umb.WorkspaceContext.Document.DocumentPropertyValueUserPermission', + api: () => import('./document-property-value-user-permission.workspace-context.js'), + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_DOCUMENT_WORKSPACE_ALIAS, + }, + ], + }, + { + type: 'workspaceContext', + name: 'Document Property Value User Permission Block Workspace Context', + alias: 'Umb.WorkspaceContext.Block.DocumentPropertyValueUserPermission', + api: () => import('./document-block-property-value-user-permission.workspace-context.js'), + conditions: [ + { + alias: UMB_WORKSPACE_CONDITION_ALIAS, + match: UMB_BLOCK_WORKSPACE_ALIAS, + }, + ], + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/property-value-user-permission-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/property-value-user-permission-workspace-context-base.ts new file mode 100644 index 000000000000..19823eb5871b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document-property-value/workspace-context/property-value-user-permission-workspace-context-base.ts @@ -0,0 +1,67 @@ +import { UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_CONDITION_ALIAS } from '../conditions/constants.js'; +import { + UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_READ, + UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_WRITE, +} from '../constants.js'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbVariantPropertyGuardManager } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; + +export class UmbPropertyValueUserPermissionWorkspaceContextBase extends UmbControllerBase { + protected _setPermissions( + properties: Array, + propertyViewGuard: UmbVariantPropertyGuardManager, + propertyWriteGuard: UmbVariantPropertyGuardManager, + ) { + properties.forEach((property) => { + this.#setPermissionForProperty({ + verb: UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_READ, + stateManager: propertyViewGuard, + property, + }); + + this.#setPermissionForProperty({ + verb: UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_WRITE, + stateManager: propertyWriteGuard, + property, + }); + }); + } + + #setPermissionForProperty(args: { + verb: string; + stateManager: UmbVariantPropertyGuardManager; + property: UmbPropertyTypeModel; + }) { + // TODO: Oh, this results in quite a few Context Consumptions. Lets try not to use a condition in this case. [NL] + createExtensionApiByAlias(this, UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_CONDITION_ALIAS, [ + { + config: { + allOf: [args.verb], + match: { + propertyType: { + unique: args.property.unique, + }, + }, + }, + onChange: (permitted: boolean) => { + const unique = 'UMB_PROPERTY_' + args.property.unique; + + if (permitted) { + args.stateManager.addRule({ + unique, + propertyType: { + unique: args.property.unique, + }, + }); + } else { + args.stateManager.removeRule(unique); + } + }, + }, + ]); + } +} + +export { UmbPropertyValueUserPermissionWorkspaceContextBase as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/document-user-permission.condition.test.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/document-user-permission.condition.test.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/document-user-permission.condition.test.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/document-user-permission.condition.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/document-user-permission.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/document-user-permission.condition.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/document-user-permission.condition.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/document-user-permission.condition.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/types.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/conditions/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/conditions/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/constants.ts new file mode 100644 index 000000000000..bb0fa17977ac --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/constants.ts @@ -0,0 +1,18 @@ +export const UMB_USER_PERMISSION_DOCUMENT_CREATE = 'Umb.Document.Create'; +export const UMB_USER_PERMISSION_DOCUMENT_READ = 'Umb.Document.Read'; +export const UMB_USER_PERMISSION_DOCUMENT_UPDATE = 'Umb.Document.Update'; +export const UMB_USER_PERMISSION_DOCUMENT_DELETE = 'Umb.Document.Delete'; +export const UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT = 'Umb.Document.CreateBlueprint'; +export const UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS = 'Umb.Document.Notifications'; +export const UMB_USER_PERMISSION_DOCUMENT_PUBLISH = 'Umb.Document.Publish'; +export const UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS = 'Umb.Document.Permissions'; +export const UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH = 'Umb.Document.Unpublish'; +export const UMB_USER_PERMISSION_DOCUMENT_DUPLICATE = 'Umb.Document.Duplicate'; +export const UMB_USER_PERMISSION_DOCUMENT_MOVE = 'Umb.Document.Move'; +export const UMB_USER_PERMISSION_DOCUMENT_SORT = 'Umb.Document.Sort'; +export const UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES = 'Umb.Document.CultureAndHostnames'; +export const UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS = 'Umb.Document.PublicAccess'; +export const UMB_USER_PERMISSION_DOCUMENT_ROLLBACK = 'Umb.Document.Rollback'; + +export * from './conditions/constants.js'; +export * from './repository/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/index.ts new file mode 100644 index 000000000000..3d76f338dddc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/index.ts @@ -0,0 +1 @@ +export * from './repository/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/input-document-granular-user-permission/input-document-granular-user-permission.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/input-document-granular-user-permission/input-document-granular-user-permission.element.ts similarity index 97% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/input-document-granular-user-permission/input-document-granular-user-permission.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/input-document-granular-user-permission/input-document-granular-user-permission.element.ts index f2d34b04a335..f2e3d263ddad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/input-document-granular-user-permission/input-document-granular-user-permission.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/input-document-granular-user-permission/input-document-granular-user-permission.element.ts @@ -1,7 +1,7 @@ import type { UmbDocumentUserPermissionModel } from '../types.js'; -import { UmbDocumentItemRepository } from '../../item/index.js'; -import type { UmbDocumentItemModel } from '../../item/types.js'; -import { UMB_DOCUMENT_PICKER_MODAL } from '../../constants.js'; +import { UmbDocumentItemRepository } from '../../../item/index.js'; +import type { UmbDocumentItemModel } from '../../../item/types.js'; +import { UMB_DOCUMENT_PICKER_MODAL } from '../../../constants.js'; import { css, customElement, html, property, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import type { UmbModalManagerContext } from '@umbraco-cms/backoffice/modal'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/manifests.ts new file mode 100644 index 000000000000..f1b18cc04e30 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/manifests.ts @@ -0,0 +1,222 @@ +import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; +import { + UMB_USER_PERMISSION_DOCUMENT_READ, + UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT, + UMB_USER_PERMISSION_DOCUMENT_DELETE, + UMB_USER_PERMISSION_DOCUMENT_CREATE, + UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS, + UMB_USER_PERMISSION_DOCUMENT_PUBLISH, + UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, + UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, + UMB_USER_PERMISSION_DOCUMENT_UPDATE, + UMB_USER_PERMISSION_DOCUMENT_DUPLICATE, + UMB_USER_PERMISSION_DOCUMENT_MOVE, + UMB_USER_PERMISSION_DOCUMENT_SORT, + UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES, + UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS, + UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, +} from './constants.js'; +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as conditionManifests } from './conditions/manifests.js'; +import type { + ManifestGranularUserPermission, + ManifestEntityUserPermission, +} from '@umbraco-cms/backoffice/user-permission'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +const permissions: Array = [ + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_READ, + name: 'Read Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Read'], + label: '#actions_browse', + description: '#actionDescriptions_browse', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT, + name: 'Create Document Blueprint User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.CreateBlueprint'], + label: '#actions_createblueprint', + description: '#actionDescriptions_createblueprint', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_DELETE, + name: 'Delete Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Delete'], + label: '#actions_delete', + description: '#actionDescriptions_delete', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_CREATE, + name: 'Create Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Create'], + label: '#actions_create', + description: '#actionDescriptions_create', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS, + name: 'Document Notifications User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Notifications'], + label: '#actions_notify', + description: '#actionDescriptions_notify', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_PUBLISH, + name: 'Publish Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Publish'], + label: '#actions_publish', + description: '#actionDescriptions_publish', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, + name: 'Document Permissions User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Permissions'], + label: '#actions_setPermissions', + description: '#actionDescriptions_rights', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, + name: 'Unpublish Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Unpublish'], + label: '#actions_unpublish', + description: '#actionDescriptions_unpublish', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_UPDATE, + name: 'Update Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Update'], + label: '#actions_update', + description: '#actionDescriptions_update', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_DUPLICATE, + name: 'Duplicate Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Duplicate'], + label: '#actions_copy', + description: '#actionDescriptions_copy', + group: 'structure', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_MOVE, + name: 'Move Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Move'], + label: '#actions_move', + description: '#actionDescriptions_move', + group: 'structure', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_SORT, + name: 'Sort Document User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Sort'], + label: '#actions_sort', + description: '#actionDescriptions_sort', + group: 'structure', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES, + name: 'Document Culture And Hostnames User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.CultureAndHostnames'], + label: '#actions_assigndomain', + description: '#actionDescriptions_assignDomain', + group: 'administration', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS, + name: 'Document Public Access User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.PublicAccess'], + label: '#actions_protect', + description: '#actionDescriptions_protect', + group: 'administration', + }, + }, + { + type: 'entityUserPermission', + alias: UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, + name: 'Document Rollback User Permission', + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + verbs: ['Umb.Document.Rollback'], + label: '#actions_rollback', + description: '#actionDescriptions_rollback', + group: 'administration', + }, + }, +]; + +export const granularPermissions: Array = [ + { + type: 'userGranularPermission', + alias: 'Umb.UserGranularPermission.Document', + name: 'Document Granular User Permission', + weight: 1000, + element: () => + import('./input-document-granular-user-permission/input-document-granular-user-permission.element.js'), + meta: { + schemaType: 'DocumentPermissionPresentationModel', + label: '#user_granularRightsLabel', + description: '{#user_granularRightsDescription}', + }, + }, +]; + +export const manifests: Array = [ + ...repositoryManifests, + ...permissions, + ...granularPermissions, + ...conditionManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/document-permission.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.repository.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/document-permission.repository.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.repository.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/document-permission.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/document-permission.server.data.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/document-permission.server.data.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/repository/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/repository/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/types.ts new file mode 100644 index 000000000000..0028a6bf3533 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/types.ts @@ -0,0 +1,6 @@ +import type { UmbUserPermissionModel } from '@umbraco-cms/backoffice/user-permission'; +export type * from './conditions/types.js'; +export interface UmbDocumentUserPermissionModel extends UmbUserPermissionModel { + // TODO: this should be unique instead of an id, but we currently have now way to map a mixed server response. + document: { id: string }; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/utils.ts new file mode 100644 index 000000000000..003d3017f684 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/document/utils.ts @@ -0,0 +1,9 @@ +import type { DocumentPermissionPresentationModel } from '@umbraco-cms/backoffice/external/backend-api'; + +/** + * + * @param permission + */ +export function isDocumentUserPermission(permission: unknown): permission is DocumentPermissionPresentationModel { + return (permission as DocumentPermissionPresentationModel).$type === 'DocumentPermissionPresentationModel'; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/index.ts index 3ed3c5c183d5..fbad52ff40a9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/index.ts @@ -1,3 +1 @@ -export * from './repository/index.js'; -export * from './constants.js'; -export type * from './types.js'; +export * from './document/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/manifests.ts index 30505ba03ad9..386fae38d02f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/manifests.ts @@ -1,220 +1,8 @@ -import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; -import { - UMB_USER_PERMISSION_DOCUMENT_READ, - UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT, - UMB_USER_PERMISSION_DOCUMENT_DELETE, - UMB_USER_PERMISSION_DOCUMENT_CREATE, - UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS, - UMB_USER_PERMISSION_DOCUMENT_PUBLISH, - UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, - UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, - UMB_USER_PERMISSION_DOCUMENT_UPDATE, - UMB_USER_PERMISSION_DOCUMENT_DUPLICATE, - UMB_USER_PERMISSION_DOCUMENT_MOVE, - UMB_USER_PERMISSION_DOCUMENT_SORT, - UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES, - UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS, - UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, -} from './constants.js'; -import { manifests as repositoryManifests } from './repository/manifests.js'; -import { manifests as conditionManifests } from './conditions/manifests.js'; -import type { - ManifestGranularUserPermission, - ManifestEntityUserPermission, -} from '@umbraco-cms/backoffice/user-permission'; +import { manifests as documentManifests } from './document/manifests.js'; +import { manifests as documentPropertyValueManifests } from './document-property-value/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -const permissions: Array = [ - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_READ, - name: 'Read Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Read'], - label: '#actions_browse', - description: '#actionDescriptions_browse', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_CREATE_BLUEPRINT, - name: 'Create Document Blueprint User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.CreateBlueprint'], - label: '#actions_createblueprint', - description: '#actionDescriptions_createblueprint', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_DELETE, - name: 'Delete Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Delete'], - label: '#actions_delete', - description: '#actionDescriptions_delete', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_CREATE, - name: 'Create Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Create'], - label: '#actions_create', - description: '#actionDescriptions_create', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_NOTIFICATIONS, - name: 'Document Notifications User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Notifications'], - label: '#actions_notify', - description: '#actionDescriptions_notify', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_PUBLISH, - name: 'Publish Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Publish'], - label: '#actions_publish', - description: '#actionDescriptions_publish', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_PERMISSIONS, - name: 'Document Permissions User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Permissions'], - label: '#actions_setPermissions', - description: '#actionDescriptions_rights', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_UNPUBLISH, - name: 'Unpublish Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Unpublish'], - label: '#actions_unpublish', - description: '#actionDescriptions_unpublish', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_UPDATE, - name: 'Update Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Update'], - label: '#actions_update', - description: '#actionDescriptions_update', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_DUPLICATE, - name: 'Duplicate Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Duplicate'], - label: '#actions_copy', - description: '#actionDescriptions_copy', - group: 'structure', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_MOVE, - name: 'Move Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Move'], - label: '#actions_move', - description: '#actionDescriptions_move', - group: 'structure', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_SORT, - name: 'Sort Document User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Sort'], - label: '#actions_sort', - description: '#actionDescriptions_sort', - group: 'structure', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_CULTURE_AND_HOSTNAMES, - name: 'Document Culture And Hostnames User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.CultureAndHostnames'], - label: '#actions_assigndomain', - description: '#actionDescriptions_assignDomain', - group: 'administration', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_PUBLIC_ACCESS, - name: 'Document Public Access User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.PublicAccess'], - label: '#actions_protect', - description: '#actionDescriptions_protect', - group: 'administration', - }, - }, - { - type: 'entityUserPermission', - alias: UMB_USER_PERMISSION_DOCUMENT_ROLLBACK, - name: 'Document Rollback User Permission', - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - verbs: ['Umb.Document.Rollback'], - label: '#actions_rollback', - description: '#actionDescriptions_rollback', - group: 'administration', - }, - }, -]; - -export const granularPermissions: Array = [ - { - type: 'userGranularPermission', - alias: 'Umb.UserGranularPermission.Document', - name: 'Document Granular User Permission', - element: () => - import('./input-document-granular-user-permission/input-document-granular-user-permission.element.js'), - meta: { - schemaType: 'DocumentPermissionPresentationModel', - label: '#user_granularRightsLabel', - description: '{#user_granularRightsDescription}', - }, - }, -]; - -export const manifests: Array = [ - ...repositoryManifests, - ...permissions, - ...granularPermissions, - ...conditionManifests, +export const manifests: Array = [ + ...documentManifests, + ...documentPropertyValueManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/types.ts index 0028a6bf3533..edcc7ad260db 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/types.ts @@ -1,6 +1,2 @@ -import type { UmbUserPermissionModel } from '@umbraco-cms/backoffice/user-permission'; -export type * from './conditions/types.js'; -export interface UmbDocumentUserPermissionModel extends UmbUserPermissionModel { - // TODO: this should be unique instead of an id, but we currently have now way to map a mixed server response. - document: { id: string }; -} +export type * from './document/types.js'; +export type * from './document-property-value/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/utils.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/utils.ts deleted file mode 100644 index c9b647678dee..000000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/user-permissions/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { - DocumentPermissionPresentationModel, - UnknownTypePermissionPresentationModel, -} from '@umbraco-cms/backoffice/external/backend-api'; - -/** - * - * @param permission - */ -export function isDocumentUserPermission( - permission: DocumentPermissionPresentationModel | UnknownTypePermissionPresentationModel, -): permission is DocumentPermissionPresentationModel { - return (permission as DocumentPermissionPresentationModel).$type === 'DocumentPermissionPresentationModel'; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts index ff4d77318840..231629d3c761 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save-and-preview.action.ts @@ -1,6 +1,6 @@ -import { UmbDocumentUserPermissionCondition } from '../../user-permissions/conditions/document-user-permission.condition.js'; +import { UmbDocumentUserPermissionCondition } from '../../user-permissions/document/conditions/document-user-permission.condition.js'; import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../document-workspace.context-token.js'; -import { UMB_USER_PERMISSION_DOCUMENT_UPDATE } from '../../user-permissions/index.js'; +import { UMB_USER_PERMISSION_DOCUMENT_UPDATE } from '../../user-permissions/document/constants.js'; import { UmbWorkspaceActionBase } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts index ee14d3ff053b..11a0bfd416f8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/actions/save.action.ts @@ -1,7 +1,5 @@ -import type { UmbDocumentVariantModel } from '../../types.js'; import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../document-workspace.context-token.js'; import type UmbDocumentWorkspaceContext from '../document-workspace.context.js'; -import type { UmbVariantState } from '@umbraco-cms/backoffice/utils'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { @@ -15,9 +13,6 @@ export class UmbDocumentSaveWorkspaceAction extends UmbSubmitWorkspaceAction implements UmbWorkspaceActionDefaultKind { - #variants: Array = []; - #readOnlyStates: Array = []; - constructor(host: UmbControllerHost, args: UmbSubmitWorkspaceActionArgs) { super(host, { workspaceContextToken: UMB_DOCUMENT_WORKSPACE_CONTEXT, ...args }); } @@ -31,39 +26,25 @@ export class UmbDocumentSaveWorkspaceAction override _gotWorkspaceContext() { super._gotWorkspaceContext(); this.#observeVariants(); - this.#observeReadOnlyStates(); } #observeVariants() { this.observe( this._workspaceContext?.variants, (variants) => { - this.#variants = variants ?? []; - this.#check(); + const allVariantsAreReadOnly = + variants?.filter((variant) => + this._workspaceContext!.readonlyGuard.getPermittedForVariant(UmbVariantId.Create(variant)), + ).length === variants?.length; + if (allVariantsAreReadOnly) { + this.disable(); + } else { + this.enable(); + } }, 'saveWorkspaceActionVariantsObserver', ); } - - #observeReadOnlyStates() { - this.observe( - this._workspaceContext?.readOnlyState.states, - (readOnlyStates) => { - this.#readOnlyStates = readOnlyStates ?? []; - this.#check(); - }, - 'saveWorkspaceActionReadOnlyStatesObserver', - ); - } - - #check() { - const allVariantsAreReadOnly = this.#variants.every((variant) => { - const variantId = new UmbVariantId(variant.culture, variant.segment); - return this.#readOnlyStates.some((state) => state.variantId.equal(variantId)); - }); - - return allVariantsAreReadOnly ? this.disable() : this.enable(); - } } export { UmbDocumentSaveWorkspaceAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 62cac87fb99a..82079774eab6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -37,10 +37,12 @@ import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app'; import { ensurePathEndsWithSlash, UmbDeprecation } from '@umbraco-cms/backoffice/utils'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_DOCUMENT_CONFIGURATION_CONTEXT } from '../index.js'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_LANGUAGE_USER_PERMISSION_CONDITION_ALIAS } from '@umbraco-cms/backoffice/language'; type ContentModel = UmbDocumentDetailModel; type ContentTypeModel = UmbDocumentTypeDetailModel; - export class UmbDocumentWorkspaceContext extends UmbContentDetailWorkspaceContextBase< ContentModel, @@ -87,6 +89,17 @@ export class UmbDocumentWorkspaceContext saveModalToken: UMB_DOCUMENT_SAVE_MODAL, }); + this.consumeContext(UMB_DOCUMENT_CONFIGURATION_CONTEXT, async (context) => { + const documentConfiguration = (await context?.getDocumentConfiguration()) ?? undefined; + + if (documentConfiguration) { + if (documentConfiguration.allowEditInvariantFromNonDefault !== true) { + this.#preventEditInvariantFromNonDefault(); + } else { + } + } + }); + this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); // TODO: Remove this in v17 as we have moved the publishing methods to the UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT. @@ -193,6 +206,38 @@ export class UmbDocumentWorkspaceContext ]); } + #preventEditInvariantFromNonDefault() { + this.observe(observeMultiple([this.structure.contentTypeProperties, this.languages]), ([properties, languages]) => { + if (properties.length === 0) return; + if (languages.length === 0) return; + + const defaultLanguageUnique = languages.find((x) => x.isDefault)?.unique; + + createExtensionApiByAlias(this, UMB_LANGUAGE_USER_PERMISSION_CONDITION_ALIAS, [ + { + config: { + match: defaultLanguageUnique, + }, + onChange: (permitted: boolean) => { + const unique = 'UMB_preventEditInvariantFromNonDefault'; + + if (permitted) { + this.propertyReadonlyGuard.removeRule(unique); + } else { + const rule = { + unique, + permitted: false, + message: 'Shared properties can only be edited in the default language', + variantId: new UmbVariantId(), + }; + this.propertyReadonlyGuard.addRule(rule); + } + }, + }, + ]); + }); + } + override resetState(): void { super.resetState(); this.#isTrashedContext.setIsTrashed(false); @@ -205,25 +250,38 @@ export class UmbDocumentWorkspaceContext this.#isTrashedContext.setIsTrashed(response.data.isTrashed); } + await this.#setReadOnlyStateForUserPermission( + UMB_USER_PERMISSION_DOCUMENT_UPDATE, + this.#userCanUpdate, + 'You do not have permission to update documents.', + ); + return response; } async create(parent: UmbEntityModel, documentTypeUnique: string, blueprintUnique?: string) { + let preset: Partial = { + documentType: { + unique: documentTypeUnique, + collection: null, + }, + }; if (blueprintUnique) { const blueprintRepository = new UmbDocumentBlueprintDetailRepository(this); const { data } = await blueprintRepository.requestByUnique(blueprintUnique); - return this.createScaffold({ - parent, - preset: { - documentType: data?.documentType, - values: data?.values, - variants: data?.variants as Array, - }, - }); + if (!data) { + throw new Error(`Blueprint with unique ${blueprintUnique} not found`); + } + + preset = { + documentType: data.documentType, + values: data.values, + variants: data.variants as Array, + }; } - return this.createScaffold({ + const scaffold = this.createScaffold({ parent, preset: { documentType: { @@ -232,6 +290,15 @@ export class UmbDocumentWorkspaceContext }, }, }); + + // TODO: how can we be sure that this.#userCanCreate is set at this point? + await this.#setReadOnlyStateForUserPermission( + UMB_USER_PERMISSION_DOCUMENT_CREATE, + this.#userCanCreate, + 'You do not have permission to create documents.', + ); + + return scaffold; } getCollectionAlias() { @@ -390,27 +457,15 @@ export class UmbDocumentWorkspaceContext } async #setReadOnlyStateForUserPermission(identifier: string, permitted: boolean, message: string) { - const variants = this.getVariants(); - const uniques = variants?.map((variant) => identifier + variant.culture) || []; - if (permitted) { - this.readOnlyState?.removeStates(uniques); + this.readonlyGuard?.removeRule(identifier); return; } - const variantIds = variants?.map((variant) => new UmbVariantId(variant.culture, variant.segment)) || []; - - const readOnlyStates = variantIds.map((variantId) => { - return { - unique: identifier + variantId.culture, - variantId, - message, - }; + this.readonlyGuard?.addRule({ + unique: identifier, + message, }); - - this.readOnlyState?.removeStates(uniques); - - this.readOnlyState?.addStates(readOnlyStates); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts b/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts index 6b6c217dce53..b38c0489bead 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/app-language-select/app-language-select.element.ts @@ -37,6 +37,7 @@ export class UmbAppLanguageSelectElement extends UmbLitElement { #appLanguageContext?: UmbAppLanguageContext; #languagesObserver?: any; + // TODO: Here we have some read only state logic and then we have it again in the context. We should align this otherwise it will become a nightmare to maintain. [NL] #currentUserAllowedLanguages?: Array; #currentUserHasAccessToAllLanguages?: boolean; @@ -82,7 +83,7 @@ export class UmbAppLanguageSelectElement extends UmbLitElement { this._appLanguage = language; }); - this.observe(this.#appLanguageContext.appLanguageReadOnlyState.isReadOnly, (isReadOnly) => { + this.observe(this.#appLanguageContext.appLanguageReadOnlyState.isOn, (isReadOnly) => { this._appLanguageIsReadOnly = isReadOnly; }); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/constants.ts new file mode 100644 index 000000000000..ffc9908a02c2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/constants.ts @@ -0,0 +1 @@ +export const UMB_LANGUAGE_USER_PERMISSION_CONDITION_ALIAS = 'Umb.Condition.UserPermission.Language'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/language-user-permission.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/language-user-permission.condition.ts new file mode 100644 index 000000000000..24c025566167 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/language-user-permission.condition.ts @@ -0,0 +1,65 @@ +import type { UmbLanguageUserPermissionConditionConfig } from './types.js'; +import { UMB_CURRENT_USER_CONTEXT, type UmbCurrentUserModel } from '@umbraco-cms/backoffice/current-user'; +import type { UmbConditionControllerArguments, UmbExtensionCondition } from '@umbraco-cms/backoffice/extension-api'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; + +// Do not export - for internal use only +type UmbOnChangeCallbackType = (permitted: boolean) => void; + +export class UmbLanguageUserPermissionCondition extends UmbControllerBase implements UmbExtensionCondition { + config: UmbLanguageUserPermissionConditionConfig; + permitted = false; + + #onChange: UmbOnChangeCallbackType; + + constructor( + host: UmbControllerHost, + args: UmbConditionControllerArguments, + ) { + super(host); + this.config = args.config; + this.#onChange = args.onChange; + + this.consumeContext(UMB_CURRENT_USER_CONTEXT, (context) => { + this.observe( + context.currentUser, + (currentUser) => { + this.#check(currentUser); + }, + 'umbLanguageUserPermissionConditionObserver', + ); + }); + } + + #check(currentUser?: UmbCurrentUserModel) { + if (currentUser?.hasAccessToAllLanguages) { + this.permitted = true; + this.#onChange(true); + return; + } + const cultures = currentUser?.languages ?? []; + /* we default to true se we don't require both allOf and oneOf to be defined + but they can be combined for more complex scenarios */ + let allOfPermitted = true; + let oneOfPermitted = true; + + // check if all of the verbs are present + if (this.config.allOf?.length) { + allOfPermitted = this.config.allOf.every((verb) => cultures.includes(verb)); + } + + // check if at least one of the verbs is present + if (this.config.oneOf?.length) { + oneOfPermitted = this.config.oneOf.some((verb) => cultures.includes(verb)); + } + + const permitted = allOfPermitted && oneOfPermitted; + if (permitted === this.permitted) return; + + this.permitted = permitted; + this.#onChange(permitted); + } +} + +export { UmbLanguageUserPermissionCondition as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/manifests.ts new file mode 100644 index 000000000000..955d63696cba --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/manifests.ts @@ -0,0 +1,8 @@ +import { UMB_LANGUAGE_USER_PERMISSION_CONDITION_ALIAS } from './constants.js'; + +export const manifest: UmbExtensionManifest = { + type: 'condition', + name: 'Language User Permission Condition', + alias: UMB_LANGUAGE_USER_PERMISSION_CONDITION_ALIAS, + api: () => import('./language-user-permission.condition.js'), +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/types.ts b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/types.ts new file mode 100644 index 000000000000..a77459ac62e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/language-user-permission/types.ts @@ -0,0 +1,26 @@ +import type { UmbConditionConfigBase } from '@umbraco-cms/backoffice/extension-api'; + +export type UmbLanguageUserPermissionConditionConfig = + UmbConditionConfigBase<'Umb.Condition.UserPermission.Language'> & { + /** + * The user must have all of the permissions in this array for the condition to be met. + * @example + * ["en", "da"] + */ + allOf?: Array; + + /** + * The user must have at least one of the permissions in this array for the condition to be met. + * @example + * ["en", "da"] + */ + oneOf?: Array; + + match: string; + }; + +declare global { + interface UmbExtensionConditionConfigMap { + umbLanguageUserPermissionConditionConfig: UmbLanguageUserPermissionConditionConfig; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/conditions/multiple-app-languages.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/multiple-app-languages/multiple-app-languages.condition.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/language/conditions/multiple-app-languages.condition.ts rename to src/Umbraco.Web.UI.Client/src/packages/language/conditions/multiple-app-languages/multiple-app-languages.condition.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/conditions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/language/conditions/multiple-app-languages/types.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/language/conditions/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/language/conditions/multiple-app-languages/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/language/constants.ts index 206ab574d52e..f3101e65b8d4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/constants.ts @@ -1,4 +1,5 @@ export * from './collection/constants.js'; +export * from './conditions/language-user-permission/constants.js'; export * from './modals/constants.js'; export * from './repository/constants.js'; export * from './workspace/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts index 0be0edadee07..be3f908b1ffb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/global-contexts/app-language.context.ts @@ -33,6 +33,7 @@ export class UmbAppLanguageContext extends UmbContextBase public readonly appLanguage = this.#appLanguage.asObservable(); public readonly appLanguageCulture = this.#appLanguage.asObservablePart((x) => x?.unique); + // TODO: I think we should move all read only states to this context and then make a observable regarding the read only state of the app language. [NL] public readonly appLanguageReadOnlyState = new UmbReadOnlyStateManager(this); public readonly appMandatoryLanguages = this.#languages.asObservablePart((languages) => diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/language/manifests.ts index 6b5c12c08910..07b6076ab119 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/manifests.ts @@ -1,4 +1,5 @@ -import { manifest as conditionsManifest } from './conditions/multiple-app-languages.condition.js'; +import { manifest as multiLanguageConditionManifest } from './conditions/multiple-app-languages/multiple-app-languages.condition.js'; +import { manifest as userPermissionConditionManifest } from './conditions/language-user-permission/manifests.js'; import { manifests as appLanguageSelect } from './app-language-select/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as entityActions } from './entity-actions/manifests.js'; @@ -20,7 +21,8 @@ export const manifests: Array = [ ...modalManifests, ...repositoryManifests, ...workspaceManifests, - conditionsManifest, + multiLanguageConditionManifest, + userPermissionConditionManifest, { type: 'workspaceContext', name: 'Document Language Access Workspace Context', diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/permissions/language-access.workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/language/permissions/language-access.workspace.context.ts index 100a3c590e64..566460bdd0bb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/permissions/language-access.workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/permissions/language-access.workspace.context.ts @@ -57,7 +57,7 @@ export class UmbLanguageAccessWorkspaceContext extends UmbContextBase { + const readOnlyRules = variantIds.map((variantId) => { return { unique: identifier + variantId.culture, variantId, @@ -66,11 +66,12 @@ export class UmbLanguageAccessWorkspaceContext extends UmbContextBase identifier + variant.culture) || []; - this.#workspaceContext.readOnlyState?.removeStates(uniques); + this.#workspaceContext.readonlyGuard?.removeRules(uniques); // add new states - this.#workspaceContext.readOnlyState?.addStates(readOnlyStates); + this.#workspaceContext.readonlyGuard?.addRules(readOnlyRules); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/language/types.ts b/src/Umbraco.Web.UI.Client/src/packages/language/types.ts index 4f61decb2379..c3686bfe9d49 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/language/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/language/types.ts @@ -1,7 +1,8 @@ import type { UmbLanguageEntityType } from './entity.js'; export type { UmbLanguageEntityType, UmbLanguageRootEntityType } from './entity.js'; -export type * from './conditions/types.js'; +export type * from './conditions/language-user-permission/types.js'; +export type * from './conditions/multiple-app-languages/types.js'; export type * from './repository/types.js'; export interface UmbLanguageDetailModel { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts index 64c6bfb6ca06..20b73c10061d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/dropzone/dropzone-manager.class.ts @@ -1,5 +1,3 @@ -import { UmbMediaDetailRepository } from '../media/repository/index.js'; -import type { UmbMediaDetailModel, UmbMediaValueModel } from '../media/types.js'; import { UmbFileDropzoneItemStatus } from './constants.js'; import { UMB_DROPZONE_MEDIA_TYPE_PICKER_MODAL } from './modals/index.js'; import type { @@ -25,6 +23,12 @@ import type { UmbAllowedMediaTypeModel } from '@umbraco-cms/backoffice/media-typ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { + UMB_MEDIA_PROPERTY_VALUE_ENTITY_TYPE, + UmbMediaDetailRepository, + type UmbMediaDetailModel, + type UmbMediaValueModel, +} from '@umbraco-cms/backoffice/media'; /** * Manages the dropzone and uploads folders and files to the server. @@ -317,6 +321,7 @@ export class UmbDropzoneManager extends UmbControllerBase { value: { temporaryFileId: item.temporaryFile?.temporaryUnique }, culture: null, segment: null, + entityType: UMB_MEDIA_PROPERTY_VALUE_ENTITY_TYPE, }; const preset: Partial = { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/detail/media-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/detail/media-type-detail.server.data-source.ts index 7fc5078ce846..b97fa63dd737 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/detail/media-type-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/repository/detail/media-type-detail.server.data-source.ts @@ -87,6 +87,7 @@ export class UmbMediaTypeServerDataSource implements UmbDetailDataSource { return { id: property.id, + unique: property.id, container: property.container, sortOrder: property.sortOrder, alias: property.alias, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts index 7c28975d0baf..9e21ba81dae5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/constants.ts @@ -13,4 +13,9 @@ export * from './workspace/constants.js'; export * from './paths.js'; export { UMB_MEDIA_VARIANT_CONTEXT } from './property-dataset-context/media-property-dataset-context.token.js'; -export { UMB_MEDIA_ENTITY_TYPE, UMB_MEDIA_ROOT_ENTITY_TYPE, UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE } from './entity.js'; +export { + UMB_MEDIA_ENTITY_TYPE, + UMB_MEDIA_ROOT_ENTITY_TYPE, + UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE, + UMB_MEDIA_PROPERTY_VALUE_ENTITY_TYPE, +} from './entity.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity.ts index 06dc488450e5..d13b1eac8d21 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity.ts @@ -8,3 +8,7 @@ export type UmbMediaRootEntityType = typeof UMB_MEDIA_ROOT_ENTITY_TYPE; export type UmbMediaPlaceholderEntityType = typeof UMB_MEDIA_PLACEHOLDER_ENTITY_TYPE; export type UmbMediaEntityTypeUnion = UmbMediaEntityType | UmbMediaRootEntityType; + +// TODO: move this to a better location inside the media module +export const UMB_MEDIA_PROPERTY_VALUE_ENTITY_TYPE = `${UMB_MEDIA_ENTITY_TYPE}-property-value`; +export type UmbMediaPropertyValueEntityType = typeof UMB_MEDIA_PROPERTY_VALUE_ENTITY_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index fd144a61e8c2..029aae91fa72 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -56,6 +56,9 @@ export class UmbMediaWorkspaceContext this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); + this.propertyViewGuard.fallbackToPermitted(); + this.propertyWriteGuard.fallbackToPermitted(); + this.routes.setRoutes([ { path: UMB_CREATE_MEDIA_WORKSPACE_PATH_PATTERN.toString(), diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts index c78aeda13b08..c998d90cfa94 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/repository/detail/member-type-detail.server.data-source.ts @@ -87,6 +87,7 @@ export class UmbMemberTypeServerDataSource implements UmbDetailDataSource { return { id: property.id, + unique: property.id, container: property.container ? { id: property.container.id } : null, sortOrder: property.sortOrder, alias: property.alias, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/constants.ts index cc342219abe5..c8c4f1e7f2a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/constants.ts @@ -1,4 +1,8 @@ -export { UMB_MEMBER_ENTITY_TYPE, UMB_MEMBER_ROOT_ENTITY_TYPE } from './entity.js'; +export { + UMB_MEMBER_ENTITY_TYPE, + UMB_MEMBER_ROOT_ENTITY_TYPE, + UMB_MEMBER_PROPERTY_VALUE_ENTITY_TYPE, +} from './entity.js'; export { UMB_MEMBER_VARIANT_CONTEXT } from './property-dataset-context/member-property-dataset.context-token.js'; export * from './collection/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/entity.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/entity.ts index 3878b8d0d59b..ff071dabbc5a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/entity.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/entity.ts @@ -3,3 +3,7 @@ export const UMB_MEMBER_ROOT_ENTITY_TYPE = 'member-root'; export type UmbMemberEntityType = typeof UMB_MEMBER_ENTITY_TYPE; export type UmbMemberRootEntityType = typeof UMB_MEMBER_ROOT_ENTITY_TYPE; + +// TODO: move this to a better location inside the member module +export const UMB_MEMBER_PROPERTY_VALUE_ENTITY_TYPE = `${UMB_MEMBER_ENTITY_TYPE}-property-value`; +export type UmbMemberPropertyValueEntityType = typeof UMB_MEMBER_PROPERTY_VALUE_ENTITY_TYPE; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts index 30d4bddac99c..7b26ad573bf8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/detail/member-detail.server.data-source.ts @@ -1,5 +1,5 @@ import type { UmbMemberDetailModel } from '../../types.js'; -import { UMB_MEMBER_ENTITY_TYPE } from '../../entity.js'; +import { UMB_MEMBER_ENTITY_TYPE, UMB_MEMBER_PROPERTY_VALUE_ENTITY_TYPE } from '../../entity.js'; import { UmbMemberKind } from '../../utils/index.js'; import { UmbId } from '@umbraco-cms/backoffice/id'; import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository'; @@ -102,10 +102,11 @@ export class UmbMemberServerDataSource implements UmbDetailDataSource { return { - editorAlias: value.editorAlias, + alias: value.alias, culture: value.culture || null, + editorAlias: value.editorAlias, + entityType: UMB_MEMBER_PROPERTY_VALUE_ENTITY_TYPE, segment: value.segment || null, - alias: value.alias, value: value.value, }; }), diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts index 40c0643e5a92..426a039c9509 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts @@ -46,6 +46,9 @@ export class UmbMemberWorkspaceContext this.observe(this.contentTypeUnique, (unique) => this.structure.loadType(unique), null); + this.propertyViewGuard.fallbackToPermitted(); + this.propertyWriteGuard.fallbackToPermitted(); + this.routes.setRoutes([ { path: 'create/:memberTypeUnique', diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts index 55a12a7824c8..113feb2b6a3f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/repository/current-user.server.data-source.ts @@ -1,23 +1,15 @@ import type { UmbCurrentUserModel } from '../types.js'; import { UserService } from '@umbraco-cms/backoffice/external/backend-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { tryExecute, tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbManagementApiDataMapper } from '@umbraco-cms/backoffice/repository'; /** * A data source for the current user that fetches data from the server * @class UmbCurrentUserServerDataSource */ -export class UmbCurrentUserServerDataSource { - #host: UmbControllerHost; - - /** - * Creates an instance of UmbCurrentUserServerDataSource. - * @param {UmbControllerHost} host - The controller host for this controller to be appended to - * @memberof UmbCurrentUserServerDataSource - */ - constructor(host: UmbControllerHost) { - this.#host = host; - } +export class UmbCurrentUserServerDataSource extends UmbControllerBase { + #dataMapper = new UmbManagementApiDataMapper(this); /** * Get the current user @@ -25,9 +17,24 @@ export class UmbCurrentUserServerDataSource { * @memberof UmbCurrentUserServerDataSource */ async getCurrentUser() { - const { data, error } = await tryExecuteAndNotify(this.#host, UserService.getUserCurrent()); + const { data, error } = await tryExecuteAndNotify(this, UserService.getUserCurrent()); if (data) { + const permissionDataPromises = data.permissions.map(async (item) => { + return this.#dataMapper.map({ + forDataModel: item.$type, + data: item, + fallback: async () => { + return { + ...item, + permissionType: 'unknown', + }; + }, + }); + }); + + const permissions = await Promise.all(permissionDataPromises); + const user: UmbCurrentUserModel = { allowedSections: data.allowedSections, avatarUrls: data.avatarUrls, @@ -51,7 +58,7 @@ export class UmbCurrentUserServerDataSource { }; }), name: data.name, - permissions: data.permissions, + permissions, unique: data.id, userName: data.userName, userGroupUniques: data.userGroupIds.map((group) => group.id), @@ -67,7 +74,7 @@ export class UmbCurrentUserServerDataSource { * @memberof UmbCurrentUserServerDataSource */ async getExternalLoginProviders() { - return tryExecuteAndNotify(this.#host, UserService.getUserCurrentLoginProviders()); + return tryExecuteAndNotify(this, UserService.getUserCurrentLoginProviders()); } /** @@ -75,7 +82,7 @@ export class UmbCurrentUserServerDataSource { * @memberof UmbCurrentUserServerDataSource */ async getMfaLoginProviders() { - const { data, error } = await tryExecuteAndNotify(this.#host, UserService.getUserCurrent2Fa()); + const { data, error } = await tryExecuteAndNotify(this, UserService.getUserCurrent2Fa()); if (data) { return { data }; @@ -127,7 +134,7 @@ export class UmbCurrentUserServerDataSource { */ async changePassword(newPassword: string, oldPassword: string) { return tryExecuteAndNotify( - this.#host, + this, UserService.postUserCurrentChangePassword({ requestBody: { newPassword, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts index e199d687806a..4c26a67af309 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/current-user/types.ts @@ -1,8 +1,6 @@ import type { ApiError, CancelError, - DocumentPermissionPresentationModel, - UnknownTypePermissionPresentationModel, UserExternalLoginProviderModel, UserTwoFactorProviderModel, } from '@umbraco-cms/backoffice/external/backend-api'; @@ -27,7 +25,7 @@ export interface UmbCurrentUserModel { languages: Array; mediaStartNodeUniques: Array; name: string; - permissions: Array; + permissions: Array; unique: string; userName: string; userGroupUniques: string[]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/detail/user-group-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/detail/user-group-detail.server.data-source.ts index fe1e4bef9933..15d8e29e3160 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/detail/user-group-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/repository/detail/user-group-detail.server.data-source.ts @@ -6,26 +6,20 @@ import type { } from '@umbraco-cms/backoffice/external/backend-api'; import { UserGroupService } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbId } from '@umbraco-cms/backoffice/id'; -import type { UmbDetailDataSource } from '@umbraco-cms/backoffice/repository'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbManagementApiDataMapper, type UmbDetailDataSource } from '@umbraco-cms/backoffice/repository'; import { tryExecuteAndNotify } from '@umbraco-cms/backoffice/resources'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; /** * A data source for the User Group that fetches data from the server * @class UmbUserGroupServerDataSource * @implements {RepositoryDetailDataSource} */ -export class UmbUserGroupServerDataSource implements UmbDetailDataSource { - #host: UmbControllerHost; - - /** - * Creates an instance of UmbUserGroupServerDataSource. - * @param {UmbControllerHost} host - The controller host for this controller to be appended to - * @memberof UmbUserGroupServerDataSource - */ - constructor(host: UmbControllerHost) { - this.#host = host; - } +export class UmbUserGroupServerDataSource + extends UmbControllerBase + implements UmbDetailDataSource +{ + #dataMapper = new UmbManagementApiDataMapper(this); /** * Creates a new User Group scaffold @@ -65,12 +59,27 @@ export class UmbUserGroupServerDataSource implements UmbDetailDataSource { + return this.#dataMapper.map({ + forDataModel: item.$type, + data: item, + fallback: async () => { + return { + ...item, + permissionType: 'unknown', + }; + }, + }); + }); + + const permissions = await Promise.all(permissionDataPromises); + // TODO: make data mapper to prevent errors const userGroup: UmbUserGroupDetailModel = { alias: data.alias, @@ -86,7 +95,7 @@ export class UmbUserGroupServerDataSource implements UmbDetailDataSource { + return this.#dataMapper.map({ + forDataModel: item.permissionType, + data: item, + fallback: async () => item, + }); + }); + + const permissions = await Promise.all(permissionDataPromises); + // TODO: make data mapper to prevent errors const requestBody: CreateUserGroupRequestModel = { alias: model.alias, @@ -115,12 +134,12 @@ export class UmbUserGroupServerDataSource implements UmbDetailDataSource { + return this.#dataMapper.map({ + forDataModel: item.userPermissionType, + data: item, + fallback: async () => item, + }); + }); + + const permissions = await Promise.all(permissionDataPromises); + // TODO: make data mapper to prevent errors const requestBody: UpdateUserGroupRequestModel = { alias: model.alias, @@ -155,12 +184,12 @@ export class UmbUserGroupServerDataSource implements UmbDetailDataSource; @state() - private _entityTypes: Array = []; + private _groups: Array<{ entityType: string; headline: string }> = []; #userGroupWorkspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; @@ -36,7 +36,15 @@ export class UmbUserGroupEntityUserPermissionListElement extends UmbLitElement { this.observe( umbExtensionsRegistry.byType('entityUserPermission'), (manifests) => { - this._entityTypes = [...new Set(manifests.flatMap((manifest) => manifest.forEntityTypes))]; + const entityTypes = [...new Set(manifests.flatMap((manifest) => manifest.forEntityTypes))]; + this._groups = entityTypes + .map((entityType) => { + return { + entityType, + headline: this.localize.term(`user_permissionsEntityGroup_${entityType}`), + }; + }) + .sort((a, b) => a.headline.localeCompare(b.headline)); }, 'umbUserPermissionsObserver', ); @@ -51,14 +59,14 @@ export class UmbUserGroupEntityUserPermissionListElement extends UmbLitElement { } override render() { - return html` ${this._entityTypes.map((entityType) => this.#renderPermissionsByEntityType(entityType))} `; + return html` ${this._groups.map((group) => this.#renderPermissionsForEntityType(group))}`; } - #renderPermissionsByEntityType(entityType: string) { + #renderPermissionsForEntityType(group: { entityType: string; headline: string }) { return html` -

${entityType}

+

${group.headline}

`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts index c97dbf9c70fe..9ed14cd3713d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/components/user-group-granular-permission-list.element.ts @@ -80,6 +80,7 @@ export class UmbUserGroupGranularPermissionListElement extends UmbLitElement { this._userGroupPermissions.filter((permission) => permission.$type === schemaType) || []; (extension.component as any).permissions = permissionsForSchemaType; + (extension.component as any).fallbackPermissions = this._userGroupFallbackPermissions; extension.component.addEventListener(UmbChangeEvent.TYPE, this.#onValueChange); return html` diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts index 88ceb2d8fd86..3babce3f3a3f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/modals/settings/entity-user-permission-settings-modal.token.ts @@ -1,8 +1,14 @@ import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; export interface UmbEntityUserPermissionSettingsModalData { - unique: string; entityType: string; + /** + * Unique identifier for the entity + * @deprecated The unique is not used in the modal as it is not needed. It is kept for backwards compatibility. Will be removed in v17. + * @type {string} + * @memberof UmbEntityUserPermissionSettingsModalData + */ + unique?: string; headline?: string; preset?: UmbEntityUserPermissionSettingsModalValue; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/types.ts index 7f10a7e1b0bd..ffba0215efa7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-permission/types.ts @@ -2,5 +2,6 @@ export type * from './user-granular-permission.extension.js'; export type * from './entity-user-permission.extension.js'; export interface UmbUserPermissionModel { $type: string; + userPermissionType?: string; verbs: Array; } diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserGroupPresentationFactoryTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserGroupPresentationFactoryTests.cs index 3ae7769ee6ef..35490314acd4 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserGroupPresentationFactoryTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserGroupPresentationFactoryTests.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership.Permissions; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.ContentTypeEditing; using Umbraco.Cms.Core.Services.OperationStatus; @@ -23,8 +24,11 @@ namespace Umbraco.Cms.Tests.Integration.ManagementApi.Factories; public class UserGroupPresentationFactoryTests : UmbracoIntegrationTest { public IUserGroupPresentationFactory UserGroupPresentationFactory => GetRequiredService(); + public IUserGroupService UserGroupService => GetRequiredService(); + public ITemplateService TemplateService => GetRequiredService(); + public IContentTypeEditingService ContentTypeEditingService => GetRequiredService(); public IContentEditingService ContentEditingService => GetRequiredService(); @@ -33,12 +37,12 @@ protected override void ConfigureTestServices(IServiceCollection services) { services.AddTransient(); services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(x => x.GetRequiredService()); - services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } - [Test] public async Task Can_Map_Create_Model_And_Create() { @@ -49,7 +53,7 @@ public async Task Can_Map_Create_Model_And_Create() HasAccessToAllLanguages = true, Languages = new List(), Name = "Test Name", - Sections = new [] {"Umb.Section.Content"}, + Sections = new[] { "Umb.Section.Content" }, Permissions = new HashSet() }; @@ -78,7 +82,7 @@ public async Task Cannot_Create_UserGroup_With_Unexisting_Document_Reference() HasAccessToAllLanguages = true, Languages = new List(), Name = "Test Name", - Sections = new [] {"Umb.Section.Content"}, + Sections = new[] { "Umb.Section.Content" }, Permissions = new HashSet() { new DocumentPermissionPresentationModel() @@ -101,6 +105,40 @@ public async Task Cannot_Create_UserGroup_With_Unexisting_Document_Reference() }); } + [Test] + public async Task Cannot_Create_UserGroup_With_Unexisting_DocumentType_Reference() + { + var updateModel = new CreateUserGroupRequestModel() + { + Alias = "testAlias", + FallbackPermissions = new HashSet(), + HasAccessToAllLanguages = true, + Languages = new List(), + Name = "Test Name", + Sections = new[] { "Umb.Section.Content" }, + Permissions = new HashSet() + { + new DocumentPropertyValuePermissionPresentationModel() + { + DocumentType = new ReferenceByIdModel(Guid.NewGuid()), + PropertyType = new ReferenceByIdModel(Guid.NewGuid()), + Verbs = new HashSet() + } + } + }; + + var attempt = await UserGroupPresentationFactory.CreateAsync(updateModel); + Assert.IsTrue(attempt.Success); + + var userGroupCreateAttempt = await UserGroupService.CreateAsync(attempt.Result, Constants.Security.SuperUserKey); + + Assert.Multiple(() => + { + Assert.IsFalse(userGroupCreateAttempt.Success); + Assert.AreEqual(UserGroupOperationStatus.DocumentTypePermissionKeyNotFound, userGroupCreateAttempt.Status); + }); + } + [Test] public async Task Can_Create_Usergroup_With_Empty_Granular_Permissions_For_Document() { @@ -113,7 +151,7 @@ public async Task Can_Create_Usergroup_With_Empty_Granular_Permissions_For_Docum HasAccessToAllLanguages = true, Languages = new List(), Name = "Test Name", - Sections = new [] {"Umb.Section.Content"}, + Sections = new[] { "Umb.Section.Content" }, Permissions = new HashSet { new DocumentPermissionPresentationModel() @@ -140,6 +178,170 @@ public async Task Can_Create_Usergroup_With_Empty_Granular_Permissions_For_Docum }); } + [Test] + public async Task Can_Create_Usergroup_With_Granular_Permissions_For_Document_PropertyValue() + { + var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate"); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + + var contentType = (await ContentTypeEditingService.CreateAsync( + ContentTypeEditingBuilder.CreateSimpleContentType(defaultTemplateKey: template.Key), + Constants.Security.SuperUserKey)).Result!; + + var propertyType = contentType.PropertyTypes.First(); + + var updateModel = new CreateUserGroupRequestModel() + { + Alias = "testAlias", + FallbackPermissions = new HashSet(), + HasAccessToAllLanguages = true, + Languages = new List(), + Name = "Test Name", + Sections = new[] { "Umb.Section.Content" }, + Permissions = new HashSet + { + new DocumentPropertyValuePermissionPresentationModel + { + DocumentType = new ReferenceByIdModel(contentType.Key), + PropertyType = new ReferenceByIdModel(propertyType.Key), + Verbs = new HashSet(["Some", "Another"]) + } + } + }; + + var attempt = await UserGroupPresentationFactory.CreateAsync(updateModel); + Assert.IsTrue(attempt.Success); + + var userGroupCreateAttempt = await UserGroupService.CreateAsync(attempt.Result, Constants.Security.SuperUserKey); + var userGroup = userGroupCreateAttempt.Result; + + Assert.Multiple(() => + { + Assert.IsTrue(userGroupCreateAttempt.Success); + Assert.IsNotNull(userGroup); + }); + + Assert.AreEqual(2, userGroup.GranularPermissions.Count); + var documentTypeGranularPermissions = userGroup.GranularPermissions.OfType().ToArray(); + Assert.AreEqual(2, documentTypeGranularPermissions.Length); + Assert.Multiple(() => + { + Assert.IsTrue(documentTypeGranularPermissions.All(x => x.Key == contentType.Key)); + Assert.AreEqual($"{propertyType.Key}|Some", documentTypeGranularPermissions.First().Permission); + Assert.AreEqual($"{propertyType.Key}|Another", documentTypeGranularPermissions.Last().Permission); + }); + } + + [Test] + public async Task Can_Create_Usergroup_With_Granular_Permissions_For_Document_PropertyValue_Without_Verbs() + { + var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate"); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + + var contentType = (await ContentTypeEditingService.CreateAsync( + ContentTypeEditingBuilder.CreateSimpleContentType(defaultTemplateKey: template.Key), + Constants.Security.SuperUserKey)).Result!; + + var propertyType = contentType.PropertyTypes.First(); + + var updateModel = new CreateUserGroupRequestModel() + { + Alias = "testAlias", + FallbackPermissions = new HashSet(), + HasAccessToAllLanguages = true, + Languages = new List(), + Name = "Test Name", + Sections = new[] { "Umb.Section.Content" }, + Permissions = new HashSet + { + new DocumentPropertyValuePermissionPresentationModel + { + DocumentType = new ReferenceByIdModel(contentType.Key), + PropertyType = new ReferenceByIdModel(propertyType.Key), + Verbs = new HashSet() + } + } + }; + + var attempt = await UserGroupPresentationFactory.CreateAsync(updateModel); + Assert.IsTrue(attempt.Success); + + var userGroupCreateAttempt = await UserGroupService.CreateAsync(attempt.Result, Constants.Security.SuperUserKey); + var userGroup = userGroupCreateAttempt.Result; + + Assert.Multiple(() => + { + Assert.IsTrue(userGroupCreateAttempt.Success); + Assert.IsNotNull(userGroup); + }); + + Assert.AreEqual(1, userGroup.GranularPermissions.Count); + var documentTypeGranularPermissions = userGroup.GranularPermissions.OfType().ToArray(); + Assert.AreEqual(1, documentTypeGranularPermissions.Length); + Assert.Multiple(() => + { + Assert.IsTrue(documentTypeGranularPermissions.All(x => x.Key == contentType.Key)); + Assert.AreEqual($"{propertyType.Key}|", documentTypeGranularPermissions.First().Permission); + }); + } + + [Test] + public async Task Usergroup_Granular_Permissions_For_Document_PropertyValue_Are_Cleaned_Up_When_DocumentType_Is_Deleted() + { + var template = TemplateBuilder.CreateTextPageTemplate("defaultTemplate"); + await TemplateService.CreateAsync(template, Constants.Security.SuperUserKey); + + var contentType1 = (await ContentTypeEditingService.CreateAsync( + ContentTypeEditingBuilder.CreateSimpleContentType(defaultTemplateKey: template.Key), + Constants.Security.SuperUserKey)).Result!; + + var contentType2 = (await ContentTypeEditingService.CreateAsync( + ContentTypeEditingBuilder.CreateSimpleContentType(alias: "anotherAlias", defaultTemplateKey: template.Key), + Constants.Security.SuperUserKey)).Result!; + + var propertyType1 = contentType1.PropertyTypes.First(); + var propertyType2 = contentType2.PropertyTypes.First(); + + var updateModel = new CreateUserGroupRequestModel() + { + Alias = "testAlias", + FallbackPermissions = new HashSet(), + HasAccessToAllLanguages = true, + Languages = new List(), + Name = "Test Name", + Sections = new[] { "Umb.Section.Content" }, + Permissions = new HashSet + { + new DocumentPropertyValuePermissionPresentationModel + { + DocumentType = new ReferenceByIdModel(contentType1.Key), + PropertyType = new ReferenceByIdModel(propertyType1.Key), + Verbs = new HashSet(["Some", "Another"]) + }, + new DocumentPropertyValuePermissionPresentationModel + { + DocumentType = new ReferenceByIdModel(contentType2.Key), + PropertyType = new ReferenceByIdModel(propertyType2.Key), + Verbs = new HashSet(["Even", "More"]) + } + } + }; + + var attempt = await UserGroupPresentationFactory.CreateAsync(updateModel); + + var userGroupCreateAttempt = await UserGroupService.CreateAsync(attempt.Result, Constants.Security.SuperUserKey); + Assert.IsTrue(userGroupCreateAttempt.Success); + Assert.AreEqual(4, userGroupCreateAttempt.Result!.GranularPermissions.Count); + + var deleteResult = await GetRequiredService().DeleteAsync(contentType1.Key, Constants.Security.SuperUserKey); + Assert.AreEqual(ContentTypeOperationStatus.Success, deleteResult); + + var userGroup = await UserGroupService.GetAsync(userGroupCreateAttempt.Result!.Key); + Assert.IsNotNull(userGroup); + + Assert.AreEqual(2, userGroup.GranularPermissions.Count); + } + private async Task CreateContent() { // NOTE Maybe not the best way to create/save test data as we are using the services, which are being tested. @@ -151,7 +353,8 @@ private async Task CreateContent() Assert.IsTrue(contentTypeAttempt.Success); var contentTypeResult = contentTypeAttempt.Result; - var contentTypeUpdateModel = ContentTypeUpdateHelper.CreateContentTypeUpdateModel(contentTypeResult); contentTypeUpdateModel.AllowedContentTypes = new[] + var contentTypeUpdateModel = ContentTypeUpdateHelper.CreateContentTypeUpdateModel(contentTypeResult); + contentTypeUpdateModel.AllowedContentTypes = new[] { new ContentTypeSort(contentTypeResult.Key, 0, contentTypeCreateModel.Alias), };