From dc31ac8d6f525892f0b2c40b26abf63a9b01d29c Mon Sep 17 00:00:00 2001 From: Akosah Date: Tue, 16 Jan 2024 14:56:52 -0500 Subject: [PATCH 1/7] backend code for entry toggle --- packages/server/schema.gql | 1 + packages/server/src/tag/tag.resolver.ts | 10 ++++++++++ packages/server/src/tag/tag.service.ts | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 8e64ef5b..07134562 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -163,6 +163,7 @@ type Mutation { createTags(study: ID!, entries: [ID!]!): [Tag!]! assignTag(study: ID!): Tag completeTag(tag: ID!, data: JSON!): Boolean! + setEntryEnabled(study: ID!, tag: ID!, enabled: Boolean!): Boolean! } input OrganizationCreate { diff --git a/packages/server/src/tag/tag.resolver.ts b/packages/server/src/tag/tag.resolver.ts index 224060c3..734338b2 100644 --- a/packages/server/src/tag/tag.resolver.ts +++ b/packages/server/src/tag/tag.resolver.ts @@ -56,6 +56,16 @@ export class TagResolver { return true; } + @Mutation(() => Boolean) + async setEntryEnabled( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @Args('tag', { type: () => ID }, TagPipe) entry: Entry, + @Args('enabled', { type: () => Boolean }) enabled: boolean + ): Promise { + await this.tagService.setEnabled(study, entry, enabled); + return true; + } + @ResolveField(() => Entry) async entry(@Parent() tag: Tag): Promise { return this.entryPipe.transform(tag.entry); diff --git a/packages/server/src/tag/tag.service.ts b/packages/server/src/tag/tag.service.ts index 22af37de..a30635c5 100644 --- a/packages/server/src/tag/tag.service.ts +++ b/packages/server/src/tag/tag.service.ts @@ -110,6 +110,23 @@ export class TagService { await this.tagModel.findOneAndUpdate({ _id: tag._id }, { $set: { data, complete: true } }); } + async setEnabled(study: Study, entry: Entry, enabled: boolean): Promise { + const existingTag = await this.tagModel.findOne({ _id: entry._id }); + if (existingTag) { + await this.tagModel.updateMany({ entry: entry._id, study: study._id }, { $set: { enabled: enabled } }); + } else { + for (let order = 0; order < study.tagsPerEntry; order++) { + await this.tagModel.create({ + entry: entry._id, + study: study._id, + complete: false, + order, + enabled: true + }); + } + } + } + private async getIncomplete(study: Study, user: string): Promise { return this.tagModel.findOne({ study: study._id, user, complete: false, enabled: true }); } From 9671d4602dd277bc531914aa3cd14296f75507ff Mon Sep 17 00:00:00 2001 From: Akosah Date: Wed, 17 Jan 2024 15:27:18 -0500 Subject: [PATCH 2/7] toggle entry wip --- packages/client/src/App.tsx | 2 + .../src/components/SideBar.component.tsx | 2 +- .../ToggleEntryEnabled.component.tsx | 66 ++++++++++++++ .../src/graphql/dataset/dataset.graphql | 12 ++- .../client/src/graphql/dataset/dataset.ts | 46 +++++++++- packages/client/src/graphql/graphql.ts | 15 ++++ .../client/src/graphql/study/study.graphql | 28 +++--- packages/client/src/graphql/tag/tag.graphql | 28 +++--- packages/client/src/graphql/tag/tag.ts | 84 +++++++++++++++++ .../src/pages/studies/EntryControls.tsx | 90 +++++++++++++++++++ packages/server/schema.gql | 3 +- packages/server/src/tag/tag.resolver.ts | 13 ++- packages/server/src/tag/tag.service.ts | 18 +++- 13 files changed, 374 insertions(+), 33 deletions(-) create mode 100644 packages/client/src/components/ToggleEntryEnabled.component.tsx diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 6bc30d06..cd080f84 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -29,6 +29,7 @@ import { setContext } from '@apollo/client/link/context'; import { StudyProvider } from './context/Study.context'; import { ConfirmationProvider } from './context/Confirmation.context'; import { DatasetProvider } from './context/Dataset.context'; +import { EntryControls } from './pages/studies/EntryControls'; const drawerWidth = 256; const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ @@ -127,6 +128,7 @@ const MyRoutes: FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/packages/client/src/components/SideBar.component.tsx b/packages/client/src/components/SideBar.component.tsx index 91fcbdc4..0064d185 100644 --- a/packages/client/src/components/SideBar.component.tsx +++ b/packages/client/src/components/SideBar.component.tsx @@ -33,7 +33,7 @@ export const SideBar: FC = ({ open, drawerWidth }) => { { name: 'New Study', action: () => navigate('/study/new') }, { name: 'Study Control', action: () => navigate('/study/controls') }, { name: 'User Permissions', action: () => navigate('/study/permissions') }, - { name: 'Entry Controls', action: () => navigate('/study/controls') }, + { name: 'Entry Controls', action: () => navigate('/study/entries') }, { name: 'Download Tags', action: () => navigate('/study/tags') } ] }, diff --git a/packages/client/src/components/ToggleEntryEnabled.component.tsx b/packages/client/src/components/ToggleEntryEnabled.component.tsx new file mode 100644 index 00000000..73bba87e --- /dev/null +++ b/packages/client/src/components/ToggleEntryEnabled.component.tsx @@ -0,0 +1,66 @@ +import { Switch } from '@mui/material'; +import { useEffect, useState } from 'react'; +import { useIsEntryEnabledLazyQuery, useSetEntryEnabledMutation } from '../graphql/tag/tag'; +import { useStudy } from '../context/Study.context'; +import { useConfirmation } from '../context/Confirmation.context'; + +export default function ToggleEntryEnabled(props: { entryId: string }) { + const [isEntryEnabled, isEntryEnabledResults] = useIsEntryEnabledLazyQuery(); + const [setEntryEnabledMutation, setEntryEnabledResults] = useSetEntryEnabledMutation(); + + const { study } = useStudy(); + const confirmation = useConfirmation(); + + useEffect(() => { + if (study) { + isEntryEnabled({ + variables: { + study: study._id, + entry: props.entryId + } + }); + } + }, [study]); + console.log('isEntryEnabledResults',isEntryEnabledResults); + + + useEffect(() => { + console.log('setEntryEnabledResults: ', setEntryEnabledResults); + + if (setEntryEnabledResults.error) { + //show error message + console.log('error toggling entry', setEntryEnabledResults.error); + } else { + } + }, [setEntryEnabledResults.data]); + + const handleToggleEnabled = async (entryId: string, checked: boolean) => { + console.log('checked', checked); + + if (study) { + confirmation.pushConfirmationRequest({ + title: 'Enable Entry', + message: 'Are you sure you want to delete this study? Doing so will delete all contained tags', + onConfirm: () => { + setEntryEnabledMutation({ + variables: { study: study._id, entry: entryId, enabled: checked } + }); + }, + onCancel: () => {} + }); + } else { + console.log('default study not selected', study); + } + }; + return ( + <> + handleToggleEnabled(props.entryId, event.target.checked)} + inputProps={{ 'aria-label': 'controlled' }} + /> + + ); +} diff --git a/packages/client/src/graphql/dataset/dataset.graphql b/packages/client/src/graphql/dataset/dataset.graphql index 95991352..f57196a0 100644 --- a/packages/client/src/graphql/dataset/dataset.graphql +++ b/packages/client/src/graphql/dataset/dataset.graphql @@ -1,7 +1,15 @@ query getDatasets { getDatasets { - _id, - name, + _id + name + description + } +} + +query getDatasetsByProject($project: ID!) { + getDatasetsByProject(project: $project) { + _id + name description } } diff --git a/packages/client/src/graphql/dataset/dataset.ts b/packages/client/src/graphql/dataset/dataset.ts index a716295f..8081d99d 100644 --- a/packages/client/src/graphql/dataset/dataset.ts +++ b/packages/client/src/graphql/dataset/dataset.ts @@ -10,6 +10,13 @@ export type GetDatasetsQueryVariables = Types.Exact<{ [key: string]: never; }>; export type GetDatasetsQuery = { __typename?: 'Query', getDatasets: Array<{ __typename?: 'Dataset', _id: string, name: string, description: string }> }; +export type GetDatasetsByProjectQueryVariables = Types.Exact<{ + project: Types.Scalars['ID']['input']; +}>; + + +export type GetDatasetsByProjectQuery = { __typename?: 'Query', getDatasetsByProject: Array<{ __typename?: 'Dataset', _id: string, name: string, description: string }> }; + export const GetDatasetsDocument = gql` query getDatasets { @@ -46,4 +53,41 @@ export function useGetDatasetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOption } export type GetDatasetsQueryHookResult = ReturnType; export type GetDatasetsLazyQueryHookResult = ReturnType; -export type GetDatasetsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetDatasetsQueryResult = Apollo.QueryResult; +export const GetDatasetsByProjectDocument = gql` + query getDatasetsByProject($project: ID!) { + getDatasetsByProject(project: $project) { + _id + name + description + } +} + `; + +/** + * __useGetDatasetsByProjectQuery__ + * + * To run a query within a React component, call `useGetDatasetsByProjectQuery` and pass it any options that fit your needs. + * When your component renders, `useGetDatasetsByProjectQuery` 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 } = useGetDatasetsByProjectQuery({ + * variables: { + * project: // value for 'project' + * }, + * }); + */ +export function useGetDatasetsByProjectQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetDatasetsByProjectDocument, options); + } +export function useGetDatasetsByProjectLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetDatasetsByProjectDocument, options); + } +export type GetDatasetsByProjectQueryHookResult = ReturnType; +export type GetDatasetsByProjectLazyQueryHookResult = ReturnType; +export type GetDatasetsByProjectQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 498d85ef..a314e94f 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -212,6 +212,7 @@ export type Mutation = { refresh: AccessToken; resendInvite: InviteModel; resetPassword: Scalars['Boolean']['output']; + setEntryEnabled: Scalars['Boolean']['output']; signLabCreateProject: Project; signup: AccessToken; updateProject: ProjectModel; @@ -413,6 +414,13 @@ export type MutationResetPasswordArgs = { }; +export type MutationSetEntryEnabledArgs = { + enabled: Scalars['Boolean']['input']; + entry: Scalars['ID']['input']; + study: Scalars['ID']['input']; +}; + + export type MutationSignLabCreateProjectArgs = { project: ProjectCreate; }; @@ -552,6 +560,7 @@ export type Query = { getUser: UserModel; invite: InviteModel; invites: Array; + isEntryEnabled: Scalars['Boolean']['output']; lexFindAll: Array; lexiconByKey: LexiconEntry; lexiconSearch: Array; @@ -633,6 +642,12 @@ export type QueryInvitesArgs = { }; +export type QueryIsEntryEnabledArgs = { + entry: Scalars['ID']['input']; + study: Scalars['ID']['input']; +}; + + export type QueryLexiconByKeyArgs = { key: Scalars['String']['input']; lexicon: Scalars['String']['input']; diff --git a/packages/client/src/graphql/study/study.graphql b/packages/client/src/graphql/study/study.graphql index aff0536f..2d880667 100644 --- a/packages/client/src/graphql/study/study.graphql +++ b/packages/client/src/graphql/study/study.graphql @@ -1,13 +1,13 @@ query findStudies($project: ID!) { findStudies(project: $project) { - _id, - name, - description, - instructions, - project, - tagsPerEntry, + _id + name + description + instructions + project + tagsPerEntry tagSchema { - dataSchema, + dataSchema uiSchema } } @@ -19,14 +19,14 @@ mutation deleteStudy($study: ID!) { mutation createStudy($study: StudyCreate!) { createStudy(study: $study) { - _id, - name, - description, - instructions, - project, - tagsPerEntry, + _id + name + description + instructions + project + tagsPerEntry tagSchema { - dataSchema, + dataSchema uiSchema } } diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index 8d204df3..aa6cc4f6 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -4,19 +4,27 @@ mutation createTags($study: ID!, $entries: [ID!]!) { } } +mutation setEntryEnabled($study: ID!, $entry: ID!, $enabled: Boolean!) { + setEntryEnabled(study: $study, entry: $entry, enabled: $enabled) +} + +query isEntryEnabled($study: ID!, $entry: ID!) { + isEntryEnabled(study: $study, entry: $entry) +} + mutation assignTag($study: ID!) { assignTag(study: $study) { - _id, + _id entry { - _id, - organization, - entryID, - contentType, - dataset, - creator, - dateCreated, - meta, - signedUrl, + _id + organization + entryID + contentType + dataset + creator + dateCreated + meta + signedUrl signedUrlExpiration } } diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index 05886d60..a299e24c 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -13,6 +13,23 @@ export type CreateTagsMutationVariables = Types.Exact<{ export type CreateTagsMutation = { __typename?: 'Mutation', createTags: Array<{ __typename?: 'Tag', _id: string }> }; +export type SetEntryEnabledMutationVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; + entry: Types.Scalars['ID']['input']; + enabled: Types.Scalars['Boolean']['input']; +}>; + + +export type SetEntryEnabledMutation = { __typename?: 'Mutation', setEntryEnabled: boolean }; + +export type IsEntryEnabledQueryVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; + entry: Types.Scalars['ID']['input']; +}>; + + +export type IsEntryEnabledQuery = { __typename?: 'Query', isEntryEnabled: boolean }; + export type AssignTagMutationVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; }>; @@ -63,6 +80,73 @@ export function useCreateTagsMutation(baseOptions?: Apollo.MutationHookOptions; export type CreateTagsMutationResult = Apollo.MutationResult; export type CreateTagsMutationOptions = Apollo.BaseMutationOptions; +export const SetEntryEnabledDocument = gql` + mutation setEntryEnabled($study: ID!, $entry: ID!, $enabled: Boolean!) { + setEntryEnabled(study: $study, entry: $entry, enabled: $enabled) +} + `; +export type SetEntryEnabledMutationFn = Apollo.MutationFunction; + +/** + * __useSetEntryEnabledMutation__ + * + * To run a mutation, you first call `useSetEntryEnabledMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useSetEntryEnabledMutation` 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 [setEntryEnabledMutation, { data, loading, error }] = useSetEntryEnabledMutation({ + * variables: { + * study: // value for 'study' + * entry: // value for 'entry' + * enabled: // value for 'enabled' + * }, + * }); + */ +export function useSetEntryEnabledMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(SetEntryEnabledDocument, options); + } +export type SetEntryEnabledMutationHookResult = ReturnType; +export type SetEntryEnabledMutationResult = Apollo.MutationResult; +export type SetEntryEnabledMutationOptions = Apollo.BaseMutationOptions; +export const IsEntryEnabledDocument = gql` + query isEntryEnabled($study: ID!, $entry: ID!) { + isEntryEnabled(study: $study, entry: $entry) +} + `; + +/** + * __useIsEntryEnabledQuery__ + * + * To run a query within a React component, call `useIsEntryEnabledQuery` and pass it any options that fit your needs. + * When your component renders, `useIsEntryEnabledQuery` 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 } = useIsEntryEnabledQuery({ + * variables: { + * study: // value for 'study' + * entry: // value for 'entry' + * }, + * }); + */ +export function useIsEntryEnabledQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(IsEntryEnabledDocument, options); + } +export function useIsEntryEnabledLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(IsEntryEnabledDocument, options); + } +export type IsEntryEnabledQueryHookResult = ReturnType; +export type IsEntryEnabledLazyQueryHookResult = ReturnType; +export type IsEntryEnabledQueryResult = Apollo.QueryResult; export const AssignTagDocument = gql` mutation assignTag($study: ID!) { assignTag(study: $study) { diff --git a/packages/client/src/pages/studies/EntryControls.tsx b/packages/client/src/pages/studies/EntryControls.tsx index e69de29b..90aceadd 100644 --- a/packages/client/src/pages/studies/EntryControls.tsx +++ b/packages/client/src/pages/studies/EntryControls.tsx @@ -0,0 +1,90 @@ +import { Box, Switch, Typography } from '@mui/material'; +import { useEffect, useState } from 'react'; +import DeleteIcon from '@mui/icons-material/DeleteOutlined'; +import { useConfirmation } from '../../context/Confirmation.context'; +import { DataGrid, GridActionsCellItem, GridRowId, GridColDef } from '@mui/x-data-grid'; +import { useStudy } from '../../context/Study.context'; +import { Dataset, Study } from '../../graphql/graphql'; +import { DatasetsView } from '../../components/DatasetsView.component'; +import { useGetDatasetsByProjectQuery, useGetDatasetsQuery } from '../../graphql/dataset/dataset'; +import { useProject } from '../../context/Project.context'; +import { useIsEntryEnabledLazyQuery, useSetEntryEnabledMutation } from '../../graphql/tag/tag'; +import ToggleEntryEnabled from '../../components/ToggleEntryEnabled.component'; + +export const EntryControls: React.FC = () => { + const { study } = useStudy(); + const { project } = useProject(); + const [datasets, setDatasets] = useState([]); + const getDatasetsResults = useGetDatasetsQuery(); + const getDatasetsByProjectResults = useGetDatasetsByProjectQuery({ + variables: { + project: project ? project._id : '' // value for 'project' + } + }); +// const [setEntryEnabledMutation, setEntryEnabledResults] = useSetEntryEnabledMutation(); +// const confirmation = useConfirmation(); + +// const [isEntryEnabled, isEntryEnabledResults] = useIsEntryEnabledLazyQuery(); + + useEffect(() => { + if (getDatasetsResults.data) { + setDatasets(getDatasetsResults.data.getDatasets); + } + }, [getDatasetsResults.data]); + + + + // useEffect(() => { + // if (getDatasetsByProjectResults.data) { + // setDatasets(getDatasetsByProjectResults.data.getDatasetsByProject); + // } + // }, [getDatasetsByProjectResults.data]); + +// const handleToggleEnabled = async (id: GridRowId, checked: boolean) => { +// console.log('cehcked', checked); + +// if (study) { +// confirmation.pushConfirmationRequest({ +// title: 'Enable Entry', +// message: 'Are you sure you want to delete this study? Doing so will delete all contained tags', +// onConfirm: () => { +// setEntryEnabledMutation({ +// variables: { study: study._id, entry: id.toString(), enabled: checked } +// }); +// }, +// onCancel: () => {} +// }); +// } else { +// console.log('default study not selected', study); +// } +// }; + + const additionalColumns: GridColDef[] = [ + { + field: 'enabled', + type: 'actions', + headerName: 'Enable', + width: 120, + maxWidth: 120, + cellClassName: 'enabled', + getActions: (params) => { + return [ + + // handleToggleEnabled(params.id, event.target.checked)} + // inputProps={{ 'aria-label': 'controlled' }} + // /> + ]; + } + } + ]; + + return ( + <> + Entry Control + + + ); +}; diff --git a/packages/server/schema.gql b/packages/server/schema.gql index 8cebd2c0..0e0d95ef 100644 --- a/packages/server/schema.gql +++ b/packages/server/schema.gql @@ -147,6 +147,7 @@ type Query { getCSVUploadURL(session: ID!): String! validateCSV(session: ID!): UploadResult! getEntryUploadURL(session: ID!, filename: String!, contentType: String!): String! + isEntryEnabled(study: ID!, entry: ID!): Boolean! } type Mutation { @@ -172,7 +173,7 @@ type Mutation { createTags(study: ID!, entries: [ID!]!): [Tag!]! assignTag(study: ID!): Tag completeTag(tag: ID!, data: JSON!): Boolean! - setEntryEnabled(study: ID!, tag: ID!, enabled: Boolean!): Boolean! + setEntryEnabled(study: ID!, entry: ID!, enabled: Boolean!): Boolean! } input OrganizationCreate { diff --git a/packages/server/src/tag/tag.resolver.ts b/packages/server/src/tag/tag.resolver.ts index 734338b2..3256bb28 100644 --- a/packages/server/src/tag/tag.resolver.ts +++ b/packages/server/src/tag/tag.resolver.ts @@ -1,4 +1,4 @@ -import { Resolver, Mutation, Args, ID, ResolveField, Parent } from '@nestjs/graphql'; +import { Resolver, Mutation, Query, Args, ID, ResolveField, Parent } from '@nestjs/graphql'; import { TagService } from './tag.service'; import { Tag } from './tag.model'; import { StudyPipe } from '../study/pipes/study.pipe'; @@ -59,13 +59,22 @@ export class TagResolver { @Mutation(() => Boolean) async setEntryEnabled( @Args('study', { type: () => ID }, StudyPipe) study: Study, - @Args('tag', { type: () => ID }, TagPipe) entry: Entry, + @Args('entry', { type: () => ID }, EntryPipe) entry: Entry, @Args('enabled', { type: () => Boolean }) enabled: boolean ): Promise { await this.tagService.setEnabled(study, entry, enabled); return true; } + @Query(() => Boolean) + async isEntryEnabled( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @Args('entry', { type: () => ID }, EntryPipe) entry: Entry, + ): Promise { + return this.tagService.isEntryEnabled(study, entry,); + } + + @ResolveField(() => Entry) async entry(@Parent() tag: Tag): Promise { return this.entryPipe.transform(tag.entry); diff --git a/packages/server/src/tag/tag.service.ts b/packages/server/src/tag/tag.service.ts index 34992b9a..a5995515 100644 --- a/packages/server/src/tag/tag.service.ts +++ b/packages/server/src/tag/tag.service.ts @@ -115,8 +115,17 @@ export class TagService { await this.tagModel.findOneAndUpdate({ _id: tag._id }, { $set: { data, complete: true } }); } - async setEnabled(study: Study, entry: Entry, enabled: boolean): Promise { - const existingTag = await this.tagModel.findOne({ _id: entry._id }); + async isEntryEnabled(study: Study, entry: Entry) { + const existingTag = await this.tagModel.findOne({ entry: entry._id, study: study._id }); + return existingTag ? existingTag.enabled : false + } + + async setEnabled(study: Study, entry: Entry, enabled: boolean): Promise { + console.log('setEnabled called with', enabled); + + const existingTag = await this.tagModel.findOne({ entry: entry._id, study: study._id }); + console.log('existingTag', existingTag); + if (existingTag) { await this.tagModel.updateMany({ entry: entry._id, study: study._id }, { $set: { enabled: enabled } }); } else { @@ -130,6 +139,11 @@ export class TagService { }); } } + console.log('find all tags'); + const all = await this.tagModel.find({ entry: entry._id, study: study._id }); + console.log(all); + + return true; } private async getIncomplete(study: Study, user: string): Promise { From 790e95ad33fb00e3af0a4eb1c0cd6cbf2246bf11 Mon Sep 17 00:00:00 2001 From: Akosah Date: Thu, 18 Jan 2024 12:54:53 -0500 Subject: [PATCH 3/7] update toggle switch with backend data --- .../ToggleEntryEnabled.component.tsx | 66 +++++++++---------- .../src/pages/studies/EntryControls.tsx | 48 ++------------ packages/server/src/tag/tag.service.ts | 8 --- 3 files changed, 36 insertions(+), 86 deletions(-) diff --git a/packages/client/src/components/ToggleEntryEnabled.component.tsx b/packages/client/src/components/ToggleEntryEnabled.component.tsx index 73bba87e..7f123da7 100644 --- a/packages/client/src/components/ToggleEntryEnabled.component.tsx +++ b/packages/client/src/components/ToggleEntryEnabled.component.tsx @@ -1,5 +1,5 @@ import { Switch } from '@mui/material'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { useIsEntryEnabledLazyQuery, useSetEntryEnabledMutation } from '../graphql/tag/tag'; import { useStudy } from '../context/Study.context'; import { useConfirmation } from '../context/Confirmation.context'; @@ -17,50 +17,48 @@ export default function ToggleEntryEnabled(props: { entryId: string }) { variables: { study: study._id, entry: props.entryId - } + }, + fetchPolicy: 'network-only' }); } - }, [study]); - console.log('isEntryEnabledResults',isEntryEnabledResults); - + }, [study, setEntryEnabledResults.data]); useEffect(() => { - console.log('setEntryEnabledResults: ', setEntryEnabledResults); - - if (setEntryEnabledResults.error) { - //show error message - console.log('error toggling entry', setEntryEnabledResults.error); - } else { + if (setEntryEnabledResults.called) { + if (setEntryEnabledResults.error) { + //show error message + console.log('error toggling entry', setEntryEnabledResults.error); + } } }, [setEntryEnabledResults.data]); const handleToggleEnabled = async (entryId: string, checked: boolean) => { - console.log('checked', checked); - if (study) { - confirmation.pushConfirmationRequest({ - title: 'Enable Entry', - message: 'Are you sure you want to delete this study? Doing so will delete all contained tags', - onConfirm: () => { - setEntryEnabledMutation({ - variables: { study: study._id, entry: entryId, enabled: checked } - }); - }, - onCancel: () => {} - }); - } else { - console.log('default study not selected', study); + if (!checked) { + confirmation.pushConfirmationRequest({ + title: 'Disable Entry', + message: 'Are you sure you want to disable this entry? Doing so will exclude this entry from study', + onConfirm: () => { + setEntryEnabledMutation({ + variables: { study: study._id, entry: entryId, enabled: checked } + }); + }, + onCancel: () => {} + }); + } else { + setEntryEnabledMutation({ + variables: { study: study._id, entry: entryId, enabled: checked } + }); + } } }; + return ( - <> - handleToggleEnabled(props.entryId, event.target.checked)} - inputProps={{ 'aria-label': 'controlled' }} - /> - + handleToggleEnabled(props.entryId, event.target.checked)} + inputProps={{ 'aria-label': 'controlled' }} + /> ); } diff --git a/packages/client/src/pages/studies/EntryControls.tsx b/packages/client/src/pages/studies/EntryControls.tsx index 90aceadd..542f986d 100644 --- a/packages/client/src/pages/studies/EntryControls.tsx +++ b/packages/client/src/pages/studies/EntryControls.tsx @@ -1,18 +1,14 @@ -import { Box, Switch, Typography } from '@mui/material'; +import { Typography } from '@mui/material'; import { useEffect, useState } from 'react'; -import DeleteIcon from '@mui/icons-material/DeleteOutlined'; -import { useConfirmation } from '../../context/Confirmation.context'; -import { DataGrid, GridActionsCellItem, GridRowId, GridColDef } from '@mui/x-data-grid'; -import { useStudy } from '../../context/Study.context'; -import { Dataset, Study } from '../../graphql/graphql'; +import { GridColDef } from '@mui/x-data-grid'; +import { Dataset } from '../../graphql/graphql'; import { DatasetsView } from '../../components/DatasetsView.component'; import { useGetDatasetsByProjectQuery, useGetDatasetsQuery } from '../../graphql/dataset/dataset'; import { useProject } from '../../context/Project.context'; -import { useIsEntryEnabledLazyQuery, useSetEntryEnabledMutation } from '../../graphql/tag/tag'; import ToggleEntryEnabled from '../../components/ToggleEntryEnabled.component'; export const EntryControls: React.FC = () => { - const { study } = useStudy(); + const { project } = useProject(); const [datasets, setDatasets] = useState([]); const getDatasetsResults = useGetDatasetsQuery(); @@ -21,10 +17,6 @@ export const EntryControls: React.FC = () => { project: project ? project._id : '' // value for 'project' } }); -// const [setEntryEnabledMutation, setEntryEnabledResults] = useSetEntryEnabledMutation(); -// const confirmation = useConfirmation(); - -// const [isEntryEnabled, isEntryEnabledResults] = useIsEntryEnabledLazyQuery(); useEffect(() => { if (getDatasetsResults.data) { @@ -33,32 +25,6 @@ export const EntryControls: React.FC = () => { }, [getDatasetsResults.data]); - - // useEffect(() => { - // if (getDatasetsByProjectResults.data) { - // setDatasets(getDatasetsByProjectResults.data.getDatasetsByProject); - // } - // }, [getDatasetsByProjectResults.data]); - -// const handleToggleEnabled = async (id: GridRowId, checked: boolean) => { -// console.log('cehcked', checked); - -// if (study) { -// confirmation.pushConfirmationRequest({ -// title: 'Enable Entry', -// message: 'Are you sure you want to delete this study? Doing so will delete all contained tags', -// onConfirm: () => { -// setEntryEnabledMutation({ -// variables: { study: study._id, entry: id.toString(), enabled: checked } -// }); -// }, -// onCancel: () => {} -// }); -// } else { -// console.log('default study not selected', study); -// } -// }; - const additionalColumns: GridColDef[] = [ { field: 'enabled', @@ -70,12 +36,6 @@ export const EntryControls: React.FC = () => { getActions: (params) => { return [ - // handleToggleEnabled(params.id, event.target.checked)} - // inputProps={{ 'aria-label': 'controlled' }} - // /> ]; } } diff --git a/packages/server/src/tag/tag.service.ts b/packages/server/src/tag/tag.service.ts index a5995515..0f417a23 100644 --- a/packages/server/src/tag/tag.service.ts +++ b/packages/server/src/tag/tag.service.ts @@ -121,11 +121,7 @@ export class TagService { } async setEnabled(study: Study, entry: Entry, enabled: boolean): Promise { - console.log('setEnabled called with', enabled); - const existingTag = await this.tagModel.findOne({ entry: entry._id, study: study._id }); - console.log('existingTag', existingTag); - if (existingTag) { await this.tagModel.updateMany({ entry: entry._id, study: study._id }, { $set: { enabled: enabled } }); } else { @@ -139,10 +135,6 @@ export class TagService { }); } } - console.log('find all tags'); - const all = await this.tagModel.find({ entry: entry._id, study: study._id }); - console.log(all); - return true; } From 1169f7ea9fbe6f9c1fab1190a9e1b4aa2045816e Mon Sep 17 00:00:00 2001 From: Akosah Date: Thu, 18 Jan 2024 13:04:36 -0500 Subject: [PATCH 4/7] add perm checks --- packages/server/src/tag/tag.resolver.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/server/src/tag/tag.resolver.ts b/packages/server/src/tag/tag.resolver.ts index 3256bb28..02519613 100644 --- a/packages/server/src/tag/tag.resolver.ts +++ b/packages/server/src/tag/tag.resolver.ts @@ -14,6 +14,7 @@ import * as casbin from 'casbin'; import { TokenContext } from '../jwt/token.context'; import { TokenPayload } from '../jwt/token.dto'; import { StudyPermissions } from '../permission/permissions/study'; +import { TagPermissions } from 'src/permission/permissions/tag'; // TODO: Add permissioning @UseGuards(JwtAuthGuard) @@ -60,8 +61,12 @@ export class TagResolver { async setEntryEnabled( @Args('study', { type: () => ID }, StudyPipe) study: Study, @Args('entry', { type: () => ID }, EntryPipe) entry: Entry, - @Args('enabled', { type: () => Boolean }) enabled: boolean + @Args('enabled', { type: () => Boolean }) enabled: boolean, + @TokenContext() user: TokenPayload ): Promise { + if (!(await this.enforcer.enforce(user.id, TagPermissions.UPDATE, study._id.toString()))) { + throw new UnauthorizedException('User cannot update tags in this study'); + } await this.tagService.setEnabled(study, entry, enabled); return true; } @@ -70,11 +75,14 @@ export class TagResolver { async isEntryEnabled( @Args('study', { type: () => ID }, StudyPipe) study: Study, @Args('entry', { type: () => ID }, EntryPipe) entry: Entry, + @TokenContext() user: TokenPayload ): Promise { - return this.tagService.isEntryEnabled(study, entry,); + if (!(await this.enforcer.enforce(user.id, TagPermissions.READ, study._id.toString()))) { + throw new UnauthorizedException('User cannot read tags in this study'); + } + return this.tagService.isEntryEnabled(study, entry); } - @ResolveField(() => Entry) async entry(@Parent() tag: Tag): Promise { return this.entryPipe.transform(tag.entry); From ba7289fe4ad86b9f1abf538043944b49b57efabe Mon Sep 17 00:00:00 2001 From: Akosah Date: Thu, 18 Jan 2024 15:37:06 -0500 Subject: [PATCH 5/7] minor clean up --- .../client/src/pages/studies/EntryControls.tsx | 17 ++++++----------- .../server/src/project/pipes/project.pipe.ts | 3 +++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/client/src/pages/studies/EntryControls.tsx b/packages/client/src/pages/studies/EntryControls.tsx index 542f986d..f99688f4 100644 --- a/packages/client/src/pages/studies/EntryControls.tsx +++ b/packages/client/src/pages/studies/EntryControls.tsx @@ -3,27 +3,24 @@ import { useEffect, useState } from 'react'; import { GridColDef } from '@mui/x-data-grid'; import { Dataset } from '../../graphql/graphql'; import { DatasetsView } from '../../components/DatasetsView.component'; -import { useGetDatasetsByProjectQuery, useGetDatasetsQuery } from '../../graphql/dataset/dataset'; +import { useGetDatasetsByProjectQuery } from '../../graphql/dataset/dataset'; import { useProject } from '../../context/Project.context'; import ToggleEntryEnabled from '../../components/ToggleEntryEnabled.component'; export const EntryControls: React.FC = () => { - const { project } = useProject(); const [datasets, setDatasets] = useState([]); - const getDatasetsResults = useGetDatasetsQuery(); const getDatasetsByProjectResults = useGetDatasetsByProjectQuery({ variables: { - project: project ? project._id : '' // value for 'project' + project: project ? project._id : '' } }); useEffect(() => { - if (getDatasetsResults.data) { - setDatasets(getDatasetsResults.data.getDatasets); + if (getDatasetsByProjectResults.data) { + setDatasets(getDatasetsByProjectResults.data.getDatasetsByProject); } - }, [getDatasetsResults.data]); - + }, [getDatasetsByProjectResults.data]); const additionalColumns: GridColDef[] = [ { @@ -34,9 +31,7 @@ export const EntryControls: React.FC = () => { maxWidth: 120, cellClassName: 'enabled', getActions: (params) => { - return [ - - ]; + return []; } } ]; diff --git a/packages/server/src/project/pipes/project.pipe.ts b/packages/server/src/project/pipes/project.pipe.ts index 5bc67ee8..ff394de0 100644 --- a/packages/server/src/project/pipes/project.pipe.ts +++ b/packages/server/src/project/pipes/project.pipe.ts @@ -7,6 +7,9 @@ export class ProjectPipe implements PipeTransform> { constructor(private readonly projectService: ProjectService) {} async transform(value: string): Promise { + if (!value){ + throw new BadRequestException(`Invalid project ID ${value}given`); + } const project = await this.projectService.findById(value); if (!project) { throw new BadRequestException(`Project with ID ${value} does not exist`); From a4f1a15ae0061eaf02ca2c9a5db5abc7c825cf44 Mon Sep 17 00:00:00 2001 From: Akosah Date: Thu, 18 Jan 2024 16:02:15 -0500 Subject: [PATCH 6/7] minor changes --- .../client/src/components/ToggleEntryEnabled.component.tsx | 7 ++++--- packages/server/src/tag/tag.service.ts | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/ToggleEntryEnabled.component.tsx b/packages/client/src/components/ToggleEntryEnabled.component.tsx index 7f123da7..6df73f12 100644 --- a/packages/client/src/components/ToggleEntryEnabled.component.tsx +++ b/packages/client/src/components/ToggleEntryEnabled.component.tsx @@ -27,17 +27,18 @@ export default function ToggleEntryEnabled(props: { entryId: string }) { if (setEntryEnabledResults.called) { if (setEntryEnabledResults.error) { //show error message - console.log('error toggling entry', setEntryEnabledResults.error); + console.error('error toggling entry', setEntryEnabledResults.error); } } - }, [setEntryEnabledResults.data]); + }, [setEntryEnabledResults.error]); const handleToggleEnabled = async (entryId: string, checked: boolean) => { if (study) { if (!checked) { confirmation.pushConfirmationRequest({ title: 'Disable Entry', - message: 'Are you sure you want to disable this entry? Doing so will exclude this entry from study', + message: + 'Are you sure you want to disable this entry? Doing so will exclude this entry from the current study.', onConfirm: () => { setEntryEnabledMutation({ variables: { study: study._id, entry: entryId, enabled: checked } diff --git a/packages/server/src/tag/tag.service.ts b/packages/server/src/tag/tag.service.ts index 0f417a23..bba6fb57 100644 --- a/packages/server/src/tag/tag.service.ts +++ b/packages/server/src/tag/tag.service.ts @@ -131,7 +131,7 @@ export class TagService { study: study._id, complete: false, order, - enabled: true + enabled: enabled }); } } From e7213ebb7773dd077f74f33ad4ef670c638aa53d Mon Sep 17 00:00:00 2001 From: Akosah Date: Thu, 18 Jan 2024 16:05:35 -0500 Subject: [PATCH 7/7] apply prettier --- packages/server/src/project/pipes/project.pipe.ts | 2 +- packages/server/src/tag/tag.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/src/project/pipes/project.pipe.ts b/packages/server/src/project/pipes/project.pipe.ts index ff394de0..c393ebac 100644 --- a/packages/server/src/project/pipes/project.pipe.ts +++ b/packages/server/src/project/pipes/project.pipe.ts @@ -7,7 +7,7 @@ export class ProjectPipe implements PipeTransform> { constructor(private readonly projectService: ProjectService) {} async transform(value: string): Promise { - if (!value){ + if (!value) { throw new BadRequestException(`Invalid project ID ${value}given`); } const project = await this.projectService.findById(value); diff --git a/packages/server/src/tag/tag.service.ts b/packages/server/src/tag/tag.service.ts index bba6fb57..5efe6d4d 100644 --- a/packages/server/src/tag/tag.service.ts +++ b/packages/server/src/tag/tag.service.ts @@ -117,7 +117,7 @@ export class TagService { async isEntryEnabled(study: Study, entry: Entry) { const existingTag = await this.tagModel.findOne({ entry: entry._id, study: study._id }); - return existingTag ? existingTag.enabled : false + return existingTag ? existingTag.enabled : false; } async setEnabled(study: Study, entry: Entry, enabled: boolean): Promise {