Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7ad3ed6
add users section into user group
LanThuyNguyen Mar 23, 2026
be35cf3
fix test failed
LanThuyNguyen Mar 23, 2026
d86a38b
fix unchange issue
LanThuyNguyen Mar 24, 2026
4bea90a
Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into v1…
LanThuyNguyen Mar 24, 2026
9446192
add notification
LanThuyNguyen Mar 24, 2026
5a3ec5e
add remainging count
LanThuyNguyen Mar 31, 2026
cf49815
update take 100
LanThuyNguyen Mar 31, 2026
4b06947
split user list into separate element
LanThuyNguyen Apr 1, 2026
772e6be
Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into v1…
LanThuyNguyen Apr 1, 2026
5f0fd9e
Merge branch 'main' into v17/feature/manage-users-from-group
AndyButland Apr 1, 2026
4d191c3
add localization for text
LanThuyNguyen Apr 6, 2026
0074244
Merge branch 'v17/feature/manage-users-from-group' of https://github.…
LanThuyNguyen Apr 6, 2026
e525b37
Merge branch 'main' into v17/feature/manage-users-from-group
NguyenThuyLan Apr 6, 2026
445a979
add repository for user list in user group
LanThuyNguyen Apr 6, 2026
b324a8e
Merge branch 'v17/feature/manage-users-from-group' of https://github.…
LanThuyNguyen Apr 6, 2026
9ba4404
Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into v1…
LanThuyNguyen Apr 7, 2026
321eba0
Merge branch 'main' into v17/feature/manage-users-from-group
AndyButland Apr 8, 2026
c3d6e00
update key message
LanThuyNguyen Apr 9, 2026
aa68e7d
Merge branch 'v17/feature/manage-users-from-group' of https://github.…
LanThuyNguyen Apr 9, 2026
1e099e5
remove remainingCount from user-input
LanThuyNguyen Apr 15, 2026
77657cc
Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS into v1…
LanThuyNguyen Apr 15, 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 @@ -10,6 +10,9 @@
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbRoutableWorkspaceContext, UmbSubmittableWorkspaceContext } from '@umbraco-cms/backoffice/workspace';
import type { UmbUserPermissionModel } from '@umbraco-cms/backoffice/user-permission';
import { UserGroupService, UserService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecute } from '@umbraco-cms/backoffice/resources';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';

export class UmbUserGroupWorkspaceContext
extends UmbEntityNamedDetailWorkspaceContextBase<UmbUserGroupDetailModel, UmbUserGroupDetailRepository>
Expand All @@ -31,6 +34,11 @@
readonly permissions = this._data.createObservablePartOfCurrent((data) => data?.permissions || []);
readonly description = this._data.createObservablePartOfCurrent((data) => data?.description || '');

#initialUserUniques: string[] = [];
#usersEdited = false;
readonly #userUniquesState = new UmbArrayState<string>([], (v) => v);
readonly userUniques = this.#userUniquesState.asObservable();

constructor(host: UmbControllerHost) {
super(host, {
workspaceAlias: UMB_USER_GROUP_WORKSPACE_ALIAS,
Expand Down Expand Up @@ -63,6 +71,105 @@
]);
}

override async load(unique: string) {
const result = await super.load(unique);
if (!result.error) {
await this.#loadUsers(unique);
}
return result;
}

async #loadUsers(unique: string) {
const { data } = await tryExecute(
this,
UserService.getFilterUser({ query: { userGroupIds: [unique], take: 10000 } }),
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
);
const uniques = data?.items.map((u) => u.id) ?? [];
this.#initialUserUniques = [...uniques];
this.#userUniquesState.setValue(uniques);
this.#usersEdited = false;
}

/**
* Sets the pending user uniques for this group (client-side only until Save).
* Also sets the dirty flag so the workspace knows there are unpersisted changes.
* @param {Array<string>} uniques
* @memberof UmbUserGroupWorkspaceContext
*/
setUserUniques(uniques: Array<string>) {
this.#userUniquesState.setValue(uniques);
this.#usersEdited = true;
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
}

override getHasUnpersistedChanges(): boolean {
return super.getHasUnpersistedChanges() || this.#usersEdited;
}

override async submit() {
if (this.getIsNew()) {
// For new groups: create group first (so it exists on server), then add users.
await super.submit();
await this.#persistUserChanges();
} else {
// For existing groups
await this.#persistUserChanges();
await super.submit();
}
}

