From bc1a07251522af0e747b1f8ed797b4767e32bf05 Mon Sep 17 00:00:00 2001
From: Davor <davor.ivankovic2@gmail.com>
Date: Tue, 19 Jul 2022 00:21:06 +0200
Subject: [PATCH] implement progress steps component

---
 src/components/Form/Select/Select.tsx         |   2 +-
 src/components/Modal/Modal/Modal.tsx          |   6 +-
 src/components/Modal/Modal/types.ts           |   1 +
 src/components/Steps/ProgressSteps.tsx        | 107 ++++++++++
 src/components/Steps/index.ts                 |   1 +
 src/components/Steps/types.ts                 |  20 ++
 src/components/UserModal/consts.ts            |   2 +-
 src/components/index.ts                       |   1 +
 .../MultipleUsersModal/MultipleUsersModal.tsx | 187 ++++++++++++++----
 9 files changed, 286 insertions(+), 41 deletions(-)
 create mode 100644 src/components/Steps/ProgressSteps.tsx
 create mode 100644 src/components/Steps/index.ts
 create mode 100644 src/components/Steps/types.ts

diff --git a/src/components/Form/Select/Select.tsx b/src/components/Form/Select/Select.tsx
index aad51fa9..06a5322f 100644
--- a/src/components/Form/Select/Select.tsx
+++ b/src/components/Form/Select/Select.tsx
@@ -26,7 +26,7 @@ export const Select = ({ control, name, label, options, disabled = false }: Sele
         value={field.value ? field.value : ''} // input value
         name={name} // send down the input name
         ref={field.ref} // send input ref, so we can focus on input when error appear
-        className="shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
+        className="block shadow-sm focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 rounded-md"
         disabled={disabled}
       >
         {options?.map((option) => (
diff --git a/src/components/Modal/Modal/Modal.tsx b/src/components/Modal/Modal/Modal.tsx
index 048b315d..e61aaee5 100644
--- a/src/components/Modal/Modal/Modal.tsx
+++ b/src/components/Modal/Modal/Modal.tsx
@@ -12,6 +12,7 @@ export const Modal: React.FC<ModalProps> = ({
   useCancelButton = false,
   isLoading = false,
   leftActions = <></>,
+  saveButtonDisabled = false,
 }) => {
   const cancelButtonRef = useRef(null);
   const saveButtonRef = useRef(null);
@@ -86,9 +87,12 @@ export const Modal: React.FC<ModalProps> = ({
                   <div className="ml-auto sm:flex sm:flex-row-reverse">
                     <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"
+                      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>
diff --git a/src/components/Modal/Modal/types.ts b/src/components/Modal/Modal/types.ts
index afd720cb..e679e696 100644
--- a/src/components/Modal/Modal/types.ts
+++ b/src/components/Modal/Modal/types.ts
@@ -8,4 +8,5 @@ export type ModalProps = {
   useCancelButton?: boolean;
   isLoading?: boolean;
   leftActions?: React.ReactNode;
+  saveButtonDisabled?: boolean;
 };
diff --git a/src/components/Steps/ProgressSteps.tsx b/src/components/Steps/ProgressSteps.tsx
new file mode 100644
index 00000000..2a24d675
--- /dev/null
+++ b/src/components/Steps/ProgressSteps.tsx
@@ -0,0 +1,107 @@
+import _ from 'lodash';
+import React from 'react';
+import { ProgressStepsProps, ProgressStepStatus } from './types';
+
+export const ProgressSteps: React.FC<ProgressStepsProps> = ({ steps, onNext, onPrevious, onStepClick, children }) => {
+  const handleNext = () => {
+    if (onNext) {
+      onNext();
+    }
+  };
+  const handlePrevious = () => {
+    if (onPrevious) {
+      onPrevious();
+    }
+  };
+
+  const showNextPage = () => {
+    if (onNext) {
+      return _.some(steps, { status: ProgressStepStatus.Upcoming });
+    }
+    return false;
+  };
+
+  const showPreviousPage = () => {
+    if (onPrevious) {
+      return _.some(steps, { status: ProgressStepStatus.Complete });
+    }
+    return false;
+  };
+
+  const handleStepClick = (stepId: string) => {
+    if (onStepClick) {
+      onStepClick(stepId);
+    }
+  };
+
+  return (
+    <>
+      <nav aria-label="Progress">
+        {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
+        <ol role="list" className="space-y-4 md:flex md:space-y-0 md:space-x-8 mb-4">
+          {steps.map((step) => (
+            <li key={step.name} className="md:flex-1" onClick={() => handleStepClick(step.id)}>
+              {step.status === ProgressStepStatus.Complete ? (
+                <a
+                  href={step.href}
+                  className="group pl-4 py-2 flex flex-col border-l-4 border-primary-500 md:pl-0 md:pb-0 md:border-l-0 md:border-t-4"
+                >
+                  <span className="text-xs text-primary-600 font-semibold tracking-wide uppercase group-hover:text-primary-800">
+                    {step.id}
+                  </span>
+                  <span className="text-sm font-medium">{step.name}</span>
+                </a>
+              ) : step.status === ProgressStepStatus.Current ? (
+                <a
+                  href={step.href}
+                  className="pl-4 py-2 flex flex-col border-l-4 border-primary-500 md:pl-0 md:pb-0 md:border-l-0 md:border-t-4"
+                  aria-current="step"
+                >
+                  <span className="text-xs text-primary-600 font-semibold tracking-wide uppercase">{step.id}</span>
+                  <span className="text-sm font-medium">{step.name}</span>
+                </a>
+              ) : (
+                <a
+                  href={step.href}
+                  className="group pl-4 py-2 flex flex-col border-l-4 border-gray-200 hover:border-gray-300 md:pl-0 md:pb-0 md:border-l-0 md:border-t-4"
+                >
+                  <span className="text-xs text-gray-500 font-semibold tracking-wide uppercase group-hover:text-gray-700">
+                    {step.id}
+                  </span>
+                  <span className="text-sm font-medium">{step.name}</span>
+                </a>
+              )}
+            </li>
+          ))}
+        </ol>
+      </nav>
+
+      {children}
+
+      {(showNextPage() || showPreviousPage()) && (
+        <div className="pt-4 sm sm:flex">
+          <div className="ml-auto sm:flex sm:flex-row-reverse">
+            {showNextPage() && (
+              <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={handleNext}
+              >
+                Next
+              </button>
+            )}
+            {showPreviousPage() && (
+              <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={handlePrevious}
+              >
+                Previous
+              </button>
+            )}
+          </div>
+        </div>
+      )}
+    </>
+  );
+};
diff --git a/src/components/Steps/index.ts b/src/components/Steps/index.ts
new file mode 100644
index 00000000..2bc8859d
--- /dev/null
+++ b/src/components/Steps/index.ts
@@ -0,0 +1 @@
+export { ProgressSteps } from './ProgressSteps';
diff --git a/src/components/Steps/types.ts b/src/components/Steps/types.ts
new file mode 100644
index 00000000..3eed8b3b
--- /dev/null
+++ b/src/components/Steps/types.ts
@@ -0,0 +1,20 @@
+export type ProgressStepsProps = {
+  steps: ProgressStepInfo[];
+  onNext?: () => void;
+  onPrevious?: () => void;
+  onStepClick?: (stepId: string) => void;
+};
+
+export interface ProgressStepInfo {
+  id: string;
+  name: string;
+  status: ProgressStepStatus;
+  component?: React.ReactNode;
+  href?: string;
+}
+
+export enum ProgressStepStatus {
+  Complete = 0,
+  Current = 1,
+  Upcoming = 2,
+}
diff --git a/src/components/UserModal/consts.ts b/src/components/UserModal/consts.ts
index 2229a230..f1566526 100644
--- a/src/components/UserModal/consts.ts
+++ b/src/components/UserModal/consts.ts
@@ -24,12 +24,12 @@ export const appAccessList = [
 ];
 
 export const allAppAccessList = [
-  ...appAccessList,
   {
     name: 'dashboard',
     image: '/assets/logo-small.svg',
     label: 'Dashboard',
   },
+  ...appAccessList,
 ];
 
 export const initialAppRoles = [
diff --git a/src/components/index.ts b/src/components/index.ts
index 565e17d3..aff49e2e 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -5,3 +5,4 @@ export { Banner } from './Banner';
 export { Tabs } from './Tabs';
 export { Modal, ConfirmationModal } from './Modal';
 export { UserModal } from './UserModal';
+export { ProgressSteps } from './Steps';
diff --git a/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx b/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx
index 63e3021b..02200e8f 100644
--- a/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx
+++ b/src/modules/users/components/MultipleUsersModal/MultipleUsersModal.tsx
@@ -1,50 +1,64 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import _ from 'lodash';
-import { useFieldArray, useForm } from 'react-hook-form';
+import { useFieldArray, useForm, useWatch } from 'react-hook-form';
 
-import { Modal, Tabs } from 'src/components';
+import { Banner, Modal, 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 { initialMultipleUsersForm, MultipleUsersModalProps } from './types';
 
 export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) => {
+  const [steps, setSteps] = useState<ProgressStepInfo[]>([]);
+  const [isAdminRoleSelected, setAdminRoleSelected] = useState(false);
   const { createUsers, userModalLoading } = useUsers();
 
   const { control, handleSubmit } = useForm<MultipleUsersData>({
     defaultValues: initialMultipleUsersForm,
   });
 
-  const { fields } = useFieldArray({
+  const { fields, update } = useFieldArray({
     control,
     name: 'appRoles',
   });
 
-  const handleSave = async () => {
-    try {
-      await handleSubmit((data) => createUsers(data))();
-    } catch (e: any) {
-      // Continue
-    }
+  const dashboardRole = useWatch({
+    control,
+    name: 'appRoles.0.role',
+  });
 
-    onClose();
-  };
+  const csvDataWatch = useWatch({
+    control,
+    name: 'csvUserData',
+  });
 
-  const handleClose = () => {
-    onClose();
-  };
+  useEffect(() => {
+    const isAdminDashboardRoleSelected = dashboardRole === UserRole.Admin;
+    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]);
 
   const renderUsersCsvDataInput = () => {
     return (
       <div>
-        <TextArea
-          control={control}
-          name="csvUserData"
-          label="CSV user data"
-          placeholder="Please paste users in CSV format: email,name"
-          rows={15}
-          required
-        />
+        <div className="mt-8">
+          <h3 className="text-lg leading-6 font-medium text-gray-900">CSV data</h3>
+        </div>
+        <div className="mt-6">
+          <TextArea
+            control={control}
+            name="csvUserData"
+            placeholder={`Please paste users in CSV format: email, name\nuser1@example.com,User One\nuser2@example.com,User Two`}
+            rows={15}
+            required
+          />
+        </div>
       </div>
     );
   };
@@ -56,11 +70,18 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) =
           <h3 className="text-lg leading-6 font-medium text-gray-900">App Access</h3>
         </div>
 
+        {isAdminRoleSelected && (
+          <div className="sm:col-span-6">
+            <Banner title="Admin users automatically have admin-level access to all apps." titleSm="Admin user" />
+          </div>
+        )}
+
         <div>
           <div className="flow-root mt-6">
-            <ul className="-my-5 divide-y divide-gray-200 ">
-              {fields.map((item, index) => {
-                return (
+            <ul className="-my-5 divide-y divide-gray-200">
+              {fields
+                .filter((field) => field.name === 'dashboard')
+                .map((item, index) => (
                   <li className="py-4" key={item.name}>
                     <div className="flex items-center space-x-4">
                       <div className="flex-shrink-0 flex-1 flex items-center">
@@ -73,13 +94,12 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) =
                           {_.find(allAppAccessList, ['name', item.name!])?.label}
                         </h3>
                       </div>
-                      <div>
+                      <div className="sm:col-span-2">
                         <Select
                           key={item.id}
                           control={control}
                           name={`appRoles.${index}.role`}
                           options={[
-                            { value: UserRole.NoAccess, name: 'No Access' },
                             { value: UserRole.User, name: 'User' },
                             { value: UserRole.Admin, name: 'Admin' },
                           ]}
@@ -87,8 +107,43 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) =
                       </div>
                     </div>
                   </li>
-                );
-              })}
+                ))}
+              {!isAdminRoleSelected &&
+                fields.map((item, index) => {
+                  if (item.name === 'dashboard') {
+                    return null;
+                  }
+
+                  return (
+                    <li className="py-4" key={item.name}>
+                      <div className="flex items-center space-x-4">
+                        <div className="flex-shrink-0 flex-1 flex items-center">
+                          <img
+                            className="h-10 w-10 rounded-md overflow-hidden"
+                            src={_.find(allAppAccessList, ['name', item.name!])?.image}
+                            alt={item.name ?? 'Image'}
+                          />
+                          <h3 className="ml-4 text-md leading-6 font-medium text-gray-900">
+                            {_.find(allAppAccessList, ['name', item.name!])?.label}
+                          </h3>
+                        </div>
+                        <div className="sm:col-span-2">
+                          <Select
+                            key={item.id}
+                            control={control}
+                            name={`appRoles.${index}.role`}
+                            disabled={isAdminRoleSelected}
+                            options={[
+                              { value: UserRole.NoAccess, name: 'No Access' },
+                              { value: UserRole.User, name: 'User' },
+                              { value: UserRole.Admin, name: 'Admin' },
+                            ]}
+                          />
+                        </div>
+                      </div>
+                    </li>
+                  );
+                })}
             </ul>
           </div>
         </div>
@@ -96,21 +151,77 @@ export const MultipleUsersModal = ({ open, onClose }: MultipleUsersModalProps) =
     );
   };
 
+  useEffect(() => {
+    setSteps([
+      {
+        id: 'Step 1',
+        name: 'Enter CSV user data',
+        status: ProgressStepStatus.Current,
+      },
+      {
+        id: 'Step 2',
+        name: 'Define app access roles',
+        status: ProgressStepStatus.Upcoming,
+      },
+    ]);
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [open]);
+
+  const handleSave = async () => {
+    try {
+      await handleSubmit((data) => createUsers(data))();
+    } catch (e: any) {
+      // Continue
+    }
+
+    onClose();
+  };
+
+  const handleClose = () => {
+    onClose();
+  };
+
+  const updateStepsStatus = (nextIndex: number) => {
+    const updatedSteps = [...steps];
+    _.forEach(updatedSteps, (step, index) => {
+      if (index < nextIndex) {
+        step.status = ProgressStepStatus.Complete;
+      } else if (index === nextIndex) {
+        step.status = ProgressStepStatus.Current;
+      } else {
+        step.status = ProgressStepStatus.Upcoming;
+      }
+    });
+    setSteps(updatedSteps);
+  };
+
+  const handleStepClick = (stepId: string) => {
+    const activeStepIndex = _.findIndex(steps, { id: stepId });
+    updateStepsStatus(activeStepIndex);
+  };
+
+  const getActiveStepIndex = _.findIndex(steps, { status: ProgressStepStatus.Current });
+  const disableSave = _.isEmpty(csvDataWatch) || _.some(steps, { status: ProgressStepStatus.Upcoming });
+
   return (
-    <Modal onClose={handleClose} open={open} onSave={handleSave} isLoading={userModalLoading} useCancelButton>
+    <Modal
+      onClose={handleClose}
+      open={open}
+      onSave={handleSave}
+      isLoading={userModalLoading}
+      useCancelButton
+      saveButtonDisabled={disableSave}
+    >
       <div className="bg-white px-4">
         <div className="space-y-10 divide-y divide-gray-200">
           <div>
             <div>
               <h3 className="text-lg leading-6 font-medium text-gray-900">Add new users</h3>
             </div>
-            <div className="sm:p-6">
-              <Tabs
-                tabs={[
-                  { name: 'Add users', component: renderUsersCsvDataInput() },
-                  { name: 'App access roles', component: renderAppAccess() },
-                ]}
-              />
+            <div className="sm:px-6 pt-6">
+              <ProgressSteps steps={steps} onStepClick={handleStepClick}>
+                {getActiveStepIndex === 0 ? renderUsersCsvDataInput() : renderAppAccess()}
+              </ProgressSteps>
             </div>
           </div>
         </div>
-- 
GitLab