Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/client/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"completed": "All steps completed - your new study is created",
"createStudy": "Create New Study",
"disableClear": "Disable Clear Button",
"showPriorCue": "Show Original Cue Sign",
"disableSameUserTagging": "Disable users tagging entries they recorded",
"formTitle": "Study Information",
"noDatasets": "No datasets available, create a dataset and grant the project access under Datasets > Project Access",
Expand Down Expand Up @@ -143,7 +144,9 @@
"succefullyCreated": "Successfully created!"
},
"tagView": {
"originalEntry": "Original Entry"
"originalEntry": "Original Entry",
"originalCue": "Original Cue",
"responseToCue": "Response to Original Cue"
},
"userPermissions": {
"contributor": "Contributor",
Expand Down
33 changes: 30 additions & 3 deletions packages/client/src/components/EntryView.component.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Box } from '@mui/material';
import { Entry } from '../graphql/graphql';
import { Box, Grid, Typography } from '@mui/material';
import { VideoViewProps, VideoEntryView } from './VideoView.component';
import { AssignTagMutation } from '../graphql/tag/tag';
import { useTranslation } from 'react-i18next';

export interface EntryViewProps extends Omit<VideoViewProps, 'url'> {
entry: Entry;
entry: NonNullable<AssignTagMutation['assignTag']>['entry'];
showCue?: boolean;
}

export const EntryView: React.FC<EntryViewProps> = (props) => {
if (props.showCue) {
return <ShowWithCue {...props} />;
}
return getEntryView(props);
};

Expand All @@ -28,3 +33,25 @@ const ImageEntryView: React.FC<EntryViewProps> = (props) => {
</Box>
);
};

