From e830a095b809044502a527e8309061f73678b911 Mon Sep 17 00:00:00 2001 From: Davor <davor.ivankovic2@gmail.com> Date: Tue, 19 Jul 2022 16:47:50 +0200 Subject: [PATCH] added steps modal with next and previous buttons - changes for improve app access setting flow --- .../Modal/StepsModal/StepsModal.tsx | 143 ++++++++++++++++++ src/components/Modal/StepsModal/index.ts | 1 + src/components/Modal/StepsModal/types.ts | 16 ++ src/components/Modal/index.ts | 1 + .../ProgressSteps.tsx | 0 .../{Steps => ProgressSteps}/index.ts | 0 .../{Steps => ProgressSteps}/types.ts | 0 src/components/UserModal/UserModal.tsx | 2 + src/components/index.ts | 4 +- .../MultipleUsersModal/MultipleUsersModal.tsx | 33 +++- src/services/users/redux/actions.ts | 21 +-- src/services/users/transformations.ts | 5 +- 12 files changed, 200 insertions(+), 26 deletions(-) create mode 100644 src/components/Modal/StepsModal/StepsModal.tsx create mode 100644 src/components/Modal/StepsModal/index.ts create mode 100644 src/components/Modal/StepsModal/types.ts rename src/components/{Steps => ProgressSteps}/ProgressSteps.tsx (100%) rename src/components/{Steps => ProgressSteps}/index.ts (100%) rename src/components/{Steps => ProgressSteps}/types.ts (100%) diff --git a/src/components/Modal/StepsModal/StepsModal.tsx b/src/components/Modal/StepsModal/StepsModal.tsx new file mode 100644 index 00000000..7e2135b4 --- /dev/null +++ b/src/components/Modal/StepsModal/StepsModal.tsx @@ -0,0 +1,143 @@ +import React, { Fragment, useRef } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; +import { XIcon } from '@heroicons/react/solid'; +import { StepsModalProps } from './types'; + +export const StepsModal: React.FC<StepsModalProps> = ({ + open, + onClose, + onSave, + onNext, + onPrevious, + children, + title = '', + useCancelButton = false, + isLoading = false, + leftActions = <></>, + showSaveButton = false, + showPreviousButton = false, + saveButtonDisabled = false, +}) => { + const cancelButtonRef = useRef(null); + const saveButtonRef = useRef(null); + const nextButtonRef = useRef(null); + const previousButtonRef = useRef(null); + + return ( + <Transition.Root show={open} as={Fragment}> + <Dialog as="div" className="fixed z-10 inset-0 overflow-y-auto" initialFocus={cancelButtonRef} onClose={() => {}}> + <div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0"> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0" + enterTo="opacity-100" + leave="ease-in duration-200" + leaveFrom="opacity-100" + leaveTo="opacity-0" + > + <Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" /> + </Transition.Child> + + {/* This element is to trick the browser into centering the modal contents. */} + <span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true"> + ​ + </span> + <Transition.Child + as={Fragment} + enter="ease-out duration-300" + enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" + enterTo="opacity-100 translate-y-0 sm:scale-100" + leave="ease-in duration-200" + leaveFrom="opacity-100 translate-y-0 sm:scale-100" + leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" + > + <div className="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full relative"> + {isLoading && ( + <Dialog.Overlay className="inset-0 bg-gray-400 bg-opacity-75 transition-opacity absolute flex justify-center items-center"> + <svg + className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + > + <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" /> + <path + className="opacity-75" + fill="currentColor" + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" + /> + </svg> + </Dialog.Overlay> + )} + + {!useCancelButton && ( + <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:items-center sm:justify-between"> + <div>{title}</div> + <button + type="button" + className="w-full inline-flex justify-center rounded-md border border-gray-200 p-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" + onClick={onClose} + ref={cancelButtonRef} + > + <XIcon className="h-5 w-5 text-gray-400" aria-hidden="true" /> + </button> + </div> + )} + + <div className="bg-white px-4 p-6">{children}</div> + <div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex"> + {leftActions} + <div className="ml-auto sm:flex sm:flex-row-reverse"> + {showSaveButton && onSave && ( + <button + type="button" + className={`w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm ${ + saveButtonDisabled ? 'opacity-50' : '' + }`} + onClick={onSave} + ref={saveButtonRef} + disabled={saveButtonDisabled} + > + Save Changes + </button> + )} + {!showSaveButton && ( + <button + type="button" + className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm" + onClick={onNext} + ref={nextButtonRef} + > + Next + </button> + )} + {showPreviousButton && ( + <button + type="button" + className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:ml-3 sm:w-auto sm:text-sm" + onClick={onPrevious} + ref={previousButtonRef} + > + Previous + </button> + )} + {useCancelButton && ( + <button + type="button" + className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-200 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm" + onClick={onClose} + ref={cancelButtonRef} + > + Cancel + </button> + )} + </div> + </div> + </div> + </Transition.Child> + </div> + </Dialog> + </Transition.Root> + ); +}; diff --git a/src/components/Modal/StepsModal/index.ts b/src/components/Modal/StepsModal/index.ts new file mode 100644 index 00000000..7c9bae5e --- /dev/null +++ b/src/components/Modal/StepsModal/index.ts @@ -0,0 +1 @@ +export { StepsModal } from './StepsModal'; diff --git a/src/components/Modal/StepsModal/types.ts b/src/components/Modal/StepsModal/types.ts new file mode 100644 index 00000000..f0b5bbd1 --- /dev/null +++ b/src/components/Modal/StepsModal/types.ts @@ -0,0 +1,16 @@ +import React from 'react'; + +export type StepsModalProps = { + open: boolean; + onClose: () => void; + onNext: () => void; + onPrevious: () => void; + title?: string; + onSave?: () => void; + useCancelButton?: boolean; + isLoading?: boolean; + leftActions?: React.ReactNode; + showSaveButton?: boolean; + showPreviousButton?: boolean; + saveButtonDisabled?: boolean; +}; diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts index 4ef3e384..e5800df0 100644 --- a/src/components/Modal/index.ts +++ b/src/components/Modal/index.ts @@ -1,2 +1,3 @@ export { ConfirmationModal } from './ConfirmationModal'; export { Modal } from './Modal'; +export { StepsModal } from './StepsModal'; diff --git a/src/components/Steps/ProgressSteps.tsx b/src/components/ProgressSteps/ProgressSteps.tsx similarity index 100% rename from src/components/Steps/ProgressSteps.tsx rename to src/components/ProgressSteps/ProgressSteps.tsx diff --git a/src/components/Steps/index.ts b/src/components/ProgressSteps/index.ts similarity index 100% rename from src/components/Steps/index.ts rename to src/components/ProgressSteps/index.ts diff --git a/src/components/Steps/types.ts b/src/components/ProgressSteps/types.ts similarity index 100% rename from src/components/Steps/types.ts rename to src/components/ProgressSteps/types.ts diff --git a/src/components/UserModal/UserModal.tsx b/src/components/UserModal/UserModal.tsx index 0993b94f..404db4b1 100644 --- a/src/components/UserModal/UserModal.tsx +++ b/src/components/UserModal/UserModal.tsx @@ -68,6 +68,8 @@ export const UserModal = ({ open, onClose, userId, setUserId }: UserModalProps) setAdminRoleSelected(isAdminDashboardRoleSelected); if (isAdminDashboardRoleSelected) { fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.Admin })); + } else { + fields.forEach((field, index) => update(index, { name: field.name, role: UserRole.User })); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [dashboardRole]); diff --git a/src/components/index.ts b/src/components/index.ts index aff49e2e..9a2f607c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,6 +3,6 @@ export { Header } from './Header'; export { Table } from './Table'; export { Banner } from './Banner'; export { Tabs } from './Tabs'; -export { Modal, ConfirmationModal } from './Modal'; +export { Modal, ConfirmationModal, StepsModal } from './Modal'; export { UserModal } from './UserModal'; -export { ProgressSteps } from './Steps'; +export { ProgressSteps } from './ProgressSteps'; diff --git a/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx b/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx index 02200e8f..74f60c69 100644 --- a/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx +++ b/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx @@ -2,11 +2,11 @@ import React, { useEffect, useState } from 'react'; import _ from 'lodash'; import { useFieldArray, useForm, useWatch } from 'react-hook-form'; -import { Banner, Modal, ProgressSteps } from 'src/components'; +import { Banner, StepsModal, ProgressSteps } from 'src/components'; import { Select, TextArea } from 'src/components/Form'; import { MultipleUsersData, UserRole, useUsers } from 'src/services/users'; import { allAppAccessList } from 'src/components/UserModal/consts'; -import { ProgressStepInfo, ProgressStepStatus } from 'src/components/Steps/types'; +import { ProgressStepInfo, ProgressStepStatus } from 'src/components/ProgressSteps/types'; import { initialMultipleUsersForm, MultipleUsersModalProps } from './types'; export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) => { @@ -181,6 +181,8 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) = onClose(); }; + const getActiveStepIndex = () => _.findIndex(steps, { status: ProgressStepStatus.Current }); + const updateStepsStatus = (nextIndex: number) => { const updatedSteps = [...steps]; _.forEach(updatedSteps, (step, index) => { @@ -200,17 +202,32 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) = updateStepsStatus(activeStepIndex); }; - const getActiveStepIndex = _.findIndex(steps, { status: ProgressStepStatus.Current }); - const disableSave = _.isEmpty(csvDataWatch) || _.some(steps, { status: ProgressStepStatus.Upcoming }); + const handleNext = () => { + const nextIndex = getActiveStepIndex() + 1; + updateStepsStatus(nextIndex); + }; + + const handlePrevious = () => { + const nextIndex = getActiveStepIndex() - 1; + updateStepsStatus(nextIndex); + }; + + const activeStepIndex = getActiveStepIndex(); + const showSave = !_.some(steps, { status: ProgressStepStatus.Upcoming }); + const showPrevious = _.some(steps, { status: ProgressStepStatus.Complete }); return ( - <Modal + <StepsModal onClose={handleClose} open={open} onSave={handleSave} + onNext={handleNext} + onPrevious={handlePrevious} + showPreviousButton={showPrevious} isLoading={userModalLoading} useCancelButton - saveButtonDisabled={disableSave} + showSaveButton={showSave} + saveButtonDisabled={_.isEmpty(csvDataWatch)} > <div className="bg-white px-4"> <div className="space-y-10 divide-y divide-gray-200"> @@ -220,12 +237,12 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) = </div> <div className="sm:px-6 pt-6"> <ProgressSteps steps={steps} onStepClick={handleStepClick}> - {getActiveStepIndex === 0 ? renderUsersCsvDataInput() : renderAppAccess()} + {activeStepIndex === 0 ? renderUsersCsvDataInput() : renderAppAccess()} </ProgressSteps> </div> </div> </div> </div> - </Modal> + </StepsModal> ); }; diff --git a/src/services/users/redux/actions.ts b/src/services/users/redux/actions.ts index ac5adc98..a09b5902 100644 --- a/src/services/users/redux/actions.ts +++ b/src/services/users/redux/actions.ts @@ -227,21 +227,14 @@ export const createBatchUsers = (users: any) => async (dispatch: Dispatch<any>) }); // show information about created users - const createdUsersNumber = _.size(responseData.createdUsers); - if (createdUsersNumber > 0) { - showToast( - `${_.size(responseData.createdUsers)} ${createdUsersNumber > 1 ? 'users' : 'user'} created successfully.`, - ToastType.Success, - ); + if (!_.isEmpty(responseData.success)) { + showToast(responseData.success.message, ToastType.Success); } - const notCreatedUsersNumber = _.size(responseData.notCreatedUsers); - if (notCreatedUsersNumber > 0) { - showToast( - `${_.size(responseData.notCreatedUsers)} - ${notCreatedUsersNumber > 1 ? 'users' : 'user'} not created with - ${notCreatedUsersNumber > 1 ? 'emails' : 'email'}: \n${responseData.notCreatedUsers.join(',\n')}`, - ToastType.Error, - ); + if (!_.isEmpty(responseData.existing)) { + showToast(responseData.existing.message, ToastType.Error); + } + if (!_.isEmpty(responseData.failed)) { + showToast(responseData.failed.message, ToastType.Error); } dispatch(fetchUsers()); diff --git a/src/services/users/transformations.ts b/src/services/users/transformations.ts index 65b869c3..21e7db46 100644 --- a/src/services/users/transformations.ts +++ b/src/services/users/transformations.ts @@ -86,7 +86,8 @@ export const transformRequestMultipleUsers = (data: MultipleUsersData) => { export const transformBatchResponse = (response: any): any => { return { - createdUsers: response.created_users, - notCreatedUsers: response.not_created_users, + success: response.success, + existing: response.existing, + failed: response.failed, }; }; -- GitLab