diff --git a/packages/client/src/context/Project.context.tsx b/packages/client/src/context/Project.context.tsx index fe426bd1..b1da75a0 100644 --- a/packages/client/src/context/Project.context.tsx +++ b/packages/client/src/context/Project.context.tsx @@ -24,6 +24,11 @@ export const ProjectProvider: FC = ({ children }) => { useEffect(() => { if (getProjectResults.data) { setProjects(getProjectResults.data.getProjects); + + // Check if the current project is still in the list + if (project && !getProjectResults.data.getProjects.find((p) => p._id === project._id)) { + setProject(null); + } } }, [getProjectResults.data, getProjectResults.error]); diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 5dd5ba12..db152a22 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -296,6 +296,11 @@ export type MutationCreateTagsArgs = { }; +export type MutationDeleteProjectArgs = { + project: Scalars['ID']['input']; +}; + + export type MutationDeleteStudyArgs = { study: Scalars['ID']['input']; }; diff --git a/packages/client/src/graphql/project/project.graphql b/packages/client/src/graphql/project/project.graphql index d5920dd4..4e38a75b 100644 --- a/packages/client/src/graphql/project/project.graphql +++ b/packages/client/src/graphql/project/project.graphql @@ -6,3 +6,7 @@ query getProjects { created } } + +mutation deleteProject($project: ID!) { + deleteProject(project: $project) +} diff --git a/packages/client/src/graphql/project/project.ts b/packages/client/src/graphql/project/project.ts index a2462004..683052c5 100644 --- a/packages/client/src/graphql/project/project.ts +++ b/packages/client/src/graphql/project/project.ts @@ -10,6 +10,13 @@ export type GetProjectsQueryVariables = Types.Exact<{ [key: string]: never; }>; export type GetProjectsQuery = { __typename?: 'Query', getProjects: Array<{ __typename?: 'Project', _id: string, name: string, description: string, created: any }> }; +export type DeleteProjectMutationVariables = Types.Exact<{ + project: Types.Scalars['ID']['input']; +}>; + + +export type DeleteProjectMutation = { __typename?: 'Mutation', deleteProject: boolean }; + export const GetProjectsDocument = gql` query getProjects { @@ -47,4 +54,35 @@ export function useGetProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOption } export type GetProjectsQueryHookResult = ReturnType; export type GetProjectsLazyQueryHookResult = ReturnType; -export type GetProjectsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetProjectsQueryResult = Apollo.QueryResult; +export const DeleteProjectDocument = gql` + mutation deleteProject($project: ID!) { + deleteProject(project: $project) +} + `; +export type DeleteProjectMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteProjectMutation__ + * + * To run a mutation, you first call `useDeleteProjectMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteProjectMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteProjectMutation, { data, loading, error }] = useDeleteProjectMutation({ + * variables: { + * project: // value for 'project' + * }, + * }); + */ +export function useDeleteProjectMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteProjectDocument, options); + } +export type DeleteProjectMutationHookResult = ReturnType; +export type DeleteProjectMutationResult = Apollo.MutationResult; +export type DeleteProjectMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file diff --git a/packages/client/src/pages/projects/ProjectControl.tsx b/packages/client/src/pages/projects/ProjectControl.tsx index b98025ad..bc714588 100644 --- a/packages/client/src/pages/projects/ProjectControl.tsx +++ b/packages/client/src/pages/projects/ProjectControl.tsx @@ -1,13 +1,38 @@ import { Box, Typography } from '@mui/material'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { DataGrid, GridColDef, GridRowId } from '@mui/x-data-grid'; import { useProject } from '../../context/Project.context'; import { GridActionsCellItem } from '@mui/x-data-grid-pro'; import DeleteIcon from '@mui/icons-material/DeleteOutlined'; import { Project } from '../../graphql/graphql'; +import { useDeleteProjectMutation } from '../../graphql/project/project'; +import { useConfirmation } from '../../context/Confirmation.context'; +import {useEffect} from 'react'; const ProjectControl: React.FC = () => { - const { projects } = useProject(); + const { projects, updateProjectList } = useProject(); + + const [deleteProjectMutation, deleteProjectResults] = useDeleteProjectMutation(); + const confirmation = useConfirmation(); + + const handleDelete = async (id: GridRowId) => { + // Execute delete mutation + confirmation.pushConfirmationRequest({ + title: 'Delete Study', + message: 'Are you sure you want to delete this project? Doing so will delete all contained studies and tags', + onConfirm: () => { + deleteProjectMutation({ variables: { project: id.toString() } }); + }, + onCancel: () => {} + }); + }; + + // TODO: Add error message + useEffect(() => { + if (deleteProjectResults.called && deleteProjectResults.data) { + updateProjectList(); + } + }, [deleteProjectResults.data, deleteProjectResults.called]); const columns: GridColDef[] = [ { @@ -29,17 +54,12 @@ const ProjectControl: React.FC = () => { width: 120, maxWidth: 120, cellClassName: 'delete', - getActions: () => { - return [} label="Delete" />]; + getActions: (params) => { + return [} label="Delete" onClick={() => handleDelete(params.id)}/>]; } } ]; - // Make sure the lines between rows are visible - const rowStyle = { - borderBottom: '1px solid rgba(224, 224, 224, 1)' - }; - return ( <> Project Control diff --git a/packages/client/src/pages/studies/StudyControl.tsx b/packages/client/src/pages/studies/StudyControl.tsx index e22a1b79..d2aa258c 100644 --- a/packages/client/src/pages/studies/StudyControl.tsx +++ b/packages/client/src/pages/studies/StudyControl.tsx @@ -18,7 +18,7 @@ export const StudyControl: React.FC = () => { // Execute delete mutation confirmation.pushConfirmationRequest({ title: 'Delete Study', - message: 'Are you sure you want to delete this study?', + message: 'Are you sure you want to delete this study? Doing so will delete all contained tags', onConfirm: () => { deleteStudyMutation({ variables: { study: id.toString() } }); }, @@ -26,6 +26,7 @@ export const StudyControl: React.FC = () => { }); }; + // TODO: Add error message useEffect(() => { if (deleteStudyResults.called && deleteStudyResults.data) { updateStudies(); diff --git a/packages/server/schema.gql b/packages/server/schema.gql index a90b0988..e4c45005 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -97,7 +97,7 @@ type Mutation { changeDatasetName(dataset: ID!, newName: String!): Boolean! changeDatasetDescription(dataset: ID!, newDescription: String!): Boolean! signLabCreateProject(project: ProjectCreate!): Project! - deleteProject: Boolean! + deleteProject(project: ID!): Boolean! createStudy(study: StudyCreate!): Study! deleteStudy(study: ID!): Boolean! changeStudyName(study: ID!, newName: String!): Study! diff --git a/packages/server/src/project/project.module.ts b/packages/server/src/project/project.module.ts index 8f8d925c..b0b06f04 100644 --- a/packages/server/src/project/project.module.ts +++ b/packages/server/src/project/project.module.ts @@ -4,10 +4,28 @@ import { ProjectService } from './project.service'; import { MongooseModule } from '@nestjs/mongoose'; import { Project, ProjectSchema } from './project.model'; import { ProjectPipe } from './pipes/project.pipe'; +import { MongooseMiddlewareService } from 'src/shared/service/mongoose-callback.service'; +import { SharedModule } from 'src/shared/shared.module'; @Module({ imports: [ - MongooseModule.forFeature([{ name: Project.name, schema: ProjectSchema }]) + MongooseModule.forFeatureAsync([ + { + name: Project.name, + useFactory: (middlewareService: MongooseMiddlewareService) => { + const schema = ProjectSchema; + + schema.pre('deleteOne', async function () { + const project = await this.model.findOne(this.getQuery()); + await middlewareService.apply(Project.name, 'deleteOne', project); + }); + + return schema; + }, + imports: [SharedModule], + inject: [MongooseMiddlewareService], + } + ]) ], providers: [ProjectResolver, ProjectService, ProjectPipe], exports: [ProjectPipe, ProjectService] diff --git a/packages/server/src/project/project.resolver.ts b/packages/server/src/project/project.resolver.ts index 8ee21226..7ca89eb0 100644 --- a/packages/server/src/project/project.resolver.ts +++ b/packages/server/src/project/project.resolver.ts @@ -1,10 +1,11 @@ import {BadRequestException} from '@nestjs/common'; -import { Resolver, Mutation, Query, Args } from '@nestjs/graphql'; +import { Resolver, Mutation, Query, Args, ID } from '@nestjs/graphql'; import { OrganizationContext } from 'src/organization/organization.context'; import { Organization } from 'src/organization/organization.model'; import { ProjectCreate } from './dtos/create.dto'; import { Project } from './project.model'; import { ProjectService } from './project.service'; +import {ProjectPipe} from './pipes/project.pipe'; @Resolver(() => Project) export class ProjectResolver { @@ -26,7 +27,8 @@ export class ProjectResolver { // TODO: Handle Project deletion @Mutation(() => Boolean) - async deleteProject(): Promise { + async deleteProject(@Args('project', { type: () => ID }, ProjectPipe) project: Project): Promise { + await this.projectService.delete(project); return true; } diff --git a/packages/server/src/project/project.service.ts b/packages/server/src/project/project.service.ts index 41dfcdef..d7b7f2cb 100644 --- a/packages/server/src/project/project.service.ts +++ b/packages/server/src/project/project.service.ts @@ -28,4 +28,8 @@ export class ProjectService { async findAll(organization: string): Promise { return this.projectModel.find({ organization }).exec(); } + + async delete(project: Project): Promise { + await this.projectModel.deleteOne({ _id: project._id }); + } } diff --git a/packages/server/src/study/study.service.ts b/packages/server/src/study/study.service.ts index 2726e68e..b9f13e41 100644 --- a/packages/server/src/study/study.service.ts +++ b/packages/server/src/study/study.service.ts @@ -5,10 +5,16 @@ import { Study } from './study.model'; import { StudyCreate } from './dtos/create.dto'; import { Validator } from 'jsonschema'; import { Project } from 'src/project/project.model'; +import { MongooseMiddlewareService } from 'src/shared/service/mongoose-callback.service'; @Injectable() export class StudyService { - constructor(@InjectModel(Study.name) private readonly studyModel: Model) {} + constructor(@InjectModel(Study.name) private readonly studyModel: Model, middlewareService: MongooseMiddlewareService) { + // Remove cooresponding studies when a project is deleted + middlewareService.register(Project.name, 'deleteOne', async (project: Project) => { + await this.removeForProject(project); + }); + } async create(study: StudyCreate): Promise { return this.studyModel.create(study); @@ -58,4 +64,11 @@ export class StudyService { async delete(study: Study): Promise { await this.studyModel.deleteOne({ _id: study._id }); } + + private async removeForProject(project: Project): Promise { + const studies = await this.findAll(project); + for (const study of studies) { + await this.delete(study); + } + } }