From 7ca172f1063209112ea4f5d583729a2a7c5b1263 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 19 Jan 2024 11:48:59 -0500 Subject: [PATCH 1/8] Simplify components used for TagsDisplay --- .../src/components/TagsDisplay.component.tsx | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/packages/client/src/components/TagsDisplay.component.tsx b/packages/client/src/components/TagsDisplay.component.tsx index 57b2e33f..2ceb7c75 100644 --- a/packages/client/src/components/TagsDisplay.component.tsx +++ b/packages/client/src/components/TagsDisplay.component.tsx @@ -1,4 +1,4 @@ -import { Box, Grid, Button, Container, Typography } from '@mui/material'; +import { Grid, Button, Typography, Stack } from '@mui/material'; import AccessibilityIcon from '@mui/icons-material/Accessibility'; import TextFormatIcon from '@mui/icons-material/TextFormat'; import AssistantPhotoIcon from '@mui/icons-material/AssistantPhoto'; @@ -113,52 +113,47 @@ export const TagsDisplay: React.FC = (props) => { return ( - - + + Tag Fields {tagFieldOptions.map((button: any) => ( ))} - - + + + + - + {tagFields.length > 0 ? ( tagFields.map((value: TagField, index: number) => ( - + - - - - ) : ( - <> - - {getSectionComponent()} - - - - - - - )} - - + + {steps.map((label) => { + return ( + + {label} + + ); + })} + + {activeStep === steps.length ? ( + <> + All steps completed - your new study is created + + + + + + ) : ( + <> + {getSectionComponent()} + + + + + + )} + ); }; From fa2271b1b3903d741c881fcb3a6b6f23e0200202 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 19 Jan 2024 12:04:45 -0500 Subject: [PATCH 3/8] Simplify UI of tag field --- .../src/components/TagField.component.tsx | 47 ++++++------------- .../src/components/TagsDisplay.component.tsx | 22 +++++---- .../client/src/pages/studies/NewStudy.tsx | 2 +- 3 files changed, 28 insertions(+), 43 deletions(-) diff --git a/packages/client/src/components/TagField.component.tsx b/packages/client/src/components/TagField.component.tsx index 32ca68f4..6cd6ed88 100644 --- a/packages/client/src/components/TagField.component.tsx +++ b/packages/client/src/components/TagField.component.tsx @@ -1,6 +1,6 @@ import { JsonForms } from '@jsonforms/react'; import Ajv from 'ajv'; -import { Card, CardContent, Container, Typography } from '@mui/material'; +import { Card, CardContent, Typography } from '@mui/material'; import { TagField } from '../models/TagField'; import { materialCells, materialRenderers } from '@jsonforms/material-renderers'; import { useEffect, useState } from 'react'; @@ -34,43 +34,26 @@ export const TagFieldView: React.FC = ({ field, valid, validate, ind }; return ( - - - {field.data.fieldName || 'Empty'} + + <> + {field.data.fieldName || 'Empty'} {field.kindDisplay} - + - - handleChange(data)} - schema={schema} - uischema={uiSchema} - renderers={[...materialRenderers]} - cells={materialCells} - ajv={ajv} - /> - + handleChange(data)} + schema={schema} + uischema={uiSchema} + renderers={[...materialRenderers]} + cells={materialCells} + ajv={ajv} + /> ); diff --git a/packages/client/src/components/TagsDisplay.component.tsx b/packages/client/src/components/TagsDisplay.component.tsx index 2ceb7c75..00be2a27 100644 --- a/packages/client/src/components/TagsDisplay.component.tsx +++ b/packages/client/src/components/TagsDisplay.component.tsx @@ -1,4 +1,4 @@ -import { Grid, Button, Typography, Stack } from '@mui/material'; +import { Grid, Button, Typography, Stack, Box } from '@mui/material'; import AccessibilityIcon from '@mui/icons-material/Accessibility'; import TextFormatIcon from '@mui/icons-material/TextFormat'; import AssistantPhotoIcon from '@mui/icons-material/AssistantPhoto'; @@ -114,7 +114,7 @@ export const TagsDisplay: React.FC = (props) => { return ( - + Tag Fields {tagFieldOptions.map((button: any) => ( + + + diff --git a/packages/client/src/pages/studies/NewStudy.tsx b/packages/client/src/pages/studies/NewStudy.tsx index 660218ff..d3fc813a 100644 --- a/packages/client/src/pages/studies/NewStudy.tsx +++ b/packages/client/src/pages/studies/NewStudy.tsx @@ -109,7 +109,7 @@ export const NewStudy: React.FC = () => { return ( <> - + Create New Study From 67b8b327ad0b6fc4941ad831cce99ab504323865 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 19 Jan 2024 16:58:28 -0500 Subject: [PATCH 4/8] New approach with tag providers --- .../src/components/TagField.component.tsx | 60 ------- .../src/components/TagsDisplay.component.tsx | 162 ------------------ .../tagbuilder/AslLexTagProvider.tsx | 49 ++++++ .../tagbuilder/TagField.component.tsx | 60 +++++++ .../tagbuilder/TagFormBuilder.component.tsx | 123 +++++++++++++ .../TagFormPreview.component.tsx | 16 +- .../src/components/tagbuilder/TagProvider.tsx | 77 +++++++++ .../client/src/pages/studies/NewStudy.tsx | 6 +- 8 files changed, 318 insertions(+), 235 deletions(-) delete mode 100644 packages/client/src/components/TagField.component.tsx delete mode 100644 packages/client/src/components/TagsDisplay.component.tsx create mode 100644 packages/client/src/components/tagbuilder/AslLexTagProvider.tsx create mode 100644 packages/client/src/components/tagbuilder/TagField.component.tsx create mode 100644 packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx rename packages/client/src/components/{ => tagbuilder}/TagFormPreview.component.tsx (74%) create mode 100644 packages/client/src/components/tagbuilder/TagProvider.tsx diff --git a/packages/client/src/components/TagField.component.tsx b/packages/client/src/components/TagField.component.tsx deleted file mode 100644 index 6cd6ed88..00000000 --- a/packages/client/src/components/TagField.component.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { JsonForms } from '@jsonforms/react'; -import Ajv from 'ajv'; -import { Card, CardContent, Typography } from '@mui/material'; -import { TagField } from '../models/TagField'; -import { materialCells, materialRenderers } from '@jsonforms/material-renderers'; -import { useEffect, useState } from 'react'; -import { JsonSchema } from '@jsonforms/core'; - -interface FieldProps { - field: TagField; - valid: boolean[]; - validate: React.Dispatch>; - index: number; -} - -export const TagFieldView: React.FC = ({ field, valid, validate, index }: FieldProps) => { - const [jsonData, setJsonData] = useState({}); - const [schema, setSchema] = useState({}); - const [uiSchema, setUiSchema] = useState({ type: 'object' }); - const ajv = new Ajv({ allErrors: true, schemaId: 'id' }); - - useEffect(() => { - field.getDataSchema().then((value) => setSchema(value)); - setUiSchema(field.getUISchema()); - }, [field]); - - const handleChange = (data: any) => { - field.setData(data); - setJsonData(data); - if (ajv.validate(schema, data)) { - valid[index] = true; - validate([...valid]); - } - }; - - return ( - - <> - {field.data.fieldName || 'Empty'} - - {field.kindDisplay} - - - - handleChange(data)} - schema={schema} - uischema={uiSchema} - renderers={[...materialRenderers]} - cells={materialCells} - ajv={ajv} - /> - - - ); -}; diff --git a/packages/client/src/components/TagsDisplay.component.tsx b/packages/client/src/components/TagsDisplay.component.tsx deleted file mode 100644 index 00be2a27..00000000 --- a/packages/client/src/components/TagsDisplay.component.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Grid, Button, Typography, Stack, Box } from '@mui/material'; -import AccessibilityIcon from '@mui/icons-material/Accessibility'; -import TextFormatIcon from '@mui/icons-material/TextFormat'; -import AssistantPhotoIcon from '@mui/icons-material/AssistantPhoto'; -import VideoLibraryIcon from '@mui/icons-material/VideoLibrary'; -import TextFieldsIcon from '@mui/icons-material/TextFields'; -import BarChartIcon from '@mui/icons-material/BarChart'; -import TuneIcon from '@mui/icons-material/Tune'; -import VideocamIcon from '@mui/icons-material/Videocam'; -import DeleteIcon from '@mui/icons-material/Delete'; -import { materialRenderers } from '@jsonforms/material-renderers'; -import { TagField, TagFieldType } from '../models/TagField'; -import { TagFormPreviewDialog } from './TagFormPreview.component'; -import { TagFieldGeneratorService } from '../services/tag-field-generator.service'; -import { useState, Dispatch, SetStateAction, useEffect } from 'react'; -import { TagFieldView } from './TagField.component'; -import { TagSchema } from '../graphql/graphql'; - -type TagPreviewInformation = { - previewDataSchema: any; - previewUiSchema: any; - renderers: any; -}; - -export interface TagsDisplayProps { - tagSchema: TagSchema | null; - setTagSchema: Dispatch>; -} - -export const TagsDisplay: React.FC = (props) => { - const [tagFields, setTagFields] = useState([]); - const [data, setData] = useState({ - previewDataSchema: {}, - previewUiSchema: {}, - renderers: [] - }); - const [valid, setValid] = useState([]); - const [open, setOpen] = useState(false); - const renderers = [...materialRenderers]; - - const addTagField = (tagFieldType: TagFieldType) => { - const field = TagFieldGeneratorService(tagFieldType); - setTagFields([...tagFields, field]); - setValid([...valid, false]); - }; - - const removeField = (index: number) => { - tagFields.splice(index, 1); - setTagFields([...tagFields]); - valid.splice(index, 1); - setValid([...valid]); - }; - - // Handling keeping track of complete tag schema - useEffect(() => { - if (valid.length === 0 || valid.includes(false)) { - return; - } - const schema = produceJSONForm(); - props.setTagSchema({ - dataSchema: schema.dataSchema, - uiSchema: schema.uiSchema - }); - }, [valid, tagFields]); - - const produceJSONForm = () => { - const dataSchema: { type: string; properties: any; required: string[] } = { - type: 'object', - properties: {}, - required: [] - }; - const uiSchema: { type: string; elements: any[] } = { type: 'VerticalLayout', elements: [] }; - - for (const tagField of tagFields) { - dataSchema.properties = { - ...dataSchema.properties, - ...tagField.asDataProperty() - }; - if (tagField.isRequired()) { - dataSchema.required.push(tagField.getFieldName()); - } - uiSchema.elements = [...uiSchema.elements, ...tagField.asUIProperty()]; - } - - return { dataSchema: dataSchema, uiSchema: uiSchema }; - }; - - const openTagFormPreview = () => { - const jsonForms = produceJSONForm(); - const data: TagPreviewInformation = { - previewDataSchema: jsonForms.dataSchema, - previewUiSchema: jsonForms.uiSchema, - renderers: renderers - }; - setData(data); - setOpen(true); - }; - - const toggleModal = () => { - setOpen((open) => !open); - }; - - const tagFieldOptions = [ - { name: 'ASL-LEX Sign', icon: , type: TagFieldType.AslLex }, - { name: 'Categorical', icon: , type: TagFieldType.AutoComplete }, - { name: 'True/False Option', icon: , type: TagFieldType.BooleanOption }, - { name: 'Video Option', icon: , type: TagFieldType.EmbeddedVideoOption }, - { name: 'Free Text', icon: , type: TagFieldType.FreeText }, - { name: 'Numeric', icon: , type: TagFieldType.Numeric }, - { name: 'Slider', icon: , type: TagFieldType.Slider }, - { name: 'Record Video', icon: , type: TagFieldType.VideoRecord } - ]; - - return ( - - - - Tag Fields - {tagFieldOptions.map((button: any) => ( - - ))} - - - - - - - - - - - - {tagFields.length > 0 ? ( - tagFields.map((value: TagField, index: number) => ( - - - + + + {tagSchema && } + + + + + {tagFields.length > 0 ? ( + tagFields.map((value: TagField, index: number) => ( + + updateTagSchemaFragment(index, fragment)} /> + +}; + +/** + * Represents how to construct a field of a tag. Used both for building the + * tag field form as well as for how to generate the final data and ui + * schema for the given field. + */ +export interface TagField { + /** Data schema for the generating the tag field form */ + dataSchema: JsonSchema; + /** UI schema for generating the tag field form */ + uiSchema: Layout; + /** The kind of field this is */ + fieldKind: string; + /** How to generate the data schema snippet for the final tag schema */ + produceDataSchema: (data: any) => any; + /** How to generate the ui schema snippet for the final tag schema */ + produceUISchema: (data: any) => any; +} + +/** Helper function to make the JSON form for the tag field form */ +export const produceJSONForm = (additionaProperties: { [property: string]: JsonSchema7 }, additionalUISchema: any[], additionalRequired: string[]): { dataSchema: JsonSchema, uiSchema: Layout } => { + return { + dataSchema: { + type: 'object', + properties: { + fieldName: { type: 'string' }, + description: { type: 'string' }, + ...additionaProperties, + required: { type: 'boolean' } + }, + required: ['fieldName', 'description', ...additionalRequired] + }, + uiSchema: { + type: 'HorizontalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/fieldName' + }, + { + type: 'Control', + scope: '#/properties/description' + }, + ...additionalUISchema, + { + type: 'Control', + scope: '#/properties/required' + } + ] + } + }; +}; + +/** How all tag field providers should be structured */ +export interface TagFieldProviderProps { + handleClick: (tagField: TagField) => void; +}; + +/** Representation of the final schema for a tag field */ +export interface TagFieldFragmentSchema { + dataSchema: { [property: string]: JsonSchema7 }; + uiSchema: any[]; + required: string | null; +}; diff --git a/packages/client/src/pages/studies/NewStudy.tsx b/packages/client/src/pages/studies/NewStudy.tsx index d3fc813a..81d003a0 100644 --- a/packages/client/src/pages/studies/NewStudy.tsx +++ b/packages/client/src/pages/studies/NewStudy.tsx @@ -1,5 +1,5 @@ -import { Container, Typography, Button, Box, Stepper, Step, StepLabel } from '@mui/material'; -import { TagsDisplay } from '../../components/TagsDisplay.component'; +import { Typography, Button, Box, Stepper, Step, StepLabel } from '@mui/material'; +import { TagFormBuilder } from '../../components/tagbuilder/TagFormBuilder.component'; import { NewStudyJsonForm } from '../../components/NewStudyJsonForm.component'; import { TagTrainingComponent } from '../../components/TagTraining.component'; import { useState, useEffect } from 'react'; @@ -99,7 +99,7 @@ export const NewStudy: React.FC = () => { case 0: return ; case 1: - return ; + return ; case 2: return ; default: From cde71210ca97a5687af86288d5653d47e9d0bef8 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 19 Jan 2024 17:21:15 -0500 Subject: [PATCH 5/8] Begin adding additional tag providers into new method --- .../tagbuilder/AutocompleteProvider.tsx | 53 +++++++++++++ .../components/tagbuilder/BooleanProvider.tsx | 33 ++++++++ .../tagbuilder/EmbeddedVideoProvider.tsx | 75 +++++++++++++++++++ .../tagbuilder/FreeTextProvider.tsx | 0 .../components/tagbuilder/NumericProvider.tsx | 0 .../components/tagbuilder/SliderProvider.tsx | 0 .../tagbuilder/TagFormBuilder.component.tsx | 8 +- .../tagbuilder/VideoRecordProvider.tsx | 0 8 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 packages/client/src/components/tagbuilder/AutocompleteProvider.tsx create mode 100644 packages/client/src/components/tagbuilder/BooleanProvider.tsx create mode 100644 packages/client/src/components/tagbuilder/EmbeddedVideoProvider.tsx create mode 100644 packages/client/src/components/tagbuilder/FreeTextProvider.tsx create mode 100644 packages/client/src/components/tagbuilder/NumericProvider.tsx create mode 100644 packages/client/src/components/tagbuilder/SliderProvider.tsx create mode 100644 packages/client/src/components/tagbuilder/VideoRecordProvider.tsx diff --git a/packages/client/src/components/tagbuilder/AutocompleteProvider.tsx b/packages/client/src/components/tagbuilder/AutocompleteProvider.tsx new file mode 100644 index 00000000..c4165f8d --- /dev/null +++ b/packages/client/src/components/tagbuilder/AutocompleteProvider.tsx @@ -0,0 +1,53 @@ +import { TagFieldProviderProps, produceJSONForm, ProviderButton } from './TagProvider.tsx'; +import { TextFormat } from '@mui/icons-material'; + +export const AutocompleteProvider: React.FC = (props) => { + const customFields = { + userOptions: { type: 'array', items: { type: 'string' } } + }; + + const customUISchema = [ + { + type: 'Control', + scope: '#/properties/userOptions', + options: { + customType: 'file-list' + } + } + ]; + + const produceDataSchema = (data: any) => { + return { + [data.fieldName]: { + type: 'string', + description: data.description, + enum: [...data.userOptions] + } + }; + }; + + const produceUISchema = (data: any) => { + return [ + { + type: 'Control', + scope: `#/properties/${data.fieldName}`, + options: { + customType: 'asl-lex', + allowCustomLabels: data.allowCustomLabels, + showUnfocusedDescription: true + } + } + ]; + }; + + const handleClick = () => { + props.handleClick({ + ...produceJSONForm(customFields, customUISchema, ['userOptions']), + fieldKind: 'Categorical', + produceDataSchema, + produceUISchema + }); + }; + + return } name="Categorical" onClick={handleClick} /> +}; diff --git a/packages/client/src/components/tagbuilder/BooleanProvider.tsx b/packages/client/src/components/tagbuilder/BooleanProvider.tsx new file mode 100644 index 00000000..08981041 --- /dev/null +++ b/packages/client/src/components/tagbuilder/BooleanProvider.tsx @@ -0,0 +1,33 @@ +import { TagFieldProviderProps, produceJSONForm, ProviderButton } from './TagProvider.tsx'; +import { AssistantPhoto } from '@mui/icons-material'; + +export const BooleanProvider: React.FC = (props) => { + const produceDataSchema = (data: any) => { + return { + [data.fieldName]: { + type: 'boolean', + description: data.description, + } + }; + }; + + const produceUISchema = (data: any) => { + return [ + { + type: 'Control', + scope: `#/properties/${data.fieldName}`, + } + ]; + }; + + const handleClick = () => { + props.handleClick({ + ...produceJSONForm({}, [], []), + fieldKind: 'True/False Option', + produceDataSchema, + produceUISchema + }); + }; + + return } name="True/False Option" onClick={handleClick} /> +}; diff --git a/packages/client/src/components/tagbuilder/EmbeddedVideoProvider.tsx b/packages/client/src/components/tagbuilder/EmbeddedVideoProvider.tsx new file mode 100644 index 00000000..f4d192d4 --- /dev/null +++ b/packages/client/src/components/tagbuilder/EmbeddedVideoProvider.tsx @@ -0,0 +1,75 @@ +import { TagFieldProviderProps, produceJSONForm, ProviderButton } from './TagProvider.tsx'; +import { VideoLibrary } from '@mui/icons-material'; + +export const EmbeddedProvider: React.FC = (props) => { + const customFields = { + allowCustomLabels: { type: 'boolean' }, + userVideoParameters: { + type: 'array', + items: { + type: 'object', + properties: { + videoURL: { + type: 'string' + }, + code: { + type: 'string' + }, + searchTerm: { + type: 'string' + } + }, + required: ['videoURL', 'code', 'searchTerm'] + } + } + }; + + const customUISchema = [ + { + type: 'Control', + scope: '#/properties/allowCustomLabels' + }, + { + type: 'Control', + scope: '#/properties/userVideoParameters', + options: { + customType: 'video-option-upload' + } + } + ]; + + const produceDataSchema = (data: any) => { + return { + [data.fieldName]: { + type: 'string', + description: data.description, + } + }; + }; + + const produceUISchema = (data: any) => { + return [ + { + type: 'Control', + scope: `#/properties/${data.fieldName}`, + options: { + customType: 'video-options', + allowCustomLabels: data.allowCustomLabels, + userVideoParameters: data.userVideoParameters, + showUnfocusedDescription: true + } + } + ]; + }; + + const handleClick = () => { + props.handleClick({ + ...produceJSONForm(customFields, customUISchema, ['userVideoParameters']), + fieldKind: 'List of Video Options', + produceDataSchema, + produceUISchema + }); + }; + + return } name="List of Video Options" onClick={handleClick} /> +}; diff --git a/packages/client/src/components/tagbuilder/FreeTextProvider.tsx b/packages/client/src/components/tagbuilder/FreeTextProvider.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/components/tagbuilder/NumericProvider.tsx b/packages/client/src/components/tagbuilder/NumericProvider.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/components/tagbuilder/SliderProvider.tsx b/packages/client/src/components/tagbuilder/SliderProvider.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx b/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx index e7050f49..c0bcf7c1 100644 --- a/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx +++ b/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx @@ -5,6 +5,9 @@ import { useState, Dispatch, SetStateAction, useEffect } from 'react'; import { TagFieldView } from './TagField.component'; import { TagSchema } from '../../graphql/graphql'; import { AslLexFieldProvider } from './AslLexTagProvider'; +import { AutocompleteProvider } from './AutocompleteProvider'; +import { BooleanProvider } from './BooleanProvider'; +import { EmbeddedProvider } from './EmbeddedVideoProvider'; import { TagField, TagFieldFragmentSchema } from './TagProvider'; export interface TagsDisplayProps { @@ -69,8 +72,6 @@ export const TagFormBuilder: React.FC = ({ tagSchema, setTagSc elements: uiElements } }); - - }, [tagSchemaFragments]); const openTagFormPreview = () => { @@ -87,6 +88,9 @@ export const TagFormBuilder: React.FC = ({ tagSchema, setTagSc Tag Fields + + + diff --git a/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx b/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx new file mode 100644 index 00000000..e69de29b From 71a381128d8e6d5b3c487c2a661f1be3cde98221 Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 22 Jan 2024 10:02:04 -0500 Subject: [PATCH 6/8] Replace fields with react style providers --- .../tagbuilder/FreeTextProvider.tsx | 33 +++++++ .../components/tagbuilder/NumericProvider.tsx | 33 +++++++ .../components/tagbuilder/SliderProvider.tsx | 62 ++++++++++++ .../tagbuilder/TagFormBuilder.component.tsx | 8 ++ .../tagbuilder/VideoRecordProvider.tsx | 95 +++++++++++++++++++ .../src/permission/permission.service.ts | 1 - 6 files changed, 231 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/tagbuilder/FreeTextProvider.tsx b/packages/client/src/components/tagbuilder/FreeTextProvider.tsx index e69de29b..21a37247 100644 --- a/packages/client/src/components/tagbuilder/FreeTextProvider.tsx +++ b/packages/client/src/components/tagbuilder/FreeTextProvider.tsx @@ -0,0 +1,33 @@ +import { TagFieldProviderProps, produceJSONForm, ProviderButton } from './TagProvider.tsx'; +import { AssistantPhoto } from '@mui/icons-material'; + +export const FreeTextProvider: React.FC = (props) => { + const produceDataSchema = (data: any) => { + return { + [data.fieldName]: { + type: 'string', + description: data.description, + } + }; + }; + + const produceUISchema = (data: any) => { + return [ + { + type: 'Control', + scope: `#/properties/${data.fieldName}`, + } + ]; + }; + + const handleClick = () => { + props.handleClick({ + ...produceJSONForm({}, [], []), + fieldKind: 'Free Text', + produceDataSchema, + produceUISchema + }); + }; + + return } name="Free Text Option" onClick={handleClick} /> +}; diff --git a/packages/client/src/components/tagbuilder/NumericProvider.tsx b/packages/client/src/components/tagbuilder/NumericProvider.tsx index e69de29b..69d43306 100644 --- a/packages/client/src/components/tagbuilder/NumericProvider.tsx +++ b/packages/client/src/components/tagbuilder/NumericProvider.tsx @@ -0,0 +1,33 @@ +import { TagFieldProviderProps, produceJSONForm, ProviderButton } from './TagProvider.tsx'; +import { AssistantPhoto } from '@mui/icons-material'; + +export const NumericProvider: React.FC = (props) => { + const produceDataSchema = (data: any) => { + return { + [data.fieldName]: { + type: 'number', + description: data.description, + } + }; + }; + + const produceUISchema = (data: any) => { + return [ + { + type: 'Control', + scope: `#/properties/${data.fieldName}`, + } + ]; + }; + + const handleClick = () => { + props.handleClick({ + ...produceJSONForm({}, [], []), + fieldKind: 'Numeric', + produceDataSchema, + produceUISchema + }); + }; + + return } name="Numeric" onClick={handleClick} /> +}; diff --git a/packages/client/src/components/tagbuilder/SliderProvider.tsx b/packages/client/src/components/tagbuilder/SliderProvider.tsx index e69de29b..7d4ac1b5 100644 --- a/packages/client/src/components/tagbuilder/SliderProvider.tsx +++ b/packages/client/src/components/tagbuilder/SliderProvider.tsx @@ -0,0 +1,62 @@ +import { TagFieldProviderProps, produceJSONForm, ProviderButton } from './TagProvider.tsx'; +import { AssistantPhoto } from '@mui/icons-material'; + +export const SliderProvider: React.FC = (props) => { + const customFields = { + minimum: { type: 'number', description: 'The minimum value of the slider' }, + maximum: { type: 'number', description: 'The maximum value of the slider' }, + stepSize: { type: 'number', description: 'The step size of the slider' }, + }; + + const customUISchema = [ + { + type: 'Control', + scope: '#/properties/minimum', + }, + { + type: 'Control', + scope: '#/properties/maximum', + }, + { + type: 'Control', + scope: '#/properties/stepSize', + }, + ]; + + const produceDataSchema = (data: any) => { + return { + [data.fieldName]: { + type: 'number', + description: data.description, + minimum: data.minimum, + maximum: data.maximum, + multipleOf: data.stepSize, + default: data.minimum + } + }; + }; + + const produceUISchema = (data: any) => { + return [ + { + type: 'Control', + scope: `#/properties/${data.fieldName}`, + options: { + slider: true, + showUnfocusedDescription: true, + } + } + ]; + }; + + const handleClick = () => { + props.handleClick({ + ...produceJSONForm(customFields, customUISchema, ['minimum', 'maximum']), + fieldKind: 'Slider', + produceDataSchema, + produceUISchema + }); + }; + + return } name="Slider" onClick={handleClick} /> +}; diff --git a/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx b/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx index c0bcf7c1..b0a0420e 100644 --- a/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx +++ b/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx @@ -8,6 +8,10 @@ import { AslLexFieldProvider } from './AslLexTagProvider'; import { AutocompleteProvider } from './AutocompleteProvider'; import { BooleanProvider } from './BooleanProvider'; import { EmbeddedProvider } from './EmbeddedVideoProvider'; +import { FreeTextProvider } from './FreeTextProvider'; +import { NumericProvider } from './NumericProvider'; +import { SliderProvider } from './SliderProvider'; +import { VideoRecordProvider } from './VideoRecordProvider'; import { TagField, TagFieldFragmentSchema } from './TagProvider'; export interface TagsDisplayProps { @@ -91,6 +95,10 @@ export const TagFormBuilder: React.FC = ({ tagSchema, setTagSc + + + + diff --git a/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx b/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx index e69de29b..1e86ff03 100644 --- a/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx +++ b/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx @@ -0,0 +1,95 @@ +import { TagFieldProviderProps, produceJSONForm, ProviderButton } from './TagProvider.tsx'; +import { AssistantPhoto } from '@mui/icons-material'; +import { Dataset } from '../../graphql/graphql'; +import { useState, useEffect } from 'react'; +import { useGetDatasetsByProjectLazyQuery } from '../../graphql/dataset/dataset.ts'; +import { useProject } from '../../context/Project.context.tsx'; + +export const VideoRecordProvider: React.FC = (props) => { + const [datasets, setDatasets] = useState([]); + const { project } = useProject(); + const [getDatasets, getDatasetsResults] = useGetDatasetsByProjectLazyQuery(); + + useEffect(() => { + if (project) { + getDatasets({ variables: { project: project._id } }); + } + }, [project]); + + useEffect(() => { + if (getDatasetsResults.data) { + setDatasets(getDatasetsResults.data.getDatasetsByProject); + } + }, [getDatasetsResults.data]); + + const customFields = { + dataset: { + type: 'string', + oneOf: datasets.map((dataset) => ({ const: dataset.name, title: dataset.name })), + description: 'The dataset to save the videos into' + }, + minimumRequired: { + type: 'number', + description: 'The minimum number of videos the user needs to record, (defaults to 1)' + }, + maximumOptional: { + type: 'number', + description: 'The maximum number of videos the user can record (including required, defaults to 1)' + }, + }; + + const customUISchema = [ + { + type: 'Control', + scope: '#/properties/dataset', + }, + { + type: 'Control', + scope: '#/properties/minimumRequired', + }, + { + type: 'Control', + scope: '#/properties/maximumOptional', + }, + ]; + + const produceDataSchema = (data: any) => { + return { + [data.fieldName]: { + type: 'array', + description: data.description, + items: { + type: 'string', + }, + minItems: data.minimumRequired || 1, + } + }; + }; + + const produceUISchema = (data: any) => { + return [ + { + type: 'Control', + scope: `#/properties/${data.fieldName}`, + options: { + customType: 'video', + dataset: datasets.find((dataset) => dataset.name === data.dataset)!._id, + minimumRequired: data.minimumRequired || 1, + maximumOptional: data.maximumOptional || 1, + showUnfocusedDescription: true + } + } + ]; + }; + + const handleClick = () => { + props.handleClick({ + ...produceJSONForm(customFields, customUISchema, ['dataset']), + fieldKind: 'Video Record', + produceDataSchema, + produceUISchema + }); + }; + + return } name="Video Record" onClick={handleClick} /> +}; diff --git a/packages/server/src/permission/permission.service.ts b/packages/server/src/permission/permission.service.ts index c66d8d31..9e2ce751 100644 --- a/packages/server/src/permission/permission.service.ts +++ b/packages/server/src/permission/permission.service.ts @@ -84,7 +84,6 @@ export class PermissionService { users.map(async (user) => { const isStudyAdmin = await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString()); const isStudyAdminEditable = !(await this.enforcer.enforce(user.id, Roles.PROJECT_ADMIN, study._id.toString())); - console.log(user, isStudyAdminEditable); const isContributor = await this.enforcer.enforce(user.id, Roles.CONTRIBUTOR, study._id.toString()); const isContributorEditable = !(await this.enforcer.enforce(user.id, Roles.STUDY_ADMIN, study._id.toString())); From 1b08de9c51b9222da7a6b47f15f2cfdc5596385e Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 22 Jan 2024 10:04:39 -0500 Subject: [PATCH 7/8] Delete old process for holding tags --- packages/client/src/models/TagField.tsx | 535 ------------------ .../services/tag-field-generator.service.tsx | 35 -- packages/client/src/services/tag.service.tsx | 20 - 3 files changed, 590 deletions(-) delete mode 100644 packages/client/src/models/TagField.tsx delete mode 100644 packages/client/src/services/tag-field-generator.service.tsx delete mode 100644 packages/client/src/services/tag.service.tsx diff --git a/packages/client/src/models/TagField.tsx b/packages/client/src/models/TagField.tsx deleted file mode 100644 index d4b3dd88..00000000 --- a/packages/client/src/models/TagField.tsx +++ /dev/null @@ -1,535 +0,0 @@ -import { JsonSchema, Layout } from '@jsonforms/core'; - -/* - * Different kind of tag fields that are supported - */ -export enum TagFieldType { - AslLex, - AutoComplete, - BooleanOption, - EmbeddedVideoOption, - FreeText, - Numeric, - Slider, - VideoRecord -} - -export abstract class TagField { - kind: TagFieldType; - kindDisplay: string; - name = ''; - isValid = false; - data: any = {}; - type: string; - - constructor(kind: TagFieldType, kindDisplay: string, type: string) { - this.kind = kind; - this.kindDisplay = kindDisplay; - this.type = type; - } - //in TagFieldComponent we need to set data on json form change - setData(data: any) { - this.data = data; - } - - getFieldName(): string { - return this.data.fieldName || ''; - } - - isRequired(): boolean { - return this.data.required || false; - } - - getDescription(): string { - return this.data.shortDescription || ''; - } - - async getDataSchema(): Promise { - const properties = await this.getFieldSpecificProperties(); - return { - type: 'object', - properties: { - fieldName: { - type: 'string' - }, - shortDescription: { - type: 'string' - }, - ...properties, - required: { - type: 'boolean' - } - }, - required: ['fieldName', 'shortDescription', ...this.getRequiredFieldProperties()] - }; - } - - getUISchema(): Layout { - return { - type: 'HorizontalLayout', - elements: [ - { - type: 'Control', - scope: '#/properties/fieldName' - }, - { - type: 'Control', - scope: '#/properties/shortDescription' - }, - ...this.getFieldSpecificUiSchema(), - { - type: 'Control', - scope: '#/properties/required' - } - ] - }; - } - - protected getFieldSpecificProperties(): Promise<{ - [property: string]: JsonSchema; - }> { - return Promise.resolve({}); - } - - protected getRequiredFieldProperties(): string[] { - return []; - } - - protected getFieldSpecificUiSchema(): any[] { - return []; - } - - asDataProperty(): JsonSchema { - return { - [this.getFieldName()]: { - type: this.type, - description: this.getDescription() - } - }; - } - - asUIProperty(): any[] { - return [ - { - type: 'Control', - scope: `#/properties/${this.getFieldName()}`, - options: { - showUnfocusedDescription: true - } - } - ]; - } -} - -/* - * Now we move on to actual custom fields - */ -export class AslLexField extends TagField { - constructor() { - super(TagFieldType.AslLex, 'ASL-LEX Sign', 'string'); - } - - //option for custom labels - protected getFieldSpecificProperties(): Promise<{ - [property: string]: JsonSchema; - }> { - return Promise.resolve({ - allowCustomLabels: { - type: 'boolean' - } - }); - } - - //reflect optional custom labels in UI - protected getFieldSpecificUiSchema(): any[] { - return [ - { - type: 'Control', - scope: '#/properties/allowCustomLabels' - } - ]; - } - - //overried this method to properly display ASL-LEX video options - asUIProperty(): any[] { - return [ - { - type: 'Control', - scope: `#/properties/${this.getFieldName()}`, - options: { - customType: 'asl-lex', - allowCustomLabels: this.data.allowCustomLabels, - showUnfocusedDescription: true - } - } - ]; - } -} - -export class AutoCompleteField extends TagField { - constructor() { - super(TagFieldType.AutoComplete, 'AutoComplete', 'string'); - } - - /** - * The autocomplete field needs a list of options as a data field which - * will later become the enum values in the tag field. - */ - protected getFieldSpecificProperties(): Promise<{ - [property: string]: JsonSchema; - }> { - return Promise.resolve({ - userOptions: { - type: 'array', - items: { - type: 'string' - } - } - }); - } - - //reflect optional custom labels in UI - protected getFieldSpecificUiSchema(): any[] { - return [ - { - type: 'Control', - scope: '#/properties/userOptions', - options: { - customType: 'file-list' - } - } - ]; - } - - asDataProperty(): JsonSchema { - return { - [this.getFieldName()]: { - type: this.type, - description: this.getDescription(), - enum: [...this.data.userOptions] - } - }; - } - - protected getRequiredFieldProperties(): string[] { - return ['userOptions']; - } -} - -export class BooleanField extends TagField { - constructor() { - super(TagFieldType.BooleanOption, 'Boolean Option', 'boolean'); - } -} - -export class EmbeddedVideoOption extends TagField { - constructor() { - super(TagFieldType.EmbeddedVideoOption, 'Video Option', 'string'); - } - - /* - * Provides options to allow users to select a custom input and the format - * of the video options - */ - protected getFieldSpecificProperties(): Promise<{ [property: string]: JsonSchema }> { - return Promise.resolve({ - allowCustomLabels: { - type: 'boolean' - }, - userVideoParameters: { - type: 'array', - items: { - type: 'object', - properties: { - videoURL: { - type: 'string' - }, - code: { - type: 'string' - }, - searchTerm: { - type: 'string' - } - }, - required: ['videoURL', 'code', 'searchTerm'] - } - } - }); - } - - protected getFieldSpecificUiSchema(): any[] { - return [ - { type: 'Control', scope: '#/properties/allowCustomLabels' }, - { - type: 'Control', - scope: '#/properties/userVideoParameters', - options: { - customType: 'video-option-upload' - } - } - ]; - } - - asUIProperty(): any[] { - return [ - { - type: 'Control', - scope: `#/properties/${this.getFieldName()}`, - options: { - customType: 'video-options', - allowCustomLabels: this.data.allowCustomLabels, - userVideoParameters: this.data.userVideoParameters, - showUnfocusedDescription: true - } - } - ]; - } - - protected getRequiredFieldProperties(): string[] { - return ['userVideoParameters']; - } -} - -export class FreeTextField extends TagField { - constructor() { - super(TagFieldType.FreeText, 'Free Text', 'string'); - } -} - -export class NumericField extends TagField { - constructor() { - super(TagFieldType.Numeric, 'Numeric', 'number'); - } - - //there are optional fields for min and max values - getFieldSpecificProperties(): Promise<{ [property: string]: JsonSchema }> { - return Promise.resolve({ - minimum: { - type: 'number' - }, - maximum: { - type: 'number' - } - }); - } - - protected getFieldSpecificUiSchema(): any[] { - return [ - { - type: 'Control', - scope: '#/properties/minimum' - }, - { - type: 'Control', - scope: '#/properties/maximum' - } - ]; - } - - asDataProperty(): JsonSchema { - const schema: JsonSchema = { - type: 'number', - description: this.getDescription() - }; - - if (this.data.minimum) { - schema.minimum = this.data.minimum; - } - - if (this.data.maximum) { - schema.maximum = this.data.maximum; - } - - return { - [this.getFieldName()]: schema - }; - } -} - -export class SliderField extends TagField { - constructor() { - super(TagFieldType.Slider, 'Slider', 'number'); - } - - // slider field required minimum and maximum value - protected getFieldSpecificProperties(): Promise<{ [property: string]: JsonSchema }> { - return Promise.resolve({ - minimum: { - type: 'number' - }, - maximum: { - type: 'number' - }, - stepSize: { - type: 'number', - description: 'The step size of the slider' - } - }); - } - - protected getFieldSpecificUiSchema(): any[] { - return [ - { - type: 'Control', - scope: '#/properties/minimum' - }, - { - type: 'Control', - scope: '/properties/maximum' - }, - { - type: 'Control', - scope: '#/properties/stepSize' - } - ]; - } - - protected getRequiredFieldProperties(): string[] { - return ['minimum', 'maximum']; - } - - asDataProperty(): JsonSchema { - return { - [this.getFieldName()]: { - type: 'number', - description: this.getDescription(), - minimum: this.data.minimum, - maximum: this.data.maximum, - multipleOf: this.data.stepSize, - default: this.data.minimum - } - }; - } - - asUIProperty(): any[] { - return [ - { - type: 'Control', - scope: `#/properties/${this.getFieldName()}`, - options: { - slider: true, - showUnfocusedDescription: true - } - } - ]; - } -} -/* -export class VideoRecordField extends TagField { - protected hasDatasets: boolean; - protected datasets: Observable; - - constructor(private datasetService: DatasetService) { - super(TagFieldType.VideoRecord, 'Video Record', 'string'); - this.datasets = datasetService.datasets; - this.hasDatasets = datasetService.datasets ? true : false; - } - - getUISchema(): Layout { - if (this.hasDatasets) { - return super.getUISchema(); - } - - //no datasets, cannot render video record field - return { - type: 'Label', - text: 'No datasets to save into, make a dataset before adding a video record field' - } as any; - } - - async getFieldSpecificProperties(): Promise<{ - [property: string]: JsonSchema; - }> { - this.datasets = await firstValueFrom(this.datasetService.datasets); - const options = this.datasets.map((dataset: any) => { - return { - const: dataset.name, - title: dataset.name - }; - }); - this.hasDatasets = options.length > 0; - - if (!this.hasDatasets) { - return { - alwaysError: { - type: 'number', - default: 'BAD' - } - }; - } - - return { - dataset: { - type: 'string', - oneOf: options, - description: 'The dataset to save the videos into' - }, - minimumRequired: { - type: 'number', - description: 'The minimum number of videos the user needs to record, (defaults to 1)' - }, - maximumOptional: { - type: 'number', - description: 'The maximum number of videos the user can record (including required, defaults to 1)' - } - }; - } - - protected getFieldSpecificUiSchema(): any[] { - return [ - { - type: 'Control', - scope: '#/properties/dataset' - }, - { - type: 'Control', - scope: '#/properties/minimumRequired', - options: { - showUnfocusedDescription: true - } - }, - { - type: 'Control', - scope: '#/properties/maximumOptional', - options: { - showUnfocusedDescription: true - } - } - ]; - } - - protected getRequiredFieldProperties(): string[] { - return ['dataset']; - } - - asDataProperty(): JsonSchema { - return { - [this.getFieldName()]: { - type: 'array', - description: this.getDescription(), - items: { - type: 'string' - }, - minItems: this.data.minimumRequired || 1 - } - }; - } - - asUIProperty(): any[] { - const dataset = this.datasets.find((dataset: any) => dataset.name === this.data.dataset); - console.log(dataset); - return [ - { - type: 'Control', - scope: `#properties/${this.getFieldName()}`, - options: { - customType: 'video', - dataset: dataset?.id, - minimumRequired: this.data.minimumRequired || 1, - maximumOptional: this.data.maximumOptional || 1, - showUnfocusedDescription: true - } - } - ]; - } -} */ diff --git a/packages/client/src/services/tag-field-generator.service.tsx b/packages/client/src/services/tag-field-generator.service.tsx deleted file mode 100644 index fbac1c3e..00000000 --- a/packages/client/src/services/tag-field-generator.service.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { - TagFieldType, - AslLexField, - AutoCompleteField, - BooleanField, - EmbeddedVideoOption, - FreeTextField, - NumericField, - SliderField -} from '../models/TagField'; - -export const TagFieldGeneratorService = (tagFieldType: TagFieldType) => { - /** - * Factory method to get the field definition associated with the given - * field type. - */ - switch (tagFieldType) { - case TagFieldType.AslLex: - return new AslLexField(); - case TagFieldType.AutoComplete: - return new AutoCompleteField(); - case TagFieldType.BooleanOption: - return new BooleanField(); - case TagFieldType.EmbeddedVideoOption: - return new EmbeddedVideoOption(); - case TagFieldType.FreeText: - return new FreeTextField(); - case TagFieldType.Numeric: - return new NumericField(); - case TagFieldType.Slider: - return new SliderField(); - default: - return new FreeTextField(); - } -}; diff --git a/packages/client/src/services/tag.service.tsx b/packages/client/src/services/tag.service.tsx deleted file mode 100644 index f3968831..00000000 --- a/packages/client/src/services/tag.service.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Tag } from '../graphql/graphql'; - -export class TagService { - protected currentTag: Tag | null = null; - - getCurrent = () => { - if (this.currentTag === null) { - throw new Error('no current tag'); - } - return this.currentTag; - }; - - hasCurrenttag = () => { - return this.currentTag !== null; - }; - - clearCurrentTag = () => { - this.currentTag = null; - }; -} From a2ece821d7ecd4f1d41828f59af169f775b1acbd Mon Sep 17 00:00:00 2001 From: cbolles Date: Mon, 22 Jan 2024 10:05:03 -0500 Subject: [PATCH 8/8] Fix formatting --- .../tagbuilder/AslLexTagProvider.tsx | 4 ++-- .../tagbuilder/AutocompleteProvider.tsx | 2 +- .../components/tagbuilder/BooleanProvider.tsx | 6 +++--- .../tagbuilder/EmbeddedVideoProvider.tsx | 4 ++-- .../components/tagbuilder/FreeTextProvider.tsx | 6 +++--- .../components/tagbuilder/NumericProvider.tsx | 6 +++--- .../components/tagbuilder/SliderProvider.tsx | 14 +++++++------- .../tagbuilder/TagField.component.tsx | 7 ++----- .../tagbuilder/TagFormBuilder.component.tsx | 2 +- .../tagbuilder/TagFormPreview.component.tsx | 2 +- .../src/components/tagbuilder/TagProvider.tsx | 18 +++++++++++++----- .../tagbuilder/VideoRecordProvider.tsx | 18 +++++++++--------- packages/client/src/pages/studies/NewStudy.tsx | 4 +--- 13 files changed, 48 insertions(+), 45 deletions(-) diff --git a/packages/client/src/components/tagbuilder/AslLexTagProvider.tsx b/packages/client/src/components/tagbuilder/AslLexTagProvider.tsx index c563569a..6dc4e8fd 100644 --- a/packages/client/src/components/tagbuilder/AslLexTagProvider.tsx +++ b/packages/client/src/components/tagbuilder/AslLexTagProvider.tsx @@ -17,7 +17,7 @@ export const AslLexFieldProvider: React.FC = (props) => { return { [data.fieldName]: { type: 'string', - description: data.description, + description: data.description } }; }; @@ -45,5 +45,5 @@ export const AslLexFieldProvider: React.FC = (props) => { }); }; - return } name="ASL-LEX" onClick={handleClick} /> + return } name="ASL-LEX" onClick={handleClick} />; }; diff --git a/packages/client/src/components/tagbuilder/AutocompleteProvider.tsx b/packages/client/src/components/tagbuilder/AutocompleteProvider.tsx index c4165f8d..72c3652b 100644 --- a/packages/client/src/components/tagbuilder/AutocompleteProvider.tsx +++ b/packages/client/src/components/tagbuilder/AutocompleteProvider.tsx @@ -49,5 +49,5 @@ export const AutocompleteProvider: React.FC = (props) => }); }; - return } name="Categorical" onClick={handleClick} /> + return } name="Categorical" onClick={handleClick} />; }; diff --git a/packages/client/src/components/tagbuilder/BooleanProvider.tsx b/packages/client/src/components/tagbuilder/BooleanProvider.tsx index 08981041..a733f4e2 100644 --- a/packages/client/src/components/tagbuilder/BooleanProvider.tsx +++ b/packages/client/src/components/tagbuilder/BooleanProvider.tsx @@ -6,7 +6,7 @@ export const BooleanProvider: React.FC = (props) => { return { [data.fieldName]: { type: 'boolean', - description: data.description, + description: data.description } }; }; @@ -15,7 +15,7 @@ export const BooleanProvider: React.FC = (props) => { return [ { type: 'Control', - scope: `#/properties/${data.fieldName}`, + scope: `#/properties/${data.fieldName}` } ]; }; @@ -29,5 +29,5 @@ export const BooleanProvider: React.FC = (props) => { }); }; - return } name="True/False Option" onClick={handleClick} /> + return } name="True/False Option" onClick={handleClick} />; }; diff --git a/packages/client/src/components/tagbuilder/EmbeddedVideoProvider.tsx b/packages/client/src/components/tagbuilder/EmbeddedVideoProvider.tsx index f4d192d4..8b3225b9 100644 --- a/packages/client/src/components/tagbuilder/EmbeddedVideoProvider.tsx +++ b/packages/client/src/components/tagbuilder/EmbeddedVideoProvider.tsx @@ -42,7 +42,7 @@ export const EmbeddedProvider: React.FC = (props) => { return { [data.fieldName]: { type: 'string', - description: data.description, + description: data.description } }; }; @@ -71,5 +71,5 @@ export const EmbeddedProvider: React.FC = (props) => { }); }; - return } name="List of Video Options" onClick={handleClick} /> + return } name="List of Video Options" onClick={handleClick} />; }; diff --git a/packages/client/src/components/tagbuilder/FreeTextProvider.tsx b/packages/client/src/components/tagbuilder/FreeTextProvider.tsx index 21a37247..505c2a6d 100644 --- a/packages/client/src/components/tagbuilder/FreeTextProvider.tsx +++ b/packages/client/src/components/tagbuilder/FreeTextProvider.tsx @@ -6,7 +6,7 @@ export const FreeTextProvider: React.FC = (props) => { return { [data.fieldName]: { type: 'string', - description: data.description, + description: data.description } }; }; @@ -15,7 +15,7 @@ export const FreeTextProvider: React.FC = (props) => { return [ { type: 'Control', - scope: `#/properties/${data.fieldName}`, + scope: `#/properties/${data.fieldName}` } ]; }; @@ -29,5 +29,5 @@ export const FreeTextProvider: React.FC = (props) => { }); }; - return } name="Free Text Option" onClick={handleClick} /> + return } name="Free Text Option" onClick={handleClick} />; }; diff --git a/packages/client/src/components/tagbuilder/NumericProvider.tsx b/packages/client/src/components/tagbuilder/NumericProvider.tsx index 69d43306..dfcff1e0 100644 --- a/packages/client/src/components/tagbuilder/NumericProvider.tsx +++ b/packages/client/src/components/tagbuilder/NumericProvider.tsx @@ -6,7 +6,7 @@ export const NumericProvider: React.FC = (props) => { return { [data.fieldName]: { type: 'number', - description: data.description, + description: data.description } }; }; @@ -15,7 +15,7 @@ export const NumericProvider: React.FC = (props) => { return [ { type: 'Control', - scope: `#/properties/${data.fieldName}`, + scope: `#/properties/${data.fieldName}` } ]; }; @@ -29,5 +29,5 @@ export const NumericProvider: React.FC = (props) => { }); }; - return } name="Numeric" onClick={handleClick} /> + return } name="Numeric" onClick={handleClick} />; }; diff --git a/packages/client/src/components/tagbuilder/SliderProvider.tsx b/packages/client/src/components/tagbuilder/SliderProvider.tsx index 7d4ac1b5..c7c398af 100644 --- a/packages/client/src/components/tagbuilder/SliderProvider.tsx +++ b/packages/client/src/components/tagbuilder/SliderProvider.tsx @@ -5,22 +5,22 @@ export const SliderProvider: React.FC = (props) => { const customFields = { minimum: { type: 'number', description: 'The minimum value of the slider' }, maximum: { type: 'number', description: 'The maximum value of the slider' }, - stepSize: { type: 'number', description: 'The step size of the slider' }, + stepSize: { type: 'number', description: 'The step size of the slider' } }; const customUISchema = [ { type: 'Control', - scope: '#/properties/minimum', + scope: '#/properties/minimum' }, { type: 'Control', - scope: '#/properties/maximum', + scope: '#/properties/maximum' }, { type: 'Control', - scope: '#/properties/stepSize', - }, + scope: '#/properties/stepSize' + } ]; const produceDataSchema = (data: any) => { @@ -43,7 +43,7 @@ export const SliderProvider: React.FC = (props) => { scope: `#/properties/${data.fieldName}`, options: { slider: true, - showUnfocusedDescription: true, + showUnfocusedDescription: true } } ]; @@ -58,5 +58,5 @@ export const SliderProvider: React.FC = (props) => { }); }; - return } name="Slider" onClick={handleClick} /> + return } name="Slider" onClick={handleClick} />; }; diff --git a/packages/client/src/components/tagbuilder/TagField.component.tsx b/packages/client/src/components/tagbuilder/TagField.component.tsx index 402b23a1..ea73e3bd 100644 --- a/packages/client/src/components/tagbuilder/TagField.component.tsx +++ b/packages/client/src/components/tagbuilder/TagField.component.tsx @@ -1,5 +1,5 @@ import { JsonForms } from '@jsonforms/react'; -import Ajv, {ErrorObject} from 'ajv'; +import Ajv, { ErrorObject } from 'ajv'; import { Card, CardContent, Typography } from '@mui/material'; import { materialCells, materialRenderers } from '@jsonforms/material-renderers'; import { useState } from 'react'; @@ -37,10 +37,7 @@ export const TagFieldView: React.FC = ({ field, setFieldFragment }: <> {'Empty'} - + {field.fieldKind} diff --git a/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx b/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx index b0a0420e..b706c3e3 100644 --- a/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx +++ b/packages/client/src/components/tagbuilder/TagFormBuilder.component.tsx @@ -120,7 +120,7 @@ export const TagFormBuilder: React.FC = ({ tagSchema, setTagSc {tagFields.length > 0 ? ( tagFields.map((value: TagField, index: number) => ( - updateTagSchemaFragment(index, fragment)} /> + updateTagSchemaFragment(index, fragment)} /> + return ( + + ); }; /** @@ -31,7 +35,11 @@ export interface TagField { } /** Helper function to make the JSON form for the tag field form */ -export const produceJSONForm = (additionaProperties: { [property: string]: JsonSchema7 }, additionalUISchema: any[], additionalRequired: string[]): { dataSchema: JsonSchema, uiSchema: Layout } => { +export const produceJSONForm = ( + additionaProperties: { [property: string]: JsonSchema7 }, + additionalUISchema: any[], + additionalRequired: string[] +): { dataSchema: JsonSchema; uiSchema: Layout } => { return { dataSchema: { type: 'object', @@ -67,11 +75,11 @@ export const produceJSONForm = (additionaProperties: { [property: string]: JsonS /** How all tag field providers should be structured */ export interface TagFieldProviderProps { handleClick: (tagField: TagField) => void; -}; +} /** Representation of the final schema for a tag field */ export interface TagFieldFragmentSchema { dataSchema: { [property: string]: JsonSchema7 }; uiSchema: any[]; required: string | null; -}; +} diff --git a/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx b/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx index 1e86ff03..96c25d38 100644 --- a/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx +++ b/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx @@ -1,7 +1,7 @@ import { TagFieldProviderProps, produceJSONForm, ProviderButton } from './TagProvider.tsx'; import { AssistantPhoto } from '@mui/icons-material'; import { Dataset } from '../../graphql/graphql'; -import { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { useGetDatasetsByProjectLazyQuery } from '../../graphql/dataset/dataset.ts'; import { useProject } from '../../context/Project.context.tsx'; @@ -35,22 +35,22 @@ export const VideoRecordProvider: React.FC = (props) => { maximumOptional: { type: 'number', description: 'The maximum number of videos the user can record (including required, defaults to 1)' - }, + } }; const customUISchema = [ { type: 'Control', - scope: '#/properties/dataset', + scope: '#/properties/dataset' }, { type: 'Control', - scope: '#/properties/minimumRequired', + scope: '#/properties/minimumRequired' }, { type: 'Control', - scope: '#/properties/maximumOptional', - }, + scope: '#/properties/maximumOptional' + } ]; const produceDataSchema = (data: any) => { @@ -59,9 +59,9 @@ export const VideoRecordProvider: React.FC = (props) => { type: 'array', description: data.description, items: { - type: 'string', + type: 'string' }, - minItems: data.minimumRequired || 1, + minItems: data.minimumRequired || 1 } }; }; @@ -91,5 +91,5 @@ export const VideoRecordProvider: React.FC = (props) => { }); }; - return } name="Video Record" onClick={handleClick} /> + return } name="Video Record" onClick={handleClick} />; }; diff --git a/packages/client/src/pages/studies/NewStudy.tsx b/packages/client/src/pages/studies/NewStudy.tsx index 81d003a0..44bf3f77 100644 --- a/packages/client/src/pages/studies/NewStudy.tsx +++ b/packages/client/src/pages/studies/NewStudy.tsx @@ -109,9 +109,7 @@ export const NewStudy: React.FC = () => { return ( <> - - Create New Study - + Create New Study {steps.map((label) => { return (