Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
29edc58
Adding description to user groups
LanThuyNguyen Dec 4, 2025
edc9287
Add description to dto and test and umbraco plan
LanThuyNguyen Dec 4, 2025
971e747
update unit test for user group
LanThuyNguyen Dec 5, 2025
d0e1647
remove change from link picker
LanThuyNguyen Dec 5, 2025
9368725
edit icon ui for user group
LanThuyNguyen Dec 5, 2025
abc4aa9
Fixed table exists check in migration.
AndyButland Dec 5, 2025
7a8bb7d
Merge branch 'main' into v17/add-description-to-user-group
andr317c Dec 5, 2025
f7e59f9
Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into v1…
LanThuyNguyen Dec 8, 2025
81c3091
update description in table user groups
LanThuyNguyen Dec 9, 2025
dabd7e7
Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into v1…
LanThuyNguyen Dec 9, 2025
7759309
update user group editor css
LanThuyNguyen Dec 9, 2025
3867349
Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into v1…
LanThuyNguyen Dec 16, 2025
0b982fa
update description default
LanThuyNguyen Dec 16, 2025
d655015
remove description column element
LanThuyNguyen Dec 17, 2025
76667f5
fix conflict
LanThuyNguyen Dec 17, 2025
7367e96
add ignore large method
LanThuyNguyen Dec 17, 2025
f561b66
remove codesence
LanThuyNguyen Dec 17, 2025
da2c09f
update user group descriptions default
LanThuyNguyen Dec 17, 2025
c707c8c
Merge branch 'main' into v17/add-description-to-user-group
AndyButland Jan 7, 2026
cc39b1d
Added description to constructor of ReadOnlyUserGroup.
AndyButland Jan 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public async Task<UserGroupResponseModel> 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,
Expand Down Expand Up @@ -87,6 +88,7 @@ public async Task<UserGroupResponseModel> 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),
Expand Down Expand Up @@ -132,6 +134,7 @@ public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> CreateAsync(Cre
{
Name = CleanUserGroupNameOrAliasForXss(requestModel.Name),
Alias = CleanUserGroupNameOrAliasForXss(requestModel.Alias),
Description = requestModel.Description,
Icon = requestModel.Icon,
HasAccessToAllLanguages = requestModel.HasAccessToAllLanguages,
Permissions = requestModel.FallbackPermissions,
Expand Down Expand Up @@ -197,9 +200,10 @@ public async Task<Attempt<IUserGroup, UserGroupOperationStatus>> 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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -22,6 +22,11 @@ public class UserGroupBase
/// </summary>
public required string Alias { get; init; }

/// <summary>
/// The description of the user group
/// </summary>
public string? Description { get; set; }

/// <summary>
/// The Icon for the user group
/// </summary>
Expand Down
10 changes: 5 additions & 5 deletions src/Umbraco.Core/Models/Membership/IReadOnlyUserGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ public interface IReadOnlyUserGroup
{
string? Name { get; }

string Alias { get; }

// TODO (V18): Remove the default implementations.
string? Description { get { return null; } }

string? Icon { get; }

int Id { get; }
Expand All @@ -19,11 +24,6 @@ public interface IReadOnlyUserGroup

int? StartMediaId { get; }

/// <summary>
/// The alias
/// </summary>
string Alias { get; }

// This is set to return true as default to avoid breaking changes.
bool HasAccessToAllLanguages => true;

Expand Down
6 changes: 6 additions & 0 deletions src/Umbraco.Core/Models/Membership/IUserGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public interface IUserGroup : IEntity, IRememberBeingDirty
/// </summary>
string? Name { get; set; }

/// TODO (V18): Remove the default implementations.
string? Description {
get => null;
set { }
}

/// <summary>
/// If this property is true it will give the group access to all languages
/// </summary>
Expand Down
71 changes: 53 additions & 18 deletions src/Umbraco.Core/Models/Membership/ReadOnlyUserGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Umbraco.Cms.Core.Models.Membership;

public class ReadOnlyUserGroup : IReadOnlyUserGroup, IEquatable<ReadOnlyUserGroup>
{
[Obsolete("Please use the constructor that includes all parameters. Scheduled for removal in Umbraco 19.")]
public ReadOnlyUserGroup(
int id,
Guid key,
Expand All @@ -17,11 +18,43 @@ public ReadOnlyUserGroup(
ISet<string> permissions,
ISet<IGranularPermission> granularPermissions,
bool hasAccessToAllLanguages)
: this(
id,
key,
name,
null,
icon,
startContentId,
startMediaId,
alias,
allowedLanguages,
allowedSections,
permissions,
granularPermissions,
hasAccessToAllLanguages)
{
}

public ReadOnlyUserGroup(
int id,
Guid key,
string? name,
string? description,
string? icon,
int? startContentId,
int? startMediaId,
string? alias,
IEnumerable<int> allowedLanguages,
IEnumerable<string> allowedSections,
ISet<string> permissions,
ISet<IGranularPermission> granularPermissions,
bool hasAccessToAllLanguages)
{
Name = name ?? string.Empty;
Icon = icon;
Id = id;
Key = key;
Name = name ?? string.Empty;
Description = description;
Icon = icon;
Alias = alias ?? string.Empty;
AllowedLanguages = allowedLanguages.ToArray();
AllowedSections = allowedSections.ToArray();
Expand All @@ -38,23 +71,10 @@ public ReadOnlyUserGroup(

public Guid Key { get; }

public bool Equals(ReadOnlyUserGroup? other)
{
if (ReferenceEquals(null, other))
{
return false;
}

if (ReferenceEquals(this, other))
{
return true;
}

return string.Equals(Alias, other.Alias);
}

public string Name { get; }

public string? Description { get; }

public string? Icon { get; }

public int? StartContentId { get; }
Expand All @@ -77,7 +97,7 @@ public bool Equals(ReadOnlyUserGroup? other)

public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj))
if (obj is null)
{
return false;
}
Expand All @@ -95,6 +115,21 @@ public override bool Equals(object? obj)
return Equals((ReadOnlyUserGroup)obj);
}

public bool Equals(ReadOnlyUserGroup? other)
{
if (other is null)
{
return false;
}

if (ReferenceEquals(this, other))
{
return true;
}

return string.Equals(Alias, other.Alias);
}

public override int GetHashCode() => Alias?.GetHashCode() ?? base.GetHashCode();

public static bool operator !=(ReadOnlyUserGroup left, ReadOnlyUserGroup right) => !Equals(left, right);
Expand Down
8 changes: 8 additions & 0 deletions src/Umbraco.Core/Models/Membership/UserGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> _permissions;
private ISet<IGranularPermission> _granularPermissions;
Expand Down Expand Up @@ -112,6 +113,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
{
Expand Down
1 change: 1 addition & 0 deletions src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public static IReadOnlyUserGroup ToReadOnlyGroup(this IUserGroup group)
group.Id,
group.Key,
group.Name,
group.Description,
group.Icon,
group.StartContentId,
group.StartMediaId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,7 @@ private void CreateUserGroupData()
StartContentId = -1,
Alias = Constants.Security.AdminGroupAlias,
Name = "Administrators",
Description = "Users with full access to all sections and functionality",
CreateDate = DateTime.UtcNow,
UpdateDate = DateTime.UtcNow,
Icon = "icon-medal",
Expand All @@ -1329,6 +1330,7 @@ private void CreateUserGroupData()
StartContentId = -1,
Alias = WriterGroupAlias,
Name = "Writers",
Description = "Users with permission to create and update but not publish content",
CreateDate = DateTime.UtcNow,
UpdateDate = DateTime.UtcNow,
Icon = "icon-edit",
Expand All @@ -1346,6 +1348,7 @@ private void CreateUserGroupData()
StartContentId = -1,
Alias = EditorGroupAlias,
Name = "Editors",
Description = "Users with full permission to create, update and publish content",
CreateDate = DateTime.UtcNow,
UpdateDate = DateTime.UtcNow,
Icon = "icon-tools",
Expand All @@ -1363,6 +1366,7 @@ private void CreateUserGroupData()
StartContentId = -1,
Alias = TranslatorGroupAlias,
Name = "Translators",
Description = "Users with permission to manage dictionary entries",
CreateDate = DateTime.UtcNow,
UpdateDate = DateTime.UtcNow,
Icon = "icon-globe",
Expand All @@ -1378,6 +1382,7 @@ private void CreateUserGroupData()
Key = Constants.Security.SensitiveDataGroupKey,
Alias = SensitiveDataGroupAlias,
Name = "Sensitive data",
Description = "Users with the specific permission to be able to manage properties and data marked as sensitive",
CreateDate = DateTime.UtcNow,
UpdateDate = DateTime.UtcNow,
Icon = "icon-lock",
Expand Down
3 changes: 3 additions & 0 deletions src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
// To 17.1.0
To<V_17_1_0.ChangeValidationRegExpToNvarcharMax>("{1CE2E78B-E736-45D8-97A2-CE3EF2F31BCD}");

// To 17.2.0
To<V_17_2_0.AddDescriptionToUserGroup>("{F1A2B3C4-D5E6-4789-ABCD-1234567890AB}");

Check warning on line 152 in src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Large Method

UmbracoPlan increases from 70 to 71 lines of code, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
// To 18.0.0
// TODO (V18): Enable on 18 branch
//// To<V_18_0_0.MigrateSingleBlockList>("{74332C49-B279-4945-8943-F8F00B1F5949}");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;

namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_2_0
{
/// <summary>
/// Migration to add a description column to the user group table.
/// </summary>
public class AddDescriptionToUserGroup : AsyncMigrationBase
{
/// <summary>
/// Initializes a new instance of the <see cref="AddDescriptionToUserGroup"/> class.
/// </summary>
/// <param name="context">The migration context.</param>
public AddDescriptionToUserGroup(
IMigrationContext context)
: base(context)
{
}

/// <inheritdoc/>
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<UserGroupDto>(Constants.DatabaseSchema.Tables.UserGroup, ColumnName);
}
}
}
5 changes: 5 additions & 0 deletions src/Umbraco.Infrastructure/Persistence/Dtos/UserGroupDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<UserGroup2AppDto>(),
CreateDate = entity.CreateDate,
UpdateDate = entity.UpdateDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ protected override void DefineMaps()
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Icon), nameof(UserGroupDto.Icon));
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.StartContentId), nameof(UserGroupDto.StartContentId));
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.StartMediaId), nameof(UserGroupDto.StartMediaId));
DefineMap<UserGroup, UserGroupDto>(nameof(UserGroup.Description), nameof(UserGroupDto.Description));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ private static void AppendGroupBy(Sql<ISqlContext> sql) =>
x => x.UpdateDate,
x => x.Alias,
x => x.Name,
x => x.Description,
x => x.HasAccessToAllLanguages,
x => x.Key,
x => x.DefaultPermissions)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const data: Array<UmbMockUserGroupModel> = [
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,
Expand Down Expand Up @@ -91,6 +92,7 @@ export const data: Array<UmbMockUserGroupModel> = [
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: [
Expand Down
Loading
Loading