diff --git a/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs index b040d6334c80..e4fea6347097 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/UserGroupPresentationFactory.cs @@ -54,6 +54,7 @@ public async Task CreateAsync(IUserGroup userGroup) { Id = userGroup.Key, Name = userGroup.Name ?? string.Empty, + Description = userGroup.Description ?? string.Empty, Alias = userGroup.Alias, DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey), DocumentRootAccess = contentRootAccess, @@ -87,6 +88,7 @@ public async Task CreateAsync(IReadOnlyUserGroup userGro { Id = userGroup.Key, Name = userGroup.Name ?? string.Empty, + Description = userGroup.Description ?? string.Empty, Alias = userGroup.Alias, DocumentStartNode = ReferenceByIdModel.ReferenceOrNull(contentStartNodeKey), MediaStartNode = ReferenceByIdModel.ReferenceOrNull(mediaStartNodeKey), @@ -132,6 +134,7 @@ public async Task> CreateAsync(Cre { Name = CleanUserGroupNameOrAliasForXss(requestModel.Name), Alias = CleanUserGroupNameOrAliasForXss(requestModel.Alias), + Description = requestModel.Description, Icon = requestModel.Icon, HasAccessToAllLanguages = requestModel.HasAccessToAllLanguages, Permissions = requestModel.FallbackPermissions, @@ -197,9 +200,10 @@ public async Task> UpdateAsync(IUs current.Name = CleanUserGroupNameOrAliasForXss(request.Name); current.Alias = CleanUserGroupNameOrAliasForXss(request.Alias); + current.Description = request.Description; current.Icon = request.Icon; current.HasAccessToAllLanguages = request.HasAccessToAllLanguages; - + current.Permissions = request.FallbackPermissions; current.GranularPermissions = await _permissionPresentationFactory.CreatePermissionSetsAsync(request.Permissions); diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs index d9f16953596c..a8d46405ee7c 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; +using Umbraco.Cms.Api.Management.ViewModels.UserGroup.Permissions; namespace Umbraco.Cms.Api.Management.ViewModels.UserGroup; @@ -22,6 +22,11 @@ public class UserGroupBase /// public required string Alias { get; init; } + /// + /// The description of the user group + /// + public string? Description { get; set; } + /// /// The Icon for the user group /// diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index fa2110cbcfe5..0baf42845906 100644 --- a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -9,6 +9,10 @@ public interface IReadOnlyUserGroup { string? Name { get; } + string Alias { get; } + + string? Description { get { return null; } } + string? Icon { get; } int Id { get; } @@ -19,11 +23,6 @@ public interface IReadOnlyUserGroup int? StartMediaId { get; } - /// - /// The alias - /// - string Alias { get; } - // This is set to return true as default to avoid breaking changes. bool HasAccessToAllLanguages => true; diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index c4adc5999bfb..8ea7ecb036b5 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -22,6 +22,14 @@ public interface IUserGroup : IEntity, IRememberBeingDirty /// string? Name { get; set; } + /// + /// The description + /// + string? Description { + get => null; + set { } + } + /// /// If this property is true it will give the group access to all languages /// diff --git a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs index 91bd6daca57b..c962e14e4ac6 100644 --- a/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs @@ -55,6 +55,8 @@ public bool Equals(ReadOnlyUserGroup? other) public string Name { get; } + public string? Description { get; } + public string? Icon { get; } public int? StartContentId { get; } diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs index 95f8dee7b854..430303519bb6 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs @@ -30,6 +30,7 @@ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup private string _alias; private string? _icon; private string _name; + private string? _description; private bool _hasAccessToAllLanguages; private ISet _permissions; private ISet _granularPermissions; @@ -111,6 +112,13 @@ public string? Name set => SetPropertyValueAndDetectChanges(value, ref _name!, nameof(Name)); } + [DataMember] + public string? Description + { + get => _description; + set => SetPropertyValueAndDetectChanges(value, ref _description!, nameof(Description)); + } + [DataMember] public bool HasAccessToAllLanguages { diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 3dc5f2e7a5c7..5b074a0a83cc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -144,6 +144,9 @@ protected virtual void DefinePlan() // To 17.0.1 To("{BE5CA411-E12D-4455-A59E-F12A669E5363}"); + // To 17.1.0 + To("{F1A2B3C4-D5E6-4789-ABCD-1234567890AB}"); + // To 18.0.0 // TODO (V18): Enable on 18 branch //// To("{74332C49-B279-4945-8943-F8F00B1F5949}"); diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs new file mode 100644 index 000000000000..f378ff0c7a04 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs @@ -0,0 +1,43 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_1_0 +{ + /// + /// Migration to add a description column to the user group table. + /// + public class AddDescriptionToUserGroup : AsyncMigrationBase + { + /// + /// Initializes a new instance of the class. + /// + /// The migration context. + public AddDescriptionToUserGroup( + IMigrationContext context) + : base(context) + { + } + + /// + protected override async Task MigrateAsync() + { + if (TableExists(Constants.DatabaseSchema.Tables.UserGroup) is false) + { + return; + } + + const string ColumnName = "description"; + var hasColumn = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database) + .Any(c => + c.TableName == Constants.DatabaseSchema.Tables.UserGroup && + c.ColumnName == ColumnName); + + if (hasColumn) + { + return; + } + + AddColumn(Constants.DatabaseSchema.Tables.UserGroup, ColumnName); + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs index 591b495db6ab..049c3fc940a9 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs @@ -40,6 +40,11 @@ public UserGroupDto() [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoUserGroup_userGroupName")] public string? Name { get; set; } + [Column(Name = "description")] + [SpecialDbType(SpecialDbTypes.NVARCHARMAX)] + [NullSetting(NullSetting = NullSettings.Null)] + public string? Description { get; set; } + [Column("userGroupDefaultPermissions")] [Length(50)] [NullSetting(NullSetting = NullSettings.Null)] diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs index cf12fe463008..4d715c4ed840 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/UserGroupFactory.cs @@ -30,6 +30,7 @@ public static IUserGroup BuildEntity(IShortStringHelper shortStringHelper, UserG userGroup.StartMediaId = dto.StartMediaId; userGroup.Permissions = dto.UserGroup2PermissionDtos.Select(x => x.Permission).ToHashSet(); userGroup.HasAccessToAllLanguages = dto.HasAccessToAllLanguages; + userGroup.Description = dto.Description; if (dto.UserGroup2AppDtos != null) { foreach (UserGroup2AppDto app in dto.UserGroup2AppDtos) @@ -84,6 +85,7 @@ public static UserGroupDto BuildDto(IUserGroup entity) Key = entity.Key, Alias = entity.Alias, Name = entity.Name, + Description = entity.Description, UserGroup2AppDtos = new List(), CreateDate = entity.CreateDate, UpdateDate = entity.UpdateDate, diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs index 885981453e44..e0f27eb81357 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/UserGroupMapper.cs @@ -25,5 +25,6 @@ protected override void DefineMaps() DefineMap(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon)); DefineMap(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId)); DefineMap(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId)); + DefineMap(nameof(UserGroup.Description), nameof(UserGroupDto.Description)); } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs index 97d2e60a40b2..2a5e31d50f2c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserGroupRepository.cs @@ -481,6 +481,7 @@ private static void AppendGroupBy(Sql sql) => x => x.UpdateDate, x => x.Alias, x => x.Name, + x => x.Description, x => x.HasAccessToAllLanguages, x => x.Key, x => x.DefaultPermissions) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts index 988582df2228..73ce46fc4601 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/user-group/user-group.data.ts @@ -27,6 +27,7 @@ export const data: Array = [ id: 'user-group-administrators-id', name: 'Administrators', alias: 'admin', + description: 'Administrators have full access to all settings and features within the CMS.', icon: 'icon-medal', fallbackPermissions: [ UMB_USER_PERMISSION_DOCUMENT_READ, @@ -91,6 +92,7 @@ export const data: Array = [ id: 'user-group-editors-id', name: 'Editors', alias: 'editors', + description: 'The Editors group is responsible for creating, updating, and managing Content and Media within the platform. While they do not have access to system-level areas such as Settings, Users, or Packages, they play a crucial role in maintaining the website’s daily content operations. Editors start from the Media root node when handling media files, ensuring they can upload, modify, and organize assets relevant to their work.', icon: 'icon-tools', documentStartNode: { id: 'all-property-editors-document-id' }, fallbackPermissions: [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts index 07582d290f76..4fdcf6684471 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/backend-api/types.gen.ts @@ -419,6 +419,7 @@ export type CreateUserGroupRequestModel = { fallbackPermissions: Array; permissions: Array; id?: string | null; + description?: string | null; }; export type CreateUserRequestModel = { @@ -2810,6 +2811,7 @@ export type UpdateUserDataRequestModel = { export type UpdateUserGroupRequestModel = { name: string; + description?: string | null; alias: string; icon?: string | null; sections: Array; @@ -2925,6 +2927,7 @@ export type UserGroupResponseModel = { id: string; isDeletable: boolean; aliasCanBeChanged: boolean; + description?: string | null; }; export type UserInstallRequestModel = { diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/components/user-group-table-description-column-layout.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/components/user-group-table-description-column-layout.element.ts new file mode 100644 index 000000000000..e3cf4ec4ba61 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/components/user-group-table-description-column-layout.element.ts @@ -0,0 +1,42 @@ +import { css, html, LitElement, customElement, property } from '@umbraco-cms/backoffice/external/lit'; +import type { UmbTableItem } from '@umbraco-cms/backoffice/components'; + +@customElement('umb-user-group-table-description-column-layout') +export class UmbUserGroupTableDescriptionColumnLayoutElement extends LitElement { + @property({ type: Object, attribute: false }) + item!: UmbTableItem; + + @property({ attribute: false }) + value!: string; + + override render() { + if (!this.value) { + return html``; + } + return html`${this.value}`; + } + + static override styles = [ + css` + :host { + display: block; + max-width: 300px; + } + + span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + `, + ]; +} + +export default UmbUserGroupTableDescriptionColumnLayoutElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-user-group-table-description-column-layout': UmbUserGroupTableDescriptionColumnLayoutElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/repository/user-group-collection.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/repository/user-group-collection.server.data-source.ts index 3dae6945f13f..7252cc060892 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/repository/user-group-collection.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/repository/user-group-collection.server.data-source.ts @@ -45,6 +45,7 @@ export class UmbUserGroupCollectionServerDataSource implements UmbCollectionData mediaRootAccess: item.mediaRootAccess, mediaStartNode: item.mediaStartNode ? { unique: item.mediaStartNode.id } : null, name: item.name, + description: item.description || null, permissions: item.permissions, sections: item.sections, unique: item.id, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts index f66c0e73f803..3bc905f4148e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/views/user-group-table-collection-view.element.ts @@ -16,6 +16,7 @@ import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; import type { UmbUniqueItemModel } from '@umbraco-cms/backoffice/models'; import '../components/user-group-table-name-column-layout.element.js'; +import '../components/user-group-table-description-column-layout.element.js'; import '../components/user-group-table-sections-column-layout.element.js'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; @@ -33,6 +34,11 @@ export class UmbUserGroupCollectionTableViewElement extends UmbLitElement { alias: 'userGroupName', elementName: 'umb-user-group-table-name-column-layout', }, + { + name: this.localize.term('general_description'), + alias: 'description', + elementName: 'umb-user-group-table-description-column-layout', + }, { name: this.localize.term('main_sections'), alias: 'userGroupSections', @@ -133,6 +139,10 @@ export class UmbUserGroupCollectionTableViewElement extends UmbLitElement { name: userGroup.name, }, }, + { + columnAlias: 'description', + value: userGroup.description ?? '', + }, { columnAlias: 'userGroupSections', value: userGroup.sections, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/user-group-ref/user-group-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/user-group-ref/user-group-ref.element.ts index 0dfb70b6fddf..b54638201af0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/user-group-ref/user-group-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/components/user-group-ref/user-group-ref.element.ts @@ -36,6 +36,9 @@ export class UmbUserGroupRefElement extends UmbElementMixin(UUIRefNodeElement) { this.#observeMediaStartNode(value); } + @property({ type: String }) + description: string | null = null; + @property({ type: Array }) public get sections(): Array { return []; @@ -151,17 +154,19 @@ export class UmbUserGroupRefElement extends UmbElementMixin(UUIRefNodeElement) { } #renderDetails() { - const hasSections = this._sectionLabels.length; - const hasDocument = !!this._documentLabel || this.documentRootAccess; - const hasMedia = !!this._mediaLabel || this.mediaRootAccess; - const hasUserPermissions = this._userPermissionLabels.length; - - if (!hasSections && !hasDocument && !hasMedia && !hasUserPermissions) return; - return html`
- ${this.#renderSections()} ${this.#renderDocumentStartNode()} ${this.#renderMediaStartNode()} - ${this.#renderUserPermissions()} + ${this.#renderDescription()} ${this.#renderSections()} ${this.#renderDocumentStartNode()} + ${this.#renderMediaStartNode()} ${this.#renderUserPermissions()} +
+ `; + } + + #renderDescription() { + if (!this.description) return; + return html` +
+ ${this.description}
`; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts index 5c25b7ac61c3..f5a89206395c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/modals/user-group-picker/user-group-picker-modal.element.ts @@ -114,6 +114,7 @@ export class UmbUserGroupPickerModalElement extends UmbModalBaseElement< ?mediaRootAccess=${userGroup.mediaRootAccess} .mediaStartNode=${!userGroup.mediaRootAccess ? userGroup.mediaStartNode?.unique : null} .sections=${userGroup.sections} + .description=${userGroup.description} @selected=${(event: UUIMenuItemEvent) => this.#onSelected(event, userGroup)} @deselected=${(event: UUIMenuItemEvent) => this.#onDeselected(event, userGroup)}> ${when(userGroup.icon, () => html``)} 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 0eecb446c5a6..76b1a20b7f8e 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 @@ -45,6 +45,7 @@ export class UmbUserGroupServerDataSource permissions: [], sections: [], unique: UmbId.new(), + description: '', }; return { data }; @@ -98,6 +99,7 @@ export class UmbUserGroupServerDataSource permissions, sections: data.sections, unique: data.id, + description: data.description ?? null, }; return { data: userGroup }; @@ -136,6 +138,7 @@ export class UmbUserGroupServerDataSource name: model.name, permissions, sections: model.sections, + description: model.description, }; const { data, error } = await tryExecute( @@ -186,6 +189,7 @@ export class UmbUserGroupServerDataSource name: model.name, permissions, sections: model.sections, + description: model.description, }; const { error } = await tryExecute( diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/types.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/types.ts index fa45aa1c7789..6f27c2f369b7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/types.ts @@ -19,4 +19,5 @@ export interface UmbUserGroupDetailModel { permissions: Array; sections: Array; unique: string; + description: string | null; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts index 08aeea40244f..59e2ff3d6bd3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace-editor.element.ts @@ -3,6 +3,7 @@ import type { UmbUserGroupDetailModel } from '../../types.js'; import { UMB_USER_GROUP_WORKSPACE_CONTEXT } from './user-group-workspace.context-token.js'; import type { UmbInputWithAliasElement } from '@umbraco-cms/backoffice/components'; import { css, html, customElement, state, ifDefined } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIInputElement } from '@umbraco-cms/backoffice/external/uui'; import { UMB_ICON_PICKER_MODAL } from '@umbraco-cms/backoffice/icon'; import { umbFocus, UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; @@ -25,6 +26,9 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { @state() private _icon?: UmbUserGroupDetailModel['icon']; + @state() + private _description?: UmbUserGroupDetailModel['description']; + #workspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE; constructor() { @@ -36,6 +40,11 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { }); } + #onDescriptionChange(event: InputEvent & { target: UUIInputElement }) { + const value = event.target.value.toString(); + this.#workspaceContext?.setDescription(value); + } + #observeUserGroup() { this.observe(this.#workspaceContext?.isNew, (value) => (this._isNew = value), '_observeIsNew'); this.observe(this.#workspaceContext?.name, (value) => (this._name = value), '_observeName'); @@ -46,6 +55,11 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { '_observeAliasCanBeChanged', ); this.observe(this.#workspaceContext?.icon, (value) => (this._icon = value), '_observeIcon'); + this.observe( + this.#workspaceContext?.description, + (value) => (this._description = value || ''), + '_observeDescription', + ); } #onNameAndAliasChange(event: InputEvent & { target: UmbInputWithAliasElement }) { @@ -86,16 +100,25 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { - - +
+ + + + +
`; } @@ -117,9 +140,11 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { } #icon { - font-size: var(--uui-size-5); - height: 30px; - width: 30px; + font-size: var(--uui-size-8); + height: 60px; + width: 60px; + --uui-button-border-color: transparent; + --uui-button-border-color-hover: var(--uui-color-border); } #name { @@ -127,6 +152,21 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { flex: 1 1 auto; align-items: center; } + + #editors { + width: 100%; + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: 1px; + } + + #description { + width: 100%; + margin-top: -1px; + --uui-input-height: var(--uui-size-8); + --uui-input-border-color: transparent; + } `, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts index 1923edb9d01d..52303de88ef2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts @@ -29,6 +29,7 @@ export class UmbUserGroupWorkspaceContext readonly mediaRootAccess = this._data.createObservablePartOfCurrent((data) => data?.mediaRootAccess || false); readonly fallbackPermissions = this._data.createObservablePartOfCurrent((data) => data?.fallbackPermissions || []); readonly permissions = this._data.createObservablePartOfCurrent((data) => data?.permissions || []); + readonly description = this._data.createObservablePartOfCurrent((data) => data?.description || ''); constructor(host: UmbControllerHost) { super(host, { @@ -99,6 +100,15 @@ export class UmbUserGroupWorkspaceContext setFallbackPermissions(fallbackPermissions: Array) { this._data.updateCurrent({ fallbackPermissions }); } + + /** + * Sets the description + * @param {string} description - The description + * @memberof UmbUserGroupWorkspaceContext + */ + setDescription(description: string) { + this._data.updateCurrent({ description }); + } } export { UmbUserGroupWorkspaceContext as api }; diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/UserGroup/CreateUserGroupControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/UserGroup/CreateUserGroupControllerTests.cs index 884bde730c34..73448b98a59c 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/UserGroup/CreateUserGroupControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/UserGroup/CreateUserGroupControllerTests.cs @@ -47,6 +47,7 @@ protected override async Task ClientRequest() { Name = "CreatedTestGroup", Alias = "testAlias", + Description = "Test group description", FallbackPermissions = new HashSet(), HasAccessToAllLanguages = true, Languages = [], diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/UserGroup/UpdateUserGroupControllerTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/UserGroup/UpdateUserGroupControllerTests.cs index 3e3fbebb7f9f..180ed956dcd1 100644 --- a/tests/Umbraco.Tests.Integration/ManagementApi/UserGroup/UpdateUserGroupControllerTests.cs +++ b/tests/Umbraco.Tests.Integration/ManagementApi/UserGroup/UpdateUserGroupControllerTests.cs @@ -63,6 +63,7 @@ protected override async Task ClientRequest() { Name = "UpdatedTestGroup", Alias = "testAlias", + Description = "Updated test group description", FallbackPermissions = new HashSet(), HasAccessToAllLanguages = true, Languages = [], diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceTests.cs similarity index 62% rename from tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs rename to tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceTests.cs index 9d9f2e997ebf..cd4396609dcd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceTests.cs @@ -11,14 +11,93 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services; [TestFixture] [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] -internal sealed class UserGroupServiceValidationTests : UmbracoIntegrationTest +internal sealed class UserGroupServiceTests : UmbracoIntegrationTest { private IUserGroupService UserGroupService => GetRequiredService(); private IShortStringHelper ShortStringHelper => GetRequiredService(); [Test] - public async Task Cannot_create_user_group_with_name_equals_null() + public async Task Can_Create_User_Group() + { + var allowedSections = new[] { "content", "media", "settings" }; + var userGroup = new UserGroup(ShortStringHelper) + { + Name = "Some Name", + Alias = "someAlias", + Description = "This is a test user group description", + Icon = "icon-users", + HasAccessToAllLanguages = true, + Permissions = new HashSet { "A", "B", "C" } + }; + + foreach (var allowedSection in allowedSections) + { + userGroup.AddAllowedSection(allowedSection); + } + + var result = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + var createdUserGroup = await UserGroupService.GetAsync(result.Result.Key); + + Assert.IsTrue(result.Success); + Assert.IsNotNull(createdUserGroup); + Assert.AreEqual(userGroup.Name, createdUserGroup.Name); + Assert.AreEqual(userGroup.Alias, createdUserGroup.Alias); + Assert.AreEqual(userGroup.Description, createdUserGroup.Description); + Assert.AreEqual(userGroup.Icon, createdUserGroup.Icon); + Assert.AreEqual(userGroup.HasAccessToAllLanguages, createdUserGroup.HasAccessToAllLanguages); + CollectionAssert.AreEquivalent(userGroup.Permissions, createdUserGroup.Permissions); + CollectionAssert.AreEquivalent(userGroup.AllowedSections, createdUserGroup.AllowedSections); + } + + [Test] + public async Task Can_Update_User_Group() + { + var allowedSections = new[] { "content", "media", "settings" }; + var userGroup = new UserGroup(ShortStringHelper) + { + Name = "Some Name", + Alias = "someAlias", + Description = "This is a test user group description", + Icon = "icon-users", + HasAccessToAllLanguages = true, + Permissions = new HashSet { "A", "B", "C" } + }; + + foreach (var allowedSection in allowedSections) + { + userGroup.AddAllowedSection(allowedSection); + } + + var createResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + var createdUserGroup = await UserGroupService.GetAsync(createResult.Result.Key); + + Assert.IsTrue(createResult.Success); + createdUserGroup.Name = "Updated Name"; + createdUserGroup.Alias = "updatedAlias"; + createdUserGroup.Description = "Updated description"; + createdUserGroup.Icon = "icon-user"; + createdUserGroup.HasAccessToAllLanguages = false; + createdUserGroup.Permissions = new HashSet { "X", "Y", "Z" }; + createdUserGroup.ClearAllowedSections(); + createdUserGroup.AddAllowedSection("users"); + + var updateResult = await UserGroupService.UpdateAsync(createdUserGroup, Constants.Security.SuperUserKey); + var updatedUserGroup = await UserGroupService.GetAsync(updateResult.Result.Key); + + Assert.IsTrue(updateResult.Success); + Assert.IsNotNull(updatedUserGroup); + Assert.AreEqual(createdUserGroup.Name, updatedUserGroup.Name); + Assert.AreEqual(createdUserGroup.Alias, updatedUserGroup.Alias); + Assert.AreEqual(createdUserGroup.Description, updatedUserGroup.Description); + Assert.AreEqual(createdUserGroup.Icon, updatedUserGroup.Icon); + Assert.AreEqual(createdUserGroup.HasAccessToAllLanguages, updatedUserGroup.HasAccessToAllLanguages); + CollectionAssert.AreEquivalent(createdUserGroup.Permissions, updatedUserGroup.Permissions); + CollectionAssert.AreEquivalent(createdUserGroup.AllowedSections, updatedUserGroup.AllowedSections); + } + + [Test] + public async Task Cannot_Create_User_Group_With_Name_Equals_Null() { var userGroup = new UserGroup(ShortStringHelper) { @@ -32,7 +111,7 @@ public async Task Cannot_create_user_group_with_name_equals_null() } [Test] - public async Task Cannot_create_user_group_with_name_longer_than_max_length() + public async Task Cannot_Create_User_Group_With_Name_Longer_Than_Max_Length() { var userGroup = new UserGroup(ShortStringHelper) { @@ -46,7 +125,7 @@ public async Task Cannot_create_user_group_with_name_longer_than_max_length() } [Test] - public async Task Cannot_create_user_group_with_alias_longer_than_max_length() + public async Task Cannot_Create_User_Group_With_Alias_Longer_Than_Max_Length() { var userGroup = new UserGroup(ShortStringHelper) { @@ -61,7 +140,7 @@ public async Task Cannot_create_user_group_with_alias_longer_than_max_length() } [Test] - public async Task Cannot_update_non_existing_user_group() + public async Task Cannot_Update_Non_Existing_User_Group() { var userGroup = new UserGroup(ShortStringHelper) { @@ -76,7 +155,7 @@ public async Task Cannot_update_non_existing_user_group() } [Test] - public async Task Cannot_create_existing_user_group() + public async Task Cannot_Create_Existing_User_Group() { var userGroup = new UserGroup(ShortStringHelper) { @@ -95,7 +174,7 @@ public async Task Cannot_create_existing_user_group() } [Test] - public async Task Cannot_create_user_group_with_duplicate_alias() + public async Task Cannot_Create_User_Group_With_Duplicate_Alias() { var alias = "duplicateAlias"; @@ -119,7 +198,7 @@ public async Task Cannot_create_user_group_with_duplicate_alias() } [Test] - public async Task Cannot_update_user_group_with_duplicate_alias() + public async Task Cannot_Update_User_Group_With_Duplicate_Alias() { var alias = "duplicateAlias"; @@ -150,7 +229,7 @@ public async Task Cannot_update_user_group_with_duplicate_alias() } [Test] - public async Task Can_Update_UserGroup_To_New_Name() + public async Task Can_Update_User_Group_To_New_Name() { var userGroup = new UserGroup(ShortStringHelper) { @@ -172,7 +251,7 @@ public async Task Can_Update_UserGroup_To_New_Name() [TestCase(Constants.Security.AdminGroupKeyString, "admin")] [TestCase(Constants.Security.SensitiveDataGroupKeyString, "sensitiveData")] [TestCase(Constants.Security.TranslatorGroupString, "translator")] - public async Task Cannot_Delete_System_UserGroups(string userGroupKeyAsString, string expectedGroupAlias) + public async Task Cannot_Delete_System_User_Group(string userGroupKeyAsString, string expectedGroupAlias) { // since we can't use the constants as input, let's make sure we don't get false positives by double checking the group alias var key = Guid.Parse(userGroupKeyAsString); @@ -188,7 +267,7 @@ public async Task Cannot_Delete_System_UserGroups(string userGroupKeyAsString, s [TestCase( Constants.Security.EditorGroupKeyString, "editor")] [TestCase(Constants.Security.WriterGroupKeyString, "writer")] - public async Task Can_Delete_Non_System_UserGroups(string userGroupKeyAsString, string expectedGroupAlias) + public async Task Can_Delete_Non_System_User_Group(string userGroupKeyAsString, string expectedGroupAlias) { // since we can't use the constants as input, let's make sure we don't get false positives by double checking the group alias var key = Guid.Parse(userGroupKeyAsString);