From c8a5a6b4635a3122c6d174e0c96063520edbd770 Mon Sep 17 00:00:00 2001 From: MaolinWei Date: Thu, 25 Apr 2024 16:27:58 -0400 Subject: [PATCH 1/3] feat: Catch Trials --- .../src/components/TagTraining.component.tsx | 27 +++++ packages/client/src/graphql/graphql.ts | 15 +++ packages/client/src/graphql/tag/tag.graphql | 26 +++++ packages/client/src/graphql/tag/tag.ts | 100 +++++++++++++++++- .../client/src/pages/studies/NewStudy.tsx | 19 +++- packages/server/src/tag/models/tag.model.ts | 4 + .../server/src/tag/resolvers/tag.resolver.ts | 19 ++++ .../server/src/tag/services/tag.service.ts | 25 +++++ 8 files changed, 231 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/TagTraining.component.tsx b/packages/client/src/components/TagTraining.component.tsx index a1020556..c285f15f 100644 --- a/packages/client/src/components/TagTraining.component.tsx +++ b/packages/client/src/components/TagTraining.component.tsx @@ -11,6 +11,7 @@ import { useSnackbar } from '../context/Snackbar.context'; export interface TagTrainingComponentProps { setTrainingSet: Dispatch>; setTaggingSet: Dispatch>; + setCatchTrialSet: Dispatch>; } export const TagTrainingComponent: React.FC = (props) => { @@ -19,6 +20,7 @@ export const TagTrainingComponent: React.FC = (props) const [getDatasetsQuery, getDatasetsResults] = useGetDatasetsByProjectLazyQuery(); const [trainingSet, setTrainingSet] = useState([]); const [taggingSet, setTaggingSet] = useState([]); + const [catchTrialSet, setCatchTrialSet] = useState([]); const { t } = useTranslation(); const { pushSnackbarMessage } = useSnackbar(); @@ -64,6 +66,24 @@ export const TagTrainingComponent: React.FC = (props) entry={params.row} /> ) + }, + { + field: 'catchTrial', + headerName: 'Catch Trial', + width: 200, + renderCell: (params) => ( + {}} + add={(entry) => { + setCatchTrialSet([...catchTrialSet, entry._id]); + }} + remove={(entry) => { + setCatchTrialSet(catchTrialSet.filter(entryID => entryID !== entry._id)); + }} + entry={params.row} + /> + ) } ]; @@ -77,6 +97,13 @@ export const TagTrainingComponent: React.FC = (props) props.setTrainingSet(entries); }, [trainingSet]); + useEffect(() => { + const entries = Array.from(new Set(catchTrialSet)); + if (props.setCatchTrialSet) { + props.setCatchTrialSet(entries); + } + }, [catchTrialSet]); + useEffect(() => { if (getDatasetsResults.data) { setDatasets(getDatasetsResults.data.getDatasetsByProject); diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index dfc746d2..15b9be10 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -124,6 +124,7 @@ export type Mutation = { changeStudyName: Study; completeTag: Scalars['Boolean']['output']; completeUploadSession: UploadResult; + createCatchTrials: Array; createDataset: Dataset; createOrganization: Organization; createStudy: Study; @@ -190,6 +191,12 @@ export type MutationCompleteUploadSessionArgs = { }; +export type MutationCreateCatchTrialsArgs = { + entries: Array; + study: Scalars['ID']['input']; +}; + + export type MutationCreateDatasetArgs = { dataset: DatasetCreate; }; @@ -377,6 +384,7 @@ export type Query = { findStudies: Array; /** Get the presigned URL for where to upload the CSV against */ getCSVUploadURL: Scalars['String']['output']; + getCatchTrials: Array; getDatasetProjectPermissions: Array; getDatasets: Array; getDatasetsByProject: Array; @@ -428,6 +436,11 @@ export type QueryGetCsvUploadUrlArgs = { }; +export type QueryGetCatchTrialsArgs = { + study: Scalars['ID']['input']; +}; + + export type QueryGetDatasetProjectPermissionsArgs = { project: Scalars['ID']['input']; }; @@ -550,6 +563,8 @@ export type Tag = { /** If the tag is enabled as part of the study, way to disable certain tags */ enabled: Scalars['Boolean']['output']; entry: Entry; + /** Indicates if the tag is a catch trial */ + isCatchTrial: Scalars['Boolean']['output']; /** Way to rank tags based on order to be tagged */ order: Scalars['Float']['output']; study: Study; diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index 27acf093..230c71f5 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -4,6 +4,12 @@ mutation createTags($study: ID!, $entries: [ID!]!) { } } +mutation createCatchTrials($study: ID!, $entries: [ID!]!) { + createCatchTrials(study: $study, entries: $entries) { + _id + } +} + mutation createTrainingSet($study: ID!, $entries: [ID!]!) { createTrainingSet(study: $study, entries: $entries) } @@ -185,3 +191,23 @@ query getTrainingTags($study: ID!, $user: String!) { complete } } + +query getCatchTrials($study: ID!) { + getCatchTrials(study: $study) { + _id + entry { + _id + organization + entryID + contentType + creator + dateCreated + meta + signedUrl + signedUrlExpiration + } + data + complete + isCatchTrial + } +} diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index 27184d8d..792d0669 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -13,6 +13,14 @@ export type CreateTagsMutationVariables = Types.Exact<{ export type CreateTagsMutation = { __typename?: 'Mutation', createTags: Array<{ __typename?: 'Tag', _id: string }> }; +export type CreateCatchTrialsMutationVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; + entries: Array | Types.Scalars['ID']['input']; +}>; + + +export type CreateCatchTrialsMutation = { __typename?: 'Mutation', createCatchTrials: Array<{ __typename?: 'Tag', _id: string }> }; + export type CreateTrainingSetMutationVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; entries: Array | Types.Scalars['ID']['input']; @@ -84,6 +92,13 @@ export type GetTrainingTagsQueryVariables = Types.Exact<{ export type GetTrainingTagsQuery = { __typename?: 'Query', getTrainingTags: Array<{ __typename?: 'Tag', _id: string, complete: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number, isTraining: boolean }, data?: Array<{ __typename?: 'TagField', type: Types.TagFieldType, name: string, field?: { __typename: 'AslLexField', lexiconEntry: { __typename?: 'LexiconEntry', key: string, primary: string, video: string, lexicon: string, associates: Array, fields: any } } | { __typename: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField', entries: Array<{ __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number, isTraining: boolean }> } | null }> | null }> }; +export type GetCatchTrialsQueryVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; +}>; + + +export type GetCatchTrialsQuery = { __typename?: 'Query', getCatchTrials: Array<{ __typename?: 'Tag', _id: string, data?: any | null, complete: boolean, isCatchTrial: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }> }; + export const CreateTagsDocument = gql` mutation createTags($study: ID!, $entries: [ID!]!) { @@ -119,6 +134,40 @@ export function useCreateTagsMutation(baseOptions?: Apollo.MutationHookOptions; export type CreateTagsMutationResult = Apollo.MutationResult; export type CreateTagsMutationOptions = Apollo.BaseMutationOptions; +export const CreateCatchTrialsDocument = gql` + mutation createCatchTrials($study: ID!, $entries: [ID!]!) { + createCatchTrials(study: $study, entries: $entries) { + _id + } +} + `; +export type CreateCatchTrialsMutationFn = Apollo.MutationFunction; + +/** + * __useCreateCatchTrialsMutation__ + * + * To run a mutation, you first call `useCreateCatchTrialsMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateCatchTrialsMutation` 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 [createCatchTrialsMutation, { data, loading, error }] = useCreateCatchTrialsMutation({ + * variables: { + * study: // value for 'study' + * entries: // value for 'entries' + * }, + * }); + */ +export function useCreateCatchTrialsMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateCatchTrialsDocument, options); + } +export type CreateCatchTrialsMutationHookResult = ReturnType; +export type CreateCatchTrialsMutationResult = Apollo.MutationResult; +export type CreateCatchTrialsMutationOptions = Apollo.BaseMutationOptions; export const CreateTrainingSetDocument = gql` mutation createTrainingSet($study: ID!, $entries: [ID!]!) { createTrainingSet(study: $study, entries: $entries) @@ -545,4 +594,53 @@ export function useGetTrainingTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOp } export type GetTrainingTagsQueryHookResult = ReturnType; export type GetTrainingTagsLazyQueryHookResult = ReturnType; -export type GetTrainingTagsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetTrainingTagsQueryResult = Apollo.QueryResult; +export const GetCatchTrialsDocument = gql` + query getCatchTrials($study: ID!) { + getCatchTrials(study: $study) { + _id + entry { + _id + organization + entryID + contentType + creator + dateCreated + meta + signedUrl + signedUrlExpiration + } + data + complete + isCatchTrial + } +} + `; + +/** + * __useGetCatchTrialsQuery__ + * + * To run a query within a React component, call `useGetCatchTrialsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetCatchTrialsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetCatchTrialsQuery({ + * variables: { + * study: // value for 'study' + * }, + * }); + */ +export function useGetCatchTrialsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetCatchTrialsDocument, options); + } +export function useGetCatchTrialsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetCatchTrialsDocument, options); + } +export type GetCatchTrialsQueryHookResult = ReturnType; +export type GetCatchTrialsLazyQueryHookResult = ReturnType; +export type GetCatchTrialsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/packages/client/src/pages/studies/NewStudy.tsx b/packages/client/src/pages/studies/NewStudy.tsx index febdc27f..a743e31c 100644 --- a/packages/client/src/pages/studies/NewStudy.tsx +++ b/packages/client/src/pages/studies/NewStudy.tsx @@ -12,10 +12,13 @@ import { useApolloClient } from '@apollo/client'; import { CreateTagsDocument, CreateTrainingSetDocument, + CreateCatchTrialsDocument, CreateTagsMutationVariables, CreateTagsMutation, CreateTrainingSetMutation, - CreateTrainingSetMutationVariables + CreateTrainingSetMutationVariables, + CreateCatchTrialsMutation, + CreateCatchTrialsMutationVariables, } from '../../graphql/tag/tag'; import { useTranslation } from 'react-i18next'; import { TagFieldFragmentSchema, TagField } from '../../components/tagbuilder/TagProvider'; @@ -30,6 +33,7 @@ export const NewStudy: React.FC = () => { const { updateStudies } = useStudy(); const [trainingSet, setTrainingSet] = useState([]); const [taggingSet, setTaggingSet] = useState([]); + const [catchTrialSet, setCatchTrialSet] = useState([]); const apolloClient = useApolloClient(); // The different fields that make up the tag schema const [tagFields, setTagFields] = useState([]); @@ -90,10 +94,19 @@ export const NewStudy: React.FC = () => { return; } + // Filter taggingSet to remove IDs that are also in catchTrialSet + const filteredTaggingSet = taggingSet.filter(id => !catchTrialSet.includes(id)); + // Create the corresponding tags await apolloClient.mutate({ mutation: CreateTagsDocument, - variables: { study: result.data.createStudy._id, entries: taggingSet } + variables: { study: result.data.createStudy._id, entries: filteredTaggingSet } + }); + + // Create the corresponding Catch Trial tags + await apolloClient.mutate({ + mutation: CreateCatchTrialsDocument, + variables: { study: result.data.createStudy._id, entries: catchTrialSet } }); // Create the training set @@ -138,7 +151,7 @@ export const NewStudy: React.FC = () => { /> ); case 2: - return ; + return ; default: return null; } diff --git a/packages/server/src/tag/models/tag.model.ts b/packages/server/src/tag/models/tag.model.ts index 738642e5..835d169d 100644 --- a/packages/server/src/tag/models/tag.model.ts +++ b/packages/server/src/tag/models/tag.model.ts @@ -45,6 +45,10 @@ export class Tag { @Prop() @Field({ description: 'If the tag is part of a training' }) training: boolean; + + @Prop() + @Field({ description: 'Indicates if the tag is a catch trial' }) + isCatchTrial: boolean; } export type TagDocument = Tag & Document; diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index 742ab68f..f6bdd1c5 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -40,6 +40,18 @@ export class TagResolver { return this.tagService.createTags(study, entries); } + @Mutation(() => [Tag]) + async createCatchTrials( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @Args('entries', { type: () => [ID] }, EntriesPipe) entries: Entry[], + @TokenContext() user: TokenPayload + ) { + if (!(await this.enforcer.enforce(user.user_id, StudyPermissions.CREATE, study._id.toString()))) { + throw new UnauthorizedException('User cannot add tags to this study'); + } + return this.tagService.createCatchTrials(study, entries); + } + @Mutation(() => Tag, { nullable: true }) async assignTag( @Args('study', { type: () => ID }, StudyPipe) study: Study, @@ -141,4 +153,11 @@ export class TagResolver { async study(@Parent() tag: Tag): Promise { return this.studyPipe.transform(tag.study); } + + @Query(() => [Tag]) + async getCatchTrials( + @Args('study', { type: () => ID }, StudyPipe) study: Study + ): Promise { + return this.tagService.getCatchTrials(study); + } } diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 1fcbc9a8..7aa0f794 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -60,6 +60,24 @@ export class TagService { return tags; } + async createCatchTrials(study: Study, entries: Entry[]): Promise { + const tags: Tag[] = []; + for (const entry of entries) { + // Create catch trial tags for each entry + const newCatchTrial = await this.tagModel.create({ + entry: entry._id, + study: study._id, + complete: false, + order: 0, + enabled: true, + training: false, + isCatchTrial: true // Indicate that this tag is a catch trial + }); + tags.push(newCatchTrial); + } + return tags; + } + async assignTag(study: Study, user: string, isTrained: boolean): Promise { return isTrained ? this.assignTagFull(study, user) : this.assignTrainingTag(study, user); } @@ -258,4 +276,11 @@ export class TagService { private async removeByEntry(entry: Entry): Promise { await this.tagModel.deleteMany({ entry: entry._id }); } + + async getCatchTrials(study: Study): Promise { + return this.tagModel.find({ + study: study._id, + isCatchTrial: true + }).exec(); + } } From 9f6a721de8646e333f4e52ce5ec80078916d05b9 Mon Sep 17 00:00:00 2001 From: MaolinWei Date: Mon, 29 Apr 2024 13:25:52 -0400 Subject: [PATCH 2/3] feat: Catch Trials --- .../src/components/TagTraining.component.tsx | 4 ++-- packages/client/src/pages/studies/NewStudy.tsx | 14 ++++++++++---- packages/server/src/tag/resolvers/tag.resolver.ts | 4 +--- packages/server/src/tag/services/tag.service.ts | 12 +++++++----- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/client/src/components/TagTraining.component.tsx b/packages/client/src/components/TagTraining.component.tsx index c285f15f..b6284186 100644 --- a/packages/client/src/components/TagTraining.component.tsx +++ b/packages/client/src/components/TagTraining.component.tsx @@ -79,7 +79,7 @@ export const TagTrainingComponent: React.FC = (props) setCatchTrialSet([...catchTrialSet, entry._id]); }} remove={(entry) => { - setCatchTrialSet(catchTrialSet.filter(entryID => entryID !== entry._id)); + setCatchTrialSet(catchTrialSet.filter((entryID) => entryID !== entry._id)); }} entry={params.row} /> @@ -102,7 +102,7 @@ export const TagTrainingComponent: React.FC = (props) if (props.setCatchTrialSet) { props.setCatchTrialSet(entries); } - }, [catchTrialSet]); + }, [catchTrialSet]); useEffect(() => { if (getDatasetsResults.data) { diff --git a/packages/client/src/pages/studies/NewStudy.tsx b/packages/client/src/pages/studies/NewStudy.tsx index a743e31c..366b5ed0 100644 --- a/packages/client/src/pages/studies/NewStudy.tsx +++ b/packages/client/src/pages/studies/NewStudy.tsx @@ -18,7 +18,7 @@ import { CreateTrainingSetMutation, CreateTrainingSetMutationVariables, CreateCatchTrialsMutation, - CreateCatchTrialsMutationVariables, + CreateCatchTrialsMutationVariables } from '../../graphql/tag/tag'; import { useTranslation } from 'react-i18next'; import { TagFieldFragmentSchema, TagField } from '../../components/tagbuilder/TagProvider'; @@ -95,8 +95,8 @@ export const NewStudy: React.FC = () => { } // Filter taggingSet to remove IDs that are also in catchTrialSet - const filteredTaggingSet = taggingSet.filter(id => !catchTrialSet.includes(id)); - + const filteredTaggingSet = taggingSet.filter((id) => !catchTrialSet.includes(id)); + // Create the corresponding tags await apolloClient.mutate({ mutation: CreateTagsDocument, @@ -151,7 +151,13 @@ export const NewStudy: React.FC = () => { /> ); case 2: - return ; + return ( + + ); default: return null; } diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index f6bdd1c5..40833db0 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -155,9 +155,7 @@ export class TagResolver { } @Query(() => [Tag]) - async getCatchTrials( - @Args('study', { type: () => ID }, StudyPipe) study: Study - ): Promise { + async getCatchTrials(@Args('study', { type: () => ID }, StudyPipe) study: Study): Promise { return this.tagService.getCatchTrials(study); } } diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 7aa0f794..30e0ca08 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -77,7 +77,7 @@ export class TagService { } return tags; } - + async assignTag(study: Study, user: string, isTrained: boolean): Promise { return isTrained ? this.assignTagFull(study, user) : this.assignTrainingTag(study, user); } @@ -278,9 +278,11 @@ export class TagService { } async getCatchTrials(study: Study): Promise { - return this.tagModel.find({ - study: study._id, - isCatchTrial: true - }).exec(); + return this.tagModel + .find({ + study: study._id, + isCatchTrial: true + }) + .exec(); } } From ba494933d9330ca8074b97bb78b31e9f8ad6c8ce Mon Sep 17 00:00:00 2001 From: MaolinWei Date: Tue, 7 May 2024 13:14:06 -0400 Subject: [PATCH 3/3] feat: Catch Trials --- packages/client/src/graphql/graphql.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index a7889ab9..17da4c17 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -475,7 +475,7 @@ export type QueryGetCsvUploadUrlArgs = { export type QueryGetCatchTrialsArgs = { study: Scalars['ID']['input']; - +} export type QueryGetDatasetDownloadsArgs = { dataset: Scalars['ID']['input'];