From 29edc5885f42c9073643bc52721977bfb997620f Mon Sep 17 00:00:00 2001 From: Lan Nguyen Thuy Date: Thu, 4 Dec 2025 15:16:52 +0700 Subject: [PATCH 1/9] Adding description to user groups --- .../packages/core/backend-api/types.gen.ts | 3 ++ .../link-picker-modal.element.ts | 3 -- ...ser-group-collection.server.data-source.ts | 1 + .../user-group-ref/user-group-ref.element.ts | 23 ++++---- .../user-group-picker-modal.element.ts | 1 + .../user-group-detail.server.data-source.ts | 4 ++ .../src/packages/user/user-group/types.ts | 1 + .../user-group-workspace-editor.element.ts | 54 +++++++++++++++---- .../user-group-workspace.context.ts | 10 ++++ 9 files changed, 78 insertions(+), 22 deletions(-) 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/multi-url-picker/link-picker-modal/link-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts index e747db814aa5..c79644a0bcd8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts @@ -193,7 +193,6 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement { 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..ad727813d4d1 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 { - - +
+ + + + +
`; } @@ -127,6 +150,17 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { flex: 1 1 auto; align-items: center; } + + #editors { + width: 100%; + } + + #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 }; From edc9287bb7b82e58ba2f82f750580c82aa83e6e9 Mon Sep 17 00:00:00 2001 From: Lan Nguyen Thuy Date: Thu, 4 Dec 2025 16:00:55 +0700 Subject: [PATCH 2/9] Add description to dto and test and umbraco plan --- .../Factories/UserGroupPresentationFactory.cs | 6 ++- .../ViewModels/UserGroup/UserGroupBase.cs | 7 ++- .../Models/Membership/IReadOnlyUserGroup.cs | 4 +- .../Models/Membership/IUserGroup.cs | 5 ++ .../Models/Membership/ReadOnlyUserGroup.cs | 2 + .../Models/Membership/UserGroup.cs | 8 +++ .../Migrations/Upgrade/UmbracoPlan.cs | 1 + .../V_17_0_1/AddDescriptionToUserGroup.cs | 49 +++++++++++++++++++ .../Persistence/Dtos/UserGroupDto.cs | 5 ++ .../Persistence/Factories/UserGroupFactory.cs | 2 + .../Persistence/Mappers/UserGroupMapper.cs | 1 + .../Implement/UserGroupRepository.cs | 1 + .../CreateUserGroupControllerTests.cs | 1 + .../UpdateUserGroupControllerTests.cs | 1 + .../UserGroupServiceValidationTests.cs | 41 ++++++++++++++++ 15 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_1/AddDescriptionToUserGroup.cs 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..090ae82328fc 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; @@ -79,4 +79,9 @@ public class UserGroupBase /// public required ISet FallbackPermissions { get; init; } public required ISet Permissions { get; init; } + + /// + /// The description of the user group + /// + public string? Description { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index fa2110cbcfe5..b5e6afe0598d 100644 --- a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -36,4 +36,6 @@ public interface IReadOnlyUserGroup IEnumerable AllowedLanguages => Enumerable.Empty(); public bool HasAccessToLanguage( int languageId) => HasAccessToAllLanguages || AllowedLanguages.Contains(languageId); -} + + string? Description { get; } + } diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index c4adc5999bfb..1174b6a92552 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -22,6 +22,11 @@ public interface IUserGroup : IEntity, IRememberBeingDirty /// string? Name { get; set; } + /// + /// The description + /// + string? Description { get; 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..b23da5d6514e 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -143,6 +143,7 @@ protected virtual void DefinePlan() // To 17.0.1 To("{BE5CA411-E12D-4455-A59E-F12A669E5363}"); + To("{D3C5E3C4-1F4C-4D1A-8E2C-1C8D8E2F5B6A}"); // To 18.0.0 // TODO (V18): Enable on 18 branch diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_1/AddDescriptionToUserGroup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_1/AddDescriptionToUserGroup.cs new file mode 100644 index 000000000000..e46e2a551735 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_1/AddDescriptionToUserGroup.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_1 +{ + /// + /// 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. + /// The data type service. + /// The TinyMce to Tiptap migration settings. + public AddDescriptionToUserGroup( + IMigrationContext context, + IDataTypeService dataTypeService, + IOptions options) + : base(context) + { + } + + /// + protected override async Task MigrateAsync() + { + if (TableExists(Constants.DatabaseSchema.Tables.UserGroup)) + { + var columns = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumn(columns, "description"); + } + return; + } + + private void AddColumn(List columns, string column) + { + if (columns + .SingleOrDefault(x => x.TableName == Constants.DatabaseSchema.Tables.UserGroup && x.ColumnName == column) is null) + { + AddColumn(Constants.DatabaseSchema.Tables.UserGroup, column); + } + } + } +} 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/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/UserGroupServiceValidationTests.cs index 9d9f2e997ebf..d789f8f18487 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/UserGroupServiceValidationTests.cs @@ -201,4 +201,45 @@ public async Task Can_Delete_Non_System_UserGroups(string userGroupKeyAsString, Assert.IsTrue(result.Success); Assert.AreEqual(result.Result,UserGroupOperationStatus.Success); } + + [Test] + public async Task Can_Create_UserGroup_With_Description() + { + var description = "This is a test user group description"; + var userGroup = new UserGroup(ShortStringHelper) + { + Name = "Some Name", + Alias = "someAlias", + Description = description + }; + + var result = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Result); + Assert.AreEqual(description, result.Result.Description); + } + + [Test] + public async Task Can_Update_UserGroup_Description() + { + var initialDescription = "Initial description"; + var userGroup = new UserGroup(ShortStringHelper) + { + Name = "Some Name", + Alias = "someAlias", + Description = initialDescription + }; + + var createResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); + Assert.IsTrue(createResult.Success); + + var updatedDescription = "Updated description"; + var createdUserGroup = createResult.Result; + createdUserGroup!.Description = updatedDescription; + + var updateResult = await UserGroupService.UpdateAsync(createdUserGroup, Constants.Security.SuperUserKey); + Assert.IsTrue(updateResult.Success); + Assert.AreEqual(updatedDescription, updateResult.Result!.Description); + } } From 971e747822acb387a30b256faa8468db2b9e9575 Mon Sep 17 00:00:00 2001 From: Lan Nguyen Thuy Date: Fri, 5 Dec 2025 16:21:57 +0700 Subject: [PATCH 3/9] update unit test for user group --- .../ViewModels/UserGroup/UserGroupBase.cs | 10 +- .../Models/Membership/IReadOnlyUserGroup.cs | 13 +- .../Models/Membership/IUserGroup.cs | 5 +- .../Migrations/Upgrade/UmbracoPlan.cs | 4 +- .../V_17_0_1/AddDescriptionToUserGroup.cs | 49 ------ .../V_17_1_0/AddDescriptionToUserGroup.cs | 40 +++++ .../link-picker-modal.element.ts | 3 + ...ationTests.cs => UserGroupServiceTests.cs} | 142 +++++++++++------- 8 files changed, 150 insertions(+), 116 deletions(-) delete mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_1/AddDescriptionToUserGroup.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs rename tests/Umbraco.Tests.Integration/Umbraco.Core/Services/{UserGroupServiceValidationTests.cs => UserGroupServiceTests.cs} (68%) diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs index 090ae82328fc..a8d46405ee7c 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/UserGroup/UserGroupBase.cs @@ -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 /// @@ -79,9 +84,4 @@ public class UserGroupBase /// public required ISet FallbackPermissions { get; init; } public required ISet Permissions { get; init; } - - /// - /// The description of the user group - /// - public string? Description { get; set; } } diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index b5e6afe0598d..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; @@ -36,6 +35,4 @@ public interface IReadOnlyUserGroup IEnumerable AllowedLanguages => Enumerable.Empty(); public bool HasAccessToLanguage( int languageId) => HasAccessToAllLanguages || AllowedLanguages.Contains(languageId); - - string? Description { get; } - } +} diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index 1174b6a92552..8ea7ecb036b5 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -25,7 +25,10 @@ public interface IUserGroup : IEntity, IRememberBeingDirty /// /// The description /// - string? Description { get; set; } + string? Description { + get => null; + set { } + } /// /// If this property is true it will give the group access to all languages diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index b23da5d6514e..5b074a0a83cc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -143,7 +143,9 @@ protected virtual void DefinePlan() // To 17.0.1 To("{BE5CA411-E12D-4455-A59E-F12A669E5363}"); - To("{D3C5E3C4-1F4C-4D1A-8E2C-1C8D8E2F5B6A}"); + + // To 17.1.0 + To("{F1A2B3C4-D5E6-4789-ABCD-1234567890AB}"); // To 18.0.0 // TODO (V18): Enable on 18 branch diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_1/AddDescriptionToUserGroup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_1/AddDescriptionToUserGroup.cs deleted file mode 100644 index e46e2a551735..000000000000 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_1/AddDescriptionToUserGroup.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Microsoft.Extensions.Options; -using Umbraco.Cms.Core; -using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Infrastructure.Persistence.Dtos; - -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_1 -{ - /// - /// 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. - /// The data type service. - /// The TinyMce to Tiptap migration settings. - public AddDescriptionToUserGroup( - IMigrationContext context, - IDataTypeService dataTypeService, - IOptions options) - : base(context) - { - } - - /// - protected override async Task MigrateAsync() - { - if (TableExists(Constants.DatabaseSchema.Tables.UserGroup)) - { - var columns = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); - - AddColumn(columns, "description"); - } - return; - } - - private void AddColumn(List columns, string column) - { - if (columns - .SingleOrDefault(x => x.TableName == Constants.DatabaseSchema.Tables.UserGroup && x.ColumnName == column) is null) - { - AddColumn(Constants.DatabaseSchema.Tables.UserGroup, column); - } - } - } -} 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..77709cc59999 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs @@ -0,0 +1,40 @@ +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)) + { + return; + } + + string columnName = "description"; + var hasColumn = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database).ToList().Any(c => + c.TableName == Constants.DatabaseSchema.Tables.UserGroup && + c.ColumnName == columnName); + + if (!hasColumn) + { + AddColumn(Constants.DatabaseSchema.Tables.UserGroup, columnName); + } + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts index c79644a0bcd8..d20d0288337d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts @@ -193,6 +193,7 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement 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); @@ -201,45 +280,4 @@ public async Task Can_Delete_Non_System_UserGroups(string userGroupKeyAsString, Assert.IsTrue(result.Success); Assert.AreEqual(result.Result,UserGroupOperationStatus.Success); } - - [Test] - public async Task Can_Create_UserGroup_With_Description() - { - var description = "This is a test user group description"; - var userGroup = new UserGroup(ShortStringHelper) - { - Name = "Some Name", - Alias = "someAlias", - Description = description - }; - - var result = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); - - Assert.IsTrue(result.Success); - Assert.IsNotNull(result.Result); - Assert.AreEqual(description, result.Result.Description); - } - - [Test] - public async Task Can_Update_UserGroup_Description() - { - var initialDescription = "Initial description"; - var userGroup = new UserGroup(ShortStringHelper) - { - Name = "Some Name", - Alias = "someAlias", - Description = initialDescription - }; - - var createResult = await UserGroupService.CreateAsync(userGroup, Constants.Security.SuperUserKey); - Assert.IsTrue(createResult.Success); - - var updatedDescription = "Updated description"; - var createdUserGroup = createResult.Result; - createdUserGroup!.Description = updatedDescription; - - var updateResult = await UserGroupService.UpdateAsync(createdUserGroup, Constants.Security.SuperUserKey); - Assert.IsTrue(updateResult.Success); - Assert.AreEqual(updatedDescription, updateResult.Result!.Description); - } } From d0e1647db9ad9de825863efe5f3788cd78685a8a Mon Sep 17 00:00:00 2001 From: Lan Nguyen Thuy Date: Fri, 5 Dec 2025 16:24:23 +0700 Subject: [PATCH 4/9] remove change from link picker --- .../link-picker-modal/link-picker-modal.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts index d20d0288337d..e747db814aa5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/multi-url-picker/link-picker-modal/link-picker-modal.element.ts @@ -203,8 +203,8 @@ export class UmbLinkPickerModalElement extends UmbModalBaseElement Date: Fri, 5 Dec 2025 18:08:05 +0700 Subject: [PATCH 5/9] edit icon ui for user group --- .../user-group/user-group-workspace-editor.element.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 ad727813d4d1..6c1decf5b6fd 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 @@ -140,9 +140,10 @@ 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-hover: var(--uui-color-border); } #name { From abc4aa9b177bc498fe5307eb3679eb5cdfc9af7f Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 5 Dec 2025 12:30:31 +0100 Subject: [PATCH 6/9] Fixed table exists check in migration. --- .../V_17_1_0/AddDescriptionToUserGroup.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) 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 index 77709cc59999..f378ff0c7a04 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs @@ -21,20 +21,23 @@ public AddDescriptionToUserGroup( /// protected override async Task MigrateAsync() { - if (TableExists(Constants.DatabaseSchema.Tables.UserGroup)) + if (TableExists(Constants.DatabaseSchema.Tables.UserGroup) is false) { return; } - string columnName = "description"; - var hasColumn = Context.SqlContext.SqlSyntax.GetColumnsInSchema(Context.Database).ToList().Any(c => - c.TableName == Constants.DatabaseSchema.Tables.UserGroup && - c.ColumnName == columnName); + 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) + if (hasColumn) { - AddColumn(Constants.DatabaseSchema.Tables.UserGroup, columnName); + return; } + + AddColumn(Constants.DatabaseSchema.Tables.UserGroup, ColumnName); } } } From 81c30919d62e8cfe51df639a1982d8ad2dba904d Mon Sep 17 00:00:00 2001 From: Lan Nguyen Thuy Date: Tue, 9 Dec 2025 12:54:26 +0700 Subject: [PATCH 7/9] update description in table user groups --- .../mocks/data/user-group/user-group.data.ts | 2 + ...table-description-column-layout.element.ts | 42 +++++++++++++++++++ ...ser-group-table-collection-view.element.ts | 10 +++++ .../user-group-workspace-editor.element.ts | 1 + 4 files changed, 55 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/user/user-group/collection/components/user-group-table-description-column-layout.element.ts 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/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/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/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 6c1decf5b6fd..d14003223323 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 @@ -143,6 +143,7 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { font-size: var(--uui-size-8); height: 60px; width: 60px; + --uui-button-border-color: transparent; --uui-button-border-color-hover: var(--uui-color-border); } From 7759309dba31b5e99610495d4f9035004d9a50bd Mon Sep 17 00:00:00 2001 From: Lan Nguyen Thuy Date: Tue, 9 Dec 2025 14:42:38 +0700 Subject: [PATCH 8/9] update user group editor css --- .../user-group/user-group-workspace-editor.element.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 d14003223323..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 @@ -155,6 +155,10 @@ export class UmbUserGroupWorkspaceEditorElement extends UmbLitElement { #editors { width: 100%; + display: flex; + flex: 1 1 auto; + flex-direction: column; + gap: 1px; } #description { From 0b982fa84dea0c2df0e7ace714470491b565f391 Mon Sep 17 00:00:00 2001 From: Lan Nguyen Thuy Date: Tue, 16 Dec 2025 15:38:02 +0700 Subject: [PATCH 9/9] update description default --- src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs | 1 + src/Umbraco.Core/Models/Membership/IUserGroup.cs | 4 +--- .../Migrations/Install/DatabaseDataCreator.cs | 5 +++++ src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs | 4 ++-- .../{V_17_1_0 => V_17_2_0}/AddDescriptionToUserGroup.cs | 2 +- 5 files changed, 10 insertions(+), 6 deletions(-) rename src/Umbraco.Infrastructure/Migrations/Upgrade/{V_17_1_0 => V_17_2_0}/AddDescriptionToUserGroup.cs (95%) diff --git a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs index 0baf42845906..9799412b453e 100644 --- a/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs @@ -11,6 +11,7 @@ public interface IReadOnlyUserGroup string Alias { get; } + // TODO (V18): Remove the default implementations. string? Description { get { return null; } } string? Icon { get; } diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs index 8ea7ecb036b5..a7df258d35d9 100644 --- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs +++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs @@ -22,9 +22,7 @@ public interface IUserGroup : IEntity, IRememberBeingDirty /// string? Name { get; set; } - /// - /// The description - /// + /// TODO (V18): Remove the default implementations. string? Description { get => null; set { } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 711b449142fb..d30e3bf98862 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -1240,6 +1240,7 @@ private void CreateUserGroupData() StartContentId = -1, Alias = Constants.Security.AdminGroupAlias, Name = "Administrators", + Description = "Users with access to all sections", CreateDate = DateTime.UtcNow, UpdateDate = DateTime.UtcNow, Icon = "icon-medal", @@ -1257,6 +1258,7 @@ private void CreateUserGroupData() StartContentId = -1, Alias = WriterGroupAlias, Name = "Writers", + Description = "Users with access to write content", CreateDate = DateTime.UtcNow, UpdateDate = DateTime.UtcNow, Icon = "icon-edit", @@ -1274,6 +1276,7 @@ private void CreateUserGroupData() StartContentId = -1, Alias = EditorGroupAlias, Name = "Editors", + Description = "Users can create, edit, and manage content and media within the CMS", CreateDate = DateTime.UtcNow, UpdateDate = DateTime.UtcNow, Icon = "icon-tools", @@ -1291,6 +1294,7 @@ private void CreateUserGroupData() StartContentId = -1, Alias = TranslatorGroupAlias, Name = "Translators", + Description = "Users with access to translation features", CreateDate = DateTime.UtcNow, UpdateDate = DateTime.UtcNow, Icon = "icon-globe", @@ -1306,6 +1310,7 @@ private void CreateUserGroupData() Key = Constants.Security.SensitiveDataGroupKey, Alias = SensitiveDataGroupAlias, Name = "Sensitive data", + Description = "Users with access to sensitive data", CreateDate = DateTime.UtcNow, UpdateDate = DateTime.UtcNow, Icon = "icon-lock", diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 5b074a0a83cc..33d6e0c602d7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -144,8 +144,8 @@ 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 17.2.0 + To("{F1A2B3C4-D5E6-4789-ABCD-1234567890AB}"); // To 18.0.0 // TODO (V18): Enable on 18 branch diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_2_0/AddDescriptionToUserGroup.cs similarity index 95% rename from src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs rename to src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_2_0/AddDescriptionToUserGroup.cs index f378ff0c7a04..498684afc06f 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_1_0/AddDescriptionToUserGroup.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_2_0/AddDescriptionToUserGroup.cs @@ -1,7 +1,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Infrastructure.Persistence.Dtos; -namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_1_0 +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_2_0 { /// /// Migration to add a description column to the user group table.