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 44a69ccc957e..a23b167f26b0 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -36136,6 +36136,9 @@ { "$ref": "#/components/schemas/DocumentPermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -36424,6 +36427,9 @@ { "$ref": "#/components/schemas/DocumentPermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -37289,6 +37295,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", @@ -45715,6 +45763,9 @@ { "$ref": "#/components/schemas/DocumentPermissionPresentationModel" }, + { + "$ref": "#/components/schemas/DocumentPropertyValuePermissionPresentationModel" + }, { "$ref": "#/components/schemas/UnknownTypePermissionPresentationModel" } @@ -46138,6 +46189,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/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 e610a10f357b..19c69ab3de30 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2050,9 +2050,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 a1a97aa80d41..897d65c5c048 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; }; @@ -679,6 +679,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; @@ -2673,7 +2680,7 @@ export type UpdateUserGroupRequestModel = { mediaStartNode?: ((ReferenceByIdModel) | null); mediaRootAccess: boolean; fallbackPermissions: Array<(string)>; - permissions: Array<(DocumentPermissionPresentationModel | UnknownTypePermissionPresentationModel)>; + permissions: Array<(DocumentPermissionPresentationModel | DocumentPropertyValuePermissionPresentationModel | UnknownTypePermissionPresentationModel)>; }; export type UpdateUserGroupsOnUserRequestModel = { @@ -2773,7 +2780,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/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-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/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..6debd6e580fd 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 @@ -5,8 +5,10 @@ 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 type { UmbVariantPropertyViewState, UmbVariantPropertyWriteState } from '@umbraco-cms/backoffice/property'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { UmbDataPathPropertyValueQuery } from '@umbraco-cms/backoffice/validation'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; @customElement('umb-block-workspace-view-edit-properties') export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { @@ -40,6 +42,18 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { @state() private _ownerEntityType?: string; + @state() + _propertyViewStateIsRunning = true; + + @state() + _propertyViewStates: Array = []; + + @state() + _propertyWriteStateIsRunning = true; + + @state() + _propertyWriteStates: Array = []; + #variantId?: UmbVariantId; constructor() { @@ -62,7 +76,10 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { #setStructureManager() { if (!this.#blockWorkspace || !this.#managerName) return; - this.#propertyStructureHelper.setStructureManager(this.#blockWorkspace[this.#managerName].structure); + + const structureManager = this.#blockWorkspace[this.#managerName].structure; + + this.#propertyStructureHelper.setStructureManager(structureManager); this.observe( this.#propertyStructureHelper.propertyStructure, (propertyStructure) => { @@ -71,6 +88,24 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { }, 'observePropertyStructure', ); + + this.observe( + observeMultiple([structureManager.propertyViewState.isRunning, structureManager.propertyViewState.states]), + ([isRunning, states]) => { + this._propertyViewStateIsRunning = isRunning; + this._propertyViewStates = states; + }, + 'umbObservePropertyViewStates', + ); + + this.observe( + observeMultiple([structureManager.propertyWriteState.isRunning, structureManager.propertyWriteState.states]), + ([isEnabled, states]) => { + this._propertyWriteStateIsRunning = isEnabled; + this._propertyWriteStates = states; + }, + 'umbObservePropertyWriteStates', + ); } /* @@ -92,9 +127,44 @@ export class UmbBlockWorkspaceViewEditPropertiesElement extends UmbLitElement { ); } + #getVisibleProperties() { + return this._propertyStructure?.filter((property) => this.#isViewablePropertyType(property)) || []; + } + + #isViewablePropertyType(property: UmbPropertyTypeModel) { + // The state is not running, so the property is viewable by default. + if (this._propertyViewStateIsRunning === false) { + return true; + } + + const propertyVariantId = this.#getPropertyVariantId(property); + return this._propertyViewStates.some( + (state) => state.propertyType.unique === property.unique && state.propertyType.variantId.equal(propertyVariantId), + ); + } + + #isWritablePropertyType(property: UmbPropertyTypeModel) { + // The state is not running, so the property is writable by default. + if (this._propertyWriteStateIsRunning === false) { + return true; + } + + const propertyVariantId = this.#getPropertyVariantId(property); + return this._propertyWriteStates.some( + (state) => state.propertyType.unique === property.unique && state.propertyType.variantId.equal(propertyVariantId), + ); + } + + #getPropertyVariantId(property: UmbPropertyTypeModel) { + return new UmbVariantId( + property.variesByCulture ? this.#variantId!.culture : null, + property.variesBySegment ? this.#variantId!.segment : null, + ); + } + override render() { return repeat( - this._propertyStructure, + this.#getVisibleProperties(), (property) => property.alias, (property, index) => html``, ); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts index fee663e92577..df515cf37415 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/structure/content-type-structure-manager.class.ts @@ -19,6 +19,11 @@ import { incrementString } from '@umbraco-cms/backoffice/utils'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry, type ManifestRepository } from '@umbraco-cms/backoffice/extension-registry'; +import { + UmbVariantPropertyReadOnlyStateManager, + UmbVariantPropertyViewStateManager, + UmbVariantPropertyWriteStateManager, +} from '@umbraco-cms/backoffice/property'; type UmbPropertyTypeId = UmbPropertyTypeModel['id']; @@ -98,6 +103,10 @@ export class UmbContentTypeStructureManager< readonly variesByCulture = createObservablePart(this.ownerContentType, (x) => x?.variesByCulture); readonly variesBySegment = createObservablePart(this.ownerContentType, (x) => x?.variesBySegment); + public readonly propertyViewState = new UmbVariantPropertyViewStateManager(this); + public readonly propertyWriteState = new UmbVariantPropertyWriteStateManager(this); + public readonly propertyReadOnlyState = new UmbVariantPropertyReadOnlyStateManager(this); + #containers: UmbArrayState = new UmbArrayState( [], (x) => x.id, @@ -809,6 +818,9 @@ export class UmbContentTypeStructureManager< public override destroy() { this.#contentTypes.destroy(); this.#containers.destroy(); + this.propertyViewState.destroy(); + this.propertyWriteState.destroy(); + this.propertyReadOnlyState.destroy(); super.destroy(); } } 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/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 da8ccb7b1413..533a046cd893 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 @@ -504,7 +504,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) { 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..ea8f593b8871 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 @@ -9,8 +9,10 @@ import { UmbContentTypePropertyStructureHelper } from '@umbraco-cms/backoffice/c 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 type { UmbVariantPropertyViewState, UmbVariantPropertyWriteState } from '@umbraco-cms/backoffice/property'; import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; @customElement('umb-content-workspace-view-edit-properties') export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement { @@ -31,6 +33,21 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement @state() _dataPaths?: Array; + @state() + _propertyViewStateIsRunning = true; + + @state() + _propertyViewStates: Array = []; + + @state() + _propertyWriteStateIsRunning = true; + + @state() + _propertyWriteStates: Array = []; + + @state() + _propertyReadOnlyStates: Array = []; + constructor() { super(); @@ -39,11 +56,43 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement // Assuming its the same content model type that we are working with here... [NL] workspaceContext.structure as unknown as UmbContentTypeStructureManager, ); + + this.observe( + observeMultiple([ + workspaceContext.structure.propertyViewState.isRunning, + workspaceContext.structure.propertyViewState.states, + ]), + ([isRunning, states]) => { + this._propertyViewStateIsRunning = isRunning; + this._propertyViewStates = states; + }, + 'umbObservePropertyViewStates', + ); + + this.observe( + observeMultiple([ + workspaceContext.structure.propertyWriteState.isRunning, + workspaceContext.structure.propertyWriteState.states, + ]), + ([isEnabled, states]) => { + this._propertyWriteStateIsRunning = isEnabled; + this._propertyWriteStates = states; + }, + 'umbObservePropertyWriteStates', + ); + + this.observe( + workspaceContext.structure.propertyReadOnlyState.states, + (states) => (this._propertyReadOnlyStates = states), + 'umbObservePropertyWriteStates', + ); }); + this.consumeContext(UMB_PROPERTY_DATASET_CONTEXT, (datasetContext) => { this.#variantId = datasetContext.getVariantId(); this.#generatePropertyDataPath(); }); + this.observe( this.#propertyStructureHelper.propertyStructure, (propertyStructure) => { @@ -66,16 +115,63 @@ export class UmbContentWorkspaceViewEditPropertiesElement extends UmbLitElement ); } + #getVisibleProperties() { + return this._propertyStructure?.filter((property) => this.#isViewablePropertyType(property)) || []; + } + + #isViewablePropertyType(property: UmbPropertyTypeModel) { + // The state is not running, so the property is viewable by default. + if (this._propertyViewStateIsRunning === false) { + return true; + } + + const propertyVariantId = this.#getPropertyVariantId(property); + return this._propertyViewStates.some( + (state) => state.propertyType.unique === property.unique && state.propertyType.variantId.equal(propertyVariantId), + ); + } + + #isWritablePropertyType(property: UmbPropertyTypeModel) { + // The state is not running, so the property is writable by default. + if (this._propertyWriteStateIsRunning === false) { + return true; + } + + const propertyVariantId = this.#getPropertyVariantId(property); + + // Check if the property is writable + const isWriteAllowed = this._propertyWriteStates.some( + (state) => state.propertyType.unique === property.unique && state.propertyType.variantId.equal(propertyVariantId), + ); + + // Check if the property is read only + const isReadOnly = this._propertyReadOnlyStates.some( + (state) => state.propertyType.unique === property.unique && state.propertyType.variantId.equal(propertyVariantId), + ); + + // If the property has a read only state it will override the write state + // and the property will always be read only + return isWriteAllowed && !isReadOnly; + } + + #getPropertyVariantId(property: UmbPropertyTypeModel) { + return new UmbVariantId( + property.variesByCulture ? this.#variantId!.culture : null, + property.variesBySegment ? this.#variantId!.segment : null, + ); + } + override render() { return this._propertyStructure && this._dataPaths ? repeat( - this._propertyStructure, + this.#getVisibleProperties(), (property) => property.alias, (property, index) => html` `, + .property=${property} + ?readonly=${!this.#isWritablePropertyType(property)}>`, ) : ''; } 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/components/property/property.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/components/property/property.context.ts index 1a2608931b6d..76d17d0de4fa 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.isReadOnly; /** * Set the property editor UI element for this property. @@ -160,7 +160,16 @@ export class UmbPropertyContext extends UmbContextBase { - this.#isReadOnly.setValue(value); + const unique = 'UMB_DATASET'; + + if (value) { + this.readonlyState.addState({ + unique, + message: '', + }); + } else { + this.readonlyState.removeState(unique); + } }); } @@ -334,7 +343,7 @@ export class UmbPropertyContext extends UmbContextBase extends UmbContextBase extends UmbReadOnlyStateManager {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-view-state.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-view-state.manager.ts new file mode 100644 index 000000000000..bb19e358fa97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-view-state.manager.ts @@ -0,0 +1,33 @@ +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import { UmbStateManager, type UmbState } from '@umbraco-cms/backoffice/utils'; + +export interface UmbPropertyViewState extends UmbState { + propertyType: UmbReferenceByUnique; +} + +export class UmbPropertyViewStateManager< + ViewStateType extends UmbPropertyViewState = UmbPropertyViewState, +> extends UmbStateManager { + constructor(host: UmbControllerHost) { + super(host); + // To avoid breaking changes in rendering this state is stopped by default. This means that properties are viewable by default. + // We start this state in workspaces where we want to control the viewability of properties. + this.stop(); + } + + /** + * Get the viewable state + * @returns {Observable} True if the property is viewable + * @memberof UmbPropertyViewStateManager + */ + public readonly isViewable = this.isOn; + + /** + * Get the property viewable state + * @returns {boolean} True if the property is viewable + */ + public getIsViewable(): boolean { + return this.getIsOn(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/property-write-state.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-write-state.manager.ts new file mode 100644 index 000000000000..75cf2c047f7b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/property-write-state.manager.ts @@ -0,0 +1,33 @@ +import { UmbStateManager, type UmbState } from '@umbraco-cms/backoffice/utils'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +export interface UmbPropertyWriteState extends UmbState { + propertyType: UmbReferenceByUnique; +} + +export class UmbPropertyWriteStateManager< + WriteStateType extends UmbPropertyWriteState = UmbPropertyWriteState, +> extends UmbStateManager { + constructor(host: UmbControllerHost) { + super(host); + // To avoid breaking changes in rendering this state is stopped by default. This means that properties are viewable by default. + // We start this state in workspaces where we want to control the writability of properties. + this.stop(); + } + + /** + * Get the writable state + * @returns {Observable} True if the property is writable + * @memberof UmbPropertyWriteStateManager + */ + public readonly isWritable = this.isOn; + + /** + * Get the property writable state + * @returns {boolean} True if the property is writable + */ + public getIsWritable(): boolean { + return this.getIsOn(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-read-only-state.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-read-only-state.manager.ts new file mode 100644 index 000000000000..e92b1a498840 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-read-only-state.manager.ts @@ -0,0 +1,9 @@ +import { UmbPropertyReadOnlyStateManager, type UmbPropertyReadOnlyState } from './property-read-only-state.manager.js'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import type { UmbReferenceByVariantId } from '@umbraco-cms/backoffice/variant'; + +export interface UmbVariantPropertyReadOnlyState extends UmbPropertyReadOnlyState { + propertyType: UmbReferenceByUnique & UmbReferenceByVariantId; +} + +export class UmbVariantPropertyReadOnlyStateManager extends UmbPropertyReadOnlyStateManager {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-view-state.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-view-state.manager.ts new file mode 100644 index 000000000000..ec38d7904d28 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-view-state.manager.ts @@ -0,0 +1,9 @@ +import { UmbPropertyViewStateManager, type UmbPropertyViewState } from './property-view-state.manager.js'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import type { UmbReferenceByVariantId } from '@umbraco-cms/backoffice/variant'; + +export interface UmbVariantPropertyViewState extends UmbPropertyViewState { + propertyType: UmbReferenceByUnique & UmbReferenceByVariantId; +} + +export class UmbVariantPropertyViewStateManager extends UmbPropertyViewStateManager {} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-write-state.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-write-state.manager.ts new file mode 100644 index 000000000000..a6aaa1320d44 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/property/variant-property-write-state.manager.ts @@ -0,0 +1,9 @@ +import { UmbPropertyWriteStateManager, type UmbPropertyWriteState } from './property-write-state.manager.js'; +import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; +import type { UmbReferenceByVariantId } from '@umbraco-cms/backoffice/variant'; + +export interface UmbVariantPropertyWriteState extends UmbPropertyWriteState { + propertyType: UmbReferenceByUnique & UmbReferenceByVariantId; +} + +export class UmbVariantPropertyWriteStateManager extends UmbPropertyWriteStateManager {} 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 extends UmbStateManager { readonly isReadOnly = this.isOn; + + /** + * Get the read only state + * @returns {boolean} True if the state is read only + */ + getIsReadOnly(): boolean { + return this.getIsOn(); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.test.ts index 5f86281b339f..ce7e62e25d29 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.test.ts @@ -33,6 +33,14 @@ describe('UmbSelectionManager', () => { }); describe('methods', () => { + it('has a start method', () => { + expect(manager).to.have.property('start').that.is.a('function'); + }); + + it('has a stop method', () => { + expect(manager).to.have.property('stop').that.is.a('function'); + }); + it('has a addState method', () => { expect(manager).to.have.property('addState').that.is.a('function'); }); @@ -52,6 +60,14 @@ describe('UmbSelectionManager', () => { it('has a clear method', () => { expect(manager).to.have.property('clear').that.is.a('function'); }); + + it('has a getIsOn method', () => { + expect(manager).to.have.property('getIsOn').that.is.a('function'); + }); + + it('has a getIsOff method', () => { + expect(manager).to.have.property('getIsOff').that.is.a('function'); + }); }); }); @@ -81,6 +97,11 @@ describe('UmbSelectionManager', () => { }) .unsubscribe(); }); + + it('throws an error if the state manager is not running', () => { + manager.stop(); + expect(() => manager.addState(state1)).to.throw(); + }); }); describe('Remove State', () => { @@ -138,4 +159,41 @@ describe('UmbSelectionManager', () => { .unsubscribe(); }); }); + + describe('getIsOn', () => { + it('returns true if there are states', () => { + manager.addState(state1); + expect(manager.getIsOn()).to.be.true; + }); + + it('returns false if there are no states', () => { + expect(manager.getIsOn()).to.be.false; + }); + }); + + describe('getIsOff', () => { + it('returns true if there are no states', () => { + expect(manager.getIsOff()).to.be.true; + }); + + it('returns false if there are states', () => { + manager.addState(state1); + expect(manager.getIsOff()).to.be.false; + }); + }); + + describe('start', () => { + it('starts the state manager', () => { + manager.stop(); + manager.start(); + expect(manager.getIsRunning()).to.be.true; + }); + }); + + describe('stop', () => { + it('stops the state manager', () => { + manager.stop(); + expect(manager.getIsRunning()).to.be.false; + }); + }); }); 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..430829f5dd35 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 @@ -1,6 +1,5 @@ import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbArrayState, UmbBooleanState } from '@umbraco-cms/backoffice/observable-api'; export interface UmbState { unique: string; @@ -13,27 +12,46 @@ export class UmbStateManager extends UmbC * @memberof UmbStateManager */ protected _states = new UmbArrayState([], (x) => x.unique); - public states = this._states.asObservable(); + public readonly states = this._states.asObservable(); + + protected _isRunning = new UmbBooleanState(true); + public readonly isRunning = this._isRunning.asObservable(); /** * Observable that emits true if there are any states in the state manager * @memberof UmbStateManager */ - public isOn = this._states.asObservablePart((x) => x.length > 0); + public readonly isOn = this._states.asObservablePart((x) => x.length > 0); /** * Observable that emits true if there are no states in the state manager * @memberof UmbStateManager */ - public isOff = this._states.asObservablePart((x) => x.length === 0); + public readonly isOff = this._states.asObservablePart((x) => x.length === 0); + + /** + * Start the state - this will allow the state to be used. + */ + public start() { + this._states.unmute(); + this._isRunning.setValue(true); + } + + /** + * Stop the state - this will prevent the state from being used + */ + public stop() { + this._states.mute(); + this._isRunning.setValue(false); + } /** - * Creates an instance of UmbStateManager. - * @param {UmbControllerHost} host + * Get whether the state is running + * @returns {boolean} True if the state is running * @memberof UmbStateManager */ - constructor(host: UmbControllerHost) { - super(host); + public getIsRunning(): boolean { + return this._isRunning.getValue(); } /** @@ -42,6 +60,10 @@ export class UmbStateManager extends UmbC * @memberof UmbStateManager */ addState(state: StateType) { + if (this.getIsRunning() === false) { + throw new Error('State manager is not running. Call start() before adding states'); + } + if (!state) throw new Error('State must be defined'); if (!state.unique) throw new Error('State must have a unique property'); if (this._states.getValue().find((x) => x.unique === state.unique)) { throw new Error('State with unique already exists'); @@ -55,12 +77,16 @@ export class UmbStateManager extends UmbC * @memberof UmbStateManager */ addStates(states: StateType[]) { + if (this.getIsRunning() === false) { + throw new Error('State manager is not running. Call start() before adding states'); + } + states.forEach((state) => this.addState(state)); } /** * Remove a state from the state manager - * @param {StateType['unique']} unique + * @param {StateType['unique']} unique Unique value of the state to remove * @memberof UmbStateManager */ removeState(unique: StateType['unique']) { @@ -69,7 +95,7 @@ export class UmbStateManager extends UmbC /** * Remove multiple states from the state manager - * @param {StateType['unique'][]} uniques + * @param {StateType['unique'][]} uniques Array of unique values to remove * @memberof UmbStateManager */ removeStates(uniques: StateType['unique'][]) { @@ -93,8 +119,27 @@ export class UmbStateManager extends UmbC this._states.setValue([]); } + /** + * Get if there are any states in the state manager + * @returns {boolean} True if there are any states in the state manager + * @memberof UmbStateManager + */ + getIsOn(): boolean { + return this.getStates().length > 0; + } + + /** + * Get if there are no states in the state manager + * @returns {boolean} True if there are no states in the state manager + * @memberof UmbStateManager + */ + getIsOff(): boolean { + return this.getStates().length === 0; + } + override destroy() { - super.destroy(); this._states.destroy(); + this._isRunning.destroy(); + super.destroy(); } } 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/variant/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts index 80444415e000..8cd6eb5f1d35 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/variant/types.ts @@ -48,6 +48,10 @@ export interface UmbEntityVariantPublishModel { schedule?: ScheduleRequestModel | null; } +export interface UmbReferenceByVariantId { + variantId: UmbVariantId; +} + /** @deprecated use `UmbEntityVariantPublishModel` instead */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface UmbVariantPublishModel extends UmbEntityVariantPublishModel {} 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/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/constants.ts index 1b4981f65ab4..ca74d4d4000e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/constants.ts @@ -1,7 +1,7 @@ -export * from './paths.js'; export * from './entity-actions/constants.js'; -export * from './search/constants.js'; +export * from './paths.js'; export * from './repository/constants.js'; +export * from './search/constants.js'; export * from './tree/constants.js'; export * from './workspace/constants.js'; 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..ffdf955aaee1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/block/document-block.workspace-context.ts @@ -0,0 +1,32 @@ +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 documentWorkspaceContext = await this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, () => {}) + .passContextAliasMatches() + .asPromise(); + + if (context && documentWorkspaceContext) { + // Start the states for blocks inside documents to allow for property value permissions + context.content.structure.propertyViewState.start(); + context.content.structure.propertyWriteState.start(); + } + }); + } +} + +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/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/property-dataset-context/document-property-dataset.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset.context.ts index 5b34ca61e1cd..0da536938ca9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/property-dataset-context/document-property-dataset.context.ts @@ -1,9 +1,65 @@ -import type { UmbDocumentDetailModel, UmbDocumentVariantModel } from '../types.js'; +import type { UmbDocumentDetailModel, UmbDocumentVariantModel, UmbDocumentWorkspaceContext } from '../types.js'; +import { UMB_DOCUMENT_CONFIGURATION_CONTEXT } from '../global-contexts/index.js'; import { UmbContentPropertyDatasetContext } from '@umbraco-cms/backoffice/content'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { UmbVariantId, type UmbVariantPropertyReadOnlyState } from '@umbraco-cms/backoffice/variant'; +import type { DocumentConfigurationResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; export class UmbDocumentPropertyDatasetContext extends UmbContentPropertyDatasetContext< UmbDocumentDetailModel, UmbDocumentTypeDetailModel, UmbDocumentVariantModel -> {} +> { + #dataSetVariantId?: UmbVariantId; + #documentConfiguration?: DocumentConfigurationResponseModel; + + constructor(host: UmbControllerHost, dataOwner: UmbDocumentWorkspaceContext, variantId?: UmbVariantId) { + super(host, dataOwner, variantId); + + this.#dataSetVariantId = variantId; + + this.consumeContext(UMB_DOCUMENT_CONFIGURATION_CONTEXT, async (context) => { + this.#documentConfiguration = (await context?.getDocumentConfiguration()) ?? undefined; + + if (this.#documentConfiguration?.allowEditInvariantFromNonDefault !== true) { + this.#preventEditInvariantFromNonDefault(); + } + }); + } + + #preventEditInvariantFromNonDefault() { + this.observe( + observeMultiple([this._dataOwner.structure.contentTypeProperties, this._dataOwner.variantOptions]), + ([properties, variantOptions]) => { + if (properties.length === 0) return; + if (variantOptions.length === 0) return; + + const currentVariantOption = variantOptions.find( + (option) => option.culture === this.#dataSetVariantId?.culture, + ); + const isDefaultLanguage = currentVariantOption?.language.isDefault; + + properties.forEach((property) => { + const unique = 'UMB_PREVENT_EDIT_INVARIANT_FROM_NON_DEFAULT_' + property.unique; + + this._dataOwner.structure.propertyReadOnlyState.removeState(unique); + + if (!property.variesByCulture && !isDefaultLanguage) { + const state: UmbVariantPropertyReadOnlyState = { + unique, + message: 'Shared properties can only be edited in the default language', + propertyType: { + unique: property.unique, + variantId: new UmbVariantId(), + }, + }; + + this._dataOwner.structure.propertyReadOnlyState.addState(state); + } + }); + }, + ); + } +} 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 142e5393a5ad..a636510a0b95 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 add5a266fa1a..2e54d41e2b25 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, @@ -137,6 +137,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/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..42007dd0d74f --- /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,53 @@ +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../../workspace/constants.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { UMB_BLOCK_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/block'; +import { UMB_PROPERTY_DATASET_CONTEXT } from '@umbraco-cms/backoffice/property'; +import { UmbPropertyValueUserPermissionWorkspaceContextBase } from './property-value-user-permission-workspace-context-base.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 documentWorkspaceContext = await this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, () => {}) + .passContextAliasMatches() + .asPromise(); + + if (documentWorkspaceContext) { + this.#observeDocumentBlockProperties(); + } + }); + } + + async #observeDocumentBlockProperties() { + if (!this.#blockWorkspaceContext) return; + const datasetContext = await this.getContext(UMB_PROPERTY_DATASET_CONTEXT); + if (!datasetContext) return; + + const structureManager = this.#blockWorkspaceContext.content.structure; + + this.observe( + observeMultiple([structureManager.contentTypeProperties, this.#blockWorkspaceContext.variantId]), + ([properties, variantId]) => { + if (properties.length === 0) return; + if (!variantId) return; + + this._setPermissions( + properties, + [variantId], + structureManager.propertyViewState, + structureManager.propertyWriteState, + ); + }, + ); + } +} + +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..83f587a2ee7c --- /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,46 @@ +import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '../../../workspace/constants.js'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +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 structureManager = this.#documentWorkspaceContext.structure; + + this.observe( + observeMultiple([ + this.#documentWorkspaceContext.structure.contentTypeProperties, + this.#documentWorkspaceContext.variantOptions, + ]), + ([properties, variantOptions]) => { + if (properties.length === 0) return; + if (variantOptions.length === 0) return; + + const variantIds = variantOptions?.map((variant) => new UmbVariantId(variant.culture, variant.segment)); + + this._setPermissions( + properties, + variantIds, + structureManager.propertyViewState, + structureManager.propertyWriteState, + ); + }, + ); + } +} + +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..42975972764b --- /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,86 @@ +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 { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import type { UmbVariantPropertyViewState, UmbVariantPropertyWriteState } from '@umbraco-cms/backoffice/property'; +import type { UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import type { UmbStateManager } from '@umbraco-cms/backoffice/utils'; + +export class UmbPropertyValueUserPermissionWorkspaceContextBase extends UmbControllerBase { + protected _setPermissions( + properties: Array, + variantIds: Array, + propertyVisibilityState: UmbStateManager, + propertyWriteState: UmbStateManager, + ) { + properties.forEach((property) => { + this.#setPermissionForProperty({ + verb: UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_READ, + stateManager: propertyVisibilityState, + property, + variantIds, + }); + + this.#setPermissionForProperty({ + verb: UMB_USER_PERMISSION_DOCUMENT_PROPERTY_VALUE_WRITE, + stateManager: propertyWriteState, + property, + variantIds, + }); + }); + } + + #setPermissionForProperty(args: { + verb: string; + stateManager: UmbStateManager; + property: UmbPropertyTypeModel; + variantIds: Array; + }) { + // We can only apply states if the state manager is running + this.observe(args.stateManager.isRunning, (isRunning) => { + if (!isRunning) return; + + createExtensionApiByAlias(this, UMB_DOCUMENT_PROPERTY_VALUE_USER_PERMISSION_CONDITION_ALIAS, [ + { + config: { + allOf: [args.verb], + match: { + propertyType: { + unique: args.property.unique, + }, + }, + }, + onChange: (permitted: boolean) => { + // If the property is invariant we only need one state for the property + const isInvariant = args.property.variesByCulture === false && args.property.variesBySegment === false; + const variantIds = isInvariant ? [new UmbVariantId()] : args.variantIds; + + const states: Array = + variantIds?.map((variantId) => { + return { + unique: 'UMB_PROPERTY_' + args.property.unique + '_' + variantId.toString(), + message: '', + propertyType: { + unique: args.property.unique, + variantId, + }, + }; + }) || []; + + if (permitted) { + args.stateManager.addStates(states); + } else { + args.stateManager.removeStates(states.map((state) => state.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.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/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 2134817942f8..7a04577c087e 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 @@ -17,6 +17,7 @@ import { import { UmbDocumentPreviewRepository } from '../repository/preview/index.js'; import { UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT, UmbDocumentPublishingRepository } from '../publishing/index.js'; import { UmbDocumentValidationRepository } from '../repository/validation/index.js'; +import {} from '../user-permissions/document-property-value/constants.js'; import { UMB_DOCUMENT_DETAIL_MODEL_VARIANT_SCAFFOLD, UMB_DOCUMENT_WORKSPACE_ALIAS } from './constants.js'; import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import { UMB_INVARIANT_CULTURE, UmbVariantId } from '@umbraco-cms/backoffice/variant'; @@ -86,6 +87,11 @@ export class UmbDocumentWorkspaceContext saveModalToken: UMB_DOCUMENT_SAVE_MODAL, }); + /* Start the property view and write states for the document workspace. This means that the properties are not viewable or writable by default + but requires an entry in the state to be able to view or write to the properties. */ + this.structure.propertyViewState.start(); + this.structure.propertyWriteState.start(); + 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. @@ -181,6 +187,9 @@ export class UmbDocumentWorkspaceContext override resetState(): void { super.resetState(); this.#isTrashedContext.setIsTrashed(false); + this.structure.propertyViewState.clear(); + this.structure.propertyWriteState.clear(); + this.structure.propertyReadOnlyState.clear(); } override async load(unique: string) { 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/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts index 49a80defb64c..be8ede9dc42e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/manifests.ts @@ -1,18 +1,19 @@ import { manifests as entityActionsManifests } from './entity-actions/manifests.js'; import { manifests as menuManifests } from './menu/manifests.js'; +import { manifests as propertyEditorUiManifests } from './property-editors/manifests.js'; import { manifests as repositoryManifests } from './repository/manifests.js'; +import { manifests as searchManifests } from './search/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; -import { manifests as propertyEditorUiManifests } from './property-editors/manifests.js'; -import { manifests as searchManifests } from './search/manifests.js'; + import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ ...entityActionsManifests, ...menuManifests, + ...propertyEditorUiManifests, ...repositoryManifests, + ...searchManifests, ...treeManifests, ...workspaceManifests, - ...propertyEditorUiManifests, - ...searchManifests, ]; 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/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/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 8a279222763a..a91092efda32 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserGroupPresentationFactoryTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Factories/UserGroupPresentationFactoryTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Mapping.Permissions; @@ -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() { @@ -102,7 +106,41 @@ public async Task Cannot_Create_UserGroup_With_Unexisting_Document_Reference() } [Test] - public async Task Can_Create_Usergroup_With_Empty_Granluar_Permissions_For_Document() + 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() { var contentKey = await CreateContent(); @@ -140,6 +178,170 @@ public async Task Can_Create_Usergroup_With_Empty_Granluar_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.