From df17d4a30e776b6f8f6efae33a9c73c46838f8e2 Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 4 Mar 2024 10:00:20 -0500 Subject: [PATCH 1/7] Create context for permissions --- packages/client/src/App.tsx | 23 ++++++----- .../src/components/SideBar.component.tsx | 17 ++------- .../src/components/TagTraining.component.tsx | 2 - .../client/src/context/Permission.context.tsx | 38 +++++++++++++++++++ 4 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 packages/client/src/context/Permission.context.tsx diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index dd1c9730..de7cb754 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -27,6 +27,7 @@ import { StudyProvider } from './context/Study.context'; import { ConfirmationProvider } from './context/Confirmation.context'; import { DatasetProvider } from './context/Dataset.context'; import { EntryControls } from './pages/studies/EntryControls'; +import { PermissionProvider } from './context/Permission.context'; const drawerWidth = 256; const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ @@ -90,17 +91,19 @@ const AppInternal: FC = () => { - - - -
- - - - - + + + -
+
+ + + + + + +
+
diff --git a/packages/client/src/components/SideBar.component.tsx b/packages/client/src/components/SideBar.component.tsx index 6b466b46..f50c2467 100644 --- a/packages/client/src/components/SideBar.component.tsx +++ b/packages/client/src/components/SideBar.component.tsx @@ -1,15 +1,13 @@ -import { FC, ReactNode, useEffect, useState } from 'react'; +import { FC, ReactNode, useState } from 'react'; import { Collapse, Divider, Drawer, List, ListItem, ListItemButton, ListItemIcon, ListItemText } from '@mui/material'; import { ExpandMore, ExpandLess, School, Dataset, Work, Logout, GroupWork } from '@mui/icons-material'; import { useAuth } from '../context/Auth.context'; import { useNavigate } from 'react-router-dom'; import { Environment } from './Environment.component'; import { Permission } from '../graphql/graphql'; -import { useGetRolesQuery } from '../graphql/permission/permission'; -import { useProject } from '../context/Project.context'; -import { useStudy } from '../context/Study.context'; import { useTranslation } from 'react-i18next'; import { LanguageSelector } from './LanguageSelector'; +import { usePermission } from '../context/Permission.context'; interface SideBarProps { open: boolean; @@ -19,17 +17,8 @@ interface SideBarProps { export const SideBar: FC = ({ open, drawerWidth }) => { const { logout } = useAuth(); const navigate = useNavigate(); - const [permission, setPermission] = useState(null); - const { project } = useProject(); - const { study } = useStudy(); - const rolesQueryResults = useGetRolesQuery({ variables: { project: project?._id, study: study?._id } }); const { t } = useTranslation(); - - useEffect(() => { - if (rolesQueryResults.data) { - setPermission(rolesQueryResults.data.getRoles); - } - }, [rolesQueryResults.data]); + const { permission } = usePermission(); const navItems: NavItemProps[] = [ { diff --git a/packages/client/src/components/TagTraining.component.tsx b/packages/client/src/components/TagTraining.component.tsx index a8f0487e..21500ab1 100644 --- a/packages/client/src/components/TagTraining.component.tsx +++ b/packages/client/src/components/TagTraining.component.tsx @@ -73,8 +73,6 @@ export const TagTrainingComponent: React.FC = (props) props.setTrainingSet(entries); }, [trainingSet]); - // TODO: In the future, the datasets retrieved should only be datasets - // accessible by the current project useEffect(() => { if (getDatasetsResults.data) { setDatasets(getDatasetsResults.data.getDatasetsByProject); diff --git a/packages/client/src/context/Permission.context.tsx b/packages/client/src/context/Permission.context.tsx new file mode 100644 index 00000000..d87d10de --- /dev/null +++ b/packages/client/src/context/Permission.context.tsx @@ -0,0 +1,38 @@ +import { ReactNode, createContext, useContext, useEffect, useState } from 'react'; +import { Permission } from '../graphql/graphql'; +import { useProject } from './Project.context'; +import { useStudy } from './Study.context'; +import { useGetRolesQuery } from '../graphql/permission/permission'; + +interface PermissionContextProps { + permission: Permission | null; +} + +const PermissionContext = createContext({} as PermissionContextProps); + + +export interface PermissionProviderProps { + children: ReactNode; +} + +export const PermissionProvider: React.FC = ({ children }) => { + const [permission, setPermission] = useState(null); + const { project } = useProject(); + const { study } = useStudy(); + + const rolesQueryResult = useGetRolesQuery({ variables: { project: project?._id, study: study?._id }}); + + useEffect(() => { + if (rolesQueryResult.data) { + setPermission(rolesQueryResult.data.getRoles); + } + }, [rolesQueryResult]); + + return ( + + { children } + + ) +}; + +export const usePermission = () => useContext(PermissionContext); From a17bbebfaccc33b61ed44a8eef5ce640033315c0 Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 4 Mar 2024 10:08:56 -0500 Subject: [PATCH 2/7] Have contribute landing reflect training vs full tagging --- packages/client/src/context/Tag.context.tsx | 10 ++++++++-- .../client/src/pages/contribute/ContributeLanding.tsx | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/client/src/context/Tag.context.tsx b/packages/client/src/context/Tag.context.tsx index 0e3f0b30..958a2fd2 100644 --- a/packages/client/src/context/Tag.context.tsx +++ b/packages/client/src/context/Tag.context.tsx @@ -1,9 +1,11 @@ import { ReactNode, FC, createContext, useContext, useEffect, useState } from 'react'; import { useStudy } from './Study.context'; import { AssignTagMutation, useAssignTagMutation } from '../graphql/tag/tag'; +import { usePermission } from './Permission.context'; export interface TagContextProps { tag: AssignTagMutation['assignTag'] | null; + training: boolean; requestTag: () => void; } @@ -17,10 +19,14 @@ export const TagProvider: FC = ({ children }) => { const { study } = useStudy(); const [tag, setTag] = useState(null); const [assignTag, assignTagResult] = useAssignTagMutation(); + const { permission } = usePermission(); + const [training, setTraining] = useState(false); useEffect(() => { requestTag(); - }, [study]); + + setTraining(permission ? !permission.trainedContributor : false); + }, [permission]); useEffect(() => { setTag(assignTagResult.data?.assignTag); @@ -35,7 +41,7 @@ export const TagProvider: FC = ({ children }) => { assignTag({ variables: { study: study._id }, fetchPolicy: 'network-only' }); }; - return {children}; + return {children}; }; export const useTag = () => useContext(TagContext); diff --git a/packages/client/src/pages/contribute/ContributeLanding.tsx b/packages/client/src/pages/contribute/ContributeLanding.tsx index d57b7241..5d130ef8 100644 --- a/packages/client/src/pages/contribute/ContributeLanding.tsx +++ b/packages/client/src/pages/contribute/ContributeLanding.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; const ContributeLandingInternal: React.FC = () => { const navigate = useNavigate(); const { study } = useStudy(); - const { tag } = useTag(); + const { tag, training } = useTag(); const { t } = useTranslation(); const enterTagging = () => { @@ -26,7 +26,7 @@ const ContributeLandingInternal: React.FC = () => { - {false ? t('components.contribute.studyTraining') : t('components.contribute.studyTagging')} + {training ? t('components.contribute.studyTraining') : t('components.contribute.studyTagging')} {t('common.study')}: {study.name} From 91f279ba7a28b5b251f6d11d36c59f14c9b3d0a9 Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 4 Mar 2024 10:21:14 -0500 Subject: [PATCH 3/7] Begin adding concept of training and full tagging --- packages/server/src/tag/resolvers/tag.resolver.ts | 6 +++++- packages/server/src/tag/services/tag.service.ts | 12 +++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index 07a892e9..71ff6a72 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -15,6 +15,7 @@ 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'; +import { Roles } from 'src/permission/permissions/roles'; // TODO: Add permissioning @UseGuards(JwtAuthGuard) @@ -44,7 +45,10 @@ export class TagResolver { @Args('study', { type: () => ID }, StudyPipe) study: Study, @TokenContext() user: TokenPayload ): Promise { - return this.tagService.assignTag(study, user.user_id); + // Determine if the user is considered "trained" + const isTrained: boolean = await this.enforcer.enforce(user.user_id, Roles.TRAINED_CONTRIBUTOR, study._id.toString()); + + return this.tagService.assignTag(study, user.user_id, isTrained); } @Mutation(() => Boolean) diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 7f1e2a89..2e50b9ed 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -56,12 +56,22 @@ export class TagService { return tags; } + async assignTag(study: Study, user: string, isTrained: boolean): Promise { + return isTrained ? this.assignTagFull(study, user) : this.assignTrainingTag(study, user); + } + + private async assignTrainingTag(study: Study, user: string): Promise { + return null; + } + /** + * Assign tags based on the full (not training) data set. + * * Assign the tag to the given user. If the user already has an incomplete * tag, return that tag to the user. If there are no more remaining tags, * null is returned. */ - async assignTag(study: Study, user: string): Promise { + private async assignTagFull(study: Study, user: string): Promise { // Check for incomplete tags const incomplete = await this.getIncomplete(study, user); if (incomplete) { From 3f75cbb51fc13f34808680f03f7036a2ec3a3736 Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 4 Mar 2024 10:44:14 -0500 Subject: [PATCH 4/7] Add ability to create/query on training set --- .../server/src/tag/services/tag.service.ts | 56 ++++++++++++++++++- .../src/tag/services/training-set.service.ts | 4 ++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 2e50b9ed..9ae40f71 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -8,6 +8,8 @@ import { StudyService } from '../../study/study.service'; import { MongooseMiddlewareService } from '../../shared/service/mongoose-callback.service'; import { TagTransformer } from './tag-transformer.service'; import { TokenPayload } from '../../jwt/token.dto'; +import { TrainingSetService } from './training-set.service'; +import { TrainingSet } from '../models/training-set'; @Injectable() export class TagService { @@ -15,7 +17,8 @@ export class TagService { @InjectModel(Tag.name) private readonly tagModel: Model, private readonly studyService: StudyService, middlewareService: MongooseMiddlewareService, - private readonly tagTransformService: TagTransformer + private readonly tagTransformService: TagTransformer, + private readonly trainingSetService: TrainingSetService ) { // Subscribe to study delete events middlewareService.register(Study.name, 'deleteOne', async (study: Study) => { @@ -48,7 +51,8 @@ export class TagService { study: study._id, complete: false, order, - enabled: true + enabled: true, + training: false }); tags.push(newTag); } @@ -60,8 +64,54 @@ export class TagService { return isTrained ? this.assignTagFull(study, user) : this.assignTrainingTag(study, user); } + /** + * Assign the user a tag as part of the training set. + */ private async assignTrainingTag(study: Study, user: string): Promise { - return null; + // First get the training set associated with the study + const trainingSet = await this.trainingSetService.findByStudy(study); + + // If the training set is null or the length of entries is 0, then no tag to assign + if (!trainingSet || trainingSet.entries.length == 0) { + return null; + } + + // See if the user has any training tags. If we have reached this point in the code, + // and no training tags exist, then they haven't been generated for this user yet. + const existingTrainingTag = await this.tagModel.findOne({ + user, + study: study._id, + training: true + }); + + // If there is no existing training tag, generate the training set for the user + if (!existingTrainingTag) { + await this.createTrainingTags(study, user, trainingSet); + } + + // At this point, the next incomplete training tag can be returned + const tags = await this.tagModel.find({ + study: study._id, + user, + training: true, + complete: false + }).sort({ order: 1 }).limit(1); + + return tags[0]; + } + + private async createTrainingTags(study: Study, user: string, trainingSet: TrainingSet): Promise { + return await Promise.all(trainingSet.entries.map(async (entry, index) => { + return this.tagModel.create({ + entry, + study: study._id, + complete: false, + order: index, + enabled: true, + training: true, + user + }); + })); } /** diff --git a/packages/server/src/tag/services/training-set.service.ts b/packages/server/src/tag/services/training-set.service.ts index 774a0531..ea576006 100644 --- a/packages/server/src/tag/services/training-set.service.ts +++ b/packages/server/src/tag/services/training-set.service.ts @@ -15,4 +15,8 @@ export class TrainingSetService { entries: entries.map((entry) => entry._id) }); } + + async findByStudy(study: Study): Promise { + return this.trainingSetModel.findOne({ study: study._id }); + } } From 6ed24ae8f3f1b166dd90666307d42d9b44b44588 Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 4 Mar 2024 11:14:52 -0500 Subject: [PATCH 5/7] Start working on training result visualization --- .../client/public/locales/en/translation.json | 3 ++- .../src/pages/studies/UserPermissions.tsx | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/client/public/locales/en/translation.json b/packages/client/public/locales/en/translation.json index a2e6ef80..4af3f908 100644 --- a/packages/client/public/locales/en/translation.json +++ b/packages/client/public/locales/en/translation.json @@ -85,7 +85,8 @@ "userPermissions": { "studyAdmin": "Study Admin", "contributor": "Contributor", - "trained": "Trained" + "trained": "Trained", + "trainingView": "View Training Results" }, "projectUserPermissions": { "projectAdmin": "Project Admin", diff --git a/packages/client/src/pages/studies/UserPermissions.tsx b/packages/client/src/pages/studies/UserPermissions.tsx index 8df0f6a6..f216198d 100644 --- a/packages/client/src/pages/studies/UserPermissions.tsx +++ b/packages/client/src/pages/studies/UserPermissions.tsx @@ -1,4 +1,4 @@ -import { Switch, Typography } from '@mui/material'; +import { Switch, Typography, Button } from '@mui/material'; import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { useStudy } from '../../context/Study.context'; import { Study, StudyPermissionModel } from '../../graphql/graphql'; @@ -115,6 +115,17 @@ const EditTrainedSwitch: React.FC = (props) => { ); }; +interface TagViewButtonProps { + permission: StudyPermissionModel; +} + +const TagViewButton: React.FC = (props) => { + const { t } = useTranslation(); + return ( + + ) +}; + const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { const { decodedToken } = useAuth(); const { data, refetch } = useGetStudyPermissionsQuery({ @@ -176,6 +187,16 @@ const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { }, editable: false, flex: 1 + }, + { + field: 'traingData', + headerName: t('components.userPermissions.trainingView'), + renderCell: (params: GridRenderCellParams) => { + return ( + + ) + }, + flex: 1 } ]; return ( From e8845021f2294c40eaefee1d5fb15fc3c3dce178 Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 4 Mar 2024 11:51:32 -0500 Subject: [PATCH 6/7] Ability to view tag training results --- .../client/public/locales/en/translation.json | 3 +- packages/client/src/App.tsx | 2 + .../tag/view/TagGridView.component.tsx | 17 ++---- packages/client/src/graphql/graphql.ts | 7 +++ packages/client/src/graphql/tag/tag.graphql | 19 ++++++ packages/client/src/graphql/tag/tag.ts | 60 ++++++++++++++++++- .../src/pages/studies/TagTrainingView.tsx | 29 +++++++++ packages/client/src/pages/studies/TagView.tsx | 21 ++++++- .../src/pages/studies/UserPermissions.tsx | 17 +++++- .../server/src/tag/resolvers/tag.resolver.ts | 13 ++++ .../server/src/tag/services/tag.service.ts | 8 +++ 11 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 packages/client/src/pages/studies/TagTrainingView.tsx diff --git a/packages/client/public/locales/en/translation.json b/packages/client/public/locales/en/translation.json index 4af3f908..404144b9 100644 --- a/packages/client/public/locales/en/translation.json +++ b/packages/client/public/locales/en/translation.json @@ -86,7 +86,8 @@ "studyAdmin": "Study Admin", "contributor": "Contributor", "trained": "Trained", - "trainingView": "View Training Results" + "trainingView": "View Training Results", + "noTrainingTags": "No Tags to Show" }, "projectUserPermissions": { "projectAdmin": "Project Admin", diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index de7cb754..74234797 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -28,6 +28,7 @@ import { ConfirmationProvider } from './context/Confirmation.context'; import { DatasetProvider } from './context/Dataset.context'; import { EntryControls } from './pages/studies/EntryControls'; import { PermissionProvider } from './context/Permission.context'; +import { TagTrainingView } from './pages/studies/TagTrainingView'; const drawerWidth = 256; const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ @@ -129,6 +130,7 @@ const MyRoutes: FC = () => { } /> } /> } /> + } /> } /> } /> } /> diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 426f356b..67d6e641 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -1,10 +1,9 @@ import { useTranslation } from 'react-i18next'; import { GetGridColDefs, TagViewTest } from '../../../types/TagColumnView'; -import { Study, Entry } from '../../../graphql/graphql'; +import { Entry, Study } from '../../../graphql/graphql'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid'; -import { GetTagsQuery, useGetTagsQuery } from '../../../graphql/tag/tag'; -import { useEffect, useState } from 'react'; +import { GetTagsQuery } from '../../../graphql/tag/tag'; import { freeTextTest, getTextCols } from './FreeTextGridView.component'; import { EntryView } from '../../EntryView.component'; import { Checkbox } from '@mui/material'; @@ -16,11 +15,11 @@ import { getVideoCols, videoViewTest } from './VideoGridView.component'; export interface TagGridViewProps { study: Study; + tags: GetTagsQuery['getTags']; } -export const TagGridView: React.FC = ({ study }) => { +export const TagGridView: React.FC = ({ tags, study }) => { const { t } = useTranslation(); - const [tags, setTags] = useState([]); const tagColumnViews: { tester: TagViewTest; getGridColDefs: GetGridColDefs }[] = [ { tester: freeTextTest, getGridColDefs: getTextCols }, @@ -31,14 +30,6 @@ export const TagGridView: React.FC = ({ study }) => { { tester: videoViewTest, getGridColDefs: getVideoCols } ]; - const getTagsResults = useGetTagsQuery({ variables: { study: study._id } }); - - useEffect(() => { - if (getTagsResults.data) { - setTags(getTagsResults.data.getTags); - } - }, [getTagsResults.data]); - const entryColumns: GridColDef[] = [ { field: 'entryView', diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 486a8f49..e6c9a6bf 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -360,6 +360,7 @@ export type Query = { getRoles: Permission; getStudyPermissions: Array; getTags: Array; + getTrainingTags: Array; isEntryEnabled: Scalars['Boolean']['output']; lexFindAll: Array; lexiconByKey: LexiconEntry; @@ -438,6 +439,12 @@ export type QueryGetTagsArgs = { }; +export type QueryGetTrainingTagsArgs = { + study: Scalars['ID']['input']; + user: Scalars['String']['input']; +}; + + export type QueryIsEntryEnabledArgs = { entry: Scalars['ID']['input']; study: Scalars['ID']['input']; diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index 2efa1006..af863dca 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -65,3 +65,22 @@ query getTags($study: ID!) { } } +query getTrainingTags($study: ID!, $user: String!) { + getTrainingTags(study: $study, user: $user) { + _id + entry { + _id + organization + entryID + contentType + dataset + creator + dateCreated + meta + signedUrl + signedUrlExpiration + } + data + complete + } +} diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index 6417d986..e36489ed 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -69,6 +69,14 @@ export type GetTagsQueryVariables = Types.Exact<{ export type GetTagsQuery = { __typename?: 'Query', getTags: Array<{ __typename?: 'Tag', _id: string, data?: any | null, complete: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }> }; +export type GetTrainingTagsQueryVariables = Types.Exact<{ + study: Types.Scalars['ID']['input']; + user: Types.Scalars['String']['input']; +}>; + + +export type GetTrainingTagsQuery = { __typename?: 'Query', getTrainingTags: Array<{ __typename?: 'Tag', _id: string, data?: any | null, complete: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }> }; + export const CreateTagsDocument = gql` mutation createTags($study: ID!, $entries: [ID!]!) { @@ -364,4 +372,54 @@ export function useGetTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions; export type GetTagsLazyQueryHookResult = ReturnType; -export type GetTagsQueryResult = Apollo.QueryResult; \ No newline at end of file +export type GetTagsQueryResult = Apollo.QueryResult; +export const GetTrainingTagsDocument = gql` + query getTrainingTags($study: ID!, $user: String!) { + getTrainingTags(study: $study, user: $user) { + _id + entry { + _id + organization + entryID + contentType + dataset + creator + dateCreated + meta + signedUrl + signedUrlExpiration + } + data + complete + } +} + `; + +/** + * __useGetTrainingTagsQuery__ + * + * To run a query within a React component, call `useGetTrainingTagsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetTrainingTagsQuery` 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 } = useGetTrainingTagsQuery({ + * variables: { + * study: // value for 'study' + * user: // value for 'user' + * }, + * }); + */ +export function useGetTrainingTagsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetTrainingTagsDocument, options); + } +export function useGetTrainingTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetTrainingTagsDocument, options); + } +export type GetTrainingTagsQueryHookResult = ReturnType; +export type GetTrainingTagsLazyQueryHookResult = ReturnType; +export type GetTrainingTagsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/packages/client/src/pages/studies/TagTrainingView.tsx b/packages/client/src/pages/studies/TagTrainingView.tsx new file mode 100644 index 00000000..114034d3 --- /dev/null +++ b/packages/client/src/pages/studies/TagTrainingView.tsx @@ -0,0 +1,29 @@ +import { useLocation } from 'react-router-dom'; +import { User, Study } from '../../graphql/graphql'; +import { GetTagsQuery, useGetTrainingTagsQuery } from '../../graphql/tag/tag'; +import { useEffect, useState } from 'react'; +import { TagGridView } from '../../components/tag/view/TagGridView.component'; +import { Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +export const TagTrainingView: React.FC = () => { + const state = useLocation().state; + const user: User = state.user; + const study: Study = state.study; + const [tags, setTags] = useState([]); + const { t } = useTranslation(); + + const trainingTags = useGetTrainingTagsQuery({ variables: { study: study._id, user: user.uid }}); + + useEffect(() => { + if (trainingTags.data) { + setTags(trainingTags.data.getTrainingTags); + } + }, [trainingTags]); + + return ( + <> + {(!tags || tags.length === 0) ? {t('components.userPermissions.noTrainingTags')} : } + + ); +} diff --git a/packages/client/src/pages/studies/TagView.tsx b/packages/client/src/pages/studies/TagView.tsx index 09879e1e..36ba2ba5 100644 --- a/packages/client/src/pages/studies/TagView.tsx +++ b/packages/client/src/pages/studies/TagView.tsx @@ -2,15 +2,34 @@ import { Container, Typography } from '@mui/material'; import { useTranslation } from 'react-i18next'; import { useStudy } from '../../context/Study.context'; import { TagGridView } from '../../components/tag/view/TagGridView.component'; +import { useEffect, useState } from 'react'; +import { GetTagsQuery, useGetTagsLazyQuery } from '../../graphql/tag/tag'; export const TagView: React.FC = () => { const { t } = useTranslation(); const { study } = useStudy(); + const [tags, setTags] = useState([]); + const [getTagQuery, getTagResult] = useGetTagsLazyQuery(); + + useEffect(() => { + if (!study) { + return; + } + + getTagQuery({ variables: { study: study._id } }); + }, [study]); + + useEffect(() => { + if (!getTagResult.data) { + return; + } + setTags(getTagResult.data.getTags); + }, [getTagResult]); return ( {t('menu.viewTags')} - {study && } + {study && } ); }; diff --git a/packages/client/src/pages/studies/UserPermissions.tsx b/packages/client/src/pages/studies/UserPermissions.tsx index f216198d..4d2ded85 100644 --- a/packages/client/src/pages/studies/UserPermissions.tsx +++ b/packages/client/src/pages/studies/UserPermissions.tsx @@ -11,6 +11,7 @@ import { } from '../../graphql/permission/permission'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; export const StudyUserPermissions: React.FC = () => { const { study } = useStudy(); @@ -117,12 +118,24 @@ const EditTrainedSwitch: React.FC = (props) => { interface TagViewButtonProps { permission: StudyPermissionModel; + study: Study; } const TagViewButton: React.FC = (props) => { const { t } = useTranslation(); + const navigation = useNavigate(); + + const onClick = () => { + navigation('/study/training', { + state: { + user: props.permission.user, + study: props.study + } + }); + }; + return ( - + ) }; @@ -193,7 +206,7 @@ const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { headerName: t('components.userPermissions.trainingView'), renderCell: (params: GridRenderCellParams) => { return ( - + ) }, flex: 1 diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index 71ff6a72..ce7f274a 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -100,6 +100,19 @@ export class TagResolver { return this.tagService.getTags(study); } + @Query(() => [Tag]) + async getTrainingTags( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @Args('user') user: string, + @TokenContext() requestingUser: TokenPayload + ): Promise { + if (!(await this.enforcer.enforce(requestingUser.user_id, TagPermissions.READ, study._id.toString()))) { + throw new UnauthorizedException('User cannot read tags in this study'); + } + + return this.tagService.getTrainingTags(study, user); + } + @ResolveField(() => Entry) async entry(@Parent() tag: Tag): Promise { return this.entryPipe.transform(tag.entry); diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 9ae40f71..e740aaee 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -64,6 +64,14 @@ export class TagService { return isTrained ? this.assignTagFull(study, user) : this.assignTrainingTag(study, user); } + async getTrainingTags(study: Study, user: string): Promise { + return this.tagModel.find({ + user, + study: study._id, + training: true + }); + } + /** * Assign the user a tag as part of the training set. */ From 8851ad264ed40ac348e1a80534be926da5035a12 Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 4 Mar 2024 11:52:04 -0500 Subject: [PATCH 7/7] Fix formatting --- .../client/src/context/Permission.context.tsx | 9 +---- .../src/pages/studies/TagTrainingView.tsx | 10 +++-- .../src/pages/studies/UserPermissions.tsx | 10 ++--- .../server/src/tag/resolvers/tag.resolver.ts | 6 ++- .../server/src/tag/services/tag.service.ts | 39 +++++++++++-------- 5 files changed, 41 insertions(+), 33 deletions(-) diff --git a/packages/client/src/context/Permission.context.tsx b/packages/client/src/context/Permission.context.tsx index d87d10de..daf5e68b 100644 --- a/packages/client/src/context/Permission.context.tsx +++ b/packages/client/src/context/Permission.context.tsx @@ -10,7 +10,6 @@ interface PermissionContextProps { const PermissionContext = createContext({} as PermissionContextProps); - export interface PermissionProviderProps { children: ReactNode; } @@ -20,7 +19,7 @@ export const PermissionProvider: React.FC = ({ children const { project } = useProject(); const { study } = useStudy(); - const rolesQueryResult = useGetRolesQuery({ variables: { project: project?._id, study: study?._id }}); + const rolesQueryResult = useGetRolesQuery({ variables: { project: project?._id, study: study?._id } }); useEffect(() => { if (rolesQueryResult.data) { @@ -28,11 +27,7 @@ export const PermissionProvider: React.FC = ({ children } }, [rolesQueryResult]); - return ( - - { children } - - ) + return {children}; }; export const usePermission = () => useContext(PermissionContext); diff --git a/packages/client/src/pages/studies/TagTrainingView.tsx b/packages/client/src/pages/studies/TagTrainingView.tsx index 114034d3..3e0d920e 100644 --- a/packages/client/src/pages/studies/TagTrainingView.tsx +++ b/packages/client/src/pages/studies/TagTrainingView.tsx @@ -13,7 +13,7 @@ export const TagTrainingView: React.FC = () => { const [tags, setTags] = useState([]); const { t } = useTranslation(); - const trainingTags = useGetTrainingTagsQuery({ variables: { study: study._id, user: user.uid }}); + const trainingTags = useGetTrainingTagsQuery({ variables: { study: study._id, user: user.uid } }); useEffect(() => { if (trainingTags.data) { @@ -23,7 +23,11 @@ export const TagTrainingView: React.FC = () => { return ( <> - {(!tags || tags.length === 0) ? {t('components.userPermissions.noTrainingTags')} : } + {!tags || tags.length === 0 ? ( + {t('components.userPermissions.noTrainingTags')} + ) : ( + + )} ); -} +}; diff --git a/packages/client/src/pages/studies/UserPermissions.tsx b/packages/client/src/pages/studies/UserPermissions.tsx index 4d2ded85..df7fd8f7 100644 --- a/packages/client/src/pages/studies/UserPermissions.tsx +++ b/packages/client/src/pages/studies/UserPermissions.tsx @@ -135,8 +135,10 @@ const TagViewButton: React.FC = (props) => { }; return ( - - ) + + ); }; const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { @@ -205,9 +207,7 @@ const UserPermissionTable: React.FC<{ study: Study }> = ({ study }) => { field: 'traingData', headerName: t('components.userPermissions.trainingView'), renderCell: (params: GridRenderCellParams) => { - return ( - - ) + return ; }, flex: 1 } diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index ce7f274a..257c3f4b 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -46,7 +46,11 @@ export class TagResolver { @TokenContext() user: TokenPayload ): Promise { // Determine if the user is considered "trained" - const isTrained: boolean = await this.enforcer.enforce(user.user_id, Roles.TRAINED_CONTRIBUTOR, study._id.toString()); + const isTrained: boolean = await this.enforcer.enforce( + user.user_id, + Roles.TRAINED_CONTRIBUTOR, + study._id.toString() + ); return this.tagService.assignTag(study, user.user_id, isTrained); } diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index e740aaee..274ffc77 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -98,28 +98,33 @@ export class TagService { } // At this point, the next incomplete training tag can be returned - const tags = await this.tagModel.find({ - study: study._id, - user, - training: true, - complete: false - }).sort({ order: 1 }).limit(1); + const tags = await this.tagModel + .find({ + study: study._id, + user, + training: true, + complete: false + }) + .sort({ order: 1 }) + .limit(1); return tags[0]; } private async createTrainingTags(study: Study, user: string, trainingSet: TrainingSet): Promise { - return await Promise.all(trainingSet.entries.map(async (entry, index) => { - return this.tagModel.create({ - entry, - study: study._id, - complete: false, - order: index, - enabled: true, - training: true, - user - }); - })); + return await Promise.all( + trainingSet.entries.map(async (entry, index) => { + return this.tagModel.create({ + entry, + study: study._id, + complete: false, + order: index, + enabled: true, + training: true, + user + }); + }) + ); } /**