From adf1a06b47167f1d10acd5328d4821b47aecd0d2 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Thu, 11 Jul 2024 18:04:14 +0200 Subject: [PATCH] Refactor admin app to prepare v5 migration --- pwa/components/admin/Admin.tsx | 99 +++++++------- pwa/components/admin/AppBar.tsx | 121 ------------------ pwa/components/admin/Menu.tsx | 13 -- .../admin/book/{Form.tsx => BookForm.tsx} | 6 +- pwa/components/admin/book/BookInput.tsx | 117 +++++++++++------ .../book/{Create.tsx => BooksCreate.tsx} | 6 +- pwa/components/admin/book/BooksEdit.tsx | 18 +++ pwa/components/admin/book/BooksList.tsx | 46 +++++++ pwa/components/admin/book/ConditionInput.tsx | 16 ++- pwa/components/admin/book/Edit.tsx | 18 --- pwa/components/admin/book/List.tsx | 39 ------ pwa/components/admin/book/ShowButton.tsx | 29 +++-- pwa/components/admin/book/index.ts | 14 ++ pwa/components/admin/i18nProvider.ts | 16 +++ pwa/components/admin/layout/AppBar.tsx | 23 ++++ .../admin/layout/DocTypeMenuButton.tsx | 58 +++++++++ .../admin/{ => layout}/HydraLogo.tsx | 0 pwa/components/admin/layout/Layout.tsx | 9 ++ pwa/components/admin/layout/Logout.tsx | 43 +++++++ pwa/components/admin/layout/Menu.tsx | 19 +++ .../admin/{ => layout}/OpenApiLogo.tsx | 0 pwa/components/admin/review/BookField.tsx | 24 +++- pwa/components/admin/review/Edit.tsx | 24 ---- pwa/components/admin/review/List.tsx | 55 -------- pwa/components/admin/review/RatingField.tsx | 12 +- pwa/components/admin/review/RatingInput.tsx | 18 ++- pwa/components/admin/review/ReviewsEdit.tsx | 41 ++++++ pwa/components/admin/review/ReviewsList.tsx | 68 ++++++++++ pwa/components/admin/review/ReviewsShow.tsx | 17 +++ pwa/components/admin/review/Show.tsx | 15 --- pwa/components/admin/review/index.ts | 14 ++ 31 files changed, 577 insertions(+), 421 deletions(-) delete mode 100644 pwa/components/admin/AppBar.tsx delete mode 100644 pwa/components/admin/Menu.tsx rename pwa/components/admin/book/{Form.tsx => BookForm.tsx} (50%) rename pwa/components/admin/book/{Create.tsx => BooksCreate.tsx} (56%) create mode 100644 pwa/components/admin/book/BooksEdit.tsx create mode 100644 pwa/components/admin/book/BooksList.tsx delete mode 100644 pwa/components/admin/book/Edit.tsx delete mode 100644 pwa/components/admin/book/List.tsx create mode 100644 pwa/components/admin/book/index.ts create mode 100644 pwa/components/admin/i18nProvider.ts create mode 100644 pwa/components/admin/layout/AppBar.tsx create mode 100644 pwa/components/admin/layout/DocTypeMenuButton.tsx rename pwa/components/admin/{ => layout}/HydraLogo.tsx (100%) create mode 100644 pwa/components/admin/layout/Layout.tsx create mode 100644 pwa/components/admin/layout/Logout.tsx create mode 100644 pwa/components/admin/layout/Menu.tsx rename pwa/components/admin/{ => layout}/OpenApiLogo.tsx (100%) delete mode 100644 pwa/components/admin/review/Edit.tsx delete mode 100644 pwa/components/admin/review/List.tsx create mode 100644 pwa/components/admin/review/ReviewsEdit.tsx create mode 100644 pwa/components/admin/review/ReviewsList.tsx create mode 100644 pwa/components/admin/review/ReviewsShow.tsx delete mode 100644 pwa/components/admin/review/Show.tsx create mode 100644 pwa/components/admin/review/index.ts diff --git a/pwa/components/admin/Admin.tsx b/pwa/components/admin/Admin.tsx index 6178b86bb..adb0f6980 100644 --- a/pwa/components/admin/Admin.tsx +++ b/pwa/components/admin/Admin.tsx @@ -2,40 +2,37 @@ import Head from "next/head"; import { useContext, useRef, useState } from "react"; -import { type DataProvider, Layout, type LayoutProps, localStorageStore, resolveBrowserLocale } from "react-admin"; +import { type DataProvider, localStorageStore } from "react-admin"; import { signIn, useSession } from "next-auth/react"; import SyncLoader from "react-spinners/SyncLoader"; -import polyglotI18nProvider from "ra-i18n-polyglot"; -import englishMessages from "ra-language-english"; -import frenchMessages from "ra-language-french"; -import { fetchHydra, HydraAdmin, hydraDataProvider, OpenApiAdmin, ResourceGuesser } from "@api-platform/admin"; +import { + fetchHydra, + HydraAdmin, + hydraDataProvider, + OpenApiAdmin, + ResourceGuesser, +} from "@api-platform/admin"; import { parseHydraDocumentation } from "@api-platform/api-doc-parser"; import { type Session } from "../../app/auth"; import DocContext from "../../components/admin/DocContext"; import authProvider from "../../components/admin/authProvider"; -import AppBar from "../../components/admin/AppBar"; -import Menu from "../../components/admin/Menu"; +import Layout from "./layout/Layout"; import { ENTRYPOINT } from "../../config/entrypoint"; -import { List as BooksList } from "../../components/admin/book/List"; -import { Create as BooksCreate } from "../../components/admin/book/Create"; -import { Edit as BooksEdit } from "../../components/admin/book/Edit"; -import { List as ReviewsList } from "../../components/admin/review/List"; -import { Show as ReviewsShow } from "../../components/admin/review/Show"; -import { Edit as ReviewsEdit } from "../../components/admin/review/Edit"; -import { type Book } from "../../types/Book"; -import { type Review } from "../../types/Review"; +import bookResourceProps from "./book"; +import reviewResourceProps from "./review"; +import i18nProvider from "./i18nProvider"; const apiDocumentationParser = (session: Session) => async () => { try { return await parseHydraDocumentation(ENTRYPOINT, { - headers: { - Authorization: `Bearer ${session?.accessToken}`, - }, + headers: { + Authorization: `Bearer ${session?.accessToken}`, + }, }); } catch (result) { // @ts-ignore - const {api, response, status} = result; + const { api, response, status } = result; if (status !== 401 || !response) { throw result; } @@ -48,31 +45,26 @@ const apiDocumentationParser = (session: Session) => async () => { } }; -const messages = { - fr: frenchMessages, - en: englishMessages, -}; -const i18nProvider = polyglotI18nProvider( - // @ts-ignore - (locale) => (messages[locale] ? messages[locale] : messages.en), - resolveBrowserLocale(), -); - -const MyLayout = (props: React.JSX.IntrinsicAttributes & LayoutProps) => ; - -const AdminUI = ({ session, children }: { session: Session, children?: React.ReactNode | undefined }) => { +const AdminAdapter = ({ + session, + children, +}: { + session: Session; + children?: React.ReactNode | undefined; +}) => { // @ts-ignore const dataProvider = useRef(); const { docType } = useContext(DocContext); dataProvider.current = hydraDataProvider({ entrypoint: ENTRYPOINT, - httpClient: (url: URL, options = {}) => fetchHydra(url, { - ...options, - headers: { - Authorization: `Bearer ${session?.accessToken}`, - }, - }), + httpClient: (url: URL, options = {}) => + fetchHydra(url, { + ...options, + headers: { + Authorization: `Bearer ${session?.accessToken}`, + }, + }), apiDocumentationParser: apiDocumentationParser(session), }); @@ -84,7 +76,7 @@ const AdminUI = ({ session, children }: { session: Session, children?: React.Rea dataProvider={dataProvider.current} entrypoint={window.origin} i18nProvider={i18nProvider} - layout={MyLayout} + layout={Layout} > {!!children && children} @@ -97,7 +89,7 @@ const AdminUI = ({ session, children }: { session: Session, children?: React.Rea entrypoint={window.origin} docEntrypoint={`${window.origin}/docs.json`} i18nProvider={i18nProvider} - layout={MyLayout} + layout={Layout} > {!!children && children} @@ -105,23 +97,18 @@ const AdminUI = ({ session, children }: { session: Session, children?: React.Rea }; const store = localStorageStore(); + const AdminWithContext = ({ session }: { session: Session }) => { const [docType, setDocType] = useState( - store.getItem("docType", "hydra"), + store.getItem("docType", "hydra") ); return ( - - - `${record.title} - ${record.author}`}/> - record.user.name}/> - + + + + + ); }; @@ -131,18 +118,18 @@ const AdminWithOIDC = () => { const { data: session, status } = useSession(); if (status === "loading") { - return ; + return ; } // @ts-ignore if (!session || session?.error === "RefreshAccessTokenError") { - (async() => await signIn("keycloak"))(); + (async () => await signIn("keycloak"))(); return; } // @ts-ignore - return ; + return ; }; const Admin = () => ( @@ -152,7 +139,7 @@ const Admin = () => ( {/*@ts-ignore*/} - + ); diff --git a/pwa/components/admin/AppBar.tsx b/pwa/components/admin/AppBar.tsx deleted file mode 100644 index ba00f45cf..000000000 --- a/pwa/components/admin/AppBar.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import {ForwardedRef, forwardRef, useContext, useState} from "react"; -import { AppBar, AppBarClasses, LogoutClasses, UserMenu, useTranslate, useStore } from "react-admin"; -import { type AppBarProps } from "react-admin"; -import {Button, ListItemIcon, ListItemText, Menu, MenuItem, Typography} from "@mui/material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import ExitIcon from "@mui/icons-material/PowerSettingsNew"; -import { signOut, useSession } from "next-auth/react"; - -import DocContext from "../../components/admin/DocContext"; -import HydraLogo from "../../components/admin/HydraLogo"; -import OpenApiLogo from "../../components/admin/OpenApiLogo"; -import Logo from "../../components/admin/Logo"; -import {NEXT_PUBLIC_OIDC_SERVER_URL} from "../../config/keycloak"; - -const DocTypeMenuButton = () => { - const [anchorEl, setAnchorEl] = useState(null); - const [, setStoreDocType] = useStore("docType", "hydra"); - const { docType, setDocType } = useContext(DocContext); - - const open = Boolean(anchorEl); - // @ts-ignore - const handleClick = (event) => { - setAnchorEl(event.currentTarget); - }; - const handleClose = () => { - setAnchorEl(null); - }; - const changeDocType = (docType: string) => () => { - setStoreDocType(docType); - setDocType(docType); - handleClose(); - }; - - return ( - <> - - - Hydra - OpenAPI - - - ); -}; - -const Logout = forwardRef((props, ref: ForwardedRef) => { - const { data: session } = useSession(); - const translate = useTranslate(); - - if (!session) { - return; - } - - const handleClick = () => signOut({ - // @ts-ignore - callbackUrl: `${NEXT_PUBLIC_OIDC_SERVER_URL}/protocol/openid-connect/logout?id_token_hint=${session.idToken}&post_logout_redirect_uri=${window.location.origin}`, - }); - - return ( - - - - - - {translate('ra.auth.logout', { _: 'Logout' })} - - - ); -}); -Logout.displayName = "Logout"; - -const CustomAppBar = ({ ...props }: AppBarProps) => { - return ( - - - - } {...props}> - -
- -
- -
- ); -}; - -export default CustomAppBar; diff --git a/pwa/components/admin/Menu.tsx b/pwa/components/admin/Menu.tsx deleted file mode 100644 index a9c7f0276..000000000 --- a/pwa/components/admin/Menu.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Menu as ReactAdminMenu } from "react-admin"; -import MenuBookIcon from "@mui/icons-material/MenuBook"; -import CommentIcon from "@mui/icons-material/Comment"; - -const Menu = () => ( - - {/*@ts-ignore*/} - }/> - {/*@ts-ignore*/} - }/> - -); -export default Menu; diff --git a/pwa/components/admin/book/Form.tsx b/pwa/components/admin/book/BookForm.tsx similarity index 50% rename from pwa/components/admin/book/Form.tsx rename to pwa/components/admin/book/BookForm.tsx index 49b8b9c14..53283a5df 100644 --- a/pwa/components/admin/book/Form.tsx +++ b/pwa/components/admin/book/BookForm.tsx @@ -3,9 +3,9 @@ import { required } from "react-admin"; import { ConditionInput } from "./ConditionInput"; import { BookInput } from "./BookInput"; -export const Form = () => ( +export const BookForm = () => ( <> - - + + ); diff --git a/pwa/components/admin/book/BookInput.tsx b/pwa/components/admin/book/BookInput.tsx index 117415485..1340bd848 100644 --- a/pwa/components/admin/book/BookInput.tsx +++ b/pwa/components/admin/book/BookInput.tsx @@ -14,39 +14,53 @@ interface Result { value: string; } -interface BookInputProps extends TextInputProps { +interface BookInputProps extends Omit { title?: string; author?: string; } -const fetchOpenLibrarySearch = async (query: string, signal?: AbortSignal | undefined): Promise> => { +const fetchOpenLibrarySearch = async ( + query: string, + signal?: AbortSignal | undefined +): Promise> => { try { - const response = await fetch(`https://openlibrary.org/search.json?q=${query.replace(/ - /, ' ')}&limit=10`, { - signal, - method: "GET", - next: { revalidate: 3600 }, - }); + const response = await fetch( + `https://openlibrary.org/search.json?q=${query.replace( + / - /, + " " + )}&limit=10`, + { + signal, + method: "GET", + next: { revalidate: 3600 }, + } + ); const results: Search = await response.json(); return results.docs - .filter((result: SearchDoc) => { - return typeof result.title !== "undefined" - && typeof result.author_name !== "undefined" - && result.author_name.length > 0 - && typeof result.seed !== "undefined" - && result.seed.length > 0 - && result.seed.filter((seed) => seed.match(/^\/books\/OL\d{7}M/)).length > 0; - }) - .map(({ title, author_name, seed }): Result => { - return { - // @ts-ignore - title, - // @ts-ignore - author: author_name[0], - // @ts-ignore - value: `https://openlibrary.org${seed.filter((seed) => seed.match(/^\/books\/OL\d{7}M/))[0]}.json`, - }; - }); + .filter((result: SearchDoc) => { + return ( + typeof result.title !== "undefined" && + typeof result.author_name !== "undefined" && + result.author_name.length > 0 && + typeof result.seed !== "undefined" && + result.seed.length > 0 && + result.seed.filter((seed) => seed.match(/^\/books\/OL\d{7}M/)) + .length > 0 + ); + }) + .map(({ title, author_name, seed }): Result => { + return { + // @ts-ignore + title, + // @ts-ignore + author: author_name[0], + // @ts-ignore + value: `https://openlibrary.org${ + seed?.filter((seed) => seed.match(/^\/books\/OL\d{7}M/))[0] + }.json`, + }; + }); } catch (error) { console.error(error); @@ -55,13 +69,17 @@ const fetchOpenLibrarySearch = async (query: string, signal?: AbortSignal | unde }; export const BookInput = (props: BookInputProps) => { - const { field: { ref, ...field} } = useInput(props); + const { + field: { ref, ...field }, + } = useInput({ ...props, source: "book" }); const title = useWatch({ name: "title" }); const author = useWatch({ name: "author" }); const controller = useRef(); const [searchQuery, setSearchQuery] = useState(""); const [value, setValue] = useState( - !!title && !!author && !!field.value ? { title: title, author: author, value: field.value } : undefined + !!title && !!author && !!field.value + ? { title: title, author: author, value: field.value } + : undefined ); const { isLoading, data, isFetched } = useQuery({ queryKey: ["search", searchQuery], @@ -71,32 +89,53 @@ export const BookInput = (props: BookInputProps) => { } controller.current = new AbortController(); - return await fetchOpenLibrarySearch(searchQuery, controller.current.signal); + return await fetchOpenLibrarySearch( + searchQuery, + controller.current.signal + ); }, enabled: !!searchQuery, }); - const onInputChange = useMemo(() => - debounce((event: SyntheticEvent, value: string) => setSearchQuery(value), 400), - [] + const onInputChange = useMemo( + () => + debounce( + (event: SyntheticEvent, value: string) => setSearchQuery(value), + 400 + ), + [] ); - const onChange = (event: SyntheticEvent, value: Result | null | undefined) => { + const onChange = ( + event: SyntheticEvent, + value: Result | null | undefined + ) => { field.onChange(value?.value); setValue(value); }; - return option?.value === (val?.value || value?.value)} + options={!isFetched ? (!!value ? [value] : []) : data ?? []} + isOptionEqualToValue={(option, val) => + option?.value === (val?.value || value?.value) + } onChange={onChange} onInputChange={onInputChange} - getOptionLabel={(option: Result | undefined) => !!option ? `${option.title} - ${option.author}` : "No options"} + getOptionLabel={(option: Result | undefined) => + !!option ? `${option.title} - ${option.author}` : "No options" + } style={{ width: 500 }} loading={isLoading} renderInput={(params) => ( - + )} - />; + /> + ); }; BookInput.displayName = "BookInput"; -BookInput.defaultProps = { label: "Open Library Book" }; diff --git a/pwa/components/admin/book/Create.tsx b/pwa/components/admin/book/BooksCreate.tsx similarity index 56% rename from pwa/components/admin/book/Create.tsx rename to pwa/components/admin/book/BooksCreate.tsx index b479e2c73..f40bc657a 100644 --- a/pwa/components/admin/book/Create.tsx +++ b/pwa/components/admin/book/BooksCreate.tsx @@ -1,9 +1,9 @@ import { CreateGuesser, type CreateGuesserProps } from "@api-platform/admin"; -import { Form } from "./Form"; +import { BookForm } from "./BookForm"; -export const Create = (props: CreateGuesserProps) => ( +export const BooksCreate = (props: CreateGuesserProps) => ( -
+ ); diff --git a/pwa/components/admin/book/BooksEdit.tsx b/pwa/components/admin/book/BooksEdit.tsx new file mode 100644 index 000000000..21d1d47e9 --- /dev/null +++ b/pwa/components/admin/book/BooksEdit.tsx @@ -0,0 +1,18 @@ +import { EditGuesser, type EditGuesserProps } from "@api-platform/admin"; +import { TopToolbar } from "react-admin"; + +import { BookForm } from "./BookForm"; +import { ShowButton } from "./ShowButton"; + +// @ts-ignore +const Actions = () => ( + + + +); +export const BooksEdit = () => ( + // @ts-ignore + }> + + +); diff --git a/pwa/components/admin/book/BooksList.tsx b/pwa/components/admin/book/BooksList.tsx new file mode 100644 index 000000000..32a7b7ee6 --- /dev/null +++ b/pwa/components/admin/book/BooksList.tsx @@ -0,0 +1,46 @@ +import { FieldGuesser } from "@api-platform/admin"; +import { + TextInput, + Datagrid, + useRecordContext, + List, + EditButton, + WrapperField, +} from "react-admin"; + +import { ShowButton } from "./ShowButton"; +import { RatingField } from "../review/RatingField"; +import { ConditionInput } from "./ConditionInput"; + +const ConditionField = () => { + const record = useRecordContext(); + if (!record || !record.condition) return null; + return ( + + {record.condition.replace(/https:\/\/schema\.org\/(.+)Condition$/, "$1")} + + ); +}; + +const filters = [ + , + , + , +]; + +export const BooksList = () => ( + + + + + + + + + + + + + + +); diff --git a/pwa/components/admin/book/ConditionInput.tsx b/pwa/components/admin/book/ConditionInput.tsx index 90f7c0b53..1f70c0213 100644 --- a/pwa/components/admin/book/ConditionInput.tsx +++ b/pwa/components/admin/book/ConditionInput.tsx @@ -1,10 +1,14 @@ import { SelectInput, SelectInputProps } from "react-admin"; export const ConditionInput = (props: SelectInputProps) => ( - + ); diff --git a/pwa/components/admin/book/Edit.tsx b/pwa/components/admin/book/Edit.tsx deleted file mode 100644 index a2ae50f3b..000000000 --- a/pwa/components/admin/book/Edit.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { EditGuesser, type EditGuesserProps } from "@api-platform/admin"; -import { TopToolbar } from 'react-admin'; - -import { Form } from "./Form"; -import { ShowButton } from "./ShowButton"; - -// @ts-ignore -const Actions = ({ data }) => ( - - - -); -export const Edit = (props: EditGuesserProps) => ( - // @ts-ignore - }> - - -); diff --git a/pwa/components/admin/book/List.tsx b/pwa/components/admin/book/List.tsx deleted file mode 100644 index 6044a2822..000000000 --- a/pwa/components/admin/book/List.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { FieldGuesser, type ListGuesserProps } from "@api-platform/admin"; -import { - TextInput, - Datagrid, - useRecordContext, - type UseRecordContextParams, - List as ReactAdminList, - EditButton, -} from "react-admin"; - -import { ShowButton } from "./ShowButton"; -import { RatingField } from "../review/RatingField"; -import { ConditionInput } from "./ConditionInput"; - -const ConditionField = (props: UseRecordContextParams) => { - const record = useRecordContext(props); - - return !!record && !!record.condition ? {record.condition.replace(/https:\/\/schema\.org\/(.+)Condition$/, "$1")} : null; -}; -ConditionField.defaultProps = { label: "Condition" }; - -const filters = [ - , - , - , -]; - -export const List = (props: ListGuesserProps) => ( - - - - - - - - - - -); diff --git a/pwa/components/admin/book/ShowButton.tsx b/pwa/components/admin/book/ShowButton.tsx index c35a611b4..0d1ac7e54 100644 --- a/pwa/components/admin/book/ShowButton.tsx +++ b/pwa/components/admin/book/ShowButton.tsx @@ -4,17 +4,26 @@ import VisibilityIcon from "@mui/icons-material/Visibility"; import { getItemPath } from "../../../utils/dataAccess"; -export const ShowButton = (props: ShowButtonProps) => { - const record = useRecordContext(props); - +export const ShowButton = () => { + const record = useRecordContext(); return record ? ( - // @ts-ignore - ) : null; }; -ShowButton.defaultProps = { label: "ra.action.show" }; diff --git a/pwa/components/admin/book/index.ts b/pwa/components/admin/book/index.ts new file mode 100644 index 000000000..c89753fd6 --- /dev/null +++ b/pwa/components/admin/book/index.ts @@ -0,0 +1,14 @@ +import { BooksList } from "./BooksList"; +import { BooksCreate } from "./BooksCreate"; +import { BooksEdit } from "./BooksEdit"; +import { type Book } from "../../../types/Book"; + +const bookResourceProps = { + list: BooksList, + create: BooksCreate, + edit: BooksEdit, + hasShow: false, + recordRepresentation: (record: Book) => `${record.title} - ${record.author}`, +}; + +export default bookResourceProps; diff --git a/pwa/components/admin/i18nProvider.ts b/pwa/components/admin/i18nProvider.ts new file mode 100644 index 000000000..d607ce587 --- /dev/null +++ b/pwa/components/admin/i18nProvider.ts @@ -0,0 +1,16 @@ +import { resolveBrowserLocale } from "react-admin"; +import polyglotI18nProvider from "ra-i18n-polyglot"; +import englishMessages from "ra-language-english"; +import frenchMessages from "ra-language-french"; + +const messages = { + fr: frenchMessages, + en: englishMessages, +}; +const i18nProvider = polyglotI18nProvider( + // @ts-ignore + (locale) => (messages[locale] ? messages[locale] : messages.en), + resolveBrowserLocale() +); + +export default i18nProvider; diff --git a/pwa/components/admin/layout/AppBar.tsx b/pwa/components/admin/layout/AppBar.tsx new file mode 100644 index 000000000..4d5d0f1ce --- /dev/null +++ b/pwa/components/admin/layout/AppBar.tsx @@ -0,0 +1,23 @@ +import { AppBar, UserMenu, TitlePortal } from "react-admin"; + +import Logo from "../Logo"; +import Logout from "./Logout"; +import DocTypeMenuButton from "./DocTypeMenuButton"; + +const CustomAppBar = () => ( + + + + } + > + +
+ +
+ +
+); + +export default CustomAppBar; diff --git a/pwa/components/admin/layout/DocTypeMenuButton.tsx b/pwa/components/admin/layout/DocTypeMenuButton.tsx new file mode 100644 index 000000000..990903eed --- /dev/null +++ b/pwa/components/admin/layout/DocTypeMenuButton.tsx @@ -0,0 +1,58 @@ +import { useContext, useState } from "react"; +import { useStore } from "react-admin"; +import { Button, Menu, MenuItem } from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; + +import DocContext from "../DocContext"; +import HydraLogo from "./HydraLogo"; +import OpenApiLogo from "./OpenApiLogo"; + +const DocTypeMenuButton = () => { + const [anchorEl, setAnchorEl] = useState(null); + const [, setStoreDocType] = useStore("docType", "hydra"); + const { docType, setDocType } = useContext(DocContext); + + const open = Boolean(anchorEl); + // @ts-ignore + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const changeDocType = (docType: string) => { + setStoreDocType(docType); + setDocType(docType); + handleClose(); + }; + + return ( + <> + + + changeDocType("hydra")}>Hydra + changeDocType("openapi")}>OpenAPI + + + ); +}; + +export default DocTypeMenuButton; diff --git a/pwa/components/admin/HydraLogo.tsx b/pwa/components/admin/layout/HydraLogo.tsx similarity index 100% rename from pwa/components/admin/HydraLogo.tsx rename to pwa/components/admin/layout/HydraLogo.tsx diff --git a/pwa/components/admin/layout/Layout.tsx b/pwa/components/admin/layout/Layout.tsx new file mode 100644 index 000000000..bd8dc5bf4 --- /dev/null +++ b/pwa/components/admin/layout/Layout.tsx @@ -0,0 +1,9 @@ +import { Layout, type LayoutProps } from "react-admin"; +import AppBar from "./AppBar"; +import Menu from "./Menu"; + +const MyLayout = (props: React.JSX.IntrinsicAttributes & LayoutProps) => ( + +); + +export default MyLayout; diff --git a/pwa/components/admin/layout/Logout.tsx b/pwa/components/admin/layout/Logout.tsx new file mode 100644 index 000000000..35ae1ace1 --- /dev/null +++ b/pwa/components/admin/layout/Logout.tsx @@ -0,0 +1,43 @@ +import { ForwardedRef, forwardRef } from "react"; +import { LogoutClasses, useTranslate } from "react-admin"; + +import { ListItemIcon, ListItemText, MenuItem } from "@mui/material"; +import ExitIcon from "@mui/icons-material/PowerSettingsNew"; +import { signOut, useSession } from "next-auth/react"; + +import { NEXT_PUBLIC_OIDC_SERVER_URL } from "../../../config/keycloak"; + +const Logout = forwardRef((props, ref: ForwardedRef) => { + const { data: session } = useSession(); + const translate = useTranslate(); + + if (!session) { + return; + } + + const handleClick = () => + signOut({ + // @ts-ignore + callbackUrl: `${NEXT_PUBLIC_OIDC_SERVER_URL}/protocol/openid-connect/logout?id_token_hint=${session.idToken}&post_logout_redirect_uri=${window.location.origin}`, + }); + + return ( + + + + + + {translate("ra.auth.logout", { _: "Logout" })} + + + ); +}); +Logout.displayName = "Logout"; + +export default Logout; diff --git a/pwa/components/admin/layout/Menu.tsx b/pwa/components/admin/layout/Menu.tsx new file mode 100644 index 000000000..de805abe3 --- /dev/null +++ b/pwa/components/admin/layout/Menu.tsx @@ -0,0 +1,19 @@ +import { Menu } from "react-admin"; +import MenuBookIcon from "@mui/icons-material/MenuBook"; +import CommentIcon from "@mui/icons-material/Comment"; + +const CustomMenu = () => ( + + } + /> + } + /> + +); +export default CustomMenu; diff --git a/pwa/components/admin/OpenApiLogo.tsx b/pwa/components/admin/layout/OpenApiLogo.tsx similarity index 100% rename from pwa/components/admin/OpenApiLogo.tsx rename to pwa/components/admin/layout/OpenApiLogo.tsx diff --git a/pwa/components/admin/review/BookField.tsx b/pwa/components/admin/review/BookField.tsx index 23280ee9f..08bfd8d89 100644 --- a/pwa/components/admin/review/BookField.tsx +++ b/pwa/components/admin/review/BookField.tsx @@ -6,14 +6,24 @@ import { getItemPath } from "../../../utils/dataAccess"; export const BookField = (props: UseRecordContextParams) => { const record = useRecordContext(props); - - return !!record && !!record.book ? ( - + if (!record || !record.book) return null; + return ( + {record.book.title} - {record.book.author} - ) : null; + ); }; BookField.defaultProps = { label: "Book" }; diff --git a/pwa/components/admin/review/Edit.tsx b/pwa/components/admin/review/Edit.tsx deleted file mode 100644 index 6a9e8a441..000000000 --- a/pwa/components/admin/review/Edit.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { EditGuesser, type EditGuesserProps } from "@api-platform/admin"; -import { AutocompleteInput, ReferenceInput, required, TextInput } from "react-admin"; - -import { type Book } from "../../../types/Book"; -import { type Review } from "../../../types/Review"; -import { RatingInput } from "./RatingInput"; - -const transform = (data: Review) => ({ - ...data, - book: data.book["@id"], - rating: Number(data.rating), -}); - -export const Edit = (props: EditGuesserProps) => ( - - - ({ title: searchText })} - optionText={(choice: Book): string => `${choice.title} - ${choice.author}`} - label="Book" style={{ width: 500 }} validate={required()}/> - - - - -); diff --git a/pwa/components/admin/review/List.tsx b/pwa/components/admin/review/List.tsx deleted file mode 100644 index b9e2f545e..000000000 --- a/pwa/components/admin/review/List.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { type ListGuesserProps } from "@api-platform/admin"; -import { - TextField, - DateField, - Datagrid, - List as ReactAdminList, - EditButton, - ShowButton, - ReferenceInput, - AutocompleteInput, -} from "react-admin"; - -import { BookField } from "./BookField"; -import { RatingField } from "./RatingField"; -import { RatingInput } from "./RatingInput"; -import { type Book } from "../../../types/Book"; -import { User } from "../../../types/User"; - -const bookQuery = (searchText: string) => { - const values = searchText.split(" - ").map(n => n.trim()).filter(n => n); - const query = { title: values[0] }; - if (typeof values[1] !== "undefined") { - // @ts-ignore - query.author = values[1]; - } - - return query; -}; - -const filters = [ - - `${choice.title} - ${choice.author}`} - name="book" style={{ width: 300 }}/> - , - - ({ name: searchText })} - optionText={(choice: User): string => choice.name} - name="user" style={{ width: 300 }}/> - , - , -]; - -export const List = (props: ListGuesserProps) => ( - - - - - - - - - - -); diff --git a/pwa/components/admin/review/RatingField.tsx b/pwa/components/admin/review/RatingField.tsx index ae7b4d4b0..bea8321e1 100644 --- a/pwa/components/admin/review/RatingField.tsx +++ b/pwa/components/admin/review/RatingField.tsx @@ -1,9 +1,9 @@ -import { useRecordContext, UseRecordContextParams } from "react-admin"; +import { useRecordContext } from "react-admin"; import Rating from "@mui/material/Rating"; -export const RatingField = (props: UseRecordContextParams) => { - const record = useRecordContext(props); - - return !!record && typeof record.rating === "number" ? : null; +export const RatingField = () => { + const record = useRecordContext(); + return !!record && typeof record.rating === "number" ? ( + + ) : null; }; -RatingField.defaultProps = { label: "Rating" }; diff --git a/pwa/components/admin/review/RatingInput.tsx b/pwa/components/admin/review/RatingInput.tsx index 2d0a0a50d..dbfe05a5d 100644 --- a/pwa/components/admin/review/RatingInput.tsx +++ b/pwa/components/admin/review/RatingInput.tsx @@ -1,20 +1,26 @@ import { Labeled, useInput } from "react-admin"; -import { type CommonInputProps, type ResettableTextFieldProps } from "ra-ui-materialui"; +import { + type CommonInputProps, + type ResettableTextFieldProps, +} from "ra-ui-materialui"; import Rating, { type RatingProps } from "@mui/material/Rating"; -export type RatingInputProps = RatingProps & CommonInputProps & Omit; +export type RatingInputProps = RatingProps & + Omit & + Omit; export const RatingInput = (props: RatingInputProps) => { - const { field: { ref, ...field} } = useInput(props); + const { + field: { ref, ...field }, + } = useInput({ ...props, source: "rating" }); const value = Number(field.value); // Error with "helperText" and "validate" props: remove them from the Rating component const { helperText, validate, ...rest } = props; return ( - - + + ); }; RatingInput.displayName = "RatingInput"; -RatingInput.defaultProps = { label: "Rating" }; diff --git a/pwa/components/admin/review/ReviewsEdit.tsx b/pwa/components/admin/review/ReviewsEdit.tsx new file mode 100644 index 000000000..6ba97790f --- /dev/null +++ b/pwa/components/admin/review/ReviewsEdit.tsx @@ -0,0 +1,41 @@ +import { EditGuesser } from "@api-platform/admin"; +import { + AutocompleteInput, + ReferenceInput, + required, + TextInput, +} from "react-admin"; + +import { type Book } from "../../../types/Book"; +import { type Review } from "../../../types/Review"; +import { RatingInput } from "./RatingInput"; + +const transform = (data: Review) => ({ + ...data, + book: data.book["@id"], + rating: Number(data.rating), +}); + +export const ReviewsEdit = () => ( + + + ({ title: searchText })} + optionText={(choice: Book): string => + `${choice.title} - ${choice.author}` + } + label="Book" + style={{ width: 500 }} + validate={required()} + /> + + + + +); diff --git a/pwa/components/admin/review/ReviewsList.tsx b/pwa/components/admin/review/ReviewsList.tsx new file mode 100644 index 000000000..2b33d81db --- /dev/null +++ b/pwa/components/admin/review/ReviewsList.tsx @@ -0,0 +1,68 @@ +import { + TextField, + DateField, + Datagrid, + List, + EditButton, + ShowButton, + ReferenceInput, + AutocompleteInput, + WrapperField, +} from "react-admin"; + +import { BookField } from "./BookField"; +import { RatingField } from "./RatingField"; +import { RatingInput } from "./RatingInput"; +import { type Book } from "../../../types/Book"; +import { User } from "../../../types/User"; + +const bookQuery = (searchText: string) => { + const values = searchText + .split(" - ") + .map((n) => n.trim()) + .filter((n) => n); + const query = { title: values[0] }; + if (typeof values[1] !== "undefined") { + // @ts-ignore + query.author = values[1]; + } + + return query; +}; + +const filters = [ + + + `${choice.title} - ${choice.author}` + } + name="book" + style={{ width: 300 }} + /> + , + + ({ name: searchText })} + optionText={(choice: User): string => choice.name} + name="user" + style={{ width: 300 }} + /> + , + , +]; + +export const ReviewsList = () => ( + + + + + + + + + + + + +); diff --git a/pwa/components/admin/review/ReviewsShow.tsx b/pwa/components/admin/review/ReviewsShow.tsx new file mode 100644 index 000000000..7517fb912 --- /dev/null +++ b/pwa/components/admin/review/ReviewsShow.tsx @@ -0,0 +1,17 @@ +import { FieldGuesser, ShowGuesser } from "@api-platform/admin"; +import { TextField, Labeled } from "react-admin"; + +import { RatingField } from "./RatingField"; +import { BookField } from "./BookField"; + +export const ReviewsShow = () => ( + + + + + + + + + +); diff --git a/pwa/components/admin/review/Show.tsx b/pwa/components/admin/review/Show.tsx deleted file mode 100644 index 7dd935b6a..000000000 --- a/pwa/components/admin/review/Show.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { FieldGuesser, ShowGuesser, type ShowGuesserProps } from "@api-platform/admin"; -import { TextField } from "react-admin"; - -import { RatingField } from "./RatingField"; -import { BookField } from "./BookField"; - -export const Show = (props: ShowGuesserProps) => ( - - - - - - - -); diff --git a/pwa/components/admin/review/index.ts b/pwa/components/admin/review/index.ts new file mode 100644 index 000000000..3f20da340 --- /dev/null +++ b/pwa/components/admin/review/index.ts @@ -0,0 +1,14 @@ +import { ReviewsList } from "./ReviewsList"; +import { ReviewsEdit } from "./ReviewsEdit"; +import { ReviewsShow } from "./ReviewsShow"; +import { type Review } from "../../../types/Review"; + +const reviewResourceProps = { + list: ReviewsList, + edit: ReviewsEdit, + show: ReviewsShow, + hasCreate: false, + recordRepresentation: (record: Review) => record.user.name, +}; + +export default reviewResourceProps;