diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index a4eb35bc..de7292b1 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -28,6 +28,11 @@ module.exports = { rules: { 'react/prop-types': 'off', '@typescript-eslint/no-unused-vars': 'warn', + "@typescript-eslint/no-misused-promises": [2, { + "checksVoidReturn": { + "attributes": false + } + }] }, overrides: [ { diff --git a/frontend/src/layout/AppBar.jsx b/frontend/src/layout/AppBar.jsx deleted file mode 100644 index 63977c35..00000000 --- a/frontend/src/layout/AppBar.jsx +++ /dev/null @@ -1,113 +0,0 @@ -import React from 'react'; -import { AppBar as RaAppBar, Link } from 'react-admin'; -import { Zoom, Box, Typography } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import { UserMenu } from "@semapps/auth-provider"; -import SearchForm from './SearchForm'; - -const useStyles = makeStyles(theme => ({ - appBar: { - [theme.breakpoints.down('sm')]: { - '& .MuiToolbar-root a.MuiLink-root': { - marginRight: 'auto' - } - } - }, - menuButton: { - [theme.breakpoints.up('sm')]: { - display: 'none' - } - }, - toolbar: { - height: 56, - [theme.breakpoints.up('sm')]: { - paddingLeft: '24px' - } - }, - spacer: { - flex: 1 - }, - searchFormContainer: { - minWidth: 240, - flex: 2, - margin: '0 5%', - [theme.breakpoints.up('md')]: { - minWidth: 360, - marginRight: 100 - } - }, - searchFormWrapper: { - maxWidth: 880, - margin: 'auto' - }, - presContainer: { - flex: 1, - overflow: 'hidden', - [theme.breakpoints.up('sm')]: { - flex: 'unset', - display: 'flex', - justifyContent: 'flex-start', - alignItems: 'center' - } - }, - logoContainer: { - display: 'none', - [theme.breakpoints.up('sm')]: { - height: 48, - marginLeft: '0.2em', - marginRight: '0.2em', - display: 'block' - } - }, - logo: { - height: '100%' - }, - title: { - display: 'block', - color: theme.palette.primary.contrastText, - [theme.breakpoints.up('sm')]: { - display: 'none' - }, - [theme.breakpoints.up('md')]: { - display: 'block' - } - } -})); - -const AppBar = props => { - const classes = useStyles(); - return ( - - -
-
- - logo - -
- - {props.title} - -
- - -
-
- -
-
-
-
- ); -}; - -AppBar.defaultProps = { - userMenu: -}; - -export default AppBar; diff --git a/frontend/src/layout/AppBar.tsx b/frontend/src/layout/AppBar.tsx new file mode 100644 index 00000000..30ebecfe --- /dev/null +++ b/frontend/src/layout/AppBar.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { AppBar as RaAppBar, Link, AppBarProps, Logout } from 'react-admin'; +import { Zoom, Box, Typography } from '@mui/material'; +import { UserMenu } from '@semapps/auth-provider'; +import SearchForm from './SearchForm'; + +const AppBar = (props: AppBarProps) => { + return ( + } />} color="primary"> + + + + + logo + + + + {props.title} + + + + + + + + + + ); +}; + +export default AppBar; diff --git a/frontend/src/layout/BaseView.jsx b/frontend/src/layout/BaseView.jsx deleted file mode 100644 index 231cd625..00000000 --- a/frontend/src/layout/BaseView.jsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import { Grid, Card, Typography, Box } from '@mui/material'; - -import makeStyles from '@mui/styles/makeStyles'; - -const useStyles = makeStyles(theme => ({ - title: { - paddingTop: 20, - paddingBottom: 10, - [theme.breakpoints.down('sm')]: { - fontSize: '1.8rem', - paddingTop: 0, - paddingBottom: 6 - } - }, - actions: { - [theme.breakpoints.down('sm')]: { - '& .MuiToolbar-root': { - backgroundColor: theme.palette.background.default, - minHeight: 0, - paddingTop: 0 - }, - '& .MuiButtonBase-root': { - padding: 0 - }, - order: -1 - } - }, - card: { - marginTop: 0, - transition: theme.transitions.create('margin-top'), - position: 'relative', - flex: '1 1 auto', - [theme.breakpoints.down('sm')]: { - '& > .MuiBox-root, .MuiCardContent-root': { - paddingRight: theme.spacing(1.5), - paddingLeft: theme.spacing(1.5) - } - }, - } -})); - -const BaseView = ({ title, actions, aside, context, children }) => { - const classes = useStyles(); - return( - - - - {title ?? context.defaultTitle} - - - - {actions} - - - - - {children} - - {aside} - - - - ) -}; - -export default BaseView; diff --git a/frontend/src/layout/BaseView.tsx b/frontend/src/layout/BaseView.tsx new file mode 100644 index 00000000..76fbb401 --- /dev/null +++ b/frontend/src/layout/BaseView.tsx @@ -0,0 +1,51 @@ +import React, { PropsWithChildren, ReactNode } from 'react'; +import { Grid, Card, Typography } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const Title = styled(Typography)(({ theme }) => ({ + [theme.breakpoints.down('sm')]: { + fontSize: '1.8rem', + }, +})) as typeof Typography; + +const ActionsGrid = styled(Grid)(({ theme }) => ({ + [theme.breakpoints.down('sm')]: { + '& .MuiToolbar-root': { + backgroundColor: theme.palette.background.default, + minHeight: 0, + paddingTop: 0, + alignItems: 'center', + height: '100%', + }, + '& .MuiButtonBase-root': { + padding: 0, + }, + }, +})); + +type Props = { + title: string | ReactNode; + actions: JSX.Element; + aside?: JSX.Element; +}; + +const BaseView = ({ title, actions, aside, children }: PropsWithChildren) => { + return ( + + + + {title} + + + + {actions} + + + {children} + {aside} + + + ); +}; + +export default BaseView; diff --git a/frontend/src/layout/Layout.jsx b/frontend/src/layout/Layout.jsx deleted file mode 100644 index f621654b..00000000 --- a/frontend/src/layout/Layout.jsx +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import React from 'react'; -import { Layout as RaLayout } from 'react-admin'; -import makeStyles from '@mui/styles/makeStyles'; -import AppBar from './AppBar'; -import TreeMenu from './TreeMenu/TreeMenu'; - -const useStyles = makeStyles(theme => ({ - layout: { - '& .RaLayout-content': { - paddingTop: theme.spacing(1), - paddingRight: theme.spacing(2), - paddingBottom: theme.spacing(2), - [theme.breakpoints.down('sm')]: { - padding: theme.spacing(1), - }, - '& a:not(.MuiListItemButton-root):not(.MuiButtonBase-root)': { - overflowWrap: 'break-word', - color: theme.palette.primary.main - } - } - } -})); - -const Layout = ({ appBar, menu, children, ...otherProps }) => { - const classes = useStyles(); - return ( - - {children} - - ); -}; - -export default Layout; diff --git a/frontend/src/layout/Layout.tsx b/frontend/src/layout/Layout.tsx new file mode 100644 index 00000000..e4ccbbca --- /dev/null +++ b/frontend/src/layout/Layout.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { LayoutProps, Layout as RaLayout } from 'react-admin'; +import { useTheme } from '@mui/material/styles'; +import AppBar from './AppBar'; +import TreeMenu from './TreeMenu/TreeMenu'; + +const Layout = ({ children, ...otherProps }: LayoutProps) => { + const theme = useTheme(); + + return ( + + {children} + + ); +}; + +export default Layout; diff --git a/frontend/src/layout/SearchForm.jsx b/frontend/src/layout/SearchForm.jsx deleted file mode 100644 index 76551a23..00000000 --- a/frontend/src/layout/SearchForm.jsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { useResourceDefinitions } from 'react-admin'; -import { Grid, Select, MenuItem, TextField, Button } from '@mui/material'; -import { alpha } from '@mui/material/styles'; -import { useForm, Controller } from 'react-hook-form'; -import { useNavigate, useLocation } from 'react-router-dom'; -import makeStyles from '@mui/styles/makeStyles'; -import SearchIcon from '@mui/icons-material/Search'; - -const useStyles = makeStyles(theme => ({ - button: { - color: theme.palette.primary.contrastText, - borderColor: alpha(theme.palette.common.black, 0.42), - '& .MuiButton-startIcon': { - [theme.breakpoints.down('md')]: { - margin: 0 - } - }, - '&:hover': { - borderColor: alpha(theme.palette.common.black, 0.8), - } - }, - buttonLabel: { - [theme.breakpoints.down('md')]: { - display: 'none' - } - }, - field: { - color: theme.palette.primary.contrastText - } -})); - -const TypeSelect = (props) => { - const resourceDefinitions = useResourceDefinitions(); - const resources = useMemo(() => Object.values(resourceDefinitions), [resourceDefinitions]); - if (resources.length === 0) return null; - return ( - - ); -}; - -const SearchForm = () => { - const classes = useStyles(); - const navigate = useNavigate(); - - const location = useLocation(); - const matches = location.pathname.match(/^\/([^/]+)/); - const type = matches ? matches[1] : 'Organization'; - - let search = new URLSearchParams(location.search); - const filter = (search && JSON.parse(search.get('filter'))) || {}; - - const { register, setValue, control, handleSubmit } = useForm({ - defaultValues: { - type, - filter: filter.q - } - }); - - // Reinitialize the form on page change - useEffect(() => { - setValue('type', type); - setValue('filter', filter.q); - }, [location.pathname, type, filter.q, setValue]); - - const onSubmit = ({ filter, type }) => { - if (filter) { - navigate(`/${type}?filter=${encodeURIComponent(`{"q": "${filter}"}`)}`); - } else { - navigate(`/${type}?filter=${encodeURIComponent(`{}`)}`); - } - }; - - return ( -
- - - - - - ( - - )} - /> - - - - - -
- ); -}; - -export default SearchForm; diff --git a/frontend/src/layout/SearchForm.tsx b/frontend/src/layout/SearchForm.tsx new file mode 100644 index 00000000..e0abdba4 --- /dev/null +++ b/frontend/src/layout/SearchForm.tsx @@ -0,0 +1,119 @@ +import React, { useEffect, useMemo } from 'react'; +import { useResourceDefinitions } from 'react-admin'; +import { Select, MenuItem, TextField, Button, Box, Stack, IconButton, SelectProps } from '@mui/material'; +import { alpha } from '@mui/material/styles'; +import { useForm, Controller, SubmitHandler } from 'react-hook-form'; +import { useNavigate, useLocation } from 'react-router-dom'; +import SearchIcon from '@mui/icons-material/Search'; +import { ResourceOptions } from './TreeMenu/TreeMenu'; + +type Fields = { + filter: string; + type: string; +}; + +const TypeSelect = (props: SelectProps) => { + const resourceDefinitions = useResourceDefinitions(); + const resources = useMemo(() => Object.values(resourceDefinitions), [resourceDefinitions]); + + if (resources.length === 0) return null; + + return ( + + ); +}; + +const SearchForm = () => { + const navigate = useNavigate(); + + const location = useLocation(); + const matches = location.pathname.match(/^\/([^/]+)/); + const type = matches ? matches[1] : 'Organization'; + + const search = new URLSearchParams(location.search); + const filter = (search && (JSON.parse(search.get('filter') || '{}') as { q?: string })) || {}; + + const { register, setValue, control, handleSubmit } = useForm({ + defaultValues: { + type, + filter: filter.q, + }, + }); + + // Reinitialize the form on page change + useEffect(() => { + setValue('type', type); + setValue('filter', filter.q || ''); + }, [location.pathname, type, filter.q, setValue]); + + const onSubmit: SubmitHandler = ({ filter, type }) => { + if (filter) { + navigate(`/${type}?filter=${encodeURIComponent(`{"q": "${filter}"}`)}`); + } else { + navigate(`/${type}?filter=${encodeURIComponent(`{}`)}`); + } + }; + + return ( + + + + ( + + )} + /> + + + + + + + ); +}; + +export default SearchForm; diff --git a/frontend/src/layout/TreeMenu/ResourceMenuLink.jsx b/frontend/src/layout/TreeMenu/ResourceMenuLink.jsx deleted file mode 100644 index 8b870536..00000000 --- a/frontend/src/layout/TreeMenu/ResourceMenuLink.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import { MenuItemLink } from 'react-admin'; -import DefaultIcon from '@mui/icons-material/ViewList'; - -const ResourceMenuLink = ({ resource }) => ( - : } - /> -); - -export default ResourceMenuLink; diff --git a/frontend/src/layout/TreeMenu/ResourceMenuLink.tsx b/frontend/src/layout/TreeMenu/ResourceMenuLink.tsx new file mode 100644 index 00000000..63283ff3 --- /dev/null +++ b/frontend/src/layout/TreeMenu/ResourceMenuLink.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { MenuItemLink, useSidebarState } from 'react-admin'; +import DefaultIcon from '@mui/icons-material/ViewList'; + +type Props = { + resource: { + name: string; + icon?: React.ElementType; + options?: { + label: string; + }; + }; + root?: boolean; +}; + +const ResourceMenuLink = ({ resource, root }: Props) => { + const [sidebarIsOpen] = useSidebarState(); + + return ( + // @ts-expect-error Bad typing from react-admin + : } + sx={{ + paddingLeft: root || !sidebarIsOpen ? 2 : 4, + transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms', + }} + /> + ); +}; + +export default ResourceMenuLink; diff --git a/frontend/src/layout/TreeMenu/SubMenu.jsx b/frontend/src/layout/TreeMenu/SubMenu.jsx deleted file mode 100644 index 2d219439..00000000 --- a/frontend/src/layout/TreeMenu/SubMenu.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { MenuItemLink, useSidebarState } from 'react-admin'; -import { MenuList, Collapse, Tooltip } from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import ExpandMore from '@mui/icons-material/ExpandMore'; - -const useStyles = makeStyles(theme => ({ - sidebarIsOpen: { - '& a': { - paddingLeft: theme.spacing(4), - transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms' - } - }, - sidebarIsClosed: { - '& a': { - paddingLeft: theme.spacing(2), - transition: 'padding-left 195ms cubic-bezier(0.4, 0, 0.6, 1) 0ms' - } - } -})); - -const SubMenu = ({ handleToggle, isOpen, name, icon, children }) => { - const classes = useStyles(); - const [sidebarIsOpen, setSidebarIsOpen] = useSidebarState(); - - const header = ( - : icon} - onClick={(e) => { - e.preventDefault(); - setSidebarIsOpen(true); - handleToggle(); - }} - /> - ); - - return ( - <> - {sidebarIsOpen || isOpen ? ( - header - ) : ( - - {header} - - )} - - - {children} - - - - ); -}; - -export default SubMenu; diff --git a/frontend/src/layout/TreeMenu/SubMenu.tsx b/frontend/src/layout/TreeMenu/SubMenu.tsx new file mode 100644 index 00000000..dc912904 --- /dev/null +++ b/frontend/src/layout/TreeMenu/SubMenu.tsx @@ -0,0 +1,45 @@ +import React, { PropsWithChildren } from 'react'; +import { MenuItemLink, useSidebarState } from 'react-admin'; +import { MenuList, Collapse, Tooltip } from '@mui/material'; +import ExpandMore from '@mui/icons-material/ExpandMore'; + +type Props = { + handleToggle: () => void; + isOpen: boolean; + name: string; + icon: JSX.Element; +}; + +const SubMenu = ({ handleToggle, isOpen, name, icon, children }: PropsWithChildren) => { + const [sidebarIsOpen, setSidebarIsOpen] = useSidebarState(); + + const header = ( + // @ts-expect-error Bad typing from react-admin + : icon} + onClick={(e) => { + e.preventDefault(); + setSidebarIsOpen(true); + handleToggle(); + }} + sx={{ paddingLeft: 2 }} + /> + ); + + return ( + <> + + {header} + + + + {children} + + + + ); +}; + +export default SubMenu; diff --git a/frontend/src/layout/TreeMenu/TreeMenu.jsx b/frontend/src/layout/TreeMenu/TreeMenu.tsx similarity index 56% rename from frontend/src/layout/TreeMenu/TreeMenu.jsx rename to frontend/src/layout/TreeMenu/TreeMenu.tsx index 97799b69..f3537d74 100644 --- a/frontend/src/layout/TreeMenu/TreeMenu.jsx +++ b/frontend/src/layout/TreeMenu/TreeMenu.tsx @@ -1,22 +1,26 @@ -/* eslint-disable react/no-children-prop */ -import React, { useState, useEffect, useMemo } from "react"; -import { useResourceDefinitions, Logout, Menu, useGetIdentity, MenuItemLink, useTranslate } from "react-admin"; -import { useLocation } from "react-router"; -import { useMediaQuery, Divider } from "@mui/material"; -import DefaultIcon from "@mui/icons-material/ViewList"; +import React, { useState, useEffect, useMemo } from 'react'; +import { useResourceDefinitions, Logout, Menu, useGetIdentity, MenuItemLink, useTranslate } from 'react-admin'; +import { useLocation } from 'react-router'; +import { useMediaQuery, Divider } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import DefaultIcon from '@mui/icons-material/ViewList'; import LockOpenIcon from '@mui/icons-material/LockOpen'; import LoginIcon from '@mui/icons-material/Login'; -import SubMenu from "./SubMenu"; -import ResourceMenuLink from "./ResourceMenuLink"; +import SubMenu from './SubMenu'; +import ResourceMenuLink from './ResourceMenuLink'; +import resources from '../../resources'; + +export type ResourceOptions = { + label: string; + parent?: keyof typeof resources; +} const TreeMenu = () => { - const isXSmall = useMediaQuery((theme) => theme.breakpoints.down("sm")); + const theme = useTheme(); + const isXSmall = useMediaQuery(theme.breakpoints.down('sm')); - const resourceDefinitions = useResourceDefinitions(); - const resources = useMemo( - () => Object.values(resourceDefinitions), - [resourceDefinitions] - ); + const resourceDefinitions = useResourceDefinitions(); + const resources = useMemo(() => Object.values(resourceDefinitions), [resourceDefinitions]); const location = useLocation(); const matches = location.pathname.match(/^\/([^/]+)/); @@ -27,20 +31,17 @@ const TreeMenu = () => { const translate = useTranslate(); - const [openSubMenus, setOpenSubMenus] = useState({}); - const handleToggle = (menu) => { + const [openSubMenus, setOpenSubMenus] = useState>({}); + const handleToggle = (menu: string) => { setOpenSubMenus((state) => ({ ...state, [menu]: !state[menu] })); }; // Get menu root items - const menuRootItems = useMemo( - () => resources.filter((r) => !r.options?.parent), - [resources] - ); + const menuRootItems = useMemo(() => resources.filter((r) => !r.options?.parent), [resources]); // Calculate available categories const categories = useMemo(() => { - const names = resources.reduce((categories, resource) => { + const names = resources.reduce((categories, resource) => { if (resource.options?.parent) { categories.push(resource.options.parent); } @@ -51,15 +52,10 @@ const TreeMenu = () => { // Open submenu of current page useEffect(() => { - const currentResource = resources.find( - (resource) => resource.name === currentResourceName - ); + const currentResource = resources.find((resource) => resource.name === currentResourceName); const currentCategory = - currentResource && - categories.find( - (category) => category.name === currentResource.options?.parent - ); + currentResource && categories.find((category) => category.name === currentResource.options?.parent); if (currentCategory) { setOpenSubMenus((state) => ({ ...state, [currentCategory.name]: true })); @@ -76,15 +72,13 @@ const TreeMenu = () => { icon={menuRootItem.icon ? : } > {resources - .filter(resource => resource.hasList && resource.options.parent === menuRootItem.name) - .map(resource => ( + .filter((resource) => resource.hasList && resource.options?.parent === menuRootItem.name) + .map((resource) => ( ))} ) : ( - menuRootItem.hasList && ( - - ) + menuRootItem.hasList && ); }); @@ -92,15 +86,17 @@ const TreeMenu = () => { menuItems.push(); if (isLogged) { - menuItems.push() + menuItems.push(); } else { menuItems.push( + // @ts-expect-error Bad typing from react-admin } />, + // @ts-expect-error Bad typing from react-admin { } } - return ; + return {menuItems}; }; export default TreeMenu; diff --git a/frontend/src/layout/create/Create.jsx b/frontend/src/layout/create/Create.jsx deleted file mode 100644 index 4aeaadad..00000000 --- a/frontend/src/layout/create/Create.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { CreateActions, CreateBase } from 'react-admin'; -import CreateView from "./CreateView"; - -const Create = ({ title, actions, children, ...rest }) => ( - - - {children} - - -); - -Create.defaultProps = { - actions: -}; - -export default Create; diff --git a/frontend/src/layout/create/Create.tsx b/frontend/src/layout/create/Create.tsx new file mode 100644 index 00000000..efdfa55a --- /dev/null +++ b/frontend/src/layout/create/Create.tsx @@ -0,0 +1,13 @@ +import React, { PropsWithChildren } from 'react'; +import { CreateActions, CreateBase, CreateProps } from 'react-admin'; +import CreateView from "./CreateView"; + +const Create = ({ title, actions, children, ...rest }: PropsWithChildren) => ( + + } title={title}> + {children} + + +); + +export default Create; diff --git a/frontend/src/layout/create/CreateView.jsx b/frontend/src/layout/create/CreateView.jsx deleted file mode 100644 index 7ed774c0..00000000 --- a/frontend/src/layout/create/CreateView.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { useCreateContext } from 'react-admin'; -import { useCheckPermissions } from '@semapps/auth-provider'; -import { useCreateContainer } from '@semapps/semantic-data-provider'; -import BaseView from "../BaseView"; - -const CreateView = ({ title, actions, children }) => { - const createContext = useCreateContext({ defaultTitle: title }); - const createContainerUri = useCreateContainer(createContext.resource); - useCheckPermissions(createContainerUri, 'create'); - return( - - {children} - - ) -}; - -export default CreateView; diff --git a/frontend/src/layout/create/CreateView.tsx b/frontend/src/layout/create/CreateView.tsx new file mode 100644 index 00000000..da7d5187 --- /dev/null +++ b/frontend/src/layout/create/CreateView.tsx @@ -0,0 +1,26 @@ +import React, { PropsWithChildren, ReactElement } from 'react'; +import { useCreateContext } from 'react-admin'; +import { useCheckPermissions } from '@semapps/auth-provider'; +import { useCreateContainerUri } from '@semapps/semantic-data-provider'; +import BaseView from "../BaseView"; + +type Props = { + title?: string | ReactElement; + actions: JSX.Element; +}; + +const CreateView = ({ title, actions, children }: PropsWithChildren) => { + const createContext = useCreateContext(); + const createContainerUri = useCreateContainerUri()(createContext.resource); + + // @ts-expect-error Bad typing of Semapps + useCheckPermissions(createContainerUri || {}, 'create'); + + return( + + {children} + + ) +}; + +export default CreateView; diff --git a/frontend/src/layout/edit/Edit.jsx b/frontend/src/layout/edit/Edit.jsx deleted file mode 100644 index 863da9fd..00000000 --- a/frontend/src/layout/edit/Edit.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { EditBase } from 'react-admin'; -import { EditActionsWithPermissions } from "@semapps/auth-provider"; -import EditView from "./EditView"; - -const Edit = ({ title, actions, children, ...rest }) => ( - - - {children} - - -); - -Edit.defaultProps = { - actions: -}; - -export default Edit; diff --git a/frontend/src/layout/edit/Edit.tsx b/frontend/src/layout/edit/Edit.tsx new file mode 100644 index 00000000..a37bf9b4 --- /dev/null +++ b/frontend/src/layout/edit/Edit.tsx @@ -0,0 +1,14 @@ +import React, { PropsWithChildren } from 'react'; +import { EditBase, EditProps } from 'react-admin'; +import { EditActionsWithPermissions } from "@semapps/auth-provider"; +import EditView from "./EditView"; + +const Edit = ({ title, actions, children, ...rest }: PropsWithChildren) => ( + + }> + {children} + + +); + +export default Edit; diff --git a/frontend/src/layout/edit/EditView.jsx b/frontend/src/layout/edit/EditView.jsx deleted file mode 100644 index d78dafec..00000000 --- a/frontend/src/layout/edit/EditView.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { useEditContext, useGetRecordRepresentation, useResourceContext } from 'react-admin'; -import { useCheckPermissions } from '@semapps/auth-provider'; -import { EditToolbarWithPermissions } from "@semapps/auth-provider"; -import BaseView from "../BaseView"; - -const EditView = ({ title, actions, children }) => { - const editContext = useEditContext(); - useCheckPermissions(editContext?.record?.id, 'edit'); - - const resource = useResourceContext(); - const getRecordRepresentation = useGetRecordRepresentation(resource); - - const recordTitle = getRecordRepresentation(editContext?.record); - - return( - - {React.cloneElement(children, { - toolbar: - })} - - ) -}; - -export default EditView; diff --git a/frontend/src/layout/edit/EditView.tsx b/frontend/src/layout/edit/EditView.tsx new file mode 100644 index 00000000..8acc843e --- /dev/null +++ b/frontend/src/layout/edit/EditView.tsx @@ -0,0 +1,32 @@ +import React, { PropsWithChildren, ReactElement } from 'react'; +import { RaRecord, useEditContext, useGetRecordRepresentation, useResourceContext } from 'react-admin'; +import { useCheckPermissions } from '@semapps/auth-provider'; +import { EditToolbarWithPermissions } from '@semapps/auth-provider'; +import BaseView from '../BaseView'; + +type Props = { + title?: string | ReactElement; + actions: JSX.Element; +}; + +const EditView = ({ title, actions, children }: PropsWithChildren) => { + const editContext = useEditContext>(); + + // @ts-expect-error Bad typing of Semapps + useCheckPermissions(editContext?.record?.id || {}, 'edit'); + + const resource = useResourceContext(); + const getRecordRepresentation = useGetRecordRepresentation(resource); + + const recordTitle = getRecordRepresentation(editContext?.record); + + return ( + + {React.cloneElement(children as ReactElement, { + toolbar: + })} + + ); +}; + +export default EditView; diff --git a/frontend/src/layout/list/List.jsx b/frontend/src/layout/list/List.jsx deleted file mode 100644 index e7a3cb9c..00000000 --- a/frontend/src/layout/list/List.jsx +++ /dev/null @@ -1,15 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import React from 'react'; -import { ListBase } from 'react-admin'; -import ListView from "./ListView"; -import ListActionsWithViewsAndPermissions from "./ListActionsWithViewsAndPermissions"; - -const List = ({ actions, aside, pagination, children, perPage, ...rest }) => ( - - } pagination={pagination}> - {children} - - - ); - -export default List; diff --git a/frontend/src/layout/list/List.tsx b/frontend/src/layout/list/List.tsx new file mode 100644 index 00000000..56a6e5c3 --- /dev/null +++ b/frontend/src/layout/list/List.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { ListBase, ListProps, TopToolbar } from 'react-admin'; +import ListView from './ListView'; +import { ListActionsWithPermissions } from '@semapps/auth-provider'; +import { ViewsButtons } from '@semapps/list-components'; +import { Box } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +const ActionsBox = styled(Box)(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'flex-end', + [theme.breakpoints.down('md')]: { + flexDirection: 'row-reverse', + }, +})); + +const List = ({ aside, pagination, children, ...rest }: ListProps) => ( + + + + + + + + } + pagination={pagination} + > + {children} + + +); + +export default List; diff --git a/frontend/src/layout/list/ListActionsWithViewsAndPermissions.jsx b/frontend/src/layout/list/ListActionsWithViewsAndPermissions.jsx deleted file mode 100644 index 6b5d2c88..00000000 --- a/frontend/src/layout/list/ListActionsWithViewsAndPermissions.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React from 'react'; -import { CreateButton, ExportButton, useResourceDefinition, TopToolbar, usePermissions, useResourceContext } from 'react-admin'; -import { useMediaQuery } from '@mui/material'; -import { useCreateContainer } from "@semapps/semantic-data-provider"; -import { ViewsButtons } from "@semapps/list-components"; -import { PermissionsButton } from "@semapps/auth-provider"; - -// Custom ListActions which include the PermissionButton and ViewsButtons -const ListActionsWithViewsAndPermissions = ({ - bulkActions, - sort, - displayedFilters, - exporter, - filters, - filterValues, - onUnselectItems, - selectedIds, - showFilter, - total, - ...rest -}) => { - const resource = useResourceContext(); - const xs = useMediaQuery(theme => theme.breakpoints.down('sm')); - const resourceDefinition = useResourceDefinition(rest); - const createContainerUri = useCreateContainer(resource); - const { permissions } = usePermissions(createContainerUri); - return ( - - - {filters && - React.cloneElement(filters, { - showFilter, - displayedFilters, - filterValues, - context: 'button' - })} - {resourceDefinition.hasCreate && permissions && permissions.some(p => ['acl:Append', 'acl:Write', 'acl:Control'].includes(p['acl:mode'])) && - } - {permissions && permissions.some(p => ['acl:Control'].includes(p['acl:mode'])) && ( - - )} - {!xs && exporter !== false && ( - - )} - {bulkActions && - React.cloneElement(bulkActions, { - filterValues, - selectedIds, - onUnselectItems - })} - - ); -}; - -export default ListActionsWithViewsAndPermissions; diff --git a/frontend/src/layout/list/ListView.jsx b/frontend/src/layout/list/ListView.jsx deleted file mode 100644 index 3742ea09..00000000 --- a/frontend/src/layout/list/ListView.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { useListContext, Pagination } from 'react-admin'; -import { Box } from '@mui/material'; -import BaseView from "../BaseView"; - -const ListView = ({ title, children, aside, actions, pagination }) => { - const listContext = useListContext(); - return( - - - {children} - - {pagination === false ? null : pagination} - - ) -}; - -ListView.defaultProps = { - pagination: -}; - -export default ListView; diff --git a/frontend/src/layout/list/ListView.tsx b/frontend/src/layout/list/ListView.tsx new file mode 100644 index 00000000..b1c9dfae --- /dev/null +++ b/frontend/src/layout/list/ListView.tsx @@ -0,0 +1,30 @@ +import React, { PropsWithChildren, ReactElement } from 'react'; +import { useListContext, Pagination } from 'react-admin'; +import { Box } from '@mui/material'; +import BaseView from '../BaseView'; +import { useCheckPermissions } from '@semapps/auth-provider'; +import { useCreateContainerUri } from '@semapps/semantic-data-provider'; + +type Props = { + title?: string | ReactElement; + actions: JSX.Element; + aside?: JSX.Element; + pagination?: JSX.Element | boolean; +}; + +const ListView = ({ title, children, aside, actions, pagination }: PropsWithChildren) => { + const listContext = useListContext(); + const createContainerUri = useCreateContainerUri()(listContext.resource); + + // @ts-expect-error Bad typing of Semapps + useCheckPermissions(createContainerUri || {}, 'list'); + + return ( + + {children} + {pagination === false ? null : pagination || } + + ); +}; + +export default ListView; diff --git a/frontend/src/layout/show/Show.jsx b/frontend/src/layout/show/Show.jsx deleted file mode 100644 index 0f031710..00000000 --- a/frontend/src/layout/show/Show.jsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { ShowBase } from 'react-admin'; -import { ShowActionsWithPermissions } from "@semapps/auth-provider"; -import ShowView from "./ShowView"; - -const Show = ({ title, actions, children, ...rest }) => ( - - - {children} - - -); - -Show.defaultProps = { - actions: -}; - -export default Show; diff --git a/frontend/src/layout/show/Show.tsx b/frontend/src/layout/show/Show.tsx new file mode 100644 index 00000000..045393c2 --- /dev/null +++ b/frontend/src/layout/show/Show.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { ShowBase, ShowProps } from 'react-admin'; +import { ShowActionsWithPermissions } from '@semapps/auth-provider'; +import ShowView from './ShowView'; + +const Show = ({ title, actions, children, ...rest }: ShowProps) => ( + + }> + {children} + + +); + +export default Show; diff --git a/frontend/src/layout/show/ShowView.jsx b/frontend/src/layout/show/ShowView.jsx deleted file mode 100644 index ef8639f8..00000000 --- a/frontend/src/layout/show/ShowView.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import { useGetRecordRepresentation, useResourceContext, useShowContext } from 'react-admin'; -import { Box } from '@mui/material'; -import { useCheckPermissions } from '@semapps/auth-provider'; -import BaseView from "../BaseView"; - -const ShowView = ({ title, actions, children }) => { - const showContext = useShowContext(); - useCheckPermissions(showContext?.record?.id, 'show'); - - const resource = useResourceContext(); - const getRecordRepresentation = useGetRecordRepresentation(resource); - - const recordTitle = getRecordRepresentation(showContext?.record); - - return( - - - {children} - - - ) -}; - -export default ShowView; diff --git a/frontend/src/layout/show/ShowView.tsx b/frontend/src/layout/show/ShowView.tsx new file mode 100644 index 00000000..f9127ff4 --- /dev/null +++ b/frontend/src/layout/show/ShowView.tsx @@ -0,0 +1,30 @@ +import React, { PropsWithChildren, ReactElement } from 'react'; +import { RaRecord, useGetRecordRepresentation, useResourceContext, useShowContext } from 'react-admin'; +import { Box } from '@mui/material'; +import { useCheckPermissions } from '@semapps/auth-provider'; +import BaseView from '../BaseView'; + +type Props = { + title?: string | ReactElement; + actions: JSX.Element; +}; + +const ShowView = ({ title, actions, children }: PropsWithChildren) => { + const showContext = useShowContext>(); + + // @ts-expect-error Bad typing of Semapps + useCheckPermissions(showContext?.record?.id || {}, 'show'); + + const resource = useResourceContext(); + const getRecordRepresentation = useGetRecordRepresentation(resource); + + const recordTitle = getRecordRepresentation(showContext?.record); + + return ( + + {children} + + ); +}; + +export default ShowView;