diff --git a/packages/client/src/components/TagField.component.tsx b/packages/client/src/components/TagField.component.tsx deleted file mode 100644 index 32ca68f4..00000000 --- a/packages/client/src/components/TagField.component.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { JsonForms } from '@jsonforms/react'; -import Ajv from 'ajv'; -import { Card, CardContent, Container, 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 57b2e33f..00000000 --- a/packages/client/src/components/TagsDisplay.component.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import { Box, Grid, Button, Container, Typography } 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/components/tagbuilder/VideoRecordProvider.tsx b/packages/client/src/components/tagbuilder/VideoRecordProvider.tsx new file mode 100644 index 00000000..96c25d38 --- /dev/null +++ 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/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/pages/studies/NewStudy.tsx b/packages/client/src/pages/studies/NewStudy.tsx index 3b27075c..44bf3f77 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: @@ -108,44 +108,38 @@ export const NewStudy: React.FC = () => { }; return ( - - - Create New Study - - - - {steps.map((label) => { - return ( - - {label} - - ); - })} - - {activeStep === steps.length ? ( - <> - All steps completed - your new study is created - - - - - - ) : ( - <> - - {getSectionComponent()} - - - - - - - )} - - + <> + Create New Study + + {steps.map((label) => { + return ( + + {label} + + ); + })} + + {activeStep === steps.length ? ( + <> + All steps completed - your new study is created + + + + + + ) : ( + <> + {getSectionComponent()} + + + + + + )} + ); }; 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; - }; -} 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()));