-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add user types * fix type * return result and arg types * Fields Param * add icon * fix list filters * fix: border * teachers in school filter * fix autocomplete field and styling * fix: support multiple search params * exclude id * use full list hook * use pagination hook * export use pagination options * fix import * autocomplete field * name filter * set page to 0 if limit changes * export pagination types * add TablePagination component * support search and error reporting * add country and uk county fields * make query optional * add labels and placeholders * fix: update arg type * update with body * only teachers * getParam helper * merge from main * list result
- Loading branch information
Showing
18 changed files
with
601 additions
and
37 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
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,124 @@ | ||
import { SyncProblem as SyncProblemIcon } from "@mui/icons-material" | ||
import { | ||
CircularProgress, | ||
TablePagination as MuiTablePagination, | ||
type TablePaginationProps as MuiTablePaginationProps, | ||
Stack, | ||
type StackProps, | ||
type TablePaginationBaseProps, | ||
Typography, | ||
} from "@mui/material" | ||
import type { TypedUseLazyQuery } from "@reduxjs/toolkit/query/react" | ||
import { | ||
type ElementType, | ||
type JSXElementConstructor, | ||
type ReactNode, | ||
useEffect, | ||
} from "react" | ||
|
||
import { type Pagination, usePagination } from "../hooks/api" | ||
import type { ListArg, ListResult } from "../utils/api" | ||
|
||
export type TablePaginationProps< | ||
QueryArg extends ListArg, | ||
ResultType extends ListResult<any>, | ||
RootComponent extends | ||
ElementType = JSXElementConstructor<TablePaginationBaseProps>, | ||
AdditionalProps = {}, | ||
> = Omit< | ||
MuiTablePaginationProps<RootComponent, AdditionalProps>, | ||
| "component" | ||
| "count" | ||
| "rowsPerPage" | ||
| "onRowsPerPageChange" | ||
| "page" | ||
| "onPageChange" | ||
| "rowsPerPageOptions" | ||
> & { | ||
children: ( | ||
data: ResultType["data"], | ||
pagination: Pagination & { count?: number; maxLimit?: number }, | ||
) => ReactNode | ||
useLazyListQuery: TypedUseLazyQuery<ResultType, QueryArg, any> | ||
filters?: Omit<QueryArg, "limit" | "offset"> | ||
rowsPerPageOptions?: number[] | ||
stackProps?: StackProps | ||
page?: number | ||
rowsPerPage?: number | ||
} | ||
|
||
const TablePagination = < | ||
QueryArg extends ListArg, | ||
ResultType extends ListResult<any>, | ||
RootComponent extends | ||
ElementType = JSXElementConstructor<TablePaginationBaseProps>, | ||
AdditionalProps = {}, | ||
>({ | ||
children, | ||
useLazyListQuery, | ||
filters, | ||
page: initialPage = 0, | ||
rowsPerPage: initialLimit = 50, | ||
rowsPerPageOptions = [50, 100, 150], | ||
stackProps, | ||
...tablePaginationProps | ||
}: TablePaginationProps< | ||
QueryArg, | ||
ResultType, | ||
RootComponent, | ||
AdditionalProps | ||
>): JSX.Element => { | ||
const [trigger, { data: result, isLoading, error }] = useLazyListQuery() | ||
const [{ limit, page, offset }, setPagination] = usePagination({ | ||
page: initialPage, | ||
limit: initialLimit, | ||
}) | ||
|
||
useEffect(() => { | ||
trigger({ limit, offset, ...filters } as QueryArg) | ||
}, [trigger, limit, offset, filters]) | ||
|
||
useEffect(() => { | ||
console.error(error) | ||
}, [error]) | ||
|
||
const { data, count, max_limit } = result || {} | ||
|
||
if (max_limit) { | ||
rowsPerPageOptions = rowsPerPageOptions.filter( | ||
option => option <= max_limit, | ||
) | ||
} | ||
|
||
return ( | ||
<Stack {...stackProps}> | ||
{isLoading ? ( | ||
<CircularProgress /> | ||
) : error || !data ? ( | ||
<> | ||
<SyncProblemIcon color="error" /> | ||
<Typography color="error.main">Failed to load data</Typography> | ||
</> | ||
) : ( | ||
children(data, { limit, page, offset, count, maxLimit: max_limit }) | ||
)} | ||
<MuiTablePagination | ||
component="div" | ||
count={count ?? 0} | ||
rowsPerPage={limit} | ||
onRowsPerPageChange={event => { | ||
setPagination({ limit: parseInt(event.target.value), page: 0 }) | ||
}} | ||
page={page} | ||
onPageChange={(_, page) => { | ||
setPagination(({ limit }) => ({ limit, page })) | ||
}} | ||
// ascending order | ||
rowsPerPageOptions={rowsPerPageOptions.sort((a, b) => a - b)} | ||
{...tablePaginationProps} | ||
/> | ||
</Stack> | ||
) | ||
} | ||
|
||
export default TablePagination |
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,181 @@ | ||
import { SyncProblem as SyncProblemIcon } from "@mui/icons-material" | ||
import { | ||
Button, | ||
CircularProgress, | ||
Stack, | ||
Typography, | ||
type ChipTypeMap, | ||
} from "@mui/material" | ||
import type { TypedUseLazyQuery } from "@reduxjs/toolkit/query/react" | ||
import { | ||
Children, | ||
forwardRef, | ||
useEffect, | ||
useState, | ||
type ElementType, | ||
} from "react" | ||
|
||
import { | ||
AutocompleteField, | ||
type AutocompleteFieldProps, | ||
} from "../../components/form" | ||
import { usePagination } from "../../hooks/api" | ||
import type { ListArg, ListResult, TagId } from "../../utils/api" | ||
|
||
export interface ApiAutocompleteFieldProps< | ||
SearchKey extends keyof Omit<QueryArg, "limit" | "offset">, | ||
// api type args | ||
QueryArg extends ListArg, | ||
ResultType extends ListResult<any>, | ||
// autocomplete type args | ||
Multiple extends boolean | undefined = false, | ||
DisableClearable extends boolean | undefined = false, | ||
FreeSolo extends boolean | undefined = false, | ||
ChipComponent extends ElementType = ChipTypeMap["defaultComponent"], | ||
> extends Omit< | ||
AutocompleteFieldProps< | ||
TagId, | ||
Multiple, | ||
DisableClearable, | ||
FreeSolo, | ||
ChipComponent | ||
>, | ||
| "options" | ||
| "ListboxComponent" | ||
| "filterOptions" | ||
| "getOptionLabel" | ||
| "getOptionKey" | ||
| "onInputChange" | ||
> { | ||
useLazyListQuery: TypedUseLazyQuery<ResultType, QueryArg, any> | ||
filterOptions?: Omit<QueryArg, "limit" | "offset" | SearchKey> | ||
getOptionLabel: (result: ResultType["data"][number]) => string | ||
getOptionKey?: (result: ResultType["data"][number]) => TagId | ||
searchKey: SearchKey | ||
} | ||
|
||
const ApiAutocompleteField = < | ||
SearchKey extends keyof Omit<QueryArg, "limit" | "offset">, | ||
// api type args | ||
QueryArg extends ListArg, | ||
ResultType extends ListResult<any>, | ||
// autocomplete type args | ||
Multiple extends boolean | undefined = false, | ||
DisableClearable extends boolean | undefined = false, | ||
FreeSolo extends boolean | undefined = false, | ||
ChipComponent extends ElementType = ChipTypeMap["defaultComponent"], | ||
>({ | ||
useLazyListQuery, | ||
filterOptions, | ||
getOptionLabel, | ||
getOptionKey = result => result.id, | ||
searchKey, | ||
...otherAutocompleteFieldProps | ||
}: ApiAutocompleteFieldProps< | ||
SearchKey, | ||
// api type args | ||
QueryArg, | ||
ResultType, | ||
// autocomplete type args | ||
Multiple, | ||
DisableClearable, | ||
FreeSolo, | ||
ChipComponent | ||
>): JSX.Element => { | ||
const [search, setSearch] = useState("") | ||
const [trigger, { isLoading, isError }] = useLazyListQuery() | ||
const [{ limit, offset }, setPagination] = usePagination() | ||
const [{ options, hasMore }, setState] = useState<{ | ||
options: Record<TagId, ResultType["data"][number]> | ||
hasMore: boolean | ||
}>({ options: {}, hasMore: true }) | ||
|
||
// Call api | ||
useEffect(() => { | ||
const arg = { limit, offset, ...filterOptions } as QueryArg | ||
// @ts-expect-error | ||
if (search) arg[searchKey] = search | ||
|
||
trigger(arg) | ||
.unwrap() | ||
.then(({ data, offset, limit, count }) => { | ||
setState(({ options: previousOptions }) => { | ||
const options = { ...previousOptions } | ||
data.forEach(result => { | ||
options[getOptionKey(result)] = result | ||
}) | ||
return { options, hasMore: offset + limit < count } | ||
}) | ||
}) | ||
.catch(error => { | ||
if (error) console.error(error) | ||
// TODO: gracefully handle error | ||
}) | ||
}, [trigger, limit, offset, filterOptions, getOptionKey, searchKey, search]) | ||
|
||
// Get options keys | ||
let optionKeys: TagId[] = Object.keys(options) | ||
if (!optionKeys.length) return <></> | ||
if (typeof getOptionKey(Object.values(options)[0]) === "number") { | ||
optionKeys = optionKeys.map(Number) | ||
} | ||
|
||
function loadNextPage() { | ||
setPagination(({ page, limit }) => ({ page: page + 1, limit })) | ||
} | ||
|
||
return ( | ||
<AutocompleteField | ||
options={optionKeys} | ||
getOptionLabel={id => getOptionLabel(options[id])} | ||
onInputChange={(_, value, reason) => { | ||
setSearch(reason === "input" ? value : "") | ||
}} | ||
ListboxComponent={forwardRef(({ children, ...props }, ref) => { | ||
const listItems = Children.toArray(children) | ||
if (isLoading) listItems.push(<CircularProgress key="is-loading" />) | ||
else { | ||
if (isError) { | ||
listItems.push( | ||
<Stack direction="row" key="is-error"> | ||
<SyncProblemIcon color="error" /> | ||
<Typography color="error.main">Failed to load data</Typography> | ||
</Stack>, | ||
) | ||
} | ||
if (hasMore) { | ||
listItems.push( | ||
<Button key="load-more" onClick={loadNextPage}> | ||
Load more | ||
</Button>, | ||
) | ||
} | ||
} | ||
|
||
return ( | ||
<ul | ||
{...props} | ||
// @ts-expect-error | ||
ref={ref} | ||
onScroll={event => { | ||
// If not already loading and scrolled to bottom | ||
if ( | ||
!isLoading && | ||
event.currentTarget.clientHeight + | ||
event.currentTarget.scrollTop >= | ||
event.currentTarget.scrollHeight | ||
) { | ||
loadNextPage() | ||
} | ||
}} | ||
> | ||
{listItems} | ||
</ul> | ||
) | ||
})} | ||
{...otherAutocompleteFieldProps} | ||
/> | ||
) | ||
} | ||
|
||
export default ApiAutocompleteField |
Oops, something went wrong.