async #persistUserChanges() {
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
const unique = this.getUnique();
if (!this.#usersEdited || !unique) return;

const pending = this.#userUniquesState.getValue();
const toAdd = pending.filter((u) => !this.#initialUserUniques.includes(u));
const toRemove = this.#initialUserUniques.filter((u) => !pending.includes(u));

// Run add and remove in parallel; track whether either call errored.
// Only update local state when all API calls succeeded.
// If there was an error, keep #usersEdited = true so the next Save will retry.
const [addError, removeError] = await Promise.all([
this.#addUsersToGroup(unique, toAdd),
this.#removeUsersFromGroup(unique, toRemove),
]);

if (!addError && !removeError) {
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
this.#initialUserUniques = [...pending];
this.#usersEdited = false;
}
}

async #addUsersToGroup(unique: string, userIds: string[]): Promise<boolean> {
if (!userIds.length) return false;
const { error } = await tryExecute(
this,
UserGroupService.postUserGroupByIdUsers({
path: { id: unique },
body: userIds.map((id) => ({ id })),
}),
);
return !!error;
}

Check warning on line 152 in src/Umbraco.Web.UI.Client/src/packages/user/user-group/workspace/user-group/user-group-workspace.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Code Duplication

The module contains 2 functions with similar structure: UmbUserGroupWorkspaceContext.addUsersToGroup,UmbUserGroupWorkspaceContext.removeUsersFromGroup. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

async #removeUsersFromGroup(unique: string, userIds: string[]): Promise<boolean> {
if (!userIds.length) return false;
const { error } = await tryExecute(
this,
UserGroupService.deleteUserGroupByIdUsers({
path: { id: unique },
body: userIds.map((id) => ({ id })),
}),
);
return !!error;
}

override resetState() {
super.resetState();
this.#initialUserUniques = [];
this.#userUniquesState.setValue([]);
this.#usersEdited = false;
}

updateProperty<Alias extends keyof UmbUserGroupDetailModel>(alias: Alias, value: UmbUserGroupDetailModel[Alias]) {
this._data.updateCurrent({ [alias]: value });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { UUIBooleanInputEvent } from '@umbraco-cms/backoffice/external/uui';
import type { UmbUserInputElement } from '../../../../user/components/user-input/user-input.element.js';

import '../components/user-group-entity-type-permission-groups.element.js';
import '../../../../user/components/user-input/user-input.element.js';

@customElement('umb-user-group-details-workspace-view')
export class UmbUserGroupDetailsWorkspaceViewElement extends UmbLitElement implements UmbWorkspaceViewElement {
Expand Down Expand Up @@ -37,6 +39,9 @@ export class UmbUserGroupDetailsWorkspaceViewElement extends UmbLitElement imple
@state()
private _mediaRootAccess: UmbUserGroupDetailModel['mediaRootAccess'] = false;

@state()
private _userUniques: string[] = [];

#workspaceContext?: typeof UMB_USER_GROUP_WORKSPACE_CONTEXT.TYPE;

constructor() {
Expand Down Expand Up @@ -81,6 +86,12 @@ export class UmbUserGroupDetailsWorkspaceViewElement extends UmbLitElement imple
(value) => (this._mediaStartNode = value),
'_observeMediaStartNode',
);

this.observe(
this.#workspaceContext?.userUniques,
(value) => (this._userUniques = value ?? []),
'_observeUserUniques',
);
}

#onSectionsChange(event: UmbChangeEvent) {
Expand Down Expand Up @@ -140,6 +151,12 @@ export class UmbUserGroupDetailsWorkspaceViewElement extends UmbLitElement imple
this.#workspaceContext?.updateProperty('mediaStartNode', selected ? { unique: selected } : null);
}

#onUsersChange(event: UmbChangeEvent) {
event.stopPropagation();
const target = event.target as UmbUserInputElement;
this.#workspaceContext?.setUserUniques(target.selection);
}

override render() {
if (!this._unique) return nothing;

Expand All @@ -163,6 +180,12 @@ export class UmbUserGroupDetailsWorkspaceViewElement extends UmbLitElement imple

${this.#renderPermissionGroups()}
</umb-stack>
<div>
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
<uui-box>
<div slot="headline"><umb-localize key="general_users"></umb-localize></div>
<umb-user-input .selection=${this._userUniques} @change=${this.#onUsersChange}></umb-user-input>
Comment thread
NguyenThuyLan marked this conversation as resolved.
Outdated
</uui-box>
</div>
</div>
`;
}
Expand Down Expand Up @@ -257,6 +280,9 @@ export class UmbUserGroupDetailsWorkspaceViewElement extends UmbLitElement imple
}

#main {
display: grid;
grid-template-columns: 1fr 350px;
gap: var(--uui-size-layout-1);
padding: var(--uui-size-layout-1);
}

Expand Down
Loading