From 5c4806e5617651f2cb0d9d7e7b2438650dea1804 Mon Sep 17 00:00:00 2001 From: Stefan Kairinos Date: Fri, 12 Jul 2024 14:19:25 +0100 Subject: [PATCH] fix: portal frontend#8 (#46) * add visibility icon * fix: text field theme * fix checkbox field * fix: repeat field * auto complete field * set return type * date picker field * re-export date picker field * fix section * fix notification * fix remaining page components * allow auto complete * quick save * generate paths * add git lens * custom useParams and useSearchParams * fix exports * merge from main * read only endpoints for all user objects * fix subpath generation * fix tests * add multiple sub paths * fix path generation * use location hook * fix use location hook * null * merge from main * link icon button --- src/api/endpoints.ts | 148 +++++++++++++--- src/api/index.ts | 12 +- src/components/router/LinkIconButton.tsx | 12 ++ src/components/router/index.tsx | 3 + src/hooks/index.ts | 3 +- src/hooks/router.ts | 6 + src/utils/router.test.ts | 214 +++++++++++++---------- src/utils/router.ts | 17 +- 8 files changed, 293 insertions(+), 122 deletions(-) create mode 100644 src/components/router/LinkIconButton.tsx diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 9c56f250..51512e22 100644 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -1,34 +1,134 @@ -import type { - EndpointBuilder, - MutationDefinition, -} from "@reduxjs/toolkit/query" +import { type EndpointBuilder as _EndpointBuilder } from "@reduxjs/toolkit/query/react" -import { type FetchBaseQuery } from "./baseQuery" +import { + buildUrl, + tagData, + type ListArg, + type ListResult, + type RetrieveArg, + type RetrieveResult, +} from "../utils/api" +import type { AuthFactor, Class, School, User } from "./models" import { type TagTypes } from "./tagTypes" +import urls from "./urls" -export type LogoutQuery = null -export type LogoutResult = null - -export default function endpoints( - build: EndpointBuilder, -): { - logout: MutationDefinition< - LogoutQuery, - FetchBaseQuery, - TagTypes, - LogoutResult, - ReducerPath - > -} { - const _build = build as EndpointBuilder +type EndpointBuilder = _EndpointBuilder + +export function getReadUserEndpoints(build: EndpointBuilder) { + const tagType: TagTypes = "User" + + return { + retrieveUser: build.query< + RetrieveResult< + User, + | "first_name" + | "last_name" + | "email" + | "is_active" + | "date_joined" + | "requesting_to_join_class" + | "student" + | "teacher" + >, + RetrieveArg + >({ + query: id => ({ + url: buildUrl(urls.user.detail, { url: { id } }), + method: "GET", + }), + providesTags: tagData(tagType), + }), + listUsers: build.query< + ListResult< + User, + | "first_name" + | "last_name" + | "email" + | "is_active" + | "date_joined" + | "requesting_to_join_class" + | "student" + | "teacher" + >, + ListArg<{ students_in_class: string }> + >({ + query: search => ({ + url: buildUrl(urls.user.list, { search }), + method: "GET", + }), + providesTags: tagData(tagType), + }), + } +} + +export function getReadSchoolEndpoints(build: EndpointBuilder) { + const tagType: TagTypes = "School" + + return { + retrieveSchool: build.query< + RetrieveResult, + RetrieveArg + >({ + query: id => ({ + url: buildUrl(urls.school.detail, { url: { id } }), + method: "GET", + }), + providesTags: tagData(tagType), + }), + } +} + +export function getReadClassEndpoints(build: EndpointBuilder) { + const tagType: TagTypes = "Class" + + return { + retrieveClass: build.query< + RetrieveResult< + Class, + | "name" + | "read_classmates_data" + | "receive_requests_until" + | "school" + | "teacher" + >, + RetrieveArg + >({ + query: id => ({ + url: buildUrl(urls.class.detail, { url: { id } }), + method: "GET", + }), + providesTags: tagData(tagType), + }), + listClasses: build.query< + ListResult< + Class, + | "name" + | "read_classmates_data" + | "receive_requests_until" + | "school" + | "teacher" + >, + ListArg + >({ + query: search => ({ + url: buildUrl(urls.class.list, { search }), + method: "GET", + }), + providesTags: tagData(tagType), + }), + } +} + +export function getReadAuthFactorEndpoints(build: EndpointBuilder) { + const tagType: TagTypes = "AuthFactor" return { - // TODO: https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#implementing-a-queryfn - logout: _build.mutation({ - query: () => ({ - url: "session/logout/", + listAuthFactors: build.query, ListArg>({ + query: search => ({ + url: buildUrl(urls.authFactor.list, { search }), method: "GET", }), + providesTags: tagData(tagType), }), } } diff --git a/src/api/index.ts b/src/api/index.ts index 4b9faa3e..b6bcdb65 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,5 +1,10 @@ import baseQuery from "./baseQuery" -import endpoints from "./endpoints" +import { + getReadAuthFactorEndpoints, + getReadClassEndpoints, + getReadSchoolEndpoints, + getReadUserEndpoints, +} from "./endpoints" import type { AuthFactor, Class, @@ -14,7 +19,10 @@ import urls from "./urls" export { baseQuery, - endpoints, + getReadAuthFactorEndpoints, + getReadClassEndpoints, + getReadSchoolEndpoints, + getReadUserEndpoints, tagTypes, urls, type AuthFactor, diff --git a/src/components/router/LinkIconButton.tsx b/src/components/router/LinkIconButton.tsx new file mode 100644 index 00000000..ed09390a --- /dev/null +++ b/src/components/router/LinkIconButton.tsx @@ -0,0 +1,12 @@ +import { IconButton, type IconButtonProps } from "@mui/material" +import type { FC } from "react" +import { Link, type LinkProps } from "react-router-dom" + +export type LinkIconButtonProps = Omit & LinkProps + +// https://mui.com/material-ui/integrations/routing/#button +const LinkIconButton: FC = props => { + return +} + +export default LinkIconButton diff --git a/src/components/router/index.tsx b/src/components/router/index.tsx index ada155ca..b118360f 100644 --- a/src/components/router/index.tsx +++ b/src/components/router/index.tsx @@ -1,14 +1,17 @@ import Link, { type LinkProps } from "./Link" import LinkButton, { type LinkButtonProps } from "./LinkButton" +import LinkIconButton, { type LinkIconButtonProps } from "./LinkIconButton" import LinkListItem, { type LinkListItemProps } from "./LinkListItem" import LinkTab, { type LinkTabProps } from "./LinkTab" export { Link, LinkButton, + LinkIconButton, LinkListItem, LinkTab, type LinkButtonProps, + type LinkIconButtonProps, type LinkListItemProps, type LinkProps, type LinkTabProps, diff --git a/src/hooks/index.ts b/src/hooks/index.ts index c54e1f3e..11ca21a5 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -7,12 +7,13 @@ import { type UseSessionOptions, } from "./auth" import { useCountdown, useEventListener, useExternalScript } from "./general" -import { useNavigate, useParams, useSearchParams } from "./router" +import { useLocation, useNavigate, useParams, useSearchParams } from "./router" export { useCountdown, useEventListener, useExternalScript, + useLocation, useNavigate, useParams, useSearchParams, diff --git a/src/hooks/router.ts b/src/hooks/router.ts index cb83f6b3..4d79767d 100644 --- a/src/hooks/router.ts +++ b/src/hooks/router.ts @@ -1,7 +1,9 @@ import { + useLocation as _useLocation, useNavigate as _useNavigate, useParams as _useParams, useSearchParams as _useSearchParams, + type Location, type NavigateOptions, type To as NavigateTo, type Params, @@ -37,6 +39,10 @@ export function useNavigate(): < } } +export function useLocation() { + return _useLocation() as Location> +} + // ----------------------------------------------------------------------------- // Use Search Params // ----------------------------------------------------------------------------- diff --git a/src/utils/router.test.ts b/src/utils/router.test.ts index a7b67007..b5567538 100644 --- a/src/utils/router.test.ts +++ b/src/utils/router.test.ts @@ -1,91 +1,119 @@ -import { path as _ } from "./router" +import { path as p, type Parameters, type Path } from "./router" -test("no nested paths", () => { - const paths = _("") +const m = >( + _: string, + __: string | Parameters, + subMatches: SubMatches = {} as SubMatches, +) => ({ _, __, ...subMatches }) - expect(paths._).toBe("/") - expect(paths.__).toBe("") +function testPaths({ + name, + paths, + match, +}: { + name: string + paths: Path + match: Path +}) { + test(name, () => { + expect(paths).toMatchObject(match) + }) +} + +testPaths({ + name: "no nested paths", + paths: p(""), + match: m("/", ""), }) -test("nested paths", () => { - const paths = _("", { - a: _("/a", { - b: _("/b"), +testPaths({ + name: "nested paths", + paths: p("", { + a: p("/a", { + b: p("/b"), }), - }) - - expect(paths._).toBe("/") - expect(paths.__).toBe("") - expect(paths.a._).toBe("/a") - expect(paths.a.__).toBe("/a") - expect(paths.a.b._).toBe("/a/b") - expect(paths.a.b.__).toBe("/b") + }), + match: m("/", "", { + a: m("/a", "/a"), + }), }) -test("one param", () => { - const paths = _("", { - person: _("/person", { - name: _("/:name", { - sam: _({ name: "samantha" }), +testPaths({ + name: "one param", + paths: p("", { + person: p("/person", { + name: p("/:name", { + sam: p({ name: "samantha" }), }), }), - }) - - expect(paths._).toBe("/") - expect(paths.__).toBe("") - expect(paths.person._).toBe("/person") - expect(paths.person.__).toBe("/person") - expect(paths.person.name._).toBe("/person/:name") - expect(paths.person.name.__).toBe("/:name") - expect(paths.person.name.sam._).toBe("/person/samantha") - expect(paths.person.name.sam.__).toMatchObject({ name: "samantha" }) + }), + match: m("/", "", { + person: m("/person", "/person", { + name: m("/person/:name", "/:name", { + sam: m("/person/samantha", { name: "samantha" }), + }), + }), + }), }) -test("multiple params", () => { - const paths = _("", { - hero: _("/hero", { - firstAndLastName: _("/:firstName/:lastName", { - spiderMan: _( +testPaths({ + name: "multiple params", + paths: p("", { + hero: p("/hero", { + firstAndLastName: p("/:firstName/:lastName", { + spiderMan: p( { firstName: "peter", lastName: "parker" }, { - mainVillain: _("/green-goblin"), + mainVillain: p("/green-goblin"), }, ), }), }), - }) - - expect(paths._).toBe("/") - expect(paths.__).toBe("") - expect(paths.hero._).toBe("/hero") - expect(paths.hero.__).toBe("/hero") - expect(paths.hero.firstAndLastName._).toBe("/hero/:firstName/:lastName") - expect(paths.hero.firstAndLastName.__).toBe("/:firstName/:lastName") - expect(paths.hero.firstAndLastName.spiderMan._).toBe("/hero/peter/parker") - expect(paths.hero.firstAndLastName.spiderMan.__).toMatchObject({ - firstName: "peter", - lastName: "parker", - }) - expect(paths.hero.firstAndLastName.spiderMan.mainVillain._).toBe( - "/hero/peter/parker/green-goblin", - ) - expect(paths.hero.firstAndLastName.spiderMan.mainVillain.__).toBe( - "/green-goblin", - ) + }), + match: m("/", "", { + hero: m("/hero", "/hero", { + firstAndLastName: m( + "/hero/:firstName/:lastName", + "/:firstName/:lastName", + { + spiderMan: m( + "/hero/peter/parker", + { firstName: "peter", lastName: "parker" }, + { + mainVillain: m( + "/hero/peter/parker/green-goblin", + "/green-goblin", + ), + }, + ), + }, + ), + }), + }), }) -test("nested params", () => { - const paths = _("", { - hero: _("/hero", { - firstName: _("/:firstName", { - batMan: _( +testPaths({ + name: "nested params", + paths: p("", { + hero: p("/hero", { + firstName: p("/:firstName", { + spiderMan: p({ firstName: "peter" }), + lastName: p("/:lastName?", { + superMan: p( + { firstName: "clark" }, + { + superMan: p({ lastName: "kent" }), + }, + ), + }), + batMan: p( { firstName: "bruce" }, { - lastName: _("/:lastName", { - batMan: _( + lastName: p("/:lastName", { + batMan: p( { lastName: "wayne" }, { - mainVillain: _("/joker"), + mainVillain: p("/joker"), }, ), }), @@ -93,28 +121,36 @@ test("nested params", () => { ), }), }), - }) - - expect(paths._).toBe("/") - expect(paths.__).toBe("") - expect(paths.hero._).toBe("/hero") - expect(paths.hero.__).toBe("/hero") - expect(paths.hero.firstName._).toBe("/hero/:firstName") - expect(paths.hero.firstName.__).toBe("/:firstName") - expect(paths.hero.firstName.batMan._).toBe("/hero/bruce") - expect(paths.hero.firstName.batMan.__).toMatchObject({ firstName: "bruce" }) - expect(paths.hero.firstName.batMan.lastName._).toBe("/hero/bruce/:lastName") - expect(paths.hero.firstName.batMan.lastName.__).toBe("/:lastName") - expect(paths.hero.firstName.batMan.lastName.batMan._).toBe( - "/hero/bruce/wayne", - ) - expect(paths.hero.firstName.batMan.lastName.batMan.__).toMatchObject({ - lastName: "wayne", - }) - expect(paths.hero.firstName.batMan.lastName.batMan.mainVillain._).toBe( - "/hero/bruce/wayne/joker", - ) - expect(paths.hero.firstName.batMan.lastName.batMan.mainVillain.__).toBe( - "/joker", - ) + }), + match: m("/", "", { + hero: m("/hero", "/hero", { + firstName: m("/hero/:firstName", "/:firstName", { + spiderMan: m("/hero/peter", { firstName: "peter" }), + lastName: m("/hero/:firstName/:lastName?", "/:lastName?", { + superMan: m( + "/hero/clark", + { firstName: "clark" }, + { + superMan: m("/hero/clark/kent", { lastName: "kent" }), + }, + ), + }), + batMan: m( + "/hero/bruce", + { firstName: "bruce" }, + { + lastName: m("/hero/bruce/:lastName", "/:lastName", { + batMan: m( + "/hero/bruce/wayne", + { lastName: "wayne" }, + { + mainVillain: m("/hero/bruce/wayne/joker", "/joker"), + }, + ), + }), + }, + ), + }), + }), + }), }) diff --git a/src/utils/router.ts b/src/utils/router.ts index e40f3cfe..df04acbd 100644 --- a/src/utils/router.ts +++ b/src/utils/router.ts @@ -16,19 +16,24 @@ export function path>( _: string | Parameters, subpaths: Subpaths = {} as Subpaths, ): Path & Subpaths { - function updatePath(path: Path, root: boolean): void { - if (typeof path.__ === "object" && typeof _ === "string") { - _ = generatePath(_, path.__) + function updatePath(path: Path, root: boolean, params?: Parameters) { + if (typeof path.__ === "object") { + params = params ? { ...params, ...path.__ } : path.__ } + const _path = typeof _ === "string" && params ? generatePath(_, params) : _ + Object.entries(path).forEach(([key, subpath]) => { if (key !== "__") { + subpath = subpath as string | Path if (typeof subpath === "string") { - if (typeof _ === "string" && (!root || key !== "_")) { - path[key] = _ + subpath + if (typeof _path === "string" && (!root || key !== "_")) { + let __path = _path + subpath + if (__path.endsWith("/")) __path = __path.slice(0, -1) + path[key] = __path } } else { - updatePath(subpath as Path, false) + updatePath(subpath, false, params) } } })