From 8ccffe40b3add776c342a2388bb875dd3d34cb9f Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 8 Dec 2023 10:32:34 -0500 Subject: [PATCH 1/5] Begin work on study deletion --- packages/server/schema.gql | 2 +- packages/server/src/study/study.resolver.ts | 3 ++- packages/server/src/study/study.service.ts | 28 ++++++++++++++++++++- packages/server/src/tag/tag.service.ts | 12 ++++++++- 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 92f4564d..a90b0988 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -99,7 +99,7 @@ type Mutation { signLabCreateProject(project: ProjectCreate!): Project! deleteProject: Boolean! createStudy(study: StudyCreate!): Study! - deleteStudy: Boolean! + deleteStudy(study: ID!): Boolean! changeStudyName(study: ID!, newName: String!): Study! changeStudyDescription(study: ID!, newDescription: String!): Study! createEntry(entry: EntryCreate!, dataset: ID!): Entry! diff --git a/packages/server/src/study/study.resolver.ts b/packages/server/src/study/study.resolver.ts index 2d95a3a1..8bac41d5 100644 --- a/packages/server/src/study/study.resolver.ts +++ b/packages/server/src/study/study.resolver.ts @@ -29,7 +29,8 @@ export class StudyResolver { } @Mutation(() => Boolean) - async deleteStudy(): Promise { + async deleteStudy(@Args('study', { type: () => ID }, StudyPipe) study: Study): Promise { + await this.studyService.delete(study); return true; } diff --git a/packages/server/src/study/study.service.ts b/packages/server/src/study/study.service.ts index 274cd54b..c71be117 100644 --- a/packages/server/src/study/study.service.ts +++ b/packages/server/src/study/study.service.ts @@ -8,7 +8,21 @@ import { Project } from 'src/project/project.model'; @Injectable() export class StudyService { - constructor(@InjectModel(Study.name) private readonly studyModel: Model) {} + private onDeleteFunctions: ((study: Study) => Promise)[] = []; + + constructor(@InjectModel(Study.name) private readonly studyModel: Model) { + + // Attach logic for when a study is deleted + const onDeleteFunctions = this.onDeleteFunctions; + this.studyModel.schema.pre('deleteOne', async function(next) { + // Get the study about the be deleted + const study = await this.model.findOne(this.getQuery()); + console.log('here'); + // Call any subscribed functions + await Promise.all(onDeleteFunctions.map((callback) => callback(study))); + next(); + }); + } async create(study: StudyCreate): Promise { return this.studyModel.create(study); @@ -54,4 +68,16 @@ export class StudyService { await this.studyModel.updateOne({ _id: study._id }, { $set: { description: newDescription } }); return (await this.findById(study._id))!; } + + async delete(study: Study): Promise { + console.log('here'); + await this.studyModel.deleteOne({ _id: study._id }); + } + + /** + * Add the ability to attach logic when a study is deleted + */ + async onDelete(callback: (study: Study) => Promise): Promise { + this.onDeleteFunctions.push(callback); + } } diff --git a/packages/server/src/tag/tag.service.ts b/packages/server/src/tag/tag.service.ts index 0d74098b..e9e9569c 100644 --- a/packages/server/src/tag/tag.service.ts +++ b/packages/server/src/tag/tag.service.ts @@ -9,7 +9,12 @@ import { StudyService } from '../study/study.service'; @Injectable() export class TagService { - constructor(@InjectModel(Tag.name) private readonly tagModel: Model, private readonly studyService: StudyService) {} + constructor(@InjectModel(Tag.name) private readonly tagModel: Model, private readonly studyService: StudyService) { + // Subscribe to study delete events + this.studyService.onDelete(async (study: Study) => { + await this.removeByStudy(study); + }); + } async find(id: string): Promise { return this.tagModel.findOne({ _id: id }); @@ -107,4 +112,9 @@ export class TagService { private async getIncomplete(study: Study, user: string): Promise { return this.tagModel.findOne({ study: study._id, user, complete: false, enabled: true }); } + + private async removeByStudy(study: Study): Promise { + console.log('Called with study', study); + await this.tagModel.deleteMany({ study: study._id }); + } } From 4400b1ae1e5f953fac258e1eff7c98656743ebe6 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 8 Dec 2023 10:52:09 -0500 Subject: [PATCH 2/5] Ability to register and call shared delete logic --- packages/server/src/app.module.ts | 4 ++- .../shared/service/study-delete.service.ts | 21 ++++++++++++++++ packages/server/src/shared/shared.module.ts | 8 ++++++ packages/server/src/study/study.module.ts | 20 ++++++++++++++- packages/server/src/study/study.service.ts | 25 +++---------------- packages/server/src/tag/tag.service.ts | 1 - 6 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 packages/server/src/shared/service/study-delete.service.ts create mode 100644 packages/server/src/shared/shared.module.ts diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index 2bec1f7d..1e049ca6 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -10,6 +10,7 @@ import { ProjectModule } from './project/project.module'; import { StudyModule } from './study/study.module'; import { EntryModule } from './entry/entry.module'; import { TagModule } from './tag/tag.module'; +import { SharedModule } from './shared/shared.module'; @Module({ imports: [ @@ -36,7 +37,8 @@ import { TagModule } from './tag/tag.module'; ProjectModule, StudyModule, EntryModule, - TagModule + TagModule, + SharedModule ], }) export class AppModule {} diff --git a/packages/server/src/shared/service/study-delete.service.ts b/packages/server/src/shared/service/study-delete.service.ts new file mode 100644 index 00000000..ae6bf859 --- /dev/null +++ b/packages/server/src/shared/service/study-delete.service.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { Study } from '../../study/study.model'; + +/** + * Service which handles storing and applying functions which are called when + * a study is deleted. Since the Mongoose Middleware needs to be registered + * before the module is loaded, this intermediate service is used to + * seperate the storage of the callback functions from the module itself + */ +@Injectable() +export class StudyDeletionService { + private onDeleteFunctions: ((study: Study) => Promise)[] = []; + + registerOnDelete(callback: (study: Study) => Promise) { + this.onDeleteFunctions.push(callback); + } + + async apply(study: Study): Promise { + await Promise.all(this.onDeleteFunctions.map((callback) => callback(study))); + } +} diff --git a/packages/server/src/shared/shared.module.ts b/packages/server/src/shared/shared.module.ts new file mode 100644 index 00000000..ff9aaeff --- /dev/null +++ b/packages/server/src/shared/shared.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { StudyDeletionService } from './service/study-delete.service'; + +@Module({ + providers: [StudyDeletionService], + exports: [StudyDeletionService] +}) +export class SharedModule {} diff --git a/packages/server/src/study/study.module.ts b/packages/server/src/study/study.module.ts index 90f73156..d71fbc0f 100644 --- a/packages/server/src/study/study.module.ts +++ b/packages/server/src/study/study.module.ts @@ -6,9 +6,27 @@ import { Study, StudySchema } from './study.model'; import { ProjectModule } from '../project/project.module'; import { StudyPipe } from './pipes/study.pipe'; import { StudyCreatePipe } from './pipes/create.pipe'; +import { StudyDeletionService } from '../shared/service/study-delete.service'; +import { SharedModule } from '../shared/shared.module'; @Module({ - imports: [MongooseModule.forFeature([{ name: Study.name, schema: StudySchema }]), ProjectModule], + imports: [MongooseModule.forFeatureAsync([ + { + name: Study.name, + useFactory: (deletionService: StudyDeletionService) => { + const schema = StudySchema; + + schema.pre('deleteOne', async function () { + const study = await this.model.findOne(this.getQuery()); + await deletionService.apply(study); + }); + + return schema; + }, + imports: [SharedModule], + inject: [StudyDeletionService], + } + ]), ProjectModule, SharedModule], providers: [StudyService, StudyResolver, StudyPipe, StudyCreatePipe], exports: [StudyService, StudyPipe] }) diff --git a/packages/server/src/study/study.service.ts b/packages/server/src/study/study.service.ts index c71be117..52e44c26 100644 --- a/packages/server/src/study/study.service.ts +++ b/packages/server/src/study/study.service.ts @@ -5,24 +5,11 @@ import { Study } from './study.model'; import { StudyCreate } from './dtos/create.dto'; import { Validator } from 'jsonschema'; import { Project } from 'src/project/project.model'; +import { StudyDeletionService } from 'src/shared/service/study-delete.service'; @Injectable() export class StudyService { - private onDeleteFunctions: ((study: Study) => Promise)[] = []; - - constructor(@InjectModel(Study.name) private readonly studyModel: Model) { - - // Attach logic for when a study is deleted - const onDeleteFunctions = this.onDeleteFunctions; - this.studyModel.schema.pre('deleteOne', async function(next) { - // Get the study about the be deleted - const study = await this.model.findOne(this.getQuery()); - console.log('here'); - // Call any subscribed functions - await Promise.all(onDeleteFunctions.map((callback) => callback(study))); - next(); - }); - } + constructor(@InjectModel(Study.name) private readonly studyModel: Model, private readonly studyDelete: StudyDeletionService) {} async create(study: StudyCreate): Promise { return this.studyModel.create(study); @@ -70,14 +57,10 @@ export class StudyService { } async delete(study: Study): Promise { - console.log('here'); await this.studyModel.deleteOne({ _id: study._id }); } - /** - * Add the ability to attach logic when a study is deleted - */ - async onDelete(callback: (study: Study) => Promise): Promise { - this.onDeleteFunctions.push(callback); + async onDelete(callback: (study: Study) => Promise){ + this.studyDelete.registerOnDelete(callback); } } diff --git a/packages/server/src/tag/tag.service.ts b/packages/server/src/tag/tag.service.ts index e9e9569c..e9864094 100644 --- a/packages/server/src/tag/tag.service.ts +++ b/packages/server/src/tag/tag.service.ts @@ -114,7 +114,6 @@ export class TagService { } private async removeByStudy(study: Study): Promise { - console.log('Called with study', study); await this.tagModel.deleteMany({ study: study._id }); } } From c2fd454826fda37e1157e4bf5641b431edff7e86 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 8 Dec 2023 11:14:07 -0500 Subject: [PATCH 3/5] Make the middleware registration generic --- .../service/mongoose-callback.service.ts | 78 +++++++++++++++++++ .../shared/service/study-delete.service.ts | 21 ----- packages/server/src/shared/shared.module.ts | 6 +- packages/server/src/study/study.module.ts | 8 +- packages/server/src/study/study.service.ts | 7 +- packages/server/src/tag/tag.module.ts | 4 +- packages/server/src/tag/tag.service.ts | 6 +- 7 files changed, 93 insertions(+), 37 deletions(-) create mode 100644 packages/server/src/shared/service/mongoose-callback.service.ts delete mode 100644 packages/server/src/shared/service/study-delete.service.ts diff --git a/packages/server/src/shared/service/mongoose-callback.service.ts b/packages/server/src/shared/service/mongoose-callback.service.ts new file mode 100644 index 00000000..f0de004b --- /dev/null +++ b/packages/server/src/shared/service/mongoose-callback.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@nestjs/common'; + +// TODO: The data is really generic and depends on the model +type MiddlewareOperations = (data: any) => Promise; +type SupportedOperations = 'deleteOne' | 'updateOne' | 'save'; + +/** + * Mongoose supports middleware which can execute when certain operations + * take place. The downside is that the middleware needs to be registerd + * before the schema is compiled into the model. With NestJS, the model + * compilation takes place at the module level. + * + * For example, say a middleware is needed for the "deleteOne" operation on + * a study. The middleware could ideally be registered with the context of + * the study service. However, the study service would not be available + * yet since the middleware is registerd **before** the rest of the module + * is loaded. + * + * This serivce allows for middleware to be registerd independetly by only + * storing the callbacks. This service is then called by the Mongoose + * middleware. + * + * + * Mongoose Middleware: + * https://mongoosejs.com/docs/middleware.html + * + * NestJS MongooseModule: + * https://docs.nestjs.com/techniques/mongodb#hooks-middleware + */ +@Injectable() +export class MongooseMiddlewareService { + /** + * Supports storing middleware operations. For example + * + * middlewareOperations = { + * 'Study': { + * 'deleteOne': [callback1, callback2], + * 'updateOne': [callback3] + * }, + * 'Project': { + * 'deleteOne': [callback4] + * } + * } + */ + private middlewareOperations: Map> = new Map(); + + register(modelName: string, operation: SupportedOperations, callback: MiddlewareOperations) { + // If the model is not already registered, create a new map + if (!this.middlewareOperations.has(modelName)) { + this.middlewareOperations.set(modelName, new Map()); + } + + // Get all supported operations for the model + const modelOperations = this.middlewareOperations.get(modelName)!; + + // Update the list of callbacks for the operation + const callbacks = modelOperations.get(operation) || []; + callbacks.push(callback); + modelOperations.set(operation, callbacks); + } + + async apply(modelName: string, operation: SupportedOperations, data: any): Promise { + // If there isn't any callbacks for the model, don't continue + if (!this.middlewareOperations.has(modelName)) { + return; + } + + // Get the list of callbacks for the operation if there aren't any, don't continue + const modelOperations = this.middlewareOperations.get(modelName)!; + if (!modelOperations.has(operation)) { + return; + } + + // Apply all callbacks + const callbacks = modelOperations.get(operation) || []; + await Promise.all(callbacks.map((callback) => callback(data))); + } +} diff --git a/packages/server/src/shared/service/study-delete.service.ts b/packages/server/src/shared/service/study-delete.service.ts deleted file mode 100644 index ae6bf859..00000000 --- a/packages/server/src/shared/service/study-delete.service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { Study } from '../../study/study.model'; - -/** - * Service which handles storing and applying functions which are called when - * a study is deleted. Since the Mongoose Middleware needs to be registered - * before the module is loaded, this intermediate service is used to - * seperate the storage of the callback functions from the module itself - */ -@Injectable() -export class StudyDeletionService { - private onDeleteFunctions: ((study: Study) => Promise)[] = []; - - registerOnDelete(callback: (study: Study) => Promise) { - this.onDeleteFunctions.push(callback); - } - - async apply(study: Study): Promise { - await Promise.all(this.onDeleteFunctions.map((callback) => callback(study))); - } -} diff --git a/packages/server/src/shared/shared.module.ts b/packages/server/src/shared/shared.module.ts index ff9aaeff..cb5cdc75 100644 --- a/packages/server/src/shared/shared.module.ts +++ b/packages/server/src/shared/shared.module.ts @@ -1,8 +1,8 @@ import { Module } from '@nestjs/common'; -import { StudyDeletionService } from './service/study-delete.service'; +import { MongooseMiddlewareService } from './service/mongoose-callback.service'; @Module({ - providers: [StudyDeletionService], - exports: [StudyDeletionService] + providers: [MongooseMiddlewareService], + exports: [MongooseMiddlewareService] }) export class SharedModule {} diff --git a/packages/server/src/study/study.module.ts b/packages/server/src/study/study.module.ts index d71fbc0f..be1b9b29 100644 --- a/packages/server/src/study/study.module.ts +++ b/packages/server/src/study/study.module.ts @@ -6,25 +6,25 @@ import { Study, StudySchema } from './study.model'; import { ProjectModule } from '../project/project.module'; import { StudyPipe } from './pipes/study.pipe'; import { StudyCreatePipe } from './pipes/create.pipe'; -import { StudyDeletionService } from '../shared/service/study-delete.service'; +import { MongooseMiddlewareService } from '../shared/service/mongoose-callback.service'; import { SharedModule } from '../shared/shared.module'; @Module({ imports: [MongooseModule.forFeatureAsync([ { name: Study.name, - useFactory: (deletionService: StudyDeletionService) => { + useFactory: (middlewareService: MongooseMiddlewareService) => { const schema = StudySchema; schema.pre('deleteOne', async function () { const study = await this.model.findOne(this.getQuery()); - await deletionService.apply(study); + await middlewareService.apply(Study.name, 'deleteOne', study); }); return schema; }, imports: [SharedModule], - inject: [StudyDeletionService], + inject: [MongooseMiddlewareService], } ]), ProjectModule, SharedModule], providers: [StudyService, StudyResolver, StudyPipe, StudyCreatePipe], diff --git a/packages/server/src/study/study.service.ts b/packages/server/src/study/study.service.ts index 52e44c26..2726e68e 100644 --- a/packages/server/src/study/study.service.ts +++ b/packages/server/src/study/study.service.ts @@ -5,11 +5,10 @@ import { Study } from './study.model'; import { StudyCreate } from './dtos/create.dto'; import { Validator } from 'jsonschema'; import { Project } from 'src/project/project.model'; -import { StudyDeletionService } from 'src/shared/service/study-delete.service'; @Injectable() export class StudyService { - constructor(@InjectModel(Study.name) private readonly studyModel: Model, private readonly studyDelete: StudyDeletionService) {} + constructor(@InjectModel(Study.name) private readonly studyModel: Model) {} async create(study: StudyCreate): Promise { return this.studyModel.create(study); @@ -59,8 +58,4 @@ export class StudyService { async delete(study: Study): Promise { await this.studyModel.deleteOne({ _id: study._id }); } - - async onDelete(callback: (study: Study) => Promise){ - this.studyDelete.registerOnDelete(callback); - } } diff --git a/packages/server/src/tag/tag.module.ts b/packages/server/src/tag/tag.module.ts index 3d7ca7e7..4fcd12ae 100644 --- a/packages/server/src/tag/tag.module.ts +++ b/packages/server/src/tag/tag.module.ts @@ -6,12 +6,14 @@ import { Tag, TagSchema } from './tag.model'; import { StudyModule } from '../study/study.module'; import { EntryModule } from '../entry/entry.module'; import { TagPipe } from './pipes/tag.pipe'; +import { SharedModule } from '../shared/shared.module'; @Module({ imports: [ MongooseModule.forFeature([{ name: Tag.name, schema: TagSchema }]), StudyModule, - EntryModule + EntryModule, + SharedModule ], providers: [TagService, TagResolver, TagPipe] }) diff --git a/packages/server/src/tag/tag.service.ts b/packages/server/src/tag/tag.service.ts index e9864094..0deb844d 100644 --- a/packages/server/src/tag/tag.service.ts +++ b/packages/server/src/tag/tag.service.ts @@ -5,13 +5,15 @@ import { Model } from 'mongoose'; import { Study } from '../study/study.model'; import { Entry } from '../entry/entry.model'; import { StudyService } from '../study/study.service'; +import { MongooseMiddlewareService } from '../shared/service/mongoose-callback.service'; @Injectable() export class TagService { - constructor(@InjectModel(Tag.name) private readonly tagModel: Model, private readonly studyService: StudyService) { + constructor(@InjectModel(Tag.name) private readonly tagModel: Model, private readonly studyService: StudyService, + middlewareService: MongooseMiddlewareService) { // Subscribe to study delete events - this.studyService.onDelete(async (study: Study) => { + middlewareService.register(Study.name, 'deleteOne', async (study: Study) => { await this.removeByStudy(study); }); } From 4ca25e986746a56e2c3064920a909fd3cc932821 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 8 Dec 2023 11:48:16 -0500 Subject: [PATCH 4/5] Working study delete --- packages/client/src/context/Study.context.tsx | 9 ++++- packages/client/src/graphql/graphql.ts | 5 +++ .../client/src/graphql/study/study.graphql | 4 ++ packages/client/src/graphql/study/study.ts | 40 ++++++++++++++++++- .../client/src/pages/studies/StudyControl.tsx | 25 +++++++++--- 5 files changed, 76 insertions(+), 7 deletions(-) diff --git a/packages/client/src/context/Study.context.tsx b/packages/client/src/context/Study.context.tsx index 2e1d64dc..b2d0ff07 100644 --- a/packages/client/src/context/Study.context.tsx +++ b/packages/client/src/context/Study.context.tsx @@ -7,6 +7,7 @@ export interface StudyContextProps { study: Study | null; setStudy: Dispatch>; studies: Study[]; + updateStudies: () => void; } const StudyContext = createContext({} as StudyContextProps); @@ -23,6 +24,12 @@ export const StudyProvider: FC = (props) => { const { project } = useProject(); + const updateStudies = () => { + if (project) { + findStudiesResults.refetch({ project: project._id }); + } + }; + // Effect to re-query for studies useEffect(() => { if (!project) { @@ -39,7 +46,7 @@ export const StudyProvider: FC = (props) => { } }, [findStudiesResults]); - return {props.children}; + return {props.children}; }; export const useStudy = () => useContext(StudyContext); diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 38b86997..5dd5ba12 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -296,6 +296,11 @@ export type MutationCreateTagsArgs = { }; +export type MutationDeleteStudyArgs = { + study: Scalars['ID']['input']; +}; + + export type MutationForgotPasswordArgs = { user: ForgotDto; }; diff --git a/packages/client/src/graphql/study/study.graphql b/packages/client/src/graphql/study/study.graphql index 1574ae94..c1f9c254 100644 --- a/packages/client/src/graphql/study/study.graphql +++ b/packages/client/src/graphql/study/study.graphql @@ -12,3 +12,7 @@ query findStudies($project: ID!) { } } } + +mutation deleteStudy($study: ID!) { + deleteStudy(study: $study) +} diff --git a/packages/client/src/graphql/study/study.ts b/packages/client/src/graphql/study/study.ts index 1ed2cd74..c8fd2e53 100644 --- a/packages/client/src/graphql/study/study.ts +++ b/packages/client/src/graphql/study/study.ts @@ -12,6 +12,13 @@ export type FindStudiesQueryVariables = Types.Exact<{ export type FindStudiesQuery = { __typename?: 'Query', findStudies: Array<{ __typename?: 'Study', _id: string, name: string, description: string, instructions: string, project: string, tagsPerEntry: number, tagSchema: { __typename?: 'TagSchema', dataSchema: any, uiSchema: any } }> }; +export type DeleteStudyMutationVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; +}>; + + +export type DeleteStudyMutation = { __typename?: 'Mutation', deleteStudy: boolean }; + export const FindStudiesDocument = gql` query findStudies($project: ID!) { @@ -56,4 +63,35 @@ export function useFindStudiesLazyQuery(baseOptions?: Apollo.LazyQueryHookOption } export type FindStudiesQueryHookResult = ReturnType; export type FindStudiesLazyQueryHookResult = ReturnType; -export type FindStudiesQueryResult = Apollo.QueryResult; \ No newline at end of file +export type FindStudiesQueryResult = Apollo.QueryResult; +export const DeleteStudyDocument = gql` + mutation deleteStudy($study: ID!) { + deleteStudy(study: $study) +} + `; +export type DeleteStudyMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteStudyMutation__ + * + * To run a mutation, you first call `useDeleteStudyMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteStudyMutation` 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 [deleteStudyMutation, { data, loading, error }] = useDeleteStudyMutation({ + * variables: { + * study: // value for 'study' + * }, + * }); + */ +export function useDeleteStudyMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteStudyDocument, options); + } +export type DeleteStudyMutationHookResult = ReturnType; +export type DeleteStudyMutationResult = Apollo.MutationResult; +export type DeleteStudyMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file diff --git a/packages/client/src/pages/studies/StudyControl.tsx b/packages/client/src/pages/studies/StudyControl.tsx index c5b414d7..e0fa2bdf 100644 --- a/packages/client/src/pages/studies/StudyControl.tsx +++ b/packages/client/src/pages/studies/StudyControl.tsx @@ -1,13 +1,28 @@ import { Typography, Box } from '@mui/material'; import { useStudy } from '../../context/Study.context'; -import { DataGrid, GridColDef } from '@mui/x-data-grid'; +import { DataGrid, GridColDef, GridRowId } from '@mui/x-data-grid'; import DeleteIcon from '@mui/icons-material/DeleteOutlined'; import { GridActionsCellItem } from '@mui/x-data-grid-pro'; import { Study } from '../../graphql/graphql'; - +import { useDeleteStudyMutation } from '../../graphql/study/study'; +import {useEffect} from 'react'; export const StudyControl: React.FC = () => { - const { studies } = useStudy(); + const { studies, updateStudies } = useStudy(); + + const [deleteStudyMutation, deleteStudyResults] = useDeleteStudyMutation(); + + const handleDelete = async (id: GridRowId) => { + // Execute delete mutation + deleteStudyMutation({ variables: { study: id.toString() } }); + }; + + useEffect(() => { + if (deleteStudyResults.called && deleteStudyResults.data) { + updateStudies(); + } + }, [deleteStudyResults.called, deleteStudyResults.data]); + const columns: GridColDef[] = [ { field: 'name', @@ -28,8 +43,8 @@ export const StudyControl: React.FC = () => { width: 120, maxWidth: 120, cellClassName: 'delete', - getActions: () => { - return [} label="Delete" />]; + getActions: (params) => { + return [} label="Delete" onClick={() => handleDelete(params.id)}/>]; } } ]; From 59f9bfa27d25fa0db1ec80a1c288b3f243dacb81 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 8 Dec 2023 12:54:35 -0500 Subject: [PATCH 5/5] Add confirmation logic --- packages/client/src/App.tsx | 9 ++- .../src/context/Confirmation.context.tsx | 69 +++++++++++++++++++ packages/client/src/hooks/useProject.tsx | 0 packages/client/src/hooks/useVerifyEntry.tsx | 0 .../client/src/pages/studies/StudyControl.tsx | 13 +++- 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 packages/client/src/context/Confirmation.context.tsx delete mode 100644 packages/client/src/hooks/useProject.tsx delete mode 100644 packages/client/src/hooks/useVerifyEntry.tsx diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index a51e646f..5d4d8e61 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -26,7 +26,8 @@ import { SideBar } from './components/SideBar.component'; import { ProjectProvider } from './context/Project.context'; import { ApolloClient, ApolloProvider, InMemoryCache, concat, createHttpLink } from '@apollo/client'; import { setContext } from '@apollo/client/link/context'; -import {StudyProvider} from './context/Study.context'; +import { StudyProvider } from './context/Study.context'; +import { ConfirmationProvider } from './context/Confirmation.context'; const drawerWidth = 256; const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ @@ -70,8 +71,10 @@ const App: FC = () => { - - + + + + diff --git a/packages/client/src/context/Confirmation.context.tsx b/packages/client/src/context/Confirmation.context.tsx new file mode 100644 index 00000000..e0fdaf1f --- /dev/null +++ b/packages/client/src/context/Confirmation.context.tsx @@ -0,0 +1,69 @@ +import { createContext, useContext, useState } from 'react'; +import { Dialog, DialogContent, DialogTitle, DialogActions, Button, Typography } from '@mui/material'; + +export interface ConfirmationRequest { + message: string; + title: string; + onConfirm: () => void; + onCancel: () => void; +} + +export interface ConfirmationContextProps { + pushConfirmationRequest: (confirmationRequest: ConfirmationRequest) => void; +} + +const ConfirmationContext = createContext({} as ConfirmationContextProps); + +export interface ConfirmationProviderProps { + children: React.ReactNode; +} + +export const ConfirmationProvider: React.FC = ({ children }) => { + const [open, setOpen] = useState(false); + const [confirmationRequest, setConfirmationRequest] = useState(null); + + + const pushConfirmationRequest = (confirmationRequest: ConfirmationRequest) => { + setConfirmationRequest(confirmationRequest); + setOpen(true); + }; + + const handleConfirm = () => { + if (confirmationRequest) { + confirmationRequest.onConfirm(); + } + setOpen(false); + }; + + const handleCancel = () => { + if (confirmationRequest) { + confirmationRequest.onCancel(); + } + setOpen(false); + }; + + + return ( + + + {confirmationRequest && confirmationRequest.title} + + {confirmationRequest && confirmationRequest.message} + + + + + + + {children} + + ); +} + +export const useConfirmation = () => useContext(ConfirmationContext); diff --git a/packages/client/src/hooks/useProject.tsx b/packages/client/src/hooks/useProject.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/client/src/hooks/useVerifyEntry.tsx b/packages/client/src/hooks/useVerifyEntry.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/client/src/pages/studies/StudyControl.tsx b/packages/client/src/pages/studies/StudyControl.tsx index e0fa2bdf..e22a1b79 100644 --- a/packages/client/src/pages/studies/StudyControl.tsx +++ b/packages/client/src/pages/studies/StudyControl.tsx @@ -5,16 +5,25 @@ import DeleteIcon from '@mui/icons-material/DeleteOutlined'; import { GridActionsCellItem } from '@mui/x-data-grid-pro'; import { Study } from '../../graphql/graphql'; import { useDeleteStudyMutation } from '../../graphql/study/study'; -import {useEffect} from 'react'; +import { useEffect } from 'react'; +import { useConfirmation } from '../../context/Confirmation.context'; export const StudyControl: React.FC = () => { const { studies, updateStudies } = useStudy(); const [deleteStudyMutation, deleteStudyResults] = useDeleteStudyMutation(); + const confirmation = useConfirmation(); const handleDelete = async (id: GridRowId) => { // Execute delete mutation - deleteStudyMutation({ variables: { study: id.toString() } }); + confirmation.pushConfirmationRequest({ + title: 'Delete Study', + message: 'Are you sure you want to delete this study?', + onConfirm: () => { + deleteStudyMutation({ variables: { study: id.toString() } }); + }, + onCancel: () => {} + }); }; useEffect(() => {