Skip to content

Commit

Permalink
Component-alize the MtnTable component, including a MtnStatusField fo…
Browse files Browse the repository at this point in the history
…r filtering by status and a new MtnSearchField component.

The new filter fields stores and reads the state in a URL search parameter.

quick aria label fixes for MtnList
  • Loading branch information
dpgraham4401 committed Dec 6, 2023
1 parent 75d90d7 commit b0ec7b6
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 55 deletions.
45 changes: 45 additions & 0 deletions client/src/components/Mtn/MtnSearchField/MtnSearchField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React, { ChangeEventHandler, useEffect } from 'react';
import { Form } from 'react-bootstrap';
import { useSearchParams } from 'react-router-dom';

interface MtnSearchFieldProps {
value: string;
onChange: ChangeEventHandler;
}

export function MtnSearchField({ value, onChange }: MtnSearchFieldProps) {
const [searchParams, setSearchParams] = useSearchParams();

const setMtnSearchParam = (value: string) => {
if (value) {
searchParams.set('mtn', value);
} else {
searchParams.delete('mtn');
}
setSearchParams(searchParams);
};

const onBlur = () => {
setMtnSearchParam(value);
};

const onKeyReturn = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
setMtnSearchParam(value);
}
};

return (
<>
<Form.Control
id={'mtnGlobalSearch'}
value={value ?? ''}
onChange={onChange}
onBlur={onBlur}
onKeyUp={onKeyReturn}
placeholder="search..."
aria-label="search"
/>
</>
);
}
70 changes: 70 additions & 0 deletions client/src/components/Mtn/MtnStatusField/MtnStatusField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import Select from 'react-select';

export interface StatusOption {
value: string;
label: string;
}

const statusOptions: readonly StatusOption[] = [
{ value: 'Scheduled', label: 'Scheduled' },
{ value: 'InTransit', label: 'In Transit' },
{ value: 'ReadyForSignature', label: 'Ready to Sign' },
{ value: 'Corrected', label: 'Corrected' },
{ value: 'Signed', label: 'Signed' },
{ value: 'NotAssigned', label: 'Draft' },
{ value: 'UnderCorrection', label: 'Under Correction' },
];

interface MtnStatusFieldProps {
onChange: (newValue: StatusOption | null) => void;
}

const parseSearchParam = (searchParam: string | null): StatusOption | null => {
if (!searchParam) return null;
const option = statusOptions.find(
(option) => option.value.toLowerCase() === searchParam.toLowerCase()
);
return option || null;
};

