Skip to content

Commit

Permalink
Be able to remove Graph and Graph Collections
Browse files Browse the repository at this point in the history
  • Loading branch information
Peyton-McKee committed Jan 21, 2025
1 parent eca3e8d commit 24037ec
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 6 deletions.
30 changes: 30 additions & 0 deletions src/backend/src/controllers/statistics.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,34 @@ export default class StatisticsController {
next(error);
}
}

static async removeGraphFromGraphCollection(req: Request, res: Response, next: NextFunction) {
try {
const { graphCollectionId, graphId } = req.params;
console.log('test');
const message: { message: string } = await StatisticsService.removeGraphFromCollection(
req.currentUser,
graphCollectionId,
graphId,
req.organization
);
res.status(200).json(message);
} catch (error: unknown) {
next(error);
}
}

static async deleteGraphCollection(req: Request, res: Response, next: NextFunction) {
try {
const { graphCollectionId } = req.params;
const message: { message: string } = await StatisticsService.deleteGraphCollection(
req.currentUser,
graphCollectionId,
req.organization
);
res.status(200).json(message);
} catch (error: unknown) {
next(error);
}
}
}
7 changes: 7 additions & 0 deletions src/backend/src/routes/statistics.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,11 @@ statisticsRouter.post(
StatisticsController.editGraphCollection
);

statisticsRouter.post(
'/graph-collections/:graphCollectionId/remove/:graphId',
StatisticsController.removeGraphFromGraphCollection
);

statisticsRouter.delete('/graph-collections/:graphCollectionId/delete', StatisticsController.deleteGraphCollection);

export default statisticsRouter;
114 changes: 114 additions & 0 deletions src/backend/src/services/statistics.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,15 @@ export default class StatisticsService {
);
}

/**
* Creates a graph collection in the database
*
* @param user The user who is creating the graph collection
* @param title The title of the graph collection that is being created
* @param specialPermissions Any special permissions related to the graph collection
* @param organization The organization the collection is in
* @returns The created graph collection
*/
static async createGraphCollection(
user: User,
title: string,
Expand Down Expand Up @@ -391,6 +400,16 @@ export default class StatisticsService {
);
}

/**
* Edits the given graph collection with the updated values
*
* @param user The user who is editing the graph collection
* @param graphCollectionId The id of the collection that is being edited
* @param title The new title of the collection
* @param specialPermission The new permissions of the collection
* @param organization The organization that the user is currently in
* @returns The updated Graph collection
*/
static async editGraphCollection(
user: User,
graphCollectionId: string,
Expand Down Expand Up @@ -442,4 +461,99 @@ export default class StatisticsService {
)
);
}

/**
* Removes a graph from the given graph collection
*
* @param user The user who is removing the graph
* @param graphCollectionId The collection that the graph will be removed from
* @param graphId The graph that is being removed
* @param organization The organization the user is currently in
*/
static async removeGraphFromCollection(
user: User,
graphCollectionId: string,
graphId: string,
organization: Organization
): Promise<{ message: string }> {
if (!(await userHasPermissionNew(user.userId, organization.organizationId, [Permission.EDIT_GRAPH_COLLECTION]))) {
throw new AccessDeniedException('You do not have permission to edit graph collections');
}

const graph = await prisma.graph.findUnique({
where: { id: graphId, organizationId: organization.organizationId }
});

if (!graph) {
throw new NotFoundException('Graph', graphId);
}
if (graph.dateDeleted) {
throw new DeletedException('Graph', graphId);
}

const collection = await prisma.graph_Collection.findUnique({
where: { id: graphCollectionId, organizationId: organization.organizationId }
});

if (!collection) {
throw new NotFoundException('Graph Collection', graphCollectionId);
}
if (collection.dateDeleted) {
throw new DeletedException('Graph Collection', graphCollectionId);
}

console.log('test');

await prisma.graph.update({
where: { id: graphId },
data: {
graphCollectionId: null
},
...getGraphQueryArgs(organization.organizationId)
});

return { message: 'Graph unlinked' };
}

