Skip to content

Commit

Permalink
feat: Portal frontend#19 (#51)
Browse files Browse the repository at this point in the history
* 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
SKairinos authored Aug 7, 2024
1 parent c7b9913 commit f371fde
Show file tree
Hide file tree
Showing 18 changed files with 601 additions and 37 deletions.
4 changes: 2 additions & 2 deletions src/api/endpoints/klass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type RetrieveArg,
type RetrieveResult,
} from "../../utils/api"
import type { Class } from "../models"
import type { Class, Teacher } from "../models"
import { type TagTypes } from "../tagTypes"
import urls from "../urls"

Expand All @@ -32,7 +32,7 @@ export type ListClassesResult = ListResult<
| "school"
| "teacher"
>
export type ListClassesArg = ListArg
export type ListClassesArg = ListArg<{ teacher: Teacher["id"] }>

export default function getReadClassEndpoints(
build: EndpointBuilder<any, any, any>,
Expand Down
9 changes: 7 additions & 2 deletions src/api/endpoints/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type RetrieveArg,
type RetrieveResult,
} from "../../utils/api"
import type { User } from "../models"
import type { Class, User } from "../models"
import { type TagTypes } from "../tagTypes"
import urls from "../urls"

Expand Down Expand Up @@ -38,7 +38,12 @@ export type ListUsersResult = ListResult<
| "student"
| "teacher"
>
export type ListUsersArg = ListArg<{ students_in_class: string }>
export type ListUsersArg = ListArg<{
students_in_class: Class["id"]
only_teachers: boolean
_id: User["id"] | User["id"][]
name: string
}>

export default function getReadUserEndpoints(
build: EndpointBuilder<any, any, any>,
Expand Down
124 changes: 124 additions & 0 deletions src/components/TablePagination.tsx
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
181 changes: 181 additions & 0 deletions src/components/form/ApiAutocompleteField.tsx
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
Loading

0 comments on commit f371fde

Please sign in to comment.