From 9d20cee81fa416ba474f3b845117ab7d9b8bcf44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20B=C3=A9gaudeau?= Date: Sat, 9 Sep 2023 00:30:32 +0200 Subject: [PATCH] [202] Update the way change proposals are displayed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/svalyn/svalyn-studio/issues/202 Signed-off-by: Stéphane Bégaudeau --- frontend/svalyn-studio-app/src/app/App.tsx | 2 - .../changeproposals/ChangeProposalView.tsx | 118 -------- .../ChangeProposalViewTabPanel.tsx | 126 --------- .../svalyn-studio-app/src/forms/useForm.tsx | 5 + .../src/forms/useForm.types.ts | 1 + .../src/projects/ProjectBreadcrumbs.tsx | 4 +- .../src/projects/ProjectDrawer.tsx | 1 - .../src/projects/ProjectRouter.tsx | 2 + .../src/projects/ProjectShell.tsx | 2 +- .../src/projects/ProjectViewHeader.tsx | 38 +++ .../ProjectViewHeader.types.ts} | 18 +- .../projects/activity/ProjectActivityView.tsx | 18 +- .../changeproposal/ChangeProposalRouter.tsx} | 42 +-- .../changeproposal/ChangeProposalShell.tsx | 122 ++++++++ .../ChangeProposalShell.types.ts} | 21 +- .../changeproposal}/ReviewDialog.tsx | 0 .../changeproposal}/ReviewDialog.types.ts | 0 .../files/ChangeProposalFilesView.tsx} | 41 +-- .../files/ChangeProposalFilesView.types.ts} | 9 +- .../overview/ChangeProposalHeader.tsx | 4 +- .../overview/ChangeProposalHeader.types.ts | 0 .../overview/ChangeProposalOverviewView.tsx} | 21 +- .../ChangeProposalOverviewView.types.ts} | 9 +- .../overview/ChangeProposalStatus.tsx | 0 .../overview/ChangeProposalStatus.types.ts | 0 .../ChangeProposalsTableToolbar.tsx | 23 +- .../ProjectChangeProposalsView.tsx | 30 +- .../src/projects/settings/DetailsCard.tsx | 262 ++++++++++++++++++ .../projects/settings/DetailsCard.types.ts | 73 +++++ .../projects/settings/ProjectSettingsView.tsx | 196 +------------ .../settings/ProjectSettingsView.types.ts | 43 --- .../src/projects/tags/NewTagCard.tsx | 137 +++++++++ .../src/projects/tags/NewTagCard.types.ts | 51 ++++ .../src/projects/tags/ProjectTagsView.tsx | 141 +--------- .../projects/tags/ProjectTagsView.types.ts | 26 -- 35 files changed, 807 insertions(+), 779 deletions(-) delete mode 100644 frontend/svalyn-studio-app/src/changeproposals/ChangeProposalView.tsx delete mode 100644 frontend/svalyn-studio-app/src/changeproposals/ChangeProposalViewTabPanel.tsx create mode 100644 frontend/svalyn-studio-app/src/projects/ProjectViewHeader.tsx rename frontend/svalyn-studio-app/src/{changeproposals/ChangeProposalsRouter.tsx => projects/ProjectViewHeader.types.ts} (73%) rename frontend/svalyn-studio-app/src/{changeproposals/ChangeProposalViewTabPanel.types.ts => projects/changeproposal/ChangeProposalRouter.tsx} (64%) create mode 100644 frontend/svalyn-studio-app/src/projects/changeproposal/ChangeProposalShell.tsx rename frontend/svalyn-studio-app/src/{changeproposals/ChangeProposalView.types.ts => projects/changeproposal/ChangeProposalShell.types.ts} (76%) rename frontend/svalyn-studio-app/src/{changeproposals => projects/changeproposal}/ReviewDialog.tsx (100%) rename frontend/svalyn-studio-app/src/{changeproposals => projects/changeproposal}/ReviewDialog.types.ts (100%) rename frontend/svalyn-studio-app/src/{changeproposals/files/ChangeProposalFiles.tsx => projects/changeproposal/files/ChangeProposalFilesView.tsx} (83%) rename frontend/svalyn-studio-app/src/{changeproposals/files/ChangeProposalFiles.types.ts => projects/changeproposal/files/ChangeProposalFilesView.types.ts} (90%) rename frontend/svalyn-studio-app/src/{changeproposals => projects/changeproposal}/overview/ChangeProposalHeader.tsx (95%) rename frontend/svalyn-studio-app/src/{changeproposals => projects/changeproposal}/overview/ChangeProposalHeader.types.ts (100%) rename frontend/svalyn-studio-app/src/{changeproposals/overview/ChangeProposalOverview.tsx => projects/changeproposal/overview/ChangeProposalOverviewView.tsx} (93%) rename frontend/svalyn-studio-app/src/{changeproposals/overview/ChangeProposalOverview.types.ts => projects/changeproposal/overview/ChangeProposalOverviewView.types.ts} (92%) rename frontend/svalyn-studio-app/src/{changeproposals => projects/changeproposal}/overview/ChangeProposalStatus.tsx (100%) rename frontend/svalyn-studio-app/src/{changeproposals => projects/changeproposal}/overview/ChangeProposalStatus.types.ts (100%) create mode 100644 frontend/svalyn-studio-app/src/projects/settings/DetailsCard.tsx create mode 100644 frontend/svalyn-studio-app/src/projects/settings/DetailsCard.types.ts create mode 100644 frontend/svalyn-studio-app/src/projects/tags/NewTagCard.tsx create mode 100644 frontend/svalyn-studio-app/src/projects/tags/NewTagCard.types.ts diff --git a/frontend/svalyn-studio-app/src/app/App.tsx b/frontend/svalyn-studio-app/src/app/App.tsx index b422ca36..535bcee1 100644 --- a/frontend/svalyn-studio-app/src/app/App.tsx +++ b/frontend/svalyn-studio-app/src/app/App.tsx @@ -19,7 +19,6 @@ import { Route, Routes } from 'react-router-dom'; import { AdminRouter } from '../admin/AdminRouter'; -import { ChangeProposalsRouter } from '../changeproposals/ChangeProposalsRouter'; import { DomainsRouter } from '../domains/DomainsRouter'; import { ErrorsRouter } from '../errors/ErrorsRouter'; import { HelpRouter } from '../help/HelpRouter'; @@ -47,7 +46,6 @@ export const App = () => { } /> } /> } /> - } /> } /> } /> } /> diff --git a/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalView.tsx b/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalView.tsx deleted file mode 100644 index 8765e7e3..00000000 --- a/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalView.tsx +++ /dev/null @@ -1,118 +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 ClassIcon from '@mui/icons-material/Class'; -import CorporateFareIcon from '@mui/icons-material/CorporateFare'; -import { useSnackbar } from 'notistack'; -import { useEffect, useState } from 'react'; -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 { ChangeProposalViewState, GetChangeProposalData, GetChangeProposalVariables } from './ChangeProposalView.types'; -import { ChangeProposalViewTabPanel } from './ChangeProposalViewTabPanel'; - -const getChangeProposalQuery = gql` - query getChangeProposal($id: ID!) { - viewer { - changeProposal(id: $id) { - id - name - project { - identifier - name - organization { - identifier - name - role - } - } - } - } - } -`; - -export const ChangeProposalView = () => { - const { changeProposalId } = useParams(); - const [state, setState] = useState({ changeProposal: null }); - - const { enqueueSnackbar } = useSnackbar(); - - const variables: GetChangeProposalVariables = { id: changeProposalId ?? '' }; - const { loading, data, error } = useQuery(getChangeProposalQuery, { - variables, - }); - - const { setActions } = usePalette(); - - useEffect(() => { - if (!loading) { - if (data) { - const { - viewer: { changeProposal }, - } = data; - if (changeProposal) { - setState((prevState) => ({ ...prevState, changeProposal })); - - const backToProject: PaletteNavigationAction = { - type: 'navigation-action', - id: 'go-to-project', - icon: , - label: changeProposal.project.name, - to: `/projects/${changeProposal.project.identifier}`, - }; - - const backToOrganization: PaletteNavigationAction = { - type: 'navigation-action', - id: 'go-to-organization', - icon: , - label: changeProposal.project.organization.name, - to: `/orgs/${changeProposal.project.organization.identifier}`, - }; - setActions([ - goToHome, - backToProject, - backToOrganization, - goToDomains, - goToNotifications, - goToSettings, - goToHelp, - ]); - } - } - if (error) { - enqueueSnackbar(error.message, { variant: 'error' }); - } - } - }, [loading, data, error]); - - if (!loading && state.changeProposal === null) { - return ; - } - - return ( -
- - {state.changeProposal ? : null} -
- ); -}; diff --git a/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalViewTabPanel.tsx b/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalViewTabPanel.tsx deleted file mode 100644 index 9c5ef8c4..00000000 --- a/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalViewTabPanel.tsx +++ /dev/null @@ -1,126 +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 Box from '@mui/material/Box'; -import Link from '@mui/material/Link'; -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 { useEffect, useState } from 'react'; -import { Link as RouterLink, generatePath, matchPath, useLocation, useNavigate } from 'react-router-dom'; -import { ChangeProposalViewTabPanelProps, ChangeProposalViewTabPanelState } from './ChangeProposalViewTabPanel.types'; -import { ChangeProposalFiles } from './files/ChangeProposalFiles'; -import { ChangeProposalOverview } from './overview/ChangeProposalOverview'; - -const a11yProps = (index: number) => { - return { - id: `tab-${index}`, - 'aria-controls': `tabpanel-${index}`, - }; -}; - -export const ChangeProposalViewTabPanel = ({ changeProposal }: ChangeProposalViewTabPanelProps) => { - const location = useLocation(); - const overviewMatch = matchPath('/changeproposals/:changeProposalId', location.pathname); - const filesMatch = matchPath('/changeproposals/:changeProposalId/files', location.pathname); - let activeTab = 0; - if (filesMatch) { - activeTab = 1; - } - - const [state, setState] = useState({ activeTab }); - const navigate = useNavigate(); - useEffect(() => { - if (state.activeTab === 0 && !overviewMatch) { - const path = generatePath('/changeproposals/:changeProposalId', { changeProposalId: changeProposal.id }); - navigate(path); - } else if (state.activeTab === 1 && !filesMatch) { - const path = generatePath('/changeproposals/:changeProposalId/files', { changeProposalId: changeProposal.id }); - navigate(path); - } - }, [state.activeTab]); - - const handleTabChanged = (_: React.SyntheticEvent, newValue: number) => - setState((prevState) => ({ ...prevState, activeTab: newValue })); - - return ( -
- `1px solid ${theme.palette.divider}`, - }} - > - theme.spacing(1), - marginRight: (theme) => theme.spacing(4), - }} - > - - {changeProposal.project.organization.name} - - / - - {changeProposal.project.name} - - / - - {changeProposal.name} - - - - - - - - - -
- ); -}; diff --git a/frontend/svalyn-studio-app/src/forms/useForm.tsx b/frontend/svalyn-studio-app/src/forms/useForm.tsx index 661d697d..8dac9e70 100644 --- a/frontend/svalyn-studio-app/src/forms/useForm.tsx +++ b/frontend/svalyn-studio-app/src/forms/useForm.tsx @@ -31,6 +31,10 @@ export function useForm({ isFormValid: false, }); + const reset = () => { + setState((prevState) => ({ ...prevState, data: initialValue, validationState: {}, isFormValid: false })); + }; + const getTextFieldProps = (name: keyof FormDataType, helperText?: string): TextFieldProps => { let error = false; let computedHelperText: string | undefined = helperText; @@ -73,6 +77,7 @@ export function useForm({ return { data: state.data, isFormValid: state.isFormValid, + reset, getTextFieldProps, }; } diff --git a/frontend/svalyn-studio-app/src/forms/useForm.types.ts b/frontend/svalyn-studio-app/src/forms/useForm.types.ts index 75f949fc..97c79588 100644 --- a/frontend/svalyn-studio-app/src/forms/useForm.types.ts +++ b/frontend/svalyn-studio-app/src/forms/useForm.types.ts @@ -27,6 +27,7 @@ export interface UseFormProps { export interface UseFormValue { data: FormDataType; isFormValid: boolean; + reset: () => void; getTextFieldProps: (name: keyof FormDataType, helperText?: string) => TextFieldProps; } diff --git a/frontend/svalyn-studio-app/src/projects/ProjectBreadcrumbs.tsx b/frontend/svalyn-studio-app/src/projects/ProjectBreadcrumbs.tsx index c819f2cb..db4202f8 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectBreadcrumbs.tsx +++ b/frontend/svalyn-studio-app/src/projects/ProjectBreadcrumbs.tsx @@ -34,6 +34,8 @@ const patterns = [ '/projects/:projectIdentifier', '/projects/:projectIdentifier/activity', '/projects/:projectIdentifier/changeproposals', + '/projects/:projectIdentifier/changeproposals/:changeProposalIdentifier', + '/projects/:projectIdentifier/changeproposals/:changeProposalIdentifier/files', '/projects/:projectIdentifier/new/changeproposal', '/projects/:projectIdentifier/tags', '/projects/:projectIdentifier/settings', @@ -116,7 +118,7 @@ const AdditionalBreadcrumbEntry = () => { label: 'Activity', icon: , }; - } else if (currentTab === '/projects/:projectIdentifier/changeproposals') { + } else if (currentTab?.startsWith('/projects/:projectIdentifier/changeproposals')) { tabBreadcrumbEntry = { label: 'Change Proposals', icon: , diff --git a/frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx b/frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx index a16de303..cd41e067 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx +++ b/frontend/svalyn-studio-app/src/projects/ProjectDrawer.tsx @@ -32,7 +32,6 @@ 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}`, })); diff --git a/frontend/svalyn-studio-app/src/projects/ProjectRouter.tsx b/frontend/svalyn-studio-app/src/projects/ProjectRouter.tsx index d782a0fa..5adf8bf9 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectRouter.tsx +++ b/frontend/svalyn-studio-app/src/projects/ProjectRouter.tsx @@ -20,6 +20,7 @@ import { Route, Routes } from 'react-router-dom'; import { ProjectShell } from './ProjectShell'; import { ProjectActivityView } from './activity/ProjectActivityView'; +import { ChangeProposalRouter } from './changeproposal/ChangeProposalRouter'; import { ProjectChangeProposalsView } from './changeproposals/ProjectChangeProposalsView'; import { ProjectHomeView } from './home/ProjectHomeView'; import { NewChangeProposalView } from './new-changeproposal/NewChangeProposalView'; @@ -34,6 +35,7 @@ export const ProjectRouter = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx b/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx index cc3cb6f7..0b3ee3cb 100644 --- a/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx +++ b/frontend/svalyn-studio-app/src/projects/ProjectShell.tsx @@ -98,7 +98,7 @@ export const ProjectShell = ({ children }: ProjectShellProps) => { sx={{ display: 'grid', gridTemplateRows: '1fr', - gridTemplateColumns: 'min-content 1fr', + gridTemplateColumns: '64px calc(100vw - 64px)', flexGrow: '1', }} > diff --git a/frontend/svalyn-studio-app/src/projects/ProjectViewHeader.tsx b/frontend/svalyn-studio-app/src/projects/ProjectViewHeader.tsx new file mode 100644 index 00000000..02ac6fab --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/ProjectViewHeader.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 Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import { ProjectViewHeaderProps } from './ProjectViewHeader.types'; + +export const ProjectViewHeader = ({ children }: ProjectViewHeaderProps) => { + return ( + `1px solid ${theme.palette.divider}`, + }} + > + theme.spacing(2) }}> + {children} + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalsRouter.tsx b/frontend/svalyn-studio-app/src/projects/ProjectViewHeader.types.ts similarity index 73% rename from frontend/svalyn-studio-app/src/changeproposals/ChangeProposalsRouter.tsx rename to frontend/svalyn-studio-app/src/projects/ProjectViewHeader.types.ts index 79afd13f..616ccfea 100644 --- a/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalsRouter.tsx +++ b/frontend/svalyn-studio-app/src/projects/ProjectViewHeader.types.ts @@ -17,18 +17,6 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { Outlet, Route, Routes } from 'react-router-dom'; -import { ChangeProposalView } from './ChangeProposalView'; - -export const ChangeProposalsRouter = () => { - return ( - <> - - } /> - } /> - - - - - ); -}; +export interface ProjectViewHeaderProps { + children?: React.ReactElement[]; +} diff --git a/frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.tsx b/frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.tsx index 5fb68c40..17675942 100644 --- a/frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.tsx +++ b/frontend/svalyn-studio-app/src/projects/activity/ProjectActivityView.tsx @@ -19,13 +19,12 @@ import { gql, useQuery } from '@apollo/client'; import TimelineIcon from '@mui/icons-material/Timeline'; -import Box from '@mui/material/Box'; import Container from '@mui/material/Container'; -import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { useSnackbar } from 'notistack'; import { useEffect } from 'react'; import { ActivityTimeline } from '../../activity/ActivityTimeline'; +import { ProjectViewHeader } from '../ProjectViewHeader'; import { useProject } from '../useProject'; import { GetProjectActivityData, GetProjectActivityVariables } from './ProjectActivityView.types'; @@ -72,17 +71,10 @@ export const ProjectActivityView = () => { return (
- `1px solid ${theme.palette.divider}`, - }} - > - theme.spacing(2) }}> - - Activity - - + + + Activity + { + return ( + + + } /> + } /> + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/projects/changeproposal/ChangeProposalShell.tsx b/frontend/svalyn-studio-app/src/projects/changeproposal/ChangeProposalShell.tsx new file mode 100644 index 00000000..4f2c7a80 --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/changeproposal/ChangeProposalShell.tsx @@ -0,0 +1,122 @@ +/* + * 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 Box from '@mui/material/Box'; +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 { Link as RouterLink, useParams } from 'react-router-dom'; +import { useRouteMatch } from '../../hooks/useRouteMatch'; +import { useProject } from '../useProject'; +import { + ChangeProposalShellProps, + GetChangeProposalData, + GetChangeProposalVariables, +} from './ChangeProposalShell.types'; + +const getChangeProposalQuery = gql` + query getChangeProposal($id: ID!) { + viewer { + changeProposal(id: $id) { + id + name + } + } + } +`; + +const a11yProps = (index: number) => { + return { + id: `tab-${index}`, + 'aria-controls': `tabpanel-${index}`, + }; +}; + +const patterns = [ + '/projects/:projectIdentifier/changeproposals/:changeProposalIdentifier', + '/projects/:projectIdentifier/changeproposals/:changeProposalIdentifier/files', +]; + +export const ChangeProposalShell = ({ children }: ChangeProposalShellProps) => { + const { identifier: projectIdentifier } = useProject(); + const { changeProposalIdentifier } = useParams(); + const routeMatch = useRouteMatch(patterns); + const currentTab = routeMatch?.pattern?.path; + + const variables: GetChangeProposalVariables = { id: changeProposalIdentifier ?? '' }; + const { data, error } = useQuery(getChangeProposalQuery, { + variables, + }); + + if (!data) { + return null; + } + if (!data.viewer.changeProposal) { + return
Not found
; + } + + return ( + + `1px solid ${theme.palette.divider}`, + }} + > + theme.spacing(1), + marginRight: (theme) => theme.spacing(4), + }} + > + + {data.viewer.changeProposal.name} + + + + + + + + + {children} + + ); +}; diff --git a/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalView.types.ts b/frontend/svalyn-studio-app/src/projects/changeproposal/ChangeProposalShell.types.ts similarity index 76% rename from frontend/svalyn-studio-app/src/changeproposals/ChangeProposalView.types.ts rename to frontend/svalyn-studio-app/src/projects/changeproposal/ChangeProposalShell.types.ts index 0357c236..a60e2b3e 100644 --- a/frontend/svalyn-studio-app/src/changeproposals/ChangeProposalView.types.ts +++ b/frontend/svalyn-studio-app/src/projects/changeproposal/ChangeProposalShell.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,8 +17,8 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface ChangeProposalViewState { - changeProposal: ChangeProposal | null; +export interface ChangeProposalShellProps { + children: React.ReactNode; } export interface GetChangeProposalData { @@ -32,23 +32,8 @@ export interface Viewer { export interface ChangeProposal { id: string; name: string; - project: Project; -} - -export interface Project { - identifier: string; - name: string; - organization: Organization; } -export interface Organization { - identifier: string; - name: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - export interface GetChangeProposalVariables { id: string; } diff --git a/frontend/svalyn-studio-app/src/changeproposals/ReviewDialog.tsx b/frontend/svalyn-studio-app/src/projects/changeproposal/ReviewDialog.tsx similarity index 100% rename from frontend/svalyn-studio-app/src/changeproposals/ReviewDialog.tsx rename to frontend/svalyn-studio-app/src/projects/changeproposal/ReviewDialog.tsx diff --git a/frontend/svalyn-studio-app/src/changeproposals/ReviewDialog.types.ts b/frontend/svalyn-studio-app/src/projects/changeproposal/ReviewDialog.types.ts similarity index 100% rename from frontend/svalyn-studio-app/src/changeproposals/ReviewDialog.types.ts rename to frontend/svalyn-studio-app/src/projects/changeproposal/ReviewDialog.types.ts diff --git a/frontend/svalyn-studio-app/src/changeproposals/files/ChangeProposalFiles.tsx b/frontend/svalyn-studio-app/src/projects/changeproposal/files/ChangeProposalFilesView.tsx similarity index 83% rename from frontend/svalyn-studio-app/src/changeproposals/files/ChangeProposalFiles.tsx rename to frontend/svalyn-studio-app/src/projects/changeproposal/files/ChangeProposalFilesView.tsx index 5a9d15c0..c730b74e 100644 --- a/frontend/svalyn-studio-app/src/changeproposals/files/ChangeProposalFiles.tsx +++ b/frontend/svalyn-studio-app/src/projects/changeproposal/files/ChangeProposalFilesView.tsx @@ -29,14 +29,14 @@ import ListItemText from '@mui/material/ListItemText'; import Paper from '@mui/material/Paper'; import { useSnackbar } from 'notistack'; import { useEffect, useState } from 'react'; -import { ViewerCard } from '../../viewers/ViewerCard'; +import { useParams } from 'react-router-dom'; +import { ViewerCard } from '../../../viewers/ViewerCard'; import { - ChangeProposalFilesProps, - ChangeProposalFilesState, + ChangeProposalFilesViewState, ChangeResourceMetadata, GetChangeProposalData, GetChangeProposalVariables, -} from './ChangeProposalFiles.types'; +} from './ChangeProposalFilesView.types'; const getChangeProposalFilesQuery = gql` query getChangeProposalFiles($id: ID!) { @@ -61,12 +61,13 @@ const getChangeProposalFilesQuery = gql` } `; -export const ChangeProposalFiles = ({ changeProposalId }: ChangeProposalFilesProps) => { - const [state, setState] = useState({ changeProposal: null }); +export const ChangeProposalFilesView = () => { + const { changeProposalIdentifier } = useParams(); + const [state, setState] = useState({ changeProposal: null }); const { enqueueSnackbar } = useSnackbar(); - const variables: GetChangeProposalVariables = { id: changeProposalId }; + const variables: GetChangeProposalVariables = { id: changeProposalIdentifier ?? '' }; const { loading, data, error } = useQuery( getChangeProposalFilesQuery, { variables } @@ -104,7 +105,11 @@ export const ChangeProposalFiles = ({ changeProposalId }: ChangeProposalFilesPro } return ( - theme.spacing(4) }}> + theme.spacing(4), + }} + > @@ -134,21 +139,23 @@ export const ChangeProposalFiles = ({ changeProposalId }: ChangeProposalFilesPro + - {state.changeProposal.change.resources.edges - .map((edge) => edge.node) - .map((resource) => { - const fullpath = resource.path.length > 0 ? `${resource.path}/${resource.name}` : resource.name; - return ( - theme.spacing(4) }} key={fullpath}> + theme.spacing(2) }}> + {state.changeProposal.change.resources.edges + .map((edge) => edge.node) + .map((resource) => { + const fullpath = resource.path.length > 0 ? `${resource.path}/${resource.name}` : resource.name; + return ( - - ); - })} + ); + })} + diff --git a/frontend/svalyn-studio-app/src/changeproposals/files/ChangeProposalFiles.types.ts b/frontend/svalyn-studio-app/src/projects/changeproposal/files/ChangeProposalFilesView.types.ts similarity index 90% rename from frontend/svalyn-studio-app/src/changeproposals/files/ChangeProposalFiles.types.ts rename to frontend/svalyn-studio-app/src/projects/changeproposal/files/ChangeProposalFilesView.types.ts index 0ecc8b26..dc332540 100644 --- a/frontend/svalyn-studio-app/src/changeproposals/files/ChangeProposalFiles.types.ts +++ b/frontend/svalyn-studio-app/src/projects/changeproposal/files/ChangeProposalFilesView.types.ts @@ -17,14 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface ChangeProposalFilesProps { - changeProposalId: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface ChangeProposalFilesState { +export interface ChangeProposalFilesViewState { changeProposal: ChangeProposal | null; } diff --git a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalHeader.tsx b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalHeader.tsx similarity index 95% rename from frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalHeader.tsx rename to frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalHeader.tsx index 9d8542ec..f152ffe4 100644 --- a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalHeader.tsx +++ b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalHeader.tsx @@ -22,8 +22,8 @@ import NotInterestedIcon from '@mui/icons-material/NotInterested'; import PanoramaFishEyeIcon from '@mui/icons-material/PanoramaFishEye'; import Box from '@mui/material/Box'; import Chip from '@mui/material/Chip'; -import { CreatedOn } from '../../widgets/CreatedOn'; -import { LastModifiedOn } from '../../widgets/LastModifiedOn'; +import { CreatedOn } from '../../../widgets/CreatedOn'; +import { LastModifiedOn } from '../../../widgets/LastModifiedOn'; import { ChangeProposalHeaderProps } from './ChangeProposalHeader.types'; export const ChangeProposalHeader = ({ changeProposal }: ChangeProposalHeaderProps) => { diff --git a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalHeader.types.ts b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalHeader.types.ts similarity index 100% rename from frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalHeader.types.ts rename to frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalHeader.types.ts diff --git a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalOverview.tsx b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalOverviewView.tsx similarity index 93% rename from frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalOverview.tsx rename to frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalOverviewView.tsx index f2b1980c..5519b294 100644 --- a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalOverview.tsx +++ b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalOverviewView.tsx @@ -29,17 +29,18 @@ import Typography from '@mui/material/Typography'; import { useSnackbar } from 'notistack'; import { useEffect, useState } from 'react'; import ReactMarkdown from 'react-markdown'; -import { EditReadMeDialog } from '../../dialogs/EditReadMeDialog'; +import { useParams } from 'react-router-dom'; +import { EditReadMeDialog } from '../../../dialogs/EditReadMeDialog'; +import { useProject } from '../../useProject'; import { ChangeProposalHeader } from './ChangeProposalHeader'; import { - ChangeProposalOverviewProps, - ChangeProposalOverviewState, + ChangeProposalOverviewViewState, ErrorPayload, GetChangeProposalData, GetChangeProposalVariables, UpdateChangeProposalReadMeData, UpdateChangeProposalReadMeVariables, -} from './ChangeProposalOverview.types'; +} from './ChangeProposalOverviewView.types'; import { ChangeProposalStatus } from './ChangeProposalStatus'; const getChangeProposalQuery = gql` @@ -103,15 +104,19 @@ const trimLines = (content: string): string => .map((line) => line.trim()) .join('\n'); -export const ChangeProposalOverview = ({ changeProposalId, role }: ChangeProposalOverviewProps) => { - const [state, setState] = useState({ +export const ChangeProposalOverviewView = () => { + const { changeProposalIdentifier } = useParams(); + const { + organization: { role }, + } = useProject(); + const [state, setState] = useState({ changeProposal: null, editReadMeDialogOpen: false, }); const { enqueueSnackbar } = useSnackbar(); - const variables: GetChangeProposalVariables = { id: changeProposalId }; + const variables: GetChangeProposalVariables = { id: changeProposalIdentifier ?? '' }; const { loading, data, error, refetch } = useQuery( getChangeProposalQuery, { @@ -173,7 +178,7 @@ export const ChangeProposalOverview = ({ changeProposalId, role }: ChangeProposa const variables: UpdateChangeProposalReadMeVariables = { input: { id: crypto.randomUUID(), - changeProposalId, + changeProposalId: changeProposalIdentifier ?? '', content: value, }, }; diff --git a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalOverview.types.ts b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalOverviewView.types.ts similarity index 92% rename from frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalOverview.types.ts rename to frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalOverviewView.types.ts index 492f53fb..55eede4e 100644 --- a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalOverview.types.ts +++ b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalOverviewView.types.ts @@ -17,14 +17,7 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export interface ChangeProposalOverviewProps { - changeProposalId: string; - role: MembershipRole; -} - -export type MembershipRole = 'ADMIN' | 'MEMBER' | 'NONE'; - -export interface ChangeProposalOverviewState { +export interface ChangeProposalOverviewViewState { changeProposal: ChangeProposal | null; editReadMeDialogOpen: boolean; } diff --git a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalStatus.tsx b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalStatus.tsx similarity index 100% rename from frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalStatus.tsx rename to frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalStatus.tsx diff --git a/frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalStatus.types.ts b/frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalStatus.types.ts similarity index 100% rename from frontend/svalyn-studio-app/src/changeproposals/overview/ChangeProposalStatus.types.ts rename to frontend/svalyn-studio-app/src/projects/changeproposal/overview/ChangeProposalStatus.types.ts diff --git a/frontend/svalyn-studio-app/src/projects/changeproposals/ChangeProposalsTableToolbar.tsx b/frontend/svalyn-studio-app/src/projects/changeproposals/ChangeProposalsTableToolbar.tsx index 993248ce..ec47036d 100644 --- a/frontend/svalyn-studio-app/src/projects/changeproposals/ChangeProposalsTableToolbar.tsx +++ b/frontend/svalyn-studio-app/src/projects/changeproposals/ChangeProposalsTableToolbar.tsx @@ -18,11 +18,14 @@ */ import ClearIcon from '@mui/icons-material/Clear'; +import DifferenceIcon from '@mui/icons-material/Difference'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Toolbar from '@mui/material/Toolbar'; import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; +import { Link as RouterLink } from 'react-router-dom'; +import { useProject } from '../useProject'; import { ChangeProposalsTableToolbarProps } from './ChangeProposalsTableToolbar.types'; export const ChangeProposalsTableToolbar = ({ @@ -30,11 +33,12 @@ export const ChangeProposalsTableToolbar = ({ onDelete, role, }: ChangeProposalsTableToolbarProps) => { + const { identifier: projectIdentifier } = useProject(); return ( Change Proposals - {selectedChangeProposalsCount > 0 ? ( - theme.spacing(1) }}> + theme.spacing(1) }}> + {selectedChangeProposalsCount > 0 ? ( - - ) : null} + ) : null} + + ); }; diff --git a/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.tsx b/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.tsx index af62709a..bb28b9ad 100644 --- a/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.tsx +++ b/frontend/svalyn-studio-app/src/projects/changeproposals/ProjectChangeProposalsView.tsx @@ -20,7 +20,6 @@ import { gql, useMutation, useQuery } from '@apollo/client'; import DifferenceIcon from '@mui/icons-material/Difference'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import Checkbox from '@mui/material/Checkbox'; import Container from '@mui/material/Container'; import Link from '@mui/material/Link'; @@ -31,11 +30,11 @@ import TableCell from '@mui/material/TableCell'; import TableContainer from '@mui/material/TableContainer'; import TablePagination from '@mui/material/TablePagination'; import TableRow from '@mui/material/TableRow'; -import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { useSnackbar } from 'notistack'; import { useEffect, useState } from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import { ProjectViewHeader } from '../ProjectViewHeader'; import { useProject } from '../useProject'; import { ChangeProposalsTableHead } from './ChangeProposalsTableHead'; import { ChangeProposalsTableToolbar } from './ChangeProposalsTableToolbar'; @@ -202,27 +201,10 @@ export const ProjectChangeProposalsView = () => { const changeProposals = state.project?.changeProposals.edges.map((edge) => edge.node) ?? []; return (
- `1px solid ${theme.palette.divider}`, - }} - > - theme.spacing(2) }}> - - Change proposals - - - + + + Change proposals + theme.spacing(4) }}> { {changeProposal.name} diff --git a/frontend/svalyn-studio-app/src/projects/settings/DetailsCard.tsx b/frontend/svalyn-studio-app/src/projects/settings/DetailsCard.tsx new file mode 100644 index 00000000..a2879fcf --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/settings/DetailsCard.tsx @@ -0,0 +1,262 @@ +/* + * 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, useMutation } from '@apollo/client'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { useSnackbar } from 'notistack'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { hasMinLength, useForm } from '../../forms/useForm'; +import { useProject } from '../useProject'; +import { + DescriptionFormData, + DescriptionFormProps, + DetailsCardProps, + ErrorPayload, + NameFormData, + NameFormProps, + UpdateProjectDescriptionData, + UpdateProjectDescriptionVariables, + UpdateProjectNameData, + UpdateProjectNameVariables, +} from './DetailsCard.types'; + +export const DetailsCard = ({}: DetailsCardProps) => { + return ( + theme.spacing(2), marginTop: (theme) => theme.spacing(4) }}> + + General + + theme.spacing(2), + marginTop: (theme) => theme.spacing(2), + }} + > + + + + + ); +}; + +const updateProjectNameMutation = gql` + mutation updateProjectName($input: UpdateProjectNameInput!) { + updateProjectName(input: $input) { + ... on ErrorPayload { + message + } + } + } +`; + +export const NameForm = ({}: NameFormProps) => { + const { + identifier: projectIdentifier, + organization: { role }, + } = useProject(); + + const { data, isFormValid, getTextFieldProps } = useForm({ + initialValue: { + name: '', + }, + validationRules: { + name: (data) => hasMinLength(data.name, 1), + }, + }); + + const navigate = useNavigate(); + const { enqueueSnackbar } = useSnackbar(); + + const [ + updateProjectName, + { loading: updateProjectNameLoading, data: updateProjectNameData, error: updateProjectNameError }, + ] = useMutation(updateProjectNameMutation); + useEffect(() => { + if (!updateProjectNameLoading) { + if (updateProjectNameData) { + const { updateProjectName } = updateProjectNameData; + if (updateProjectName.__typename === 'SuccessPayload') { + navigate(`/projects/${projectIdentifier}`); + } else if (updateProjectName.__typename === 'ErrorPayload') { + const errorPayload = updateProjectName as ErrorPayload; + enqueueSnackbar(errorPayload.message, { variant: 'error' }); + } + } + if (updateProjectNameError) { + enqueueSnackbar(updateProjectNameError.message, { variant: 'error' }); + } + } + }, [updateProjectNameLoading, updateProjectNameData, updateProjectNameError]); + + const handleUpdateProjectName: React.FormEventHandler = (event) => { + event.preventDefault(); + + const variables: UpdateProjectNameVariables = { + input: { + id: crypto.randomUUID(), + projectIdentifier, + name: data.name, + }, + }; + updateProjectName({ variables }); + }; + + return ( +
+ + + + ); +}; + +const updateProjectDescriptionMutation = gql` + mutation updateProjectDescription($input: UpdateProjectDescriptionInput!) { + updateProjectDescription(input: $input) { + ... on ErrorPayload { + message + } + } + } +`; + +export const DescriptionForm = ({}: DescriptionFormProps) => { + const { + identifier: projectIdentifier, + organization: { role }, + } = useProject(); + + const { data, isFormValid, getTextFieldProps } = useForm({ + initialValue: { + description: '', + }, + validationRules: { + description: (data) => hasMinLength(data.description, 1), + }, + }); + + const navigate = useNavigate(); + const { enqueueSnackbar } = useSnackbar(); + + const [ + updateProjectDescription, + { + loading: updateProjectDescriptionLoading, + data: updateProjectDescriptionData, + error: updateProjectDescriptionError, + }, + ] = useMutation(updateProjectDescriptionMutation); + useEffect(() => { + if (!updateProjectDescriptionLoading) { + if (updateProjectDescriptionData) { + const { updateProjectDescription } = updateProjectDescriptionData; + if (updateProjectDescription.__typename === 'SuccessPayload') { + navigate(`/projects/${projectIdentifier}`); + } else if (updateProjectDescription.__typename === 'ErrorPayload') { + const errorPayload = updateProjectDescription as ErrorPayload; + enqueueSnackbar(errorPayload.message, { variant: 'error' }); + } + } + if (updateProjectDescriptionError) { + enqueueSnackbar(updateProjectDescriptionError.message, { variant: 'error' }); + } + } + }, [updateProjectDescriptionLoading, updateProjectDescriptionData, updateProjectDescriptionError]); + + const handleUpdateProjectDescription: React.FormEventHandler = (event) => { + event.preventDefault(); + + const variables: UpdateProjectDescriptionVariables = { + input: { + id: crypto.randomUUID(), + projectIdentifier, + description: data.description, + }, + }; + updateProjectDescription({ variables }); + }; + + return ( +
+ + + + ); +}; diff --git a/frontend/svalyn-studio-app/src/projects/settings/DetailsCard.types.ts b/frontend/svalyn-studio-app/src/projects/settings/DetailsCard.types.ts new file mode 100644 index 00000000..b08ee4ae --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/settings/DetailsCard.types.ts @@ -0,0 +1,73 @@ +/* + * 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 DetailsCardProps {} + +export interface NameFormProps {} + +export interface NameFormData { + name: string; +} + +export interface UpdateProjectNameData { + updateProjectName: UpdateProjectNamePayload; +} + +export interface UpdateProjectNamePayload { + __typename: string; +} + +export interface UpdateProjectNameVariables { + input: UpdateProjectNameInput; +} + +export interface UpdateProjectNameInput { + id: string; + projectIdentifier: string; + name: string; +} + +export interface DescriptionFormProps {} + +export interface DescriptionFormData { + description: string; +} + +export interface UpdateProjectDescriptionData { + updateProjectDescription: UpdateProjectDescriptionPayload; +} + +export interface UpdateProjectDescriptionPayload { + __typename: string; +} + +export interface UpdateProjectDescriptionVariables { + input: UpdateProjectDescriptionInput; +} + +export interface UpdateProjectDescriptionInput { + id: string; + projectIdentifier: string; + description: string; +} + +export interface ErrorPayload extends UpdateProjectDescriptionPayload, UpdateProjectNamePayload { + __typename: 'ErrorPayload'; + message: string; +} diff --git a/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.tsx b/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.tsx index e21f3c57..0093a0aa 100644 --- a/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.tsx +++ b/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.tsx @@ -17,48 +17,18 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { gql, useMutation } from '@apollo/client'; import SettingsIcon from '@mui/icons-material/Settings'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Container from '@mui/material/Container'; import Paper from '@mui/material/Paper'; -import TextField from '@mui/material/TextField'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; -import { useSnackbar } from 'notistack'; -import { useEffect, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { useState } from 'react'; import { useProject } from '../useProject'; import { DeleteProjectDialog } from './DeleteProjectDialog'; -import { - ErrorPayload, - ProjectSettingsViewState, - UpdateProjectDescriptionData, - UpdateProjectDescriptionVariables, - UpdateProjectNameData, - UpdateProjectNameVariables, -} from './ProjectSettingsView.types'; - -const updateProjectNameMutation = gql` - mutation updateProjectName($input: UpdateProjectNameInput!) { - updateProjectName(input: $input) { - ... on ErrorPayload { - message - } - } - } -`; - -const updateProjectDescriptionMutation = gql` - mutation updateProjectDescription($input: UpdateProjectDescriptionInput!) { - updateProjectDescription(input: $input) { - ... on ErrorPayload { - message - } - } - } -`; +import { DetailsCard } from './DetailsCard'; +import { ProjectSettingsViewState } from './ProjectSettingsView.types'; export const ProjectSettingsView = () => { const { @@ -66,97 +36,9 @@ export const ProjectSettingsView = () => { organization: { role }, } = useProject(); const [state, setState] = useState({ - name: '', - description: '', deleteProjectDialogOpen: false, }); - const { enqueueSnackbar } = useSnackbar(); - - const handleNameChange: React.ChangeEventHandler = (event) => { - const { - target: { value }, - } = event; - setState((prevState) => ({ ...prevState, name: value })); - }; - - const handleDescriptionChange: React.ChangeEventHandler = (event) => { - const { - target: { value }, - } = event; - setState((prevState) => ({ ...prevState, description: value })); - }; - - const navigate = useNavigate(); - - const [ - updateProjectName, - { loading: updateProjectNameLoading, data: updateProjectNameData, error: updateProjectNameError }, - ] = useMutation(updateProjectNameMutation); - useEffect(() => { - if (!updateProjectNameLoading) { - if (updateProjectNameData) { - const { updateProjectName } = updateProjectNameData; - if (updateProjectName.__typename === 'SuccessPayload') { - navigate(`/projects/${projectIdentifier}`); - } else if (updateProjectName.__typename === 'ErrorPayload') { - const errorPayload = updateProjectName as ErrorPayload; - enqueueSnackbar(errorPayload.message, { variant: 'error' }); - } - } - if (updateProjectNameError) { - enqueueSnackbar(updateProjectNameError.message, { variant: 'error' }); - } - } - }, [updateProjectNameLoading, updateProjectNameData, updateProjectNameError]); - - const handleUpdateProjectName: React.MouseEventHandler = () => { - const variables: UpdateProjectNameVariables = { - input: { - id: crypto.randomUUID(), - projectIdentifier, - name: state.name, - }, - }; - updateProjectName({ variables }); - }; - - const [ - updateProjectDescription, - { - loading: updateProjectDescriptionLoading, - data: updateProjectDescriptionData, - error: updateProjectDescriptionError, - }, - ] = useMutation(updateProjectDescriptionMutation); - useEffect(() => { - if (!updateProjectDescriptionLoading) { - if (updateProjectDescriptionData) { - const { updateProjectDescription } = updateProjectDescriptionData; - if (updateProjectDescription.__typename === 'SuccessPayload') { - navigate(`/projects/${projectIdentifier}`); - } else if (updateProjectDescription.__typename === 'ErrorPayload') { - const errorPayload = updateProjectDescription as ErrorPayload; - enqueueSnackbar(errorPayload.message, { variant: 'error' }); - } - } - if (updateProjectDescriptionError) { - enqueueSnackbar(updateProjectDescriptionError.message, { variant: 'error' }); - } - } - }, [updateProjectDescriptionLoading, updateProjectDescriptionData, updateProjectDescriptionError]); - - const handleUpdateProjectDescription: React.MouseEventHandler = () => { - const variables: UpdateProjectDescriptionVariables = { - input: { - id: crypto.randomUUID(), - projectIdentifier, - description: state.description, - }, - }; - updateProjectDescription({ variables }); - }; - const openDeleteProjectDialog: React.MouseEventHandler = () => { setState((prevState) => ({ ...prevState, deleteProjectDialogOpen: true })); }; @@ -168,84 +50,26 @@ export const ProjectSettingsView = () => { <>
`1px solid ${theme.palette.divider}`, }} > theme.spacing(2) }}> - - Settings + + Settings - theme.spacing(3), marginTop: (theme) => theme.spacing(4) }}> - - General - - theme.spacing(2), - marginTop: (theme) => theme.spacing(2), - }} - > - - - - - theme.spacing(2), - marginTop: (theme) => theme.spacing(2), - }} - > - - - - + - theme.spacing(3), marginTop: (theme) => theme.spacing(4) }}> - + theme.spacing(2), marginTop: (theme) => theme.spacing(4) }}> + Delete this project Once you delete a project, there is no going back. Please be certain. - diff --git a/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.types.ts b/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.types.ts index ce269b8f..e4fe7720 100644 --- a/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.types.ts +++ b/frontend/svalyn-studio-app/src/projects/settings/ProjectSettingsView.types.ts @@ -18,48 +18,5 @@ */ export interface ProjectSettingsViewState { - name: string; - description: string; deleteProjectDialogOpen: boolean; } - -export interface UpdateProjectNameData { - updateProjectName: UpdateProjectNamePayload; -} - -export interface UpdateProjectNamePayload { - __typename: string; -} - -export interface UpdateProjectNameVariables { - input: UpdateProjectNameInput; -} - -export interface UpdateProjectNameInput { - id: string; - projectIdentifier: string; - name: string; -} - -export interface UpdateProjectDescriptionData { - updateProjectDescription: UpdateProjectDescriptionPayload; -} - -export interface UpdateProjectDescriptionPayload { - __typename: string; -} - -export interface UpdateProjectDescriptionVariables { - input: UpdateProjectDescriptionInput; -} - -export interface UpdateProjectDescriptionInput { - id: string; - projectIdentifier: string; - description: string; -} - -export interface ErrorPayload extends UpdateProjectDescriptionPayload, UpdateProjectNamePayload { - __typename: 'ErrorPayload'; - message: string; -} diff --git a/frontend/svalyn-studio-app/src/projects/tags/NewTagCard.tsx b/frontend/svalyn-studio-app/src/projects/tags/NewTagCard.tsx new file mode 100644 index 00000000..6b8f01f5 --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/tags/NewTagCard.tsx @@ -0,0 +1,137 @@ +/* + * 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, useMutation } from '@apollo/client'; +import TagIcon from '@mui/icons-material/Tag'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { useSnackbar } from 'notistack'; +import { useEffect } from 'react'; +import { hasMinLength, useForm } from '../../forms/useForm'; +import { useProject } from '../useProject'; +import { + AddTagToProjectData, + AddTagToProjectVariables, + ErrorPayload, + NewTagCardProps, + NewTagFormData, +} from './NewTagCard.types'; + +const addTagToProjectMutation = gql` + mutation addTagToProject($input: AddTagToProjectInput!) { + addTagToProject(input: $input) { + __typename + ... on ErrorPayload { + message + } + } + } +`; + +export const NewTagCard = ({ onTagCreated }: NewTagCardProps) => { + const { + identifier: projectIdentifier, + organization: { role }, + } = useProject(); + + const { data, isFormValid, reset, getTextFieldProps } = useForm({ + initialValue: { + key: '', + value: '', + }, + validationRules: { + key: (data) => hasMinLength(data.key, 1), + value: (data) => hasMinLength(data.value, 1), + }, + }); + + const { enqueueSnackbar } = useSnackbar(); + + const [addTagToProject, { data: addTagToProjectData, error: addTagToProjectError }] = useMutation< + AddTagToProjectData, + AddTagToProjectVariables + >(addTagToProjectMutation); + useEffect(() => { + if (addTagToProjectData) { + if (addTagToProjectData.addTagToProject.__typename === 'ErrorPayload') { + const errorPayload = addTagToProjectData.addTagToProject as ErrorPayload; + enqueueSnackbar(errorPayload.message, { variant: 'error' }); + } else if (addTagToProjectData.addTagToProject.__typename === 'SuccessPayload') { + reset(); + onTagCreated(); + } + } + if (addTagToProjectError) { + enqueueSnackbar(addTagToProjectError.message, { variant: 'error' }); + } + }, [addTagToProjectData, addTagToProjectError]); + + const handleAddTagToProject: React.FormEventHandler = (event) => { + event.preventDefault(); + + const variables: AddTagToProjectVariables = { + input: { + id: crypto.randomUUID(), + projectIdentifier, + key: data.key, + value: data.value, + }, + }; + addTagToProject({ variables }); + }; + + return ( + theme.spacing(2), + padding: (theme) => theme.spacing(2), + }} + > + Add new tag +
+ theme.spacing(2), + }} + > + + + + +
+
+ ); +}; diff --git a/frontend/svalyn-studio-app/src/projects/tags/NewTagCard.types.ts b/frontend/svalyn-studio-app/src/projects/tags/NewTagCard.types.ts new file mode 100644 index 00000000..52025ebb --- /dev/null +++ b/frontend/svalyn-studio-app/src/projects/tags/NewTagCard.types.ts @@ -0,0 +1,51 @@ +/* + * 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 NewTagCardProps { + onTagCreated: () => void; +} + +export interface NewTagFormData { + key: string; + value: string; +} + +export interface AddTagToProjectVariables { + input: AddTagToProjectInput; +} + +export interface AddTagToProjectInput { + id: string; + projectIdentifier: string; + key: string; + value: string; +} + +export interface AddTagToProjectData { + addTagToProject: AddTagToProjectPayload; +} + +export interface AddTagToProjectPayload { + __typename: string; +} + +export interface ErrorPayload extends AddTagToProjectPayload { + __typename: 'ErrorPayload'; + message: string; +} diff --git a/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.tsx b/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.tsx index 141280d3..df6bf60b 100644 --- a/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.tsx +++ b/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.tsx @@ -17,11 +17,9 @@ * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { gql, useMutation, useQuery } from '@apollo/client'; -import AddIcon from '@mui/icons-material/Add'; +import { gql, useQuery } from '@apollo/client'; import TagIcon from '@mui/icons-material/Tag'; import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; import Container from '@mui/material/Container'; import Paper from '@mui/material/Paper'; import Table from '@mui/material/Table'; @@ -31,20 +29,13 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TablePagination from '@mui/material/TablePagination'; import TableRow from '@mui/material/TableRow'; -import TextField from '@mui/material/TextField'; -import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import { useSnackbar } from 'notistack'; import { useEffect, useState } from 'react'; +import { ProjectViewHeader } from '../ProjectViewHeader'; import { useProject } from '../useProject'; -import { - AddTagToProjectData, - AddTagToProjectVariables, - ErrorPayload, - GetProjectTagsData, - GetProjectTagsVariables, - ProjectTagsViewState, -} from './ProjectTagsView.types'; +import { NewTagCard } from './NewTagCard'; +import { GetProjectTagsData, GetProjectTagsVariables, ProjectTagsViewState } from './ProjectTagsView.types'; const getProjectTagsQuery = gql` query getProjectTags($identifier: ID!, $page: Int!, $rowsPerPage: Int!) { @@ -67,25 +58,9 @@ const getProjectTagsQuery = gql` } `; -const addTagToProjectMutation = gql` - mutation addTagToProject($input: AddTagToProjectInput!) { - addTagToProject(input: $input) { - __typename - ... on ErrorPayload { - message - } - } - } -`; - export const ProjectTagsView = () => { - const { - identifier: projectIdentifier, - organization: { role }, - } = useProject(); + const { identifier: projectIdentifier } = useProject(); const [state, setState] = useState({ - key: '', - value: '', page: 0, rowsPerPage: 10, }); @@ -106,69 +81,15 @@ export const ProjectTagsView = () => { } }, [error]); - const handleKeyChange: React.ChangeEventHandler = (event) => { - const { - target: { value }, - } = event; - setState((prevState) => ({ ...prevState, key: value })); - }; - - const handleValueChange: React.ChangeEventHandler = (event) => { - const { - target: { value }, - } = event; - setState((prevState) => ({ ...prevState, value })); - }; - - const [addTagToProject, { data: addTagToProjectData, error: addTagToProjectError }] = useMutation< - AddTagToProjectData, - AddTagToProjectVariables - >(addTagToProjectMutation); - useEffect(() => { - if (addTagToProjectData) { - if (addTagToProjectData.addTagToProject.__typename === 'ErrorPayload') { - const errorPayload = addTagToProjectData.addTagToProject as ErrorPayload; - enqueueSnackbar(errorPayload.message, { variant: 'error' }); - } else if (addTagToProjectData.addTagToProject.__typename === 'SuccessPayload') { - setState((prevState) => ({ ...prevState, key: '', value: '' })); - refetch(variables); - } - } - if (addTagToProjectError) { - enqueueSnackbar(addTagToProjectError.message, { variant: 'error' }); - } - }, [addTagToProjectData, addTagToProjectError]); - - const handleAddTag: React.MouseEventHandler = () => { - const variables: AddTagToProjectVariables = { - input: { - id: crypto.randomUUID(), - projectIdentifier, - key: state.key, - value: state.value, - }, - }; - addTagToProject({ variables }); - }; - const onPageChange = (_: React.MouseEvent | null, page: number) => setState((prevState) => ({ ...prevState, page })); - const isValidNewTag = state.key.trim().length > 0 && state.value.trim().length > 0; - return (
- `1px solid ${theme.palette.divider}`, - }} - > - theme.spacing(2) }}> - - Tags - - + + + Tags + { paddingTop: (theme) => theme.spacing(4), }} > - theme.spacing(2), - padding: (theme) => theme.spacing(3), - }} - > - Tags - theme.spacing(2), - }} - > - - - - - + refetch(variables)} /> {data && data.viewer && data.viewer.project && data.viewer.project.tags.edges.length > 0 ? ( <> diff --git a/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.types.ts b/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.types.ts index a9f42a4a..96ea075b 100644 --- a/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.types.ts +++ b/frontend/svalyn-studio-app/src/projects/tags/ProjectTagsView.types.ts @@ -18,8 +18,6 @@ */ export interface ProjectTagsViewState { - key: string; - value: string; page: number; rowsPerPage: number; } @@ -60,27 +58,3 @@ export interface Tag { export interface PageInfo { count: number; } - -export interface AddTagToProjectVariables { - input: AddTagToProjectInput; -} - -export interface AddTagToProjectInput { - id: string; - projectIdentifier: string; - key: string; - value: string; -} - -export interface AddTagToProjectData { - addTagToProject: AddTagToProjectPayload; -} - -export interface AddTagToProjectPayload { - __typename: string; -} - -export interface ErrorPayload extends AddTagToProjectPayload { - __typename: 'ErrorPayload'; - message: string; -}