Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

emanifest backend logic separation and Floating Acion Buttons #663

Merged
merged 12 commits into from
Dec 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getHaztrakProfile,
getHaztrakUser,
getRcraProfile,
selectHaztrakProfile,
selectRcraProfile,
selectUserName,
useAppDispatch,
Expand All @@ -19,7 +20,7 @@ import { HtSpinner } from 'components/UI';

function App(): ReactElement {
const userName = useAppSelector(selectUserName);
const profile = useAppSelector(selectRcraProfile);
const profile = useAppSelector(selectHaztrakProfile);
const dispatch = useAppDispatch();

useEffect(() => {
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/Layout/Root.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -29,7 +29,7 @@ export function Root() {
<Suspense
fallback={
<Container fluid className="d-flex justify-content-center vh-100">
<HtSpinner size="7x" />
<HtSpinner size="6x" className="my-auto" />
</Container>
}
>
Expand Down
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 client/src/components/Manifest/ActionBtns/ManifestActionBtns.tsx
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>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,16 @@ export function AdditionalInfoForm({ readOnly }: AdditionalFormProps) {
})}
</Row>
</Col>
<div>
<div className="d-flex justify-content-end ">
{readOnly ? (
<></>
) : (
<Button onClick={() => append({ description: '', label: '' })}>Add Reference</Button>
<Button
variant="outline-secondary"
onClick={() => append({ description: '', label: '' })}
>
Add Reference
</Button>
)}
</div>
</>
Expand Down
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();
});
});
Loading