Merge pull request #542 from jenny-s51/updateUserForm
Users(Create user form): Update create user form with new design
This commit is contained in:
commit
4182f2bd58
8 changed files with 241 additions and 48 deletions
|
@ -16,7 +16,6 @@ describe("Group creation", () => {
|
|||
const loginPage = new LoginPage();
|
||||
const masthead = new Masthead();
|
||||
const sidebarPage = new SidebarPage();
|
||||
const listingPage = new ListingPage();
|
||||
const groupModal = new GroupModal();
|
||||
|
||||
beforeEach(function () {
|
||||
|
@ -25,33 +24,24 @@ describe("Group creation", () => {
|
|||
sidebarPage.goToGroups();
|
||||
});
|
||||
|
||||
function createNewGroup() {
|
||||
groupName += "_" + (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
groupModal
|
||||
.open("openCreateGroupModal")
|
||||
.fillGroupForm(groupName)
|
||||
.clickCreate();
|
||||
|
||||
groupsList = [...groupsList, groupName];
|
||||
masthead.checkNotificationMessage("Group created");
|
||||
|
||||
sidebarPage.goToGroups();
|
||||
}
|
||||
|
||||
it("Add groups to be joined", () => {
|
||||
groupName += "_" + (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
groupModal
|
||||
.open("openCreateGroupModal")
|
||||
.fillGroupForm(groupName)
|
||||
.clickCreate();
|
||||
|
||||
groupsList = [...groupsList, groupName];
|
||||
masthead.checkNotificationMessage("Group created");
|
||||
|
||||
sidebarPage.goToGroups();
|
||||
listingPage.searchItem(groupName, false).itemExist(groupName);
|
||||
|
||||
groupName = "group";
|
||||
groupName += "_" + (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
groupModal
|
||||
.open("openCreateGroupModal")
|
||||
.fillGroupForm(groupName)
|
||||
.clickCreate();
|
||||
|
||||
groupsList = [...groupsList, groupName];
|
||||
masthead.checkNotificationMessage("Group created");
|
||||
|
||||
sidebarPage.goToGroups();
|
||||
listingPage.searchItem(groupName, false).itemExist(groupName);
|
||||
for (let i = 0; i <= 2; i++) {
|
||||
createNewGroup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -93,7 +83,21 @@ describe("Users test", () => {
|
|||
|
||||
createUserPage.goToCreateUser();
|
||||
|
||||
createUserPage.createUser(itemId).save();
|
||||
createUserPage.createUser(itemId);
|
||||
|
||||
createUserPage.toggleAddGroupModal();
|
||||
|
||||
const groupsListCopy = groupsList.slice(0, 1);
|
||||
|
||||
console.log(groupsList);
|
||||
|
||||
groupsListCopy.forEach((element) => {
|
||||
cy.getId(`${element}-check`).click();
|
||||
});
|
||||
|
||||
createUserPage.joinGroups();
|
||||
|
||||
createUserPage.save();
|
||||
|
||||
masthead.checkNotificationMessage("The user has been created");
|
||||
|
||||
|
@ -126,15 +130,17 @@ describe("Users test", () => {
|
|||
userGroupsPage.goToGroupsTab();
|
||||
userGroupsPage.toggleAddGroupModal();
|
||||
|
||||
groupsList.forEach((element) => {
|
||||
cy.wait(1000);
|
||||
|
||||
const groupsListCopy = groupsList.slice(1, 2);
|
||||
|
||||
groupsListCopy.forEach((element) => {
|
||||
cy.getId(`${element}-check`).click();
|
||||
});
|
||||
|
||||
userGroupsPage.joinGroup();
|
||||
userGroupsPage.joinGroups();
|
||||
|
||||
cy.wait(1000);
|
||||
|
||||
listingPage.itemExist(groupName);
|
||||
});
|
||||
|
||||
it("Leave group test", function () {
|
||||
|
@ -142,7 +148,8 @@ describe("Users test", () => {
|
|||
listingPage.goToItemDetails(itemId);
|
||||
// Go to user groups
|
||||
userGroupsPage.goToGroupsTab();
|
||||
cy.getId(`leave-${groupName}`).click();
|
||||
cy.wait(1000);
|
||||
cy.contains("Leave").click();
|
||||
cy.getId("modalConfirm").click();
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ export default class CreateUserPage {
|
|||
emptyStateCreateUserBtn: string;
|
||||
searchPgCreateUserBtn: string;
|
||||
addUserBtn: string;
|
||||
joinGroupsBtn: string;
|
||||
joinBtn: string;
|
||||
saveBtn: string;
|
||||
cancelBtn: string;
|
||||
|
||||
|
@ -14,6 +16,8 @@ export default class CreateUserPage {
|
|||
this.emptyStateCreateUserBtn = "empty-primary-action";
|
||||
this.searchPgCreateUserBtn = "create-new-user";
|
||||
this.addUserBtn = "add-user";
|
||||
this.joinGroupsBtn = "join-groups-button";
|
||||
this.joinBtn = "join-button";
|
||||
this.saveBtn = "create-user";
|
||||
this.cancelBtn = "cancel-create-user";
|
||||
}
|
||||
|
@ -44,6 +48,18 @@ export default class CreateUserPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
toggleAddGroupModal() {
|
||||
cy.getId(this.joinGroupsBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
joinGroups() {
|
||||
cy.getId(this.joinBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
save() {
|
||||
cy.getId(this.saveBtn).click();
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ export default class UserGroupsPage {
|
|||
constructor() {
|
||||
this.userGroupsTab = "user-groups-tab";
|
||||
this.addGroupButton = "add-group-button";
|
||||
this.joinGroupButton = "joinGroup";
|
||||
this.joinGroupButton = "join-button";
|
||||
}
|
||||
|
||||
goToGroupsTab() {
|
||||
|
@ -21,7 +21,7 @@ export default class UserGroupsPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
joinGroup() {
|
||||
joinGroups() {
|
||||
cy.getId(this.joinGroupButton).click();
|
||||
|
||||
return this;
|
||||
|
|
|
@ -31,8 +31,9 @@ export type JoinGroupDialogProps = {
|
|||
open: boolean;
|
||||
toggleDialog: () => void;
|
||||
onClose: () => void;
|
||||
username: string;
|
||||
username?: string;
|
||||
onConfirm: (newGroups: Group[]) => void;
|
||||
chips?: any;
|
||||
};
|
||||
|
||||
type Group = GroupRepresentation & {
|
||||
|
@ -45,6 +46,7 @@ export const JoinGroupDialog = ({
|
|||
toggleDialog,
|
||||
onConfirm,
|
||||
username,
|
||||
chips,
|
||||
}: JoinGroupDialogProps) => {
|
||||
const { t } = useTranslation("roles");
|
||||
const adminClient = useAdminClient();
|
||||
|
@ -65,17 +67,23 @@ export const JoinGroupDialog = ({
|
|||
() =>
|
||||
asyncStateFetch(
|
||||
async () => {
|
||||
const existingUserGroups = await adminClient.users.listGroups({ id });
|
||||
const allGroups = await adminClient.groups.find();
|
||||
|
||||
if (groupId) {
|
||||
const group = await adminClient.groups.findOne({ id: groupId });
|
||||
return { group, groups: group.subGroups! };
|
||||
} else {
|
||||
} else if (id) {
|
||||
const existingUserGroups = await adminClient.users.listGroups({
|
||||
id,
|
||||
});
|
||||
|
||||
return {
|
||||
groups: _.differenceBy(allGroups, existingUserGroups, "id"),
|
||||
};
|
||||
}
|
||||
} else
|
||||
return {
|
||||
groups: allGroups,
|
||||
};
|
||||
},
|
||||
async ({ group: selectedGroup, groups }) => {
|
||||
if (selectedGroup) {
|
||||
|
@ -85,7 +93,9 @@ export const JoinGroupDialog = ({
|
|||
groups.forEach((group: Group) => {
|
||||
group.checked = !!selectedRows.find((r) => r.id === group.id);
|
||||
});
|
||||
setGroups(groups);
|
||||
id
|
||||
? setGroups(groups)
|
||||
: setGroups([...groups.filter((row) => !chips.includes(row.name))]);
|
||||
},
|
||||
errorHandler
|
||||
),
|
||||
|
@ -95,12 +105,14 @@ export const JoinGroupDialog = ({
|
|||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={`Join groups for user ${username}`}
|
||||
title={
|
||||
username ? t("users:joinGroupsFor") + username : t("users:selectGroups")
|
||||
}
|
||||
isOpen={open}
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="joinGroup"
|
||||
data-testid="join-button"
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
form="group-form"
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertVariant,
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
Switch,
|
||||
|
@ -18,18 +22,23 @@ import { useRealm } from "../context/realm-context/RealmContext";
|
|||
import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import moment from "moment";
|
||||
import { JoinGroupDialog } from "./JoinGroupDialog";
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
|
||||
export type UserFormProps = {
|
||||
form: UseFormMethods<UserRepresentation>;
|
||||
save: (user: UserRepresentation) => void;
|
||||
editMode: boolean;
|
||||
timestamp?: number;
|
||||
onGroupsUpdate: (groups: GroupRepresentation[]) => void;
|
||||
};
|
||||
|
||||
export const UserForm = ({
|
||||
form: { handleSubmit, register, errors, watch, control, setValue, reset },
|
||||
save,
|
||||
editMode,
|
||||
onGroupsUpdate,
|
||||
}: UserFormProps) => {
|
||||
const { t } = useTranslation("users");
|
||||
const { realm } = useRealm();
|
||||
|
@ -45,6 +54,14 @@ export const UserForm = ({
|
|||
|
||||
const watchUsernameInput = watch("username");
|
||||
const [timestamp, setTimestamp] = useState(null);
|
||||
const [chips, setChips] = useState<(string | undefined)[]>([]);
|
||||
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editMode) {
|
||||
|
@ -56,7 +73,7 @@ export const UserForm = ({
|
|||
handleError
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
}, [chips]);
|
||||
|
||||
const setupForm = (user: UserRepresentation) => {
|
||||
reset();
|
||||
|
@ -90,6 +107,50 @@ export const UserForm = ({
|
|||
setRequiredUserActionsDropdownOpen(false);
|
||||
};
|
||||
|
||||
const deleteItem = (id: string) => {
|
||||
const copyOfChips = chips;
|
||||
const copyOfGroups = selectedGroups;
|
||||
|
||||
setChips(copyOfChips.filter((item) => item !== id));
|
||||
setSelectedGroups(copyOfGroups.filter((item) => item.name !== id));
|
||||
onGroupsUpdate(selectedGroups);
|
||||
};
|
||||
|
||||
const addChips = async (groups: GroupRepresentation[]): Promise<void> => {
|
||||
const newSelectedGroups = groups;
|
||||
|
||||
const newGroupNames: (string | undefined)[] = newSelectedGroups!.map(
|
||||
(item) => item.name
|
||||
);
|
||||
setChips([...chips!, ...newGroupNames]);
|
||||
setSelectedGroups([...selectedGroups!, ...newSelectedGroups]);
|
||||
};
|
||||
|
||||
onGroupsUpdate(selectedGroups);
|
||||
|
||||
const addGroups = async (groups: GroupRepresentation[]): Promise<void> => {
|
||||
const newGroups = groups;
|
||||
|
||||
newGroups.forEach(async (group) => {
|
||||
try {
|
||||
await adminClient.users.addToGroup({
|
||||
id: id,
|
||||
groupId: group.id!,
|
||||
});
|
||||
addAlert(t("users:addedGroupMembership"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("users:addedGroupMembershipError", { error }),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const toggleModal = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
|
@ -97,6 +158,15 @@ export const UserForm = ({
|
|||
role="manage-users"
|
||||
className="pf-u-mt-lg"
|
||||
>
|
||||
{open && (
|
||||
<JoinGroupDialog
|
||||
open={open}
|
||||
onClose={() => setOpen(!open)}
|
||||
onConfirm={editMode ? addGroups : addChips}
|
||||
toggleDialog={() => toggleModal()}
|
||||
chips={chips}
|
||||
/>
|
||||
)}
|
||||
{editMode ? (
|
||||
<>
|
||||
<FormGroup
|
||||
|
@ -295,6 +365,53 @@ export const UserForm = ({
|
|||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{!editMode && (
|
||||
<FormGroup
|
||||
label={t("common:groups")}
|
||||
fieldId="kc-groups"
|
||||
validated={errors.requiredActions ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("requiredUserActionsHelpText")}
|
||||
forLabel={t("requiredUserActions")}
|
||||
forID="required-user-actions-label"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="groups"
|
||||
defaultValue={[]}
|
||||
typeAheadAriaLabel="Select an action"
|
||||
control={control}
|
||||
render={() => (
|
||||
<>
|
||||
<InputGroup>
|
||||
<ChipGroup categoryName={" "}>
|
||||
{chips.map((currentChip) => (
|
||||
<Chip
|
||||
key={currentChip}
|
||||
onClick={() => deleteItem(currentChip!)}
|
||||
>
|
||||
{currentChip}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
<Button
|
||||
id="kc-join-groups-button"
|
||||
onClick={toggleModal}
|
||||
variant="secondary"
|
||||
data-testid="join-groups-button"
|
||||
>
|
||||
{t("users:joinGroups")}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid={!editMode ? "create-user" : "save-user"}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
|||
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||
import { UserGroups } from "./UserGroups";
|
||||
import { UserConsents } from "./UserConsents";
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
|
||||
export const UsersTabs = () => {
|
||||
const { t } = useTranslation("roles");
|
||||
|
@ -28,6 +29,7 @@ export const UsersTabs = () => {
|
|||
const userForm = useForm<UserRepresentation>({ mode: "onChange" });
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [user, setUser] = useState("");
|
||||
const [addedGroups, setAddedGroups] = useState<GroupRepresentation[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const update = async () => {
|
||||
|
@ -39,13 +41,25 @@ export const UsersTabs = () => {
|
|||
setTimeout(update, 100);
|
||||
}, []);
|
||||
|
||||
const updateGroups = (groups: GroupRepresentation[]) => {
|
||||
setAddedGroups(groups);
|
||||
};
|
||||
|
||||
const save = async (user: UserRepresentation) => {
|
||||
try {
|
||||
if (id) {
|
||||
await adminClient.users.update({ id: user.id! }, user);
|
||||
addAlert(t("users:userSaved"), AlertVariant.success);
|
||||
} else {
|
||||
await adminClient.users.create(user);
|
||||
const getNewUserId = await adminClient.users.create(user);
|
||||
|
||||
addedGroups.forEach(async (group) => {
|
||||
await adminClient.users.addToGroup({
|
||||
id: getNewUserId.id!,
|
||||
groupId: group.id!,
|
||||
});
|
||||
});
|
||||
|
||||
addAlert(t("users:userCreated"), AlertVariant.success);
|
||||
history.push(url.substr(0, url.lastIndexOf("/")));
|
||||
}
|
||||
|
@ -70,7 +84,12 @@ export const UsersTabs = () => {
|
|||
data-testid="user-details-tab"
|
||||
title={<TabTitleText>{t("details")}</TabTitleText>}
|
||||
>
|
||||
<UserForm form={userForm} save={save} editMode={true} />
|
||||
<UserForm
|
||||
onGroupsUpdate={updateGroups}
|
||||
form={userForm}
|
||||
save={save}
|
||||
editMode={true}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
|
@ -88,7 +107,14 @@ export const UsersTabs = () => {
|
|||
</Tab>
|
||||
</KeycloakTabs>
|
||||
)}
|
||||
{!id && <UserForm form={userForm} save={save} editMode={false} />}
|
||||
{!id && (
|
||||
<UserForm
|
||||
onGroupsUpdate={updateGroups}
|
||||
form={userForm}
|
||||
save={save}
|
||||
editMode={false}
|
||||
/>
|
||||
)}
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,9 @@
|
|||
"noGroups": "No groups",
|
||||
"noGroupsText": "You haven't added this user to any groups. Join a group to get started.",
|
||||
"joinGroup": "Join Group",
|
||||
"joinGroups": "Join Groups",
|
||||
"joinGroupsFor": "Join groups for user ",
|
||||
"selectGroups": "Select groups to join",
|
||||
"searchForGroups": "Search for groups",
|
||||
"leave": "Leave",
|
||||
"leaveGroup": "Leave group {{name}}?",
|
||||
|
@ -56,7 +59,5 @@
|
|||
"consents": "Consents",
|
||||
"noConsents": "No consents",
|
||||
"noConsentsText": "The consents will only be recorded when users try to access a client that is configured to require consent. In that case, users will get a consent page which asks them to grant access to the client."
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,3 +6,17 @@ button.pf-c-button.pf-m-primary.kc-join-group-button {
|
|||
margin-left: var(--pf-global--spacer--md);
|
||||
margin-right: var(--pf-global--spacer--xl);
|
||||
}
|
||||
|
||||
.pf-c-chip-group,
|
||||
.pf-c-chip-group__list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button#kc-join-groups-button {
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
.pf-c-chip-group.pf-m-category {
|
||||
margin-right: var(--pf-global--spacer--md);
|
||||
padding: var(--pf-global--spacer--xs);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue