-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
emanifest backend logic separation and Floating Acion Buttons (#663)
* Rename class 'ManifestService' to 'EManifest'. This class will now be used to encapsulate intereactions with e-Manifest (the RCRAInfo module) we're extracting the e-Manifest parts of the manifest service to help keep the separation of responsiblities clear * add a faker library provider for manifest status and use in manifest_factory * create_manifest service module functionality and utilize in CreateManifestView * use different endpoints depending on whether the user is saving a manifest to RCRAInfo or just saving a manifest to haztrak * refactor various components to use RTK query hooks instead of dispatching async thunks or custom hook API calls. In effect, we are centralizing our API layer into the redux store * add badge to show handler search component is searching though the RCRAInfo web services * fix warning from redux selectors made with createSelector that used output selectors that returned input values. The warning stated that likely would result in memoization errors. See the redux documentation https://redux.js.org/usage/deriving-data-selectors\#writing-memoized-selectors-with-reselect Add new selectHaztrakSiteEpaIds redux selector * Refactor rcrainfo search badge, which notifies the user when HazTrak is using the RCRAInfo site search services, into separate component. The badge also shows a warning when a user's organization admin has not set up their RCRAInfo API ID and Keys in RCRAInfo. * refactor DOT ID number select component to use RTK query's useLazyQuery for dot ID number selection. Also, the component fetched and pre-populate DOT ID numbers with options upon mounting * New UI component, FloatingActionBtn a reusable floating action button, aimed at implementing material design's FAB * Add floating Action buttons to Manifest, the ManifestActionBtns shows various actions depending on the manifest state * Minor UI adjustments and ManifestActionBtns test suite Adjust ManifestForm test suite to accommodate multiple save buttons (one floating action button, one regular) Minor changes to make HtSpinner for flexible and use correct profile selector in Profile Feature
- Loading branch information
1 parent
35cad0f
commit acfc60d
Showing
52 changed files
with
1,232 additions
and
780 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
client/src/components/Manifest/ActionBtns/ManifestActionBtns.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
<ManifestActionBtns readOnly={false} manifestStatus={status} signAble={signAble} /> | ||
); | ||
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 | ||
<ManifestActionBtns readOnly={readOnly} manifestStatus={'Scheduled'} signAble={signAble} /> | ||
); | ||
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 | ||
<ManifestActionBtns readOnly={readOnly} manifestStatus={'Scheduled'} signAble={signAble} /> | ||
); | ||
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 | ||
<ManifestActionBtns | ||
readOnly={readOnly} | ||
manifestStatus={'NotAssigned'} | ||
signAble={signAble} | ||
/> | ||
); | ||
expect(screen.queryByRole('button', { name: /Sign/i })).not.toBeInTheDocument(); | ||
} | ||
); | ||
}); |
54 changes: 54 additions & 0 deletions
54
client/src/components/Manifest/ActionBtns/ManifestActionBtns.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
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; | ||
signAble?: boolean; | ||
} | ||
|
||
export function ManifestActionBtns({ | ||
manifestStatus, | ||
readOnly, | ||
signAble, | ||
}: ManifestActionBtnsProps) { | ||
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'; | ||
name = 'signFAB'; | ||
type = 'button'; | ||
} else if (readOnly) { | ||
variant = 'primary'; | ||
icon = faPenToSquare; | ||
text = 'Edit'; | ||
name = 'editFAB'; | ||
type = 'button'; | ||
} else { | ||
return <></>; | ||
} | ||
|
||
return ( | ||
<FloatingActionBtn name={name} variant={variant} type={type} position="bottom-left" extended> | ||
<span className="h5 me-3">{text}</span> | ||
<span className="h5"> | ||
<FontAwesomeIcon icon={icon} /> | ||
</span> | ||
</FloatingActionBtn> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
93 changes: 93 additions & 0 deletions
93
client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import React from 'react'; | ||
import '@testing-library/jest-dom'; | ||
import { HandlerSearchForm } from './HandlerSearchForm'; | ||
import { cleanup, renderWithProviders, screen } from 'test-utils'; | ||
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; | ||
import { createMockRcrainfoSite } from 'test-utils/fixtures'; | ||
import { setupServer } from 'msw/node'; | ||
import { http, HttpResponse } from 'msw'; | ||
import { API_BASE_URL } from 'test-utils/mock/handlers'; | ||
import userEvent from '@testing-library/user-event'; | ||
|
||
const mockRcraSite1Id = 'VATEST111111111'; | ||
const mockRcraSite2Id = 'VATEST222222222'; | ||
const mockRcrainfoSite1Id = 'VATEST333333333'; | ||
const mockRcrainfoSite2Id = 'VATEST444444444'; | ||
|
||
const mockRcrainfoSite1 = createMockRcrainfoSite({ epaSiteId: mockRcrainfoSite1Id }); | ||
const mockRcrainfoSite2 = createMockRcrainfoSite({ epaSiteId: mockRcrainfoSite2Id }); | ||
const mockRcraSite1 = createMockRcrainfoSite({ epaSiteId: mockRcraSite1Id }); | ||
const mockRcraSite2 = createMockRcrainfoSite({ epaSiteId: mockRcraSite2Id }); | ||
export const testURL = [ | ||
http.get(`${API_BASE_URL}/api/site/search`, (info) => { | ||
return HttpResponse.json([mockRcraSite1, mockRcraSite2], { status: 200 }); | ||
}), | ||
http.get(`${API_BASE_URL}/api/rcra/handler/search`, (info) => { | ||
return HttpResponse.json([mockRcrainfoSite1, mockRcrainfoSite2], { status: 200 }); | ||
}), | ||
http.post(`${API_BASE_URL}/api/rcra/handler/search`, (info) => { | ||
return HttpResponse.json([mockRcrainfoSite1, mockRcrainfoSite2], { status: 200 }); | ||
}), | ||
]; | ||
|
||
const server = setupServer(...testURL); | ||
afterEach(() => { | ||
cleanup(); | ||
}); | ||
beforeAll(() => server.listen()); | ||
afterAll(() => server.close()); // Disable API mocking after the tests are done. | ||
|
||
describe('HandlerSearchForm', () => { | ||
test('renders with basic information inputs', () => { | ||
renderWithProviders( | ||
<HandlerSearchForm handleClose={() => undefined} handlerType="generator" /> | ||
); | ||
expect(screen.getByText(/EPA ID/i)).toBeInTheDocument(); | ||
}); | ||
test('retrieves rcra sites from haztrak and RCRAInfo', async () => { | ||
renderWithProviders( | ||
<HandlerSearchForm handleClose={() => undefined} handlerType="generator" />, | ||
{ | ||
preloadedState: { | ||
profile: { | ||
user: 'testuser1', | ||
org: { | ||
name: 'my org', | ||
rcrainfoIntegrated: true, | ||
id: '1234', | ||
}, | ||
}, | ||
}, | ||
} | ||
); | ||
const epaId = screen.getByRole('combobox'); | ||
await userEvent.type(epaId, 'VATEST'); | ||
expect(await screen.findByText(new RegExp(mockRcraSite1Id, 'i'))).toBeInTheDocument(); | ||
expect(await screen.findByText(new RegExp(mockRcraSite2Id, 'i'))).toBeInTheDocument(); | ||
expect(await screen.findByText(new RegExp(mockRcrainfoSite1Id, 'i'))).toBeInTheDocument(); | ||
expect(await screen.findByText(new RegExp(mockRcrainfoSite2Id, 'i'))).toBeInTheDocument(); | ||
}); | ||
test('retrieves rcra sites from haztrak if org not rcrainfo integrated', async () => { | ||
renderWithProviders( | ||
<HandlerSearchForm handleClose={() => undefined} handlerType="generator" />, | ||
{ | ||
preloadedState: { | ||
profile: { | ||
user: 'testuser1', | ||
org: { | ||
name: 'my org', | ||
rcrainfoIntegrated: false, | ||
id: '1234', | ||
}, | ||
}, | ||
}, | ||
} | ||
); | ||
const epaId = screen.getByRole('combobox'); | ||
await userEvent.type(epaId, 'VATEST'); | ||
expect(await screen.findByText(new RegExp(mockRcraSite1Id, 'i'))).toBeInTheDocument(); | ||
expect(await screen.findByText(new RegExp(mockRcraSite2Id, 'i'))).toBeInTheDocument(); | ||
expect(screen.queryByText(new RegExp(mockRcrainfoSite1Id, 'i'))).not.toBeInTheDocument(); | ||
expect(screen.queryByText(new RegExp(mockRcrainfoSite2Id, 'i'))).not.toBeInTheDocument(); | ||
}); | ||
}); |
Oops, something went wrong.