const ShowWithCue: React.FC<EntryViewProps> = (props) => {
const { t } = useTranslation();
const originalCue = props.entry.signlabRecording?.tag.entry;

return (
<Grid container>
<Grid item xs={2}>
<Typography variant="body1">{t('components.tagView.originalCue')}</Typography>
</Grid>
<Grid item xs={10}>
{originalCue ? getEntryView({ ...props, entry: originalCue }) : <></>}
</Grid>
<Grid item xs={2}>
<Typography variant="body1">{t('components.tagView.responseToCue')}</Typography>
</Grid>
<Grid item xs={10}>
{getEntryView({ ...props })}
</Grid>
</Grid>
);
};
11 changes: 10 additions & 1 deletion packages/client/src/components/NewStudyJsonForm.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ export const NewStudyJsonForm: React.FC<NewStudyFormProps> = (props) => {
},
disableClear: {
type: 'boolean',
default: 'false'
default: false
},
showPriorCue: {
type: 'boolean',
default: false
}
}
}
Expand Down Expand Up @@ -93,6 +97,11 @@ export const NewStudyJsonForm: React.FC<NewStudyFormProps> = (props) => {
type: 'Control',
label: t('components.newStudy.disableClear'),
scope: '#/properties/studyConfig/properties/disableClear'
},
{
type: 'Control',
label: t('components.newStudy.showPriorCue'),
scope: '#/properties/studyConfig/properties/showPriorCue'
}
]
};
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/graphql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ export type QueryValidateCsvArgs = {
export type SignLabRecorded = {
__typename?: 'SignLabRecorded';
fieldName: Scalars['String']['output'];
tag: Tag;
};

export type SliderField = {
Expand All @@ -674,12 +675,14 @@ export type StudyConfig = {
__typename?: 'StudyConfig';
disableClear?: Maybe<Scalars['Boolean']['output']>;
disableSameUserEntryTagging?: Maybe<Scalars['Boolean']['output']>;
showPriorCue?: Maybe<Scalars['Boolean']['output']>;
sortByEntryID?: Maybe<Scalars['Boolean']['output']>;
};

export type StudyConfigInput = {
disableClear?: InputMaybe<Scalars['Boolean']['input']>;
disableSameUserEntryTagging?: InputMaybe<Scalars['Boolean']['input']>;
showPriorCue?: InputMaybe<Scalars['Boolean']['input']>;
sortByEntryID?: InputMaybe<Scalars['Boolean']['input']>;
};

Expand Down
1 change: 1 addition & 0 deletions packages/client/src/graphql/study/study.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ query findStudies($project: ID!) {
disableSameUserEntryTagging
sortByEntryID
disableClear
showPriorCue
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/graphql/study/study.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type FindStudiesQueryVariables = Types.Exact<{
}>;


export type FindStudiesQuery = { __typename?: 'Query', findStudies: Array<{ __typename?: 'Study', _id: string, name: string, description: string, instructions: string, project: string, tagsPerEntry: number, tagSchema: { __typename?: 'TagSchema', dataSchema: any, uiSchema: any }, studyConfig?: { __typename?: 'StudyConfig', disableSameUserEntryTagging?: boolean | null, sortByEntryID?: boolean | null, disableClear?: boolean | null } | null }> };
export type FindStudiesQuery = { __typename?: 'Query', findStudies: Array<{ __typename?: 'Study', _id: string, name: string, description: string, instructions: string, project: string, tagsPerEntry: number, tagSchema: { __typename?: 'TagSchema', dataSchema: any, uiSchema: any }, studyConfig?: { __typename?: 'StudyConfig', disableSameUserEntryTagging?: boolean | null, sortByEntryID?: boolean | null, disableClear?: boolean | null, showPriorCue?: boolean | null } | null }> };

export type DeleteStudyMutationVariables = Types.Exact<{
study: Types.Scalars['ID']['input'];
Expand Down Expand Up @@ -67,6 +67,7 @@ export const FindStudiesDocument = gql`
disableSameUserEntryTagging
sortByEntryID
disableClear
showPriorCue
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions packages/client/src/graphql/tag/tag.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ mutation assignTag($study: ID!) {
signedUrl
signedUrlExpiration
isTraining
signlabRecording {
tag {
_id
entry {
_id
organization
entryID
contentType
dataset
creator
dateCreated
meta
signedUrl
signedUrlExpiration
isTraining
}
}
}
}
}
}
Expand Down
20 changes: 19 additions & 1 deletion packages/client/src/graphql/tag/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export type AssignTagMutationVariables = Types.Exact<{
}>;


export type AssignTagMutation = { __typename?: 'Mutation', assignTag?: { __typename?: 'Tag', _id: string, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number, isTraining: boolean } } | null };
export type AssignTagMutation = { __typename?: 'Mutation', assignTag?: { __typename?: 'Tag', _id: string, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number, isTraining: boolean, signlabRecording?: { __typename?: 'SignLabRecorded', tag: { __typename?: 'Tag', _id: string, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number, isTraining: boolean } } } | null } } | null };

export type CompleteTagMutationVariables = Types.Exact<{
tag: Types.Scalars['ID']['input'];
Expand Down Expand Up @@ -284,6 +284,24 @@ export const AssignTagDocument = gql`
signedUrl
signedUrlExpiration
isTraining
signlabRecording {
tag {
_id
entry {
_id
organization
entryID
contentType
dataset
creator
dateCreated
meta
signedUrl
signedUrlExpiration
isTraining
}
}
}
}
}
}
Expand Down
10 changes: 7 additions & 3 deletions packages/client/src/pages/contribute/TaggingInterface.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box } from '@mui/material';
import { Stack } from '@mui/material';
import { EntryView } from '../../components/EntryView.component';
import { TagForm } from '../../components/contribute/TagForm.component';
import { useStudy } from '../../context/Study.context';
Expand Down Expand Up @@ -58,17 +58,21 @@ const MainView: React.FC<MainViewProps> = (props) => {
return (
<>
{tag ? (
<Box sx={{ justifyContent: 'space-between', display: 'flex', maxWidth: '80%', margin: 'auto' }}>
<Stack
direction="row"
sx={{ justifyContent: 'space-between', display: 'flex', maxWidth: '80%', margin: 'auto' }}
>
<EntryView
entry={tag.entry}
width={500}
autoPlay={true}
pauseFrame="start"
mouseOverControls={false}
displayControls={true}
showCue={!!props.study.studyConfig?.showPriorCue}
/>
<TagForm study={props.study} setTagData={setTagData} />
</Box>
</Stack>
) : (
<NoTagNotification studyName={props.study.name} />
)}
Expand Down
10 changes: 7 additions & 3 deletions packages/server/src/entry/entry.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { Entry, EntrySchema } from './models/entry.model';
import { EntryResolver } from './resolvers/entry.resolver';
Expand All @@ -19,6 +19,8 @@ import { MongooseMiddlewareService } from '../shared/service/mongoose-callback.s
import { SharedModule } from '../shared/shared.module';
import { OrganizationModule } from '../organization/organization.module';
import { BucketModule } from 'src/bucket/bucket.module';
import { SignLabRecordingResolver } from './resolvers/signlab-recording.resolver';
import { TagModule } from '../tag/tag.module';

@Module({
imports: [
Expand Down Expand Up @@ -46,7 +48,8 @@ import { BucketModule } from 'src/bucket/bucket.module';
PermissionModule,
JwtModule,
OrganizationModule,
BucketModule
BucketModule,
forwardRef(() => TagModule)
],
providers: [
EntryResolver,
Expand All @@ -57,7 +60,8 @@ import { BucketModule } from 'src/bucket/bucket.module';
UploadSessionResolver,
UploadSessionPipe,
EntryUploadService,
CsvValidationService
CsvValidationService,
SignLabRecordingResolver
],
exports: [EntryPipe, EntriesPipe, EntryService]
})
Expand Down
2 changes: 2 additions & 0 deletions packages/server/src/entry/models/entry.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose';
import { ObjectType, Field, ID } from '@nestjs/graphql';
import mongoose, { Document } from 'mongoose';
import JSON from 'graphql-type-json';
import { Tag } from '../../tag/models/tag.model';

@Schema()
@ObjectType()
export class SignLabRecorded {
/** The tag the recording is associated with */
@Prop({ required: true })
@Field(() => Tag)
tag: string;

/** The name of the field within the tag */
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/entry/resolvers/entry.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class EntryResolver {
async countEntryForDataset(
@Args('dataset', { type: () => ID }, DatasetPipe) dataset: Dataset,
@TokenContext() user: TokenPayload
): Promise<Number> {
): Promise<number> {
if (!(await this.enforcer.enforce(user.user_id, DatasetPermissions.READ, dataset._id.toString()))) {
throw new UnauthorizedException('User cannot read entries on this dataset');
}
Expand Down
21 changes: 21 additions & 0 deletions packages/server/src/entry/resolvers/signlab-recording.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { NotFoundException, UseGuards } from '@nestjs/common';
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
import { Tag } from '../../tag/models/tag.model';
import { JwtAuthGuard } from '../../jwt/jwt.guard';
import { SignLabRecorded } from '../models/entry.model';
import { TagService } from '../../tag/services/tag.service';

@UseGuards(JwtAuthGuard)
@Resolver(() => SignLabRecorded)
export class SignLabRecordingResolver {
constructor(private readonly tagService: TagService) {}

@ResolveField(() => Tag)
async tag(@Parent() signlabRecorded: SignLabRecorded): Promise<Tag> {
const tag = await this.tagService.find(signlabRecorded.tag);
if (!tag) {
throw new NotFoundException(`Tag with id ${signlabRecorded.tag} not found`);
}
return tag;
}
}
4 changes: 2 additions & 2 deletions packages/server/src/entry/services/entry.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class EntryService {
}

async findForDataset(dataset: Dataset | string, page?: number, pageSize?: number): Promise<Entry[]> {
let id: string = '';
let id = '';

if (typeof dataset === 'string') {
id = dataset;
Expand All @@ -68,7 +68,7 @@ export class EntryService {
}

async countForDataset(dataset: Dataset | string) {
let id: string = '';
let id = '';

if (typeof dataset === 'string') {
id = dataset;
Expand Down
9 changes: 9 additions & 0 deletions packages/server/src/study/study.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ export class StudyConfig {
@Prop({ required: false })
@Field({ nullable: true })
disableClear?: boolean;

/**
* If set, and if the entry prosented from the user was originally
* recorded in SignTag, the user will be presented with the cue
* entry that resulted in the entry they are now labeling
*/
@Prop({ required: false })
@Field({ nullable: true })
showPriorCue?: boolean;
}

const StudyConfigSchema = SchemaFactory.createForClass(StudyConfig);
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/study/study.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class StudyResolver {
@Args('name') name: string,
@Args('project', { type: () => ID }, ProjectPipe) project: Project,
@TokenContext() user: TokenPayload
): Promise<Boolean> {
): Promise<boolean> {
if (!(await this.enforcer.enforce(user.user_id, StudyPermissions.READ, project._id.toString()))) {
throw new UnauthorizedException('User cannot read studies on this project');
}
Expand Down
6 changes: 3 additions & 3 deletions packages/server/src/tag/resolvers/tag.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export class TagResolver {
@Args('study', { type: () => ID }, StudyPipe) study: Study,
@Args('entry', { type: () => ID }, EntryPipe) entry: Entry,
@TokenContext() user: TokenPayload
): Promise<Boolean> {
): Promise<boolean> {
if (!(await this.enforcer.enforce(user.user_id, TagPermissions.READ, study._id.toString()))) {
throw new UnauthorizedException('User cannot read tags in this study');
}
Expand All @@ -125,7 +125,7 @@ export class TagResolver {
async countTagForStudy(
@Args('study', { type: () => ID }, StudyPipe) study: Study,
@TokenContext() user: TokenPayload
): Promise<Number> {
): Promise<number> {
if (!(await this.enforcer.enforce(user.user_id, TagPermissions.READ, study._id.toString()))) {
throw new UnauthorizedException('User cannot read tags in this study');
}
Expand Down Expand Up @@ -153,7 +153,7 @@ export class TagResolver {
@Args('study', { type: () => ID }, StudyPipe) study: Study,
@Args('user') user: string,
@TokenContext() requestingUser: TokenPayload
): Promise<Number> {
): Promise<number> {
if (!(await this.enforcer.enforce(requestingUser.user_id, TagPermissions.READ, study._id.toString()))) {
throw new UnauthorizedException('User cannot read tags in this study');
}
Expand Down
4 changes: 2 additions & 2 deletions packages/server/src/tag/tag.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TagService } from './services/tag.service';
import { TagResolver } from './resolvers/tag.resolver';
import { MongooseModule } from '@nestjs/mongoose';
Expand Down Expand Up @@ -40,7 +40,7 @@ import { VideoFieldResolver } from './resolvers/video-field.resolver';
{ name: VideoField.name, schema: VideoFieldSchema }
]),
StudyModule,
EntryModule,
forwardRef(() => EntryModule),
SharedModule,
PermissionModule,
GcpModule,
Expand Down
Loading
Loading