/**
* Deletes a graph collection
*
* @param user The user who is deleting the graph collection
* @param graphCollectionId The collection to be deleted
* @param organization The organization the user is currently in
*/
static async deleteGraphCollection(
user: User,
graphCollectionId: string,
organization: Organization
): Promise<{ message: string }> {
if (!(await userHasPermissionNew(user.userId, organization.organizationId, [Permission.DELETE_GRAPH_COLLECTION]))) {
throw new AccessDeniedException('You do not have permission to edit graph collections');
}

const collection = await prisma.graph_Collection.findUnique({
where: { id: graphCollectionId, organizationId: organization.organizationId }
});

if (!collection) {
throw new NotFoundException('Graph Collection', graphCollectionId);
}
if (collection.dateDeleted) {
throw new DeletedException('Graph Collection', graphCollectionId);
}

await prisma.graph_Collection.update({
where: { id: graphCollectionId },
data: {
dateDeleted: new Date(),
userDeleted: {
connect: {
userId: user.userId
}
}
}
});

return { message: 'Graph Deleted' };
}
}
8 changes: 8 additions & 0 deletions src/frontend/src/apis/statistics.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,11 @@ export const updateGraphCollection = (id: string, payload: GraphCollectionFormIn
transformResponse: (data) => graphCollectionTransformer(JSON.parse(data))
});
};

export const deleteGraphCollection = (id: string) => {
return axios.delete<{ message: string }>(apiUrls.deleteGraphCollection(id));
};

export const removeGraphFromCollection = (collectionId: string, graphId: string) => {
return axios.post<{ message: string }>(apiUrls.removeGraphFromGraphCollection(collectionId, graphId));
};
32 changes: 29 additions & 3 deletions src/frontend/src/components/GraphCollectionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,43 @@
import { Card, CardContent, Grid, Link, Typography } from '@mui/material';
import { Card, CardContent, Grid, IconButton, Link, Typography } from '@mui/material';
import { GraphCollection } from 'shared';
import { datePipe, displayEnum, fullNamePipe } from '../utils/pipes';
import { Link as RouterLink } from 'react-router-dom';
import { Construction } from '@mui/icons-material';
import { Construction, Delete } from '@mui/icons-material';
import { DateRangeIcon } from '@mui/x-date-pickers';
import LoadingIndicator from './LoadingIndicator';
import { useDeleteGraphCollection } from '../hooks/statistics.hooks';
import { useToast } from '../hooks/toasts.hooks';

interface GraphCollectionCardProps {
graphCollection: GraphCollection;
}

const GraphCollectionCard = ({ graphCollection }: GraphCollectionCardProps) => {
const { isLoading: removeGraphCollectionIsLoading, mutateAsync: removeGraphCollection } = useDeleteGraphCollection(
graphCollection.id
);
const toast = useToast();

if (removeGraphCollectionIsLoading) {
return <LoadingIndicator />;
}

const onDeletePressed = async () => {
try {
await removeGraphCollection();
toast.success('Successfully deleted collection');
} catch (error) {
if (error instanceof Error) {
toast.error('Failed to delete collection' + error.message);
}
}
};

return (
<Card sx={{ width: '100%', borderRadius: 5 }}>
<Card sx={{ width: '100%', borderRadius: 5, position: 'relative' }}>
<IconButton sx={{ position: 'absolute', top: 5, right: 5 }} onClick={onDeletePressed}>
<Delete />
</IconButton>
<CardContent>
<Grid container spacing={1}>
<Grid item xs={12}>
Expand Down
43 changes: 43 additions & 0 deletions src/frontend/src/hooks/statistics.hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { CreateGraphArgs, Graph, GraphCollection, GraphCollectionFormInput } fro
import {
createGraph,
createGraphCollection,
deleteGraphCollection,
getAllGraphCollections,
getSingleGraph,
getSingleGraphCollection,
removeGraphFromCollection,
updateGraph,
updateGraphCollection
} from '../apis/statistics.api';
Expand Down Expand Up @@ -118,3 +120,44 @@ export const useUpdateGraphCollection = (id: string) => {
}
);
};

/**
* Custom react hook to remove a graph from a graph collection
*
* @param collectionId The id of the graph collection to update
* @param graphId The id of the graph to remove from the collection
* @returns Mutation function to remove the graph from the collection
*/
export const useRemoveGraphFromCollection = (collectionId: string, graphId: string) => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, Error>(
[],
async () => {
const { data } = await removeGraphFromCollection(collectionId, graphId);
return data;
},
{
onSuccess: () => queryClient.invalidateQueries(['graph-collections', collectionId])
}
);
};

