From 2c14b24cfc5f7286d633201c5e510d0ca93e871d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20B=C3=A9gaudeau?= Date: Mon, 24 Jul 2023 01:48:52 +0200 Subject: [PATCH] [209] Improve the navbar used by the project views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/svalyn/svalyn-studio/issues/209 Signed-off-by: Stéphane Bégaudeau --- .../src/domains/DomainsShell.tsx | 8 +- .../svalyn-studio-app/src/navbars/Navbar.tsx | 138 +++------------- .../src/navbars/Navbar.types.ts | 1 - .../src/navbars/SearchButton.tsx | 55 ++++++ .../src/navbars/SearchButton.types.ts | 22 +++ .../src/navbars/UserMenu.tsx | 112 +++++++++++++ .../src/navbars/UserMenu.types.ts | 29 ++++ .../src/projects/ProjectBreadcrumbs.tsx | 156 ++++++++++++++++++ .../src/projects/ProjectShell.tsx | 18 +- 9 files changed, 408 insertions(+), 131 deletions(-) create mode 100644 frontend/svalyn-studio-app/src/navbars/SearchButton.tsx create mode 100644 frontend/svalyn-studio-app/src/navbars/SearchButton.types.ts create mode 100644 frontend/svalyn-studio-app/src/navbars/UserMenu.tsx create mode 100644 frontend/svalyn-studio-app/src/navbars/UserMenu.types.ts create mode 100644 frontend/svalyn-studio-app/src/projects/ProjectBreadcrumbs.tsx diff --git a/frontend/svalyn-studio-app/src/domains/DomainsShell.tsx b/frontend/svalyn-studio-app/src/domains/DomainsShell.tsx index c74c1a6..3759ae6 100644 --- a/frontend/svalyn-studio-app/src/domains/DomainsShell.tsx +++ b/frontend/svalyn-studio-app/src/domains/DomainsShell.tsx @@ -18,9 +18,7 @@ */ import Container from '@mui/material/Container'; -import Link from '@mui/material/Link'; import Toolbar from '@mui/material/Toolbar'; -import { Link as RouterLink } from 'react-router-dom'; import { Navbar } from '../navbars/Navbar'; import { DomainsShellProps } from './DomainsShell.types'; @@ -28,11 +26,7 @@ export const DomainsShell = ({ children }: DomainsShellProps) => { return ( <>
- - - Domains - - + diff --git a/frontend/svalyn-studio-app/src/navbars/Navbar.tsx b/frontend/svalyn-studio-app/src/navbars/Navbar.tsx index 9563ba5..97f925f 100644 --- a/frontend/svalyn-studio-app/src/navbars/Navbar.tsx +++ b/frontend/svalyn-studio-app/src/navbars/Navbar.tsx @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Stéphane Bégaudeau. + * Copyright (c) 2022, 2023 Stéphane Bégaudeau. * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and * associated documentation files (the "Software"), to deal in the Software without restriction, @@ -18,36 +18,24 @@ */ import { gql, useQuery } from '@apollo/client'; -import HelpIcon from '@mui/icons-material/Help'; -import HomeIcon from '@mui/icons-material/Home'; -import LogoutIcon from '@mui/icons-material/Logout'; -import MailOutlineIcon from '@mui/icons-material/MailOutline'; +import HubOutlinedIcon from '@mui/icons-material/HubOutlined'; import NotificationsNoneIcon from '@mui/icons-material/NotificationsNone'; -import PersonIcon from '@mui/icons-material/Person'; -import SearchIcon from '@mui/icons-material/Search'; -import SettingsIcon from '@mui/icons-material/Settings'; import AppBar from '@mui/material/AppBar'; import Avatar from '@mui/material/Avatar'; import Badge from '@mui/material/Badge'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Divider from '@mui/material/Divider'; import IconButton from '@mui/material/IconButton'; -import ListItem from '@mui/material/ListItem'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; +import Link from '@mui/material/Link'; import Toolbar from '@mui/material/Toolbar'; import { useContext, useEffect, useState } from 'react'; -import { Navigate, Link as RouterLink } from 'react-router-dom'; -import { getCookie } from '../cookies/getCookie'; +import { Link as RouterLink } from 'react-router-dom'; import { Svalyn } from '../icons/Svalyn'; import { PaletteContext } from '../palette/PaletteContext'; import { PaletteContextValue } from '../palette/PaletteContext.types'; import { ErrorSnackbar } from '../snackbar/ErrorSnackbar'; import { GetViewerData, GetViewerVariables, NavbarProps, NavbarState } from './Navbar.types'; -const { VITE_BACKEND_URL } = import.meta.env; +import { SearchButton } from './SearchButton'; +import { UserMenu } from './UserMenu'; const getViewerQuery = gql` query getViewer { @@ -64,7 +52,6 @@ export const Navbar = ({ children }: NavbarProps) => { const [state, setState] = useState({ viewer: null, anchorElement: null, - redirectToLogin: false, message: null, }); @@ -93,27 +80,6 @@ export const Navbar = ({ children }: NavbarProps) => { }; const handleCloseUserMenu = () => setState((prevState) => ({ ...prevState, anchorElement: null })); - const handleLogout: React.MouseEventHandler = () => { - const csrfToken = getCookie('XSRF-TOKEN'); - - fetch(`${VITE_BACKEND_URL}/api/logout`, { - method: 'POST', - credentials: 'include', - mode: 'cors', - headers: { - 'X-XSRF-TOKEN': csrfToken, - }, - }).then(() => { - setState((prevState) => ({ ...prevState, redirectToLogin: true })); - }); - }; - - if (state.redirectToLogin) { - return ; - } - - var isApple = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform); - return ( <> @@ -133,32 +99,23 @@ export const Navbar = ({ children }: NavbarProps) => { marginLeft: 'auto', }} > - + + Domains + + @@ -167,59 +124,16 @@ export const Navbar = ({ children }: NavbarProps) => { - - - - - - - - - Dashboard - - - - - - Profile - - - - - - Invitations - - - - - - Settings - - - - - - Help - - - - - - - Sign out - - + /> ) : null} diff --git a/frontend/svalyn-studio-app/src/navbars/Navbar.types.ts b/frontend/svalyn-studio-app/src/navbars/Navbar.types.ts index 8ad5299..50c360f 100644 --- a/frontend/svalyn-studio-app/src/navbars/Navbar.types.ts +++ b/frontend/svalyn-studio-app/src/navbars/Navbar.types.ts @@ -24,7 +24,6 @@ export interface NavbarProps { export interface NavbarState { viewer: Viewer | null; anchorElement: HTMLElement | null; - redirectToLogin: boolean; message: string | null; } diff --git a/frontend/svalyn-studio-app/src/navbars/SearchButton.tsx b/frontend/svalyn-studio-app/src/navbars/SearchButton.tsx new file mode 100644 index 0000000..6a92da7 --- /dev/null +++ b/frontend/svalyn-studio-app/src/navbars/SearchButton.tsx @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import SearchIcon from '@mui/icons-material/Search'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import { SearchButtonProps } from './SearchButton.types'; + +export const SearchButton = ({ onClick }: SearchButtonProps) => { + var isApple = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform); + return ( + + ); +}; diff --git a/frontend/svalyn-studio-app/src/navbars/SearchButton.types.ts b/frontend/svalyn-studio-app/src/navbars/SearchButton.types.ts new file mode 100644 index 0000000..2dfb02f --- /dev/null +++ b/frontend/svalyn-studio-app/src/navbars/SearchButton.types.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +export interface SearchButtonProps { + onClick?: React.MouseEventHandler | undefined; +} diff --git a/frontend/svalyn-studio-app/src/navbars/UserMenu.tsx b/frontend/svalyn-studio-app/src/navbars/UserMenu.tsx new file mode 100644 index 0000000..b8e6c47 --- /dev/null +++ b/frontend/svalyn-studio-app/src/navbars/UserMenu.tsx @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import HelpIcon from '@mui/icons-material/Help'; +import HomeIcon from '@mui/icons-material/Home'; +import LogoutIcon from '@mui/icons-material/Logout'; +import MailOutlineIcon from '@mui/icons-material/MailOutline'; +import PersonIcon from '@mui/icons-material/Person'; +import SettingsIcon from '@mui/icons-material/Settings'; +import Divider from '@mui/material/Divider'; +import ListItem from '@mui/material/ListItem'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import { useState } from 'react'; +import { Navigate, Link as RouterLink } from 'react-router-dom'; +import { getCookie } from '../cookies/getCookie'; +import { UserMenuProps, UserMenuState } from './UserMenu.types'; +const { VITE_BACKEND_URL } = import.meta.env; + +export const UserMenu = ({ name, username, onClose, ...props }: UserMenuProps) => { + const [state, setState] = useState({ + redirectToLogin: false, + }); + + const handleCloseUserMenu: React.MouseEventHandler = (event) => { + if (onClose) { + onClose(event, 'backdropClick'); + } + }; + + const handleLogout: React.MouseEventHandler = () => { + const csrfToken = getCookie('XSRF-TOKEN'); + + fetch(`${VITE_BACKEND_URL}/api/logout`, { + method: 'POST', + credentials: 'include', + mode: 'cors', + headers: { + 'X-XSRF-TOKEN': csrfToken, + }, + }).then(() => { + setState((prevState) => ({ ...prevState, redirectToLogin: true })); + }); + }; + + if (state.redirectToLogin) { + return ; + } + + return ( + + + + + + + + + Dashboard + + + + + + Profile + + + + + + Invitations + + + + + + Settings + + + + + + Help + + + + + + + Sign out + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/navbars/UserMenu.types.ts b/frontend/svalyn-studio-app/src/navbars/UserMenu.types.ts new file mode 100644 index 0000000..f45ade6 --- /dev/null +++ b/frontend/svalyn-studio-app/src/navbars/UserMenu.types.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { MenuProps } from '@mui/material/Menu'; + +export interface UserMenuProps extends MenuProps { + name: string; + username: string; +} + +export interface UserMenuState { + redirectToLogin: boolean; +} diff --git a/frontend/svalyn-studio-app/src/projects/ProjectBreadcrumbs.tsx b/frontend/svalyn-studio-app/src/projects/ProjectBreadcrumbs.tsx new file mode 100644 index 0000000..c819f2c --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/ProjectBreadcrumbs.tsx @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import ClassIcon from '@mui/icons-material/Class'; +import CorporateFareIcon from '@mui/icons-material/CorporateFare'; +import DifferenceIcon from '@mui/icons-material/Difference'; +import SettingsIcon from '@mui/icons-material/Settings'; +import TagIcon from '@mui/icons-material/Tag'; +import TimelineIcon from '@mui/icons-material/Timeline'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import Typography from '@mui/material/Typography'; +import { Link as RouterLink } from 'react-router-dom'; +import { useRouteMatch } from '../hooks/useRouteMatch'; +import { useProject } from './useProject'; + +const patterns = [ + '/projects/:projectIdentifier', + '/projects/:projectIdentifier/activity', + '/projects/:projectIdentifier/changeproposals', + '/projects/:projectIdentifier/new/changeproposal', + '/projects/:projectIdentifier/tags', + '/projects/:projectIdentifier/settings', + '/projects/:projectIdentifier/changes/:changeId/resources/*', +]; + +export const ProjectBreadcrumbs = () => { + const routeMatch = useRouteMatch(patterns); + const currentTab = routeMatch?.pattern?.path; + return ( + + + + {currentTab !== '/projects/:projectIdentifier' && + currentTab !== '/projects/:projectIdentifier/changes/:changeId/resources/*' ? ( + + ) : null} + + ); +}; + +const OrganizationBreadcrumbEntry = () => { + const project = useProject(); + + return ( + theme.spacing(0.5) }} + > + + {project.organization.name} + + ); +}; + +const ProjectBreadcrumbEntry = () => { + const project = useProject(); + const routeMatch = useRouteMatch(patterns); + const currentTab = routeMatch?.pattern?.path; + + if (currentTab === '/projects/:projectIdentifier') { + return ( + theme.spacing(0.5) }} + > + + {project.name} + + ); + } + + return ( + theme.spacing(0.5) }} + > + + {project.name} + + ); +}; + +const AdditionalBreadcrumbEntry = () => { + const routeMatch = useRouteMatch(patterns); + const currentTab = routeMatch?.pattern?.path; + + let tabBreadcrumbEntry: { label: string; icon: JSX.Element } | null = null; + if (currentTab === '/projects/:projectIdentifier/activity') { + tabBreadcrumbEntry = { + label: 'Activity', + icon: , + }; + } else if (currentTab === '/projects/:projectIdentifier/changeproposals') { + tabBreadcrumbEntry = { + label: 'Change Proposals', + icon: , + }; + } else if (currentTab === '/projects/:projectIdentifier/new/changeproposal') { + tabBreadcrumbEntry = { + label: 'New Change Proposal', + icon: , + }; + } else if (currentTab === '/projects/:projectIdentifier/tags') { + tabBreadcrumbEntry = { + label: 'Tags', + icon: , + }; + } else if (currentTab === '/projects/:projectIdentifier/settings') { + tabBreadcrumbEntry = { + label: 'Settings', + icon: , + }; + } + + if (tabBreadcrumbEntry) { + const { label, icon } = tabBreadcrumbEntry; + return ( + theme.spacing(0.5) }} + > + {icon} + {label} + + ); + } + + return null; +}; diff --git a/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx b/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx index 79eedc8..048bc12 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx +++ b/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx @@ -20,15 +20,15 @@ import { gql, useQuery } from '@apollo/client'; import CorporateFareIcon from '@mui/icons-material/CorporateFare'; import Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; import { useEffect, useState } from 'react'; -import { Link as RouterLink, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { Navbar } from '../navbars/Navbar'; import { NotFoundView } from '../notfound/NotFoundView'; import { goToDomains, goToHelp, goToHome, goToNotifications, goToSettings } from '../palette/DefaultPaletteActions'; import { PaletteNavigationAction } from '../palette/Palette.types'; import { usePalette } from '../palette/usePalette'; import { ErrorSnackbar } from '../snackbar/ErrorSnackbar'; +import { ProjectBreadcrumbs } from './ProjectBreadcrumbs'; import { ProjectDrawer } from './ProjectDrawer'; import { GetProjectData, GetProjectVariables, ProjectShellProps, ProjectShellState } from './ProjectShell.types'; import { ProjectContext } from './useProject'; @@ -93,12 +93,10 @@ export const ProjectShell = ({ children }: ProjectShellProps) => { } return ( - <> + - - Domains - + { flexGrow: '1', }} > - - - {children} - + + {children} - + ); };