From 3f10c99e0e9865d29274806c19d537cc875713df Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 16 Apr 2024 12:39:07 -0400 Subject: [PATCH 01/19] Create custom toolbar for holding custom exporting --- .../components/tag/view/TagGridView.component.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 0c3e4d35..42bc3372 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; import { GetGridColDefs, TagViewTest } from '../../../types/TagColumnView'; import { Entry, Study } from '../../../graphql/graphql'; -import { GridColDef, GridRenderCellParams, GridToolbar } from '@mui/x-data-grid'; +import { GridColDef, GridRenderCellParams, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarExport, GridToolbarFilterButton } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid'; import { GetTagsQuery, useRemoveTagMutation } from '../../../graphql/tag/tag'; import { freeTextTest, getTextCols } from './FreeTextGridView.component'; @@ -104,7 +104,17 @@ export const TagGridView: React.FC = ({ tags, study, refetchTa rows={tags} columns={entryColumns.concat(tagMetaColumns).concat(dataColunms).concat(tagRedoColumns)} getRowId={(row) => row._id} - slots={{ toolbar: GridToolbar }} + slots={{ toolbar: TagToolbar }} /> ); }; + +const TagToolbar: React.FC = () => { + return ( + + + + + + ) +}; From 419ad9a0eab4410d840fc6e7420e1b75e186c7a7 Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 16 Apr 2024 15:46:39 -0400 Subject: [PATCH 02/19] Leverage value getter for getting reasonable values for CSV --- .../client/public/locales/en/translation.json | 3 ++- .../tag/view/AslLexGridView.component.tsx | 18 +++++++++--------- .../tag/view/BooleanGridView.component.tsx | 4 ++-- .../tag/view/FreeTextGridView.component.tsx | 7 ++----- .../tag/view/NumericGridView.component.tsx | 6 +++--- .../tag/view/SliderGridView.component.tsx | 6 +++--- .../tag/view/TagGridView.component.tsx | 11 +++++++++++ .../tag/view/VideoGridView.component.tsx | 6 +++--- 8 files changed, 35 insertions(+), 26 deletions(-) diff --git a/packages/client/public/locales/en/translation.json b/packages/client/public/locales/en/translation.json index 377d922a..f7831145 100644 --- a/packages/client/public/locales/en/translation.json +++ b/packages/client/public/locales/en/translation.json @@ -129,7 +129,8 @@ "redirectToOrg": "Redirect to Organization Login" }, "tagView": { - "originalEntry": "Original Entry" + "originalEntry": "Original Entry", + "export": "Export" } }, "errors": { diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index fae73e6a..9461aa65 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -70,28 +70,28 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { field: `${property}-video`, headerName: `${property}: ${i18next.t('common.video')}`, width: 300, + valueGetter: (params) => params.row.data && params.row.data[property], renderCell: (params) => - params.row.data && - params.row.data[property] && ( - + params.value && ( + ) }, { field: `${property}-key`, headerName: `${property}: ${i18next.t('common.key')}`, + valueGetter: (params) => params.row.data && params.row.data[property], renderCell: (params) => - params.row.data && - params.row.data[property] && ( - + params.value && ( + ) }, { field: `${property}-primary`, headerName: `${property}: ${i18next.t('common.primary')}`, + valueGetter: (params) => params.row.data && params.row.data[property], renderCell: (params) => - params.row.data && - params.row.data[property] && ( - + params.value && ( + ) } ]; diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx index 74a3b834..ed874696 100644 --- a/packages/client/src/components/tag/view/BooleanGridView.component.tsx +++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx @@ -19,9 +19,9 @@ export const getBoolCols: GetGridColDefs = (uischema, schema, property) => { { field: property, headerName: property, + valueGetter: (params) => params.row.data && params.row.data[property], renderCell: (params) => - params.row.data && - params.row.data[property] && ( + params.value && ( ) } diff --git a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx index 509441c1..1a574de5 100644 --- a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx +++ b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx @@ -18,11 +18,8 @@ export const getTextCols: GetGridColDefs = (uischema, schema, property) => { { field: property, headerName: property, - renderCell: (params) => - params.row.data && - params.row.data[property] && ( - - ) + valueGetter: (params) => params.row.data && params.row.data[property], + renderCell: (params) => params.value && } ]; }; diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx index 2aa8b549..77afd95c 100644 --- a/packages/client/src/components/tag/view/NumericGridView.component.tsx +++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx @@ -18,10 +18,10 @@ export const getNumericCols: GetGridColDefs = (uischema, schema, property) => { { field: property, headerName: property, + valueGetter: (params) => params.row.data && params.row.data[property], renderCell: (params) => - params.row.data && - params.row.data[property] && ( - + params.value && ( + ) } ]; diff --git a/packages/client/src/components/tag/view/SliderGridView.component.tsx b/packages/client/src/components/tag/view/SliderGridView.component.tsx index 8b125950..0b8b9b98 100644 --- a/packages/client/src/components/tag/view/SliderGridView.component.tsx +++ b/packages/client/src/components/tag/view/SliderGridView.component.tsx @@ -18,10 +18,10 @@ export const getSliderCols: GetGridColDefs = (uischema, schema, property) => { { field: property, headerName: property, + valueGetter: (params) => params.row.data && params.row.data[property], renderCell: (params) => - params.row.data && - params.row.data[property] && ( - + params.value && ( + ) } ]; diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 42bc3372..43def770 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -12,6 +12,7 @@ import { getSliderCols, sliderTest } from './SliderGridView.component'; import { getBoolCols, booleanTest } from './BooleanGridView.component'; import { aslLexTest, getAslLexCols } from './AslLexGridView.component'; import { getVideoCols, videoViewTest } from './VideoGridView.component'; +import { Download } from '@mui/icons-material'; export interface TagGridViewProps { study: Study; @@ -118,3 +119,13 @@ const TagToolbar: React.FC = () => { ) }; + +/* +const CustomExport: React.FC = () => { + const { t } = useTranslation(); + + return ( + + ) +}; +*/ diff --git a/packages/client/src/components/tag/view/VideoGridView.component.tsx b/packages/client/src/components/tag/view/VideoGridView.component.tsx index 314b5c65..34373c36 100644 --- a/packages/client/src/components/tag/view/VideoGridView.component.tsx +++ b/packages/client/src/components/tag/view/VideoGridView.component.tsx @@ -54,10 +54,10 @@ export const getVideoCols: GetGridColDefs = (uischema, schema, property) => { field: `${property}-video-${i + 1}`, headerName: `${property}: ${i18next.t('common.video')} ${i + 1}`, width: 350, + valueGetter: (params) => params.row.data && params.row.data[property][i], renderCell: (params) => - params.row.data && - params.row.data[property] && ( - + params.value && ( + ) }); } From 1fb8388a49e80c7f499d10e8574e6649d571216e Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 17 Apr 2024 10:56:33 -0400 Subject: [PATCH 03/19] Begin work on creating field types --- .../src/tag/models/asl-lex-field.model.ts | 0 .../tag/models/autocomplete-field.model.ts | 0 .../src/tag/models/boolean-field.model.ts | 7 +++ .../src/tag/models/embedded-field.model.ts | 0 .../src/tag/models/free-text-field.model.ts | 7 +++ .../src/tag/models/numeric-field.model.ts | 7 +++ .../src/tag/models/slider-field.model.ts | 7 +++ .../server/src/tag/models/tag-field.model.ts | 48 +++++++++++++++++++ .../src/tag/models/video-field.model.ts | 4 +- 9 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/tag/models/asl-lex-field.model.ts create mode 100644 packages/server/src/tag/models/autocomplete-field.model.ts create mode 100644 packages/server/src/tag/models/boolean-field.model.ts create mode 100644 packages/server/src/tag/models/embedded-field.model.ts create mode 100644 packages/server/src/tag/models/free-text-field.model.ts create mode 100644 packages/server/src/tag/models/numeric-field.model.ts create mode 100644 packages/server/src/tag/models/slider-field.model.ts create mode 100644 packages/server/src/tag/models/tag-field.model.ts diff --git a/packages/server/src/tag/models/asl-lex-field.model.ts b/packages/server/src/tag/models/asl-lex-field.model.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/src/tag/models/autocomplete-field.model.ts b/packages/server/src/tag/models/autocomplete-field.model.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/src/tag/models/boolean-field.model.ts b/packages/server/src/tag/models/boolean-field.model.ts new file mode 100644 index 00000000..e4f94c7e --- /dev/null +++ b/packages/server/src/tag/models/boolean-field.model.ts @@ -0,0 +1,7 @@ +import { ObjectType, Field } from '@nestjs/graphql'; + +@ObjectType() +export class BooleanField { + @Field() + value: boolean; +} diff --git a/packages/server/src/tag/models/embedded-field.model.ts b/packages/server/src/tag/models/embedded-field.model.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/src/tag/models/free-text-field.model.ts b/packages/server/src/tag/models/free-text-field.model.ts new file mode 100644 index 00000000..a73693a9 --- /dev/null +++ b/packages/server/src/tag/models/free-text-field.model.ts @@ -0,0 +1,7 @@ +import { ObjectType, Field } from '@nestjs/graphql'; + +@ObjectType() +export class FreeTextField { + @Field() + value: string; +} diff --git a/packages/server/src/tag/models/numeric-field.model.ts b/packages/server/src/tag/models/numeric-field.model.ts new file mode 100644 index 00000000..bb33d101 --- /dev/null +++ b/packages/server/src/tag/models/numeric-field.model.ts @@ -0,0 +1,7 @@ +import { ObjectType, Field } from '@nestjs/graphql'; + +@ObjectType() +export class NumericField { + @Field() + value: number; +} diff --git a/packages/server/src/tag/models/slider-field.model.ts b/packages/server/src/tag/models/slider-field.model.ts new file mode 100644 index 00000000..f799503e --- /dev/null +++ b/packages/server/src/tag/models/slider-field.model.ts @@ -0,0 +1,7 @@ +import { ObjectType, Field } from '@nestjs/graphql'; + +@ObjectType() +export class SliderField { + @Field() + value: number; +} diff --git a/packages/server/src/tag/models/tag-field.model.ts b/packages/server/src/tag/models/tag-field.model.ts new file mode 100644 index 00000000..cda48306 --- /dev/null +++ b/packages/server/src/tag/models/tag-field.model.ts @@ -0,0 +1,48 @@ +import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; +import { createUnionType, Field, ObjectType, registerEnumType } from '@nestjs/graphql'; +import mongoose from 'mongoose'; +import { VideoField } from './video-field.model'; +import { FreeTextField } from './free-text-field.model'; +import { BooleanField } from './boolean-field.model'; +import { NumericField } from './numeric-field.model'; +import { SliderField } from './slider-field.model'; + +export enum TagFieldType { + ASL_LEX = 'ASL_LEX', + AUTOCOMPLETE = 'AUTOCOMPLETE', + BOOLEAN = 'BOOLEAN', + EMBEDDED = 'EMBEDDED', + FREE_TEXT = 'FREE_TEXT', + NUMERIC = 'NUMERIC', + SLIDER = 'SLIDER', + VIDEO_RECORD = 'VIDEO_RECORD' +} + +registerEnumType(TagFieldType, { + name: 'TagFieldType' +}); + +export const TagFieldUnion = createUnionType({ + name: 'TagFieldUnion', + types: () => [BooleanField, FreeTextField, NumericField, SliderField, VideoField] as const +}); + +@Schema() +@ObjectType() +export class TagField { + /** + * Used to determine what kind of field this tag field represents + */ + @Prop({ required: true, enum: TagFieldType }) + @Field(() => TagFieldType) + type: TagFieldType; + + /** + * Holds the data itself, this can be an ID referencing a more complex + * object or be the value itself + */ + @Prop({ required: true, type: mongoose.Schema.Types.Mixed }) + data: any; +} + +export const TagFieldSchema = SchemaFactory.createForClass(TagField); diff --git a/packages/server/src/tag/models/video-field.model.ts b/packages/server/src/tag/models/video-field.model.ts index 8fb1f084..e471b1c9 100644 --- a/packages/server/src/tag/models/video-field.model.ts +++ b/packages/server/src/tag/models/video-field.model.ts @@ -2,13 +2,13 @@ import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Field, ObjectType } from '@nestjs/graphql'; import { Document } from 'mongoose'; -@Schema() -@ObjectType() /** * Represents a single video field in a study. This is used for temporarily * storing the recording video data before the tag is submitted and the video * is turned into an Entry. */ +@Schema() +@ObjectType() export class VideoField { @Field() _id: string; From a056f68a7ef48f641f48467548989d2932889096 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 17 Apr 2024 11:42:53 -0400 Subject: [PATCH 04/19] Create transformers to handle converting from JSON schema to tag fields --- .../server/src/tag/models/tag-field.model.ts | 7 ++++- packages/server/src/tag/models/tag.model.ts | 10 +++--- .../tag/services/tag-transformer.service.ts | 19 +++++++----- packages/server/src/tag/tag.module.ts | 10 +++++- .../tag/transformers/asl-lex-transformer.ts | 0 .../transformers/autocomplete-transformer.ts | 0 .../tag/transformers/boolean-transformer.ts | 31 +++++++++++++++++++ .../tag/transformers/embedded-transformer.ts | 0 .../transformers/field-transformer-factory.ts | 16 +++++++++- .../src/tag/transformers/field-transformer.ts | 3 +- .../tag/transformers/free-text.transformer.ts | 31 +++++++++++++++++++ .../tag/transformers/numeric-transformer.ts | 31 +++++++++++++++++++ .../tag/transformers/slider-transformer.ts | 31 +++++++++++++++++++ .../transformers/video-field-transformer.ts | 12 +++++-- 14 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 packages/server/src/tag/transformers/asl-lex-transformer.ts create mode 100644 packages/server/src/tag/transformers/autocomplete-transformer.ts create mode 100644 packages/server/src/tag/transformers/boolean-transformer.ts create mode 100644 packages/server/src/tag/transformers/embedded-transformer.ts create mode 100644 packages/server/src/tag/transformers/free-text.transformer.ts create mode 100644 packages/server/src/tag/transformers/numeric-transformer.ts create mode 100644 packages/server/src/tag/transformers/slider-transformer.ts diff --git a/packages/server/src/tag/models/tag-field.model.ts b/packages/server/src/tag/models/tag-field.model.ts index cda48306..cbaf9c1f 100644 --- a/packages/server/src/tag/models/tag-field.model.ts +++ b/packages/server/src/tag/models/tag-field.model.ts @@ -37,9 +37,14 @@ export class TagField { @Field(() => TagFieldType) type: TagFieldType; + @Prop({ required: true }) + @Field() + name: string; + /** * Holds the data itself, this can be an ID referencing a more complex - * object or be the value itself + * object or be the value itself. A factory method exists for converting + * the data into the field the user is querying for */ @Prop({ required: true, type: mongoose.Schema.Types.Mixed }) data: any; diff --git a/packages/server/src/tag/models/tag.model.ts b/packages/server/src/tag/models/tag.model.ts index 78408a0c..f800e310 100644 --- a/packages/server/src/tag/models/tag.model.ts +++ b/packages/server/src/tag/models/tag.model.ts @@ -1,9 +1,9 @@ import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { Field, ObjectType } from '@nestjs/graphql'; -import JSON from 'graphql-type-json'; -import mongoose, { Document } from 'mongoose'; +import { Document } from 'mongoose'; import { Study } from '../../study/study.model'; import { Entry } from '../../entry/models/entry.model'; +import { TagFieldSchema, TagField } from './tag-field.model'; @Schema() @ObjectType() @@ -27,10 +27,10 @@ export class Tag { @Field({ nullable: true, description: 'The user assigned to the tag ' }) user?: string; - @Prop({ requried: false, type: mongoose.Schema.Types.Mixed }) - @Field(() => JSON, { + @Prop({ requried: false, type: [TagFieldSchema] }) + @Field(() => [TagField], { nullable: true, - description: 'The data stored in the tag, not populated until a colaborator has tagged' + description: 'The data stored in the tag, not populated until a contributor has tagged' }) data?: any; diff --git a/packages/server/src/tag/services/tag-transformer.service.ts b/packages/server/src/tag/services/tag-transformer.service.ts index eb9a0162..1beb667c 100644 --- a/packages/server/src/tag/services/tag-transformer.service.ts +++ b/packages/server/src/tag/services/tag-transformer.service.ts @@ -3,6 +3,7 @@ import { Study } from '../../study/study.model'; import { FieldTransformerFactory } from '../transformers/field-transformer-factory'; import { TokenPayload } from '../../jwt/token.dto'; import { Tag } from '../../tag/models/tag.model'; +import { TagField } from '../models/tag-field.model'; @Injectable() export class TagTransformer { @@ -12,9 +13,7 @@ export class TagTransformer { * Transforms the tag data. Takes in the whole tag and produces the modified * tag data. */ - async transformTagData(tag: Tag, data: any, study: Study, user: TokenPayload): Promise { - const transformedData: { [property: string]: any } = {}; - + async transformTagData(tag: Tag, data: any, study: Study, user: TokenPayload): Promise { const schema = study.tagSchema.dataSchema; const uischema = study.tagSchema.uiSchema; @@ -22,6 +21,8 @@ export class TagTransformer { return data; } + const fields: TagField[] = []; + for (const field in schema.properties) { // Get the schema and ui schema for the field const fieldSchema = schema.properties[field]; @@ -35,13 +36,15 @@ export class TagTransformer { // Try to get the transformer for the field const transformer = this.fieldTransformerFactory.getTransformer(fieldUiSchema, fieldSchema); + if (!transformer) { + throw new Error(`Unsupported field type for field ${field}`); + } + // Apply the transformation if present, otherwise just return the data - const transformed = transformer - ? await transformer.transformField(tag, data[field], fieldUiSchema, fieldSchema, user) - : data[field]; - transformedData[field] = transformed; + const transformed = await transformer.transformField(tag, data[field], fieldUiSchema, fieldSchema, user, field); + fields.push(transformed); } - return transformedData; + return fields; } } diff --git a/packages/server/src/tag/tag.module.ts b/packages/server/src/tag/tag.module.ts index 09b06125..71da9c8e 100644 --- a/packages/server/src/tag/tag.module.ts +++ b/packages/server/src/tag/tag.module.ts @@ -20,6 +20,10 @@ import { TrainingSet, TrainingSetSchema } from './models/training-set'; import { TrainingSetResolver } from './resolvers/training-set.resolver'; import { TrainingSetService } from './services/training-set.service'; import { BucketModule } from 'src/bucket/bucket.module'; +import { BooleanFieldTransformer } from './transformers/boolean-transformer'; +import { FreeTextFieldTransformer } from './transformers/free-text.transformer'; +import { NumericFieldTransformer } from './transformers/numeric-transformer'; +import { SliderFieldTransformer } from './transformers/slider-transformer'; @Module({ imports: [ @@ -46,7 +50,11 @@ import { BucketModule } from 'src/bucket/bucket.module'; FieldTransformerFactory, VideoFieldTransformer, TrainingSetResolver, - TrainingSetService + TrainingSetService, + BooleanFieldTransformer, + FreeTextFieldTransformer, + NumericFieldTransformer, + SliderFieldTransformer ] }) export class TagModule {} diff --git a/packages/server/src/tag/transformers/asl-lex-transformer.ts b/packages/server/src/tag/transformers/asl-lex-transformer.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/src/tag/transformers/autocomplete-transformer.ts b/packages/server/src/tag/transformers/autocomplete-transformer.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/src/tag/transformers/boolean-transformer.ts b/packages/server/src/tag/transformers/boolean-transformer.ts new file mode 100644 index 00000000..dee60113 --- /dev/null +++ b/packages/server/src/tag/transformers/boolean-transformer.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { FieldTransformer } from './field-transformer'; +import { JsonSchema, UISchemaElement, isBooleanControl } from '@jsonforms/core'; +import { TokenPayload } from '../../jwt/token.dto'; +import { Tag } from '../models/tag.model'; +import { TagField, TagFieldType } from '../models/tag-field.model'; + +@Injectable() +export class BooleanFieldTransformer implements FieldTransformer { + async transformField( + _tag: Tag, + data: string, + _uischema: UISchemaElement, + _schema: JsonSchema, + _user: TokenPayload, + property: string + ): Promise { + return { + name: property, + data, + type: TagFieldType.BOOLEAN + } + } +} + +export const BooleanFieldTransformerTest = (uischema: UISchemaElement, schema: JsonSchema) => { + if (isBooleanControl(uischema, schema, {} as any)) { + return 10; + } + return -1; +}; diff --git a/packages/server/src/tag/transformers/embedded-transformer.ts b/packages/server/src/tag/transformers/embedded-transformer.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/server/src/tag/transformers/field-transformer-factory.ts b/packages/server/src/tag/transformers/field-transformer-factory.ts index 77d6b8cc..44ff6cd5 100644 --- a/packages/server/src/tag/transformers/field-transformer-factory.ts +++ b/packages/server/src/tag/transformers/field-transformer-factory.ts @@ -1,17 +1,31 @@ import { JsonSchema, UISchemaElement } from '@jsonforms/core'; import { Injectable } from '@nestjs/common'; +import { BooleanFieldTransformer, BooleanFieldTransformerTest } from './boolean-transformer'; import { FieldTransformer, FieldTransformerTest, NOT_APPLICABLE } from './field-transformer'; import { VideoFieldTransformer, VideoFieldTransformerTest } from './video-field-transformer'; +import { NumericFieldTransformerTest, NumericFieldTransformer } from './numeric-transformer'; +import { FreeTextFieldTransformer, FreeTextFieldTransformerTest } from './free-text.transformer'; +import { SliderFieldTransformerTest, SliderFieldTransformer } from './slider-transformer'; type FieldTransformerOptions = { tester: FieldTransformerTest; transformer: FieldTransformer }; @Injectable() export class FieldTransformerFactory { private readonly transformers: FieldTransformerOptions[] = [ + { tester: BooleanFieldTransformerTest, transformer: this.booleanFieldTransformer }, + { tester: FreeTextFieldTransformerTest, transformer: this.freeTextFieldTransformer }, + { tester: NumericFieldTransformerTest, transformer: this.numericFieldTransformer }, + { tester: SliderFieldTransformerTest, transformer: this.sliderFieldTransformer }, { tester: VideoFieldTransformerTest, transformer: this.videoFieldTransformer } ]; - constructor(private readonly videoFieldTransformer: VideoFieldTransformer) {} + constructor( + private readonly booleanFieldTransformer: BooleanFieldTransformer, + private readonly freeTextFieldTransformer: FreeTextFieldTransformer, + private readonly numericFieldTransformer: NumericFieldTransformer, + private readonly sliderFieldTransformer: SliderFieldTransformer, + private readonly videoFieldTransformer: VideoFieldTransformer + ) {} /** Get the transformer for the given field */ getTransformer(uischema: UISchemaElement, schema: JsonSchema): FieldTransformer | null { diff --git a/packages/server/src/tag/transformers/field-transformer.ts b/packages/server/src/tag/transformers/field-transformer.ts index 129db94e..a66068ab 100644 --- a/packages/server/src/tag/transformers/field-transformer.ts +++ b/packages/server/src/tag/transformers/field-transformer.ts @@ -1,5 +1,6 @@ import { JsonSchema, UISchemaElement } from '@jsonforms/core'; import { TokenPayload } from '../../jwt/token.dto'; +import { TagField } from '../models/tag-field.model'; import { Tag } from '../models/tag.model'; /** @@ -8,7 +9,7 @@ import { Tag } from '../models/tag.model'; * and ensuring that the data meets any additional formatting requirements. */ export interface FieldTransformer { - transformField(tag: Tag, data: any, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload): Promise; + transformField(tag: Tag, data: any, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload, property: string): Promise; } /** diff --git a/packages/server/src/tag/transformers/free-text.transformer.ts b/packages/server/src/tag/transformers/free-text.transformer.ts new file mode 100644 index 00000000..32f1aa9f --- /dev/null +++ b/packages/server/src/tag/transformers/free-text.transformer.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { FieldTransformer } from './field-transformer'; +import { JsonSchema, UISchemaElement, isStringControl } from '@jsonforms/core'; +import { TokenPayload } from '../../jwt/token.dto'; +import { Tag } from '../models/tag.model'; +import { TagField, TagFieldType } from '../models/tag-field.model'; + +@Injectable() +export class FreeTextFieldTransformer implements FieldTransformer { + async transformField( + _tag: Tag, + data: string, + _uischema: UISchemaElement, + _schema: JsonSchema, + _user: TokenPayload, + property: string + ): Promise { + return { + name: property, + data, + type: TagFieldType.FREE_TEXT + } + } +} + +export const FreeTextFieldTransformerTest = (uischema: UISchemaElement, schema: JsonSchema) => { + if (isStringControl(uischema, schema, {} as any)) { + return 10; + } + return -1; +}; diff --git a/packages/server/src/tag/transformers/numeric-transformer.ts b/packages/server/src/tag/transformers/numeric-transformer.ts new file mode 100644 index 00000000..897879af --- /dev/null +++ b/packages/server/src/tag/transformers/numeric-transformer.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { FieldTransformer } from './field-transformer'; +import { JsonSchema, UISchemaElement, isNumberControl } from '@jsonforms/core'; +import { TokenPayload } from '../../jwt/token.dto'; +import { Tag } from '../models/tag.model'; +import { TagField, TagFieldType } from '../models/tag-field.model'; + +@Injectable() +export class NumericFieldTransformer implements FieldTransformer { + async transformField( + _tag: Tag, + data: number, + _uischema: UISchemaElement, + _schema: JsonSchema, + _user: TokenPayload, + property: string + ): Promise { + return { + name: property, + data, + type: TagFieldType.NUMERIC + } + } +} + +export const NumericFieldTransformerTest = (uischema: UISchemaElement, schema: JsonSchema) => { + if (isNumberControl(uischema, schema, {} as any)) { + return 10; + } + return -1; +}; diff --git a/packages/server/src/tag/transformers/slider-transformer.ts b/packages/server/src/tag/transformers/slider-transformer.ts new file mode 100644 index 00000000..c119891b --- /dev/null +++ b/packages/server/src/tag/transformers/slider-transformer.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { FieldTransformer } from './field-transformer'; +import { JsonSchema, UISchemaElement, isNumberControl } from '@jsonforms/core'; +import { TokenPayload } from '../../jwt/token.dto'; +import { Tag } from '../models/tag.model'; +import { TagField, TagFieldType } from '../models/tag-field.model'; + +@Injectable() +export class SliderFieldTransformer implements FieldTransformer { + async transformField( + _tag: Tag, + data: number, + _uischema: UISchemaElement, + _schema: JsonSchema, + _user: TokenPayload, + property: string + ): Promise { + return { + name: property, + data, + type: TagFieldType.SLIDER + } + } +} + +export const SliderFieldTransformerTest = (uischema: UISchemaElement, schema: JsonSchema) => { + if (isNumberControl(uischema, schema, {} as any)) { + return 10; + } + return -1; +}; diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts index a130f996..c656c2ee 100644 --- a/packages/server/src/tag/transformers/video-field-transformer.ts +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -4,6 +4,7 @@ import { JsonSchema, UISchemaElement } from '@jsonforms/core'; import { VideoFieldService } from '../services/video-field.service'; import { TokenPayload } from '../../jwt/token.dto'; import { Tag } from '../models/tag.model'; +import { TagField, TagFieldType } from '../models/tag-field.model'; @Injectable() export class VideoFieldTransformer implements FieldTransformer { @@ -14,8 +15,9 @@ export class VideoFieldTransformer implements FieldTransformer { data: string[], uischema: UISchemaElement, _schema: JsonSchema, - user: TokenPayload - ): Promise { + user: TokenPayload, + property: string + ): Promise { const datasetID = uischema.options?.dataset; if (!datasetID) { throw new BadRequestException('Dataset ID not provided'); @@ -28,7 +30,11 @@ export class VideoFieldTransformer implements FieldTransformer { }) ); - return videoFields; + return { + name: property, + data: JSON.stringify(videoFields), + type: TagFieldType.VIDEO_RECORD + } } } From d976ae1f2b3633304fa760a1e29847662f52dfd4 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 17 Apr 2024 13:22:15 -0400 Subject: [PATCH 05/19] Starting work on resolving tag fields --- packages/client/src/graphql/tag/tag.graphql | 23 ++++++++++++++- .../server/src/tag/models/tag-field.model.ts | 2 ++ packages/server/src/tag/models/tag.model.ts | 2 +- .../src/tag/resolvers/tag-field.resolver.ts | 28 +++++++++++++++++++ packages/server/src/tag/tag.module.ts | 2 ++ 5 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 packages/server/src/tag/resolvers/tag-field.resolver.ts diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index 37603904..a894fc97 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -65,7 +65,28 @@ query getTags($study: ID!) { signedUrlExpiration isTraining } - data + data { + type + name + field { + + ... on BooleanField { + value + } + + ... on FreeTextField { + value + } + + ... on NumericField { + value + } + + ... on SliderField { + value + } + } + } complete } } diff --git a/packages/server/src/tag/models/tag-field.model.ts b/packages/server/src/tag/models/tag-field.model.ts index cbaf9c1f..7e977607 100644 --- a/packages/server/src/tag/models/tag-field.model.ts +++ b/packages/server/src/tag/models/tag-field.model.ts @@ -48,6 +48,8 @@ export class TagField { */ @Prop({ required: true, type: mongoose.Schema.Types.Mixed }) data: any; + + /* Not shown is a resolve field for the representation of the data */ } export const TagFieldSchema = SchemaFactory.createForClass(TagField); diff --git a/packages/server/src/tag/models/tag.model.ts b/packages/server/src/tag/models/tag.model.ts index f800e310..738642e5 100644 --- a/packages/server/src/tag/models/tag.model.ts +++ b/packages/server/src/tag/models/tag.model.ts @@ -32,7 +32,7 @@ export class Tag { nullable: true, description: 'The data stored in the tag, not populated until a contributor has tagged' }) - data?: any; + data?: TagField[]; @Prop() @Field({ description: 'Way to rank tags based on order to be tagged' }) diff --git a/packages/server/src/tag/resolvers/tag-field.resolver.ts b/packages/server/src/tag/resolvers/tag-field.resolver.ts new file mode 100644 index 00000000..ad1a6165 --- /dev/null +++ b/packages/server/src/tag/resolvers/tag-field.resolver.ts @@ -0,0 +1,28 @@ +import { TagField, TagFieldType, TagFieldUnion } from '../models/tag-field.model'; +import { ResolveField, Parent, Resolver } from '@nestjs/graphql'; +import { UseGuards } from '@nestjs/common'; +import { JwtAuthGuard } from '../../jwt/jwt.guard'; +import { BooleanField } from '../models/boolean-field.model'; +import { FreeTextField } from '../models/free-text-field.model'; +import { NumericField } from '../models/numeric-field.model'; +import { SliderField } from '../models/slider-field.model'; + +@UseGuards(JwtAuthGuard) +@Resolver(() => TagField) +export class TagFieldResolver { + @ResolveField(() => TagFieldUnion) + async field(@Parent() tagField: TagField): Promise { + switch(tagField.type) { + case TagFieldType.BOOLEAN: + return { value: tagField.data } as BooleanField; + case TagFieldType.FREE_TEXT: + return { value: tagField.data } as FreeTextField; + case TagFieldType.NUMERIC: + return { value: tagField.data } as NumericField; + case TagFieldType.SLIDER: + return { value: tagField.data } as SliderField; + default: + throw new Error(`Unsupported tag field type: ${tagField.type}`); + } + } +} diff --git a/packages/server/src/tag/tag.module.ts b/packages/server/src/tag/tag.module.ts index 71da9c8e..89da721b 100644 --- a/packages/server/src/tag/tag.module.ts +++ b/packages/server/src/tag/tag.module.ts @@ -24,6 +24,7 @@ import { BooleanFieldTransformer } from './transformers/boolean-transformer'; import { FreeTextFieldTransformer } from './transformers/free-text.transformer'; import { NumericFieldTransformer } from './transformers/numeric-transformer'; import { SliderFieldTransformer } from './transformers/slider-transformer'; +import { TagFieldResolver } from './resolvers/tag-field.resolver'; @Module({ imports: [ @@ -43,6 +44,7 @@ import { SliderFieldTransformer } from './transformers/slider-transformer'; providers: [ TagService, TagResolver, + TagFieldResolver, TagPipe, VideoFieldService, VideoFieldResolver, From 9eeebfd7c05626f9b9de9da973451f388ef0bdd2 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 17 Apr 2024 13:30:05 -0400 Subject: [PATCH 06/19] Add in resolver for querying tags --- packages/client/src/graphql/graphql.ts | 44 ++++++++++++++++++++- packages/client/src/graphql/tag/tag.graphql | 32 ++++++++++++--- packages/client/src/graphql/tag/tag.ts | 42 ++++++++++++++++++-- 3 files changed, 107 insertions(+), 11 deletions(-) diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 5b994fe3..ca04dc0b 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -18,6 +18,11 @@ export type Scalars = { JSON: { input: any; output: any; } }; +export type BooleanField = { + __typename?: 'BooleanField'; + value: Scalars['Boolean']['output']; +}; + export type Dataset = { __typename?: 'Dataset'; _id: Scalars['ID']['output']; @@ -52,6 +57,11 @@ export type Entry = { signedUrlExpiration: Scalars['Float']['output']; }; +export type FreeTextField = { + __typename?: 'FreeTextField'; + value: Scalars['String']['output']; +}; + /** Represents an entier lexicon */ export type Lexicon = { __typename?: 'Lexicon'; @@ -300,6 +310,11 @@ export type MutationSignLabCreateProjectArgs = { project: ProjectCreate; }; +export type NumericField = { + __typename?: 'NumericField'; + value: Scalars['Float']['output']; +}; + export type Organization = { __typename?: 'Organization'; _id: Scalars['ID']['output']; @@ -485,6 +500,11 @@ export type QueryValidateCsvArgs = { session: Scalars['ID']['input']; }; +export type SliderField = { + __typename?: 'SliderField'; + value: Scalars['Float']['output']; +}; + export type Study = { __typename?: 'Study'; _id: Scalars['ID']['output']; @@ -520,8 +540,8 @@ export type Tag = { __typename?: 'Tag'; _id: Scalars['String']['output']; complete: Scalars['Boolean']['output']; - /** The data stored in the tag, not populated until a colaborator has tagged */ - data?: Maybe; + /** The data stored in the tag, not populated until a contributor has tagged */ + data?: Maybe>; /** If the tag is enabled as part of the study, way to disable certain tags */ enabled: Scalars['Boolean']['output']; entry: Entry; @@ -534,6 +554,26 @@ export type Tag = { user?: Maybe; }; +export type TagField = { + __typename?: 'TagField'; + field: TagFieldUnion; + name: Scalars['String']['output']; + type: TagFieldType; +}; + +export enum TagFieldType { + AslLex = 'ASL_LEX', + Autocomplete = 'AUTOCOMPLETE', + Boolean = 'BOOLEAN', + Embedded = 'EMBEDDED', + FreeText = 'FREE_TEXT', + Numeric = 'NUMERIC', + Slider = 'SLIDER', + VideoRecord = 'VIDEO_RECORD' +} + +export type TagFieldUnion = BooleanField | FreeTextField | NumericField | SliderField | VideoField; + export type TagSchema = { __typename?: 'TagSchema'; dataSchema: Scalars['JSON']['output']; diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index a894fc97..f04519d2 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -71,19 +71,19 @@ query getTags($study: ID!) { field { ... on BooleanField { - value + boolValue: value } ... on FreeTextField { - value + textValue: value } ... on NumericField { - value + numericValue: value } ... on SliderField { - value + sliderValue: value } } } @@ -106,7 +106,29 @@ query getTrainingTags($study: ID!, $user: String!) { signedUrlExpiration isTraining } - data + data { + type + name + field { + + ... on BooleanField { + boolValue: value + } + + ... on FreeTextField { + textValue: value + } + + ... on NumericField { + numericValue: value + } + + ... on SliderField { + sliderValue: value + } + } + } + complete } } diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index 753448c9..40b6e8d4 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -74,7 +74,7 @@ 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, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number, isTraining: boolean } }> }; +export type GetTagsQuery = { __typename?: 'Query', getTags: 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?: 'BooleanField', boolValue: boolean } | { __typename?: 'FreeTextField', textValue: string } | { __typename?: 'NumericField', numericValue: number } | { __typename?: 'SliderField', sliderValue: number } | { __typename?: 'VideoField' } }> | null }> }; export type GetTrainingTagsQueryVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; @@ -82,7 +82,7 @@ export type GetTrainingTagsQueryVariables = Types.Exact<{ }>; -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, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number, isTraining: boolean } }> }; +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?: 'BooleanField', boolValue: boolean } | { __typename?: 'FreeTextField', textValue: string } | { __typename?: 'NumericField', numericValue: number } | { __typename?: 'SliderField', sliderValue: number } | { __typename?: 'VideoField' } }> | null }> }; export const CreateTagsDocument = gql` @@ -379,7 +379,24 @@ export const GetTagsDocument = gql` signedUrlExpiration isTraining } - data + data { + type + name + field { + ... on BooleanField { + boolValue: value + } + ... on FreeTextField { + textValue: value + } + ... on NumericField { + numericValue: value + } + ... on SliderField { + sliderValue: value + } + } + } complete } } @@ -428,7 +445,24 @@ export const GetTrainingTagsDocument = gql` signedUrlExpiration isTraining } - data + data { + type + name + field { + ... on BooleanField { + boolValue: value + } + ... on FreeTextField { + textValue: value + } + ... on NumericField { + numericValue: value + } + ... on SliderField { + sliderValue: value + } + } + } complete } } From 63c49cf243a5403f601a9357cd2b419584c21d12 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 17 Apr 2024 13:50:00 -0400 Subject: [PATCH 07/19] Working query for tag field union --- packages/client/src/graphql/tag/tag.graphql | 3 +++ packages/client/src/graphql/tag/tag.ts | 6 ++++-- .../server/src/tag/models/boolean-field.model.ts | 4 ++++ .../src/tag/models/free-text-field.model.ts | 4 ++++ .../server/src/tag/models/numeric-field.model.ts | 4 ++++ .../server/src/tag/models/slider-field.model.ts | 4 ++++ packages/server/src/tag/models/tag-field.model.ts | 2 +- .../src/tag/resolvers/tag-field.resolver.ts | 15 +++++++++------ 8 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index f04519d2..7b47920f 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -69,6 +69,8 @@ query getTags($study: ID!) { type name field { + __typename + ... on BooleanField { boolValue: value @@ -110,6 +112,7 @@ query getTrainingTags($study: ID!, $user: String!) { type name field { + __typename ... on BooleanField { boolValue: value diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index 40b6e8d4..7aa5446b 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -74,7 +74,7 @@ export type GetTagsQueryVariables = Types.Exact<{ }>; -export type GetTagsQuery = { __typename?: 'Query', getTags: 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?: 'BooleanField', boolValue: boolean } | { __typename?: 'FreeTextField', textValue: string } | { __typename?: 'NumericField', numericValue: number } | { __typename?: 'SliderField', sliderValue: number } | { __typename?: 'VideoField' } }> | null }> }; +export type GetTagsQuery = { __typename?: 'Query', getTags: 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: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } }> | null }> }; export type GetTrainingTagsQueryVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; @@ -82,7 +82,7 @@ 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?: 'BooleanField', boolValue: boolean } | { __typename?: 'FreeTextField', textValue: string } | { __typename?: 'NumericField', numericValue: number } | { __typename?: 'SliderField', sliderValue: number } | { __typename?: 'VideoField' } }> | null }> }; +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: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } }> | null }> }; export const CreateTagsDocument = gql` @@ -383,6 +383,7 @@ export const GetTagsDocument = gql` type name field { + __typename ... on BooleanField { boolValue: value } @@ -449,6 +450,7 @@ export const GetTrainingTagsDocument = gql` type name field { + __typename ... on BooleanField { boolValue: value } diff --git a/packages/server/src/tag/models/boolean-field.model.ts b/packages/server/src/tag/models/boolean-field.model.ts index e4f94c7e..4bf796a5 100644 --- a/packages/server/src/tag/models/boolean-field.model.ts +++ b/packages/server/src/tag/models/boolean-field.model.ts @@ -4,4 +4,8 @@ import { ObjectType, Field } from '@nestjs/graphql'; export class BooleanField { @Field() value: boolean; + + constructor(value: boolean) { + this.value = value; + } } diff --git a/packages/server/src/tag/models/free-text-field.model.ts b/packages/server/src/tag/models/free-text-field.model.ts index a73693a9..3abdeb02 100644 --- a/packages/server/src/tag/models/free-text-field.model.ts +++ b/packages/server/src/tag/models/free-text-field.model.ts @@ -4,4 +4,8 @@ import { ObjectType, Field } from '@nestjs/graphql'; export class FreeTextField { @Field() value: string; + + constructor(value: string) { + this.value = value; + } } diff --git a/packages/server/src/tag/models/numeric-field.model.ts b/packages/server/src/tag/models/numeric-field.model.ts index bb33d101..f3ec2c02 100644 --- a/packages/server/src/tag/models/numeric-field.model.ts +++ b/packages/server/src/tag/models/numeric-field.model.ts @@ -4,4 +4,8 @@ import { ObjectType, Field } from '@nestjs/graphql'; export class NumericField { @Field() value: number; + + constructor(value: number) { + this.value = value; + } } diff --git a/packages/server/src/tag/models/slider-field.model.ts b/packages/server/src/tag/models/slider-field.model.ts index f799503e..b2f4a80a 100644 --- a/packages/server/src/tag/models/slider-field.model.ts +++ b/packages/server/src/tag/models/slider-field.model.ts @@ -4,4 +4,8 @@ import { ObjectType, Field } from '@nestjs/graphql'; export class SliderField { @Field() value: number; + + constructor(value: number) { + this.value = value; + } } diff --git a/packages/server/src/tag/models/tag-field.model.ts b/packages/server/src/tag/models/tag-field.model.ts index 7e977607..e738012d 100644 --- a/packages/server/src/tag/models/tag-field.model.ts +++ b/packages/server/src/tag/models/tag-field.model.ts @@ -46,7 +46,7 @@ export class TagField { * object or be the value itself. A factory method exists for converting * the data into the field the user is querying for */ - @Prop({ required: true, type: mongoose.Schema.Types.Mixed }) + @Prop({ required: false, type: mongoose.Schema.Types.Mixed }) data: any; /* Not shown is a resolve field for the representation of the data */ diff --git a/packages/server/src/tag/resolvers/tag-field.resolver.ts b/packages/server/src/tag/resolvers/tag-field.resolver.ts index ad1a6165..523ad2ad 100644 --- a/packages/server/src/tag/resolvers/tag-field.resolver.ts +++ b/packages/server/src/tag/resolvers/tag-field.resolver.ts @@ -10,17 +10,20 @@ import { SliderField } from '../models/slider-field.model'; @UseGuards(JwtAuthGuard) @Resolver(() => TagField) export class TagFieldResolver { - @ResolveField(() => TagFieldUnion) - async field(@Parent() tagField: TagField): Promise { + @ResolveField(() => TagFieldUnion, { nullable: true }) + async field(@Parent() tagField: TagField): Promise { + if (!tagField.data) { + return null; + } switch(tagField.type) { case TagFieldType.BOOLEAN: - return { value: tagField.data } as BooleanField; + return new BooleanField(tagField.data); case TagFieldType.FREE_TEXT: - return { value: tagField.data } as FreeTextField; + return new FreeTextField(tagField.data); case TagFieldType.NUMERIC: - return { value: tagField.data } as NumericField; + return new NumericField(tagField.data); case TagFieldType.SLIDER: - return { value: tagField.data } as SliderField; + return new SliderField(tagField.data); default: throw new Error(`Unsupported tag field type: ${tagField.type}`); } From 92a5130dd95d65ac0d4cc58ae99631d598d9cdd4 Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 17 Apr 2024 15:17:16 -0400 Subject: [PATCH 08/19] Add in ability to convert data into grid friendly shape --- .../tag/view/TagGridView.component.tsx | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 43def770..342a0660 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -13,6 +13,7 @@ import { getBoolCols, booleanTest } from './BooleanGridView.component'; import { aslLexTest, getAslLexCols } from './AslLexGridView.component'; import { getVideoCols, videoViewTest } from './VideoGridView.component'; import { Download } from '@mui/icons-material'; +import { useEffect, useState } from 'react'; export interface TagGridViewProps { study: Study; @@ -20,9 +21,39 @@ export interface TagGridViewProps { refetchTags: () => void; } +/** + * The GridData represents how to get the tag into the grid view. The data type + * itself matches the tag query except the data field is represented as key value + * fields instead of a list of fields. + * + * So + * + * { + * data: [ + * { name: "property name a", ...fields } + * ] + * } + * + * Becomes + * + * { + * data: { + * "property name 1": { + * name: "property name 1", + * ...fields + * } + * } + * } + */ +interface GridData extends Omit { + data: { [property: string]: any } | null; +} + export const TagGridView: React.FC = ({ tags, study, refetchTags }) => { const { t } = useTranslation(); + const [gridData, setGridData] = useState<(GridData | null)[]>([]); + const tagColumnViews: { tester: TagViewTest; getGridColDefs: GetGridColDefs }[] = [ { tester: freeTextTest, getGridColDefs: getTextCols }, { tester: numericTest, getGridColDefs: getNumericCols }, @@ -32,6 +63,17 @@ export const TagGridView: React.FC = ({ tags, study, refetchTa { tester: videoViewTest, getGridColDefs: getVideoCols } ]; + useEffect(() => { + + // This logic justs pulls out the fields from an array into an object + setGridData(tags.map(tag => { + return { + ...tag, + data: tag.data ? Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties).map((property: string) => tag.data!.find(row => row.name == property)) : null + } + })) + }, [tags]); + const entryColumns: GridColDef[] = [ { field: 'entryView', @@ -102,7 +144,7 @@ export const TagGridView: React.FC = ({ tags, study, refetchTa return ( 'auto'} - rows={tags} + rows={gridData} columns={entryColumns.concat(tagMetaColumns).concat(dataColunms).concat(tagRedoColumns)} getRowId={(row) => row._id} slots={{ toolbar: TagToolbar }} From 9191104895702e983459f626be969c16540bbf1e Mon Sep 17 00:00:00 2001 From: cbolles Date: Wed, 17 Apr 2024 15:54:35 -0400 Subject: [PATCH 09/19] Rendering of basic values --- .../tag/view/BooleanGridView.component.tsx | 2 +- .../tag/view/FreeTextGridView.component.tsx | 2 +- .../tag/view/NumericGridView.component.tsx | 2 +- .../tag/view/TagGridView.component.tsx | 23 +++++++++++++++++-- packages/client/src/graphql/graphql.ts | 2 +- packages/client/src/graphql/tag/tag.ts | 4 ++-- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx index ed874696..46538d02 100644 --- a/packages/client/src/components/tag/view/BooleanGridView.component.tsx +++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx @@ -19,7 +19,7 @@ export const getBoolCols: GetGridColDefs = (uischema, schema, property) => { { field: property, headerName: property, - valueGetter: (params) => params.row.data && params.row.data[property], + valueGetter: (params) => params.row.data[property]?.field?.boolValue, renderCell: (params) => params.value && ( diff --git a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx index 1a574de5..5ade363c 100644 --- a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx +++ b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx @@ -18,7 +18,7 @@ export const getTextCols: GetGridColDefs = (uischema, schema, property) => { { field: property, headerName: property, - valueGetter: (params) => params.row.data && params.row.data[property], + valueGetter: (params) => params.row.data[property]?.field?.textValue, renderCell: (params) => params.value && } ]; diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx index 77afd95c..ff88fd27 100644 --- a/packages/client/src/components/tag/view/NumericGridView.component.tsx +++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx @@ -18,7 +18,7 @@ export const getNumericCols: GetGridColDefs = (uischema, schema, property) => { { field: property, headerName: property, - valueGetter: (params) => params.row.data && params.row.data[property], + valueGetter: (params) => params.row.data[property]?.field?.numericValue, renderCell: (params) => params.value && ( diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 342a0660..a07ed841 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -66,12 +66,31 @@ export const TagGridView: React.FC = ({ tags, study, refetchTa useEffect(() => { // This logic justs pulls out the fields from an array into an object - setGridData(tags.map(tag => { + /* + const newGridData = tags.map(tag => { return { ...tag, data: tag.data ? Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties).map((property: string) => tag.data!.find(row => row.name == property)) : null } - })) + }); + */ + + const newGridData: (GridData | null)[] = []; + + for (const tag of tags) { + const properties = {} as any; + + for (const property of Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties)) { + properties[property] = tag.data!.find(row => row.name === property); + } + + newGridData.push({ + ...tag, + data: properties, + }) + } + + setGridData(newGridData); }, [tags]); const entryColumns: GridColDef[] = [ diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index ca04dc0b..12791705 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -556,7 +556,7 @@ export type Tag = { export type TagField = { __typename?: 'TagField'; - field: TagFieldUnion; + field?: Maybe; name: Scalars['String']['output']; type: TagFieldType; }; diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index 7aa5446b..b5f5ae04 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -74,7 +74,7 @@ export type GetTagsQueryVariables = Types.Exact<{ }>; -export type GetTagsQuery = { __typename?: 'Query', getTags: 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: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } }> | null }> }; +export type GetTagsQuery = { __typename?: 'Query', getTags: 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: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } | null }> | null }> }; export type GetTrainingTagsQueryVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; @@ -82,7 +82,7 @@ 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: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } }> | null }> }; +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: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } | null }> | null }> }; export const CreateTagsDocument = gql` From d1b1646b1c96ad1bf53ae6d97d432c932e8f4de7 Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 18 Apr 2024 10:25:58 -0400 Subject: [PATCH 10/19] Add concept between intermediate and full video field --- .../tag/view/TagGridView.component.tsx | 14 +---- .../src/tag/models/video-field-inter.model.ts | 37 +++++++++++++ .../src/tag/models/video-field.model.ts | 39 +++----------- .../src/tag/resolvers/tag-field.resolver.ts | 23 ++------ ...olver.ts => video-field-inter.resolver.ts} | 16 +++--- .../src/tag/services/tag-field.service.ts | 52 +++++++++++++++++++ ...ervice.ts => video-field-inter.service.ts} | 12 ++--- packages/server/src/tag/tag.module.ts | 16 +++--- .../transformers/video-field-transformer.ts | 4 +- 9 files changed, 127 insertions(+), 86 deletions(-) create mode 100644 packages/server/src/tag/models/video-field-inter.model.ts rename packages/server/src/tag/resolvers/{video-field.resolver.ts => video-field-inter.resolver.ts} (79%) create mode 100644 packages/server/src/tag/services/tag-field.service.ts rename packages/server/src/tag/services/{video-field.service.ts => video-field-inter.service.ts} (92%) diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index a07ed841..4572552b 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -64,19 +64,9 @@ export const TagGridView: React.FC = ({ tags, study, refetchTa ]; useEffect(() => { - - // This logic justs pulls out the fields from an array into an object - /* - const newGridData = tags.map(tag => { - return { - ...tag, - data: tag.data ? Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties).map((property: string) => tag.data!.find(row => row.name == property)) : null - } - }); - */ - const newGridData: (GridData | null)[] = []; + // This logic justs pulls out the fields from an array into an object for (const tag of tags) { const properties = {} as any; @@ -97,7 +87,7 @@ export const TagGridView: React.FC = ({ tags, study, refetchTa { field: 'entryView', headerName: t('components.tagView.originalEntry'), - width: 300, + width: 350, renderCell: (params: GridRenderCellParams) => } ]; diff --git a/packages/server/src/tag/models/video-field-inter.model.ts b/packages/server/src/tag/models/video-field-inter.model.ts new file mode 100644 index 00000000..6df544c0 --- /dev/null +++ b/packages/server/src/tag/models/video-field-inter.model.ts @@ -0,0 +1,37 @@ +import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; +import { Field, ObjectType } from '@nestjs/graphql'; +import { Document } from 'mongoose'; + +/** + * Represents a single video field in a study. This is used for temporarily + * storing the recording video data before the tag is submitted and the video + * is turned into an Entry. + */ +@Schema() +@ObjectType() +export class VideoFieldIntermediate { + @Field() + _id: string; + + /** The tag the video field is a part of */ + @Prop() + tag: string; + + /** The field of the tag the video field is a part of */ + @Prop() + field: string; + + /** The index of the video field in the tag */ + @Prop() + index: number; + + /** Where within the bucket the video is stored */ + @Prop() + bucketLocation: string; + + @Prop() + organization: string; +} + +export type VideoFieldIntermediateDocument = VideoFieldIntermediate & Document; +export const VideoFieldIntermediateSchema = SchemaFactory.createForClass(VideoFieldIntermediate); diff --git a/packages/server/src/tag/models/video-field.model.ts b/packages/server/src/tag/models/video-field.model.ts index e471b1c9..1424f127 100644 --- a/packages/server/src/tag/models/video-field.model.ts +++ b/packages/server/src/tag/models/video-field.model.ts @@ -1,37 +1,12 @@ -import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; -import { Field, ObjectType } from '@nestjs/graphql'; -import { Document } from 'mongoose'; +import { ObjectType, Field } from '@nestjs/graphql'; +import { Entry } from 'src/entry/models/entry.model'; -/** - * Represents a single video field in a study. This is used for temporarily - * storing the recording video data before the tag is submitted and the video - * is turned into an Entry. - */ -@Schema() @ObjectType() export class VideoField { - @Field() - _id: string; + @Field(() => [Entry]) + entries: Entry[]; - /** The tag the video field is a part of */ - @Prop() - tag: string; - - /** The field of the tag the video field is a part of */ - @Prop() - field: string; - - /** The index of the video field in the tag */ - @Prop() - index: number; - - /** Where within the bucket the video is stored */ - @Prop() - bucketLocation: string; - - @Prop() - organization: string; + constructor(entries: Entry[]) { + this.entries = entries; + } } - -export type VideoFieldDocument = VideoField & Document; -export const VideoFieldSchema = SchemaFactory.createForClass(VideoField); diff --git a/packages/server/src/tag/resolvers/tag-field.resolver.ts b/packages/server/src/tag/resolvers/tag-field.resolver.ts index 523ad2ad..03abebf2 100644 --- a/packages/server/src/tag/resolvers/tag-field.resolver.ts +++ b/packages/server/src/tag/resolvers/tag-field.resolver.ts @@ -2,30 +2,15 @@ import { TagField, TagFieldType, TagFieldUnion } from '../models/tag-field.model import { ResolveField, Parent, Resolver } from '@nestjs/graphql'; import { UseGuards } from '@nestjs/common'; import { JwtAuthGuard } from '../../jwt/jwt.guard'; -import { BooleanField } from '../models/boolean-field.model'; -import { FreeTextField } from '../models/free-text-field.model'; -import { NumericField } from '../models/numeric-field.model'; -import { SliderField } from '../models/slider-field.model'; +import { TagFieldService } from '../services/tag-field.service'; @UseGuards(JwtAuthGuard) @Resolver(() => TagField) export class TagFieldResolver { + constructor(private readonly tagFieldService: TagFieldService) {} + @ResolveField(() => TagFieldUnion, { nullable: true }) async field(@Parent() tagField: TagField): Promise { - if (!tagField.data) { - return null; - } - switch(tagField.type) { - case TagFieldType.BOOLEAN: - return new BooleanField(tagField.data); - case TagFieldType.FREE_TEXT: - return new FreeTextField(tagField.data); - case TagFieldType.NUMERIC: - return new NumericField(tagField.data); - case TagFieldType.SLIDER: - return new SliderField(tagField.data); - default: - throw new Error(`Unsupported tag field type: ${tagField.type}`); - } + return this.tagFieldService.produceField(tagField); } } diff --git a/packages/server/src/tag/resolvers/video-field.resolver.ts b/packages/server/src/tag/resolvers/video-field-inter.resolver.ts similarity index 79% rename from packages/server/src/tag/resolvers/video-field.resolver.ts rename to packages/server/src/tag/resolvers/video-field-inter.resolver.ts index b905b2cf..54257949 100644 --- a/packages/server/src/tag/resolvers/video-field.resolver.ts +++ b/packages/server/src/tag/resolvers/video-field-inter.resolver.ts @@ -1,8 +1,8 @@ import { Resolver, Args, Mutation, ID, ResolveField, Parent, Int } from '@nestjs/graphql'; -import { VideoFieldService } from '../services/video-field.service'; +import { VideoFieldIntermediateService } from '../services/video-field-inter.service'; import { TagPipe } from '../pipes/tag.pipe'; import { Tag } from '../models/tag.model'; -import { VideoField } from '../models/video-field.model'; +import { VideoFieldIntermediate } from '../models/video-field-inter.model'; import { TokenContext } from '../../jwt/token.context'; import { TokenPayload } from '../../jwt/token.dto'; import { Inject, UnauthorizedException, UseGuards } from '@nestjs/common'; @@ -12,21 +12,21 @@ import { TagPermissions } from '../../permission/permissions/tag'; import { JwtAuthGuard } from '../../jwt/jwt.guard'; @UseGuards(JwtAuthGuard) -@Resolver(() => VideoField) -export class VideoFieldResolver { +@Resolver(() => VideoFieldIntermediate) +export class VideoFieldIntermediateResolver { constructor( - private readonly videoFieldService: VideoFieldService, + private readonly videoFieldService: VideoFieldIntermediateService, @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, private readonly tagPipe: TagPipe ) {} - @Mutation(() => VideoField) + @Mutation(() => VideoFieldIntermediate) async saveVideoField( @Args('tag', { type: () => ID }, TagPipe) tag: Tag, @Args('field') field: string, @Args('index', { type: () => Int }) index: number, @TokenContext() user: TokenPayload - ): Promise { + ): Promise { // Make sure the user first has permission to create video fields for this tag if (!(await this.enforcer.enforce(user.user_id, TagPermissions.CREATE, tag.study.toString()))) { throw new UnauthorizedException('User does not have permission to create video fields for this tag'); @@ -41,7 +41,7 @@ export class VideoFieldResolver { } @ResolveField(() => String) - async uploadURL(@Parent() videoField: VideoField, @TokenContext() user: TokenPayload): Promise { + async uploadURL(@Parent() videoField: VideoFieldIntermediate, @TokenContext() user: TokenPayload): Promise { const tag = await this.tagPipe.transform(videoField.tag); if (!tag) { throw new Error(`Tag ${videoField.tag} not found`); diff --git a/packages/server/src/tag/services/tag-field.service.ts b/packages/server/src/tag/services/tag-field.service.ts new file mode 100644 index 00000000..37dd2e3d --- /dev/null +++ b/packages/server/src/tag/services/tag-field.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@nestjs/common'; +import { TagField, TagFieldUnion, TagFieldType } from '../models/tag-field.model'; +import { BooleanField } from '../models/boolean-field.model'; +import { FreeTextField } from '../models/free-text-field.model'; +import { NumericField } from '../models/numeric-field.model'; +import { SliderField } from '../models/slider-field.model'; +import { EntryService } from '../../entry/services/entry.service'; +import { VideoField } from '../models/video-field.model'; +import { Entry } from 'src/entry/models/entry.model'; + + +/** + * Handles turning the rawdata fields into TagFields + */ +@Injectable() +export class TagFieldService { + constructor(private readonly entryService: EntryService) {} + + async produceField(tagField: TagField): Promise { + if (!tagField.data) { + return null; + } + switch(tagField.type) { + case TagFieldType.BOOLEAN: + return new BooleanField(tagField.data); + case TagFieldType.FREE_TEXT: + return new FreeTextField(tagField.data); + case TagFieldType.NUMERIC: + return new NumericField(tagField.data); + case TagFieldType.SLIDER: + return new SliderField(tagField.data); + case TagFieldType.VIDEO_RECORD: + return this.getVideoField(tagField.data); + default: + throw new Error(`Unsupported tag field type: ${tagField.type}`); + } + } + + private async getVideoField(tagField: TagField): Promise { + const data: string[] = JSON.parse(tagField.data); + const entryIDs = data.filter((data) => data != null) + const entries: (Entry | null)[] = []; + + for (const entryID of entryIDs) { + entries.push(await this.entryService.find(entryID)); + } + + const filtered: Entry[] = entries.filter(entry => entry != null) as Entry[]; + + return new VideoField(filtered); + } +} diff --git a/packages/server/src/tag/services/video-field.service.ts b/packages/server/src/tag/services/video-field-inter.service.ts similarity index 92% rename from packages/server/src/tag/services/video-field.service.ts rename to packages/server/src/tag/services/video-field-inter.service.ts index 8e658681..27c5ef36 100644 --- a/packages/server/src/tag/services/video-field.service.ts +++ b/packages/server/src/tag/services/video-field-inter.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { VideoField, VideoFieldDocument } from '../models/video-field.model'; +import { VideoFieldIntermediate, VideoFieldIntermediateDocument } from '../models/video-field-inter.model'; import { Model } from 'mongoose'; import { Tag } from '../models/tag.model'; import { StudyService } from '../../study/study.service'; @@ -14,14 +14,14 @@ import { BucketFactory } from 'src/bucket/bucket-factory.service'; import { BucketObjectAction } from 'src/bucket/bucket'; @Injectable() -export class VideoFieldService { +export class VideoFieldIntermediateService { private readonly bucketPrefix = this.configService.getOrThrow('tag.videoFieldFolder'); private readonly videoRecordFileType = this.configService.getOrThrow('tag.videoRecordFileType'); private readonly expiration = this.configService.getOrThrow('tag.videoUploadExpiration'); private readonly trainingPrefix = this.configService.getOrThrow('tag.trainingPrefix'); constructor( - @InjectModel(VideoField.name) private readonly videoFieldModel: Model, + @InjectModel(VideoFieldIntermediate.name) private readonly videoFieldModel: Model, private readonly studyService: StudyService, private readonly configService: ConfigService, private readonly entryService: EntryService, @@ -29,7 +29,7 @@ export class VideoFieldService { private readonly bucketFactory: BucketFactory ) {} - async saveVideoField(tag: Tag, field: string, index: number): Promise { + async saveVideoField(tag: Tag, field: string, index: number): Promise { // First do a correctness check to make sure the field shows up in the tag // TODO: Can do a correctness check on the index and using the UI schema as well const study = await this.studyService.findById(tag.study); @@ -59,7 +59,7 @@ export class VideoFieldService { }); } - async getUploadURL(videoField: VideoField): Promise { + async getUploadURL(videoField: VideoFieldIntermediate): Promise { const bucket = await this.bucketFactory.getBucket(videoField.organization); if (!bucket) { throw new Error('Could not find bucket for video field'); @@ -126,7 +126,7 @@ export class VideoFieldService { return `${this.bucketPrefix}/${tagID}/${field}/${index}.${this.videoRecordFileType}`; } - private async getVideoField(tag: Tag, field: string, index: number): Promise { + private async getVideoField(tag: Tag, field: string, index: number): Promise { return this.videoFieldModel.findOne({ tag: tag._id, field, index }).exec(); } } diff --git a/packages/server/src/tag/tag.module.ts b/packages/server/src/tag/tag.module.ts index 89da721b..a1985edc 100644 --- a/packages/server/src/tag/tag.module.ts +++ b/packages/server/src/tag/tag.module.ts @@ -8,9 +8,9 @@ import { EntryModule } from '../entry/entry.module'; import { TagPipe } from './pipes/tag.pipe'; import { SharedModule } from '../shared/shared.module'; import { PermissionModule } from '../permission/permission.module'; -import { VideoField, VideoFieldSchema } from './models/video-field.model'; -import { VideoFieldService } from './services/video-field.service'; -import { VideoFieldResolver } from './resolvers/video-field.resolver'; +import { VideoFieldIntermediate, VideoFieldIntermediateSchema } from './models/video-field-inter.model'; +import { VideoFieldIntermediateService } from './services/video-field-inter.service'; +import { VideoFieldIntermediateResolver } from './resolvers/video-field-inter.resolver'; import { GcpModule } from '../gcp/gcp.module'; import { TagTransformer } from './services/tag-transformer.service'; import { FieldTransformerFactory } from './transformers/field-transformer-factory'; @@ -25,12 +25,13 @@ import { FreeTextFieldTransformer } from './transformers/free-text.transformer'; import { NumericFieldTransformer } from './transformers/numeric-transformer'; import { SliderFieldTransformer } from './transformers/slider-transformer'; import { TagFieldResolver } from './resolvers/tag-field.resolver'; +import { TagFieldService } from './services/tag-field.service'; @Module({ imports: [ MongooseModule.forFeature([ { name: Tag.name, schema: TagSchema }, - { name: VideoField.name, schema: VideoFieldSchema }, + { name: VideoFieldIntermediate.name, schema: VideoFieldIntermediateSchema }, { name: TrainingSet.name, schema: TrainingSetSchema } ]), StudyModule, @@ -46,8 +47,8 @@ import { TagFieldResolver } from './resolvers/tag-field.resolver'; TagResolver, TagFieldResolver, TagPipe, - VideoFieldService, - VideoFieldResolver, + VideoFieldIntermediateService, + VideoFieldIntermediateResolver, TagTransformer, FieldTransformerFactory, VideoFieldTransformer, @@ -56,7 +57,8 @@ import { TagFieldResolver } from './resolvers/tag-field.resolver'; BooleanFieldTransformer, FreeTextFieldTransformer, NumericFieldTransformer, - SliderFieldTransformer + SliderFieldTransformer, + TagFieldService ] }) export class TagModule {} diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts index c656c2ee..e015bdf2 100644 --- a/packages/server/src/tag/transformers/video-field-transformer.ts +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -1,14 +1,14 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import { FieldTransformer } from './field-transformer'; import { JsonSchema, UISchemaElement } from '@jsonforms/core'; -import { VideoFieldService } from '../services/video-field.service'; +import { VideoFieldIntermediateService } from '../services/video-field-inter.service'; import { TokenPayload } from '../../jwt/token.dto'; import { Tag } from '../models/tag.model'; import { TagField, TagFieldType } from '../models/tag-field.model'; @Injectable() export class VideoFieldTransformer implements FieldTransformer { - constructor(private readonly videoFieldService: VideoFieldService) {} + constructor(private readonly videoFieldService: VideoFieldIntermediateService) {} async transformField( tag: Tag, From a3b81542c9018c1a5189538425744e5f87e349bd Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 18 Apr 2024 11:10:11 -0400 Subject: [PATCH 11/19] Video field loading --- .../tag/view/VideoGridView.component.tsx | 15 +++------------ packages/client/src/graphql/graphql.ts | 7 ++++++- packages/client/src/graphql/tag/tag.graphql | 15 +++++++++++++++ packages/client/src/graphql/tag/tag.ts | 18 ++++++++++++++++-- .../src/tag/services/tag-field.service.ts | 2 +- 5 files changed, 41 insertions(+), 16 deletions(-) diff --git a/packages/client/src/components/tag/view/VideoGridView.component.tsx b/packages/client/src/components/tag/view/VideoGridView.component.tsx index 34373c36..24d6e3ad 100644 --- a/packages/client/src/components/tag/view/VideoGridView.component.tsx +++ b/packages/client/src/components/tag/view/VideoGridView.component.tsx @@ -1,20 +1,11 @@ import { GridColDef } from '@mui/x-data-grid'; import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView'; import i18next from 'i18next'; -import { useEntryFromIdQuery } from '../../../graphql/entry/entry'; -import { Entry } from '../../../graphql/graphql'; -import { useEffect, useState } from 'react'; import { VideoEntryView } from '../../VideoView.component'; +import { Entry } from '../../../graphql/graphql'; const VideoGridView: React.FC = ({ data }) => { - const [entry, setEntry] = useState(null); - const entryFromIdResult = useEntryFromIdQuery({ variables: { entry: data } }); - - useEffect(() => { - if (entryFromIdResult.data) { - setEntry(entryFromIdResult.data.entryFromID); - } - }, [entryFromIdResult]); + const entry = data as Entry; return ( <> @@ -54,7 +45,7 @@ export const getVideoCols: GetGridColDefs = (uischema, schema, property) => { field: `${property}-video-${i + 1}`, headerName: `${property}: ${i18next.t('common.video')} ${i + 1}`, width: 350, - valueGetter: (params) => params.row.data && params.row.data[property][i], + valueGetter: (params) => params.row.data[property]?.field?.entries[i], renderCell: (params) => params.value && ( diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 12791705..3aa3162d 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -139,7 +139,7 @@ export type Mutation = { lexiconClearEntries: Scalars['Boolean']['output']; lexiconCreate: Lexicon; removeTag: Scalars['Boolean']['output']; - saveVideoField: VideoField; + saveVideoField: VideoFieldIntermediate; setEntryEnabled: Scalars['Boolean']['output']; signLabCreateProject: Project; }; @@ -615,6 +615,11 @@ export type User = { export type VideoField = { __typename?: 'VideoField'; + entries: Array; +}; + +export type VideoFieldIntermediate = { + __typename?: 'VideoFieldIntermediate'; _id: Scalars['String']['output']; uploadURL: Scalars['String']['output']; }; diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index 7b47920f..605db1f3 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -71,6 +71,21 @@ query getTags($study: ID!) { field { __typename + ... on VideoField { + entries { + _id + organization + entryID + contentType + creator + dateCreated + meta + signedUrl + signedUrlExpiration + isTraining + } + } + ... on BooleanField { boolValue: value diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index b5f5ae04..e4201c9d 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -67,14 +67,14 @@ export type SaveVideoFieldMutationVariables = Types.Exact<{ }>; -export type SaveVideoFieldMutation = { __typename?: 'Mutation', saveVideoField: { __typename?: 'VideoField', _id: string, uploadURL: string } }; +export type SaveVideoFieldMutation = { __typename?: 'Mutation', saveVideoField: { __typename?: 'VideoFieldIntermediate', _id: string, uploadURL: string } }; export type GetTagsQueryVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; }>; -export type GetTagsQuery = { __typename?: 'Query', getTags: 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: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } | null }> | null }> }; +export type GetTagsQuery = { __typename?: 'Query', getTags: 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: '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 GetTrainingTagsQueryVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; @@ -384,6 +384,20 @@ export const GetTagsDocument = gql` name field { __typename + ... on VideoField { + entries { + _id + organization + entryID + contentType + creator + dateCreated + meta + signedUrl + signedUrlExpiration + isTraining + } + } ... on BooleanField { boolValue: value } diff --git a/packages/server/src/tag/services/tag-field.service.ts b/packages/server/src/tag/services/tag-field.service.ts index 37dd2e3d..fdc3fa42 100644 --- a/packages/server/src/tag/services/tag-field.service.ts +++ b/packages/server/src/tag/services/tag-field.service.ts @@ -30,7 +30,7 @@ export class TagFieldService { case TagFieldType.SLIDER: return new SliderField(tagField.data); case TagFieldType.VIDEO_RECORD: - return this.getVideoField(tagField.data); + return this.getVideoField(tagField); default: throw new Error(`Unsupported tag field type: ${tagField.type}`); } From 7e25f0ab8f3821cc947eb452187664e4d3acec66 Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 18 Apr 2024 11:51:37 -0400 Subject: [PATCH 12/19] Add method for resolving ASL lex field --- packages/server/src/config/configuration.ts | 3 ++ .../src/tag/models/asl-lex-field.model.ts | 30 +++++++++++++++++++ .../server/src/tag/models/tag-field.model.ts | 3 +- .../src/tag/services/tag-field.service.ts | 14 ++++++++- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/server/src/config/configuration.ts b/packages/server/src/config/configuration.ts index 82b28685..5c86526c 100644 --- a/packages/server/src/config/configuration.ts +++ b/packages/server/src/config/configuration.ts @@ -36,5 +36,8 @@ export default () => ({ videoRecordFileType: 'webm', videoUploadExpiration: process.env.TAG_VIDEO_UPLOAD_EXPIRATION || 15 * 60 * 1000, // 15 minutes trainingPrefix: process.env.TAG_TRAINING_PREFIX || 'training' + }, + lexicon: { + aslLexID: process.env.ASL_LEX_LEXICON_ID || '64e4e63ecade2ec090d6765e' } }); diff --git a/packages/server/src/tag/models/asl-lex-field.model.ts b/packages/server/src/tag/models/asl-lex-field.model.ts index e69de29b..0ad60f73 100644 --- a/packages/server/src/tag/models/asl-lex-field.model.ts +++ b/packages/server/src/tag/models/asl-lex-field.model.ts @@ -0,0 +1,30 @@ +import { Field, ObjectType, Directive } from '@nestjs/graphql'; + + +@ObjectType() +@Directive('@key(fields: "key, lexicon")') +@Directive('@extends') +export class LexiconEntry { + @Field() + @Directive('@external') + key: string; + + @Field() + @Directive('@external') + lexicon: string; + + constructor(key: string, lexicon: string) { + this.key = key; + this.lexicon = lexicon; + } +} + +@ObjectType() +export class AslLexField { + @Field(() => LexiconEntry) + lexiconEntry: LexiconEntry; + + constructor(lexiconEntry: LexiconEntry) { + this.lexiconEntry = lexiconEntry; + } +} diff --git a/packages/server/src/tag/models/tag-field.model.ts b/packages/server/src/tag/models/tag-field.model.ts index e738012d..298181f1 100644 --- a/packages/server/src/tag/models/tag-field.model.ts +++ b/packages/server/src/tag/models/tag-field.model.ts @@ -6,6 +6,7 @@ import { FreeTextField } from './free-text-field.model'; import { BooleanField } from './boolean-field.model'; import { NumericField } from './numeric-field.model'; import { SliderField } from './slider-field.model'; +import { AslLexField } from './asl-lex-field.model'; export enum TagFieldType { ASL_LEX = 'ASL_LEX', @@ -24,7 +25,7 @@ registerEnumType(TagFieldType, { export const TagFieldUnion = createUnionType({ name: 'TagFieldUnion', - types: () => [BooleanField, FreeTextField, NumericField, SliderField, VideoField] as const + types: () => [AslLexField, BooleanField, FreeTextField, NumericField, SliderField, VideoField] as const }); @Schema() diff --git a/packages/server/src/tag/services/tag-field.service.ts b/packages/server/src/tag/services/tag-field.service.ts index fdc3fa42..2511e173 100644 --- a/packages/server/src/tag/services/tag-field.service.ts +++ b/packages/server/src/tag/services/tag-field.service.ts @@ -7,6 +7,8 @@ import { SliderField } from '../models/slider-field.model'; import { EntryService } from '../../entry/services/entry.service'; import { VideoField } from '../models/video-field.model'; import { Entry } from 'src/entry/models/entry.model'; +import { AslLexField, LexiconEntry } from '../models/asl-lex-field.model'; +import { ConfigService } from '@nestjs/config'; /** @@ -14,13 +16,17 @@ import { Entry } from 'src/entry/models/entry.model'; */ @Injectable() export class TagFieldService { - constructor(private readonly entryService: EntryService) {} + private readonly aslLexID = this.configService.getOrThrow('lexicon.aslLexID'); + + constructor(private readonly entryService: EntryService, private readonly configService: ConfigService) {} async produceField(tagField: TagField): Promise { if (!tagField.data) { return null; } switch(tagField.type) { + case TagFieldType.ASL_LEX: + return this.getAslLexField(tagField); case TagFieldType.BOOLEAN: return new BooleanField(tagField.data); case TagFieldType.FREE_TEXT: @@ -49,4 +55,10 @@ export class TagFieldService { return new VideoField(filtered); } + + private async getAslLexField(tagField: TagField): Promise { + const key = tagField.data as string; + const lexicon = this.aslLexID; + return new AslLexField(new LexiconEntry(key, lexicon)); + } } From 64ddab85e448985410cd2472f00c95581b7806ca Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 18 Apr 2024 13:48:33 -0400 Subject: [PATCH 13/19] ASL LEX field capturing --- .../tag/view/AslLexGridView.component.tsx | 6 ++-- packages/client/src/graphql/graphql.ts | 7 +++- packages/client/src/graphql/tag/tag.graphql | 12 ++++++- packages/client/src/graphql/tag/tag.ts | 14 ++++++-- packages/server/src/tag/tag.module.ts | 4 ++- .../tag/transformers/asl-lex-transformer.ts | 35 +++++++++++++++++++ .../transformers/field-transformer-factory.ts | 3 ++ 7 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index 9461aa65..d4dd240c 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -70,7 +70,7 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { field: `${property}-video`, headerName: `${property}: ${i18next.t('common.video')}`, width: 300, - valueGetter: (params) => params.row.data && params.row.data[property], + valueGetter: (params) => params.row.data[property]?.field?.video, renderCell: (params) => params.value && ( @@ -79,7 +79,7 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { { field: `${property}-key`, headerName: `${property}: ${i18next.t('common.key')}`, - valueGetter: (params) => params.row.data && params.row.data[property], + valueGetter: (params) => params.row.data[property]?.field?.key, renderCell: (params) => params.value && ( @@ -88,7 +88,7 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { { field: `${property}-primary`, headerName: `${property}: ${i18next.t('common.primary')}`, - valueGetter: (params) => params.row.data && params.row.data[property], + valueGetter: (params) => params.row.data[property]?.field?.primary, renderCell: (params) => params.value && ( diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 3aa3162d..dfc746d2 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -18,6 +18,11 @@ export type Scalars = { JSON: { input: any; output: any; } }; +export type AslLexField = { + __typename?: 'AslLexField'; + lexiconEntry: LexiconEntry; +}; + export type BooleanField = { __typename?: 'BooleanField'; value: Scalars['Boolean']['output']; @@ -572,7 +577,7 @@ export enum TagFieldType { VideoRecord = 'VIDEO_RECORD' } -export type TagFieldUnion = BooleanField | FreeTextField | NumericField | SliderField | VideoField; +export type TagFieldUnion = AslLexField | BooleanField | FreeTextField | NumericField | SliderField | VideoField; export type TagSchema = { __typename?: 'TagSchema'; diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index 605db1f3..bbff3024 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -71,6 +71,17 @@ query getTags($study: ID!) { field { __typename + ... on AslLexField { + lexiconEntry { + key + primary + video + lexicon + associates + fields + } + } + ... on VideoField { entries { _id @@ -86,7 +97,6 @@ query getTags($study: ID!) { } } - ... on BooleanField { boolValue: value } diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index e4201c9d..f3e64c19 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -74,7 +74,7 @@ export type GetTagsQueryVariables = Types.Exact<{ }>; -export type GetTagsQuery = { __typename?: 'Query', getTags: 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: '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 GetTagsQuery = { __typename?: 'Query', getTags: 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 GetTrainingTagsQueryVariables = Types.Exact<{ study: Types.Scalars['ID']['input']; @@ -82,7 +82,7 @@ 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: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } | null }> | null }> }; +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' } | { __typename: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } | null }> | null }> }; export const CreateTagsDocument = gql` @@ -384,6 +384,16 @@ export const GetTagsDocument = gql` name field { __typename + ... on AslLexField { + lexiconEntry { + key + primary + video + lexicon + associates + fields + } + } ... on VideoField { entries { _id diff --git a/packages/server/src/tag/tag.module.ts b/packages/server/src/tag/tag.module.ts index a1985edc..5cb6b90d 100644 --- a/packages/server/src/tag/tag.module.ts +++ b/packages/server/src/tag/tag.module.ts @@ -26,6 +26,7 @@ import { NumericFieldTransformer } from './transformers/numeric-transformer'; import { SliderFieldTransformer } from './transformers/slider-transformer'; import { TagFieldResolver } from './resolvers/tag-field.resolver'; import { TagFieldService } from './services/tag-field.service'; +import { AslLexFieldTransformer } from './transformers/asl-lex-transformer'; @Module({ imports: [ @@ -58,7 +59,8 @@ import { TagFieldService } from './services/tag-field.service'; FreeTextFieldTransformer, NumericFieldTransformer, SliderFieldTransformer, - TagFieldService + TagFieldService, + AslLexFieldTransformer ] }) export class TagModule {} diff --git a/packages/server/src/tag/transformers/asl-lex-transformer.ts b/packages/server/src/tag/transformers/asl-lex-transformer.ts index e69de29b..d28b7a94 100644 --- a/packages/server/src/tag/transformers/asl-lex-transformer.ts +++ b/packages/server/src/tag/transformers/asl-lex-transformer.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +import { FieldTransformer } from './field-transformer'; +import { JsonSchema, UISchemaElement } from '@jsonforms/core'; +import { TokenPayload } from '../../jwt/token.dto'; +import { Tag } from '../models/tag.model'; +import { TagField, TagFieldType } from '../models/tag-field.model'; + +@Injectable() +export class AslLexFieldTransformer implements FieldTransformer { + async transformField( + _tag: Tag, + data: string, + _uischema: UISchemaElement, + _schema: JsonSchema, + _user: TokenPayload, + property: string + ): Promise { + return { + name: property, + data, + type: TagFieldType.ASL_LEX + } + } +} + +export const AslLexFieldTransformerTest = (uischema: UISchemaElement, _schema: JsonSchema) => { + if ( + uischema.options != undefined && + uischema.options.customType != undefined && + uischema.options.customType == 'asl-lex' + ) { + return 15; + } + return -1; +}; diff --git a/packages/server/src/tag/transformers/field-transformer-factory.ts b/packages/server/src/tag/transformers/field-transformer-factory.ts index 44ff6cd5..50abe3dd 100644 --- a/packages/server/src/tag/transformers/field-transformer-factory.ts +++ b/packages/server/src/tag/transformers/field-transformer-factory.ts @@ -6,12 +6,14 @@ import { VideoFieldTransformer, VideoFieldTransformerTest } from './video-field- import { NumericFieldTransformerTest, NumericFieldTransformer } from './numeric-transformer'; import { FreeTextFieldTransformer, FreeTextFieldTransformerTest } from './free-text.transformer'; import { SliderFieldTransformerTest, SliderFieldTransformer } from './slider-transformer'; +import { AslLexFieldTransformer, AslLexFieldTransformerTest } from './asl-lex-transformer'; type FieldTransformerOptions = { tester: FieldTransformerTest; transformer: FieldTransformer }; @Injectable() export class FieldTransformerFactory { private readonly transformers: FieldTransformerOptions[] = [ + { tester: AslLexFieldTransformerTest, transformer: this.aslLexFieldTransformer }, { tester: BooleanFieldTransformerTest, transformer: this.booleanFieldTransformer }, { tester: FreeTextFieldTransformerTest, transformer: this.freeTextFieldTransformer }, { tester: NumericFieldTransformerTest, transformer: this.numericFieldTransformer }, @@ -20,6 +22,7 @@ export class FieldTransformerFactory { ]; constructor( + private readonly aslLexFieldTransformer: AslLexFieldTransformer, private readonly booleanFieldTransformer: BooleanFieldTransformer, private readonly freeTextFieldTransformer: FreeTextFieldTransformer, private readonly numericFieldTransformer: NumericFieldTransformer, From 5e984ff83871f470e97446aee492cc54cc76c1f3 Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 18 Apr 2024 13:53:07 -0400 Subject: [PATCH 14/19] Working video view for ASL lex field --- .../tag/view/AslLexGridView.component.tsx | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index d4dd240c..b585f5ec 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -5,17 +5,7 @@ import { VideoEntryView } from '../../VideoView.component'; import i18next from 'i18next'; const AslLexGridViewVideo: React.FC = ({ data }) => { - const [videoUrl, setVideoUrl] = useState(null); - - const lexiconByKeyResult = useLexiconByKeyQuery({ - variables: { lexicon: import.meta.env.VITE_ASL_LEXICON_ID, key: data } - }); - - useEffect(() => { - if (lexiconByKeyResult.data) { - setVideoUrl(lexiconByKeyResult.data.lexiconByKey.video); - } - }, [lexiconByKeyResult]); + const videoUrl = data as string; return ( <> @@ -69,8 +59,8 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { { field: `${property}-video`, headerName: `${property}: ${i18next.t('common.video')}`, - width: 300, - valueGetter: (params) => params.row.data[property]?.field?.video, + width: 350, + valueGetter: (params) => params.row.data[property]?.field?.lexiconEntry.video, renderCell: (params) => params.value && ( @@ -79,7 +69,7 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { { field: `${property}-key`, headerName: `${property}: ${i18next.t('common.key')}`, - valueGetter: (params) => params.row.data[property]?.field?.key, + valueGetter: (params) => params.row.data[property]?.field?.lexiconEntry.key, renderCell: (params) => params.value && ( @@ -88,7 +78,7 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { { field: `${property}-primary`, headerName: `${property}: ${i18next.t('common.primary')}`, - valueGetter: (params) => params.row.data[property]?.field?.primary, + valueGetter: (params) => params.row.data[property]?.field?.lexiconEntry.primary, renderCell: (params) => params.value && ( From 8de4c172eaa5b2d017036510a56f691b7a562d29 Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 18 Apr 2024 13:55:03 -0400 Subject: [PATCH 15/19] Fix formatting --- .../tag/view/AslLexGridView.component.tsx | 12 +++--------- .../tag/view/BooleanGridView.component.tsx | 4 +--- .../tag/view/FreeTextGridView.component.tsx | 3 ++- .../tag/view/NumericGridView.component.tsx | 4 +--- .../tag/view/SliderGridView.component.tsx | 4 +--- .../tag/view/TagGridView.component.tsx | 17 ++++++++++++----- .../tag/view/VideoGridView.component.tsx | 5 +---- .../src/tag/models/asl-lex-field.model.ts | 1 - .../src/tag/services/tag-field.service.ts | 7 +++---- .../src/tag/transformers/asl-lex-transformer.ts | 2 +- .../src/tag/transformers/boolean-transformer.ts | 2 +- .../src/tag/transformers/field-transformer.ts | 9 ++++++++- .../tag/transformers/free-text.transformer.ts | 2 +- .../src/tag/transformers/numeric-transformer.ts | 2 +- .../src/tag/transformers/slider-transformer.ts | 2 +- .../tag/transformers/video-field-transformer.ts | 2 +- 16 files changed, 38 insertions(+), 40 deletions(-) diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index b585f5ec..762828ce 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -62,27 +62,21 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { width: 350, valueGetter: (params) => params.row.data[property]?.field?.lexiconEntry.video, renderCell: (params) => - params.value && ( - - ) + params.value && }, { field: `${property}-key`, headerName: `${property}: ${i18next.t('common.key')}`, valueGetter: (params) => params.row.data[property]?.field?.lexiconEntry.key, renderCell: (params) => - params.value && ( - - ) + params.value && }, { field: `${property}-primary`, headerName: `${property}: ${i18next.t('common.primary')}`, valueGetter: (params) => params.row.data[property]?.field?.lexiconEntry.primary, renderCell: (params) => - params.value && ( - - ) + params.value && } ]; }; diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx index 46538d02..5f74876d 100644 --- a/packages/client/src/components/tag/view/BooleanGridView.component.tsx +++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx @@ -21,9 +21,7 @@ export const getBoolCols: GetGridColDefs = (uischema, schema, property) => { headerName: property, valueGetter: (params) => params.row.data[property]?.field?.boolValue, renderCell: (params) => - params.value && ( - - ) + params.value && } ]; }; diff --git a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx index 5ade363c..05d361b8 100644 --- a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx +++ b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx @@ -19,7 +19,8 @@ export const getTextCols: GetGridColDefs = (uischema, schema, property) => { field: property, headerName: property, valueGetter: (params) => params.row.data[property]?.field?.textValue, - renderCell: (params) => params.value && + renderCell: (params) => + params.value && } ]; }; diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx index ff88fd27..6a434363 100644 --- a/packages/client/src/components/tag/view/NumericGridView.component.tsx +++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx @@ -20,9 +20,7 @@ export const getNumericCols: GetGridColDefs = (uischema, schema, property) => { headerName: property, valueGetter: (params) => params.row.data[property]?.field?.numericValue, renderCell: (params) => - params.value && ( - - ) + params.value && } ]; }; diff --git a/packages/client/src/components/tag/view/SliderGridView.component.tsx b/packages/client/src/components/tag/view/SliderGridView.component.tsx index 0b8b9b98..df4c5325 100644 --- a/packages/client/src/components/tag/view/SliderGridView.component.tsx +++ b/packages/client/src/components/tag/view/SliderGridView.component.tsx @@ -20,9 +20,7 @@ export const getSliderCols: GetGridColDefs = (uischema, schema, property) => { headerName: property, valueGetter: (params) => params.row.data && params.row.data[property], renderCell: (params) => - params.value && ( - - ) + params.value && } ]; }; diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 4572552b..d4766350 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -1,7 +1,14 @@ import { useTranslation } from 'react-i18next'; import { GetGridColDefs, TagViewTest } from '../../../types/TagColumnView'; import { Entry, Study } from '../../../graphql/graphql'; -import { GridColDef, GridRenderCellParams, GridToolbarColumnsButton, GridToolbarContainer, GridToolbarExport, GridToolbarFilterButton } from '@mui/x-data-grid'; +import { + GridColDef, + GridRenderCellParams, + GridToolbarColumnsButton, + GridToolbarContainer, + GridToolbarExport, + GridToolbarFilterButton +} from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid'; import { GetTagsQuery, useRemoveTagMutation } from '../../../graphql/tag/tag'; import { freeTextTest, getTextCols } from './FreeTextGridView.component'; @@ -71,13 +78,13 @@ export const TagGridView: React.FC = ({ tags, study, refetchTa const properties = {} as any; for (const property of Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties)) { - properties[property] = tag.data!.find(row => row.name === property); + properties[property] = tag.data!.find((row) => row.name === property); } newGridData.push({ ...tag, - data: properties, - }) + data: properties + }); } setGridData(newGridData); @@ -168,7 +175,7 @@ const TagToolbar: React.FC = () => { - ) + ); }; /* diff --git a/packages/client/src/components/tag/view/VideoGridView.component.tsx b/packages/client/src/components/tag/view/VideoGridView.component.tsx index 24d6e3ad..f28f2500 100644 --- a/packages/client/src/components/tag/view/VideoGridView.component.tsx +++ b/packages/client/src/components/tag/view/VideoGridView.component.tsx @@ -46,10 +46,7 @@ export const getVideoCols: GetGridColDefs = (uischema, schema, property) => { headerName: `${property}: ${i18next.t('common.video')} ${i + 1}`, width: 350, valueGetter: (params) => params.row.data[property]?.field?.entries[i], - renderCell: (params) => - params.value && ( - - ) + renderCell: (params) => params.value && }); } diff --git a/packages/server/src/tag/models/asl-lex-field.model.ts b/packages/server/src/tag/models/asl-lex-field.model.ts index 0ad60f73..f8408843 100644 --- a/packages/server/src/tag/models/asl-lex-field.model.ts +++ b/packages/server/src/tag/models/asl-lex-field.model.ts @@ -1,6 +1,5 @@ import { Field, ObjectType, Directive } from '@nestjs/graphql'; - @ObjectType() @Directive('@key(fields: "key, lexicon")') @Directive('@extends') diff --git a/packages/server/src/tag/services/tag-field.service.ts b/packages/server/src/tag/services/tag-field.service.ts index 2511e173..a60357bb 100644 --- a/packages/server/src/tag/services/tag-field.service.ts +++ b/packages/server/src/tag/services/tag-field.service.ts @@ -10,7 +10,6 @@ import { Entry } from 'src/entry/models/entry.model'; import { AslLexField, LexiconEntry } from '../models/asl-lex-field.model'; import { ConfigService } from '@nestjs/config'; - /** * Handles turning the rawdata fields into TagFields */ @@ -24,7 +23,7 @@ export class TagFieldService { if (!tagField.data) { return null; } - switch(tagField.type) { + switch (tagField.type) { case TagFieldType.ASL_LEX: return this.getAslLexField(tagField); case TagFieldType.BOOLEAN: @@ -44,14 +43,14 @@ export class TagFieldService { private async getVideoField(tagField: TagField): Promise { const data: string[] = JSON.parse(tagField.data); - const entryIDs = data.filter((data) => data != null) + const entryIDs = data.filter((data) => data != null); const entries: (Entry | null)[] = []; for (const entryID of entryIDs) { entries.push(await this.entryService.find(entryID)); } - const filtered: Entry[] = entries.filter(entry => entry != null) as Entry[]; + const filtered: Entry[] = entries.filter((entry) => entry != null) as Entry[]; return new VideoField(filtered); } diff --git a/packages/server/src/tag/transformers/asl-lex-transformer.ts b/packages/server/src/tag/transformers/asl-lex-transformer.ts index d28b7a94..d6b8a7e0 100644 --- a/packages/server/src/tag/transformers/asl-lex-transformer.ts +++ b/packages/server/src/tag/transformers/asl-lex-transformer.ts @@ -19,7 +19,7 @@ export class AslLexFieldTransformer implements FieldTransformer { name: property, data, type: TagFieldType.ASL_LEX - } + }; } } diff --git a/packages/server/src/tag/transformers/boolean-transformer.ts b/packages/server/src/tag/transformers/boolean-transformer.ts index dee60113..2dc4e16a 100644 --- a/packages/server/src/tag/transformers/boolean-transformer.ts +++ b/packages/server/src/tag/transformers/boolean-transformer.ts @@ -19,7 +19,7 @@ export class BooleanFieldTransformer implements FieldTransformer { name: property, data, type: TagFieldType.BOOLEAN - } + }; } } diff --git a/packages/server/src/tag/transformers/field-transformer.ts b/packages/server/src/tag/transformers/field-transformer.ts index a66068ab..864a0a74 100644 --- a/packages/server/src/tag/transformers/field-transformer.ts +++ b/packages/server/src/tag/transformers/field-transformer.ts @@ -9,7 +9,14 @@ import { Tag } from '../models/tag.model'; * and ensuring that the data meets any additional formatting requirements. */ export interface FieldTransformer { - transformField(tag: Tag, data: any, uischema: UISchemaElement, schema: JsonSchema, user: TokenPayload, property: string): Promise; + transformField( + tag: Tag, + data: any, + uischema: UISchemaElement, + schema: JsonSchema, + user: TokenPayload, + property: string + ): Promise; } /** diff --git a/packages/server/src/tag/transformers/free-text.transformer.ts b/packages/server/src/tag/transformers/free-text.transformer.ts index 32f1aa9f..5f3d17b7 100644 --- a/packages/server/src/tag/transformers/free-text.transformer.ts +++ b/packages/server/src/tag/transformers/free-text.transformer.ts @@ -19,7 +19,7 @@ export class FreeTextFieldTransformer implements FieldTransformer { name: property, data, type: TagFieldType.FREE_TEXT - } + }; } } diff --git a/packages/server/src/tag/transformers/numeric-transformer.ts b/packages/server/src/tag/transformers/numeric-transformer.ts index 897879af..73bddbcf 100644 --- a/packages/server/src/tag/transformers/numeric-transformer.ts +++ b/packages/server/src/tag/transformers/numeric-transformer.ts @@ -19,7 +19,7 @@ export class NumericFieldTransformer implements FieldTransformer { name: property, data, type: TagFieldType.NUMERIC - } + }; } } diff --git a/packages/server/src/tag/transformers/slider-transformer.ts b/packages/server/src/tag/transformers/slider-transformer.ts index c119891b..175965c3 100644 --- a/packages/server/src/tag/transformers/slider-transformer.ts +++ b/packages/server/src/tag/transformers/slider-transformer.ts @@ -19,7 +19,7 @@ export class SliderFieldTransformer implements FieldTransformer { name: property, data, type: TagFieldType.SLIDER - } + }; } } diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts index e015bdf2..4a7f2ddd 100644 --- a/packages/server/src/tag/transformers/video-field-transformer.ts +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -34,7 +34,7 @@ export class VideoFieldTransformer implements FieldTransformer { name: property, data: JSON.stringify(videoFields), type: TagFieldType.VIDEO_RECORD - } + }; } } From 06711625a65738e5610477e85460bdd826661329 Mon Sep 17 00:00:00 2001 From: cbolles Date: Thu, 18 Apr 2024 14:05:46 -0400 Subject: [PATCH 16/19] asl primary view --- .../tag/view/AslLexGridView.component.tsx | 15 +-------------- .../components/tag/view/TagGridView.component.tsx | 1 - 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index 762828ce..b1aa86d5 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -1,6 +1,4 @@ import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView'; -import { useLexiconByKeyQuery } from '../../../graphql/lex'; -import { useEffect, useState } from 'react'; import { VideoEntryView } from '../../VideoView.component'; import i18next from 'i18next'; @@ -28,18 +26,7 @@ const AslLexGridViewKey: React.FC = ({ data }) => { }; const AslLexGridViewPrimary: React.FC = ({ data }) => { - const [primary, setPrimary] = useState(null); - - const lexiconByKeyResult = useLexiconByKeyQuery({ - variables: { lexicon: import.meta.env.VITE_ASL_LEXICON_ID, key: data } - }); - - useEffect(() => { - if (lexiconByKeyResult.data) { - setPrimary(lexiconByKeyResult.data.lexiconByKey.primary); - } - }, [lexiconByKeyResult]); - + const primary = data as string; return primary || ''; }; diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index d4766350..763b7389 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -19,7 +19,6 @@ import { getSliderCols, sliderTest } from './SliderGridView.component'; import { getBoolCols, booleanTest } from './BooleanGridView.component'; import { aslLexTest, getAslLexCols } from './AslLexGridView.component'; import { getVideoCols, videoViewTest } from './VideoGridView.component'; -import { Download } from '@mui/icons-material'; import { useEffect, useState } from 'react'; export interface TagGridViewProps { From 0c3e9f1e74af61829bae890f9d283c5afb3981ed Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 19 Apr 2024 12:10:38 -0400 Subject: [PATCH 17/19] Store video field as custom type --- .../src/tag/models/video-field.model.ts | 15 ++++++++--- .../src/tag/resolvers/video-field.resolver.ts | 20 ++++++++++++++ .../src/tag/services/tag-field.service.ts | 26 +++++++++---------- .../src/tag/services/video-field.service.ts | 22 ++++++++++++++++ packages/server/src/tag/tag.module.ts | 10 +++++-- .../transformers/video-field-transformer.ts | 17 ++++++++---- 6 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 packages/server/src/tag/resolvers/video-field.resolver.ts create mode 100644 packages/server/src/tag/services/video-field.service.ts diff --git a/packages/server/src/tag/models/video-field.model.ts b/packages/server/src/tag/models/video-field.model.ts index 1424f127..0f2f5b8b 100644 --- a/packages/server/src/tag/models/video-field.model.ts +++ b/packages/server/src/tag/models/video-field.model.ts @@ -1,12 +1,21 @@ +import { Schema, Prop, SchemaFactory } from '@nestjs/mongoose'; import { ObjectType, Field } from '@nestjs/graphql'; import { Entry } from 'src/entry/models/entry.model'; +import { Document } from 'mongoose'; +@Schema() @ObjectType() export class VideoField { + _id: string; + @Field(() => [Entry]) - entries: Entry[]; + @Prop() + entries: string[]; - constructor(entries: Entry[]) { - this.entries = entries; + constructor(obj: any) { + Object.assign(this, obj); } } + +export type VideoFieldDocument = VideoField & Document; +export const VideoFieldSchema = SchemaFactory.createForClass(VideoField); diff --git a/packages/server/src/tag/resolvers/video-field.resolver.ts b/packages/server/src/tag/resolvers/video-field.resolver.ts new file mode 100644 index 00000000..c2d93761 --- /dev/null +++ b/packages/server/src/tag/resolvers/video-field.resolver.ts @@ -0,0 +1,20 @@ +import { Parent, ResolveField, Resolver } from '@nestjs/graphql'; +import { Entry } from 'src/entry/models/entry.model'; +import { EntryService } from 'src/entry/services/entry.service'; +import { VideoField } from '../models/video-field.model'; + +@Resolver(() => VideoField) +export class VideoFieldResolver { + constructor(private readonly entryService: EntryService) {} + + @ResolveField(() => [Entry]) + async entries(@Parent() videoField: VideoField): Promise { + return Promise.all(videoField.entries.map(async (id) => { + const entry = await this.entryService.find(id); + if (!entry) { + throw new Error(`Invalid entry id: ${id}`); + } + return entry; + })); + } +} diff --git a/packages/server/src/tag/services/tag-field.service.ts b/packages/server/src/tag/services/tag-field.service.ts index a60357bb..3641dc28 100644 --- a/packages/server/src/tag/services/tag-field.service.ts +++ b/packages/server/src/tag/services/tag-field.service.ts @@ -4,11 +4,10 @@ import { BooleanField } from '../models/boolean-field.model'; import { FreeTextField } from '../models/free-text-field.model'; import { NumericField } from '../models/numeric-field.model'; import { SliderField } from '../models/slider-field.model'; -import { EntryService } from '../../entry/services/entry.service'; import { VideoField } from '../models/video-field.model'; -import { Entry } from 'src/entry/models/entry.model'; import { AslLexField, LexiconEntry } from '../models/asl-lex-field.model'; import { ConfigService } from '@nestjs/config'; +import { VideoFieldService } from './video-field.service'; /** * Handles turning the rawdata fields into TagFields @@ -17,7 +16,10 @@ import { ConfigService } from '@nestjs/config'; export class TagFieldService { private readonly aslLexID = this.configService.getOrThrow('lexicon.aslLexID'); - constructor(private readonly entryService: EntryService, private readonly configService: ConfigService) {} + constructor( + private readonly configService: ConfigService, + private readonly videoFieldService: VideoFieldService + ) {} async produceField(tagField: TagField): Promise { if (!tagField.data) { @@ -41,18 +43,16 @@ export class TagFieldService { } } - private async getVideoField(tagField: TagField): Promise { - const data: string[] = JSON.parse(tagField.data); - const entryIDs = data.filter((data) => data != null); - const entries: (Entry | null)[] = []; - - for (const entryID of entryIDs) { - entries.push(await this.entryService.find(entryID)); + private async getVideoField(tagField: TagField): Promise { + // The GraphQL union is resolved based on the class name, so a concrete object + // needs to be made from the document result + const videoFieldRaw = await this.videoFieldService.find(tagField.data); + if (!videoFieldRaw) { + return null; } + const videoField = new VideoField((videoFieldRaw as any).toObject()); - const filtered: Entry[] = entries.filter((entry) => entry != null) as Entry[]; - - return new VideoField(filtered); + return videoField; } private async getAslLexField(tagField: TagField): Promise { diff --git a/packages/server/src/tag/services/video-field.service.ts b/packages/server/src/tag/services/video-field.service.ts new file mode 100644 index 00000000..da14bcb0 --- /dev/null +++ b/packages/server/src/tag/services/video-field.service.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { Entry } from 'src/entry/models/entry.model'; +import { VideoField, VideoFieldDocument } from '../models/video-field.model'; + +@Injectable() +export class VideoFieldService { + constructor(@InjectModel(VideoField.name) private readonly videoFieldModel: Model) {} + + async create(entries: Entry[]): Promise { + const entryIDs = entries.map(entry => entry._id); + + return this.videoFieldModel.create({ + entries: entryIDs + }); + } + + async find(id: string): Promise { + return this.videoFieldModel.findById(id); + } +} diff --git a/packages/server/src/tag/tag.module.ts b/packages/server/src/tag/tag.module.ts index 5cb6b90d..8c4eae56 100644 --- a/packages/server/src/tag/tag.module.ts +++ b/packages/server/src/tag/tag.module.ts @@ -27,13 +27,17 @@ import { SliderFieldTransformer } from './transformers/slider-transformer'; import { TagFieldResolver } from './resolvers/tag-field.resolver'; import { TagFieldService } from './services/tag-field.service'; import { AslLexFieldTransformer } from './transformers/asl-lex-transformer'; +import { VideoField, VideoFieldSchema } from './models/video-field.model'; +import { VideoFieldService } from './services/video-field.service'; +import { VideoFieldResolver } from './resolvers/video-field.resolver'; @Module({ imports: [ MongooseModule.forFeature([ { name: Tag.name, schema: TagSchema }, { name: VideoFieldIntermediate.name, schema: VideoFieldIntermediateSchema }, - { name: TrainingSet.name, schema: TrainingSetSchema } + { name: TrainingSet.name, schema: TrainingSetSchema }, + { name: VideoField.name, schema: VideoFieldSchema } ]), StudyModule, EntryModule, @@ -60,7 +64,9 @@ import { AslLexFieldTransformer } from './transformers/asl-lex-transformer'; NumericFieldTransformer, SliderFieldTransformer, TagFieldService, - AslLexFieldTransformer + AslLexFieldTransformer, + VideoFieldService, + VideoFieldResolver ] }) export class TagModule {} diff --git a/packages/server/src/tag/transformers/video-field-transformer.ts b/packages/server/src/tag/transformers/video-field-transformer.ts index 4a7f2ddd..9f1e3419 100644 --- a/packages/server/src/tag/transformers/video-field-transformer.ts +++ b/packages/server/src/tag/transformers/video-field-transformer.ts @@ -5,10 +5,14 @@ import { VideoFieldIntermediateService } from '../services/video-field-inter.ser import { TokenPayload } from '../../jwt/token.dto'; import { Tag } from '../models/tag.model'; import { TagField, TagFieldType } from '../models/tag-field.model'; +import { VideoFieldService } from '../services/video-field.service'; @Injectable() export class VideoFieldTransformer implements FieldTransformer { - constructor(private readonly videoFieldService: VideoFieldIntermediateService) {} + constructor( + private readonly videoFieldIntermediateService: VideoFieldIntermediateService, + private readonly vidoeFieldService: VideoFieldService + ) {} async transformField( tag: Tag, @@ -23,16 +27,19 @@ export class VideoFieldTransformer implements FieldTransformer { throw new BadRequestException('Dataset ID not provided'); } - const videoFields = await Promise.all( + // Mark the intermediate video fields as complete + const entries = await Promise.all( data.map(async (videoFieldId) => { - const entry = await this.videoFieldService.markComplete(videoFieldId, datasetID, user, tag); - return entry._id; + return this.videoFieldIntermediateService.markComplete(videoFieldId, datasetID, user, tag); }) ); + // Create the complete video field + const videoField = await this.vidoeFieldService.create(entries); + return { name: property, - data: JSON.stringify(videoFields), + data: videoField._id, type: TagFieldType.VIDEO_RECORD }; } From 33681fc8be728ac6921703c7271af32ce26cf440 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 19 Apr 2024 12:12:37 -0400 Subject: [PATCH 18/19] Fix formatting --- .../tag/view/TagGridView.component.tsx | 10 ---------- .../src/tag/resolvers/video-field.resolver.ts | 16 +++++++++------- .../server/src/tag/services/tag-field.service.ts | 5 +---- .../src/tag/services/video-field.service.ts | 2 +- 4 files changed, 11 insertions(+), 22 deletions(-) diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 763b7389..c0230897 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -176,13 +176,3 @@ const TagToolbar: React.FC = () => { ); }; - -/* -const CustomExport: React.FC = () => { - const { t } = useTranslation(); - - return ( - - ) -}; -*/ diff --git a/packages/server/src/tag/resolvers/video-field.resolver.ts b/packages/server/src/tag/resolvers/video-field.resolver.ts index c2d93761..240863d6 100644 --- a/packages/server/src/tag/resolvers/video-field.resolver.ts +++ b/packages/server/src/tag/resolvers/video-field.resolver.ts @@ -9,12 +9,14 @@ export class VideoFieldResolver { @ResolveField(() => [Entry]) async entries(@Parent() videoField: VideoField): Promise { - return Promise.all(videoField.entries.map(async (id) => { - const entry = await this.entryService.find(id); - if (!entry) { - throw new Error(`Invalid entry id: ${id}`); - } - return entry; - })); + return Promise.all( + videoField.entries.map(async (id) => { + const entry = await this.entryService.find(id); + if (!entry) { + throw new Error(`Invalid entry id: ${id}`); + } + return entry; + }) + ); } } diff --git a/packages/server/src/tag/services/tag-field.service.ts b/packages/server/src/tag/services/tag-field.service.ts index 3641dc28..d405d598 100644 --- a/packages/server/src/tag/services/tag-field.service.ts +++ b/packages/server/src/tag/services/tag-field.service.ts @@ -16,10 +16,7 @@ import { VideoFieldService } from './video-field.service'; export class TagFieldService { private readonly aslLexID = this.configService.getOrThrow('lexicon.aslLexID'); - constructor( - private readonly configService: ConfigService, - private readonly videoFieldService: VideoFieldService - ) {} + constructor(private readonly configService: ConfigService, private readonly videoFieldService: VideoFieldService) {} async produceField(tagField: TagField): Promise { if (!tagField.data) { diff --git a/packages/server/src/tag/services/video-field.service.ts b/packages/server/src/tag/services/video-field.service.ts index da14bcb0..62e65a8b 100644 --- a/packages/server/src/tag/services/video-field.service.ts +++ b/packages/server/src/tag/services/video-field.service.ts @@ -9,7 +9,7 @@ export class VideoFieldService { constructor(@InjectModel(VideoField.name) private readonly videoFieldModel: Model) {} async create(entries: Entry[]): Promise { - const entryIDs = entries.map(entry => entry._id); + const entryIDs = entries.map((entry) => entry._id); return this.videoFieldModel.create({ entries: entryIDs From 8e5979f4b2d6b9a772c98f97aaf841d5243b11d6 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 19 Apr 2024 12:22:20 -0400 Subject: [PATCH 19/19] Fix build --- packages/client/src/graphql/tag/tag.graphql | 27 ++++++++++++++++++++- packages/client/src/graphql/tag/tag.ts | 26 +++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index bbff3024..27acf093 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -139,6 +139,32 @@ query getTrainingTags($study: ID!, $user: String!) { field { __typename + ... on AslLexField { + lexiconEntry { + key + primary + video + lexicon + associates + fields + } + } + + ... on VideoField { + entries { + _id + organization + entryID + contentType + creator + dateCreated + meta + signedUrl + signedUrlExpiration + isTraining + } + } + ... on BooleanField { boolValue: value } @@ -156,7 +182,6 @@ query getTrainingTags($study: ID!, $user: String!) { } } } - complete } } diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index f3e64c19..27184d8d 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -82,7 +82,7 @@ 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' } | { __typename: 'BooleanField', boolValue: boolean } | { __typename: 'FreeTextField', textValue: string } | { __typename: 'NumericField', numericValue: number } | { __typename: 'SliderField', sliderValue: number } | { __typename: 'VideoField' } | null }> | null }> }; +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 const CreateTagsDocument = gql` @@ -475,6 +475,30 @@ export const GetTrainingTagsDocument = gql` name field { __typename + ... on AslLexField { + lexiconEntry { + key + primary + video + lexicon + associates + fields + } + } + ... on VideoField { + entries { + _id + organization + entryID + contentType + creator + dateCreated + meta + signedUrl + signedUrlExpiration + isTraining + } + } ... on BooleanField { boolValue: value }