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 (
-
- );
-};
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}
+ }
+ to={`/projects/${projectIdentifier}/new/changeproposal`}
+ >
+ New Change Proposal
+
+
);
};
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
+
+
+ );
+};
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 (