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

feat: Portal frontend#19 #51

Merged
merged 30 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
86b3dfe
add user types
SKairinos Jul 22, 2024
fc1b1b7
fix type
SKairinos Jul 22, 2024
07cea0e
return result and arg types
SKairinos Jul 22, 2024
f181f06
Fields Param
SKairinos Jul 23, 2024
2c81c67
add icon
SKairinos Jul 23, 2024
7f73fbf
fix list filters
SKairinos Jul 24, 2024
ecaf956
fix: border
SKairinos Jul 24, 2024
474059e
teachers in school filter
SKairinos Jul 25, 2024
143bce3
fix autocomplete field and styling
SKairinos Jul 25, 2024
ffd967c
fix: support multiple search params
SKairinos Jul 26, 2024
9db127d
exclude id
SKairinos Jul 26, 2024
ef013f1
use full list hook
SKairinos Jul 26, 2024
164653a
use pagination hook
SKairinos Jul 30, 2024
265cb7a
export use pagination options
SKairinos Jul 30, 2024
639578c
fix import
SKairinos Jul 30, 2024
328e2cb
autocomplete field
SKairinos Jul 30, 2024
002d604
name filter
SKairinos Jul 31, 2024
f664f42
set page to 0 if limit changes
SKairinos Jul 31, 2024
ba86be2
export pagination types
SKairinos Jul 31, 2024
c0acc91
add TablePagination component
SKairinos Jul 31, 2024
eeb7429
support search and error reporting
SKairinos Aug 1, 2024
5566626
add country and uk county fields
SKairinos Aug 1, 2024
c6afc12
make query optional
SKairinos Aug 1, 2024
46d80f2
add labels and placeholders
SKairinos Aug 2, 2024
6788669
fix: update arg type
SKairinos Aug 2, 2024
868c24a
update with body
SKairinos Aug 2, 2024
70d7ca9
only teachers
SKairinos Aug 2, 2024
6950e1e
getParam helper
SKairinos Aug 2, 2024
ea1a177
merge from main
SKairinos Aug 2, 2024
e6df357
list result
SKairinos Aug 6, 2024
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
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