export function MtnStatusField({ onChange }: MtnStatusFieldProps) {
const [searchParams, setSearchParams] = useSearchParams();
const statusParam = searchParams.get('status');
const [searchValue, setSearchValue] = useState<StatusOption | null>(
parseSearchParam(statusParam)
);

const onSelection = (newValue: StatusOption | null) => {
setSearchValue(newValue);
};

useEffect(() => {
onChange(searchValue);
if (searchValue) {
searchParams.set('status', searchValue?.value.toLowerCase());
} else {
searchParams.delete('status');
}
setSearchParams(searchParams);
}, [searchValue]);

return (
<div>
<Select
name="statusFilter"
aria-label="status filter"
value={searchValue}
onChange={onSelection}
options={statusOptions}
isClearable={true}
classNames={{
control: () => 'form-select py-0 ms-2 rounded-3',
placeholder: () => 'p-0 m-0 ps-1',
}}
components={{ IndicatorSeparator: () => null, DropdownIndicator: () => null }}
/>
</div>
);
}
2 changes: 1 addition & 1 deletion client/src/components/Mtn/MtnTable.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe('MtnTable', () => {
createMockMtnDetails({ manifestTrackingNumber: oneMtn }),
];
renderWithProviders(<MtnTable manifests={mtnData} />);
const filterInput = screen.getByPlaceholderText<HTMLInputElement>('Filter...');
const filterInput = screen.getByPlaceholderText<HTMLInputElement>('search...');
fireEvent.change(filterInput, { target: { value: '00000' } });
expect(await screen.queryByText(oneMtn)).toBeNull();
expect(await screen.queryByText(zeroMtn)).not.toBeNull();
Expand Down
62 changes: 18 additions & 44 deletions client/src/components/Mtn/MtnTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ import {
useReactTable,
} from '@tanstack/react-table';
import { MtnRowActions } from 'components/Mtn/MtnRowActions';
import { MtnStatusField, StatusOption } from 'components/Mtn/MtnStatusField/MtnStatusField';
import { HtPageBtns, HtPageControls } from 'components/UI';
import React, { useState } from 'react';
import React, { ChangeEvent, useState } from 'react';
import { Button, Col, Form, Table } from 'react-bootstrap';
import Select from 'react-select';
import { z } from 'zod';
import { MtnSearchField } from 'components/Mtn/MtnSearchField/MtnSearchField';
import { useSearchParams } from 'react-router-dom';

const mtnDetailsSchema = z.object({
manifestTrackingNumber: z.string(),
Expand Down Expand Up @@ -51,7 +53,6 @@ interface MtnTableProps {

const columnHelper = createColumnHelper<MtnDetails>();

// This defines our MTN table's columns and their behavior
const columns = [
columnHelper.accessor('manifestTrackingNumber', {
header: 'MTN',
Expand Down Expand Up @@ -104,29 +105,14 @@ const fuzzyFilter: FilterFn<MtnDetails> = (row, columnId, value, addMeta) => {
return itemRank.passed;
};

interface StatusOption {
value: string;
label: string;
}

const statusOptions: readonly StatusOption[] = [
{ value: 'Scheduled', label: 'Scheduled' },
{ value: 'InTransit', label: 'In Transit' },
{ value: 'ReadyForSignature', label: 'Ready to Sign' },
{ value: 'Corrected', label: 'Corrected' },
{ value: 'Signed', label: 'Signed' },
{ value: 'NotAssigned', label: 'Draft' },
{ value: 'UnderCorrection', label: 'Under Correction' },
];

/**
* Returns a card with a table of manifest tracking numbers (MTN) and select details
* @param manifest
*/
export function MtnTable({ manifests }: MtnTableProps) {
const [searchParams, setSearchParams] = useSearchParams();
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [searchValue, setSearchValue] = useState<StatusOption | null>(null);
const [globalFilter, setGlobalFilter] = useState('');
const [searchValue, setSearchValue] = useState(searchParams.get('mtn') ?? '');
const table = useReactTable({
columns,
data: manifests,
Expand All @@ -135,10 +121,10 @@ export function MtnTable({ manifests }: MtnTableProps) {
},
state: {
columnFilters,
globalFilter,
globalFilter: searchValue,
},
onColumnFiltersChange: setColumnFilters,
onGlobalFilterChange: setGlobalFilter,
onGlobalFilterChange: setSearchValue,
globalFilterFn: fuzzyFilter,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
Expand All @@ -152,34 +138,22 @@ export function MtnTable({ manifests }: MtnTableProps) {
debugColumns: false,
});

const onStatusChange = (newValue: StatusOption | null) => {
setColumnFilters([{ id: 'status', value: newValue?.value ?? '' }]);
};

const onSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
setSearchValue(event.target.value);
};

return (
<>
<div className="d-flex flex-row justify-content-end">
<Col xs={3}>
<Form.Control
id={'mtnGlobalSearch'}
value={globalFilter ?? ''}
onChange={(event) => setGlobalFilter(event.target.value)}
placeholder="Filter..."
/>
<MtnSearchField value={searchValue} onChange={onSearchChange} />
</Col>
<Col xs={4} className="mx-2">
<Select
name="statusFilter"
value={searchValue}
onChange={(newValue) => {
setSearchValue(newValue);
setColumnFilters([{ id: 'status', value: newValue?.value ?? '' }]);
}}
options={statusOptions}
isClearable={true}
placeholder="Status"
classNames={{
control: () => 'form-select py-0 ms-2 rounded-3',
placeholder: () => 'p-0 m-0 ps-1',
}}
components={{ IndicatorSeparator: () => null, DropdownIndicator: () => null }}
/>
<MtnStatusField onChange={onStatusChange} />
</Col>
</div>
<Table responsive>
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/UI/HtForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ HtForm.Switch = React.forwardRef<HTMLInputElement, FormCheckProps>(
(props: FormCheckProps, ref: React.Ref<HTMLInputElement>) => {
const { children, dangerouslySetInnerHTML, ...rest } = props;
return (
<Form.Check ref={ref} {...rest}>
type="switch"
<Form.Check ref={ref} {...rest} type="switch">
{children}
</Form.Check>
);
Expand Down
13 changes: 5 additions & 8 deletions client/src/features/ManifestList/ManifestList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,9 @@ export function ManifestList(): ReactElement {
});

return (
<Container className="py-2">
<Row className="d-flex justify-content-start">
<h2>{siteId}</h2>
</Row>
<Row>
<Container fluid className="py-2">
<h1 className="h2 ms-0">{siteId}</h1>
<Row className="px-5">
<Stack direction="horizontal" gap={2} className="d-flex justify-content-end mb-1 px-0 mx-0">
<SyncManifestBtn
siteId={siteId}
Expand All @@ -38,9 +36,8 @@ export function ManifestList(): ReactElement {
<NewManifestBtn siteId={siteId} />
</Stack>
</Row>
<Row>
<HtCard>
<HtCard.Header title={`${siteId || 'My'} Manifests`}></HtCard.Header>
<Row className="px-5">
<HtCard title="Manifests">
<HtCard.Body>
{isLoading ? (
<HtCard.Spinner />
Expand Down

0 comments on commit b0ec7b6

Please sign in to comment.