Skip to content

Commit

Permalink
[209] Simplify the project views by sharing common code in the router
Browse files Browse the repository at this point in the history
Bug: #209
Signed-off-by: Stéphane Bégaudeau <[email protected]>
  • Loading branch information
sbegaudeau committed Jul 25, 2023
1 parent d648c12 commit 480cea7
Show file tree
Hide file tree
Showing 25 changed files with 381 additions and 389 deletions.
6 changes: 4 additions & 2 deletions frontend/svalyn-studio-app/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -51,7 +52,8 @@ export const App = () => {
<Route path="/" element={<HomeView />} />
<Route path="/new/*" element={<NewRouter />} />
<Route path="/orgs/:organizationIdentifier/*" element={<OrganizationRouter />} />
<Route path="/projects/*" element={<ProjectsRouter />} />
<Route path="/projects/:projectIdentifier/*" element={<ProjectRouter />} />
<Route path="/projects/:projectIdentifier/changes/:changeId" element={<WorkspaceView />} />
<Route path="/changeproposals/*" element={<ChangeProposalsRouter />} />
<Route path="/domains/*" element={<DomainsRouter />} />
<Route path="/search/*" element={<SearchRouter />} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -60,18 +60,11 @@ export const OrganizationShell = ({ children }: OrganizationShellProps) => {

const { organizationIdentifier } = useParams();
const variables: GetOrganizationVariables = { identifier: organizationIdentifier ?? '' };
const { data, error, refetch } = useQuery<GetOrganizationData, GetOrganizationVariables>(getOrganizationQuery, {
const { data, error } = useQuery<GetOrganizationData, GetOrganizationVariables>(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 }));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -43,7 +43,7 @@ export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTab
<Tab
label="Dashboard"
icon={<HomeIcon />}
value="/orgs/:organizationId"
value="/orgs/:organizationIdentifier"
to={`/orgs/${organizationIdentifier}`}
component={RouterLink}
iconPosition="start"
Expand All @@ -52,7 +52,7 @@ export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTab
<Tab
label="Tags"
icon={<TagIcon />}
value="/orgs/:organizationId/tags"
value="/orgs/:organizationIdentifier/tags"
to={`/orgs/${organizationIdentifier}/tags`}
component={RouterLink}
iconPosition="start"
Expand All @@ -61,7 +61,7 @@ export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTab
<Tab
label="Members"
icon={<PersonIcon />}
value="/orgs/:organizationId/members"
value="/orgs/:organizationIdentifier/members"
to={`/orgs/${organizationIdentifier}/members`}
component={RouterLink}
iconPosition="start"
Expand All @@ -70,7 +70,7 @@ export const OrganizationTabs = ({ organizationIdentifier, sx }: OrganizationTab
<Tab
label="Settings"
icon={<SettingsIcon />}
value="/orgs/:organizationId/settings"
value="/orgs/:organizationIdentifier/settings"
to={`/orgs/${organizationIdentifier}/settings`}
component={RouterLink}
iconPosition="start"
Expand Down
33 changes: 26 additions & 7 deletions frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,66 +28,85 @@ 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',
backgroundColor: theme.palette.background.paper,
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 (
<CompactDrawer>
<List disablePadding>
<ListItem selected={selectedPanel === 'Home'} disablePadding sx={{ display: 'block' }}>
<ListItem disablePadding sx={{ display: 'block' }}>
<ListItemButton
component={RouterLink}
to={`/projects/${projectIdentifier}`}
selected={currentTab === '/projects/:projectIdentifier'}
sx={{ minHeight: 48, justifyContent: 'center', px: 2.5 }}
>
<ListItemIcon sx={{ minWidth: 0, mr: 'auto', justifyContent: 'center' }}>
<HomeIcon />
</ListItemIcon>
</ListItemButton>
</ListItem>
<ListItem selected={selectedPanel === 'Activity'} disablePadding sx={{ display: 'block' }}>
<ListItem disablePadding sx={{ display: 'block' }}>
<ListItemButton
component={RouterLink}
to={`/projects/${projectIdentifier}/activity`}
selected={currentTab === '/projects/:projectIdentifier/activity'}
sx={{ minHeight: 48, justifyContent: 'center', px: 2.5 }}
>
<ListItemIcon sx={{ minWidth: 0, mr: 'auto', justifyContent: 'center' }}>
<TimelineIcon />
</ListItemIcon>
</ListItemButton>
</ListItem>
<ListItem selected={selectedPanel === 'ChangeProposals'} disablePadding sx={{ display: 'block' }}>
<ListItem disablePadding sx={{ display: 'block' }}>
<ListItemButton
component={RouterLink}
to={`/projects/${projectIdentifier}/changeproposals`}
selected={currentTab === '/projects/:projectIdentifier/changeproposals'}
sx={{ minHeight: 48, justifyContent: 'center', px: 2.5 }}
>
<ListItemIcon sx={{ minWidth: 0, mr: 'auto', justifyContent: 'center' }}>
<DifferenceIcon />
</ListItemIcon>
</ListItemButton>
</ListItem>
<ListItem selected={selectedPanel === 'Tags'} disablePadding sx={{ display: 'block' }}>
<ListItem disablePadding sx={{ display: 'block' }}>
<ListItemButton
component={RouterLink}
to={`/projects/${projectIdentifier}/tags`}
selected={currentTab === '/projects/:projectIdentifier/tags'}
sx={{ minHeight: 48, justifyContent: 'center', px: 2.5 }}
>
<ListItemIcon sx={{ minWidth: 0, mr: 'auto', justifyContent: 'center' }}>
<TagIcon />
</ListItemIcon>
</ListItemButton>
</ListItem>
<ListItem selected={selectedPanel === 'Settings'} disablePadding sx={{ display: 'block' }}>
<ListItem disablePadding sx={{ display: 'block' }}>
<ListItemButton
component={RouterLink}
to={`/projects/${projectIdentifier}/settings`}
selected={currentTab === '/projects/:projectIdentifier/settings'}
sx={{ minHeight: 48, justifyContent: 'center', px: 2.5 }}
>
<ListItemIcon sx={{ minWidth: 0, mr: 'auto', justifyContent: 'center' }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<ProjectShell>
<Routes>
<Route path=":projectIdentifier" element={<ProjectView />} />
<Route path=":projectIdentifier/activity" element={<ProjectView />} />
<Route path=":projectIdentifier/changeproposals" element={<ProjectView />} />
<Route path=":projectIdentifier/tags" element={<ProjectView />} />
<Route path=":projectIdentifier/settings" element={<ProjectView />} />
<Route path=":projectIdentifier/new/changeproposal" element={<NewChangeProposalView />} />
<Route path=":projectIdentifier/changes/:changeId/resources/*" element={<ResourceView />} />
<Route path=":projectIdentifier/changes/:changeId" element={<WorkspaceView />} />
<Route index element={<ProjectHomeView />} />
<Route path="activity" element={<ProjectActivityView />} />
<Route path="changeproposals" element={<ProjectChangeProposalsView />} />
<Route path="tags" element={<ProjectTagsView />} />
<Route path="settings" element={<ProjectSettingsView />} />
<Route path="new/changeproposal" element={<NewChangeProposalView />} />
<Route path="changes/:changeId/resources/*" element={<ResourceView />} />
</Routes>

<Outlet />
</>
</ProjectShell>
);
};
120 changes: 120 additions & 0 deletions frontend/svalyn-studio-app/src/projects/ProjectShell.tsx
Original file line number Diff line number Diff line change
@@ -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<ProjectShellState>({
errorSnackbarOpen: false,
});

const { projectIdentifier } = useParams();
const variables: GetProjectVariables = { identifier: projectIdentifier ?? '' };
const { data, error } = useQuery<GetProjectData, GetProjectVariables>(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: <CorporateFareIcon fontSize="small" />,
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 <NotFoundView />;
}

return (
<>
<Box sx={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Navbar>
<Link component={RouterLink} to="/domains" color="inherit" underline="hover" fontWeight={800}>
Domains
</Link>
</Navbar>
<Box
sx={{
display: 'grid',
gridTemplateRows: '1fr',
gridTemplateColumns: 'min-content 1fr',
flexGrow: '1',
}}
>
<ProjectContext.Provider value={{ project: data.viewer.project }}>
<ProjectDrawer />
{children}
</ProjectContext.Provider>
</Box>
</Box>
<ErrorSnackbar open={state.errorSnackbarOpen} message={error?.message ?? null} onClose={handleCloseSnackbar} />
</>
);
};
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 480cea7

Please sign in to comment.