diff --git a/client/src/components/Layout/Root.tsx b/client/src/components/Layout/Root.tsx index 9b37af8a3..48a7b2309 100644 --- a/client/src/components/Layout/Root.tsx +++ b/client/src/components/Layout/Root.tsx @@ -1,11 +1,11 @@ import { ErrorBoundary } from 'components/Error'; +import { HtSpinner } from 'components/UI'; import React, { createContext, Dispatch, SetStateAction, Suspense, useState } from 'react'; import { Container } from 'react-bootstrap'; import { Outlet } from 'react-router-dom'; import { PrivateRoute } from './PrivateRoute'; import { Sidebar } from './Sidebar/Sidebar'; import { TopNav } from './TopNav/TopNav'; -import { HtSpinner } from 'components/UI'; export interface NavContextProps { showSidebar: boolean; @@ -29,7 +29,7 @@ export function Root() { - + } > diff --git a/client/src/components/Manifest/ActionBtns/ManifestActionBtns.spec.tsx b/client/src/components/Manifest/ActionBtns/ManifestActionBtns.spec.tsx new file mode 100644 index 000000000..c0a4fa431 --- /dev/null +++ b/client/src/components/Manifest/ActionBtns/ManifestActionBtns.spec.tsx @@ -0,0 +1,65 @@ +import '@testing-library/jest-dom'; +import { ManifestActionBtns } from 'components/Manifest/ActionBtns/ManifestActionBtns'; +import React from 'react'; +import { cleanup, renderWithProviders, screen } from 'test-utils'; +import { afterEach, describe, expect, test } from 'vitest'; + +afterEach(() => { + cleanup(); +}); + +describe('ManifestActionBtns', () => { + test.each([ + ['Scheduled', true], + ['Scheduled', false], + ['NotAssigned', false], + ['NotAssigned', true], + ])('renders a save button when editing and {status: "%s", signAble: %s}', (status, signAble) => { + renderWithProviders( + // @ts-ignore - Status is expected to be a ManifestStatus, but we're passing a string + + ); + expect(screen.queryByRole('button', { name: /Save/i })).toBeInTheDocument(); + }); + test.each([[true, false]])( + 'renders a edit button when {readOnly: %s, signAble: %s}', + (readOnly, signAble) => { + renderWithProviders( + // @ts-ignore - Status is expected to be a ManifestStatus, but we're passing a string + + ); + expect(screen.queryByRole('button', { name: /Edit/i })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /Sign/i })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /Save/i })).not.toBeInTheDocument(); + } + ); + test.each([[true, true]])( + 'renders a sign button when {readOnly: %s, signAble: %s}', + (readOnly, signAble) => { + renderWithProviders( + // @ts-ignore - Status is expected to be a ManifestStatus, but we're passing a string + + ); + expect(screen.queryByRole('button', { name: /Sign/i })).toBeInTheDocument(); + } + ); + test.each([ + [true, true], + [true, false], + [false, false], + [false, true], + ])( + 'never renders a sign button when draft status with {readOnly: %s, signAble: %s}', + (readOnly, signAble) => { + renderWithProviders( + // @ts-ignore - Status is expected to be a ManifestStatus, but we're passing a string + + ); + expect(screen.queryByRole('button', { name: /Sign/i })).not.toBeInTheDocument(); + } + ); +}); diff --git a/client/src/components/Manifest/ActionBtns/ManifestActionBtns.tsx b/client/src/components/Manifest/ActionBtns/ManifestActionBtns.tsx index fe9f0a29a..ee0ffca71 100644 --- a/client/src/components/Manifest/ActionBtns/ManifestActionBtns.tsx +++ b/client/src/components/Manifest/ActionBtns/ManifestActionBtns.tsx @@ -1,44 +1,50 @@ -import React from 'react'; -import { FloatingActionBtn } from 'components/UI'; -import { ManifestStatus } from 'components/Manifest/manifestSchema'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { faFloppyDisk, faPen, faPenToSquare } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ManifestStatus } from 'components/Manifest/manifestSchema'; +import { FloatingActionBtn } from 'components/UI'; +import React from 'react'; interface ManifestActionBtnsProps { manifestStatus?: ManifestStatus; - readonly?: boolean; + readOnly?: boolean; signAble?: boolean; } export function ManifestActionBtns({ manifestStatus, - readonly, + readOnly, signAble, }: ManifestActionBtnsProps) { - let variant = 'success'; - let text = ''; - let icon = faFloppyDisk; - let type: 'button' | 'submit' | 'reset' | undefined = 'button'; - console.log('signAble', signAble); - if (signAble) { + let variant: string | undefined = undefined; + let text: string | undefined = undefined; + let icon: IconProp | undefined = undefined; + let type: 'button' | 'submit' | 'reset' | undefined = undefined; + let name: string | undefined = undefined; + if (!readOnly || manifestStatus === 'NotAssigned') { + variant = 'success'; + icon = faFloppyDisk; + text = 'Save'; + type = 'submit'; + name = 'saveFAB'; + } else if (signAble) { variant = 'primary'; icon = faPen; text = 'Sign'; - } else if (readonly) { + name = 'signFAB'; + type = 'button'; + } else if (readOnly) { variant = 'primary'; icon = faPenToSquare; text = 'Edit'; - } else if (!readonly) { - variant = 'success'; - icon = faFloppyDisk; - text = 'Save'; - type = 'submit'; + name = 'editFAB'; + type = 'button'; } else { return <>; } return ( - + {text} diff --git a/client/src/components/Manifest/ManifestForm.spec.tsx b/client/src/components/Manifest/ManifestForm.spec.tsx index f971f410a..b36945f8b 100644 --- a/client/src/components/Manifest/ManifestForm.spec.tsx +++ b/client/src/components/Manifest/ManifestForm.spec.tsx @@ -42,26 +42,23 @@ describe('ManifestForm validation', () => { test('a generator is required', async () => { // Arrange renderWithProviders(); - const saveBtn = screen.getByRole('button', { name: /Save/i }); + const saveBtn = screen.getAllByRole('button', { name: /Save/i })[0]; // Act await userEvent.click(saveBtn); // Assert expect(await screen.findByText(/Generator is required/i)).toBeInTheDocument(); }); test('at least one transporter is required', async () => { - // Arrange renderWithProviders(); - const saveBtn = screen.getByRole('button', { name: /Save/i }); - // Act + const saveBtn = screen.getAllByRole('button', { name: /Save/i })[0]; await userEvent.click(saveBtn); - // Assert expect( await screen.findByText(/A manifest requires at least 1 transporters/i) ).toBeInTheDocument(); }); test('at least one waste line is required', async () => { renderWithProviders(); - const saveBtn = screen.getByRole('button', { name: /Save/i }); + const saveBtn = screen.getAllByRole('button', { name: /Save/i })[0]; await userEvent.click(saveBtn); expect( await screen.findByText(/A manifest requires at least 1 waste line/i) @@ -69,7 +66,7 @@ describe('ManifestForm validation', () => { }); test('a TSDF is required', async () => { renderWithProviders(); - const saveBtn = screen.getByRole('button', { name: /Save/i }); + const saveBtn = screen.getAllByRole('button', { name: /Save/i })[0]; await userEvent.click(saveBtn); expect( await screen.findByText(/Designated receiving facility is required/i) diff --git a/client/src/components/Manifest/ManifestForm.tsx b/client/src/components/Manifest/ManifestForm.tsx index e364b6fac..3c961969d 100644 --- a/client/src/components/Manifest/ManifestForm.tsx +++ b/client/src/components/Manifest/ManifestForm.tsx @@ -1,5 +1,6 @@ import { ErrorMessage } from '@hookform/error-message'; import { zodResolver } from '@hookform/resolvers/zod'; +import { ManifestActionBtns } from 'components/Manifest/ActionBtns/ManifestActionBtns'; import { AdditionalInfoForm } from 'components/Manifest/AdditionalInfo'; import { UpdateRcra } from 'components/Manifest/UpdateRcra/UpdateRcra'; import { WasteLine } from 'components/Manifest/WasteLine/wasteLineSchema'; @@ -9,6 +10,7 @@ import React, { createContext, useEffect, useState } from 'react'; import { Alert, Button, Col, Container, Form, Row, Stack } from 'react-bootstrap'; import { FormProvider, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; import { manifest } from 'services'; import { selectHaztrakSiteEpaIds, @@ -22,8 +24,6 @@ import { Manifest, manifestSchema, ManifestStatus } from './manifestSchema'; import { QuickerSignData, QuickerSignModal, QuickSignBtn } from './QuickerSign'; import { Transporter, TransporterTable } from './Transporter'; import { EditWasteModal, WasteLineTable } from './WasteLine'; -import { toast } from 'react-toastify'; -import { ManifestActionBtns } from 'components/Manifest/ActionBtns/ManifestActionBtns'; const defaultValues: Manifest = { transporters: [], @@ -191,9 +191,7 @@ export function ManifestForm({ const nextSigner = manifest.getNextSigner(manifestData); const userSiteIds = useAppSelector(selectHaztrakSiteEpaIds); - console.log('userSiteIds', userSiteIds); - console.log('nextSigner', nextSigner); - + // Whether the user has permissions and manifest is in a state to be signed const signAble = userSiteIds.includes(nextSigner ?? ''); const isDraft = manifestData?.manifestTrackingNumber === undefined; @@ -606,7 +604,7 @@ export function ManifestForm({ diff --git a/client/src/components/Manifest/WasteLine/WasteLineForm.tsx b/client/src/components/Manifest/WasteLine/WasteLineForm.tsx index 2a58e62fa..a96829453 100644 --- a/client/src/components/Manifest/WasteLine/WasteLineForm.tsx +++ b/client/src/components/Manifest/WasteLine/WasteLineForm.tsx @@ -7,7 +7,7 @@ import { HazardousWasteForm } from 'components/Manifest/WasteLine/HazardousWaste import { WasteLine, wasteLineSchema } from 'components/Manifest/WasteLine/wasteLineSchema'; import { HtCard, HtForm } from 'components/UI'; import React, { useContext } from 'react'; -import { Button, Col, Container, Form, Row } from 'react-bootstrap'; +import { Button, Col, Container, Form, Row, Stack } from 'react-bootstrap'; import { Controller, FormProvider, UseFieldArrayReturn, useForm } from 'react-hook-form'; import { QuantityForm } from './QuantityForm'; @@ -103,130 +103,132 @@ export function WasteLineForm({ handleClose, wasteForm, waste, lineNumber }: Was - - -
General Information
- - - ( - toggleDotHazardous(e.target.checked)} - /> - )} - /> - - - ( - toggleEpaWaste(e.target.checked)} - /> - )} - /> - - - - - - - - - - {!dotHazardous ? ( - - - Waste Description - + + +
General Information
+ + + ( + toggleDotHazardous(e.target.checked)} + /> + )} /> -
{errors.wasteDescription?.message}
-
-
- ) : ( - - + + + ( + toggleEpaWaste(e.target.checked)} + /> + )} + /> + + + + + + + +
+ + {!dotHazardous ? ( + - DOT Description + Waste Description -
- {errors.dotInformation?.printedDotInformation?.message} -
+
{errors.wasteDescription?.message}
- - - - DOT ID Number - - - +
+ ) : ( + + + + DOT Description + +
+ {errors.dotInformation?.printedDotInformation?.message} +
+
+ + + + DOT ID Number + + + +
+ )} + + + + +
Container and Quantity
+ + - )} -
-
- - -
Container and Quantity
- - - -
-
- - -
Waste Codes
- - - -
-
- - -
Special Handing Instructions and Additional Info
- - {/* AdditionalInfoForm needs to be used in the context of a +
+
+ + +
Waste Codes
+ + + +
+
+ + +
Special Handing Instructions and Additional Info
+ + {/* AdditionalInfoForm needs to be used in the context of a WasteLineForm or Manifest (e.g., surrounded by FormProvider*/} - - -
-
+ + + + +
+ + +
+ -
- - -
); diff --git a/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx b/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx index 380f5b371..e280e3078 100644 --- a/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx +++ b/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx @@ -11,7 +11,7 @@ const testTaskID = 'testTaskId'; const server = setupServer( ...[ - http.post(`${API_BASE_URL}rcra/manifest/emanifest/sync`, (info) => { + http.post(`${API_BASE_URL}rcra/manifest/emanifest/sync`, () => { // Mock Sync Site Manifests response return HttpResponse.json( { diff --git a/client/src/components/UI/FloatingActionBtn.tsx b/client/src/components/UI/FloatingActionBtn.tsx index 3c66026be..29edcfbec 100644 --- a/client/src/components/UI/FloatingActionBtn.tsx +++ b/client/src/components/UI/FloatingActionBtn.tsx @@ -17,7 +17,7 @@ export function FloatingActionBtn({ position === undefined || position === 'bottom-right' ? 'position-fixed bottom-0 end-0 m-5' : 'position-fixed bottom-0 start-0 m-5'; - const defaultClasses = `p-2 rounded-5 ${extended ? 'px-4' : ''}`; + const defaultClasses = `p-2 rounded-5 shadow bg-gradient ${extended ? 'px-4' : ''}`; return (
-
- + if (isLoading) return ; + if (data) + return ( + + +
+ +
+ +
-
- - - {isLoading ? ( - - ) : ( - data && - )} - - - - - ); + + + + + + + + ); + else return
Something went wrong
; } diff --git a/client/src/features/SiteList/SiteList.tsx b/client/src/features/SiteList/SiteList.tsx index 193eff17d..e9f093f36 100644 --- a/client/src/features/SiteList/SiteList.tsx +++ b/client/src/features/SiteList/SiteList.tsx @@ -1,5 +1,5 @@ import { SiteListGroup } from 'components/HaztrakSite'; -import { HtCard } from 'components/UI'; +import { HtCard, HtSpinner } from 'components/UI'; import { useTitle } from 'hooks'; import React from 'react'; import { Container } from 'react-bootstrap'; @@ -16,7 +16,7 @@ export function SiteList() { {isLoading && !error ? ( - + ) : data ? ( ) : (