From aff944e0199a563d3e1dbb41390b356ad05b2de1 Mon Sep 17 00:00:00 2001 From: Johanna Date: Fri, 1 Sep 2023 10:08:54 -0400 Subject: [PATCH 01/26] Rough implementation of manage user access --- frontend/package.json | 2 +- .../src/app/Settings/Users/CreateUserForm.tsx | 11 +- .../app/Settings/Users/DeleteUserModal.tsx | 3 +- .../app/Settings/Users/EditUserEmailModal.tsx | 4 +- .../app/Settings/Users/EditUserNameModal.tsx | 4 +- .../app/Settings/Users/ResetUserMfaModal.tsx | 3 +- .../Settings/Users/ResetUserPasswordModal.tsx | 3 +- .../app/Settings/Users/UserDetailUtils.tsx | 3 +- .../Settings/Users/UserFacilitiesSettings.tsx | 49 +++--- .../src/app/Settings/Users/UserInfoTab.tsx | 3 +- .../Settings/Users/UserRoleSettingsForm.tsx | 2 +- .../src/app/commonComponents/Checkboxes.tsx | 3 + .../UserDetails/ReactivateUserModal.tsx | 3 +- .../ResendActivationEmailModal.tsx | 3 +- .../UserDetails/SpecialStatusNotice.tsx | 3 +- .../UserDetails/UndeleteUserModal.tsx | 3 +- .../UserDetails/UserHeading.tsx | 11 +- .../UserDetails/UserOrganizationFormField.tsx | 101 ++++++++++++ .../UserDetails/UserRoleFormField.tsx | 38 +++++ .../AddOrganizationAdminForm.tsx | 2 +- .../Components/OrganizationComboDropdown.tsx | 8 +- .../ManageFacility/operations.graphql | 2 + .../ManageUsers/AdminManageUser.tsx | 102 ++++++------ .../ManageUsers/UserAccessTab.tsx | 151 ++++++++++++++++++ .../ManageUsers/operations.graphql | 13 ++ frontend/src/generated/graphql.tsx | 89 +++++++++++ frontend/src/scss/base/_prime-styles.scss | 17 ++ frontend/yarn.lock | 8 +- 28 files changed, 533 insertions(+), 111 deletions(-) create mode 100644 frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx create mode 100644 frontend/src/app/commonComponents/UserDetails/UserRoleFormField.tsx create mode 100644 frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx diff --git a/frontend/package.json b/frontend/package.json index 277c42f1bf..abfbf56b27 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,7 +38,7 @@ "react": "^18.0.0", "react-csv": "^2.2.1", "react-dom": "^18.0.0", - "react-hook-form": "^7.45.2", + "react-hook-form": "^7.45.4", "react-i18next": "^13.2.0", "react-modal": "^3.16.1", "react-redux": "^8.0.5", diff --git a/frontend/src/app/Settings/Users/CreateUserForm.tsx b/frontend/src/app/Settings/Users/CreateUserForm.tsx index 75fe7d5f7e..9025b2d5e5 100644 --- a/frontend/src/app/Settings/Users/CreateUserForm.tsx +++ b/frontend/src/app/Settings/Users/CreateUserForm.tsx @@ -2,6 +2,7 @@ import React from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSelector } from "react-redux"; import { useForm } from "react-hook-form"; +import { FieldError } from "react-hook-form/dist/types/errors"; import Button from "../../commonComponents/Button/Button"; import Dropdown from "../../commonComponents/Dropdown"; @@ -84,7 +85,10 @@ const CreateUserForm: React.FC = ({ }); const formCurrentValues = watch(); return ( -
+

Invite new user @@ -109,7 +113,6 @@ const CreateUserForm: React.FC = ({ validationStatus={errors?.firstName?.type ? "error" : undefined} errorMessage={errors?.firstName?.message} /> -
= ({ />

diff --git a/frontend/src/app/Settings/Users/DeleteUserModal.tsx b/frontend/src/app/Settings/Users/DeleteUserModal.tsx index 29ec20bba5..7e0ffc2514 100644 --- a/frontend/src/app/Settings/Users/DeleteUserModal.tsx +++ b/frontend/src/app/Settings/Users/DeleteUserModal.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Button from "../../commonComponents/Button/Button"; import { displayFullName } from "../../utils"; +import { User } from "../../../generated/graphql"; import { SettingsUser } from "./ManageUsersContainer"; import "./ManageUsers.scss"; @@ -11,7 +12,7 @@ import "./ManageUsers.scss"; interface Props { onClose: () => void; onDeleteUser: (userId: string) => void; - user: SettingsUser; + user: SettingsUser | User; } const DeleteUserModal: React.FC = ({ onClose, onDeleteUser, user }) => { diff --git a/frontend/src/app/Settings/Users/EditUserEmailModal.tsx b/frontend/src/app/Settings/Users/EditUserEmailModal.tsx index 5a39175ea2..2adab42c92 100644 --- a/frontend/src/app/Settings/Users/EditUserEmailModal.tsx +++ b/frontend/src/app/Settings/Users/EditUserEmailModal.tsx @@ -4,10 +4,10 @@ import { useForm } from "react-hook-form"; import { displayFullName } from "../../utils"; import { emailRegex } from "../../utils/email"; import { TextInput } from "../../commonComponents/TextInput"; +import { User } from "../../../generated/graphql"; import { SettingsUser } from "./ManageUsersContainer"; import BaseEditModal from "./BaseEditModal"; - import "./ManageUsers.scss"; type EmailFormData = { @@ -16,7 +16,7 @@ type EmailFormData = { interface EditUserEmailModalProps { onClose: () => void; onEditUserEmail: (userId: string, emailAddress: string) => void; - user: SettingsUser; + user: SettingsUser | User; } const EditUserEmailModal: React.FC = ({ diff --git a/frontend/src/app/Settings/Users/EditUserNameModal.tsx b/frontend/src/app/Settings/Users/EditUserNameModal.tsx index ee316c5d87..9cf4d8da00 100644 --- a/frontend/src/app/Settings/Users/EditUserNameModal.tsx +++ b/frontend/src/app/Settings/Users/EditUserNameModal.tsx @@ -3,10 +3,10 @@ import { useForm } from "react-hook-form"; import { displayFullName } from "../../utils"; import { TextInput } from "../../commonComponents/TextInput"; +import { User } from "../../../generated/graphql"; import { SettingsUser } from "./ManageUsersContainer"; import { BaseEditModal } from "./BaseEditModal"; - import "./ManageUsers.scss"; type UserNameFormData = { @@ -22,7 +22,7 @@ interface EditUserNameModalProps { lastName: string, suffix: string ) => void; - user: SettingsUser; + user: SettingsUser | User; } const EditUserNameModal: React.FC = ({ diff --git a/frontend/src/app/Settings/Users/ResetUserMfaModal.tsx b/frontend/src/app/Settings/Users/ResetUserMfaModal.tsx index 6166ee941c..e413a826aa 100644 --- a/frontend/src/app/Settings/Users/ResetUserMfaModal.tsx +++ b/frontend/src/app/Settings/Users/ResetUserMfaModal.tsx @@ -3,6 +3,7 @@ import Modal from "react-modal"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Button from "../../commonComponents/Button/Button"; +import { User } from "../../../generated/graphql"; import { displayFullName } from "../../utils"; import { SettingsUser } from "./ManageUsersContainer"; @@ -11,7 +12,7 @@ import "./ManageUsers.scss"; interface Props { onClose: () => void; onResetMfa: (userId: string) => void; - user: SettingsUser; + user: SettingsUser | User; } const ResetUserMfaModal: React.FC = ({ onClose, onResetMfa, user }) => { diff --git a/frontend/src/app/Settings/Users/ResetUserPasswordModal.tsx b/frontend/src/app/Settings/Users/ResetUserPasswordModal.tsx index c6ce916269..124589a962 100644 --- a/frontend/src/app/Settings/Users/ResetUserPasswordModal.tsx +++ b/frontend/src/app/Settings/Users/ResetUserPasswordModal.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Button from "../../commonComponents/Button/Button"; import { displayFullName } from "../../utils"; +import { User } from "../../../generated/graphql"; import { SettingsUser } from "./ManageUsersContainer"; import "./ManageUsers.scss"; @@ -11,7 +12,7 @@ import "./ManageUsers.scss"; interface Props { onClose: () => void; onResetPassword: (userId: string) => void; - user: SettingsUser; + user: SettingsUser | User; } const ResetUserPasswordModal: React.FC = ({ diff --git a/frontend/src/app/Settings/Users/UserDetailUtils.tsx b/frontend/src/app/Settings/Users/UserDetailUtils.tsx index e632c8e9f2..134983f4e0 100644 --- a/frontend/src/app/Settings/Users/UserDetailUtils.tsx +++ b/frontend/src/app/Settings/Users/UserDetailUtils.tsx @@ -1,8 +1,9 @@ import { OktaUserStatus } from "../../utils/user"; +import { User } from "../../../generated/graphql"; import { SettingsUser } from "./ManageUsersContainer"; -export const isUserActive = (user: SettingsUser) => +export const isUserActive = (user: SettingsUser | User) => user.status !== OktaUserStatus.SUSPENDED && user.status !== OktaUserStatus.PROVISIONED && !user.isDeleted; diff --git a/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx b/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx index 7d9eb87b33..42a8a4e8df 100644 --- a/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx +++ b/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx @@ -1,31 +1,32 @@ import React from "react"; -import { FieldErrors, UseFormRegister, UseFormSetValue } from "react-hook-form"; +import { UseFormRegister, UseFormSetValue } from "react-hook-form"; +import { FieldError } from "react-hook-form/dist/types/errors"; import Checkboxes from "../../commonComponents/Checkboxes"; -import Required from "../../commonComponents/Required"; import { UserFacilitySetting } from "./ManageUsersContainer"; import "./ManageUsers.scss"; -import { CreateUser } from "./CreateUserSchema"; import { alphabeticalFacilitySort } from "./UserFacilitiesSettingsForm"; interface UserFacilitiesSettingProps { - formValues: CreateUser; + roleSelected: string; allFacilities: UserFacilitySetting[]; - register: UseFormRegister; - errors: FieldErrors; - setValue: UseFormSetValue; + register: UseFormRegister; + error?: FieldError; // ToDo change this to something more specific + setValue: UseFormSetValue; + disabled?: boolean; } // This is the react-hook-form supported version of UserFacilitiesSettingsForm. const UserFacilitiesSettings: React.FC = ({ - formValues, + roleSelected, allFacilities, register, - errors, + error, setValue, + disabled, }) => { - const isAdmin = formValues.role === "ADMIN"; + const isAdmin = roleSelected === "ADMIN"; const facilityAccessDescription = isAdmin ? "Admins have access to all facilities" : "All users must have access to at least one facility"; @@ -63,23 +64,17 @@ const UserFacilitiesSettings: React.FC = ({ ]; return ( - <> -

- -

-

- {facilityAccessDescription} -

- - + ); }; diff --git a/frontend/src/app/Settings/Users/UserInfoTab.tsx b/frontend/src/app/Settings/Users/UserInfoTab.tsx index 9461ea873d..bc8fb3102a 100644 --- a/frontend/src/app/Settings/Users/UserInfoTab.tsx +++ b/frontend/src/app/Settings/Users/UserInfoTab.tsx @@ -2,6 +2,7 @@ import classnames from "classnames"; import React, { useState } from "react"; import Button from "../../commonComponents/Button/Button"; +import { User } from "../../../generated/graphql"; import { SettingsUser } from "./ManageUsersContainer"; import DeleteUserModal from "./DeleteUserModal"; @@ -12,7 +13,7 @@ import EditUserEmailModal from "./EditUserEmailModal"; import "./UserInfoTab.scss"; interface UserInfoTabProps { - user: SettingsUser; + user: SettingsUser | User; isUserActive: boolean; isUserSelf?: boolean; isUpdating: boolean; diff --git a/frontend/src/app/Settings/Users/UserRoleSettingsForm.tsx b/frontend/src/app/Settings/Users/UserRoleSettingsForm.tsx index 2686c3a03a..f10465f2f8 100644 --- a/frontend/src/app/Settings/Users/UserRoleSettingsForm.tsx +++ b/frontend/src/app/Settings/Users/UserRoleSettingsForm.tsx @@ -12,7 +12,7 @@ interface RoleButton { labelDescription: string; } -const ROLES: RoleButton[] = [ +export const ROLES: RoleButton[] = [ { value: "ADMIN", label: "Admin", diff --git a/frontend/src/app/commonComponents/Checkboxes.tsx b/frontend/src/app/commonComponents/Checkboxes.tsx index b736004cee..3ff4c789cc 100644 --- a/frontend/src/app/commonComponents/Checkboxes.tsx +++ b/frontend/src/app/commonComponents/Checkboxes.tsx @@ -17,6 +17,7 @@ interface Props { boxes: Checkbox[]; legend: React.ReactNode; legendSrOnly?: boolean; + hintText?: string; name: string; disabled?: boolean; className?: string; @@ -38,6 +39,7 @@ const Checkboxes = (props: Props) => { errorMessage, required, inputRef, + hintText, } = props; return ( @@ -59,6 +61,7 @@ const Checkboxes = (props: Props) => { > {required ? : } + {hintText && {hintText}} {validationStatus === "error" && (
Error: diff --git a/frontend/src/app/commonComponents/UserDetails/ReactivateUserModal.tsx b/frontend/src/app/commonComponents/UserDetails/ReactivateUserModal.tsx index 9c8a8450d4..de844efdbc 100644 --- a/frontend/src/app/commonComponents/UserDetails/ReactivateUserModal.tsx +++ b/frontend/src/app/commonComponents/UserDetails/ReactivateUserModal.tsx @@ -8,11 +8,12 @@ import { displayFullName } from "../../utils"; import { RootState } from "../../store"; import { SettingsUser } from "../../Settings/Users/ManageUsersContainer"; import "../../Settings/Users/ManageUsers.scss"; +import { User } from "../../../generated/graphql"; interface Props { onClose: () => void; onReactivateUser: (userId: string) => void; - user: SettingsUser; + user: SettingsUser | User; isOpen: boolean; } diff --git a/frontend/src/app/commonComponents/UserDetails/ResendActivationEmailModal.tsx b/frontend/src/app/commonComponents/UserDetails/ResendActivationEmailModal.tsx index 14cf1df7b6..de47a1c2cc 100644 --- a/frontend/src/app/commonComponents/UserDetails/ResendActivationEmailModal.tsx +++ b/frontend/src/app/commonComponents/UserDetails/ResendActivationEmailModal.tsx @@ -6,11 +6,12 @@ import Button from "../Button/Button"; import { displayFullName } from "../../utils"; import { SettingsUser } from "../../Settings/Users/ManageUsersContainer"; import "../../Settings/Users/ManageUsers.scss"; +import { User } from "../../../generated/graphql"; interface Props { onClose: () => void; onResendActivationEmail: (userId: string) => void; - user: SettingsUser; + user: SettingsUser | User; isOpen: boolean; } diff --git a/frontend/src/app/commonComponents/UserDetails/SpecialStatusNotice.tsx b/frontend/src/app/commonComponents/UserDetails/SpecialStatusNotice.tsx index d57b38a83d..1051460c5f 100644 --- a/frontend/src/app/commonComponents/UserDetails/SpecialStatusNotice.tsx +++ b/frontend/src/app/commonComponents/UserDetails/SpecialStatusNotice.tsx @@ -3,13 +3,14 @@ import React, { ReactNode, useState } from "react"; import { SettingsUser } from "../../Settings/Users/ManageUsersContainer"; import { OktaUserStatus } from "../../utils/user"; import Button from "../Button/Button"; +import { User } from "../../../generated/graphql"; import ReactivateUserModal from "./ReactivateUserModal"; import ResendActivationEmailModal from "./ResendActivationEmailModal"; import UndeleteUserModal from "./UndeleteUserModal"; export const SpecialStatusNotice: React.FC<{ - user: SettingsUser; + user: SettingsUser | User; isUpdating: boolean; onResendUserActivationEmail: (userId: string) => void; onReactivateUser: (userId: string) => void; diff --git a/frontend/src/app/commonComponents/UserDetails/UndeleteUserModal.tsx b/frontend/src/app/commonComponents/UserDetails/UndeleteUserModal.tsx index 7fe19b01ce..cdf467a159 100644 --- a/frontend/src/app/commonComponents/UserDetails/UndeleteUserModal.tsx +++ b/frontend/src/app/commonComponents/UserDetails/UndeleteUserModal.tsx @@ -5,11 +5,12 @@ import Modal from "../Modal"; import { displayFullName } from "../../utils"; import { SettingsUser } from "../../Settings/Users/ManageUsersContainer"; import "../../Settings/Users/ManageUsers.scss"; +import { User } from "../../../generated/graphql"; interface Props { onClose: () => void; onUndeleteUser: () => void; - user: SettingsUser; + user: SettingsUser | User; isOpen: boolean; } diff --git a/frontend/src/app/commonComponents/UserDetails/UserHeading.tsx b/frontend/src/app/commonComponents/UserDetails/UserHeading.tsx index 59332658c3..8bd3579874 100644 --- a/frontend/src/app/commonComponents/UserDetails/UserHeading.tsx +++ b/frontend/src/app/commonComponents/UserDetails/UserHeading.tsx @@ -7,11 +7,14 @@ import { capitalizeText, formatUserStatus } from "../../utils/text"; import Alert from "../Alert"; import { ReactComponent as DeactivatedIcon } from "../../../img/account-deactivated.svg"; import { ReactComponent as PendingIcon } from "../../../img/account-pending.svg"; +import { User } from "../../../generated/graphql"; import { SpecialStatusNotice } from "./SpecialStatusNotice"; import "./UserHeading.scss"; -const NoFacilityWarning: React.FC<{ user: SettingsUser }> = ({ user }) => { +const NoFacilityWarning: React.FC<{ user: SettingsUser | User }> = ({ + user, +}) => { if ( user?.id && (!user?.organization?.testingFacility || @@ -28,7 +31,9 @@ const NoFacilityWarning: React.FC<{ user: SettingsUser }> = ({ user }) => { return null; }; -const UserStatusSubheading: React.FC<{ user: SettingsUser }> = ({ user }) => { +const UserStatusSubheading: React.FC<{ user: SettingsUser | User }> = ({ + user, +}) => { function getUserStatusText() { if (user.status === OktaUserStatus.ACTIVE) { return ( @@ -61,7 +66,7 @@ const UserStatusSubheading: React.FC<{ user: SettingsUser }> = ({ user }) => { }; const UserHeading: React.FC<{ - user: SettingsUser; + user: SettingsUser | User; isUserSelf?: boolean; isUpdating: boolean; onResendUserActivationEmail: (userId: string) => void; diff --git a/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx b/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx new file mode 100644 index 0000000000..e629e12358 --- /dev/null +++ b/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx @@ -0,0 +1,101 @@ +import { ComboBox, ComboBoxOption } from "@trussworks/react-uswds"; +import { Control, Controller } from "react-hook-form"; +import React, { useRef } from "react"; +import classnames from "classnames"; + +import Required from "../Required"; +import { useGetAllOrganizationsQuery } from "../../../generated/graphql"; + +interface OrganizationSelectFormFieldProps { + control?: Control; +} + +const UserOrganizationFormField: React.FC = ({ + control, +}) => { + /** + * Fetch organizations (on initial load) + */ + const renderVersion = useRef(Date.now().toString(10)); + + const { data: orgResponse, loading: loadingOrgs } = + useGetAllOrganizationsQuery({ + onCompleted: () => { + renderVersion.current = Date.now().toString(10); + }, + }); + + const orgOptions: ComboBoxOption[] = + orgResponse?.organizations?.map((org) => ({ + value: org.id, + label: org.name, + })) ?? []; + + /** + * Form integration + */ + /*const { + field: { onChange, value, name, ref }, + fieldState: { error }, + } = useController({ + name: "organizationId", + control, + rules: { required: "Organization is required" }, + });*/ + const describeText = (error: string | undefined) => + error ? `Error: ${error}` : undefined; + const label = "Organization access"; + const comboBoxId = "org-dropdown-select"; + + return ( + ( +
+ + {error && ( + + Error: {error?.message} + + )} + { + + } +
+ )} + name="organizationId" + control={control} + rules={{ required: "Organization is missing" }} + /> + ); +}; + +export default UserOrganizationFormField; diff --git a/frontend/src/app/commonComponents/UserDetails/UserRoleFormField.tsx b/frontend/src/app/commonComponents/UserDetails/UserRoleFormField.tsx new file mode 100644 index 0000000000..69fb4afb9a --- /dev/null +++ b/frontend/src/app/commonComponents/UserDetails/UserRoleFormField.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { UseFormRegisterReturn } from "react-hook-form"; + +import RadioGroup from "../RadioGroup"; +import { ROLES } from "../../Settings/Users/UserRoleSettingsForm"; + +interface UserRoleFormFieldProps { + value: string; + registrationProps: UseFormRegisterReturn; + error: any; +} + +const UserRoleFormField: React.FC = ({ + value, + error, + registrationProps, +}) => { + return ( +
+ +
+ ); +}; + +export default UserRoleFormField; diff --git a/frontend/src/app/supportAdmin/AddOrganizationAdmin/AddOrganizationAdminForm.tsx b/frontend/src/app/supportAdmin/AddOrganizationAdmin/AddOrganizationAdminForm.tsx index b8a3745966..5b16aad335 100644 --- a/frontend/src/app/supportAdmin/AddOrganizationAdmin/AddOrganizationAdminForm.tsx +++ b/frontend/src/app/supportAdmin/AddOrganizationAdmin/AddOrganizationAdminForm.tsx @@ -57,7 +57,7 @@ const AddOrganizationAdminForm = ({ }, }); const formCurrentValues = watch(); - + console.log("form data:", formCurrentValues); return (
diff --git a/frontend/src/app/supportAdmin/Components/OrganizationComboDropdown.tsx b/frontend/src/app/supportAdmin/Components/OrganizationComboDropdown.tsx index dd5eea70d2..824f049bd3 100644 --- a/frontend/src/app/supportAdmin/Components/OrganizationComboDropdown.tsx +++ b/frontend/src/app/supportAdmin/Components/OrganizationComboDropdown.tsx @@ -1,6 +1,7 @@ import React from "react"; import { ComboBox } from "@trussworks/react-uswds"; import { Control, Controller } from "react-hook-form"; +import classnames from "classnames"; import Required from "../../commonComponents/Required"; import { showError } from "../../utils/srToast"; @@ -67,9 +68,10 @@ const OrganizationComboDropDown: React.FC = ({ fieldState: { error }, }) => (
{error && ( diff --git a/frontend/src/app/supportAdmin/ManageFacility/operations.graphql b/frontend/src/app/supportAdmin/ManageFacility/operations.graphql index 6c50b64153..08b7003fdc 100644 --- a/frontend/src/app/supportAdmin/ManageFacility/operations.graphql +++ b/frontend/src/app/supportAdmin/ManageFacility/operations.graphql @@ -7,6 +7,8 @@ query GetAllOrganizations { query GetFacilitiesByOrgId($orgId: ID!) { organization(id: $orgId){ + id + externalId name type facilities { diff --git a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx index fa16980c59..00cb6665b2 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useDocumentTitle } from "../../utils/hooks"; import { @@ -11,8 +11,8 @@ import { useSetUserIsDeletedMutation, useUpdateUserNameMutation, useUndeleteUserMutation, + User, } from "../../../generated/graphql"; -import { SettingsUser } from "../../Settings/Users/ManageUsersContainer"; import { showSuccess } from "../../utils/srToast"; import { isUserActive } from "../../Settings/Users/UserDetailUtils"; import { displayFullName } from "../../utils"; @@ -20,6 +20,7 @@ import { OktaUserStatus } from "../../utils/user"; import UserInfoTab from "../../Settings/Users/UserInfoTab"; import UserHeading from "../../commonComponents/UserDetails/UserHeading"; +import UserAccessTab from "./UserAccessTab"; import { UserSearch } from "./UserSearch"; const userNotFoundError = ( @@ -44,13 +45,18 @@ const genericError = ( Please try again or contact the SimpleReport team for help.
); + export const AdminManageUser: React.FC = () => { useDocumentTitle("Manage Users"); - const [searchEmail, setSearchEmail] = useState(""); - const [foundUser, setFoundUser] = useState(); + //ToDo Remove this + const [searchEmail, setSearchEmail] = useState("sarah@example.com"); + useEffect(() => { + retrieveUser(); + }, []); + const [foundUser, setFoundUser] = useState(); const [displayedError, setDisplayedError] = useState(); const [navItemSelected, setNavItemSelected] = useState< - "User information" | "Organization access" + "User information" | "User access" >("User information"); const [getUserByEmail] = useFindUserByEmailLazyQuery({ fetchPolicy: "no-cache", @@ -66,6 +72,9 @@ export const AdminManageUser: React.FC = () => { const [resendUserActivationEmail] = useResendActivationEmailMutation(); const [undeleteUser] = useUndeleteUserMutation(); + /** + * Handlers + */ const handleUpdate = async (func: () => Promise) => { setIsUpdating(true); try { @@ -97,7 +106,7 @@ export const AdminManageUser: React.FC = () => { middleName, lastName, suffix, - } as SettingsUser); + } as User); const fullName = displayFullName(firstName, "", lastName); showSuccess("", `User name changed to ${fullName}`); }); @@ -114,7 +123,7 @@ export const AdminManageUser: React.FC = () => { setFoundUser({ ...foundUser, email: emailAddress, - } as SettingsUser); + } as User); }); }; const handleResetUserPassword = async (userId: string) => { @@ -165,7 +174,7 @@ export const AdminManageUser: React.FC = () => { ...foundUser, isDeleted: true, status: OktaUserStatus.SUSPENDED, - } as SettingsUser); + } as User); showSuccess("", `User account removed for ${fullName}`); }); }; @@ -184,7 +193,7 @@ export const AdminManageUser: React.FC = () => { setFoundUser({ ...foundUser, status: OktaUserStatus.ACTIVE, - } as SettingsUser); + } as User); showSuccess("", `${fullName} has been reactivated.`); }); }; @@ -239,7 +248,7 @@ export const AdminManageUser: React.FC = () => { setFoundUser(undefined); } else { setDisplayedError(undefined); - setFoundUser(data?.user as SettingsUser); + setFoundUser(data?.user as User); } } ); @@ -254,6 +263,15 @@ export const AdminManageUser: React.FC = () => { setFoundUser(undefined); }; + /** + * Tab content + */ + + const tabs: (typeof navItemSelected)[] = ["User information", "User access"]; + + /** + * HTML + */ return (
@@ -278,11 +296,7 @@ export const AdminManageUser: React.FC = () => { aria-live={"polite"} >
-
+
{ />
{navItemSelected === "User information" ? ( @@ -351,7 +345,7 @@ export const AdminManageUser: React.FC = () => { onDeleteUser={handleDeleteUser} /> ) : ( -
+ )}
diff --git a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx new file mode 100644 index 0000000000..fb7f516f24 --- /dev/null +++ b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx @@ -0,0 +1,151 @@ +import React, { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { FieldError } from "react-hook-form/dist/types/errors"; + +import Button from "../../commonComponents/Button/Button"; +import { UserFacilitySetting } from "../../Settings/Users/ManageUsersContainer"; +import UserFacilitiesSettings from "../../Settings/Users/UserFacilitiesSettings"; +import { + useGetFacilitiesByOrgIdLazyQuery, + User, + useUpdateUserPrivilegesAndGroupAccessMutation, + Role as MutationRole, +} from "../../../generated/graphql"; +import UserOrganizationFormField from "../../commonComponents/UserDetails/UserOrganizationFormField"; +import UserRoleFormField from "../../commonComponents/UserDetails/UserRoleFormField"; +import { Role } from "../../permissions"; +import { showSuccess } from "../../utils/srToast"; + +/* +* username: String!, + orgExternalId: String!, + accessAllFacilities: Boolean = false, + facilities: [ID] = [], + role: Role!): User!*/ + +//MutationUpdateUserPrivilegesAndGroupAccessArgs +type UserAccessFormData = { + organizationId: string; + role: Role; + facilityIds: string[]; +}; + +export interface UserAccessTabProps { + user: User; + isUpdating: boolean; +} + +// set the form with react hook forms +// set the prompt warning of moving away from the component +// can the DOM be split? but each element need to be compatible +// with react hook forms +// the container is the one that has the logic to call the right mutation +// +const UserAccessTab: React.FC = ({ user }) => { + // retrieve organizations + // retrieve facilities by org + // set the defaults based on what the user has + + // maybe adding the prompt when a user triggers a new search? + + /** + * Form state setup + */ + const { + control, + handleSubmit, + formState: { errors, dirtyFields }, + watch, + register, + setValue, + } = useForm({ + defaultValues: { + organizationId: user.organization?.id || "", + facilityIds: + user.organization?.testingFacility.map((facility) => facility.id) || [], + role: user.role || "USER", + }, + }); + + // ToDo revisting why this is needed here but no in create user + const isDirtyAlt = !!Object.keys(dirtyFields).length; + + const formCurrentValues = watch(); + + /** + * Fetch facilities + */ + const [ + queryGetFacilitiesByOrgId, + { data: facilitiesResponse, loading: loadingFacilities }, + ] = useGetFacilitiesByOrgIdLazyQuery(); + + useEffect(() => { + if (formCurrentValues.organizationId) { + queryGetFacilitiesByOrgId({ + fetchPolicy: "no-cache", + variables: { + orgId: formCurrentValues.organizationId, + }, + }); + } + }, [queryGetFacilitiesByOrgId, formCurrentValues.organizationId]); + + const facilityList = formCurrentValues.organizationId + ? facilitiesResponse?.organization?.facilities.map( + (facility) => + ({ id: facility.id, name: facility.name } as UserFacilitySetting) + ) + : []; + /** + * Submit access updates + */ + + const [updateUserPrivilegesAndGroupAccess] = + useUpdateUserPrivilegesAndGroupAccessMutation(); + const onSubmit = async (userAccessData: UserAccessFormData) => { + console.log("submitting data", userAccessData); + await updateUserPrivilegesAndGroupAccess({ + variables: { + username: user.email, + role: userAccessData.role as MutationRole, + orgExternalId: userAccessData.organizationId, + accessAllFacilities: false, + facilities: userAccessData.facilityIds, + }, + }); + showSuccess("User updated", "hooray"); + // I might need to convert the orgID to externalOrgId before pushing for the update + }; + console.log("dirty fields: ", isDirtyAlt, dirtyFields); + return ( +
+ + + + +
+ ); +}; + +export default UserAccessTab; diff --git a/frontend/src/app/supportAdmin/ManageUsers/operations.graphql b/frontend/src/app/supportAdmin/ManageUsers/operations.graphql index 0221baa830..612c1e8d7f 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/operations.graphql +++ b/frontend/src/app/supportAdmin/ManageUsers/operations.graphql @@ -10,6 +10,7 @@ query findUserByEmail($email: String!) { email status organization { + id testingFacility { id name @@ -29,3 +30,15 @@ mutation undeleteUser($userId: ID!){ isDeleted } } + +mutation updateUserPrivilegesAndGroupAccess($username: String!, $orgExternalId: String!, $accessAllFacilities: Boolean!, $role: Role!, $facilities: [ID]){ + updateUserPrivilegesAndGroupAccess( + username:$username + orgExternalId: $orgExternalId + accessAllFacilities: $accessAllFacilities + facilities: $facilities + role: $role + ){ + id + } +} diff --git a/frontend/src/generated/graphql.tsx b/frontend/src/generated/graphql.tsx index e9c29ff1de..cd312cead0 100644 --- a/frontend/src/generated/graphql.tsx +++ b/frontend/src/generated/graphql.tsx @@ -1719,6 +1719,8 @@ export type GetFacilitiesByOrgIdQuery = { __typename?: "Query"; organization?: { __typename?: "Organization"; + id: string; + externalId: string; name: string; type: string; facilities: Array<{ @@ -1774,6 +1776,7 @@ export type FindUserByEmailQuery = { isDeleted?: boolean | null; organization?: { __typename?: "Organization"; + id: string; testingFacility: Array<{ __typename?: "Facility"; id: string; @@ -1797,6 +1800,21 @@ export type UndeleteUserMutation = { } | null; }; +export type UpdateUserPrivilegesAndGroupAccessMutationVariables = Exact<{ + username: Scalars["String"]; + orgExternalId: Scalars["String"]; + accessAllFacilities: Scalars["Boolean"]; + role: Role; + facilities?: InputMaybe< + Array> | InputMaybe + >; +}>; + +export type UpdateUserPrivilegesAndGroupAccessMutation = { + __typename?: "Mutation"; + updateUserPrivilegesAndGroupAccess: { __typename?: "User"; id: string }; +}; + export type GetPendingOrganizationsQueryVariables = Exact<{ [key: string]: never; }>; @@ -5172,6 +5190,8 @@ export type GetAllOrganizationsQueryResult = Apollo.QueryResult< export const GetFacilitiesByOrgIdDocument = gql` query GetFacilitiesByOrgId($orgId: ID!) { organization(id: $orgId) { + id + externalId name type facilities { @@ -5355,6 +5375,7 @@ export const FindUserByEmailDocument = gql` email status organization { + id testingFacility { id name @@ -5468,6 +5489,74 @@ export type UndeleteUserMutationOptions = Apollo.BaseMutationOptions< UndeleteUserMutation, UndeleteUserMutationVariables >; +export const UpdateUserPrivilegesAndGroupAccessDocument = gql` + mutation updateUserPrivilegesAndGroupAccess( + $username: String! + $orgExternalId: String! + $accessAllFacilities: Boolean! + $role: Role! + $facilities: [ID] + ) { + updateUserPrivilegesAndGroupAccess( + username: $username + orgExternalId: $orgExternalId + accessAllFacilities: $accessAllFacilities + facilities: $facilities + role: $role + ) { + id + } + } +`; +export type UpdateUserPrivilegesAndGroupAccessMutationFn = + Apollo.MutationFunction< + UpdateUserPrivilegesAndGroupAccessMutation, + UpdateUserPrivilegesAndGroupAccessMutationVariables + >; + +/** + * __useUpdateUserPrivilegesAndGroupAccessMutation__ + * + * To run a mutation, you first call `useUpdateUserPrivilegesAndGroupAccessMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateUserPrivilegesAndGroupAccessMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updateUserPrivilegesAndGroupAccessMutation, { data, loading, error }] = useUpdateUserPrivilegesAndGroupAccessMutation({ + * variables: { + * username: // value for 'username' + * orgExternalId: // value for 'orgExternalId' + * accessAllFacilities: // value for 'accessAllFacilities' + * role: // value for 'role' + * facilities: // value for 'facilities' + * }, + * }); + */ +export function useUpdateUserPrivilegesAndGroupAccessMutation( + baseOptions?: Apollo.MutationHookOptions< + UpdateUserPrivilegesAndGroupAccessMutation, + UpdateUserPrivilegesAndGroupAccessMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + UpdateUserPrivilegesAndGroupAccessMutation, + UpdateUserPrivilegesAndGroupAccessMutationVariables + >(UpdateUserPrivilegesAndGroupAccessDocument, options); +} +export type UpdateUserPrivilegesAndGroupAccessMutationHookResult = ReturnType< + typeof useUpdateUserPrivilegesAndGroupAccessMutation +>; +export type UpdateUserPrivilegesAndGroupAccessMutationResult = + Apollo.MutationResult; +export type UpdateUserPrivilegesAndGroupAccessMutationOptions = + Apollo.BaseMutationOptions< + UpdateUserPrivilegesAndGroupAccessMutation, + UpdateUserPrivilegesAndGroupAccessMutationVariables + >; export const GetPendingOrganizationsDocument = gql` query GetPendingOrganizations { pendingOrganizations { diff --git a/frontend/src/scss/base/_prime-styles.scss b/frontend/src/scss/base/_prime-styles.scss index d8e3bea5ee..017dae7baf 100644 --- a/frontend/src/scss/base/_prime-styles.scss +++ b/frontend/src/scss/base/_prime-styles.scss @@ -40,6 +40,10 @@ margin-left: 0; padding: 0; } + + .usa-form-group--error { + padding-left: units(1); + } } } @@ -1189,3 +1193,16 @@ $results-dropdown-spacing: #{units(4)} - #{units(2)} - 22px - #{units(4)}; // he abbr.usa-hint.usa-hint--required { text-decoration: none; } + +.manage-user-form__site-admin, +.create-user-form__org-admin { + .usa-form-group { + .usa-hint { + font-style: italic; + } + + .usa-legend { + font-weight: bold; + } + } +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1658b42815..004d1174f4 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -15990,10 +15990,10 @@ react-error-overlay@^6.0.11: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== -react-hook-form@^7.45.2: - version "7.45.2" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.45.2.tgz#c757f3d5e633ccb186443d57c10fc511df35721a" - integrity sha512-9s45OdTaKN+4NSTbXVqeDITd/nwIg++nxJGL8+OD5uf1DxvhsXQ641kaYHk5K28cpIOTYm71O/fYk7rFaygb3A== +react-hook-form@^7.45.4: + version "7.45.4" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.45.4.tgz#73d228b704026ae95d7e5f7b207a681b173ec62a" + integrity sha512-HGDV1JOOBPZj10LB3+OZgfDBTn+IeEsNOKiq/cxbQAIbKaiJUe/KV8DBUzsx0Gx/7IG/orWqRRm736JwOfUSWQ== react-i18next@^13.2.0: version "13.2.0" From 76da69e89dbc81b24b716a81a737fd8c145ef83b Mon Sep 17 00:00:00 2001 From: Johanna Date: Fri, 1 Sep 2023 10:18:12 -0400 Subject: [PATCH 02/26] removed comment --- frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx | 2 +- .../AddOrganizationAdmin/AddOrganizationAdminForm.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx b/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx index 42a8a4e8df..8ac3b7d9bc 100644 --- a/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx +++ b/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx @@ -12,7 +12,7 @@ interface UserFacilitiesSettingProps { roleSelected: string; allFacilities: UserFacilitySetting[]; register: UseFormRegister; - error?: FieldError; // ToDo change this to something more specific + error?: FieldError; setValue: UseFormSetValue; disabled?: boolean; } diff --git a/frontend/src/app/supportAdmin/AddOrganizationAdmin/AddOrganizationAdminForm.tsx b/frontend/src/app/supportAdmin/AddOrganizationAdmin/AddOrganizationAdminForm.tsx index 5b16aad335..b8a3745966 100644 --- a/frontend/src/app/supportAdmin/AddOrganizationAdmin/AddOrganizationAdminForm.tsx +++ b/frontend/src/app/supportAdmin/AddOrganizationAdmin/AddOrganizationAdminForm.tsx @@ -57,7 +57,7 @@ const AddOrganizationAdminForm = ({ }, }); const formCurrentValues = watch(); - console.log("form data:", formCurrentValues); + return (
From cd24cf6977507be7d8ac87829f77550b2393b55e Mon Sep 17 00:00:00 2001 From: Johanna Date: Fri, 1 Sep 2023 12:09:15 -0400 Subject: [PATCH 03/26] Happy path working --- .../ManageUsers/AdminManageUser.tsx | 2 +- .../ManageUsers/UserAccessTab.tsx | 71 ++++++++++++------- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx index 00cb6665b2..939c141062 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx @@ -49,7 +49,7 @@ const genericError = ( export const AdminManageUser: React.FC = () => { useDocumentTitle("Manage Users"); //ToDo Remove this - const [searchEmail, setSearchEmail] = useState("sarah@example.com"); + const [searchEmail, setSearchEmail] = useState("ruby@example.com"); useEffect(() => { retrieveUser(); }, []); diff --git a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx index fb7f516f24..2aee903ae0 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx @@ -15,15 +15,8 @@ import UserOrganizationFormField from "../../commonComponents/UserDetails/UserOr import UserRoleFormField from "../../commonComponents/UserDetails/UserRoleFormField"; import { Role } from "../../permissions"; import { showSuccess } from "../../utils/srToast"; +import { displayFullName } from "../../utils"; -/* -* username: String!, - orgExternalId: String!, - accessAllFacilities: Boolean = false, - facilities: [ID] = [], - role: Role!): User!*/ - -//MutationUpdateUserPrivilegesAndGroupAccessArgs type UserAccessFormData = { organizationId: string; role: Role; @@ -35,12 +28,9 @@ export interface UserAccessTabProps { isUpdating: boolean; } -// set the form with react hook forms -// set the prompt warning of moving away from the component -// can the DOM be split? but each element need to be compatible -// with react hook forms -// the container is the one that has the logic to call the right mutation -// +// Set the prompt warning of moving away from the component in manage user component +// Move all the react-hook-form logic to manage user component so the form is not lost when changing tabs +// Make sure the latest user information is retrieve after this update takes effect. const UserAccessTab: React.FC = ({ user }) => { // retrieve organizations // retrieve facilities by org @@ -48,6 +38,17 @@ const UserAccessTab: React.FC = ({ user }) => { // maybe adding the prompt when a user triggers a new search? + const getDefaultFacilities = () => { + const facilityIds: string[] = []; + + if (user.role === "ADMIN") { + facilityIds.push("ALL_FACILITIES"); + } + + return facilityIds.concat( + user.organization?.testingFacility.map((facility) => facility.id) || [] + ); + }; /** * Form state setup */ @@ -57,12 +58,12 @@ const UserAccessTab: React.FC = ({ user }) => { formState: { errors, dirtyFields }, watch, register, + reset, setValue, } = useForm({ defaultValues: { organizationId: user.organization?.id || "", - facilityIds: - user.organization?.testingFacility.map((facility) => facility.id) || [], + facilityIds: getDefaultFacilities(), role: user.role || "USER", }, }); @@ -97,27 +98,39 @@ const UserAccessTab: React.FC = ({ user }) => { ({ id: facility.id, name: facility.name } as UserFacilitySetting) ) : []; + /** * Submit access updates */ - - const [updateUserPrivilegesAndGroupAccess] = + const [updateUserPrivilegesAndGroupAccess, { loading: updatingPrivileges }] = useUpdateUserPrivilegesAndGroupAccessMutation(); const onSubmit = async (userAccessData: UserAccessFormData) => { - console.log("submitting data", userAccessData); + const allFacilityAccess = !!userAccessData.facilityIds.find( + (id) => id === "ALL_FACILITIES" + ); + await updateUserPrivilegesAndGroupAccess({ variables: { username: user.email, role: userAccessData.role as MutationRole, - orgExternalId: userAccessData.organizationId, - accessAllFacilities: false, - facilities: userAccessData.facilityIds, + orgExternalId: facilitiesResponse?.organization?.externalId || "", + accessAllFacilities: allFacilityAccess, + facilities: userAccessData.facilityIds.filter( + (id) => id !== "ALL_FACILITIES" + ), }, }); - showSuccess("User updated", "hooray"); - // I might need to convert the orgID to externalOrgId before pushing for the update + + const fullName = displayFullName( + user?.firstName, + user?.middleName, + user?.lastName + ); + + showSuccess("", `Access updated for ${fullName}`); + reset(userAccessData); }; - console.log("dirty fields: ", isDirtyAlt, dirtyFields); + return (
= ({ user }) => { setValue={setValue} disabled={loadingFacilities} /> -
); From 09e0a74e1b16206ab902445429f5ca1e7e05d061 Mon Sep 17 00:00:00 2001 From: Johanna Date: Fri, 1 Sep 2023 12:48:14 -0400 Subject: [PATCH 04/26] Fixed admin setup --- .../src/app/supportAdmin/ManageUsers/UserAccessTab.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx index 2aee903ae0..9fab1e2306 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx @@ -105,9 +105,10 @@ const UserAccessTab: React.FC = ({ user }) => { const [updateUserPrivilegesAndGroupAccess, { loading: updatingPrivileges }] = useUpdateUserPrivilegesAndGroupAccessMutation(); const onSubmit = async (userAccessData: UserAccessFormData) => { - const allFacilityAccess = !!userAccessData.facilityIds.find( - (id) => id === "ALL_FACILITIES" - ); + console.log(userAccessData); + const allFacilityAccess = + userAccessData.role === "ADMIN" || + !!userAccessData.facilityIds.find((id) => id === "ALL_FACILITIES"); await updateUserPrivilegesAndGroupAccess({ variables: { @@ -115,7 +116,7 @@ const UserAccessTab: React.FC = ({ user }) => { role: userAccessData.role as MutationRole, orgExternalId: facilitiesResponse?.organization?.externalId || "", accessAllFacilities: allFacilityAccess, - facilities: userAccessData.facilityIds.filter( + facilities: userAccessData.facilityIds?.filter( (id) => id !== "ALL_FACILITIES" ), }, From 8007ef7362140b4ef0d986d69f8f5a9843667eb1 Mon Sep 17 00:00:00 2001 From: Johanna Date: Tue, 5 Sep 2023 16:22:30 -0400 Subject: [PATCH 05/26] Added integration with backend --- .../src/app/Settings/Users/CreateUserForm.tsx | 2 +- .../Settings/Users/UserFacilitiesSettings.tsx | 37 +++-- .../UserDetails/UserOrganizationFormField.tsx | 24 ++- .../ManageUsers/AdminManageUser.tsx | 119 ++++++++++++- .../ManageUsers/UserAccessTab.tsx | 156 ++++-------------- 5 files changed, 184 insertions(+), 154 deletions(-) diff --git a/frontend/src/app/Settings/Users/CreateUserForm.tsx b/frontend/src/app/Settings/Users/CreateUserForm.tsx index 9025b2d5e5..55ccfc3f4d 100644 --- a/frontend/src/app/Settings/Users/CreateUserForm.tsx +++ b/frontend/src/app/Settings/Users/CreateUserForm.tsx @@ -165,7 +165,7 @@ const CreateUserForm: React.FC = ({
; error?: FieldError; setValue: UseFormSetValue; @@ -20,44 +20,45 @@ interface UserFacilitiesSettingProps { // This is the react-hook-form supported version of UserFacilitiesSettingsForm. const UserFacilitiesSettings: React.FC = ({ roleSelected, - allFacilities, + facilityList, register, error, setValue, disabled, }) => { const isAdmin = roleSelected === "ADMIN"; + const facilityAccessDescription = isAdmin ? "Admins have access to all facilities" : "All users must have access to at least one facility"; - allFacilities.sort(alphabeticalFacilitySort); + + const allFacilities = [ + { + name: `Access all facilities (${facilityList?.length || 0})`, + id: "ALL_FACILITIES", + }, + ...facilityList?.sort(alphabeticalFacilitySort), + ]; const onChange = (e: React.ChangeEvent) => { const { value, checked } = e.target; if (value === "ALL_FACILITIES" && checked && setValue) { setValue("facilityIds", [ - "ALL_FACILITIES", ...allFacilities.map((facility) => facility.id), ]); } }; + const validateAtLeastOneCheck = (selectedOptions: string[]) => { + return isAdmin || selectedOptions.length > 0; + }; + let boxes = [ - { - value: "ALL_FACILITIES", - label: `Access all facilities (${allFacilities.length})`, - ...register("facilityIds", { - required: "At least one facility must be selected", - disabled: isAdmin, - onChange, - }), - }, - ...allFacilities.map((facility) => ({ + ...allFacilities?.map((facility) => ({ value: facility.id, label: facility.name, ...register("facilityIds", { - required: "At least one facility must be selected", - disabled: isAdmin, + validate: validateAtLeastOneCheck, onChange, }), })), @@ -70,9 +71,9 @@ const UserFacilitiesSettings: React.FC = ({ hintText={facilityAccessDescription} name="facilities" required - onChange={onChange} + onChange={() => {}} validationStatus={error?.type ? "error" : undefined} - errorMessage={error?.message} + errorMessage={"At least one facility must be selected"} disabled={disabled} /> ); diff --git a/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx b/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx index e629e12358..21f8ea8a6a 100644 --- a/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx +++ b/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx @@ -76,19 +76,17 @@ const UserOrganizationFormField: React.FC = ({ Error: {error?.message} )} - { - - } +
)} name="organizationId" diff --git a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx index 939c141062..9cc5cf49c5 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useForm, useWatch } from "react-hook-form"; import { useDocumentTitle } from "../../utils/hooks"; import { @@ -12,6 +13,9 @@ import { useUpdateUserNameMutation, useUndeleteUserMutation, User, + useUpdateUserPrivilegesAndGroupAccessMutation, + Role as MutationRole, + useGetFacilitiesByOrgIdLazyQuery, } from "../../../generated/graphql"; import { showSuccess } from "../../utils/srToast"; import { isUserActive } from "../../Settings/Users/UserDetailUtils"; @@ -19,10 +23,17 @@ import { displayFullName } from "../../utils"; import { OktaUserStatus } from "../../utils/user"; import UserInfoTab from "../../Settings/Users/UserInfoTab"; import UserHeading from "../../commonComponents/UserDetails/UserHeading"; +import { UserFacilitySetting } from "../../Settings/Users/ManageUsersContainer"; import UserAccessTab from "./UserAccessTab"; import { UserSearch } from "./UserSearch"; +export interface UserAccessFormData { + organizationId: string; + role: string; + facilityIds: string[]; +} + const userNotFoundError = (

User not found

@@ -73,7 +84,7 @@ export const AdminManageUser: React.FC = () => { const [undeleteUser] = useUndeleteUserMutation(); /** - * Handlers + * User information handlers */ const handleUpdate = async (func: () => Promise) => { setIsUpdating(true); @@ -248,6 +259,14 @@ export const AdminManageUser: React.FC = () => { setFoundUser(undefined); } else { setDisplayedError(undefined); + reset({ + role: data?.user?.role || "USER", + organizationId: data?.user?.organization?.id, + facilityIds: + data?.user?.organization?.testingFacility.map( + (facility) => facility.id + ) || [], + }); setFoundUser(data?.user as User); } } @@ -262,6 +281,90 @@ export const AdminManageUser: React.FC = () => { setDisplayedError(undefined); setFoundUser(undefined); }; + /** + * User access form setup + */ + const { + control, + handleSubmit, + formState: { errors, isDirty }, + register, + reset, + setValue, + getValues, + } = useForm(); + + const formValues = useWatch({ + control, + }); + + /** + * Fetch facilities + */ + const [ + queryGetFacilitiesByOrgId, + { data: facilitiesResponse, loading: loadingFacilities }, + ] = useGetFacilitiesByOrgIdLazyQuery(); + + useEffect(() => { + if (formValues.organizationId) { + queryGetFacilitiesByOrgId({ + fetchPolicy: "no-cache", + variables: { + orgId: formValues.organizationId, + }, + }); + + if (formValues.organizationId !== foundUser?.organization?.id) { + setValue("facilityIds", []); + } + } + }, [ + queryGetFacilitiesByOrgId, + formValues.organizationId, + setValue, + foundUser?.organization?.id, + ]); + + const facilityList = formValues.organizationId + ? facilitiesResponse?.organization?.facilities.map( + (facility) => + ({ id: facility.id, name: facility.name } as UserFacilitySetting) + ) + : []; + + /** + * User access submit updates + */ + const [updateUserPrivilegesAndGroupAccess, { loading: updatingPrivileges }] = + useUpdateUserPrivilegesAndGroupAccessMutation(); + const onSubmit = async (userAccessData: UserAccessFormData) => { + console.log("submitting data:", userAccessData); + const allFacilityAccess = + userAccessData.role === "ADMIN" || + !!userAccessData.facilityIds.find((id) => id === "ALL_FACILITIES"); + + await updateUserPrivilegesAndGroupAccess({ + variables: { + username: foundUser?.email || "", + role: userAccessData.role as MutationRole, + orgExternalId: facilitiesResponse?.organization?.externalId || "", + accessAllFacilities: allFacilityAccess, + facilities: allFacilityAccess + ? [] + : userAccessData.facilityIds?.filter((id) => id !== "ALL_FACILITIES"), + }, + }); + + const fullName = displayFullName( + foundUser?.firstName, + foundUser?.middleName, + foundUser?.lastName + ); + + showSuccess("", `Access updated for ${fullName}`); + await retrieveUser(); + }; /** * Tab content @@ -345,7 +448,19 @@ export const AdminManageUser: React.FC = () => { onDeleteUser={handleDeleteUser} /> ) : ( - + )}
diff --git a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx index 9fab1e2306..4d7f564e25 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx @@ -1,137 +1,53 @@ -import React, { useEffect } from "react"; -import { useForm } from "react-hook-form"; +import React from "react"; import { FieldError } from "react-hook-form/dist/types/errors"; +import { UseFormSetValue } from "react-hook-form"; +import { UseFormGetValues } from "react-hook-form/dist/types/form"; import Button from "../../commonComponents/Button/Button"; -import { UserFacilitySetting } from "../../Settings/Users/ManageUsersContainer"; import UserFacilitiesSettings from "../../Settings/Users/UserFacilitiesSettings"; -import { - useGetFacilitiesByOrgIdLazyQuery, - User, - useUpdateUserPrivilegesAndGroupAccessMutation, - Role as MutationRole, -} from "../../../generated/graphql"; +import { User, Facility } from "../../../generated/graphql"; import UserOrganizationFormField from "../../commonComponents/UserDetails/UserOrganizationFormField"; import UserRoleFormField from "../../commonComponents/UserDetails/UserRoleFormField"; -import { Role } from "../../permissions"; -import { showSuccess } from "../../utils/srToast"; -import { displayFullName } from "../../utils"; - -type UserAccessFormData = { - organizationId: string; - role: Role; - facilityIds: string[]; -}; export interface UserAccessTabProps { user: User; - isUpdating: boolean; + onSubmit: () => Promise; + setValue: UseFormSetValue; + getValues: UseFormGetValues; + control: any; + register: any; + errors: any; + isDirty: boolean; + isLoadingFacilities: boolean; + isSubmitting: boolean; + facilityList: Pick[]; } // Set the prompt warning of moving away from the component in manage user component // Move all the react-hook-form logic to manage user component so the form is not lost when changing tabs // Make sure the latest user information is retrieve after this update takes effect. -const UserAccessTab: React.FC = ({ user }) => { - // retrieve organizations - // retrieve facilities by org - // set the defaults based on what the user has - - // maybe adding the prompt when a user triggers a new search? - - const getDefaultFacilities = () => { - const facilityIds: string[] = []; - - if (user.role === "ADMIN") { - facilityIds.push("ALL_FACILITIES"); - } - - return facilityIds.concat( - user.organization?.testingFacility.map((facility) => facility.id) || [] - ); - }; - /** - * Form state setup - */ - const { - control, - handleSubmit, - formState: { errors, dirtyFields }, - watch, - register, - reset, - setValue, - } = useForm({ - defaultValues: { - organizationId: user.organization?.id || "", - facilityIds: getDefaultFacilities(), - role: user.role || "USER", - }, - }); - - // ToDo revisting why this is needed here but no in create user - const isDirtyAlt = !!Object.keys(dirtyFields).length; - - const formCurrentValues = watch(); +const UserAccessTab: React.FC = ({ + user, + facilityList, + setValue, + onSubmit, + control, + getValues, + register, + errors, + isDirty, + isLoadingFacilities, + isSubmitting, +}) => { + const selectedRole = getValues("role"); /** - * Fetch facilities + * Confirmation modal */ - const [ - queryGetFacilitiesByOrgId, - { data: facilitiesResponse, loading: loadingFacilities }, - ] = useGetFacilitiesByOrgIdLazyQuery(); - - useEffect(() => { - if (formCurrentValues.organizationId) { - queryGetFacilitiesByOrgId({ - fetchPolicy: "no-cache", - variables: { - orgId: formCurrentValues.organizationId, - }, - }); - } - }, [queryGetFacilitiesByOrgId, formCurrentValues.organizationId]); - - const facilityList = formCurrentValues.organizationId - ? facilitiesResponse?.organization?.facilities.map( - (facility) => - ({ id: facility.id, name: facility.name } as UserFacilitySetting) - ) - : []; /** - * Submit access updates + * HTML */ - const [updateUserPrivilegesAndGroupAccess, { loading: updatingPrivileges }] = - useUpdateUserPrivilegesAndGroupAccessMutation(); - const onSubmit = async (userAccessData: UserAccessFormData) => { - console.log(userAccessData); - const allFacilityAccess = - userAccessData.role === "ADMIN" || - !!userAccessData.facilityIds.find((id) => id === "ALL_FACILITIES"); - - await updateUserPrivilegesAndGroupAccess({ - variables: { - username: user.email, - role: userAccessData.role as MutationRole, - orgExternalId: facilitiesResponse?.organization?.externalId || "", - accessAllFacilities: allFacilityAccess, - facilities: userAccessData.facilityIds?.filter( - (id) => id !== "ALL_FACILITIES" - ), - }, - }); - - const fullName = displayFullName( - user?.firstName, - user?.middleName, - user?.lastName - ); - - showSuccess("", `Access updated for ${fullName}`); - reset(userAccessData); - }; - return (
= ({ user }) => { className="padding-left-1" >
From 10b78a58bcf05cdcabbd2301376293a4a360cd3b Mon Sep 17 00:00:00 2001 From: Johanna Date: Wed, 6 Sep 2023 15:31:08 -0400 Subject: [PATCH 06/26] Added org update confirmation modal --- .../UserDetails/UserOrganizationFormField.tsx | 4 +- .../UserDetails/UserRoleFormField.tsx | 12 +- .../ManageUsers/AdminManageUser.tsx | 87 +++++------- .../ManageUsers/UserAccessTab.tsx | 129 +++++++++++++++--- .../ManageUsers/operations.graphql | 4 + frontend/src/generated/graphql.tsx | 65 +++++++++ 6 files changed, 226 insertions(+), 75 deletions(-) diff --git a/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx b/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx index 21f8ea8a6a..c14b36ee8e 100644 --- a/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx +++ b/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx @@ -8,10 +8,12 @@ import { useGetAllOrganizationsQuery } from "../../../generated/graphql"; interface OrganizationSelectFormFieldProps { control?: Control; + disabled?: boolean; } const UserOrganizationFormField: React.FC = ({ control, + disabled, }) => { /** * Fetch organizations (on initial load) @@ -85,7 +87,7 @@ const UserOrganizationFormField: React.FC = ({ onChange={onChange} ref={ref} assistiveHint={describeText(error?.message)} - disabled={loadingOrgs} + disabled={loadingOrgs || disabled} />
)} diff --git a/frontend/src/app/commonComponents/UserDetails/UserRoleFormField.tsx b/frontend/src/app/commonComponents/UserDetails/UserRoleFormField.tsx index 69fb4afb9a..b68c5d3118 100644 --- a/frontend/src/app/commonComponents/UserDetails/UserRoleFormField.tsx +++ b/frontend/src/app/commonComponents/UserDetails/UserRoleFormField.tsx @@ -1,20 +1,23 @@ import React from "react"; -import { UseFormRegisterReturn } from "react-hook-form"; +import { UseFormRegisterReturn, useWatch } from "react-hook-form"; import RadioGroup from "../RadioGroup"; import { ROLES } from "../../Settings/Users/UserRoleSettingsForm"; interface UserRoleFormFieldProps { - value: string; + control: any; registrationProps: UseFormRegisterReturn; error: any; + disabled: boolean; } const UserRoleFormField: React.FC = ({ - value, + control, error, + disabled, registrationProps, }) => { + const selectedRole = useWatch({ control, name: "role" }); return (
= ({ "Admins have full access to use and change settings on SimpleReport. Standard and testing-only users have limited access for specific tasks, as described below." } buttons={ROLES} - selectedRadio={value} + selectedRadio={selectedRole} validationStatus={error ? "error" : undefined} errorMessage={error?.message} registrationProps={registrationProps} required + disabled={disabled} />
); diff --git a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx index 9cc5cf49c5..05b4f51b8b 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx @@ -83,6 +83,12 @@ export const AdminManageUser: React.FC = () => { const [resendUserActivationEmail] = useResendActivationEmailMutation(); const [undeleteUser] = useUndeleteUserMutation(); + const userFullName = displayFullName( + foundUser?.firstName, + foundUser?.middleName, + foundUser?.lastName + ); + /** * User information handlers */ @@ -144,12 +150,8 @@ export const AdminManageUser: React.FC = () => { id: userId, }, }); - const fullName = displayFullName( - foundUser?.firstName, - foundUser?.middleName, - foundUser?.lastName - ); - showSuccess("", `Password reset for ${fullName}`); + + showSuccess("", `Password reset for ${userFullName}`); }); }; const handleResetUserMfa = async (userId: string) => { @@ -159,12 +161,8 @@ export const AdminManageUser: React.FC = () => { id: userId, }, }); - const fullName = displayFullName( - foundUser?.firstName, - foundUser?.middleName, - foundUser?.lastName - ); - showSuccess("", `MFA reset for ${fullName}`); + + showSuccess("", `MFA reset for ${userFullName}`); }); }; const handleDeleteUser = async (userId: string) => { @@ -175,18 +173,13 @@ export const AdminManageUser: React.FC = () => { deleted: true, }, }); - const fullName = displayFullName( - foundUser?.firstName, - foundUser?.middleName, - foundUser?.lastName - ); setFoundUser({ ...foundUser, isDeleted: true, status: OktaUserStatus.SUSPENDED, } as User); - showSuccess("", `User account removed for ${fullName}`); + showSuccess("", `User account removed for ${userFullName}`); }); }; const handleReactivateUser = async (userId: string) => { @@ -196,16 +189,12 @@ export const AdminManageUser: React.FC = () => { id: userId, }, }); - const fullName = displayFullName( - foundUser?.firstName, - foundUser?.middleName, - foundUser?.lastName - ); + setFoundUser({ ...foundUser, status: OktaUserStatus.ACTIVE, } as User); - showSuccess("", `${fullName} has been reactivated.`); + showSuccess("", `${userFullName} has been reactivated.`); }); }; const handleResendUserActivationEmail = async (userId: string) => { @@ -215,12 +204,8 @@ export const AdminManageUser: React.FC = () => { id: userId, }, }); - const fullName = displayFullName( - foundUser?.firstName, - foundUser?.middleName, - foundUser?.lastName - ); - showSuccess("", `${fullName} has been sent a new invitation.`); + + showSuccess("", `${userFullName} has been sent a new invitation.`); }); }; @@ -232,13 +217,7 @@ export const AdminManageUser: React.FC = () => { await retrieveUser(); - const fullName = displayFullName( - foundUser?.firstName, - foundUser?.middleName, - foundUser?.lastName - ); - - showSuccess("", `User account undeleted for ${fullName}`); + showSuccess("", `User account undeleted for ${userFullName}`); }); }; @@ -281,6 +260,7 @@ export const AdminManageUser: React.FC = () => { setDisplayedError(undefined); setFoundUser(undefined); }; + /** * User access form setup */ @@ -291,11 +271,15 @@ export const AdminManageUser: React.FC = () => { register, reset, setValue, - getValues, } = useForm(); const formValues = useWatch({ control, + defaultValue: { + organizationId: "", + role: "USER", + facilityIds: [], + }, }); /** @@ -334,12 +318,12 @@ export const AdminManageUser: React.FC = () => { : []; /** - * User access submit updates + * Submit access updates */ - const [updateUserPrivilegesAndGroupAccess, { loading: updatingPrivileges }] = + const [updateUserPrivilegesAndGroupAccess, { loading: isUpdatingAccess }] = useUpdateUserPrivilegesAndGroupAccessMutation(); - const onSubmit = async (userAccessData: UserAccessFormData) => { - console.log("submitting data:", userAccessData); + + const updateUserPrivileges = async (userAccessData: UserAccessFormData) => { const allFacilityAccess = userAccessData.role === "ADMIN" || !!userAccessData.facilityIds.find((id) => id === "ALL_FACILITIES"); @@ -356,13 +340,7 @@ export const AdminManageUser: React.FC = () => { }, }); - const fullName = displayFullName( - foundUser?.firstName, - foundUser?.middleName, - foundUser?.lastName - ); - - showSuccess("", `Access updated for ${fullName}`); + showSuccess("", `Access updated for ${userFullName}`); await retrieveUser(); }; @@ -450,16 +428,17 @@ export const AdminManageUser: React.FC = () => { ) : ( )}
diff --git a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx index 4d7f564e25..a967fee3f6 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx @@ -1,19 +1,27 @@ -import React from "react"; +import React, { useRef, useState } from "react"; import { FieldError } from "react-hook-form/dist/types/errors"; -import { UseFormSetValue } from "react-hook-form"; -import { UseFormGetValues } from "react-hook-form/dist/types/form"; +import { UseFormHandleSubmit, UseFormSetValue } from "react-hook-form"; import Button from "../../commonComponents/Button/Button"; import UserFacilitiesSettings from "../../Settings/Users/UserFacilitiesSettings"; -import { User, Facility } from "../../../generated/graphql"; +import { + User, + Facility, + useGetTestResultCountByOrgLazyQuery, +} from "../../../generated/graphql"; import UserOrganizationFormField from "../../commonComponents/UserDetails/UserOrganizationFormField"; import UserRoleFormField from "../../commonComponents/UserDetails/UserRoleFormField"; +import Modal from "../../commonComponents/Modal"; +import { displayFullName } from "../../utils"; + +import { UserAccessFormData } from "./AdminManageUser"; export interface UserAccessTabProps { user: User; - onSubmit: () => Promise; - setValue: UseFormSetValue; - getValues: UseFormGetValues; + onSubmit: (data: UserAccessFormData) => Promise; + handleSubmit: UseFormHandleSubmit; + setValue: UseFormSetValue; + formValues: UserAccessFormData; control: any; register: any; errors: any; @@ -23,27 +31,112 @@ export interface UserAccessTabProps { facilityList: Pick[]; } -// Set the prompt warning of moving away from the component in manage user component -// Move all the react-hook-form logic to manage user component so the form is not lost when changing tabs -// Make sure the latest user information is retrieve after this update takes effect. +// ToDo Set the prompt warning of moving away from the component in manage user component +// ToDo check why the update method fails when sent individual facilities + const UserAccessTab: React.FC = ({ user, facilityList, setValue, onSubmit, + handleSubmit, control, - getValues, + formValues, register, errors, isDirty, isLoadingFacilities, isSubmitting, }) => { - const selectedRole = getValues("role"); + const selectedRole = formValues.role; + + const fullName = displayFullName( + user.firstName, + user.middleName, + user.lastName, + true + ); + const hasOrgChange = user.organization?.id !== formValues.organizationId; + + /** + * Confirm and Submit + */ + const dataRef = useRef(); + + const [getTestResultCount, { data }] = useGetTestResultCountByOrgLazyQuery(); + + const handleConfirmationAndSubmit = async (formData: UserAccessFormData) => { + if (hasOrgChange) { + const count = + (await getTestResultCount({ + variables: { orgId: user.organization?.id as string }, + fetchPolicy: "no-cache", + }).then((res) => res.data?.testResultsCount)) || 0; + + // if user could lose access of test results stop the submission + // and ask for confirmation + if (count > 0) { + dataRef.current = { ...formData }; + setShowModal(true); + return; + } + } + + await onSubmit(formData); + }; + + const handleSubmitFromModal = async () => { + closeModal(); + if (dataRef.current) { + await onSubmit(dataRef.current as UserAccessFormData); + dataRef.current = undefined; + } + }; /** * Confirmation modal */ + const [showModal, setShowModal] = useState(false); + const closeModal = () => setShowModal(false); + + const confirmationModal = ( + + + Organization update + +
+ +

+ This update will move {fullName} to a + different organization. The user will lose access to{" "} + {data?.testResultsCount} test results + reported under it. +

+
+ +
); diff --git a/frontend/src/app/supportAdmin/ManageUsers/operations.graphql b/frontend/src/app/supportAdmin/ManageUsers/operations.graphql index 612c1e8d7f..002f26b6b6 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/operations.graphql +++ b/frontend/src/app/supportAdmin/ManageUsers/operations.graphql @@ -42,3 +42,7 @@ mutation updateUserPrivilegesAndGroupAccess($username: String!, $orgExternalId: id } } + +query getTestResultCountByOrg($orgId: ID!){ + testResultsCount(orgId: $orgId) +} diff --git a/frontend/src/generated/graphql.tsx b/frontend/src/generated/graphql.tsx index cd312cead0..84018a39c7 100644 --- a/frontend/src/generated/graphql.tsx +++ b/frontend/src/generated/graphql.tsx @@ -1815,6 +1815,15 @@ export type UpdateUserPrivilegesAndGroupAccessMutation = { updateUserPrivilegesAndGroupAccess: { __typename?: "User"; id: string }; }; +export type GetTestResultCountByOrgQueryVariables = Exact<{ + orgId: Scalars["ID"]; +}>; + +export type GetTestResultCountByOrgQuery = { + __typename?: "Query"; + testResultsCount?: number | null; +}; + export type GetPendingOrganizationsQueryVariables = Exact<{ [key: string]: never; }>; @@ -5557,6 +5566,62 @@ export type UpdateUserPrivilegesAndGroupAccessMutationOptions = UpdateUserPrivilegesAndGroupAccessMutation, UpdateUserPrivilegesAndGroupAccessMutationVariables >; +export const GetTestResultCountByOrgDocument = gql` + query getTestResultCountByOrg($orgId: ID!) { + testResultsCount(orgId: $orgId) + } +`; + +/** + * __useGetTestResultCountByOrgQuery__ + * + * To run a query within a React component, call `useGetTestResultCountByOrgQuery` and pass it any options that fit your needs. + * When your component renders, `useGetTestResultCountByOrgQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetTestResultCountByOrgQuery({ + * variables: { + * orgId: // value for 'orgId' + * }, + * }); + */ +export function useGetTestResultCountByOrgQuery( + baseOptions: Apollo.QueryHookOptions< + GetTestResultCountByOrgQuery, + GetTestResultCountByOrgQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery< + GetTestResultCountByOrgQuery, + GetTestResultCountByOrgQueryVariables + >(GetTestResultCountByOrgDocument, options); +} +export function useGetTestResultCountByOrgLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions< + GetTestResultCountByOrgQuery, + GetTestResultCountByOrgQueryVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery< + GetTestResultCountByOrgQuery, + GetTestResultCountByOrgQueryVariables + >(GetTestResultCountByOrgDocument, options); +} +export type GetTestResultCountByOrgQueryHookResult = ReturnType< + typeof useGetTestResultCountByOrgQuery +>; +export type GetTestResultCountByOrgLazyQueryHookResult = ReturnType< + typeof useGetTestResultCountByOrgLazyQuery +>; +export type GetTestResultCountByOrgQueryResult = Apollo.QueryResult< + GetTestResultCountByOrgQuery, + GetTestResultCountByOrgQueryVariables +>; export const GetPendingOrganizationsDocument = gql` query GetPendingOrganizations { pendingOrganizations { From 961cd2e58deba7985b11e79f7da043998174a967 Mon Sep 17 00:00:00 2001 From: Johanna Date: Wed, 6 Sep 2023 15:38:39 -0400 Subject: [PATCH 07/26] Implemented confirmation modal --- frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx index a967fee3f6..6a57a6c767 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/UserAccessTab.tsx @@ -99,6 +99,7 @@ const UserAccessTab: React.FC = ({ const [showModal, setShowModal] = useState(false); const closeModal = () => setShowModal(false); + console.log(data?.testResultsCount); const confirmationModal = ( = ({

This update will move {fullName} to a different organization. The user will lose access to{" "} - {data?.testResultsCount} test results + {data?.testResultsCount} test{" "} + {data?.testResultsCount === 1 ? "result " : "results "} reported under it.

From 739ee1bb08c289f00721917c2bbbea81a6a033df Mon Sep 17 00:00:00 2001 From: Johanna Date: Thu, 7 Sep 2023 10:46:29 -0400 Subject: [PATCH 08/26] Implemented warning modals --- .../Settings/Users/UserFacilitiesSettings.tsx | 6 +- .../ManageUsers/AdminManageUser.tsx | 172 +++++++++++++----- .../ManageUsers/UnsaveChangesModal.tsx | 46 +++++ .../ManageUsers/UserAccessTab.tsx | 3 - .../supportAdmin/ManageUsers/UserSearch.tsx | 4 +- 5 files changed, 179 insertions(+), 52 deletions(-) create mode 100644 frontend/src/app/supportAdmin/ManageUsers/UnsaveChangesModal.tsx diff --git a/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx b/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx index 1bdf2b785d..ed62ed8a09 100644 --- a/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx +++ b/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx @@ -3,14 +3,14 @@ import { UseFormRegister, UseFormSetValue } from "react-hook-form"; import { FieldError } from "react-hook-form/dist/types/errors"; import Checkboxes from "../../commonComponents/Checkboxes"; +import { Facility } from "../../../generated/graphql"; -import { UserFacilitySetting } from "./ManageUsersContainer"; -import "./ManageUsers.scss"; import { alphabeticalFacilitySort } from "./UserFacilitiesSettingsForm"; +import "./ManageUsers.scss"; interface UserFacilitiesSettingProps { roleSelected: string; - facilityList: UserFacilitySetting[]; + facilityList: Pick[]; register: UseFormRegister; error?: FieldError; setValue: UseFormSetValue; diff --git a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx index 05b4f51b8b..fe8e1ece73 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useForm, useWatch } from "react-hook-form"; +import classNames from "classnames"; import { useDocumentTitle } from "../../utils/hooks"; import { @@ -17,6 +18,7 @@ import { Role as MutationRole, useGetFacilitiesByOrgIdLazyQuery, } from "../../../generated/graphql"; +import Prompt from "../../utils/Prompt"; import { showSuccess } from "../../utils/srToast"; import { isUserActive } from "../../Settings/Users/UserDetailUtils"; import { displayFullName } from "../../utils"; @@ -27,6 +29,7 @@ import { UserFacilitySetting } from "../../Settings/Users/ManageUsersContainer"; import UserAccessTab from "./UserAccessTab"; import { UserSearch } from "./UserSearch"; +import UnsavedChangesModal from "./UnsaveChangesModal"; export interface UserAccessFormData { organizationId: string; @@ -34,6 +37,18 @@ export interface UserAccessFormData { facilityIds: string[]; } +export interface UserSearchState { + searchEmail: string; + foundUser: User | undefined; + displayedError: JSX.Element | undefined; +} + +export const initialState: UserSearchState = { + searchEmail: "", + foundUser: undefined, + displayedError: undefined, +}; + const userNotFoundError = (

User not found

@@ -59,13 +74,8 @@ const genericError = ( export const AdminManageUser: React.FC = () => { useDocumentTitle("Manage Users"); - //ToDo Remove this - const [searchEmail, setSearchEmail] = useState("ruby@example.com"); - useEffect(() => { - retrieveUser(); - }, []); - const [foundUser, setFoundUser] = useState(); - const [displayedError, setDisplayedError] = useState(); + const [{ searchEmail, foundUser, displayedError }, setSearchState] = + useState(initialState); const [navItemSelected, setNavItemSelected] = useState< "User information" | "User access" >("User information"); @@ -117,13 +127,18 @@ export const AdminManageUser: React.FC = () => { suffix: suffix, }, }); - setFoundUser({ - ...foundUser, - firstName, - middleName, - lastName, - suffix, - } as User); + + setSearchState((prevState) => ({ + ...prevState, + foundUser: { + ...prevState.foundUser, + firstName, + middleName, + lastName, + suffix, + } as User, + })); + const fullName = displayFullName(firstName, "", lastName); showSuccess("", `User name changed to ${fullName}`); }); @@ -137,10 +152,13 @@ export const AdminManageUser: React.FC = () => { }, }); showSuccess("", `User email address changed to ${emailAddress}`); - setFoundUser({ - ...foundUser, - email: emailAddress, - } as User); + setSearchState((prevState) => ({ + ...prevState, + foundUser: { + ...prevState.foundUser, + email: emailAddress, + } as User, + })); }); }; const handleResetUserPassword = async (userId: string) => { @@ -174,11 +192,15 @@ export const AdminManageUser: React.FC = () => { }, }); - setFoundUser({ - ...foundUser, - isDeleted: true, - status: OktaUserStatus.SUSPENDED, - } as User); + setSearchState((prevState) => ({ + ...prevState, + foundUser: { + ...prevState.foundUser, + isDeleted: true, + status: OktaUserStatus.SUSPENDED, + } as User, + })); + showSuccess("", `User account removed for ${userFullName}`); }); }; @@ -190,10 +212,14 @@ export const AdminManageUser: React.FC = () => { }, }); - setFoundUser({ - ...foundUser, - status: OktaUserStatus.ACTIVE, - } as User); + setSearchState((prevState) => ({ + ...prevState, + foundUser: { + ...prevState.foundUser, + status: OktaUserStatus.ACTIVE, + } as User, + })); + showSuccess("", `${userFullName} has been reactivated.`); }); }; @@ -225,19 +251,27 @@ export const AdminManageUser: React.FC = () => { return getUserByEmail({ variables: { email: searchEmail.trim() } }).then( ({ data, error }) => { if (!data?.user && !error) { - setDisplayedError(userNotFoundError); - setFoundUser(undefined); + setSearchState((prevState) => ({ + ...prevState, + displayedError: userNotFoundError, + foundUser: undefined, + })); } else if ( error?.message === "header: Error finding user email; body: Please escalate this issue to the SimpleReport team." ) { - setDisplayedError(userIdentityError); - setFoundUser(undefined); + setSearchState((prevState) => ({ + ...prevState, + displayedError: userIdentityError, + foundUser: undefined, + })); } else if (error) { - setDisplayedError(genericError); - setFoundUser(undefined); + setSearchState((prevState) => ({ + ...prevState, + displayedError: genericError, + foundUser: undefined, + })); } else { - setDisplayedError(undefined); reset({ role: data?.user?.role || "USER", organizationId: data?.user?.organization?.id, @@ -246,19 +280,29 @@ export const AdminManageUser: React.FC = () => { (facility) => facility.id ) || [], }); - setFoundUser(data?.user as User); + + setSearchState((prevState) => ({ + ...prevState, + displayedError: undefined, + foundUser: data?.user as User, + })); } } ); }; const handleInputChange = (e: React.ChangeEvent) => { - setSearchEmail(e.target.value); + setSearchState((prevState) => ({ + ...prevState, + searchEmail: e.target.value, + })); }; const handleClearFilter = () => { - setSearchEmail(""); - setDisplayedError(undefined); - setFoundUser(undefined); + setSearchState(initialState); + }; + + const handleSearch = () => { + retrieveUser(); }; /** @@ -350,6 +394,23 @@ export const AdminManageUser: React.FC = () => { const tabs: (typeof navItemSelected)[] = ["User information", "User access"]; + /** + * Unsaved changes (prompt and in-progress modal) + */ + const [showUnsavedWarning, setShowUnsavedWarning] = useState(false); + const operationType = useRef<"CLEAR" | "SEARCH">("CLEAR"); + const handleWithInProgressCheck = ( + operation: typeof operationType.current, + operationMethod: Function + ) => { + if (isDirty) { + operationType.current = operation; + setShowUnsavedWarning(true); + } else { + operationMethod(); + } + }; + /** * HTML */ @@ -357,23 +418,32 @@ export const AdminManageUser: React.FC = () => {
+ handleWithInProgressCheck("CLEAR", handleClearFilter) + } onSearchClick={(e) => { e.preventDefault(); - retrieveUser(); + handleWithInProgressCheck("SEARCH", handleSearch); }} onInputChange={handleInputChange} searchEmail={searchEmail} disableClearFilters={!searchEmail && !foundUser && !displayedError} /> {displayedError && ( -
+
{displayedError}
)} {foundUser && (
@@ -442,6 +512,20 @@ export const AdminManageUser: React.FC = () => { /> )}
+ + setShowUnsavedWarning(false)} + isShowing={showUnsavedWarning} + onContinue={() => { + setShowUnsavedWarning(false); + operationType.current === "CLEAR" + ? handleClearFilter() + : handleSearch(); + }} + />
)} diff --git a/frontend/src/app/supportAdmin/ManageUsers/UnsaveChangesModal.tsx b/frontend/src/app/supportAdmin/ManageUsers/UnsaveChangesModal.tsx new file mode 100644 index 0000000000..ce1d3cc759 --- /dev/null +++ b/frontend/src/app/supportAdmin/ManageUsers/UnsaveChangesModal.tsx @@ -0,0 +1,46 @@ +import React, { MouseEventHandler } from "react"; + +import Modal from "../../commonComponents/Modal"; +import Button from "../../commonComponents/Button/Button"; + +export interface UnsavedChangesModalProps { + closeModal: () => void; + isShowing: boolean; + onContinue: MouseEventHandler; +} +const UnsavedChangesModal: React.FC = ({ + closeModal, + isShowing, + onContinue, +}) => { + return ( + +

+ You have unsaved changes in the user access tab. Do you want to proceed? +

+
+ +
@@ -374,6 +372,9 @@ exports[`Admin manage users Undelete user 1`] = `
+
+ ); }; diff --git a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.test.tsx b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.test.tsx index b45f3c98a6..79262d0a65 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.test.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.test.tsx @@ -31,7 +31,9 @@ import { AdminManageUser } from "./AdminManageUser"; import { findUserByEmailMock, getAllOrgsMock, - getFacilitiesByOrgMock, + getFacilitiesByDatOrgMock, + getFacilitiesByDisOrgMock, + getTestResultCountByOrgMock, } from "./operationMocks"; jest.mock("uuid", () => ({ @@ -676,8 +678,8 @@ describe("Admin manage users", () => { it("loads organization access tab", async () => { const { user } = renderComponent([ getAllOrgsMock, - getFacilitiesByOrgMock, - getFacilitiesByOrgMock, + getFacilitiesByDisOrgMock, + getFacilitiesByDisOrgMock, findUserByEmailMock, ]); await searchForValidUser( @@ -703,8 +705,8 @@ describe("Admin manage users", () => { it("checks form validation happens on submit", async () => { const { user } = renderComponent([ getAllOrgsMock, - getFacilitiesByOrgMock, - getFacilitiesByOrgMock, + getFacilitiesByDisOrgMock, + getFacilitiesByDisOrgMock, findUserByEmailMock, ]); await searchForValidUser( @@ -741,4 +743,59 @@ describe("Admin manage users", () => { await screen.findByText(/Error: Organization is required/i); }); }); + + it("shows warning modal if org updates that will make user lose access to data are submitted", async () => { + const { user } = renderComponent([ + findUserByEmailMock, + getAllOrgsMock, + getFacilitiesByDisOrgMock, + getFacilitiesByDisOrgMock, + getFacilitiesByDatOrgMock, + getTestResultCountByOrgMock, + ]); + + await searchForValidUser(user, "ruby@example.com", "Reynolds, Ruby Raven"); + + const orgAccessTab = await screen.findByRole("tab", { + name: /organization access/i, + }); + + await act(async () => { + orgAccessTab.click(); + }); + + // change organization + const orgComboBoxInput = await screen.findByTestId("combo-box-input"); + await act(async () => { + await user.clear(orgComboBoxInput); + }); + await act(async () => { + await user.type(orgComboBoxInput, "Dat Organization"); + }); + await act(async () => { + await user.type(orgComboBoxInput, "{enter}"); + }); + + // select facility + const downtownCheckbox = await screen.findByLabelText(/Downtown Clinic/i); + await act(async () => { + await user.click(downtownCheckbox); + }); + + // submit changes + const saveChangesBtn = screen.getByRole("button", { + name: /save changes/i, + }); + await act(async () => { + await user.click(saveChangesBtn); + }); + + // verify warning modal shows + await screen.findByText(/organization update/i); + expect( + screen.getByText( + /this update will move to a different organization\. the user will lose access to test result reported under it\./i + ) + ).toBeInTheDocument(); + }); }); diff --git a/frontend/src/app/supportAdmin/ManageUsers/operationMocks.ts b/frontend/src/app/supportAdmin/ManageUsers/operationMocks.ts index 7d6dcf32af..d6d00aa548 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/operationMocks.ts +++ b/frontend/src/app/supportAdmin/ManageUsers/operationMocks.ts @@ -2,6 +2,7 @@ import { FindUserByEmailDocument, GetAllOrganizationsDocument, GetFacilitiesByOrgIdDocument, + GetTestResultCountByOrgDocument, } from "../../../generated/graphql"; export const getAllOrgsMock = { @@ -26,7 +27,7 @@ export const getAllOrgsMock = { }, }; -export const getFacilitiesByOrgMock = { +export const getFacilitiesByDisOrgMock = { request: { query: GetFacilitiesByOrgIdDocument, variables: { @@ -102,3 +103,49 @@ export const findUserByEmailMock = { }, }, }; + +export const getTestResultCountByOrgMock = { + request: { + query: GetTestResultCountByOrgDocument, + variables: { orgId: "85988325-e5f1-4921-b8e7-4de3a1a8ead6" }, + }, + result: { data: { testResultsCount: 1 } }, +}; + +export const getFacilitiesByDatOrgMock = { + request: { + query: GetFacilitiesByOrgIdDocument, + variables: { + orgId: "6291c4db-8d4b-4db1-8604-c6c32cc5f2aa", + }, + }, + result: { + data: { + organization: { + id: "6291c4db-8d4b-4db1-8604-c6c32cc5f2aa", + externalId: "DAT_ORG", + name: "Dat Organization", + type: "urgent_care", + facilities: [ + { + name: "Downtown Clinic", + id: "886ebc8a-16cd-4c27-90d2-648b9357e393", + city: "New York", + state: "NY", + zipCode: "10010", + __typename: "Facility", + }, + { + name: "Uptown Clinic", + id: "108f5100-ed7a-477d-b31d-51a0b560ba8c", + city: "New York", + state: "NY", + zipCode: "10128", + __typename: "Facility", + }, + ], + __typename: "Organization", + }, + }, + }, +}; From 02e2837a1dc8ab6d6ff1308ac182bd2031e6fb12 Mon Sep 17 00:00:00 2001 From: Johanna Date: Tue, 19 Sep 2023 13:18:56 -0400 Subject: [PATCH 19/26] Fixed feedback --- .../src/app/Settings/Users/CreateUserForm.tsx | 3 +- .../Settings/Users/UserFacilitiesSettings.tsx | 3 +- .../UserDetails/UserOrganizationFormField.tsx | 4 +- .../UserDetails/UserRoleFormField.tsx | 11 ++- .../ManageUsers/AdminManageUser.tsx | 44 ++++++----- .../supportAdmin/ManageUsers/OrgAccessTab.tsx | 75 ++++++++++++------- 6 files changed, 90 insertions(+), 50 deletions(-) diff --git a/frontend/src/app/Settings/Users/CreateUserForm.tsx b/frontend/src/app/Settings/Users/CreateUserForm.tsx index 55ccfc3f4d..31cd1be542 100644 --- a/frontend/src/app/Settings/Users/CreateUserForm.tsx +++ b/frontend/src/app/Settings/Users/CreateUserForm.tsx @@ -1,8 +1,7 @@ import React from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSelector } from "react-redux"; -import { useForm } from "react-hook-form"; -import { FieldError } from "react-hook-form/dist/types/errors"; +import { useForm, FieldError } from "react-hook-form"; import Button from "../../commonComponents/Button/Button"; import Dropdown from "../../commonComponents/Dropdown"; diff --git a/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx b/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx index 15b3443bf6..5a45f4de6d 100644 --- a/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx +++ b/frontend/src/app/Settings/Users/UserFacilitiesSettings.tsx @@ -1,6 +1,5 @@ import React from "react"; -import { UseFormRegister, UseFormSetValue } from "react-hook-form"; -import { FieldError } from "react-hook-form/dist/types/errors"; +import { UseFormRegister, UseFormSetValue, FieldError } from "react-hook-form"; import Checkboxes from "../../commonComponents/Checkboxes"; import { Facility } from "../../../generated/graphql"; diff --git a/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx b/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx index 117629b449..07bdfe59f3 100644 --- a/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx +++ b/frontend/src/app/commonComponents/UserDetails/UserOrganizationFormField.tsx @@ -9,11 +9,13 @@ import { useGetAllOrganizationsQuery } from "../../../generated/graphql"; interface OrganizationSelectFormFieldProps { control?: Control; disabled?: boolean; + isLoadingUser: boolean; } const UserOrganizationFormField: React.FC = ({ control, disabled, + isLoadingUser, }) => { /** * Form integration @@ -69,7 +71,7 @@ const UserOrganizationFormField: React.FC = ({ Error: {error?.message} )} - {loadingOrgs ? ( + {loadingOrgs || isLoadingUser ? (
; registrationProps: UseFormRegisterReturn; - error: any; + error?: FieldError; disabled: boolean; } diff --git a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx index 0cfab19be0..8cb7757af0 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/AdminManageUser.tsx @@ -79,9 +79,10 @@ export const AdminManageUser: React.FC = () => { const [navItemSelected, setNavItemSelected] = useState< "User information" | "Organization access" >("User information"); - const [getUserByEmail] = useFindUserByEmailLazyQuery({ - fetchPolicy: "no-cache", - }); + const [getUserByEmail, { loading: loadingUser }] = + useFindUserByEmailLazyQuery({ + fetchPolicy: "no-cache", + }); const [isUpdating, setIsUpdating] = useState(false); const [updateUserName] = useUpdateUserNameMutation(); const [updateUserEmail] = useEditUserEmailMutation(); @@ -151,9 +152,11 @@ export const AdminManageUser: React.FC = () => { email: emailAddress, }, }); + showSuccess("", `User email address changed to ${emailAddress}`); setSearchState((prevState) => ({ ...prevState, + searchEmail: emailAddress, foundUser: { ...prevState.foundUser, email: emailAddress, @@ -241,14 +244,14 @@ export const AdminManageUser: React.FC = () => { variables: { userId: foundUser?.id as string }, }); - await retrieveUser(); + await retrieveUser(searchEmail); showSuccess("", `User account undeleted for ${userFullName}`); }); }; - const retrieveUser = async () => { - return getUserByEmail({ variables: { email: searchEmail.trim() } }).then( + const retrieveUser = async (username: string) => { + return getUserByEmail({ variables: { email: username } }).then( ({ data, error }) => { if (!data?.user && !error) { setSearchState((prevState) => ({ @@ -294,7 +297,7 @@ export const AdminManageUser: React.FC = () => { const handleInputChange = (e: React.ChangeEvent) => { setSearchState((prevState) => ({ ...prevState, - searchEmail: e.target.value, + searchEmail: e.target.value?.trim(), })); }; const handleClearFilter = () => { @@ -303,7 +306,7 @@ export const AdminManageUser: React.FC = () => { }; const handleSearch = () => { - retrieveUser(); + retrieveUser(searchEmail); }; /** @@ -314,6 +317,7 @@ export const AdminManageUser: React.FC = () => { handleSubmit, formState: { errors, isDirty }, register, + trigger, reset, setValue, } = useForm(); @@ -347,6 +351,8 @@ export const AdminManageUser: React.FC = () => { if (formValues.organizationId !== foundUser?.organization?.id) { setValue("facilityIds", []); } + } else { + setValue("facilityIds", []); } }, [ queryGetFacilitiesByOrgId, @@ -389,7 +395,7 @@ export const AdminManageUser: React.FC = () => { }); showSuccess("", `Access updated for ${userFullName}`); - await retrieveUser(); + await retrieveUser(searchEmail); }); }; @@ -508,16 +514,20 @@ export const AdminManageUser: React.FC = () => { )}
diff --git a/frontend/src/app/supportAdmin/ManageUsers/OrgAccessTab.tsx b/frontend/src/app/supportAdmin/ManageUsers/OrgAccessTab.tsx index 32c0a48539..dabdefe814 100644 --- a/frontend/src/app/supportAdmin/ManageUsers/OrgAccessTab.tsx +++ b/frontend/src/app/supportAdmin/ManageUsers/OrgAccessTab.tsx @@ -1,6 +1,13 @@ -import React, { useRef, useState } from "react"; -import { FieldError } from "react-hook-form/dist/types/errors"; -import { UseFormHandleSubmit, UseFormSetValue } from "react-hook-form"; +import React, { useEffect, useRef, useState } from "react"; +import { + UseFormHandleSubmit, + UseFormSetValue, + FieldError, + Control, + UseFormTrigger, + FieldErrors, + UseFormRegister, +} from "react-hook-form"; import Button from "../../commonComponents/Button/Button"; import UserFacilitiesSettings from "../../Settings/Users/UserFacilitiesSettings"; @@ -19,32 +26,41 @@ import { OrgAccessFormData } from "./AdminManageUser"; export interface UserAccessTabProps { user: User; onSubmit: (data: OrgAccessFormData) => Promise; - handleSubmit: UseFormHandleSubmit; - setValue: UseFormSetValue; - formValues: OrgAccessFormData; - control: any; - register: any; - errors: any; - isDirty: boolean; + isLoadingUser: boolean; isLoadingFacilities: boolean; - isUpdating: boolean; + disabled: boolean; facilityList: Pick[]; + formProps: { + handleSubmit: UseFormHandleSubmit; + setValue: UseFormSetValue; + formValues: OrgAccessFormData; + control: Control; + trigger: UseFormTrigger; + register: UseFormRegister; + errors: FieldErrors; + isDirty: boolean; + }; } const OrgAccessTab: React.FC = ({ user, facilityList, - setValue, onSubmit, - handleSubmit, - control, - formValues, - register, - errors, - isDirty, + formProps, + isLoadingUser, isLoadingFacilities, - isUpdating, + disabled, }) => { + const { + handleSubmit, + setValue, + formValues, + control, + trigger, + register, + errors, + isDirty, + } = formProps; const selectedRole = formValues.role; const fullName = displayFullName( @@ -135,6 +151,13 @@ const OrgAccessTab: React.FC = ({ ); + /** + * Run validation on facilities when role is changed + */ + useEffect(() => { + trigger("facilityIds"); + }, [formValues.role, trigger]); + /** * HTML */ @@ -148,12 +171,16 @@ const OrgAccessTab: React.FC = ({ onSubmit={handleSubmit(handleConfirmationAndSubmit)} className="usa-form usa-form--large manage-user-form__site-admin" > - + = ({ register={register} error={errors.facilityIds as FieldError} setValue={setValue} - disabled={ - isUpdating || isLoadingFacilities || selectedRole === "ADMIN" - } + disabled={disabled || isLoadingFacilities || selectedRole === "ADMIN"} />