/**
* Custom react hook to delete a graph collection
*
* @param id The id of the graph collection to delete
* @returns Mutation function to delete the graph collection with the given id
*/
export const useDeleteGraphCollection = (id: string) => {
const queryClient = useQueryClient();
return useMutation<{ message: string }, Error>(
[],
async () => {
const { data } = await deleteGraphCollection(id);
return data;
},
{
onSuccess: () => queryClient.invalidateQueries(['graph-collections'])
}
);
};
25 changes: 23 additions & 2 deletions src/frontend/src/pages/StatisticsPage/GraphView/GraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { Box, IconButton, Typography } from '@mui/material';
import { Graph, GraphDisplayType } from 'shared';
import GraphBarChartView from './GraphBarChartView';
import GraphPieChartView from './GraphPieChartView';
import { Edit } from '@mui/icons-material';
import { Delete, Edit } from '@mui/icons-material';
import { useHistory, useParams } from 'react-router-dom';
import { datePipe } from '../../../utils/pipes';
import { useGetCarsByIds } from '../../../hooks/cars.hooks';
import ErrorPage from '../../ErrorPage';
import LoadingIndicator from '../../../components/LoadingIndicator';
import { useRemoveGraphFromCollection } from '../../../hooks/statistics.hooks';
import { useToast } from '../../../hooks/toasts.hooks';

interface GraphViewProps {
graph: Graph;
Expand All @@ -18,15 +20,31 @@ const GraphView = ({ graph, height = 500 }: GraphViewProps) => {
const history = useHistory();
const { graphCollectionId } = useParams<{ graphCollectionId: string }>();
const { isLoading, data: cars, error, isError } = useGetCarsByIds(new Set(graph.carIds));
const { isLoading: removeGraphIsLoading, mutateAsync: removeGraph } = useRemoveGraphFromCollection(
graphCollectionId,
graph.graphId
);
const toast = useToast();

if (isError) {
return <ErrorPage error={error} />;
}

if (isLoading || !cars) {
if (isLoading || !cars || removeGraphIsLoading) {
return <LoadingIndicator />;
}

const onRemovePressed = async () => {
try {
await removeGraph();
toast.success('Successfully removed graph');
} catch (error) {
if (error instanceof Error) {
toast.error('Failed to remove graph: ' + error.message);
}
}
};

const Graph = () => {
switch (graph.graphDisplayType) {
case GraphDisplayType.BAR:
Expand All @@ -50,6 +68,9 @@ const GraphView = ({ graph, height = 500 }: GraphViewProps) => {
>
<Edit />
</IconButton>
<IconButton sx={{ height: 40 }} onClick={onRemovePressed}>
<Delete />
</IconButton>
</Box>
<Typography textAlign={'center'} fontWeight={'regular'} fontSize={20} variant="h6" noWrap>
{!graph.startDate && !graph.endDate
Expand Down
4 changes: 3 additions & 1 deletion src/frontend/src/utils/task.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export const taskPriorityColor = (task: Task) => {
};

export const getOverdueTasks = (tasks: Task[]) => {
const overdueTasks = new Set(tasks.filter((task) => (task.deadline ? daysOverdue(new Date(task.deadline)) : 0) > 0));
const overdueTasks = new Set(
tasks.filter((task) => task.status !== TaskStatus.DONE && (task.deadline ? daysOverdue(new Date(task.deadline)) : 0) > 0)
);
return [...overdueTasks];
};
5 changes: 5 additions & 0 deletions src/frontend/src/utils/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,9 @@ const createGraphCollection = () => `${graphCollections()}/create`;
const getGraphById = (id: string) => `${statistics()}/graph/${id}`;
const updateGraph = (id: string) => `${getGraphById(id)}/edit`;
const updateGraphCollection = (id: string) => `${graphCollectionById(id)}/edit`;
const removeGraphFromGraphCollection = (graphCollectionId: string, graphId: string) =>
`${graphCollectionById(graphCollectionId)}/remove/${graphId}`;
const deleteGraphCollection = (id: string) => `${graphCollectionById(id)}/delete`;

/**************** Other Endpoints ****************/
const version = () => `https://api.github.com/repos/Northeastern-Electric-Racing/FinishLine/releases/latest`;
Expand Down Expand Up @@ -425,6 +428,8 @@ export const apiUrls = {
getGraphById,
updateGraph,
updateGraphCollection,
removeGraphFromGraphCollection,
deleteGraphCollection,

onboarding,
allChecklists,
Expand Down

0 comments on commit 24037ec

Please sign in to comment.