add ability to leave multiple groups (#1431)
* add ability to leave multiple groups fixes: #496 * With v21.0.0 a new JSON format v4 was introduced * Update src/user/UserGroups.tsx Co-authored-by: Jon Koops <jonkoops@gmail.com> * Update src/user/UserGroups.tsx Co-authored-by: Jon Koops <jonkoops@gmail.com> * filter selected groups based on direct membership * fixed test * remove all the groups Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
d1e45dabde
commit
405d3310f3
6 changed files with 73 additions and 34 deletions
|
@ -62,6 +62,9 @@ describe("Users test", () => {
|
|||
beforeEach(() => {
|
||||
keycloakBefore();
|
||||
loginPage.logIn();
|
||||
cy.intercept(
|
||||
"/auth/admin/realms/master/components?type=org.keycloak.storage.UserStorageProvider"
|
||||
).as("brute-force");
|
||||
sidebarPage.goToUsers();
|
||||
});
|
||||
|
||||
|
@ -90,8 +93,6 @@ describe("Users test", () => {
|
|||
|
||||
const groupsListCopy = groupsList.slice(0, 1);
|
||||
|
||||
console.log(groupsList);
|
||||
|
||||
groupsListCopy.forEach((element) => {
|
||||
cy.findByTestId(`${element}-check`).click();
|
||||
});
|
||||
|
@ -106,6 +107,7 @@ describe("Users test", () => {
|
|||
});
|
||||
|
||||
it("User details test", () => {
|
||||
cy.wait("@brute-force");
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
|
||||
listingPage.goToItemDetails(itemId);
|
||||
|
@ -115,10 +117,12 @@ describe("Users test", () => {
|
|||
masthead.checkNotificationMessage("The user has been saved");
|
||||
|
||||
sidebarPage.goToUsers();
|
||||
cy.wait("@brute-force");
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
});
|
||||
|
||||
it("User attributes test", () => {
|
||||
cy.wait("@brute-force");
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
|
||||
listingPage.goToItemDetails(itemId);
|
||||
|
@ -132,6 +136,7 @@ describe("Users test", () => {
|
|||
});
|
||||
|
||||
it("User attributes with multiple values test", () => {
|
||||
cy.wait("@brute-force");
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
|
||||
listingPage.goToItemDetails(itemId);
|
||||
|
@ -159,6 +164,7 @@ describe("Users test", () => {
|
|||
});
|
||||
|
||||
it("Add user to groups test", () => {
|
||||
cy.wait("@brute-force");
|
||||
// Go to user groups
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
listingPage.goToItemDetails(itemId);
|
||||
|
@ -176,15 +182,17 @@ describe("Users test", () => {
|
|||
});
|
||||
|
||||
it("Leave group test", () => {
|
||||
cy.wait("@brute-force");
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
listingPage.goToItemDetails(itemId);
|
||||
// Go to user groups
|
||||
userGroupsPage.goToGroupsTab();
|
||||
cy.contains("Leave").click();
|
||||
cy.findByTestId(`leave-${groupsList[0]}`).click();
|
||||
cy.findByTestId("modalConfirm").click();
|
||||
});
|
||||
|
||||
it("Go to user consents test", () => {
|
||||
cy.wait("@brute-force");
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
|
||||
listingPage.goToItemDetails(itemId);
|
||||
|
|
|
@ -11,8 +11,8 @@ export default {
|
|||
assignedType: "Assigned type",
|
||||
displayOrder: "Display order",
|
||||
type: "Type",
|
||||
deleteClientScope: "Delete client scope {{name}}",
|
||||
deleteClientScope_plural: "Delete {{count}} client scopes",
|
||||
deleteClientScope_one: "Delete client scope {{name}}",
|
||||
deleteClientScope_other: "Delete {{count}} client scopes",
|
||||
deleteConfirm: "Are you sure you want to delete this client scope",
|
||||
changeTypeTo: "Change type to",
|
||||
changeTypeIntro: "{{count}} selected client scopes will be changed to",
|
||||
|
|
|
@ -38,8 +38,8 @@ export default {
|
|||
assignRole: "Assign role",
|
||||
unAssignRole: "Unassign",
|
||||
removeMappingTitle: "Remove mapping?",
|
||||
removeMappingConfirm: "Are you sure you want to remove this mapping?",
|
||||
removeMappingConfirm_plural:
|
||||
removeMappingConfirm_one: "Are you sure you want to remove this mapping?",
|
||||
removeMappingConfirm_other:
|
||||
"Are you sure you want to remove {{count}} mappings",
|
||||
clientScopeSearch: {
|
||||
name: "Name",
|
||||
|
|
|
@ -10,11 +10,11 @@ export default {
|
|||
renameGroup: "Rename group",
|
||||
deleteGroup: "Delete group",
|
||||
leave: "Leave group",
|
||||
usersLeft: "{{count}} user left the group",
|
||||
usersLeft_plural: "{{count}} users left the group",
|
||||
usersLeft_one: "{{count}} user left the group",
|
||||
usersLeft_other: "{{count}} users left the group",
|
||||
usersLeftError: "Could not remove users from the group: {{error}}",
|
||||
usersAdded: "{{count}} user added to the group",
|
||||
usersAdded_plural: "{{count}} users added to the group",
|
||||
usersAdded_one: "{{count}} user added to the group",
|
||||
usersAdded_other: "{{count}} users added to the group",
|
||||
usersAddedError: "Could not add users to the group: {{error}}",
|
||||
search: "Search",
|
||||
members: "Members",
|
||||
|
@ -52,12 +52,12 @@ export default {
|
|||
noGroupsInThisSubGroup: "No groups in this sub group",
|
||||
noGroupsInThisSubGroupInstructions:
|
||||
"You haven't created any groups in this sub group.",
|
||||
deleteConfirmTitle: "Delete group?",
|
||||
deleteConfirmTitle_plural: "Delete groups?",
|
||||
deleteConfirm: "Are you sure you want to delete this group",
|
||||
deleteConfirm_plural: "Are you sure you want to delete this groups.",
|
||||
groupDeleted: "Group deleted",
|
||||
groupDeleted_plural: "Groups deleted",
|
||||
deleteConfirmTitle_one: "Delete group?",
|
||||
deleteConfirmTitle_other: "Delete groups?",
|
||||
deleteConfirm_one: "Are you sure you want to delete this group",
|
||||
deleteConfirm_other: "Are you sure you want to delete this groups.",
|
||||
groupDeleted_one: "Group deleted",
|
||||
groupDeleted_other: "Groups deleted",
|
||||
groupDeleteError: "Error deleting group {error}",
|
||||
attributes: "Attributes",
|
||||
groupUpdated: "Group updated",
|
||||
|
|
|
@ -9,7 +9,7 @@ import { QuestionCircleIcon } from "@patternfly/react-icons";
|
|||
import { cellWidth } from "@patternfly/react-table";
|
||||
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import _ from "lodash";
|
||||
import { intersectionBy, sortBy } from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
|
@ -32,7 +32,9 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState<GroupRepresentation>();
|
||||
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
|
||||
[]
|
||||
);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [isDirectMembership, setDirectMembership] = useState(true);
|
||||
|
@ -45,7 +47,7 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
|
||||
const adminClient = useAdminClient();
|
||||
const alphabetize = (groupsList: GroupRepresentation[]) => {
|
||||
return _.sortBy(groupsList, (group) => group.path?.toUpperCase());
|
||||
return sortBy(groupsList, (group) => group.path?.toUpperCase());
|
||||
};
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
|
@ -170,20 +172,26 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: t("leaveGroup", {
|
||||
name: selectedGroup?.name,
|
||||
count: selectedGroups.length,
|
||||
name: selectedGroups[0]?.name,
|
||||
}),
|
||||
messageKey: t("leaveGroupConfirmDialog", {
|
||||
groupname: selectedGroup?.name,
|
||||
count: selectedGroups.length,
|
||||
groupname: selectedGroups[0]?.name,
|
||||
username: user.username,
|
||||
}),
|
||||
continueButtonLabel: "leave",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await adminClient.users.delFromGroup({
|
||||
id: user.id!,
|
||||
groupId: selectedGroup!.id!,
|
||||
});
|
||||
await Promise.all(
|
||||
selectedGroups.map((group) =>
|
||||
adminClient.users.delFromGroup({
|
||||
id: user.id!,
|
||||
groupId: group.id!,
|
||||
})
|
||||
)
|
||||
);
|
||||
refresh();
|
||||
addAlert(t("removedGroupMembership"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
|
@ -192,8 +200,8 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
},
|
||||
});
|
||||
|
||||
const leave = (group: GroupRepresentation) => {
|
||||
setSelectedGroup(group);
|
||||
const leave = (group: GroupRepresentation[]) => {
|
||||
setSelectedGroups(group);
|
||||
toggleDeleteDialog();
|
||||
};
|
||||
|
||||
|
@ -206,7 +214,7 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
canLeaveGroup && (
|
||||
<Button
|
||||
data-testid={`leave-${group.name}`}
|
||||
onClick={() => leave(group)}
|
||||
onClick={() => leave([group])}
|
||||
variant="link"
|
||||
>
|
||||
{t("leave")}
|
||||
|
@ -249,6 +257,7 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
onConfirm={(groups) => {
|
||||
addGroups(groups);
|
||||
setOpen(false);
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -260,11 +269,21 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
ariaLabelKey="roles:roleList"
|
||||
searchPlaceholderKey="groups:searchGroup"
|
||||
canSelectAll
|
||||
onSelect={(groups) =>
|
||||
isDirectMembership
|
||||
? setSelectedGroups(groups)
|
||||
: setSelectedGroups(
|
||||
intersectionBy(groups, directMembershipList, "id")
|
||||
)
|
||||
}
|
||||
isRowDisabled={(group) =>
|
||||
!isDirectMembership &&
|
||||
directMembershipList.every((item) => item.id !== group.id)
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<Button
|
||||
className="kc-join-group-button"
|
||||
key="join-group-button"
|
||||
onClick={toggleModal}
|
||||
data-testid="add-group-button"
|
||||
>
|
||||
|
@ -278,6 +297,15 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
|
|||
isChecked={isDirectMembership}
|
||||
className="direct-membership-check"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => leave(selectedGroups)}
|
||||
data-testid="leave-group-button"
|
||||
variant="link"
|
||||
isDisabled={selectedGroups.length === 0}
|
||||
>
|
||||
{t("leave")}
|
||||
</Button>
|
||||
|
||||
{enabled && (
|
||||
<Popover
|
||||
aria-label="Basic popover"
|
||||
|
|
|
@ -19,9 +19,12 @@ export default {
|
|||
selectGroups: "Select groups to join",
|
||||
searchForGroups: "Search for groups",
|
||||
leave: "Leave",
|
||||
leaveGroup: "Leave group {{name}}?",
|
||||
leaveGroupConfirmDialog:
|
||||
leaveGroup_one: "Leave group {{name}}?",
|
||||
leaveGroup_other: "Leave groups?",
|
||||
leaveGroupConfirmDialog_one:
|
||||
"Are you sure you want to remove {{username}} from the group {{groupname}}?",
|
||||
leaveGroupConfirmDialog_other:
|
||||
"Are you sure you want to remove {{username}} from the {{count}} selected groups?",
|
||||
directMembership: "Direct membership",
|
||||
groupMembership: "Group membership",
|
||||
addedGroupMembership: "Added group membership",
|
||||
|
@ -56,9 +59,9 @@ export default {
|
|||
deleteConfirm: "Delete user?",
|
||||
deleteConfirmCurrentUser:
|
||||
"Are you sure you want to permanently delete this user",
|
||||
deleteConfirmDialog:
|
||||
deleteConfirmDialog_one:
|
||||
"Are you sure you want to permanently delete {{count}} selected user",
|
||||
deleteConfirmDialog_plural:
|
||||
deleteConfirmDialog_other:
|
||||
"Are you sure you want to permanently delete {{count}} selected users",
|
||||
userID: "User ID",
|
||||
userCreated: "The user has been created",
|
||||
|
|
Loading…
Reference in a new issue