Skip to content

Commit

Permalink
feat(oh2-287): admin user group crud (#637)
Browse files Browse the repository at this point in the history
* feat(oh2-299): new user form

* feat: use UserGroupDTO as the form type rule

* feat: reset form

* feat: add user to backend and redirect

* feat: wait for the user to be saved before changing page

* feat: add translations to validations

* fix: customize username validation message

* fix: validation strings & touched

* feat(oh2-287): admin userGroups list

* feat: new group (without permissions)

* fix: show the correct tab after adding a group

* feat: display group permissions

* fix: display users by default

* feat: add mocks for permissions

* feat: display crud as a table

* feat: display acl details on hover

* OH2-326 | Tests | Add cypress e2e tests to cover admin/diseases (#622)

* tests:OH2-326 | Tests | Add cypress e2e tests to cover admin/diseases

* chore:Improve cypress indexing

* OH2-325 | Fix possibly bad designs (#620)

* fix:OH2-325 | design issues in wards admin

* chore:fix design issues in admin components

* fix:fix diseases types tests

* OH2-331 |Tests / Add cypress e2e tests to cover admission types (admin/types/admissions) (#627)

* tests:Tests | Add cypress e2e tests to cover admission types (admin/types/admissions)

* fix:check mode before setting it

* chore:OH2-329 | Tests / Add cypress e2e tests to cover admin/vaccines (#626)

* OH2-333 |   Add cypress e2e tests to cover discharge types (admin/types/discharges) (#630)

* Add cypress e2e tests to cover discharge types (admin/types/discharges)

* Correction des reviews

* File organisation

* OH2-357 | Add cypress e2e tests to cover medical types (admin/types/medicals) (#632)

* Add cypress e2e tests to cover medical types (admin/types/medicals)

* Correction de typo

* Correction des attributs mal renseignes

* Remane medical file

* Files organisation

* tests:OH2-356 | Tests / Add cypress e2e tests to cover exam types (admin/types/exams) (#631)

* OH-332  | Add cypress e2e tests to cover delivery types (admin/types/de… (#629)

* Tests | Add cypress e2e tests to cover delivery types (admin/types/deliveries)

* Ajout de la suppression

* Review corrections

* Files organisation

* OH2-328 | Tests | Add cypress e2e tests to cover admin/operations (#623)

* tests:OH2-326 | Tests | Add cypress e2e tests to cover admin/diseases

* tests:OH2-328 | Tests | Add cypress e2e tests to cover admin/operations

* chore:improve cypress indexing

* OH2-355 | Tests / Add cypress e2e tests to cover diseases types (admin/types/diseases) (#628)

* tests:OH2-355 | Tests / Add cypress e2e tests to cover diseases types (admin/types/diseases)

* chore: code quality improvement

* OH2-335 | Manager enabled/disabled diseases (#624)

* feature(OH2-335): Manager enabled/disabled diseases

* fix: Fix e2e tests

* update: Update diseases e2e tests

* fix: Fix disabled diseases e2e test

* styles(ADMIN/Diseases): Update responsiveness

* feat(oh2-299): new user form (#621)

* feat(oh2-299): new user form

* feat: use UserGroupDTO as the form type rule

* feat: reset form

* feat: add user to backend and redirect

* feat: wait for the user to be saved before changing page

* feat: add translations to validations

* fix: customize username validation message

* fix: validation strings & touched

* feat: add success modal

* fix: added user description

* feat: confirm password

* fix: error message + icon

* fix: cancel instead of reset + error goes back to form

* feat: track permission changes

* feat: update permissions

* fix: update group

* chore: optimize queries

* feat: add confirmation modals

* chore: add strings to translations

* feat: delete groups

* fix: adapt to redux toolkit

* chore: update oh.yaml from api+ generated

* chore: update yml file (before api#468)

informatici/openhospital-api#468

* chore: update oh.yaml from api + generated

* fix: remove deprecated permission update action

* feat: update logic to fit the new api

* fix: wording in permissions summary

* chore: reset runtime file

* chore: reset runtime file

* chore:apply changes. See #issuecomment-2363053731

---------

Co-authored-by: Silevester Dongmo <[email protected]>
Co-authored-by: fogouang <[email protected]>
Co-authored-by: Steve Tsala <[email protected]>
Co-authored-by: SteveGT96 <[email protected]>
Co-authored-by: SilverD3 <[email protected]>
  • Loading branch information
6 people authored Oct 23, 2024
1 parent d5daf9e commit 65d2c1c
Show file tree
Hide file tree
Showing 46 changed files with 1,559 additions and 140 deletions.
40 changes: 33 additions & 7 deletions src/components/accessories/admin/users/Users.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,49 @@
import { Tab, Tabs } from "@mui/material";
import React, { useState } from "react";
import React from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import { useLocation, useNavigate } from "react-router";

import Button from "../../button/Button";
import UserGroupsTable from "./userGroupsTable";
import UsersTable from "./usersTable";

import { PATHS } from "../../../../consts";
import { UserDTO, UserGroupDTO } from "../../../../generated";

import { UserDTO } from "../../../../generated";
export enum TabOptions {
"users" = "users",
"groups" = "groups",
}

export const Users = () => {
const navigate = useNavigate();
const { t } = useTranslation();

const { state }: { state: { tab?: TabOptions } } = useLocation();
const setTab = (tab: TabOptions) =>
navigate(PATHS.admin_users, { state: { tab } });

const handleEditGroup = (row: UserGroupDTO) =>
navigate(PATHS.admin_usergroups_edit.replace(":id", row.code!), {
state: row,
});

const handleEditUser = (row: UserDTO) =>
navigate(PATHS.admin_users_edit.replace(":id", row.userName!), {
state: row,
});

const [tab, setTab] = useState<"users" | "groups">("users");
return (
<>
<Tabs
value={tab}
value={state?.tab ?? TabOptions.users}
onChange={(_, value) => setTab(value)}
aria-label="switch between users and groups"
>
<Tab label={t("user.users")} value="users" />
<Tab label={t("user.groups")} value="groups" />
</Tabs>
{tab === "users" ? (
{state?.tab !== TabOptions.groups ? (
<UsersTable
headerActions={
<Button
Expand All @@ -48,7 +60,21 @@ export const Users = () => {
onEdit={handleEditUser}
/>
) : (
<UserGroupsTable />
<UserGroupsTable
headerActions={
<Button
onClick={() => {
navigate(PATHS.admin_usergroups_new);
}}
type="button"
variant="contained"
color="primary"
>
{t("user.addGroup")}
</Button>
}
onEdit={handleEditGroup}
/>
)}
</>
);
Expand Down
271 changes: 271 additions & 0 deletions src/components/accessories/admin/users/editGroup/EditGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { useFormik } from "formik";
import { useAppDispatch, useAppSelector } from "libraries/hooks/redux";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Navigate, useLocation, useParams } from "react-router";
import { useNavigate } from "react-router-dom";

import checkIcon from "../../../../../assets/check-icon.png";
import Button from "../../../button/Button";
import ConfirmationDialog from "../../../confirmationDialog/ConfirmationDialog";
import InfoBox from "../../../infoBox/InfoBox";
import TextField from "../../../textField/TextField";

import { PATHS } from "../../../../../consts";
import { PermissionDTO, UserGroupDTO } from "../../../../../generated";
import { usePermission } from "../../../../../libraries/permissionUtils/usePermission";

import { CircularProgress } from "@mui/material";
import { getAllPermissions } from "../../../../../state/permissions";
import {
getUserGroup,
updateUserGroup,
updateUserGroupReset,
} from "../../../../../state/usergroups";
import { GroupPermissionsEditor } from "../editPermissions/GroupPermissionsEditor";
import {
PermissionActionEnum,
PermissionActionType,
comparePermissions,
} from "../editPermissions/permission.utils";
import { TabOptions } from "../Users";
import "./styles.scss";
import { userGroupSchema } from "./validation";

export const EditGroup = () => {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const navigate = useNavigate();
const { state }: { state: UserGroupDTO } = useLocation();
const { id } = useParams();
const canUpdatePermissions = usePermission("grouppermission.update");

const update = useAppSelector((state) => state.usergroups.update);
const permissions = useAppSelector((state) => state.permissions.getAll);
const group = useAppSelector((state) => state.usergroups.currentGroup);

// local state to keep track of permissions
const [groupPermissions, setGroupPermissions] = useState<PermissionDTO[]>([]);
const [dirtyPermissions, setDirtyPermissions] = useState<boolean>(false);

// make sure everything is loaded before displaying the editor
const [isPermissionEditorAvailable, setIsPermissionEditorAvailable] =
useState<boolean>(false);

// keep track of which permissions have been updated and how
const [updatedPermissionsStack, setUpdatedPermissionsStack] = useState<
Array<PermissionActionType>
>([]);

const handleUpdatePermissions = ({
permission,
action,
}: PermissionActionType) => {
const otherPermissions = groupPermissions.filter(
(p) => p.id !== permission.id
);

if (action === PermissionActionEnum.REVOKE) {
setGroupPermissions(otherPermissions);
}
if (action === PermissionActionEnum.ASSIGN) {
setGroupPermissions([...otherPermissions, permission]);
}
};

const {
handleSubmit,
handleBlur,
getFieldProps,
isValid,
dirty,
resetForm,
errors,
touched,
} = useFormik({
initialValues: state,
validationSchema: userGroupSchema(t),
onSubmit: (values: UserGroupDTO) => {
values.permissions = groupPermissions;
const dto: UserGroupDTO = { ...values, permissions: groupPermissions };

dispatch(updateUserGroup(dto));
},
});

// load permissions and group on mount
useEffect(() => {
dispatch(getAllPermissions());
dispatch(getUserGroup(state.code));
return () => {
dispatch(updateUserGroupReset());
};
}, [dispatch, state.code]);

// update group permissions on group load
useEffect(() => {
if (group.data) {
setGroupPermissions(group.data.permissions ?? []);
}
}, [group.data]);

// compare permissions to update the update stack
// and display permissions when ready
useEffect(() => {
if (canUpdatePermissions && group.data && permissions.data) {
setIsPermissionEditorAvailable(true);

const newPermissionStack = comparePermissions(
permissions.data,
group.data?.permissions ?? [],
groupPermissions
);

setUpdatedPermissionsStack(newPermissionStack);
}
}, [canUpdatePermissions, group.data, permissions.data, groupPermissions]);

if (state?.code !== id) {
return <Navigate to={PATHS.admin_users} state={{ tab: "groups" }} />;
}

const handleFormReset = () => {
resetForm();
setGroupPermissions(group.data?.permissions ?? []);
};

if (permissions.hasFailed)
return (
<InfoBox
type="error"
message={`Unable to load permissions ${permissions.error?.toString()}`}
/>
);
if (group.hasFailed)
return (
<InfoBox
type="error"
message={`Unable to load permissions ${permissions.error?.toString()}`}
/>
);

return (
<>
{group.status === "LOADING" || permissions.status === "LOADING" ? (
<CircularProgress style={{ marginLeft: "50%", position: "relative" }} />
) : (
<div className="newGroupForm">
<form className="newGroupForm__form" onSubmit={handleSubmit}>
<div className="row start-sm center-xs">
<div className="newGroupForm__item fullWidth">
<TextField
field={getFieldProps("code")}
theme="regular"
label={t("user.code")}
isValid={!!touched.code && !!errors.code}
errorText={(touched.code && errors.code) || ""}
onBlur={handleBlur}
type="text"
disabled
/>
</div>
<div className="newGroupForm__item fullWidth">
<TextField
field={getFieldProps("desc")}
theme="regular"
label={t("user.desc")}
isValid={!!touched.desc && !!errors.desc}
errorText={(touched.desc && errors.desc) || ""}
onBlur={handleBlur}
/>
</div>
</div>

{isPermissionEditorAvailable && (
<GroupPermissionsEditor
permissions={permissions.data ?? []}
groupPermissions={groupPermissions}
setDirty={setDirtyPermissions}
update={handleUpdatePermissions}
/>
)}

<div className="newGroupForm__item fullWidth">
{isPermissionEditorAvailable &&
updatedPermissionsStack.length > 0 && (
<p>
<code>
Editing permissions:{" "}
{updatedPermissionsStack
.map(
(p) =>
`${p.permission.name}: ${
p.action === PermissionActionEnum.ASSIGN
? "assign"
: "revoked"
}`
)
.join(",")}
</code>
<br />
{updatedPermissionsStack.length} permission
{updatedPermissionsStack.length > 1 ? "s" : ""} will be
updated.
</p>
)}
{update.hasFailed && (
<div className="info-box-container">
<InfoBox
type="error"
message={
update.error?.message ?? t("common.somethingwrong")
}
/>
</div>
)}
</div>

<div className="newGroupForm__buttonSet">
<div className="submit_button">
<Button
type="submit"
variant="contained"
disabled={
!!update.isLoading ||
!isValid ||
!(dirty || dirtyPermissions)
}
>
{t("common.save")}
</Button>
</div>
<div className="reset_button">
<Button
type="reset"
variant="text"
disabled={!!update.isLoading || !(dirty || dirtyPermissions)}
onClick={handleFormReset}
>
{t("common.reset")}
</Button>
</div>
</div>
</form>
<ConfirmationDialog
isOpen={update.hasSucceeded}
title={t("user.groupUpdated")}
icon={checkIcon}
info={t("user.groupUpdateSuccess")}
primaryButtonLabel="Ok"
handlePrimaryButtonClick={() => {
navigate(PATHS.admin_users, {
state: { tab: TabOptions.groups },
});
}}
handleSecondaryButtonClick={() => ({})}
/>
</div>
)}
</>
);
};
1 change: 1 addition & 0 deletions src/components/accessories/admin/users/editGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EditGroup } from "./EditGroup";
Loading

0 comments on commit 65d2c1c

Please sign in to comment.