From 0fd7effbf5e1a65e3f48cd75e355960fad81be2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20B=C3=A9gaudeau?= Date: Wed, 19 Jul 2023 09:55:17 +0200 Subject: [PATCH 1/4] [209] Simplify the domains views by sharing common code in the router 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/DomainView.tsx | 27 +++---- .../src/domains/DomainsRouter.tsx | 25 +++++-- .../src/domains/DomainsView.tsx | 71 ++++++++----------- 3 files changed, 58 insertions(+), 65 deletions(-) diff --git a/frontend/svalyn-studio-app/src/domains/DomainView.tsx b/frontend/svalyn-studio-app/src/domains/DomainView.tsx index bef7f25..c0f3581 100644 --- a/frontend/svalyn-studio-app/src/domains/DomainView.tsx +++ b/frontend/svalyn-studio-app/src/domains/DomainView.tsx @@ -18,13 +18,10 @@ */ import { gql, useQuery } from '@apollo/client'; -import Container from '@mui/material/Container'; -import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { useParams } from 'react-router-dom'; import { DiagramEditor } from '../diagram/DiagramEditor'; import { Diagram } from '../diagram/DiagramEditor.types'; -import { Navbar } from '../navbars/Navbar'; import { GetDomainData, GetDomainVariables } from './DomainView.types'; import { convertDomain } from './converters'; @@ -71,20 +68,12 @@ export const DomainView = () => { const { data } = useQuery(getDomainQuery, { variables }); const diagram: Diagram | null = data ? convertDomain(data.viewer.domain) : null; - return ( -
- - - {data && diagram ? ( - <> - - - {data.viewer.domain.label} - - - - ) : null} - -
- ); + return data && diagram ? ( + <> + + {data.viewer.domain.label} + + + + ) : null; }; diff --git a/frontend/svalyn-studio-app/src/domains/DomainsRouter.tsx b/frontend/svalyn-studio-app/src/domains/DomainsRouter.tsx index 5dc875c..1f8caa3 100644 --- a/frontend/svalyn-studio-app/src/domains/DomainsRouter.tsx +++ b/frontend/svalyn-studio-app/src/domains/DomainsRouter.tsx @@ -17,19 +17,32 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { Outlet, Route, Routes } from 'react-router-dom'; +import Container from '@mui/material/Container'; +import Link from '@mui/material/Link'; +import Toolbar from '@mui/material/Toolbar'; +import { Route, Link as RouterLink, Routes } from 'react-router-dom'; +import { Navbar } from '../navbars/Navbar'; import { DomainView } from './DomainView'; import { DomainsView } from './DomainsView'; export const DomainsRouter = () => { return ( <> - - } /> - } /> - +
+ + + Domains + + + + - + + } /> + } /> + + +
); }; diff --git a/frontend/svalyn-studio-app/src/domains/DomainsView.tsx b/frontend/svalyn-studio-app/src/domains/DomainsView.tsx index cd0c5ad..b69ba0a 100644 --- a/frontend/svalyn-studio-app/src/domains/DomainsView.tsx +++ b/frontend/svalyn-studio-app/src/domains/DomainsView.tsx @@ -19,18 +19,15 @@ import { gql, useQuery } from '@apollo/client'; import Box from '@mui/material/Box'; -import Container from '@mui/material/Container'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemButton from '@mui/material/ListItemButton'; import ListItemText from '@mui/material/ListItemText'; import Paper from '@mui/material/Paper'; import TablePagination from '@mui/material/TablePagination'; -import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; -import { Navbar } from '../navbars/Navbar'; import { ErrorSnackbar } from '../snackbar/ErrorSnackbar'; import { Domain, DomainsViewState, GetDomainsData, GetDomainsVariables } from './DomainsView.types'; @@ -80,44 +77,38 @@ export const DomainsView = () => { return ( <> -
- - - - - Domains + + Domains + + {domains.length > 0 ? ( + + + {domains.map((domain) => { + return ( + + + + + + ); + })} + + + + ) : ( + theme.spacing(12) }}> + + No domains found - {domains.length > 0 ? ( - - - {domains.map((domain) => { - return ( - - - - - - ); - })} - - - - ) : ( - theme.spacing(12) }}> - - No domains found - - - )} - -
+ + )} ); From beaa8c2d158b47d262dcff08b06c7a495c2d4682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20B=C3=A9gaudeau?= Date: Sun, 23 Jul 2023 01:36:06 +0200 Subject: [PATCH 2/4] [209] Simplify the organization views by sharing common code in the router 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 --- frontend/svalyn-studio-app/src/app/App.tsx | 4 +- .../src/domains/DomainsRouter.tsx | 30 +-- .../src/domains/DomainsShell.tsx | 44 ++++ .../src/domains/DomainsShell.types.ts | 22 ++ .../src/organizations/OrganizationRouter.tsx | 38 ++++ .../src/organizations/OrganizationShell.tsx | 141 +++++++++++++ ...ew.types.ts => OrganizationShell.types.ts} | 9 +- .../src/organizations/OrganizationTabs.tsx | 81 ++++++++ .../organizations/OrganizationTabs.types.ts | 25 +++ .../src/organizations/OrganizationView.tsx | 78 ------- .../OrganizationViewTabPanel.tsx | 192 ------------------ ...oard.tsx => OrganizationDashboardView.tsx} | 11 +- ....ts => OrganizationDashboardView.types.ts} | 6 +- ...embers.tsx => OrganizationMembersView.tsx} | 10 +- ...es.ts => OrganizationMembersView.types.ts} | 11 +- ...tings.tsx => OrganizationSettingsView.tsx} | 13 +- ...s.ts => OrganizationSettingsView.types.ts} | 11 +- ...ationTags.tsx => OrganizationTagsView.tsx} | 14 +- ...types.ts => OrganizationTagsView.types.ts} | 11 +- ...izationsRouter.tsx => useOrganization.tsx} | 26 ++- ...anel.types.ts => useOrganization.types.ts} | 9 +- .../src/organizations/useRouteMatch.tsx | 34 ++++ 22 files changed, 450 insertions(+), 370 deletions(-) create mode 100644 frontend/svalyn-studio-app/src/domains/DomainsShell.tsx create mode 100644 frontend/svalyn-studio-app/src/domains/DomainsShell.types.ts create mode 100644 frontend/svalyn-studio-app/src/organizations/OrganizationRouter.tsx create mode 100644 frontend/svalyn-studio-app/src/organizations/OrganizationShell.tsx rename frontend/svalyn-studio-app/src/organizations/{OrganizationView.types.ts => OrganizationShell.types.ts} (88%) create mode 100644 frontend/svalyn-studio-app/src/organizations/OrganizationTabs.tsx create mode 100644 frontend/svalyn-studio-app/src/organizations/OrganizationTabs.types.ts delete mode 100644 frontend/svalyn-studio-app/src/organizations/OrganizationView.tsx delete mode 100644 frontend/svalyn-studio-app/src/organizations/OrganizationViewTabPanel.tsx rename frontend/svalyn-studio-app/src/organizations/dashboard/{OrganizationDashboard.tsx => OrganizationDashboardView.tsx} (96%) rename frontend/svalyn-studio-app/src/organizations/dashboard/{OrganizationDashboard.types.ts => OrganizationDashboardView.types.ts} (95%) rename frontend/svalyn-studio-app/src/organizations/members/{OrganizationMembers.tsx => OrganizationMembersView.tsx} (92%) rename frontend/svalyn-studio-app/src/organizations/members/{OrganizationMembers.types.ts => OrganizationMembersView.types.ts} (83%) rename frontend/svalyn-studio-app/src/organizations/settings/{OrganizationSettings.tsx => OrganizationSettingsView.tsx} (94%) rename frontend/svalyn-studio-app/src/organizations/settings/{OrganizationSettings.types.ts => OrganizationSettingsView.types.ts} (86%) rename frontend/svalyn-studio-app/src/organizations/tags/{OrganizationTags.tsx => OrganizationTagsView.tsx} (96%) rename frontend/svalyn-studio-app/src/organizations/tags/{OrganizationTags.types.ts => OrganizationTagsView.types.ts} (90%) rename frontend/svalyn-studio-app/src/organizations/{OrganizationsRouter.tsx => useOrganization.tsx} (66%) rename frontend/svalyn-studio-app/src/organizations/{OrganizationViewTabPanel.types.ts => useOrganization.types.ts} (86%) create mode 100644 frontend/svalyn-studio-app/src/organizations/useRouteMatch.tsx diff --git a/frontend/svalyn-studio-app/src/app/App.tsx b/frontend/svalyn-studio-app/src/app/App.tsx index c4ca9b0..4e63c4c 100644 --- a/frontend/svalyn-studio-app/src/app/App.tsx +++ b/frontend/svalyn-studio-app/src/app/App.tsx @@ -31,7 +31,7 @@ import { NewRouter } from '../new/NewRouter'; import { NotFoundView } from '../notfound/NotFoundView'; import { NotificationsRouter } from '../notifications/NotificationsRouter'; import { OAuth2Router } from '../oauth2/OAuth2Router'; -import { OrganizationsRouter } from '../organizations/OrganizationsRouter'; +import { OrganizationRouter } from '../organizations/OrganizationRouter'; import { PaletteProvider } from '../palette/PaletteProvider'; import { ProfilesRouter } from '../profiles/ProfilesRouter'; import { ProjectsRouter } from '../projects/ProjectsRouter'; @@ -50,7 +50,7 @@ export const App = () => { } /> } /> - } /> + } /> } /> } /> } /> diff --git a/frontend/svalyn-studio-app/src/domains/DomainsRouter.tsx b/frontend/svalyn-studio-app/src/domains/DomainsRouter.tsx index 1f8caa3..b119507 100644 --- a/frontend/svalyn-studio-app/src/domains/DomainsRouter.tsx +++ b/frontend/svalyn-studio-app/src/domains/DomainsRouter.tsx @@ -17,32 +17,18 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import Container from '@mui/material/Container'; -import Link from '@mui/material/Link'; -import Toolbar from '@mui/material/Toolbar'; -import { Route, Link as RouterLink, Routes } from 'react-router-dom'; -import { Navbar } from '../navbars/Navbar'; +import { Route, Routes } from 'react-router-dom'; import { DomainView } from './DomainView'; +import { DomainsShell } from './DomainsShell'; import { DomainsView } from './DomainsView'; export const DomainsRouter = () => { return ( - <> -
- - - Domains - - - - - - - } /> - } /> - - -
- + + + } /> + } /> + + ); }; diff --git a/frontend/svalyn-studio-app/src/domains/DomainsShell.tsx b/frontend/svalyn-studio-app/src/domains/DomainsShell.tsx new file mode 100644 index 0000000..c74c1a6 --- /dev/null +++ b/frontend/svalyn-studio-app/src/domains/DomainsShell.tsx @@ -0,0 +1,44 @@ +/* + * 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 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'; + +export const DomainsShell = ({ children }: DomainsShellProps) => { + return ( + <> +
+ + + Domains + + + + + + {children} + +
+ + ); +}; diff --git a/frontend/svalyn-studio-app/src/domains/DomainsShell.types.ts b/frontend/svalyn-studio-app/src/domains/DomainsShell.types.ts new file mode 100644 index 0000000..c6651e5 --- /dev/null +++ b/frontend/svalyn-studio-app/src/domains/DomainsShell.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 DomainsShellProps { + children?: React.ReactNode; +} diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationRouter.tsx b/frontend/svalyn-studio-app/src/organizations/OrganizationRouter.tsx new file mode 100644 index 0000000..0bf623b --- /dev/null +++ b/frontend/svalyn-studio-app/src/organizations/OrganizationRouter.tsx @@ -0,0 +1,38 @@ +/* + * 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 { Route, Routes } from 'react-router-dom'; +import { OrganizationShell } from './OrganizationShell'; +import { OrganizationDashboardView } from './dashboard/OrganizationDashboardView'; +import { OrganizationMembersView } from './members/OrganizationMembersView'; +import { OrganizationSettingsView } from './settings/OrganizationSettingsView'; +import { OrganizationTagsView } from './tags/OrganizationTagsView'; + +export const OrganizationRouter = () => { + return ( + + + } /> + } /> + } /> + } /> + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationShell.tsx b/frontend/svalyn-studio-app/src/organizations/OrganizationShell.tsx new file mode 100644 index 0000000..2e1b04a --- /dev/null +++ b/frontend/svalyn-studio-app/src/organizations/OrganizationShell.tsx @@ -0,0 +1,141 @@ +/* + * 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 { gql, useQuery } from '@apollo/client'; +import ClassIcon from '@mui/icons-material/Class'; +import CorporateFareIcon from '@mui/icons-material/CorporateFare'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import { useEffect, useState } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; +import { Navbar } from '../navbars/Navbar'; +import { NotFoundView } from '../notfound/NotFoundView'; +import { ErrorSnackbar } from '../snackbar/ErrorSnackbar'; +import { NewProjectDialog } from './NewProjectDialog'; +import { OrganizationPicker } from './OrganizationPicker'; +import { + GetOrganizationData, + GetOrganizationVariables, + OrganizationShellProps, + OrganizationShellState, +} from './OrganizationShell.types'; +import { OrganizationTabs } from './OrganizationTabs'; +import { OrganizationContext } from './useOrganization'; + +const getOrganizationQuery = gql` + query getOrganization($identifier: ID!) { + viewer { + organization(identifier: $identifier) { + identifier + name + role + } + } + } +`; + +export const OrganizationShell = ({ children }: OrganizationShellProps) => { + const [state, setState] = useState({ + newProjectDialogOpen: false, + errorSnackbarOpen: false, + }); + + const { organizationIdentifier } = useParams(); + const variables: GetOrganizationVariables = { identifier: organizationIdentifier ?? '' }; + const { data, error, refetch } = useQuery(getOrganizationQuery, { + variables, + }); + useEffect(() => setState((prevState) => ({ ...prevState, errorSnackbarOpen: !!error })), [error]); + + const location = useLocation(); + useEffect(() => { + if (data) { + refetch(variables); + } + }, [location.pathname]); + + const openNewProjectDialog = () => setState((prevState) => ({ ...prevState, newProjectDialogOpen: true })); + const closeNewProjectDialog = () => setState((prevState) => ({ ...prevState, newProjectDialogOpen: false })); + const handleCloseSnackbar = () => setState((prevState) => ({ ...prevState, errorSnackbarOpen: false })); + + if (!data) { + return null; + } + if (!data.viewer.organization) { + return ; + } + + return ( + <> +
+ + + + `1px solid ${theme.palette.divider}`, + }} + > + theme.spacing(2), + }} + > + + + {data.viewer.organization.name} + + + + + + + + {children} + +
+ {state.newProjectDialogOpen ? ( + + ) : null} + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationView.types.ts b/frontend/svalyn-studio-app/src/organizations/OrganizationShell.types.ts similarity index 88% rename from frontend/svalyn-studio-app/src/organizations/OrganizationView.types.ts rename to frontend/svalyn-studio-app/src/organizations/OrganizationShell.types.ts index 6f39701..3a5cb01 100644 --- a/frontend/svalyn-studio-app/src/organizations/OrganizationView.types.ts +++ b/frontend/svalyn-studio-app/src/organizations/OrganizationShell.types.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Stéphane Bégaudeau. + * 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, @@ -17,7 +17,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface OrganizationViewState { +export interface OrganizationShellProps { + children?: React.ReactNode; +} + +export interface OrganizationShellState { + newProjectDialogOpen: boolean; errorSnackbarOpen: boolean; } diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.tsx b/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.tsx new file mode 100644 index 0000000..3c109d9 --- /dev/null +++ b/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.tsx @@ -0,0 +1,81 @@ +/* + * 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 HomeIcon from '@mui/icons-material/Home'; +import PersonIcon from '@mui/icons-material/Person'; +import SettingsIcon from '@mui/icons-material/Settings'; +import TagIcon from '@mui/icons-material/Tag'; +import Tab from '@mui/material/Tab'; +import Tabs from '@mui/material/Tabs'; +import { Link as RouterLink } from 'react-router-dom'; +import { OrganizationTabsProps } from './OrganizationTabs.types'; +import { useRouteMatch } from './useRouteMatch'; + +const patterns = [ + '/orgs/:organizationId', + '/orgs/:organizationId/tags', + '/orgs/:organizationId/members', + '/orgs/:organizationId/settings', +]; + +export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTabsProps) => { + const routeMatch = useRouteMatch(patterns); + const currentTab = routeMatch?.pattern?.path; + + return ( + + } + value="/orgs/:organizationId" + to={`/orgs/${organizationIdentifier}`} + component={RouterLink} + iconPosition="start" + sx={{ minHeight: 0 }} + /> + } + value="/orgs/:organizationId/tags" + to={`/orgs/${organizationIdentifier}/tags`} + component={RouterLink} + iconPosition="start" + sx={{ minHeight: 0 }} + /> + } + value="/orgs/:organizationId/members" + to={`/orgs/${organizationIdentifier}/members`} + component={RouterLink} + iconPosition="start" + sx={{ minHeight: 0 }} + /> + } + value="/orgs/:organizationId/settings" + to={`/orgs/${organizationIdentifier}/settings`} + component={RouterLink} + iconPosition="start" + sx={{ minHeight: 0 }} + /> + + ); +}; diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.types.ts b/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.types.ts new file mode 100644 index 0000000..b2ea9bd --- /dev/null +++ b/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.types.ts @@ -0,0 +1,25 @@ +/* + * 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 { SxProps, Theme } from '@mui/material/styles'; + +export interface OrganizationTabsProps { + organizationIdentifier: string; + sx?: SxProps | undefined; +} diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationView.tsx b/frontend/svalyn-studio-app/src/organizations/OrganizationView.tsx deleted file mode 100644 index c352aeb..0000000 --- a/frontend/svalyn-studio-app/src/organizations/OrganizationView.tsx +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2022 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 { gql, useQuery } from '@apollo/client'; -import { useEffect, useState } from 'react'; -import { useLocation, useParams } from 'react-router-dom'; -import { Navbar } from '../navbars/Navbar'; -import { NotFoundView } from '../notfound/NotFoundView'; -import { ErrorSnackbar } from '../snackbar/ErrorSnackbar'; -import { OrganizationPicker } from './OrganizationPicker'; -import { GetOrganizationData, GetOrganizationVariables, OrganizationViewState } from './OrganizationView.types'; -import { OrganizationViewTabPanel } from './OrganizationViewTabPanel'; - -const getOrganizationQuery = gql` - query getOrganization($identifier: ID!) { - viewer { - organization(identifier: $identifier) { - identifier - name - role - } - } - } -`; - -export const OrganizationView = () => { - const [state, setState] = useState({ - errorSnackbarOpen: false, - }); - - const { organizationIdentifier } = useParams(); - const variables: GetOrganizationVariables = { identifier: organizationIdentifier ?? '' }; - const { data, error, refetch } = useQuery(getOrganizationQuery, { - variables, - }); - useEffect(() => setState((prevState) => ({ ...prevState, errorSnackbarOpen: !!error })), [error]); - - const location = useLocation(); - useEffect(() => { - if (data) { - refetch(variables); - } - }, [location.pathname]); - - const handleCloseSnackbar = () => setState((prevState) => ({ ...prevState, errorSnackbarOpen: false })); - - if (data && !data.viewer.organization) { - return ; - } - - return ( - <> -
- - {data?.viewer.organization ? : null} - - {data?.viewer.organization ? : null} -
- - - ); -}; diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationViewTabPanel.tsx b/frontend/svalyn-studio-app/src/organizations/OrganizationViewTabPanel.tsx deleted file mode 100644 index d6f3c59..0000000 --- a/frontend/svalyn-studio-app/src/organizations/OrganizationViewTabPanel.tsx +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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, - * 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 HomeIcon from '@mui/icons-material/Home'; -import PersonIcon from '@mui/icons-material/Person'; -import SettingsIcon from '@mui/icons-material/Settings'; -import TagIcon from '@mui/icons-material/Tag'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Tab from '@mui/material/Tab'; -import Tabs from '@mui/material/Tabs'; -import Toolbar from '@mui/material/Toolbar'; -import Typography from '@mui/material/Typography'; -import React, { useEffect, useState } from 'react'; -import { generatePath, matchPath, useLocation, useNavigate } from 'react-router-dom'; -import { - goToDomains, - goToHelp, - goToHome, - goToNewOrganization, - goToNotifications, - goToSettings, -} from '../palette/DefaultPaletteActions'; -import { PaletteSimpleAction } from '../palette/Palette.types'; -import { usePalette } from '../palette/usePalette'; -import { NewProjectDialog } from './NewProjectDialog'; -import { OrganizationViewTabPanelProps, OrganizationViewTabPanelState } from './OrganizationViewTabPanel.types'; -import { OrganizationDashboard } from './dashboard/OrganizationDashboard'; -import { OrganizationMembers } from './members/OrganizationMembers'; -import { OrganizationSettings } from './settings/OrganizationSettings'; -import { OrganizationTags } from './tags/OrganizationTags'; - -const a11yProps = (index: number) => { - return { - id: `tab-${index}`, - 'aria-controls': `tabpanel-${index}`, - }; -}; - -export const OrganizationViewTabPanel = ({ organization }: OrganizationViewTabPanelProps) => { - const location = useLocation(); - const dashboardMatch = matchPath('/orgs/:organizationId', location.pathname); - const tagsMatch = matchPath('/orgs/:organizationId/tags', location.pathname); - const membersMatch = matchPath('/orgs/:organizationId/members', location.pathname); - const settingsMatch = matchPath('/orgs/:organizationId/settings', location.pathname); - let activeTab = 0; - if (tagsMatch) { - activeTab = 1; - } else if (membersMatch) { - activeTab = 2; - } else if (settingsMatch) { - activeTab = 3; - } - - const [state, setState] = useState({ - activeTab: activeTab, - newProjectDialogOpen: false, - }); - - useEffect(() => { - const dashboardMatch = matchPath('/orgs/:organizationId', location.pathname); - const tagsMatch = matchPath('/orgs/:organizationId/tags', location.pathname); - const membersMatch = matchPath('/orgs/:organizationId/members', location.pathname); - const settingsMatch = matchPath('/orgs/:organizationId/settings', location.pathname); - if (dashboardMatch && state.activeTab !== 0) { - setState((prevState) => ({ ...prevState, activeTab: 0 })); - } else if (tagsMatch && state.activeTab !== 1) { - setState((prevState) => ({ ...prevState, activeTab: 1 })); - } else if (membersMatch && state.activeTab !== 2) { - setState((prevState) => ({ ...prevState, activeTab: 2 })); - } else if (settingsMatch && state.activeTab !== 3) { - setState((prevState) => ({ ...prevState, activeTab: 3 })); - } - }, [location]); - - const navigate = useNavigate(); - useEffect(() => { - if (state.activeTab === 0 && !dashboardMatch) { - const path = generatePath('/orgs/:identifier', { identifier: organization.identifier }); - navigate(path); - } else if (state.activeTab === 1 && !tagsMatch) { - const path = generatePath('/orgs/:identifier/tags', { identifier: organization.identifier }); - navigate(path); - } else if (state.activeTab === 2 && !membersMatch) { - const path = generatePath('/orgs/:identifier/members', { identifier: organization.identifier }); - navigate(path); - } else if (state.activeTab === 3 && !settingsMatch) { - const path = generatePath('/orgs/:identifier/settings', { identifier: organization.identifier }); - navigate(path); - } - }, [state.activeTab]); - - const handleTabChanged = (_: React.SyntheticEvent, newValue: number) => - setState((prevState) => ({ ...prevState, activeTab: newValue })); - const openNewProjectDialog: React.MouseEventHandler = () => - setState((prevState) => ({ ...prevState, newProjectDialogOpen: true })); - const closeNewProjectDialog = () => setState((prevState) => ({ ...prevState, newProjectDialogOpen: false })); - - const goToNewProject: PaletteSimpleAction = { - type: 'simple-action', - id: 'create-project', - icon: , - label: 'New project', - handle: () => setState((prevState) => ({ ...prevState, newProjectDialogOpen: true })), - }; - usePalette([goToHome, goToNewProject, goToNewOrganization, goToDomains, goToNotifications, goToSettings, goToHelp]); - - return ( - <> -
- `1px solid ${theme.palette.divider}`, - }} - > - theme.spacing(2) }}> - - - {organization.name} - - - - } iconPosition="start" {...a11yProps(0)} sx={{ minHeight: 0 }} /> - } iconPosition="start" {...a11yProps(1)} sx={{ minHeight: 0 }} /> - } iconPosition="start" {...a11yProps(2)} sx={{ minHeight: 0 }} /> - } - iconPosition="start" - {...a11yProps(3)} - sx={{ minHeight: 0 }} - /> - - - - - - - -
- {state.newProjectDialogOpen ? ( - - ) : null} - - ); -}; diff --git a/frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboard.tsx b/frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboardView.tsx similarity index 96% rename from frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboard.tsx rename to frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboardView.tsx index c004233..2009647 100644 --- a/frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboard.tsx +++ b/frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboardView.tsx @@ -30,14 +30,14 @@ import { ActivityTimeline } from '../../activity/ActivityTimeline'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; import { CreatedOn } from '../../widgets/CreatedOn'; import { LastModifiedOn } from '../../widgets/LastModifiedOn'; +import { useOrganization } from '../useOrganization'; import { ActivityAreaProps, GetOrganizationDashboardData, GetOrganizationDashboardVariables, - OrganizationDashboardProps, - OrganizationDashboardState, + OrganizationDashboardViewState, ProjectsAreaProps, -} from './OrganizationDashboard.types'; +} from './OrganizationDashboardView.types'; import { ProjectCard } from './ProjectCard'; const getOrganizationDashboardQuery = gql` @@ -93,8 +93,9 @@ const Main = styled(Box)(({ theme }) => ({ padding: theme.spacing(3), })); -export const OrganizationDashboard = ({ organizationIdentifier }: OrganizationDashboardProps) => { - const [state, setState] = useState({ +export const OrganizationDashboardView = () => { + const { identifier: organizationIdentifier } = useOrganization(); + const [state, setState] = useState({ organization: null, page: 0, rowsPerPage: 10, diff --git a/frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboard.types.ts b/frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboardView.types.ts similarity index 95% rename from frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboard.types.ts rename to frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboardView.types.ts index cb4a336..69cd159 100644 --- a/frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboard.types.ts +++ b/frontend/svalyn-studio-app/src/organizations/dashboard/OrganizationDashboardView.types.ts @@ -17,10 +17,6 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface OrganizationDashboardProps { - organizationIdentifier: string; -} - export interface ProjectsAreaProps { projects: Project[]; pageInfo: PageInfo; @@ -33,7 +29,7 @@ export interface ActivityAreaProps { activityEntries: ActivityEntry[]; } -export interface OrganizationDashboardState { +export interface OrganizationDashboardViewState { organization: Organization | null; page: number; rowsPerPage: number; diff --git a/frontend/svalyn-studio-app/src/organizations/members/OrganizationMembers.tsx b/frontend/svalyn-studio-app/src/organizations/members/OrganizationMembersView.tsx similarity index 92% rename from frontend/svalyn-studio-app/src/organizations/members/OrganizationMembers.tsx rename to frontend/svalyn-studio-app/src/organizations/members/OrganizationMembersView.tsx index ddd9bf8..e5bfaaa 100644 --- a/frontend/svalyn-studio-app/src/organizations/members/OrganizationMembers.tsx +++ b/frontend/svalyn-studio-app/src/organizations/members/OrganizationMembersView.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, @@ -27,14 +27,16 @@ import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; import ListItemText from '@mui/material/ListItemText'; import { useState } from 'react'; +import { useOrganization } from '../useOrganization'; import { Invitations } from './Invitations'; import { InviteMemberDialog } from './InviteMemberDialog'; import { LeaveOrganizationDialog } from './LeaveOrganizationDialog'; import { Memberships } from './Memberships'; -import { OrganizationMembersProps, OrganizationMembersState, OrganizationMemberTab } from './OrganizationMembers.types'; +import { OrganizationMembersViewState, OrganizationMemberTab } from './OrganizationMembersView.types'; -export const OrganizationMembers = ({ organizationIdentifier, role }: OrganizationMembersProps) => { - const [state, setState] = useState({ +export const OrganizationMembersView = () => { + const { identifier: organizationIdentifier, role } = useOrganization(); + const [state, setState] = useState({ tab: 'Memberships', inviteMemberDialogOpen: false, leaveOrganizationDialogOpen: false, diff --git a/frontend/svalyn-studio-app/src/organizations/members/OrganizationMembers.types.ts b/frontend/svalyn-studio-app/src/organizations/members/OrganizationMembersView.types.ts similarity index 83% rename from frontend/svalyn-studio-app/src/organizations/members/OrganizationMembers.types.ts rename to frontend/svalyn-studio-app/src/organizations/members/OrganizationMembersView.types.ts index 1c8a6c1..555d2bb 100644 --- a/frontend/svalyn-studio-app/src/organizations/members/OrganizationMembers.types.ts +++ b/frontend/svalyn-studio-app/src/organizations/members/OrganizationMembersView.types.ts @@ -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, @@ -17,14 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface OrganizationMembersProps { - organizationIdentifier: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface OrganizationMembersState { +export interface OrganizationMembersViewState { tab: OrganizationMemberTab; inviteMemberDialogOpen: boolean; leaveOrganizationDialogOpen: boolean; diff --git a/frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettings.tsx b/frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettingsView.tsx similarity index 94% rename from frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettings.tsx rename to frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettingsView.tsx index 7874e29..2386cf3 100644 --- a/frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettings.tsx +++ b/frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettingsView.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, @@ -27,14 +27,14 @@ import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; +import { useOrganization } from '../useOrganization'; import { DeleteOrganizationDialog } from './DeleteOrganizationDialog'; import { ErrorPayload, - OrganizationSettingsProps, - OrganizationSettingsState, + OrganizationSettingsViewState, UpdateOrganizationNameData, UpdateOrganizationNameVariables, -} from './OrganizationSettings.types'; +} from './OrganizationSettingsView.types'; const updateOrganizationNameMutation = gql` mutation updateOrganizationName($input: UpdateOrganizationNameInput!) { @@ -46,8 +46,9 @@ const updateOrganizationNameMutation = gql` } `; -export const OrganizationSettings = ({ organizationIdentifier, role }: OrganizationSettingsProps) => { - const [state, setState] = useState({ +export const OrganizationSettingsView = () => { + const { identifier: organizationIdentifier, role } = useOrganization(); + const [state, setState] = useState({ name: '', message: null, deleteOrganizationDialogOpen: false, diff --git a/frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettings.types.ts b/frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettingsView.types.ts similarity index 86% rename from frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettings.types.ts rename to frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettingsView.types.ts index ef021d3..dbba057 100644 --- a/frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettings.types.ts +++ b/frontend/svalyn-studio-app/src/organizations/settings/OrganizationSettingsView.types.ts @@ -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, @@ -17,14 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface OrganizationSettingsProps { - organizationIdentifier: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface OrganizationSettingsState { +export interface OrganizationSettingsViewState { name: string; message: string | null; deleteOrganizationDialogOpen: boolean; diff --git a/frontend/svalyn-studio-app/src/organizations/tags/OrganizationTags.tsx b/frontend/svalyn-studio-app/src/organizations/tags/OrganizationTagsView.tsx similarity index 96% rename from frontend/svalyn-studio-app/src/organizations/tags/OrganizationTags.tsx rename to frontend/svalyn-studio-app/src/organizations/tags/OrganizationTagsView.tsx index a30df0d..91e5866 100644 --- a/frontend/svalyn-studio-app/src/organizations/tags/OrganizationTags.tsx +++ b/frontend/svalyn-studio-app/src/organizations/tags/OrganizationTagsView.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, @@ -34,15 +34,15 @@ import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; +import { useOrganization } from '../useOrganization'; import { AddTagToOrganizationData, AddTagToOrganizationVariables, ErrorPayload, GetOrganizationTagsData, GetOrganizationTagsVariables, - OrganizationTagsProps, - OrganizationTagsState, -} from './OrganizationTags.types'; + OrganizationTagsViewState, +} from './OrganizationTagsView.types'; const getOrganizationTagsQuery = gql` query getOrganizationTags($identifier: ID!, $page: Int!, $rowsPerPage: Int!) { @@ -76,8 +76,9 @@ const addTagToOrganizationMutation = gql` } `; -export const OrganizationTags = ({ organizationIdentifier, role }: OrganizationTagsProps) => { - const [state, setState] = useState({ +export const OrganizationTagsView = () => { + const { identifier: organizationIdentifier, role } = useOrganization(); + const [state, setState] = useState({ key: '', value: '', page: 0, @@ -153,6 +154,7 @@ export const OrganizationTags = ({ organizationIdentifier, role }: OrganizationT const handleCloseSnackbar = () => setState((prevState) => ({ ...prevState, message: null })); const isValidNewTag = state.key.trim().length > 0 && state.value.trim().length > 0; + return ( <> { - return ( - <> - - } /> - } /> - } /> - } /> - +export const OrganizationContext = React.createContext({ + organization: { + identifier: '', + name: '', + role: 'NONE', + }, +}); - - - ); +export const useOrganization = (): Organization => { + const { organization } = useContext(OrganizationContext); + return organization; }; diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationViewTabPanel.types.ts b/frontend/svalyn-studio-app/src/organizations/useOrganization.types.ts similarity index 86% rename from frontend/svalyn-studio-app/src/organizations/OrganizationViewTabPanel.types.ts rename to frontend/svalyn-studio-app/src/organizations/useOrganization.types.ts index b0fed3d..3000373 100644 --- a/frontend/svalyn-studio-app/src/organizations/OrganizationViewTabPanel.types.ts +++ b/frontend/svalyn-studio-app/src/organizations/useOrganization.types.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Stéphane Bégaudeau. + * 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, @@ -17,7 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface OrganizationViewTabPanelProps { +export interface OrganizationContextValue { organization: Organization; } @@ -28,8 +28,3 @@ export interface Organization { } export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface OrganizationViewTabPanelState { - activeTab: number; - newProjectDialogOpen: boolean; -} diff --git a/frontend/svalyn-studio-app/src/organizations/useRouteMatch.tsx b/frontend/svalyn-studio-app/src/organizations/useRouteMatch.tsx new file mode 100644 index 0000000..f10c9a1 --- /dev/null +++ b/frontend/svalyn-studio-app/src/organizations/useRouteMatch.tsx @@ -0,0 +1,34 @@ +/* + * 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 { PathMatch, matchPath, useLocation } from 'react-router-dom'; + +export const useRouteMatch = (patterns: readonly string[]): PathMatch | null => { + const { pathname } = useLocation(); + + for (let index = 0; index < patterns.length; index += 1) { + const pattern = patterns[index]; + const pathMatch = matchPath(pattern, pathname); + if (pathMatch) { + return pathMatch; + } + } + + return null; +}; From c5e407a5d7936b3ee339f739640587b15498a479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20B=C3=A9gaudeau?= Date: Sun, 23 Jul 2023 02:35:34 +0200 Subject: [PATCH 3/4] [209] Simplify the project views by sharing common code in the router 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 --- frontend/svalyn-studio-app/src/app/App.tsx | 6 +- .../useRouteMatch.tsx | 0 .../src/organizations/OrganizationShell.tsx | 11 +- .../src/organizations/OrganizationTabs.tsx | 18 +- .../src/projects/ProjectDrawer.tsx | 33 +++- .../{ProjectsRouter.tsx => ProjectRouter.tsx} | 33 ++-- .../src/projects/ProjectShell.tsx | 120 ++++++++++++ ...ectView.types.ts => ProjectShell.types.ts} | 12 +- .../src/projects/ProjectView.tsx | 173 ------------------ ...ctActivity.tsx => ProjectActivityView.tsx} | 14 +- ....types.ts => ProjectActivityView.types.ts} | 6 +- ...sal.tsx => ProjectChangeProposalsView.tsx} | 14 +- ...ts => ProjectChangeProposalsView.types.ts} | 11 +- .../{ProjectHome.tsx => ProjectHomeView.tsx} | 13 +- ...Home.types.ts => ProjectHomeView.types.ts} | 9 +- .../NewChangeProposalView.tsx | 124 ++++++------- .../src/projects/resource/ResourceView.tsx | 62 +++---- ...ctSettings.tsx => ProjectSettingsView.tsx} | 14 +- ....types.ts => ProjectSettingsView.types.ts} | 11 +- .../{ProjectTags.tsx => ProjectTagsView.tsx} | 14 +- ...Tags.types.ts => ProjectTagsView.types.ts} | 9 +- .../src/projects/useProject.tsx | 39 ++++ ...ectDrawer.types.ts => useProject.types.ts} | 22 ++- .../workspace/WorkspaceView.tsx | 2 +- .../workspace/WorkspaceView.types.ts | 0 25 files changed, 381 insertions(+), 389 deletions(-) rename frontend/svalyn-studio-app/src/{organizations => hooks}/useRouteMatch.tsx (100%) rename frontend/svalyn-studio-app/src/projects/{ProjectsRouter.tsx => ProjectRouter.tsx} (56%) create mode 100644 frontend/svalyn-studio-app/src/projects/ProjectShell.tsx rename frontend/svalyn-studio-app/src/projects/{ProjectView.types.ts => ProjectShell.types.ts} (87%) delete mode 100644 frontend/svalyn-studio-app/src/projects/ProjectView.tsx rename frontend/svalyn-studio-app/src/projects/activity/{ProjectActivity.tsx => ProjectActivityView.tsx} (92%) rename frontend/svalyn-studio-app/src/projects/activity/{ProjectActivity.types.ts => ProjectActivityView.types.ts} (94%) rename frontend/svalyn-studio-app/src/projects/changeproposals/{ProjectChangeProposal.tsx => ProjectChangeProposalsView.tsx} (96%) rename frontend/svalyn-studio-app/src/projects/changeproposals/{ProjectChangeProposal.types.ts => ProjectChangeProposalsView.types.ts} (90%) rename frontend/svalyn-studio-app/src/projects/home/{ProjectHome.tsx => ProjectHomeView.tsx} (94%) rename frontend/svalyn-studio-app/src/projects/home/{ProjectHome.types.ts => ProjectHomeView.types.ts} (92%) rename frontend/svalyn-studio-app/src/projects/settings/{ProjectSettings.tsx => ProjectSettingsView.tsx} (96%) rename frontend/svalyn-studio-app/src/projects/settings/{ProjectSettings.types.ts => ProjectSettingsView.types.ts} (89%) rename frontend/svalyn-studio-app/src/projects/tags/{ProjectTags.tsx => ProjectTagsView.tsx} (96%) rename frontend/svalyn-studio-app/src/projects/tags/{ProjectTags.types.ts => ProjectTagsView.types.ts} (92%) create mode 100644 frontend/svalyn-studio-app/src/projects/useProject.tsx rename frontend/svalyn-studio-app/src/projects/{ProjectDrawer.types.ts => useProject.types.ts} (73%) rename frontend/svalyn-studio-app/src/{projects => }/workspace/WorkspaceView.tsx (96%) rename frontend/svalyn-studio-app/src/{projects => }/workspace/WorkspaceView.types.ts (100%) diff --git a/frontend/svalyn-studio-app/src/app/App.tsx b/frontend/svalyn-studio-app/src/app/App.tsx index 4e63c4c..54e7d79 100644 --- a/frontend/svalyn-studio-app/src/app/App.tsx +++ b/frontend/svalyn-studio-app/src/app/App.tsx @@ -34,9 +34,10 @@ import { OAuth2Router } from '../oauth2/OAuth2Router'; import { OrganizationRouter } from '../organizations/OrganizationRouter'; import { PaletteProvider } from '../palette/PaletteProvider'; import { ProfilesRouter } from '../profiles/ProfilesRouter'; -import { ProjectsRouter } from '../projects/ProjectsRouter'; +import { ProjectRouter } from '../projects/ProjectRouter'; import { SearchRouter } from '../search/SearchRouter'; import { SettingsRouter } from '../settings/SettingsRouter'; +import { WorkspaceView } from '../workspace/WorkspaceView'; import { AuthenticationRedirectionBoundary } from './AuthenticationRedirectionBoundary'; import { theme } from './theme'; @@ -51,7 +52,8 @@ export const App = () => { } /> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/frontend/svalyn-studio-app/src/organizations/useRouteMatch.tsx b/frontend/svalyn-studio-app/src/hooks/useRouteMatch.tsx similarity index 100% rename from frontend/svalyn-studio-app/src/organizations/useRouteMatch.tsx rename to frontend/svalyn-studio-app/src/hooks/useRouteMatch.tsx diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationShell.tsx b/frontend/svalyn-studio-app/src/organizations/OrganizationShell.tsx index 2e1b04a..9c4951b 100644 --- a/frontend/svalyn-studio-app/src/organizations/OrganizationShell.tsx +++ b/frontend/svalyn-studio-app/src/organizations/OrganizationShell.tsx @@ -25,7 +25,7 @@ import Button from '@mui/material/Button'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; -import { useLocation, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { Navbar } from '../navbars/Navbar'; import { NotFoundView } from '../notfound/NotFoundView'; import { ErrorSnackbar } from '../snackbar/ErrorSnackbar'; @@ -60,18 +60,11 @@ export const OrganizationShell = ({ children }: OrganizationShellProps) => { const { organizationIdentifier } = useParams(); const variables: GetOrganizationVariables = { identifier: organizationIdentifier ?? '' }; - const { data, error, refetch } = useQuery(getOrganizationQuery, { + const { data, error } = useQuery(getOrganizationQuery, { variables, }); useEffect(() => setState((prevState) => ({ ...prevState, errorSnackbarOpen: !!error })), [error]); - const location = useLocation(); - useEffect(() => { - if (data) { - refetch(variables); - } - }, [location.pathname]); - const openNewProjectDialog = () => setState((prevState) => ({ ...prevState, newProjectDialogOpen: true })); const closeNewProjectDialog = () => setState((prevState) => ({ ...prevState, newProjectDialogOpen: false })); const handleCloseSnackbar = () => setState((prevState) => ({ ...prevState, errorSnackbarOpen: false })); diff --git a/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.tsx b/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.tsx index 3c109d9..297d1b3 100644 --- a/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.tsx +++ b/frontend/svalyn-studio-app/src/organizations/OrganizationTabs.tsx @@ -24,14 +24,14 @@ import TagIcon from '@mui/icons-material/Tag'; import Tab from '@mui/material/Tab'; import Tabs from '@mui/material/Tabs'; import { Link as RouterLink } from 'react-router-dom'; +import { useRouteMatch } from '../hooks/useRouteMatch'; import { OrganizationTabsProps } from './OrganizationTabs.types'; -import { useRouteMatch } from './useRouteMatch'; const patterns = [ - '/orgs/:organizationId', - '/orgs/:organizationId/tags', - '/orgs/:organizationId/members', - '/orgs/:organizationId/settings', + '/orgs/:organizationIdentifier', + '/orgs/:organizationIdentifier/tags', + '/orgs/:organizationIdentifier/members', + '/orgs/:organizationIdentifier/settings', ]; export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTabsProps) => { @@ -43,7 +43,7 @@ export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTab } - value="/orgs/:organizationId" + value="/orgs/:organizationIdentifier" to={`/orgs/${organizationIdentifier}`} component={RouterLink} iconPosition="start" @@ -52,7 +52,7 @@ export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTab } - value="/orgs/:organizationId/tags" + value="/orgs/:organizationIdentifier/tags" to={`/orgs/${organizationIdentifier}/tags`} component={RouterLink} iconPosition="start" @@ -61,7 +61,7 @@ export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTab } - value="/orgs/:organizationId/members" + value="/orgs/:organizationIdentifier/members" to={`/orgs/${organizationIdentifier}/members`} component={RouterLink} iconPosition="start" @@ -70,7 +70,7 @@ export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTab } - value="/orgs/:organizationId/settings" + value="/orgs/:organizationIdentifier/settings" to={`/orgs/${organizationIdentifier}/settings`} component={RouterLink} iconPosition="start" diff --git a/frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx b/frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx index 0997370..a16de30 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx +++ b/frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx @@ -28,7 +28,8 @@ import ListItemButton from '@mui/material/ListItemButton'; import ListItemIcon from '@mui/material/ListItemIcon'; import { styled } from '@mui/material/styles'; import { Link as RouterLink } from 'react-router-dom'; -import { ProjectDrawerProps } from './ProjectDrawer.types'; +import { useRouteMatch } from '../hooks/useRouteMatch'; +import { useProject } from './useProject'; const CompactDrawer = styled('div')(({ theme }) => ({ width: '64px', @@ -36,14 +37,28 @@ const CompactDrawer = styled('div')(({ theme }) => ({ borderRight: `1px solid ${theme.palette.divider}`, })); -export const ProjectDrawer = ({ projectIdentifier, selectedPanel }: ProjectDrawerProps) => { +const patterns = [ + '/projects/:projectIdentifier', + '/projects/:projectIdentifier/activity', + '/projects/:projectIdentifier/changeproposals', + '/projects/:projectIdentifier/tags', + '/projects/:projectIdentifier/settings', +]; + +export const ProjectDrawer = () => { + const { identifier: projectIdentifier } = useProject(); + + const routeMatch = useRouteMatch(patterns); + const currentTab = routeMatch?.pattern?.path; + return ( - + @@ -51,10 +66,11 @@ export const ProjectDrawer = ({ projectIdentifier, selectedPanel }: ProjectDrawe - + @@ -62,10 +78,11 @@ export const ProjectDrawer = ({ projectIdentifier, selectedPanel }: ProjectDrawe - + @@ -73,10 +90,11 @@ export const ProjectDrawer = ({ projectIdentifier, selectedPanel }: ProjectDrawe - + @@ -84,10 +102,11 @@ export const ProjectDrawer = ({ projectIdentifier, selectedPanel }: ProjectDrawe - + diff --git a/frontend/svalyn-studio-app/src/projects/ProjectsRouter.tsx b/frontend/svalyn-studio-app/src/projects/ProjectRouter.tsx similarity index 56% rename from frontend/svalyn-studio-app/src/projects/ProjectsRouter.tsx rename to frontend/svalyn-studio-app/src/projects/ProjectRouter.tsx index 077f11f..d782a0f 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectsRouter.tsx +++ b/frontend/svalyn-studio-app/src/projects/ProjectRouter.tsx @@ -17,27 +17,28 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { Outlet, Route, Routes } from 'react-router-dom'; -import { ProjectView } from './ProjectView'; +import { Route, Routes } from 'react-router-dom'; +import { ProjectShell } from './ProjectShell'; +import { ProjectActivityView } from './activity/ProjectActivityView'; +import { ProjectChangeProposalsView } from './changeproposals/ProjectChangeProposalsView'; +import { ProjectHomeView } from './home/ProjectHomeView'; import { NewChangeProposalView } from './new-changeproposal/NewChangeProposalView'; import { ResourceView } from './resource/ResourceView'; -import { WorkspaceView } from './workspace/WorkspaceView'; +import { ProjectSettingsView } from './settings/ProjectSettingsView'; +import { ProjectTagsView } from './tags/ProjectTagsView'; -export const ProjectsRouter = () => { +export const ProjectRouter = () => { return ( - <> + - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - - - + ); }; diff --git a/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx b/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx new file mode 100644 index 0000000..79eedc8 --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx @@ -0,0 +1,120 @@ +/* + * 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 { 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 { 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 { ProjectDrawer } from './ProjectDrawer'; +import { GetProjectData, GetProjectVariables, ProjectShellProps, ProjectShellState } from './ProjectShell.types'; +import { ProjectContext } from './useProject'; + +const getProjectQuery = gql` + query getProject($identifier: ID!) { + viewer { + project(identifier: $identifier) { + identifier + name + description + organization { + identifier + name + role + } + } + } + } +`; + +export const ProjectShell = ({ children }: ProjectShellProps) => { + const [state, setState] = useState({ + errorSnackbarOpen: false, + }); + + const { projectIdentifier } = useParams(); + const variables: GetProjectVariables = { identifier: projectIdentifier ?? '' }; + const { data, error } = useQuery(getProjectQuery, { variables }); + + const { setActions } = usePalette(); + + useEffect(() => { + if (data) { + const { + viewer: { project }, + } = data; + if (project) { + setState((prevState) => ({ ...prevState, project })); + + const backToOrganization: PaletteNavigationAction = { + type: 'navigation-action', + id: 'go-to-organization', + icon: , + label: project.organization.name, + to: `/orgs/${project.organization.identifier}`, + }; + setActions([goToHome, backToOrganization, goToDomains, goToNotifications, goToSettings, goToHelp]); + } + } + setState((prevState) => ({ ...prevState, errorSnackbarOpen: !!error })); + }, [data, error]); + + const handleCloseSnackbar = () => setState((prevState) => ({ ...prevState, message: null })); + + if (!data) { + return null; + } + + if (!data.viewer.project) { + return ; + } + + return ( + <> + + + + Domains + + + + + + {children} + + + + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/projects/ProjectView.types.ts b/frontend/svalyn-studio-app/src/projects/ProjectShell.types.ts similarity index 87% rename from frontend/svalyn-studio-app/src/projects/ProjectView.types.ts rename to frontend/svalyn-studio-app/src/projects/ProjectShell.types.ts index 3f5ef55..6841958 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectView.types.ts +++ b/frontend/svalyn-studio-app/src/projects/ProjectShell.types.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Stéphane Bégaudeau. + * 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, @@ -17,12 +17,12 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { ProjectViewPanel } from './ProjectDrawer.types'; +export interface ProjectShellProps { + children?: React.ReactNode; +} -export interface ProjectViewState { - panel: ProjectViewPanel; - project: Project | null; - message: string | null; +export interface ProjectShellState { + errorSnackbarOpen: boolean; } export interface GetProjectData { diff --git a/frontend/svalyn-studio-app/src/projects/ProjectView.tsx b/frontend/svalyn-studio-app/src/projects/ProjectView.tsx deleted file mode 100644 index d07d63c..0000000 --- a/frontend/svalyn-studio-app/src/projects/ProjectView.tsx +++ /dev/null @@ -1,173 +0,0 @@ -/* - * 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, - * 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 { 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, matchPath, useLocation, 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 { ProjectDrawer } from './ProjectDrawer'; -import { ProjectViewPanel } from './ProjectDrawer.types'; -import { GetProjectData, GetProjectVariables, ProjectViewState } from './ProjectView.types'; -import { ProjectActivity } from './activity/ProjectActivity'; -import { ProjectChangeProposal } from './changeproposals/ProjectChangeProposal'; -import { ProjectHome } from './home/ProjectHome'; -import { ProjectSettings } from './settings/ProjectSettings'; -import { ProjectTags } from './tags/ProjectTags'; - -const getProjectQuery = gql` - query getProject($identifier: ID!) { - viewer { - project(identifier: $identifier) { - identifier - name - description - organization { - identifier - name - role - } - } - } - } -`; - -export const ProjectView = () => { - const location = useLocation(); - const activityMatch = matchPath('/projects/:projectId/activity', location.pathname); - const changeProposalsMatch = matchPath('/projects/:projectId/changeproposals', location.pathname); - const tagsMatch = matchPath('/projects/:projectId/tags', location.pathname); - const settingsMatch = matchPath('/projects/:projectId/settings', location.pathname); - - let panel: ProjectViewPanel = 'Home'; - if (activityMatch) { - panel = 'Activity'; - } else if (changeProposalsMatch) { - panel = 'ChangeProposals'; - } else if (tagsMatch) { - panel = 'Tags'; - } else if (settingsMatch) { - panel = 'Settings'; - } - - const [state, setState] = useState({ - panel, - project: null, - message: null, - }); - - useEffect(() => { - setState((prevState) => ({ ...prevState, panel })); - }, [location]); - - const { projectIdentifier } = useParams(); - const variables: GetProjectVariables = { identifier: projectIdentifier ?? '' }; - const { loading, data, error } = useQuery(getProjectQuery, { variables }); - - const { setActions } = usePalette(); - - useEffect(() => { - if (!loading) { - if (data) { - const { - viewer: { project }, - } = data; - if (project) { - setState((prevState) => ({ ...prevState, project })); - - const backToOrganization: PaletteNavigationAction = { - type: 'navigation-action', - id: 'go-to-organization', - icon: , - label: project.organization.name, - to: `/orgs/${project.organization.identifier}`, - }; - setActions([goToHome, backToOrganization, goToDomains, goToNotifications, goToSettings, goToHelp]); - } - } - if (error) { - setState((prevState) => ({ ...prevState, message: error.message })); - } - } - }, [loading, data, error]); - - useEffect(() => {}, []); - - const handleCloseSnackbar = () => setState((prevState) => ({ ...prevState, message: null })); - - if (!loading && state.project === null) { - return ; - } - - let panelElement = null; - if (state.project) { - if (state.panel === 'Home') { - panelElement = ( - - ); - } else if (state.panel === 'Activity') { - panelElement = ; - } else if (state.panel === 'ChangeProposals') { - panelElement = ( - - ); - } else if (state.panel === 'Tags') { - panelElement = ( - - ); - } else if (state.panel === 'Settings') { - panelElement = ( - - ); - } - } - - return ( - <> - - - - Domains - - - {state.project ? ( - - - {panelElement} - - ) : null} - - - - ); -}; diff --git a/frontend/svalyn-studio-app/src/projects/activity/ProjectActivity.tsx b/frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.tsx similarity index 92% rename from frontend/svalyn-studio-app/src/projects/activity/ProjectActivity.tsx rename to frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.tsx index 73289e4..cf9e1d5 100644 --- a/frontend/svalyn-studio-app/src/projects/activity/ProjectActivity.tsx +++ b/frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.tsx @@ -26,12 +26,12 @@ import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { ActivityTimeline } from '../../activity/ActivityTimeline'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; +import { useProject } from '../useProject'; import { GetProjectActivityData, GetProjectActivityVariables, - ProjectActivityProps, - ProjectActivityState, -} from './ProjectActivity.types'; + ProjectActivityViewState, +} from './ProjectActivityView.types'; const getProjectActivityQuery = gql` query getProjectActivity($identifier: ID!) { @@ -58,8 +58,12 @@ const getProjectActivityQuery = gql` } `; -export const ProjectActivity = ({ projectIdentifier }: ProjectActivityProps) => { - const [state, setState] = useState({ +export const ProjectActivityView = () => { + const { + identifier: projectIdentifier, + organization: { role }, + } = useProject(); + const [state, setState] = useState({ message: null, }); diff --git a/frontend/svalyn-studio-app/src/projects/activity/ProjectActivity.types.ts b/frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.types.ts similarity index 94% rename from frontend/svalyn-studio-app/src/projects/activity/ProjectActivity.types.ts rename to frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.types.ts index 91a9b1b..f85bd94 100644 --- a/frontend/svalyn-studio-app/src/projects/activity/ProjectActivity.types.ts +++ b/frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.types.ts @@ -17,11 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface ProjectActivityProps { - projectIdentifier: string; -} - -export interface ProjectActivityState { +export interface ProjectActivityViewState { message: string | null; } diff --git a/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposal.tsx b/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.tsx similarity index 96% rename from frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposal.tsx rename to frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.tsx index a7c6566..43f5e78 100644 --- a/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposal.tsx +++ b/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.tsx @@ -36,6 +36,7 @@ import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; +import { useProject } from '../useProject'; import { ChangeProposalsTableHead } from './ChangeProposalsTableHead'; import { ChangeProposalsTableToolbar } from './ChangeProposalsTableToolbar'; import { @@ -46,9 +47,8 @@ import { ErrorPayload, GetChangeProposalsData, GetChangeProposalsVariables, - ProjectChangeProposalProps, - ProjectChangeProposalState, -} from './ProjectChangeProposal.types'; + ProjectChangeProposalsViewState, +} from './ProjectChangeProposalsView.types'; const getChangeProposalsQuery = gql` query getChangeProposals($identifier: ID!, $status: [ChangeProposalStatus!]!, $page: Int!, $rowsPerPage: Int!) { @@ -80,8 +80,12 @@ const deleteChangeProposalsMutation = gql` } `; -export const ProjectChangeProposal = ({ projectIdentifier, role }: ProjectChangeProposalProps) => { - const [state, setState] = useState({ +export const ProjectChangeProposalsView = () => { + const { + identifier: projectIdentifier, + organization: { role }, + } = useProject(); + const [state, setState] = useState({ project: null, selectedChangeProposalIds: [], filter: 'OPEN', diff --git a/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposal.types.ts b/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.types.ts similarity index 90% rename from frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposal.types.ts rename to frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.types.ts index 87388ac..a31e13f 100644 --- a/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposal.types.ts +++ b/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.types.ts @@ -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, @@ -17,14 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface ProjectChangeProposalProps { - projectIdentifier: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface ProjectChangeProposalState { +export interface ProjectChangeProposalsViewState { project: Project | null; filter: ChangeProposalStatusFilter; selectedChangeProposalIds: string[]; diff --git a/frontend/svalyn-studio-app/src/projects/home/ProjectHome.tsx b/frontend/svalyn-studio-app/src/projects/home/ProjectHomeView.tsx similarity index 94% rename from frontend/svalyn-studio-app/src/projects/home/ProjectHome.tsx rename to frontend/svalyn-studio-app/src/projects/home/ProjectHomeView.tsx index 7b9e6e0..879aaa7 100644 --- a/frontend/svalyn-studio-app/src/projects/home/ProjectHome.tsx +++ b/frontend/svalyn-studio-app/src/projects/home/ProjectHomeView.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, @@ -27,8 +27,9 @@ import { Link as RouterLink } from 'react-router-dom'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; import { CreatedOn } from '../../widgets/CreatedOn'; import { LastModifiedOn } from '../../widgets/LastModifiedOn'; +import { useProject } from '../useProject'; import { ProjectBranchCard } from './ProjectBranchCard'; -import { GetProjectHomeData, GetProjectHomeVariables, ProjectHomeProps, ProjectHomeState } from './ProjectHome.types'; +import { GetProjectHomeData, GetProjectHomeVariables, ProjectHomeViewState } from './ProjectHomeView.types'; import { ProjectReadMeCard } from './ProjectReadMeCard'; const getProjectHomeQuery = gql` @@ -80,8 +81,12 @@ const getProjectHomeQuery = gql` } `; -export const ProjectHome = ({ projectIdentifier, role }: ProjectHomeProps) => { - const [state, setState] = useState({ +export const ProjectHomeView = () => { + const { + identifier: projectIdentifier, + organization: { role }, + } = useProject(); + const [state, setState] = useState({ project: null, message: null, }); diff --git a/frontend/svalyn-studio-app/src/projects/home/ProjectHome.types.ts b/frontend/svalyn-studio-app/src/projects/home/ProjectHomeView.types.ts similarity index 92% rename from frontend/svalyn-studio-app/src/projects/home/ProjectHome.types.ts rename to frontend/svalyn-studio-app/src/projects/home/ProjectHomeView.types.ts index c0d2f1e..2c5208e 100644 --- a/frontend/svalyn-studio-app/src/projects/home/ProjectHome.types.ts +++ b/frontend/svalyn-studio-app/src/projects/home/ProjectHomeView.types.ts @@ -17,14 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface ProjectHomeProps { - projectIdentifier: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface ProjectHomeState { +export interface ProjectHomeViewState { project: Project | null; message: string | null; } diff --git a/frontend/svalyn-studio-app/src/projects/new-changeproposal/NewChangeProposalView.tsx b/frontend/svalyn-studio-app/src/projects/new-changeproposal/NewChangeProposalView.tsx index d45a0f2..da2755b 100644 --- a/frontend/svalyn-studio-app/src/projects/new-changeproposal/NewChangeProposalView.tsx +++ b/frontend/svalyn-studio-app/src/projects/new-changeproposal/NewChangeProposalView.tsx @@ -37,7 +37,6 @@ import { useEffect, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { Navigate, Link as RouterLink, useParams } from 'react-router-dom'; import { getCookie } from '../../cookies/getCookie'; -import { Navbar } from '../../navbars/Navbar'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; import { CreateChangeProposalData, @@ -171,70 +170,65 @@ export const NewChangeProposalView = () => { return ( <> -
- -
- - - theme.spacing(2) }}> - - Let's create a change proposal - - {acceptedFiles.length === 0 ? ( - `2px dashed ${theme.palette.divider}`, - backgroundColor: (theme) => (isDragActive ? '#ffffff' : theme.palette.grey[100]), - minHeight: (theme) => theme.spacing(18), - }} - > - - Drag and drop files here or click to select files - - ) : null} - {acceptedFiles.length > 0 ? ( - - {acceptedFiles.map((file) => ( - - - - - - - ))} - - ) : null} - - - Back to the project - - - - -
-
+ + + theme.spacing(2) }}> + + Let's create a change proposal + + {acceptedFiles.length === 0 ? ( + `2px dashed ${theme.palette.divider}`, + backgroundColor: (theme) => (isDragActive ? '#ffffff' : theme.palette.grey[100]), + minHeight: (theme) => theme.spacing(18), + }} + > + + Drag and drop files here or click to select files + + ) : null} + {acceptedFiles.length > 0 ? ( + + {acceptedFiles.map((file) => ( + + + + + + + ))} + + ) : null} + + + Back to the project + + + + ); diff --git a/frontend/svalyn-studio-app/src/projects/resource/ResourceView.tsx b/frontend/svalyn-studio-app/src/projects/resource/ResourceView.tsx index 49dd9c3..6234431 100644 --- a/frontend/svalyn-studio-app/src/projects/resource/ResourceView.tsx +++ b/frontend/svalyn-studio-app/src/projects/resource/ResourceView.tsx @@ -25,7 +25,6 @@ import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { Link as RouterLink, useParams } from 'react-router-dom'; -import { Navbar } from '../../navbars/Navbar'; import { NotFoundView } from '../../notfound/NotFoundView'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; import { ViewerCard } from '../../viewers/ViewerCard'; @@ -78,39 +77,36 @@ export const ResourceView = () => { } return ( <> - - - {data && data.viewer.project ? ( - theme.spacing(2), - padding: (theme) => theme.spacing(4), - }} - > - theme.spacing(1) }}> - - {data.viewer.project.organization.name} - - / - - {data.viewer.project.name} - - - - - - - + {data && data.viewer.project ? ( + theme.spacing(2), + padding: (theme) => theme.spacing(4), + }} + > + theme.spacing(1) }}> + + {data.viewer.project.organization.name} + + / + + {data.viewer.project.name} + - ) : null} - + + + + + + + ) : null} ); diff --git a/frontend/svalyn-studio-app/src/projects/settings/ProjectSettings.tsx b/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.tsx similarity index 96% rename from frontend/svalyn-studio-app/src/projects/settings/ProjectSettings.tsx rename to frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.tsx index 207fcec..3c75edc 100644 --- a/frontend/svalyn-studio-app/src/projects/settings/ProjectSettings.tsx +++ b/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.tsx @@ -29,16 +29,16 @@ import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; +import { useProject } from '../useProject'; import { DeleteProjectDialog } from './DeleteProjectDialog'; import { ErrorPayload, - ProjectSettingsProps, - ProjectSettingsState, + ProjectSettingsViewState, UpdateProjectDescriptionData, UpdateProjectDescriptionVariables, UpdateProjectNameData, UpdateProjectNameVariables, -} from './ProjectSettings.types'; +} from './ProjectSettingsView.types'; const updateProjectNameMutation = gql` mutation updateProjectName($input: UpdateProjectNameInput!) { @@ -60,8 +60,12 @@ const updateProjectDescriptionMutation = gql` } `; -export const ProjectSettings = ({ projectIdentifier, role }: ProjectSettingsProps) => { - const [state, setState] = useState({ +export const ProjectSettingsView = () => { + const { + identifier: projectIdentifier, + organization: { role }, + } = useProject(); + const [state, setState] = useState({ name: '', description: '', deleteProjectDialogOpen: false, diff --git a/frontend/svalyn-studio-app/src/projects/settings/ProjectSettings.types.ts b/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.types.ts similarity index 89% rename from frontend/svalyn-studio-app/src/projects/settings/ProjectSettings.types.ts rename to frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.types.ts index 52d9794..07bbfb2 100644 --- a/frontend/svalyn-studio-app/src/projects/settings/ProjectSettings.types.ts +++ b/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.types.ts @@ -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, @@ -17,14 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface ProjectSettingsProps { - projectIdentifier: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface ProjectSettingsState { +export interface ProjectSettingsViewState { name: string; description: string; deleteProjectDialogOpen: boolean; diff --git a/frontend/svalyn-studio-app/src/projects/tags/ProjectTags.tsx b/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.tsx similarity index 96% rename from frontend/svalyn-studio-app/src/projects/tags/ProjectTags.tsx rename to frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.tsx index 6d3bd97..6eea8d1 100644 --- a/frontend/svalyn-studio-app/src/projects/tags/ProjectTags.tsx +++ b/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.tsx @@ -36,15 +36,15 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { useEffect, useState } from 'react'; import { ErrorSnackbar } from '../../snackbar/ErrorSnackbar'; +import { useProject } from '../useProject'; import { AddTagToProjectData, AddTagToProjectVariables, ErrorPayload, GetProjectTagsData, GetProjectTagsVariables, - ProjectTagsProps, - ProjectTagsState, -} from './ProjectTags.types'; + ProjectTagsViewState, +} from './ProjectTagsView.types'; const getProjectTagsQuery = gql` query getProjectTags($identifier: ID!, $page: Int!, $rowsPerPage: Int!) { @@ -78,8 +78,12 @@ const addTagToProjectMutation = gql` } `; -export const ProjectTags = ({ projectIdentifier, role }: ProjectTagsProps) => { - const [state, setState] = useState({ +export const ProjectTagsView = () => { + const { + identifier: projectIdentifier, + organization: { role }, + } = useProject(); + const [state, setState] = useState({ key: '', value: '', page: 0, diff --git a/frontend/svalyn-studio-app/src/projects/tags/ProjectTags.types.ts b/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.types.ts similarity index 92% rename from frontend/svalyn-studio-app/src/projects/tags/ProjectTags.types.ts rename to frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.types.ts index a64f8f6..75972e9 100644 --- a/frontend/svalyn-studio-app/src/projects/tags/ProjectTags.types.ts +++ b/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.types.ts @@ -17,14 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface ProjectTagsProps { - projectIdentifier: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface ProjectTagsState { +export interface ProjectTagsViewState { key: string; value: string; page: number; diff --git a/frontend/svalyn-studio-app/src/projects/useProject.tsx b/frontend/svalyn-studio-app/src/projects/useProject.tsx new file mode 100644 index 0000000..1c19406 --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/useProject.tsx @@ -0,0 +1,39 @@ +/* + * 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 React, { useContext } from 'react'; +import { Project, ProjectContextValue } from './useProject.types'; + +export const ProjectContext = React.createContext({ + project: { + identifier: '', + name: '', + description: '', + organization: { + identifier: '', + name: '', + role: 'NONE', + }, + }, +}); + +export const useProject = (): Project => { + const { project } = useContext(ProjectContext); + return project; +}; diff --git a/frontend/svalyn-studio-app/src/projects/ProjectDrawer.types.ts b/frontend/svalyn-studio-app/src/projects/useProject.types.ts similarity index 73% rename from frontend/svalyn-studio-app/src/projects/ProjectDrawer.types.ts rename to frontend/svalyn-studio-app/src/projects/useProject.types.ts index 30b27a8..4b669f8 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectDrawer.types.ts +++ b/frontend/svalyn-studio-app/src/projects/useProject.types.ts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Stéphane Bégaudeau. + * 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, @@ -17,9 +17,21 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export type ProjectViewPanel = 'Home' | 'Activity' | 'ChangeProposals' | 'Tags' | 'Settings'; +export interface ProjectContextValue { + project: Project; +} + +export interface Project { + identifier: string; + name: string; + description: string; + organization: Organization; +} -export interface ProjectDrawerProps { - projectIdentifier: string; - selectedPanel: ProjectViewPanel; +export interface Organization { + identifier: string; + name: string; + role: MembershipRole; } + +export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; diff --git a/frontend/svalyn-studio-app/src/projects/workspace/WorkspaceView.tsx b/frontend/svalyn-studio-app/src/workspace/WorkspaceView.tsx similarity index 96% rename from frontend/svalyn-studio-app/src/projects/workspace/WorkspaceView.tsx rename to frontend/svalyn-studio-app/src/workspace/WorkspaceView.tsx index 07a98e8..a360608 100644 --- a/frontend/svalyn-studio-app/src/projects/workspace/WorkspaceView.tsx +++ b/frontend/svalyn-studio-app/src/workspace/WorkspaceView.tsx @@ -19,7 +19,7 @@ import Box from '@mui/material/Box'; import { useParams } from 'react-router-dom'; -import { Workbench } from '../../workbench/Workbench'; +import { Workbench } from '../workbench/Workbench'; export const WorkspaceView = () => { const { changeId } = useParams(); diff --git a/frontend/svalyn-studio-app/src/projects/workspace/WorkspaceView.types.ts b/frontend/svalyn-studio-app/src/workspace/WorkspaceView.types.ts similarity index 100% rename from frontend/svalyn-studio-app/src/projects/workspace/WorkspaceView.types.ts rename to frontend/svalyn-studio-app/src/workspace/WorkspaceView.types.ts From b68bd1a2f086fe548856f8227c2b81b8e508f753 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 4/4] [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} - + ); };