From 4b7ce6c6703c8302d3677ee88a18ad751e4ee3f3 Mon Sep 17 00:00:00 2001 From: David Paul Graham <43794491+dpgraham4401@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:03:15 -0500 Subject: [PATCH] Wasteline table UI updates (#680) * quick fix for manifest status editable (it is editable if the manifest is saved as a draft) * initial use collapsable list instead of table for displaying waste lines * add WasteRowActions dropdown * change coverage provider to v8 (instead of istanbul) * export ContainterType type alias and use to remove typescript ts-ignores in quantity form * fix alignment for wate line table items * change waste and transporter action drop down options based on whether the details are open * add test suite for QuantityForm * export QuantityUOM types to fix ts-ignores in quantity form unit of measure selection * allowing editing the manifest type for manifest that have not been saved to e-Manifest yet --- .idea/runConfigurations/Vitest_Coverage.xml | 15 ++ .idea/runConfigurations/Vitest_UI.xml | 15 ++ client/package-lock.json | 28 +++ client/package.json | 2 +- .../Manifest/GeneralInfo/GeneralInfoForm.tsx | 9 +- .../GeneralInfo/ManifestStatusSelect.spec.tsx | 47 +++- .../GeneralInfo/ManifestStatusSelect.tsx | 20 +- .../GeneralInfo/ManifestTypeField.spec.tsx | 24 +- .../GeneralInfo/ManifestTypeSelect.tsx | 20 +- .../Transporter/TransporterRowActions.tsx | 19 +- .../Manifest/Transporter/TransporterTable.tsx | 24 +- .../Manifest/WasteLine/QuantityForm.spec.tsx | 14 ++ .../Manifest/WasteLine/QuantityForm.tsx | 52 ++--- .../Manifest/WasteLine/WasteLineForm.tsx | 6 +- .../WasteLineTable/WasteLineTable.tsx | 210 +++++++++++++----- .../WasteLineTable/WasteRowActions.tsx | 108 ++++++--- .../Manifest/WasteLine/wasteLineSchema.ts | 4 + client/vite.config.ts | 6 +- 18 files changed, 468 insertions(+), 155 deletions(-) create mode 100644 .idea/runConfigurations/Vitest_Coverage.xml create mode 100644 .idea/runConfigurations/Vitest_UI.xml create mode 100644 client/src/components/Manifest/WasteLine/QuantityForm.spec.tsx diff --git a/.idea/runConfigurations/Vitest_Coverage.xml b/.idea/runConfigurations/Vitest_Coverage.xml new file mode 100644 index 000000000..12345e012 --- /dev/null +++ b/.idea/runConfigurations/Vitest_Coverage.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Vitest_UI.xml b/.idea/runConfigurations/Vitest_UI.xml new file mode 100644 index 000000000..69f2d0129 --- /dev/null +++ b/.idea/runConfigurations/Vitest_UI.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index d74b8d665..7bb9de14f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -44,6 +44,7 @@ "@types/uuid": "^9.0.7", "@vitejs/plugin-react": "^4.2.1", "@vitest/coverage-istanbul": "^1.1.0", + "@vitest/coverage-v8": "^1.1.3", "@vitest/ui": "^1.1.0", "c8": "^8.0.1", "esbuild": "0.19.10", @@ -3090,6 +3091,33 @@ "vitest": "^1.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.1.3.tgz", + "integrity": "sha512-Uput7t3eIcbSTOTQBzGtS+0kah96bX+szW9qQrLeGe3UmgL2Akn8POnyC2lH7XsnREZOds9aCUTxgXf+4HX5RA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.2", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "^1.0.0" + } + }, "node_modules/@vitest/expect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.1.0.tgz", diff --git a/client/package.json b/client/package.json index a2eea91f3..128f6e4f0 100644 --- a/client/package.json +++ b/client/package.json @@ -52,7 +52,7 @@ "@types/react-dom": "^18.2.18", "@types/uuid": "^9.0.7", "@vitejs/plugin-react": "^4.2.1", - "@vitest/coverage-istanbul": "^1.1.0", + "@vitest/coverage-v8": "^1.1.3", "@vitest/ui": "^1.1.0", "c8": "^8.0.1", "esbuild": "0.19.10", diff --git a/client/src/components/Manifest/GeneralInfo/GeneralInfoForm.tsx b/client/src/components/Manifest/GeneralInfo/GeneralInfoForm.tsx index 5359b6a52..df38f00ac 100644 --- a/client/src/components/Manifest/GeneralInfo/GeneralInfoForm.tsx +++ b/client/src/components/Manifest/GeneralInfo/GeneralInfoForm.tsx @@ -1,6 +1,6 @@ import { ManifestStatusSelect } from 'components/Manifest/GeneralInfo/ManifestStatusSelect'; import { ManifestTypeSelect } from 'components/Manifest/GeneralInfo/ManifestTypeSelect'; -import { Manifest, SubmissionType } from 'components/Manifest/manifestSchema'; +import { Manifest } from 'components/Manifest/manifestSchema'; import { HtForm, InfoIconTooltip } from 'components/UI'; import { useReadOnly } from 'hooks/manifest'; import React from 'react'; @@ -13,13 +13,6 @@ interface GeneralInfoFormProps { isDraft?: boolean; } -const submissionTypeOptions: Array<{ value: SubmissionType; label: string }> = [ - { value: 'FullElectronic', label: 'Electronic' }, - { value: 'Hybrid', label: 'Hybrid' }, - { value: 'DataImage5Copy', label: 'Data + Image' }, - { value: 'Image', label: 'Image Only' }, -]; - export function GeneralInfoForm({ manifestData, isDraft }: GeneralInfoFormProps) { const [readOnly] = useReadOnly(); const manifestForm = useFormContext(); diff --git a/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.spec.tsx b/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.spec.tsx index 5057e4972..c3491ee39 100644 --- a/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.spec.tsx +++ b/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.spec.tsx @@ -1,15 +1,15 @@ import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; import { ManifestStatusSelect } from 'components/Manifest/GeneralInfo/ManifestStatusSelect'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; -import userEvent from '@testing-library/user-event'; -import { setupServer } from 'msw/node'; -import { userApiMocks } from 'test-utils/mock'; -import { http, HttpResponse } from 'msw'; +import { createMockHandler, createMockSite } from 'test-utils/fixtures'; import { createMockProfileResponse } from 'test-utils/fixtures/mockUser'; +import { userApiMocks } from 'test-utils/mock'; import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; -import { createMockHandler, createMockSite } from 'test-utils/fixtures'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; const server = setupServer(...userApiMocks); afterEach(() => cleanup()); @@ -37,6 +37,7 @@ describe('Manifest Status Field', () => { }); test('is not editable if read only', () => { renderWithProviders(); + expect(screen.getByLabelText(/Status/i)).toBeDisabled(); }); test('is editable if the manifest is a draft', () => { @@ -121,4 +122,38 @@ describe('Manifest Status Field', () => { expect(scheduledOption).toBeInTheDocument(); expect(scheduledOption).toHaveAttribute('aria-disabled', 'true'); }); + test('is editable if draft status', async () => { + // Arrange + const userGeneratorSite = createMockSite({ + handler: createMockHandler({ + siteType: 'Generator', + epaSiteId: 'MOCKVAGEN001', + }), + }); + server.use( + http.get(`${API_BASE_URL}/api/user/profile`, () => { + return HttpResponse.json( + { + ...createMockProfileResponse({ + sites: [ + { + site: userGeneratorSite, + eManifest: 'signer', + }, + ], + }), + }, + { status: 200 } + ); + }) + ); + renderWithProviders(, { + preloadedState: { manifest: { readOnly: false } }, + useFormProps: { values: { status: 'NotAssigned', generator: userGeneratorSite.handler } }, + }); + await userEvent.click(screen.getByLabelText(/Status/i)); + const scheduledOption = screen.queryByRole('option', { name: /Scheduled/i }); + expect(scheduledOption).toBeInTheDocument(); + expect(scheduledOption).toHaveAttribute('aria-disabled', 'true'); + }); }); diff --git a/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.tsx b/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.tsx index e6a0cf2e2..4fff48d58 100644 --- a/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.tsx +++ b/client/src/components/Manifest/GeneralInfo/ManifestStatusSelect.tsx @@ -1,11 +1,11 @@ import { Manifest, ManifestStatus } from 'components/Manifest/manifestSchema'; import { HtForm, InfoIconTooltip } from 'components/UI'; +import { useManifestStatus } from 'hooks/manifest'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { useGetProfileQuery } from 'store'; -import { manifest } from 'services'; import Select, { SingleValue } from 'react-select'; -import { useManifestStatus } from 'hooks/manifest'; +import { manifest } from 'services'; +import { useGetProfileQuery } from 'store'; interface StatusOption { value: ManifestStatus; @@ -43,6 +43,18 @@ export function ManifestStatusSelect({ readOnly, isDraft }: ManifestStatusFieldP }) : []; + // Whether the status field should be disabled + const isStatusDisabled = ( + status: ManifestStatus | undefined, + readOnly: boolean | undefined, + isDraft: boolean | undefined + ) => { + if (readOnly) return true; // Read only manifests can never be edited + if (isDraft) return false; // Draft manifests can always be edited if not read only + if (status === 'NotAssigned') return false; // if editing a previously saved 'NotAssigned', allow editing + return true; + }; + return ( @@ -61,7 +73,7 @@ export function ManifestStatusSelect({ readOnly, isDraft }: ManifestStatusFieldP aria-label="status" value={selectedStatus} options={statusOptions} - isDisabled={readOnly || !isDraft} + isDisabled={isStatusDisabled(status, readOnly, isDraft)} data-testid="manifestStatus" isLoading={profileLoading || !profile} onChange={(option: SingleValue) => { diff --git a/client/src/components/Manifest/GeneralInfo/ManifestTypeField.spec.tsx b/client/src/components/Manifest/GeneralInfo/ManifestTypeField.spec.tsx index b5899cd6a..976ed8370 100644 --- a/client/src/components/Manifest/GeneralInfo/ManifestTypeField.spec.tsx +++ b/client/src/components/Manifest/GeneralInfo/ManifestTypeField.spec.tsx @@ -1,12 +1,12 @@ import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import { ManifestTypeSelect } from 'components/Manifest/GeneralInfo/ManifestTypeSelect'; +import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; -import { setupServer } from 'msw/node'; -import { userApiMocks } from 'test-utils/mock'; -import { ManifestTypeSelect } from 'components/Manifest/GeneralInfo/ManifestTypeSelect'; -import userEvent from '@testing-library/user-event'; import { createMockHandler } from 'test-utils/fixtures'; +import { userApiMocks } from 'test-utils/mock'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; const server = setupServer(...userApiMocks); afterEach(() => cleanup()); @@ -62,4 +62,18 @@ describe('Manifest Type Field', () => { 'false' ); }); + test('is never editable if past draft status', async () => { + renderWithProviders(, { + useFormProps: { + values: { + status: 'Scheduled', + generator: createMockHandler({ + canEsign: true, + siteType: 'Generator', + }), + }, + }, + }); + expect(screen.getByLabelText(/Type/i)).toBeDisabled(); + }); }); diff --git a/client/src/components/Manifest/GeneralInfo/ManifestTypeSelect.tsx b/client/src/components/Manifest/GeneralInfo/ManifestTypeSelect.tsx index da4e86dd3..14688cf9b 100644 --- a/client/src/components/Manifest/GeneralInfo/ManifestTypeSelect.tsx +++ b/client/src/components/Manifest/GeneralInfo/ManifestTypeSelect.tsx @@ -1,8 +1,9 @@ -import { Manifest, SubmissionType } from 'components/Manifest/manifestSchema'; +import { Manifest, ManifestStatus, SubmissionType } from 'components/Manifest/manifestSchema'; import { HtForm } from 'components/UI'; +import { useManifestStatus } from 'hooks/manifest'; import React, { useState } from 'react'; -import Select, { SingleValue } from 'react-select'; import { Controller, useFormContext } from 'react-hook-form'; +import Select, { SingleValue } from 'react-select'; interface SubmissionTypeOption { value: SubmissionType; @@ -25,11 +26,24 @@ export function ManifestTypeSelect({ isDraft?: boolean; }) { const manifestForm = useFormContext(); + const [status] = useManifestStatus(); const generatorCanESign = manifestForm.getValues('generator.canEsign'); const [submissionType, setSubmissionType] = useState( manifestForm.getValues('submissionType') || 'Hybrid' ); const selectedType = submissionTypeOptions.filter((option) => option.value === submissionType); + + const isTypeDisabled = ( + readOnly: boolean | undefined, + isDraft: boolean | undefined, + status: ManifestStatus | undefined + ) => { + if (readOnly) return true; // Read only manifests can never be edited + if (isDraft) return false; // Draft manifests can always be edited if not read only + if (status === 'NotAssigned') return false; // if editing a previously saved 'NotAssigned', allow editing + return true; + }; + return ( @@ -43,7 +57,7 @@ export function ManifestTypeSelect({ {...field} id="submissionType" aria-label="submission type" - isDisabled={readOnly || !isDraft} + isDisabled={isTypeDisabled(readOnly, isDraft, status)} value={selectedType} options={submissionTypeOptions} defaultValue={submissionTypeOptions[0]} diff --git a/client/src/components/Manifest/Transporter/TransporterRowActions.tsx b/client/src/components/Manifest/Transporter/TransporterRowActions.tsx index bded1311e..c0bda24df 100644 --- a/client/src/components/Manifest/Transporter/TransporterRowActions.tsx +++ b/client/src/components/Manifest/Transporter/TransporterRowActions.tsx @@ -2,11 +2,13 @@ import { faArrowDown, faArrowUp, faEllipsisVertical, + faEye, + faEyeSlash, faTrash, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import React, { MouseEventHandler, ReactElement } from 'react'; -import { Col, Dropdown, Row } from 'react-bootstrap'; +import React, { MouseEventHandler, ReactElement, useState } from 'react'; +import { Col, Dropdown, Row, useAccordionButton } from 'react-bootstrap'; import { UseFieldArrayRemove, UseFieldArraySwap } from 'react-hook-form'; interface TranRowActionProps { @@ -14,6 +16,7 @@ interface TranRowActionProps { length: number; removeTransporter: UseFieldArrayRemove; swapTransporter: UseFieldArraySwap; + eventKey: string; } interface RowDropdownItems { @@ -34,9 +37,12 @@ export function TransporterRowActions({ removeTransporter, swapTransporter, length, + eventKey, }: TranRowActionProps) { const isFirst = index === 0; const isLast = index + 1 === length; + const [open, setOpen] = useState(false); + const decoratedOnClick = useAccordionButton(eventKey, () => setOpen(!open)); const actions: RowDropdownItems[] = [ { @@ -73,6 +79,15 @@ export function TransporterRowActions({ disabled: false, label: `remove transporter ${index}`, }, + { + text: open ? 'Close' : 'Details', + icon: , + onClick: (event) => { + decoratedOnClick(event); + }, + disabled: false, + label: `View transporter ${index} details`, + }, ]; return ( diff --git a/client/src/components/Manifest/Transporter/TransporterTable.tsx b/client/src/components/Manifest/Transporter/TransporterTable.tsx index d97652c9b..06da5e042 100644 --- a/client/src/components/Manifest/Transporter/TransporterTable.tsx +++ b/client/src/components/Manifest/Transporter/TransporterTable.tsx @@ -53,15 +53,13 @@ function TransporterTable({ transporters, arrayFieldMethods, setupSign }: Transp {transporters.map((transporter, index) => { return ( - - - -
-
{transporter.order}
- {transporter.name} -
+ + + +
{transporter.order}
+ {transporter.name} - + {readOnly ? ( )} - - {readOnly || ( + + {readOnly ? ( + + ) : ( )} - - -
diff --git a/client/src/components/Manifest/WasteLine/QuantityForm.spec.tsx b/client/src/components/Manifest/WasteLine/QuantityForm.spec.tsx new file mode 100644 index 000000000..3312a11dc --- /dev/null +++ b/client/src/components/Manifest/WasteLine/QuantityForm.spec.tsx @@ -0,0 +1,14 @@ +import '@testing-library/jest-dom'; +import { QuantityForm } from 'components/Manifest/WasteLine/QuantityForm'; +import React from 'react'; +import { cleanup, renderWithProviders, screen } from 'test-utils'; +import { afterEach, describe, expect, test } from 'vitest'; + +afterEach(() => cleanup()); + +describe('QuantityForm', () => { + test('renders', () => { + renderWithProviders(); + expect(screen.getByText(/Container/i)).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/Manifest/WasteLine/QuantityForm.tsx b/client/src/components/Manifest/WasteLine/QuantityForm.tsx index f5f33dc82..ef74af9e1 100644 --- a/client/src/components/Manifest/WasteLine/QuantityForm.tsx +++ b/client/src/components/Manifest/WasteLine/QuantityForm.tsx @@ -1,12 +1,16 @@ import { ErrorMessage } from '@hookform/error-message'; -import { WasteLine } from 'components/Manifest/WasteLine/wasteLineSchema'; +import { + ContainerType, + QuantityUOM, + WasteLine, +} from 'components/Manifest/WasteLine/wasteLineSchema'; import { HtForm } from 'components/UI'; import React from 'react'; import { Col, Form, Row } from 'react-bootstrap'; import { Controller, useFormContext } from 'react-hook-form'; import Select from 'react-select'; -const unitsOfMeasurements = [ +const unitsOfMeasurements: Array = [ { code: 'P', description: 'Pounds' }, { code: 'T', description: 'Tons (2000 Pounds)' }, { code: 'K', description: 'Kilograms' }, @@ -17,7 +21,7 @@ const unitsOfMeasurements = [ { code: 'N', description: 'Cubic Meters' }, ]; -const containerTypes = [ +const containerTypes: Array = [ { code: 'BA', description: 'Burlap, cloth, paper, or plastic bags' }, { code: 'DT', description: 'Dump truck' }, { code: 'CF', description: 'Fiber or plastic boxes, cartons, cases' }, @@ -42,11 +46,14 @@ export function QuantityForm() { return ( <> - - + + - Number + + Count + {errors.quantity?.containerNumber?.message} - + Container Type @@ -67,21 +74,17 @@ export function QuantityForm() { return ( option.description} + getOptionLabel={(option) => option.description ?? option.code} getOptionValue={(option) => option.code} openMenuOnFocus={false} classNames={{ diff --git a/client/src/components/Manifest/WasteLine/WasteLineForm.tsx b/client/src/components/Manifest/WasteLine/WasteLineForm.tsx index 0ae515503..d043356a0 100644 --- a/client/src/components/Manifest/WasteLine/WasteLineForm.tsx +++ b/client/src/components/Manifest/WasteLine/WasteLineForm.tsx @@ -6,7 +6,7 @@ import { DotIdSelect } from 'components/Manifest/WasteLine/DotIdSelect'; import { HazardousWasteForm } from 'components/Manifest/WasteLine/HazardousWasteForm'; import { WasteLine, wasteLineSchema } from 'components/Manifest/WasteLine/wasteLineSchema'; import { HtCard, HtForm } from 'components/UI'; -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { Button, Col, Container, Form, Row, Stack } from 'react-bootstrap'; import { Controller, FormProvider, UseFieldArrayReturn, useForm } from 'react-hook-form'; import { QuantityForm } from './QuantityForm'; @@ -24,11 +24,11 @@ interface WasteLineFormProps { * @constructor */ export function WasteLineForm({ handleClose, wasteForm, waste, lineNumber }: WasteLineFormProps) { - const [dotHazardous, setDotHazardous] = React.useState( + const [dotHazardous, setDotHazardous] = useState( waste?.dotHazardous === undefined ? true : waste.dotHazardous ); const { editWasteLineIndex } = useContext(ManifestContext); - const [epaWaste, setEpaWaste] = React.useState( + const [epaWaste, setEpaWaste] = useState( waste?.epaWaste === undefined ? true : waste.epaWaste ); diff --git a/client/src/components/Manifest/WasteLine/WasteLineTable/WasteLineTable.tsx b/client/src/components/Manifest/WasteLine/WasteLineTable/WasteLineTable.tsx index 9305d6368..b4c000262 100644 --- a/client/src/components/Manifest/WasteLine/WasteLineTable/WasteLineTable.tsx +++ b/client/src/components/Manifest/WasteLine/WasteLineTable/WasteLineTable.tsx @@ -1,10 +1,13 @@ +import { useAutoAnimate } from '@formkit/auto-animate/react'; +import { faAngleRight, faCheckCircle, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Manifest } from 'components/Manifest'; import { ManifestContext, ManifestContextType } from 'components/Manifest/ManifestForm'; import { WasteLine } from 'components/Manifest/WasteLine/wasteLineSchema'; import { WasteRowActions } from 'components/Manifest/WasteLine/WasteLineTable/WasteRowActions'; import { useReadOnly } from 'hooks/manifest'; -import React, { useContext } from 'react'; -import { Table } from 'react-bootstrap'; +import React, { useContext, useState } from 'react'; +import { Accordion, Button, Card, Col, Row, useAccordionButton } from 'react-bootstrap'; import { UseFieldArrayReturn } from 'react-hook-form'; interface WasteLineTableProps { @@ -13,70 +16,161 @@ interface WasteLineTableProps { wasteForm: UseFieldArrayReturn; } +const WasteCodes = ({ wasteLine }: { wasteLine: WasteLine }) => { + return ( + + {wasteLine.hazardousWaste?.federalWasteCodes?.map((item) => item.code).join(', ')} + + ); +}; + +const CustomToggle = ({ eventKey }: any) => { + const [open, setOpen] = useState(false); + const decoratedOnClick = useAccordionButton(eventKey, () => setOpen(!open)); + + return ( + + ); +}; + export function WasteLineTable({ wastes, toggleWLModal, wasteForm }: WasteLineTableProps) { const { setEditWasteLineIndex } = useContext(ManifestContext); - const [readonly] = useReadOnly(); + const [parent] = useAutoAnimate(); + const [readOnly] = useReadOnly(); if (!wastes || wastes.length < 1) { return <>; } return ( - - - - - - - - - {readonly ?? } - - - - {wastes.map((wasteLine, index) => { + <> + + {wastes.map((waste, index) => { + const description = waste.wasteDescription ?? waste.dotInformation?.printedDotInformation; + return ( - - - - - - - {readonly ?? ( - - )} - + + + +
{waste.lineNumber}
+ + {waste.quantity.containerNumber} {waste.quantity.containerType.description} + + + + {readOnly ? ( + + ) : ( + setEditWasteLineIndex(index)} + toggleWLModal={toggleWLModal} + index={index} + eventKey={waste.lineNumber.toString()} + /> + )} + + + + + + + Description +

{description}

+ + + + + Federal Waste +

+ {waste.epaWaste ? ( + + ) : ( + + )} +

+ + + DOT Haz Material +

+ {waste.dotHazardous ? ( + + ) : ( + + )} +

+ + + PCB Waste +

+ {waste.pcb ? ( + + ) : ( + + )} +

+ + + + + Waste Codes +

+ +

+ + + + + Quantity +

+ {waste.quantity.quantity} {waste.quantity.unitOfMeasurement.description} +

+ + + Container(s) +

+ {waste.quantity.containerNumber} {waste.quantity.containerType.description} +

+ + + {/*
#DescriptionContainersTypeCodesEdit
{wasteLine.lineNumber} - {wasteLine.wasteDescription ?? wasteLine.dotInformation?.printedDotInformation} - {wasteLine.quantity?.containerNumber}{String(wasteLine.quantity?.containerType.code)} - - {wasteLine.hazardousWaste?.federalWasteCodes?.map((item) => item.code).join(', ')} - - - -
*/} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/*
DescriptionFederal Waste
{description}*/} + {/*
*/} +
+
+
); })} - - +
+ ); } diff --git a/client/src/components/Manifest/WasteLine/WasteLineTable/WasteRowActions.tsx b/client/src/components/Manifest/WasteLine/WasteLineTable/WasteRowActions.tsx index 98ac98c3e..e8fa6c60e 100644 --- a/client/src/components/Manifest/WasteLine/WasteLineTable/WasteRowActions.tsx +++ b/client/src/components/Manifest/WasteLine/WasteLineTable/WasteRowActions.tsx @@ -1,8 +1,14 @@ -import { faTools, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { + faEllipsisVertical, + faEye, + faEyeSlash, + faPen, + faTrash, +} from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Manifest } from 'components/Manifest'; -import React from 'react'; -import { Button } from 'react-bootstrap'; +import React, { MouseEventHandler, ReactElement, useState } from 'react'; +import { Col, Dropdown, Row, useAccordionButton } from 'react-bootstrap'; import { UseFieldArrayReturn } from 'react-hook-form'; interface WasteRowActionProps { @@ -10,6 +16,15 @@ interface WasteRowActionProps { wasteForm: UseFieldArrayReturn; toggleWLModal: () => void; setEditWasteLine: (index: number) => void; + eventKey: string; +} + +interface RowDropdownItems { + text: string; + icon: ReactElement; + onClick: MouseEventHandler; + disabled: boolean; + label: string; } /** @@ -21,31 +36,72 @@ function WasteRowActions({ wasteForm, setEditWasteLine, toggleWLModal, + eventKey, }: WasteRowActionProps) { + const [open, setOpen] = useState(false); + const decoratedOnClick = useAccordionButton(eventKey, () => setOpen(!open)); + + const actions: RowDropdownItems[] = [ + { + text: 'Remove', + icon: , + onClick: () => { + wasteForm.remove(index); + }, + disabled: false, + label: `remove waste line ${index}`, + }, + { + text: 'Edit', + icon: , + onClick: () => { + setEditWasteLine(index); + toggleWLModal(); + }, + disabled: false, + label: `remove waste line ${index}`, + }, + { + text: open ? 'Close' : 'Details', + icon: , + onClick: (event) => { + decoratedOnClick(event); + }, + disabled: false, + label: `view waste line ${index} details`, + }, + ]; + return ( -
- - -
+ <> + + + + + + {actions.map((action, i) => { + return ( + + + {action.icon} + + {action.text} + + + + ); + })} + + + ); } diff --git a/client/src/components/Manifest/WasteLine/wasteLineSchema.ts b/client/src/components/Manifest/WasteLine/wasteLineSchema.ts index b15b24c3d..fc090ddff 100644 --- a/client/src/components/Manifest/WasteLine/wasteLineSchema.ts +++ b/client/src/components/Manifest/WasteLine/wasteLineSchema.ts @@ -21,11 +21,15 @@ export const containerTypeSchema = z.object({ description: z.string().optional(), }); +export type ContainerType = z.infer; + export const quantityUOMSchema = z.object({ code: z.enum(['P', 'T', 'K', 'M', 'G', 'L', 'Y', 'N']), description: z.string().optional(), }); +export type QuantityUOM = z.infer; + const quantitySchema = z.object({ containerNumber: z.number(), containerType: containerTypeSchema, diff --git a/client/vite.config.ts b/client/vite.config.ts index 4330770f8..2c3b9c976 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -45,7 +45,7 @@ export default defineConfig({ test: { environment: 'jsdom', coverage: { - provider: 'istanbul', // or 'v8' + provider: 'v8', // or 'istanbul' reporter: ['text', 'json', 'html'], exclude: [ '**/node_modules/**', @@ -53,6 +53,10 @@ export default defineConfig({ '**/dist/**', '**/coverage/**', '**/src/setupTests.ts', + '**/src/reportWebVitals.ts', + '**/public/**', + '**/*.d.ts', + '**/index.ts', ], }, globals: true,