From 3b0345485d8d3a178684c7dca9a6b0c8dcf5ca3c Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 20 Feb 2024 12:59:57 -0500 Subject: [PATCH 01/15] Creating dynamic tag grid view --- .../client/public/locales/en/translation.json | 2 +- packages/client/src/App.tsx | 4 +- .../src/components/SideBar.component.tsx | 2 +- .../tag/view/TagGridView.component.tsx | 59 +++++++++++++++++++ .../client/src/pages/studies/DownloadTags.tsx | 15 ----- packages/client/src/pages/studies/TagView.tsx | 12 ++++ packages/client/src/pages/tag.stories.tsx | 18 ------ packages/client/src/pages/tag.tsx | 5 -- packages/client/src/types/TagColumnView.ts | 21 +++++++ 9 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 packages/client/src/components/tag/view/TagGridView.component.tsx delete mode 100644 packages/client/src/pages/studies/DownloadTags.tsx create mode 100644 packages/client/src/pages/studies/TagView.tsx delete mode 100644 packages/client/src/pages/tag.stories.tsx delete mode 100644 packages/client/src/pages/tag.tsx create mode 100644 packages/client/src/types/TagColumnView.ts diff --git a/packages/client/public/locales/en/translation.json b/packages/client/public/locales/en/translation.json index 1a4631d1..ad1f0fc5 100644 --- a/packages/client/public/locales/en/translation.json +++ b/packages/client/public/locales/en/translation.json @@ -40,7 +40,7 @@ "newStudy": "New Study", "studyControl": "Study Control", "entryControl": "Entry Control", - "downloadTags": "Download Tags", + "viewTags": "Download Tags", "datasets": "Datasets", "datasetControl": "Dataset Control", "projectAccess": "Project Access", diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 6984421e..dd1c9730 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -13,7 +13,7 @@ import { StudyControl } from './pages/studies/StudyControl'; import { ProjectAccess } from './pages/datasets/ProjectAccess'; import { ProjectUserPermissions } from './pages/projects/ProjectUserPermissions'; import { StudyUserPermissions } from './pages/studies/UserPermissions'; -import { DownloadTags } from './pages/studies/DownloadTags'; +import { TagView } from './pages/studies/TagView'; import { DatasetControls } from './pages/datasets/DatasetControls'; import { AuthProvider, useAuth, AUTH_TOKEN_STR } from './context/Auth.context'; import { AdminGuard } from './guards/AdminGuard'; @@ -125,7 +125,7 @@ const MyRoutes: FC = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/packages/client/src/components/SideBar.component.tsx b/packages/client/src/components/SideBar.component.tsx index 5144739d..6b466b46 100644 --- a/packages/client/src/components/SideBar.component.tsx +++ b/packages/client/src/components/SideBar.component.tsx @@ -63,7 +63,7 @@ export const SideBar: FC = ({ open, drawerWidth }) => { visible: (p) => p!.studyAdmin }, { name: t('menu.entryControl'), action: () => navigate('/study/entries'), visible: (p) => p!.studyAdmin }, - { name: t('menu.downloadTags'), action: () => navigate('/study/tags'), visible: (p) => p!.studyAdmin } + { name: t('menu.viewTags'), action: () => navigate('/study/tags'), visible: (p) => p!.studyAdmin } ] }, { diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx new file mode 100644 index 00000000..e67a2cc5 --- /dev/null +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -0,0 +1,59 @@ +import { useTranslation } from 'react-i18next'; +import { TagColumnView, TagColumnViewProps, TagViewTest } from '../../../types/TagColumnView'; +import { Study } from '../../../graphql/graphql'; +import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; +import {DataGrid} from '@mui/x-data-grid'; + +export interface TagGridViewProps { + study: Study; +} + +export const TagGridView: React.FC = ({ study }) => { + const { t } = useTranslation(); + + const tagColumnViews: { tester: TagViewTest, view: TagColumnView }[] = [ + + ]; + + const columns: GridColDef[] = study.tagSchema.dataSchema.properties.map((property: string) => { + const fieldSchema = study.tagSchema.dataSchema.properties[property]; + const fieldUiSchema = study.tagSchema.uiSchema.elements.find((element: any) => element.scope === `#/properties/${property}`); + + if (!fieldSchema || !fieldUiSchema) { + throw new Error(`Could not find schema for property ${property}`); + } + + const reactNode = tagColumnViews + .filter((view) => view.tester(fieldUiSchema, fieldSchema)) + .sort((a, b) => a.tester(fieldUiSchema, fieldSchema) - b.tester(fieldUiSchema, fieldSchema)); + + if (reactNode.length === 0) { + throw new Error(`No matching view for property ${property}`); + } + + const view: React.FC = reactNode[0].view.component; + + return { + field: property, + headerName: fieldUiSchema.title || property, + editable: false, + renderCell: (params: GridRenderCellParams) => view({ data: params.row[property], schema: fieldSchema, uischema: fieldUiSchema }) + }; + }); + + return ( + 'auto'} + rows={[]} + columns={columns} + getRowId={(row) => row._id} + initialState={{ + pagination: { + paginationModel: { + pageSize: 10 + } + } + }} + /> + ); +}; diff --git a/packages/client/src/pages/studies/DownloadTags.tsx b/packages/client/src/pages/studies/DownloadTags.tsx deleted file mode 100644 index c3ff29dd..00000000 --- a/packages/client/src/pages/studies/DownloadTags.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Button, Container, Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; - -export const DownloadTags: React.FC = () => { - const { t } = useTranslation(); - - return ( - - {t('menu.downloadTags')} - - - ); -}; diff --git a/packages/client/src/pages/studies/TagView.tsx b/packages/client/src/pages/studies/TagView.tsx new file mode 100644 index 00000000..a69a0d43 --- /dev/null +++ b/packages/client/src/pages/studies/TagView.tsx @@ -0,0 +1,12 @@ +import { Container, Typography } from '@mui/material'; +import { useTranslation } from 'react-i18next'; + +export const TagView: React.FC = () => { + const { t } = useTranslation(); + + return ( + + {t('menu.viewTags')} + + ); +}; diff --git a/packages/client/src/pages/tag.stories.tsx b/packages/client/src/pages/tag.stories.tsx deleted file mode 100644 index 687f64fd..00000000 --- a/packages/client/src/pages/tag.stories.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { Meta, StoryObj } from '@storybook/react'; -import { TagPage } from './tag'; -import { ThemeProvider } from '../theme/ThemeProvider'; - -const meta: Meta = { - title: 'Tag', - component: TagPage -}; - -export default meta; -type Story = StoryObj; - -export const Primary: Story = (args: any) => ( - - - -); -Primary.args = {}; diff --git a/packages/client/src/pages/tag.tsx b/packages/client/src/pages/tag.tsx deleted file mode 100644 index ed974e41..00000000 --- a/packages/client/src/pages/tag.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { FC } from 'react'; - -export const TagPage: FC = () => { - return

Hello World

; -}; diff --git a/packages/client/src/types/TagColumnView.ts b/packages/client/src/types/TagColumnView.ts new file mode 100644 index 00000000..f54cbe3d --- /dev/null +++ b/packages/client/src/types/TagColumnView.ts @@ -0,0 +1,21 @@ +import { JsonSchema, UISchemaElement } from '@jsonforms/core'; + +export interface TagColumnViewProps { + data: any; + schema: JsonSchema; + uischema: UISchemaElement; +} + +/** + * Represents the view of a tag in a column format. Handles determining if + * the given view is applicable and applying the view to the given data. + */ +export interface TagColumnView { + component: React.FC; +} + +/** + * Test to see if a given field can be transformed into a tag column view. + */ +export type TagViewTest = (uischema: UISchemaElement, schema: JsonSchema) => number; +export const NOT_APPLICABLE = -1; From e368ed91f67dcbb1615c918909ef6edb43701ddb Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 20 Feb 2024 13:58:28 -0500 Subject: [PATCH 02/15] Working visualization of basic text --- .../tag/view/AslLexGridView.component.tsx | 0 .../tag/view/BooleanGridView.component.tsx | 0 .../view/CategoricalGridView.component.tsx | 0 .../tag/view/FreeTextGridView.component.tsx | 14 + .../tag/view/NumericGridView.component.tsx | 0 .../tag/view/SliderGridView.component.tsx | 0 .../tag/view/TagGridView.component.tsx | 35 ++- .../tag/view/VideoGridView.component.tsx | 0 packages/client/src/graphql/graphql.ts | 296 +----------------- packages/client/src/graphql/tag/tag.graphql | 20 ++ packages/client/src/graphql/tag/tag.ts | 58 +++- packages/client/src/pages/studies/TagView.tsx | 4 + packages/client/src/types/TagColumnView.ts | 4 +- .../server/src/tag/resolvers/tag.resolver.ts | 11 + .../server/src/tag/services/tag.service.ts | 4 + 15 files changed, 136 insertions(+), 310 deletions(-) create mode 100644 packages/client/src/components/tag/view/AslLexGridView.component.tsx create mode 100644 packages/client/src/components/tag/view/BooleanGridView.component.tsx create mode 100644 packages/client/src/components/tag/view/CategoricalGridView.component.tsx create mode 100644 packages/client/src/components/tag/view/FreeTextGridView.component.tsx create mode 100644 packages/client/src/components/tag/view/NumericGridView.component.tsx create mode 100644 packages/client/src/components/tag/view/SliderGridView.component.tsx create mode 100644 packages/client/src/components/tag/view/VideoGridView.component.tsx diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/components/tag/view/CategoricalGridView.component.tsx b/packages/client/src/components/tag/view/CategoricalGridView.component.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx new file mode 100644 index 00000000..6b1110df --- /dev/null +++ b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx @@ -0,0 +1,14 @@ +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE } from "../../../types/TagColumnView"; +import { materialAnyOfStringOrEnumControlTester } from "@jsonforms/material-renderers"; + +/** Visualize basic text data in a grid view */ +export const FreeTextGridView: React.FC = ({ data }) => { + return data; +} + +export const freeTextTest: TagViewTest = (uischema, schema, context) => { + if (materialAnyOfStringOrEnumControlTester(uischema, schema, context) !== NOT_APPLICABLE) { + return 1; + } + return NOT_APPLICABLE; +} diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/components/tag/view/SliderGridView.component.tsx b/packages/client/src/components/tag/view/SliderGridView.component.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index e67a2cc5..047f056d 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -2,7 +2,10 @@ import { useTranslation } from 'react-i18next'; import { TagColumnView, TagColumnViewProps, TagViewTest } from '../../../types/TagColumnView'; import { Study } from '../../../graphql/graphql'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import {DataGrid} from '@mui/x-data-grid'; +import { DataGrid } from '@mui/x-data-grid'; +import { GetTagsQuery, useGetTagsQuery } from '../../../graphql/tag/tag'; +import { useEffect, useState } from 'react'; +import { FreeTextGridView, freeTextTest} from './FreeTextGridView.component'; export interface TagGridViewProps { study: Study; @@ -10,12 +13,22 @@ export interface TagGridViewProps { export const TagGridView: React.FC = ({ study }) => { const { t } = useTranslation(); + const [tags, setTags] = useState([]); const tagColumnViews: { tester: TagViewTest, view: TagColumnView }[] = [ - + { tester: freeTextTest, view: { component: FreeTextGridView } } ]; - const columns: GridColDef[] = study.tagSchema.dataSchema.properties.map((property: string) => { + const getTagsResults = useGetTagsQuery({ variables: { study: study._id } }); + + useEffect(() => { + if (getTagsResults.data) { + setTags(getTagsResults.data.getTags); + } + }, [getTagsResults.data]); + + // Generate the dynamic columns for the grid + const columns: GridColDef[] = Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties).map((property: string) => { const fieldSchema = study.tagSchema.dataSchema.properties[property]; const fieldUiSchema = study.tagSchema.uiSchema.elements.find((element: any) => element.scope === `#/properties/${property}`); @@ -23,9 +36,10 @@ export const TagGridView: React.FC = ({ study }) => { throw new Error(`Could not find schema for property ${property}`); } + const context = { rootSchema: study.tagSchema.dataSchema, config: {} }; const reactNode = tagColumnViews - .filter((view) => view.tester(fieldUiSchema, fieldSchema)) - .sort((a, b) => a.tester(fieldUiSchema, fieldSchema) - b.tester(fieldUiSchema, fieldSchema)); + .filter((view) => view.tester(fieldUiSchema, fieldSchema, context)) + .sort((a, b) => a.tester(fieldUiSchema, fieldSchema, context) - b.tester(fieldUiSchema, fieldSchema, context)); if (reactNode.length === 0) { throw new Error(`No matching view for property ${property}`); @@ -37,23 +51,16 @@ export const TagGridView: React.FC = ({ study }) => { field: property, headerName: fieldUiSchema.title || property, editable: false, - renderCell: (params: GridRenderCellParams) => view({ data: params.row[property], schema: fieldSchema, uischema: fieldUiSchema }) + renderCell: (params: GridRenderCellParams) => view({ data: params.row.data[property], schema: fieldSchema, uischema: fieldUiSchema }) }; }); return ( 'auto'} - rows={[]} + rows={tags} columns={columns} getRowId={(row) => row._id} - initialState={{ - pagination: { - paginationModel: { - pageSize: 10 - } - } - }} /> ); }; diff --git a/packages/client/src/components/tag/view/VideoGridView.component.tsx b/packages/client/src/components/tag/view/VideoGridView.component.tsx new file mode 100644 index 00000000..e69de29b diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index 4833e4ce..cff59a77 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -18,35 +18,6 @@ export type Scalars = { JSON: { input: any; output: any; } }; -/** Input type for accepting an invite */ -export type AcceptInviteModel = { - /** The email address of the user accepting the invite */ - email: Scalars['String']['input']; - /** The full name of the user accepting the invite */ - fullname: Scalars['String']['input']; - /** The invite code that was included in the invite email */ - inviteCode: Scalars['String']['input']; - /** The password for the new user account */ - password: Scalars['String']['input']; - /** The ID of the project the invite is associated with */ - projectId: Scalars['String']['input']; -}; - -export type AccessToken = { - __typename?: 'AccessToken'; - accessToken: Scalars['String']['output']; - refreshToken: Scalars['String']['output']; -}; - -export type ConfigurableProjectSettings = { - description?: InputMaybe; - homePage?: InputMaybe; - logo?: InputMaybe; - muiTheme?: InputMaybe; - name?: InputMaybe; - redirectUrl?: InputMaybe; -}; - export type Dataset = { __typename?: 'Dataset'; _id: Scalars['ID']['output']; @@ -65,12 +36,6 @@ export type DatasetProjectPermission = { projectHasAccess: Scalars['Boolean']['output']; }; -export type EmailLoginDto = { - email: Scalars['String']['input']; - password: Scalars['String']['input']; - projectId: Scalars['String']['input']; -}; - export type Entry = { __typename?: 'Entry'; _id: Scalars['String']['output']; @@ -86,46 +51,6 @@ export type Entry = { signedUrlExpiration: Scalars['Float']['output']; }; -export type ForgotDto = { - email: Scalars['String']['input']; - projectId: Scalars['String']['input']; -}; - -export type GoogleLoginDto = { - credential: Scalars['String']['input']; - projectId: Scalars['String']['input']; -}; - -export type InviteModel = { - __typename?: 'InviteModel'; - /** The date and time at which the invitation was created. */ - createdAt: Scalars['DateTime']['output']; - /** The date and time at which the invitation was deleted, if applicable. */ - deletedAt?: Maybe; - /** The email address of the user being invited. */ - email: Scalars['String']['output']; - /** The date and time at which the invitation expires. */ - expiresAt: Scalars['DateTime']['output']; - /** The ID of the invitation. */ - id: Scalars['ID']['output']; - /** The ID of the project to which the invitation belongs. */ - projectId: Scalars['String']['output']; - /** The role that the user being invited will have. */ - role: Scalars['Int']['output']; - /** The status of the invitation. */ - status: InviteStatus; - /** The date and time at which the invitation was last updated. */ - updatedAt: Scalars['DateTime']['output']; -}; - -/** The status of an invite */ -export enum InviteStatus { - Accepted = 'ACCEPTED', - Cancelled = 'CANCELLED', - Expired = 'EXPIRED', - Pending = 'PENDING' -} - /** Represents an entier lexicon */ export type Lexicon = { __typename?: 'Lexicon'; @@ -176,9 +101,7 @@ export type LexiconEntry = { export type Mutation = { __typename?: 'Mutation'; - acceptInvite: InviteModel; assignTag?: Maybe; - cancelInvite: InviteModel; changeDatasetDescription: Scalars['Boolean']['output']; changeDatasetName: Scalars['Boolean']['output']; changeStudyDescription: Study; @@ -186,16 +109,13 @@ export type Mutation = { completeTag: Scalars['Boolean']['output']; completeUploadSession: UploadResult; createDataset: Dataset; - createInvite: InviteModel; createOrganization: Organization; - createProject: ProjectModel; createStudy: Study; createTags: Array; createUploadSession: UploadSession; deleteEntry: Scalars['Boolean']['output']; deleteProject: Scalars['Boolean']['output']; deleteStudy: Scalars['Boolean']['output']; - forgotPassword: Scalars['Boolean']['output']; grantContributor: Scalars['Boolean']['output']; grantOwner: Scalars['Boolean']['output']; grantProjectDatasetAccess: Scalars['Boolean']['output']; @@ -206,25 +126,9 @@ export type Mutation = { /** Remove all entries from a given lexicon */ lexiconClearEntries: Scalars['Boolean']['output']; lexiconCreate: Lexicon; - loginEmail: AccessToken; - loginGoogle: AccessToken; - loginUsername: AccessToken; - refresh: AccessToken; - resendInvite: InviteModel; - resetPassword: Scalars['Boolean']['output']; saveVideoField: VideoField; setEntryEnabled: Scalars['Boolean']['output']; signLabCreateProject: Project; - signup: AccessToken; - updateProject: ProjectModel; - updateProjectAuthMethods: ProjectModel; - updateProjectSettings: ProjectModel; - updateUser: UserModel; -}; - - -export type MutationAcceptInviteArgs = { - input: AcceptInviteModel; }; @@ -233,11 +137,6 @@ export type MutationAssignTagArgs = { }; -export type MutationCancelInviteArgs = { - id: Scalars['ID']['input']; -}; - - export type MutationChangeDatasetDescriptionArgs = { dataset: Scalars['ID']['input']; newDescription: Scalars['String']['input']; @@ -278,22 +177,11 @@ export type MutationCreateDatasetArgs = { }; -export type MutationCreateInviteArgs = { - email: Scalars['String']['input']; - role?: InputMaybe; -}; - - export type MutationCreateOrganizationArgs = { organization: OrganizationCreate; }; -export type MutationCreateProjectArgs = { - project: ProjectCreateInput; -}; - - export type MutationCreateStudyArgs = { study: StudyCreate; }; @@ -325,11 +213,6 @@ export type MutationDeleteStudyArgs = { }; -export type MutationForgotPasswordArgs = { - user: ForgotDto; -}; - - export type MutationGrantContributorArgs = { isContributor: Scalars['Boolean']['input']; study: Scalars['ID']['input']; @@ -385,36 +268,6 @@ export type MutationLexiconCreateArgs = { }; -export type MutationLoginEmailArgs = { - user: EmailLoginDto; -}; - - -export type MutationLoginGoogleArgs = { - user: GoogleLoginDto; -}; - - -export type MutationLoginUsernameArgs = { - user: UsernameLoginDto; -}; - - -export type MutationRefreshArgs = { - refreshToken: Scalars['String']['input']; -}; - - -export type MutationResendInviteArgs = { - id: Scalars['ID']['input']; -}; - - -export type MutationResetPasswordArgs = { - user: ResetDto; -}; - - export type MutationSaveVideoFieldArgs = { field: Scalars['String']['input']; index: Scalars['Int']['input']; @@ -433,35 +286,6 @@ export type MutationSignLabCreateProjectArgs = { project: ProjectCreate; }; - -export type MutationSignupArgs = { - user: UserSignupDto; -}; - - -export type MutationUpdateProjectArgs = { - id: Scalars['String']['input']; - settings: ConfigurableProjectSettings; -}; - - -export type MutationUpdateProjectAuthMethodsArgs = { - id: Scalars['String']['input']; - projectAuthMethods: ProjectAuthMethodsInput; -}; - - -export type MutationUpdateProjectSettingsArgs = { - id: Scalars['String']['input']; - projectSettings: ProjectSettingsInput; -}; - - -export type MutationUpdateUserArgs = { - email: Scalars['String']['input']; - fullname: Scalars['String']['input']; -}; - export type Organization = { __typename?: 'Organization'; _id: Scalars['ID']['output']; @@ -498,52 +322,11 @@ export type Project = { name: Scalars['String']['output']; }; -export type ProjectAuthMethodsInput = { - emailAuth?: InputMaybe; - googleAuth?: InputMaybe; -}; - -export type ProjectAuthMethodsModel = { - __typename?: 'ProjectAuthMethodsModel'; - emailAuth: Scalars['Boolean']['output']; - googleAuth: Scalars['Boolean']['output']; -}; - export type ProjectCreate = { description: Scalars['String']['input']; name: Scalars['String']['input']; }; -export type ProjectCreateInput = { - allowSignup?: InputMaybe; - description?: InputMaybe; - displayProjectName?: InputMaybe; - emailAuth?: InputMaybe; - googleAuth?: InputMaybe; - homePage?: InputMaybe; - logo?: InputMaybe; - muiTheme?: InputMaybe; - name: Scalars['String']['input']; - redirectUrl?: InputMaybe; -}; - -export type ProjectModel = { - __typename?: 'ProjectModel'; - authMethods: ProjectAuthMethodsModel; - createdAt: Scalars['DateTime']['output']; - deletedAt?: Maybe; - description?: Maybe; - homePage?: Maybe; - id: Scalars['ID']['output']; - logo?: Maybe; - muiTheme: Scalars['JSON']['output']; - name: Scalars['String']['output']; - redirectUrl?: Maybe; - settings: ProjectSettingsModel; - updatedAt: Scalars['DateTime']['output']; - users: Array; -}; - export type ProjectPermissionModel = { __typename?: 'ProjectPermissionModel'; editable: Scalars['Boolean']['output']; @@ -551,17 +334,6 @@ export type ProjectPermissionModel = { user: User; }; -export type ProjectSettingsInput = { - allowSignup?: InputMaybe; - displayProjectName?: InputMaybe; -}; - -export type ProjectSettingsModel = { - __typename?: 'ProjectSettingsModel'; - allowSignup: Scalars['Boolean']['output']; - displayProjectName: Scalars['Boolean']['output']; -}; - export type Query = { __typename?: 'Query'; datasetExists: Scalars['Boolean']['output']; @@ -575,25 +347,17 @@ export type Query = { getDatasetsByProject: Array; getEntryUploadURL: Scalars['String']['output']; getOrganizations: Array; - getProject: ProjectModel; getProjectPermissions: Array; getProjects: Array; getRoles: Permission; getStudyPermissions: Array; - getUser: UserModel; - invite: InviteModel; - invites: Array; + getTags: Array; isEntryEnabled: Scalars['Boolean']['output']; lexFindAll: Array; lexiconByKey: LexiconEntry; lexiconSearch: Array; - listProjects: Array; - me: UserModel; projectExists: Scalars['Boolean']['output']; - projectUsers: Array; - publicKey: Array; studyExists: Scalars['Boolean']['output']; - users: Array; validateCSV: UploadResult; }; @@ -640,11 +404,6 @@ export type QueryGetEntryUploadUrlArgs = { }; -export type QueryGetProjectArgs = { - id: Scalars['String']['input']; -}; - - export type QueryGetProjectPermissionsArgs = { project: Scalars['ID']['input']; }; @@ -661,18 +420,8 @@ export type QueryGetStudyPermissionsArgs = { }; -export type QueryGetUserArgs = { - id: Scalars['ID']['input']; -}; - - -export type QueryInviteArgs = { - id: Scalars['ID']['input']; -}; - - -export type QueryInvitesArgs = { - status?: InputMaybe; +export type QueryGetTagsArgs = { + study: Scalars['ID']['input']; }; @@ -699,11 +448,6 @@ export type QueryProjectExistsArgs = { }; -export type QueryProjectUsersArgs = { - projectId: Scalars['String']['input']; -}; - - export type QueryStudyExistsArgs = { name: Scalars['String']['input']; project: Scalars['ID']['input']; @@ -714,13 +458,6 @@ export type QueryValidateCsvArgs = { session: Scalars['ID']['input']; }; -export type ResetDto = { - code: Scalars['String']['input']; - email: Scalars['String']['input']; - password: Scalars['String']['input']; - projectId: Scalars['String']['input']; -}; - export type Study = { __typename?: 'Study'; _id: Scalars['ID']['output']; @@ -807,33 +544,6 @@ export type User = { uid: Scalars['String']['output']; }; -export type UserModel = { - __typename?: 'UserModel'; - createdAt: Scalars['DateTime']['output']; - deletedAt?: Maybe; - email?: Maybe; - fullname?: Maybe; - id: Scalars['ID']['output']; - projectId: Scalars['String']['output']; - role: Scalars['Int']['output']; - updatedAt: Scalars['DateTime']['output']; - username?: Maybe; -}; - -export type UserSignupDto = { - email: Scalars['String']['input']; - fullname: Scalars['String']['input']; - password: Scalars['String']['input']; - projectId: Scalars['String']['input']; - username?: InputMaybe; -}; - -export type UsernameLoginDto = { - password: Scalars['String']['input']; - projectId: Scalars['String']['input']; - username: Scalars['String']['input']; -}; - export type VideoField = { __typename?: 'VideoField'; _id: Scalars['String']['output']; diff --git a/packages/client/src/graphql/tag/tag.graphql b/packages/client/src/graphql/tag/tag.graphql index bcd89b97..d30a7514 100644 --- a/packages/client/src/graphql/tag/tag.graphql +++ b/packages/client/src/graphql/tag/tag.graphql @@ -40,3 +40,23 @@ mutation saveVideoField($tag: ID!, $field: String!, $index: Int!) { uploadURL } } + +query getTags($study: ID!) { + getTags(study: $study) { + _id + entry { + _id + organization + entryID + contentType + dataset + creator + dateCreated + meta + signedUrl + signedUrlExpiration + } + data + complete + } +} diff --git a/packages/client/src/graphql/tag/tag.ts b/packages/client/src/graphql/tag/tag.ts index 58b7f376..d3c4b3b5 100644 --- a/packages/client/src/graphql/tag/tag.ts +++ b/packages/client/src/graphql/tag/tag.ts @@ -54,6 +54,13 @@ export type SaveVideoFieldMutationVariables = Types.Exact<{ export type SaveVideoFieldMutation = { __typename?: 'Mutation', saveVideoField: { __typename?: 'VideoField', _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, data?: any | null, complete: boolean, entry: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }> }; + export const CreateTagsDocument = gql` mutation createTags($study: ID!, $entries: [ID!]!) { @@ -268,4 +275,53 @@ export function useSaveVideoFieldMutation(baseOptions?: Apollo.MutationHookOptio } export type SaveVideoFieldMutationHookResult = ReturnType; export type SaveVideoFieldMutationResult = Apollo.MutationResult; -export type SaveVideoFieldMutationOptions = Apollo.BaseMutationOptions; \ No newline at end of file +export type SaveVideoFieldMutationOptions = Apollo.BaseMutationOptions; +export const GetTagsDocument = gql` + query getTags($study: ID!) { + getTags(study: $study) { + _id + entry { + _id + organization + entryID + contentType + dataset + creator + dateCreated + meta + signedUrl + signedUrlExpiration + } + data + complete + } +} + `; + +/** + * __useGetTagsQuery__ + * + * To run a query within a React component, call `useGetTagsQuery` and pass it any options that fit your needs. + * When your component renders, `useGetTagsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetTagsQuery({ + * variables: { + * study: // value for 'study' + * }, + * }); + */ +export function useGetTagsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetTagsDocument, options); + } +export function useGetTagsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetTagsDocument, options); + } +export type GetTagsQueryHookResult = ReturnType; +export type GetTagsLazyQueryHookResult = ReturnType; +export type GetTagsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/packages/client/src/pages/studies/TagView.tsx b/packages/client/src/pages/studies/TagView.tsx index a69a0d43..09879e1e 100644 --- a/packages/client/src/pages/studies/TagView.tsx +++ b/packages/client/src/pages/studies/TagView.tsx @@ -1,12 +1,16 @@ import { Container, Typography } from '@mui/material'; import { useTranslation } from 'react-i18next'; +import { useStudy } from '../../context/Study.context'; +import { TagGridView } from '../../components/tag/view/TagGridView.component'; export const TagView: React.FC = () => { const { t } = useTranslation(); + const { study } = useStudy(); return ( {t('menu.viewTags')} + {study && } ); }; diff --git a/packages/client/src/types/TagColumnView.ts b/packages/client/src/types/TagColumnView.ts index f54cbe3d..3bc1bd9e 100644 --- a/packages/client/src/types/TagColumnView.ts +++ b/packages/client/src/types/TagColumnView.ts @@ -1,4 +1,4 @@ -import { JsonSchema, UISchemaElement } from '@jsonforms/core'; +import { JsonSchema, TesterContext, UISchemaElement } from '@jsonforms/core'; export interface TagColumnViewProps { data: any; @@ -17,5 +17,5 @@ export interface TagColumnView { /** * Test to see if a given field can be transformed into a tag column view. */ -export type TagViewTest = (uischema: UISchemaElement, schema: JsonSchema) => number; +export type TagViewTest = (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext) => number; export const NOT_APPLICABLE = -1; diff --git a/packages/server/src/tag/resolvers/tag.resolver.ts b/packages/server/src/tag/resolvers/tag.resolver.ts index 3da04202..07a892e9 100644 --- a/packages/server/src/tag/resolvers/tag.resolver.ts +++ b/packages/server/src/tag/resolvers/tag.resolver.ts @@ -85,6 +85,17 @@ export class TagResolver { return this.tagService.isEntryEnabled(study, entry); } + @Query(() => [Tag]) + async getTags( + @Args('study', { type: () => ID }, StudyPipe) study: Study, + @TokenContext() user: TokenPayload + ): Promise { + if (!(await this.enforcer.enforce(user.user_id, TagPermissions.READ, study._id.toString()))) { + throw new UnauthorizedException('User cannot read tags in this study'); + } + return this.tagService.getTags(study); + } + @ResolveField(() => Entry) async entry(@Parent() tag: Tag): Promise { return this.entryPipe.transform(tag.entry); diff --git a/packages/server/src/tag/services/tag.service.ts b/packages/server/src/tag/services/tag.service.ts index 97f02ced..7f1e2a89 100644 --- a/packages/server/src/tag/services/tag.service.ts +++ b/packages/server/src/tag/services/tag.service.ts @@ -144,6 +144,10 @@ export class TagService { return true; } + async getTags(study: Study): Promise { + return this.tagModel.find({ study: study._id }); + } + private async getIncomplete(study: Study, user: string): Promise { return this.tagModel.findOne({ study: study._id, user, complete: false, enabled: true }); } From 3a93000c6cf5ae14ef1afc73257dd53d152c4a52 Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 20 Feb 2024 14:33:25 -0500 Subject: [PATCH 03/15] Integrate entry visualization into tag view --- .../tag/view/TagGridView.component.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 047f056d..e6c37355 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -1,11 +1,12 @@ import { useTranslation } from 'react-i18next'; import { TagColumnView, TagColumnViewProps, TagViewTest } from '../../../types/TagColumnView'; -import { Study } from '../../../graphql/graphql'; +import { Study, Entry } from '../../../graphql/graphql'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid'; import { GetTagsQuery, useGetTagsQuery } from '../../../graphql/tag/tag'; import { useEffect, useState } from 'react'; import { FreeTextGridView, freeTextTest} from './FreeTextGridView.component'; +import { EntryView } from '../../EntryView.component'; export interface TagGridViewProps { study: Study; @@ -27,8 +28,17 @@ export const TagGridView: React.FC = ({ study }) => { } }, [getTagsResults.data]); + const entryColumns: GridColDef[] = [ + { + field: 'entryView', + headerName: t('common.view'), + width: 300, + renderCell: (params: GridRenderCellParams) => + } + ]; + // Generate the dynamic columns for the grid - const columns: GridColDef[] = Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties).map((property: string) => { + const dataColunms: GridColDef[] = Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties).map((property: string) => { const fieldSchema = study.tagSchema.dataSchema.properties[property]; const fieldUiSchema = study.tagSchema.uiSchema.elements.find((element: any) => element.scope === `#/properties/${property}`); @@ -59,7 +69,7 @@ export const TagGridView: React.FC = ({ study }) => { 'auto'} rows={tags} - columns={columns} + columns={entryColumns.concat(dataColunms)} getRowId={(row) => row._id} /> ); From 0882bd0815448e053d55bf7c89890537a7f09e20 Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 20 Feb 2024 14:42:08 -0500 Subject: [PATCH 04/15] View if the tag is complete or not --- .../client/public/locales/en/translation.json | 6 +++++- .../components/tag/view/TagGridView.component.tsx | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/client/public/locales/en/translation.json b/packages/client/public/locales/en/translation.json index ad1f0fc5..18da1a68 100644 --- a/packages/client/public/locales/en/translation.json +++ b/packages/client/public/locales/en/translation.json @@ -20,7 +20,8 @@ "view": "View", "entryId": "Entry ID", "login": "Login", - "clear": "clear" + "clear": "clear", + "complete": "complete" }, "languages": { "en": "English", @@ -117,6 +118,9 @@ "login": { "selectOrg": "Select an Organization to Login", "redirectToOrg": "Redirect to Organization Login" + }, + "tagView": { + "originalEntry": "Original Entry" } } } diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index e6c37355..ff5923a7 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -7,6 +7,7 @@ import { GetTagsQuery, useGetTagsQuery } from '../../../graphql/tag/tag'; import { useEffect, useState } from 'react'; import { FreeTextGridView, freeTextTest} from './FreeTextGridView.component'; import { EntryView } from '../../EntryView.component'; +import { Checkbox } from '@mui/material'; export interface TagGridViewProps { study: Study; @@ -31,12 +32,20 @@ export const TagGridView: React.FC = ({ study }) => { const entryColumns: GridColDef[] = [ { field: 'entryView', - headerName: t('common.view'), + headerName: t('components.tagView.originalEntry'), width: 300, renderCell: (params: GridRenderCellParams) => } ]; + const tagMetaColumns: GridColDef[] = [ + { + field: 'complete', + headerName: t('common.complete'), + renderCell: (params: GridRenderCellParams) => + } + ]; + // Generate the dynamic columns for the grid const dataColunms: GridColDef[] = Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties).map((property: string) => { const fieldSchema = study.tagSchema.dataSchema.properties[property]; @@ -61,7 +70,7 @@ export const TagGridView: React.FC = ({ study }) => { field: property, headerName: fieldUiSchema.title || property, editable: false, - renderCell: (params: GridRenderCellParams) => view({ data: params.row.data[property], schema: fieldSchema, uischema: fieldUiSchema }) + renderCell: (params: GridRenderCellParams) => params.row.data && view({ data: params.row.data[property], schema: fieldSchema, uischema: fieldUiSchema }) }; }); @@ -69,7 +78,7 @@ export const TagGridView: React.FC = ({ study }) => { 'auto'} rows={tags} - columns={entryColumns.concat(dataColunms)} + columns={entryColumns.concat(tagMetaColumns).concat(dataColunms)} getRowId={(row) => row._id} /> ); From 524441ac416643026e4c0a22ce8cf01944a93ca8 Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 20 Feb 2024 15:02:13 -0500 Subject: [PATCH 05/15] Create views for fundamental types --- .../tag/view/BooleanGridView.component.tsx | 15 +++++++++++++++ .../tag/view/NumericGridView.component.tsx | 14 ++++++++++++++ .../tag/view/SliderGridView.component.tsx | 14 ++++++++++++++ .../components/tag/view/TagGridView.component.tsx | 8 +++++++- 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx index e69de29b..ac7443a8 100644 --- a/packages/client/src/components/tag/view/BooleanGridView.component.tsx +++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx @@ -0,0 +1,15 @@ +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE } from '../../../types/TagColumnView'; +import { materialBooleanControlTester } from '@jsonforms/material-renderers'; +import { Checkbox } from '@mui/material'; + +/** Visualize basic text data in a grid view */ +export const BooleanGridView: React.FC = ({ data }) => { + return ; +} + +export const booleanTest: TagViewTest = (uischema, schema, context) => { + if (materialBooleanControlTester(uischema, schema, context) !== NOT_APPLICABLE) { + return 1; + } + return NOT_APPLICABLE; +} diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx index e69de29b..58c2bb80 100644 --- a/packages/client/src/components/tag/view/NumericGridView.component.tsx +++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx @@ -0,0 +1,14 @@ +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE } from "../../../types/TagColumnView"; +import { materialNumberControlTester } from "@jsonforms/material-renderers"; + +/** Visualize basic text data in a grid view */ +export const NumericGridView: React.FC = ({ data }) => { + return data; +} + +export const numericTest: TagViewTest = (uischema, schema, context) => { + if (materialNumberControlTester(uischema, schema, context) !== NOT_APPLICABLE) { + return 1; + } + return NOT_APPLICABLE; +} diff --git a/packages/client/src/components/tag/view/SliderGridView.component.tsx b/packages/client/src/components/tag/view/SliderGridView.component.tsx index e69de29b..35d49e84 100644 --- a/packages/client/src/components/tag/view/SliderGridView.component.tsx +++ b/packages/client/src/components/tag/view/SliderGridView.component.tsx @@ -0,0 +1,14 @@ +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE } from '../../../types/TagColumnView'; +import { materialSliderControlTester } from '@jsonforms/material-renderers'; + +/** Visualize basic text data in a grid view */ +export const SliderGridView: React.FC = ({ data }) => { + return data; +} + +export const sliderTest: TagViewTest = (uischema, schema, context) => { + if (materialSliderControlTester(uischema, schema, context) !== NOT_APPLICABLE) { + return 2; + } + return NOT_APPLICABLE; +} diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index ff5923a7..98d30dd2 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -8,6 +8,9 @@ import { useEffect, useState } from 'react'; import { FreeTextGridView, freeTextTest} from './FreeTextGridView.component'; import { EntryView } from '../../EntryView.component'; import { Checkbox } from '@mui/material'; +import { NumericGridView, numericTest } from './NumericGridView.component'; +import { sliderTest, SliderGridView } from './SliderGridView.component'; +import { booleanTest, BooleanGridView } from './BooleanGridView.component'; export interface TagGridViewProps { study: Study; @@ -18,7 +21,10 @@ export const TagGridView: React.FC = ({ study }) => { const [tags, setTags] = useState([]); const tagColumnViews: { tester: TagViewTest, view: TagColumnView }[] = [ - { tester: freeTextTest, view: { component: FreeTextGridView } } + { tester: freeTextTest, view: { component: FreeTextGridView } }, + { tester: numericTest, view: { component: NumericGridView } }, + { tester: sliderTest, view: { component: SliderGridView } }, + { tester: booleanTest, view: { component: BooleanGridView } } ]; const getTagsResults = useGetTagsQuery({ variables: { study: study._id } }); From 7fc813284bca96006cb6ec3342d46065e8a83533 Mon Sep 17 00:00:00 2001 From: cbolles Date: Tue, 20 Feb 2024 16:48:36 -0500 Subject: [PATCH 06/15] Working visualization of basic fields --- .../src/components/tag/view/BooleanGridView.component.tsx | 2 +- .../src/components/tag/view/NumericGridView.component.tsx | 2 +- .../client/src/components/tag/view/TagGridView.component.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx index ac7443a8..52f2d312 100644 --- a/packages/client/src/components/tag/view/BooleanGridView.component.tsx +++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx @@ -9,7 +9,7 @@ export const BooleanGridView: React.FC = ({ data }) => { export const booleanTest: TagViewTest = (uischema, schema, context) => { if (materialBooleanControlTester(uischema, schema, context) !== NOT_APPLICABLE) { - return 1; + return 2; } return NOT_APPLICABLE; } diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx index 58c2bb80..61117556 100644 --- a/packages/client/src/components/tag/view/NumericGridView.component.tsx +++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx @@ -8,7 +8,7 @@ export const NumericGridView: React.FC = ({ data }) => { export const numericTest: TagViewTest = (uischema, schema, context) => { if (materialNumberControlTester(uischema, schema, context) !== NOT_APPLICABLE) { - return 1; + return 2; } return NOT_APPLICABLE; } diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 98d30dd2..400edad7 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -64,7 +64,7 @@ export const TagGridView: React.FC = ({ study }) => { const context = { rootSchema: study.tagSchema.dataSchema, config: {} }; const reactNode = tagColumnViews .filter((view) => view.tester(fieldUiSchema, fieldSchema, context)) - .sort((a, b) => a.tester(fieldUiSchema, fieldSchema, context) - b.tester(fieldUiSchema, fieldSchema, context)); + .sort((a, b) => b.tester(fieldUiSchema, fieldSchema, context) - a.tester(fieldUiSchema, fieldSchema, context)); if (reactNode.length === 0) { throw new Error(`No matching view for property ${property}`); From 8b7f72dee95618e4e748e5dc49a6d2d101c6dd65 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 11:02:13 -0500 Subject: [PATCH 07/15] Adjust code to support multiple columns per tag field --- .../tag/view/FreeTextGridView.component.tsx | 12 +++++++-- .../tag/view/TagGridView.component.tsx | 27 +++++++------------ packages/client/src/types/TagColumnView.ts | 5 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx index 6b1110df..a7387832 100644 --- a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx +++ b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx @@ -1,8 +1,8 @@ -import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE } from "../../../types/TagColumnView"; +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from "../../../types/TagColumnView"; import { materialAnyOfStringOrEnumControlTester } from "@jsonforms/material-renderers"; /** Visualize basic text data in a grid view */ -export const FreeTextGridView: React.FC = ({ data }) => { +const FreeTextGridView: React.FC = ({ data }) => { return data; } @@ -12,3 +12,11 @@ export const freeTextTest: TagViewTest = (uischema, schema, context) => { } return NOT_APPLICABLE; } + +export const getTextCols: GetGridColDefs = (uischema, schema, property) => { + return [{ + field: property, + headerName: property, + renderCell: (params) => params.row.data && + }] +} diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 400edad7..adb2077a 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -1,11 +1,11 @@ import { useTranslation } from 'react-i18next'; -import { TagColumnView, TagColumnViewProps, TagViewTest } from '../../../types/TagColumnView'; +import { GetGridColDefs, TagViewTest } from '../../../types/TagColumnView'; import { Study, Entry } from '../../../graphql/graphql'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid'; import { GetTagsQuery, useGetTagsQuery } from '../../../graphql/tag/tag'; import { useEffect, useState } from 'react'; -import { FreeTextGridView, freeTextTest} from './FreeTextGridView.component'; +import { freeTextTest, getTextCols } from './FreeTextGridView.component'; import { EntryView } from '../../EntryView.component'; import { Checkbox } from '@mui/material'; import { NumericGridView, numericTest } from './NumericGridView.component'; @@ -20,12 +20,12 @@ export const TagGridView: React.FC = ({ study }) => { const { t } = useTranslation(); const [tags, setTags] = useState([]); - const tagColumnViews: { tester: TagViewTest, view: TagColumnView }[] = [ - { tester: freeTextTest, view: { component: FreeTextGridView } }, - { tester: numericTest, view: { component: NumericGridView } }, - { tester: sliderTest, view: { component: SliderGridView } }, - { tester: booleanTest, view: { component: BooleanGridView } } - ]; + const tagColumnViews: { tester: TagViewTest, getGridColDefs: GetGridColDefs }[] = [ + { tester: freeTextTest, getGridColDefs: getTextCols }, ]; + // { tester: numericTest, view: { component: NumericGridView } }, + // { tester: sliderTest, view: { component: SliderGridView } }, + // { tester: booleanTest, view: { component: BooleanGridView } } + // ]; const getTagsResults = useGetTagsQuery({ variables: { study: study._id } }); @@ -70,15 +70,8 @@ export const TagGridView: React.FC = ({ study }) => { throw new Error(`No matching view for property ${property}`); } - const view: React.FC = reactNode[0].view.component; - - return { - field: property, - headerName: fieldUiSchema.title || property, - editable: false, - renderCell: (params: GridRenderCellParams) => params.row.data && view({ data: params.row.data[property], schema: fieldSchema, uischema: fieldUiSchema }) - }; - }); + return reactNode[0].getGridColDefs(fieldUiSchema, fieldSchema, property); + }).flat(); return ( ; -} +export type GetGridColDefs = (uischema: UISchemaElement, schema: JsonSchema, property: string) => GridColDef[]; /** * Test to see if a given field can be transformed into a tag column view. From 2274cfce08c628e73724ed225267898932631bf4 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 11:39:54 -0500 Subject: [PATCH 08/15] Transition views to leverage multicolumn support --- .../tag/view/BooleanGridView.component.tsx | 12 ++++++++++-- .../tag/view/NumericGridView.component.tsx | 12 ++++++++++-- .../tag/view/SliderGridView.component.tsx | 12 ++++++++++-- .../tag/view/TagGridView.component.tsx | 16 ++++++++-------- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx index 52f2d312..04059b50 100644 --- a/packages/client/src/components/tag/view/BooleanGridView.component.tsx +++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx @@ -1,9 +1,9 @@ -import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE } from '../../../types/TagColumnView'; +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView'; import { materialBooleanControlTester } from '@jsonforms/material-renderers'; import { Checkbox } from '@mui/material'; /** Visualize basic text data in a grid view */ -export const BooleanGridView: React.FC = ({ data }) => { +const BooleanGridView: React.FC = ({ data }) => { return ; } @@ -13,3 +13,11 @@ export const booleanTest: TagViewTest = (uischema, schema, context) => { } return NOT_APPLICABLE; } + +export const getBoolCols: GetGridColDefs = (uischema, schema, property) => { + return [{ + field: property, + headerName: property, + renderCell: (params) => params.row.data && + }] +} diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx index 61117556..766f6498 100644 --- a/packages/client/src/components/tag/view/NumericGridView.component.tsx +++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx @@ -1,8 +1,8 @@ -import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE } from "../../../types/TagColumnView"; +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from "../../../types/TagColumnView"; import { materialNumberControlTester } from "@jsonforms/material-renderers"; /** Visualize basic text data in a grid view */ -export const NumericGridView: React.FC = ({ data }) => { +const NumericGridView: React.FC = ({ data }) => { return data; } @@ -12,3 +12,11 @@ export const numericTest: TagViewTest = (uischema, schema, context) => { } return NOT_APPLICABLE; } + +export const getNumericCols: GetGridColDefs = (uischema, schema, property) => { + return [{ + field: property, + headerName: property, + renderCell: (params) => params.row.data && + }] +} diff --git a/packages/client/src/components/tag/view/SliderGridView.component.tsx b/packages/client/src/components/tag/view/SliderGridView.component.tsx index 35d49e84..705e3641 100644 --- a/packages/client/src/components/tag/view/SliderGridView.component.tsx +++ b/packages/client/src/components/tag/view/SliderGridView.component.tsx @@ -1,8 +1,8 @@ -import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE } from '../../../types/TagColumnView'; +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView'; import { materialSliderControlTester } from '@jsonforms/material-renderers'; /** Visualize basic text data in a grid view */ -export const SliderGridView: React.FC = ({ data }) => { +const SliderGridView: React.FC = ({ data }) => { return data; } @@ -12,3 +12,11 @@ export const sliderTest: TagViewTest = (uischema, schema, context) => { } return NOT_APPLICABLE; } + +export const getSliderCols: GetGridColDefs = (uischema, schema, property) => { + return [{ + field: property, + headerName: property, + renderCell: (params) => params.row.data && + }] +} diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index adb2077a..a1e13553 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -8,9 +8,9 @@ import { useEffect, useState } from 'react'; import { freeTextTest, getTextCols } from './FreeTextGridView.component'; import { EntryView } from '../../EntryView.component'; import { Checkbox } from '@mui/material'; -import { NumericGridView, numericTest } from './NumericGridView.component'; -import { sliderTest, SliderGridView } from './SliderGridView.component'; -import { booleanTest, BooleanGridView } from './BooleanGridView.component'; +import { getNumericCols, numericTest } from './NumericGridView.component'; +import { getSliderCols, sliderTest } from './SliderGridView.component'; +import { getBoolCols, booleanTest } from './BooleanGridView.component'; export interface TagGridViewProps { study: Study; @@ -21,11 +21,11 @@ export const TagGridView: React.FC = ({ study }) => { const [tags, setTags] = useState([]); const tagColumnViews: { tester: TagViewTest, getGridColDefs: GetGridColDefs }[] = [ - { tester: freeTextTest, getGridColDefs: getTextCols }, ]; - // { tester: numericTest, view: { component: NumericGridView } }, - // { tester: sliderTest, view: { component: SliderGridView } }, - // { tester: booleanTest, view: { component: BooleanGridView } } - // ]; + { tester: freeTextTest, getGridColDefs: getTextCols }, + { tester: numericTest, getGridColDefs: getNumericCols }, + { tester: sliderTest, getGridColDefs: getSliderCols }, + { tester: booleanTest, getGridColDefs: getBoolCols } + ]; const getTagsResults = useGetTagsQuery({ variables: { study: study._id } }); From bd9e7daadb0ab7930f6751bbd8a874d6f6419ad7 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 12:15:14 -0500 Subject: [PATCH 09/15] Begin work on ASL-LEX field visualization --- .../tag/view/AslLexGridView.component.tsx | 20 +++++++++++++++++++ .../tag/view/TagGridView.component.tsx | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index e69de29b..72014086 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -0,0 +1,20 @@ +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView'; + +const AslLexGridView: React.FC = ({ data }) => { + return 'hello world'; +} + +export const aslLexTest: TagViewTest = (uischema, _schema, _context) => { + if (uischema.options != undefined && uischema.options.customType != undefined && uischema.options.customType == 'asl-lex') { + return 5; + } + return NOT_APPLICABLE; +} + +export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { + return [{ + field: property, + headerName: property, + renderCell: (params) => params.row.data && + }] +} diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index a1e13553..0a2d558e 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -11,6 +11,7 @@ import { Checkbox } from '@mui/material'; import { getNumericCols, numericTest } from './NumericGridView.component'; import { getSliderCols, sliderTest } from './SliderGridView.component'; import { getBoolCols, booleanTest } from './BooleanGridView.component'; +import { aslLexTest, getAslLexCols } from './AslLexGridView.component'; export interface TagGridViewProps { study: Study; @@ -24,7 +25,8 @@ export const TagGridView: React.FC = ({ study }) => { { tester: freeTextTest, getGridColDefs: getTextCols }, { tester: numericTest, getGridColDefs: getNumericCols }, { tester: sliderTest, getGridColDefs: getSliderCols }, - { tester: booleanTest, getGridColDefs: getBoolCols } + { tester: booleanTest, getGridColDefs: getBoolCols }, + { tester: aslLexTest, getGridColDefs: getAslLexCols } ]; const getTagsResults = useGetTagsQuery({ variables: { study: study._id } }); From a9f792d28ee8d35b74deaf9160d3df3c4522e15e Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 12:29:44 -0500 Subject: [PATCH 10/15] Pull out video visualization component --- .../src/components/EntryView.component.tsx | 95 +----------------- .../src/components/VideoView.component.tsx | 96 +++++++++++++++++++ packages/client/src/types/EntryView.ts | 0 3 files changed, 99 insertions(+), 92 deletions(-) create mode 100644 packages/client/src/components/VideoView.component.tsx create mode 100644 packages/client/src/types/EntryView.ts diff --git a/packages/client/src/components/EntryView.component.tsx b/packages/client/src/components/EntryView.component.tsx index 2be4d788..59071e5f 100644 --- a/packages/client/src/components/EntryView.component.tsx +++ b/packages/client/src/components/EntryView.component.tsx @@ -1,14 +1,9 @@ import { Box } from '@mui/material'; import { Entry } from '../graphql/graphql'; -import { useEffect, useRef } from 'react'; +import { VideoViewProps, VideoEntryView } from './VideoView.component'; -export interface EntryViewProps { +export interface EntryViewProps extends Omit { entry: Entry; - width: number; - pauseFrame?: 'start' | 'end' | 'middle'; - autoPlay?: boolean; - mouseOverControls?: boolean; - displayControls?: boolean; } export const EntryView: React.FC = (props) => { @@ -17,7 +12,7 @@ export const EntryView: React.FC = (props) => { const getEntryView = (props: EntryViewProps) => { if (props.entry.contentType.startsWith('video/')) { - return ; + return ; } if (props.entry.contentType.startsWith('image/')) { return ; @@ -26,90 +21,6 @@ const getEntryView = (props: EntryViewProps) => { return

Placeholder

; }; -// TODO: Add in ability to control video play, pause, and middle frame selection -const VideoEntryView: React.FC = (props) => { - const videoRef = useRef(null); - - /** Start the video at the begining */ - const handleStart: React.MouseEventHandler = () => { - if (!videoRef.current || (props.mouseOverControls != undefined && !props.mouseOverControls)) { - return; - } - videoRef.current.currentTime = 0; - videoRef.current?.play(); - }; - - /** Stop the video */ - const handleStop: React.MouseEventHandler = () => { - if (!videoRef.current || (props.mouseOverControls != undefined && !props.mouseOverControls)) { - return; - } - videoRef.current.pause(); - setPauseFrame(); - }; - - /** Set the video to the middle frame */ - const setPauseFrame = async () => { - if (!videoRef.current) { - return; - } - - if (!props.pauseFrame || props.pauseFrame === 'middle') { - const duration = await getDuration(); - videoRef.current.currentTime = duration / 2; - } else if (props.pauseFrame === 'start') { - videoRef.current.currentTime = 0; - } - }; - - /** Get the duration, there is a known issue on Chrome with some audio/video durations */ - const getDuration = async () => { - if (!videoRef.current) { - return 0; - } - - const video = videoRef.current!; - - // If the duration is infinity, this is part of a Chrome bug that causes - // some durations to not load for audio and video. The StackOverflow - // link below discusses the issues and possible solutions - // Then, wait for the update event to be triggered - await new Promise((resolve, _reject) => { - video.ontimeupdate = () => { - // Remove the callback - video.ontimeupdate = () => {}; - // Reset the time - video.currentTime = 0; - resolve(); - }; - - video.currentTime = 1e101; - }); - - // Now try to get the duration again - return video.duration; - }; - - // Set the video to the middle frame when the video is loaded - useEffect(() => { - setPauseFrame(); - }, [videoRef.current]); - - return ( - - - - ); -}; const ImageEntryView: React.FC = (props) => { return ( diff --git a/packages/client/src/components/VideoView.component.tsx b/packages/client/src/components/VideoView.component.tsx new file mode 100644 index 00000000..e36f7d21 --- /dev/null +++ b/packages/client/src/components/VideoView.component.tsx @@ -0,0 +1,96 @@ +import { useRef, useEffect } from 'react'; +import { Box } from '@mui/material'; + +export interface VideoViewProps { + url: string; + width: number; + pauseFrame?: 'start' | 'end' | 'middle'; + autoPlay?: boolean; + mouseOverControls?: boolean; + displayControls?: boolean; +} + +export const VideoEntryView: React.FC = (props) => { + const videoRef = useRef(null); + + /** Start the video at the begining */ + const handleStart: React.MouseEventHandler = () => { + if (!videoRef.current || (props.mouseOverControls != undefined && !props.mouseOverControls)) { + return; + } + videoRef.current.currentTime = 0; + videoRef.current?.play(); + }; + + /** Stop the video */ + const handleStop: React.MouseEventHandler = () => { + if (!videoRef.current || (props.mouseOverControls != undefined && !props.mouseOverControls)) { + return; + } + videoRef.current.pause(); + setPauseFrame(); + }; + + /** Set the video to the middle frame */ + const setPauseFrame = async () => { + if (!videoRef.current) { + return; + } + + if (!props.pauseFrame || props.pauseFrame === 'middle') { + const duration = await getDuration(); + videoRef.current.currentTime = duration / 2; + } else if (props.pauseFrame === 'start') { + videoRef.current.currentTime = 0; + } + }; + + /** Get the duration, there is a known issue on Chrome with some audio/video durations */ + const getDuration = async () => { + if (!videoRef.current) { + return 0; + } + + const video = videoRef.current!; + + // If the duration is infinity, this is part of a Chrome bug that causes + // some durations to not load for audio and video. The StackOverflow + // link below discusses the issues and possible solutions + // Then, wait for the update event to be triggered + await new Promise((resolve, _reject) => { + video.ontimeupdate = () => { + // Remove the callback + video.ontimeupdate = () => {}; + // Reset the time + video.currentTime = 0; + resolve(); + }; + + video.currentTime = 1e101; + }); + + // Now try to get the duration again + return video.duration; + }; + + // Set the video to the middle frame when the video is loaded + useEffect(() => { + setPauseFrame(); + }, [videoRef.current]); + + return ( + + + + ); +}; + diff --git a/packages/client/src/types/EntryView.ts b/packages/client/src/types/EntryView.ts new file mode 100644 index 00000000..e69de29b From 0aedf071266f3f55a5fa9df1964f3d00adeb45c9 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 13:35:32 -0500 Subject: [PATCH 11/15] Working ASL-LEX visualization in tag page --- .../client/public/locales/en/translation.json | 5 +- .../tag/view/AslLexGridView.component.tsx | 63 ++++++++++++++++--- packages/client/src/graphql/lex.graphql | 10 +++ packages/client/src/graphql/lex.ts | 57 +++++++++++++++++ 4 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 packages/client/src/graphql/lex.graphql create mode 100644 packages/client/src/graphql/lex.ts diff --git a/packages/client/public/locales/en/translation.json b/packages/client/public/locales/en/translation.json index 18da1a68..a2e6ef80 100644 --- a/packages/client/public/locales/en/translation.json +++ b/packages/client/public/locales/en/translation.json @@ -21,7 +21,10 @@ "entryId": "Entry ID", "login": "Login", "clear": "clear", - "complete": "complete" + "complete": "complete", + "video": "video", + "key": "key", + "primary": "primay" }, "languages": { "en": "English", diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index 72014086..1edeff62 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -1,7 +1,43 @@ 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'; -const AslLexGridView: React.FC = ({ data }) => { - return 'hello world'; +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]); + + return ( + <> + {videoUrl && } + + ); +} + +const AslLexGridViewKey: React.FC = ({ data }) => { + return 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]); + + return primary || ''; } export const aslLexTest: TagViewTest = (uischema, _schema, _context) => { @@ -12,9 +48,22 @@ export const aslLexTest: TagViewTest = (uischema, _schema, _context) => { } export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { - return [{ - field: property, - headerName: property, - renderCell: (params) => params.row.data && - }] + return [ + { + field: `${property}-video`, + headerName: `${property}: ${i18next.t('common.video')}`, + width: 300, + renderCell: (params) => params.row.data && + }, + { + field: `${property}-key`, + headerName: `${property}: ${i18next.t('common.key')}`, + renderCell: (params) => params.row.data && + }, + { + field: `${property}-primary`, + headerName: `${property}: ${i18next.t('common.primary')}`, + renderCell: (params) => params.row.data && + } + ]; } diff --git a/packages/client/src/graphql/lex.graphql b/packages/client/src/graphql/lex.graphql new file mode 100644 index 00000000..d00ce02d --- /dev/null +++ b/packages/client/src/graphql/lex.graphql @@ -0,0 +1,10 @@ +query lexiconByKey($lexicon: String!, $key: String!) { + lexiconByKey(lexicon: $lexicon, key: $key) { + key, + primary, + video, + lexicon, + associates, + fields + } +} diff --git a/packages/client/src/graphql/lex.ts b/packages/client/src/graphql/lex.ts new file mode 100644 index 00000000..451eb1a0 --- /dev/null +++ b/packages/client/src/graphql/lex.ts @@ -0,0 +1,57 @@ +/* Generated File DO NOT EDIT. */ +/* tslint:disable */ +import * as Types from './graphql'; + +import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; +const defaultOptions = {} as const; +export type LexiconByKeyQueryVariables = Types.Exact<{ + lexicon: Types.Scalars['String']['input']; + key: Types.Scalars['String']['input']; +}>; + + +export type LexiconByKeyQuery = { __typename?: 'Query', lexiconByKey: { __typename?: 'LexiconEntry', key: string, primary: string, video: string, lexicon: string, associates: Array, fields: any } }; + + +export const LexiconByKeyDocument = gql` + query lexiconByKey($lexicon: String!, $key: String!) { + lexiconByKey(lexicon: $lexicon, key: $key) { + key + primary + video + lexicon + associates + fields + } +} + `; + +/** + * __useLexiconByKeyQuery__ + * + * To run a query within a React component, call `useLexiconByKeyQuery` and pass it any options that fit your needs. + * When your component renders, `useLexiconByKeyQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useLexiconByKeyQuery({ + * variables: { + * lexicon: // value for 'lexicon' + * key: // value for 'key' + * }, + * }); + */ +export function useLexiconByKeyQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(LexiconByKeyDocument, options); + } +export function useLexiconByKeyLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(LexiconByKeyDocument, options); + } +export type LexiconByKeyQueryHookResult = ReturnType; +export type LexiconByKeyLazyQueryHookResult = ReturnType; +export type LexiconByKeyQueryResult = Apollo.QueryResult; \ No newline at end of file From 2e884ed9d1ef76b669d650d2d046eb3576e51e11 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 15:41:46 -0500 Subject: [PATCH 12/15] Begin work on visualizing recorded user videos --- .../tag/view/AslLexGridView.component.tsx | 6 +-- .../tag/view/BooleanGridView.component.tsx | 2 +- .../tag/view/FreeTextGridView.component.tsx | 2 +- .../tag/view/NumericGridView.component.tsx | 2 +- .../tag/view/SliderGridView.component.tsx | 2 +- .../tag/view/VideoGridView.component.tsx | 37 +++++++++++++++++++ 6 files changed, 44 insertions(+), 7 deletions(-) diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index 1edeff62..bab5b882 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -53,17 +53,17 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { field: `${property}-video`, headerName: `${property}: ${i18next.t('common.video')}`, width: 300, - renderCell: (params) => params.row.data && + renderCell: (params) => params.row.data && params.row.data[property] && }, { field: `${property}-key`, headerName: `${property}: ${i18next.t('common.key')}`, - renderCell: (params) => params.row.data && + renderCell: (params) => params.row.data && params.row.data[property] && }, { field: `${property}-primary`, headerName: `${property}: ${i18next.t('common.primary')}`, - renderCell: (params) => params.row.data && + renderCell: (params) => params.row.data && params.row.data[property] && } ]; } diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx index 04059b50..f5f23fbe 100644 --- a/packages/client/src/components/tag/view/BooleanGridView.component.tsx +++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx @@ -18,6 +18,6 @@ export const getBoolCols: GetGridColDefs = (uischema, schema, property) => { return [{ field: property, headerName: property, - renderCell: (params) => params.row.data && + renderCell: (params) => params.row.data && params.row.data[property] && }] } diff --git a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx index a7387832..697a2885 100644 --- a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx +++ b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx @@ -17,6 +17,6 @@ export const getTextCols: GetGridColDefs = (uischema, schema, property) => { return [{ field: property, headerName: property, - renderCell: (params) => params.row.data && + renderCell: (params) => params.row.data && params.row.data[property] && }] } diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx index 766f6498..119ef1a8 100644 --- a/packages/client/src/components/tag/view/NumericGridView.component.tsx +++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx @@ -17,6 +17,6 @@ export const getNumericCols: GetGridColDefs = (uischema, schema, property) => { return [{ field: property, headerName: property, - renderCell: (params) => params.row.data && + renderCell: (params) => params.row.data && params.row.data[property] && }] } diff --git a/packages/client/src/components/tag/view/SliderGridView.component.tsx b/packages/client/src/components/tag/view/SliderGridView.component.tsx index 705e3641..3fac266f 100644 --- a/packages/client/src/components/tag/view/SliderGridView.component.tsx +++ b/packages/client/src/components/tag/view/SliderGridView.component.tsx @@ -17,6 +17,6 @@ export const getSliderCols: GetGridColDefs = (uischema, schema, property) => { return [{ field: property, headerName: property, - renderCell: (params) => params.row.data && + renderCell: (params) => params.row.data && params.row.data[property] && }] } diff --git a/packages/client/src/components/tag/view/VideoGridView.component.tsx b/packages/client/src/components/tag/view/VideoGridView.component.tsx index e69de29b..e898ecfe 100644 --- a/packages/client/src/components/tag/view/VideoGridView.component.tsx +++ b/packages/client/src/components/tag/view/VideoGridView.component.tsx @@ -0,0 +1,37 @@ +import { GridColDef } from '@mui/x-data-grid'; +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView'; +import i18next from 'i18next'; + +const VideoGridView: React.FC = ({ data }) => { + return ( + 'hello world' + ) +} + +export const videoViewTest: TagViewTest = (uischema, _schema, _context) => { + if (uischema.options && uischema.options.customType && uischema.options.customType === 'video') { + return 5; + } + return NOT_APPLICABLE; +} + +export const getVideoCols: GetGridColDefs = (uischema, schema, property) => { + const minVideos = uischema.options!.minimumRequired!; + + let maxVideos = uischema.options!.maximumRequired; + if (!maxVideos) { + maxVideos = minVideos; + } + + const columns: GridColDef[] = []; + + for (let i = 0; i < maxVideos; i++) { + columns.push({ + field: `${property}-video-${i+1}`, + headerName: `${property}: ${i18next.t('common.video')} ${i + 1}`, + renderCell: (params) => params.row.data && params.row.data[property] && + }) + } + + return columns; +} From 32b5abc4a453be5f8266237a13edd0708d3e2d9f Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 15:52:14 -0500 Subject: [PATCH 13/15] Add ability to query for entry from ID --- .../client/src/graphql/entry/entry.graphql | 15 ++++++ packages/client/src/graphql/entry/entry.ts | 51 +++++++++++++++++++ packages/client/src/graphql/graphql.ts | 6 +++ .../src/entry/resolvers/entry.resolver.ts | 20 +++++++- 4 files changed, 91 insertions(+), 1 deletion(-) diff --git a/packages/client/src/graphql/entry/entry.graphql b/packages/client/src/graphql/entry/entry.graphql index 37d74d90..c2cab931 100644 --- a/packages/client/src/graphql/entry/entry.graphql +++ b/packages/client/src/graphql/entry/entry.graphql @@ -13,6 +13,21 @@ query entryForDataset($dataset: ID!) { } } +query entryFromID($entry: ID!) { + entryFromID(entry: $entry) { + _id + organization + entryID + contentType + dataset + creator + dateCreated + meta + signedUrl + signedUrlExpiration + } +} + mutation deleteEntry($entry: ID!) { deleteEntry(entry: $entry) } diff --git a/packages/client/src/graphql/entry/entry.ts b/packages/client/src/graphql/entry/entry.ts index fdd15e24..7d4c3acc 100644 --- a/packages/client/src/graphql/entry/entry.ts +++ b/packages/client/src/graphql/entry/entry.ts @@ -12,6 +12,13 @@ export type EntryForDatasetQueryVariables = Types.Exact<{ export type EntryForDatasetQuery = { __typename?: 'Query', entryForDataset: Array<{ __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number }> }; +export type EntryFromIdQueryVariables = Types.Exact<{ + entry: Types.Scalars['ID']['input']; +}>; + + +export type EntryFromIdQuery = { __typename?: 'Query', entryFromID: { __typename?: 'Entry', _id: string, organization: string, entryID: string, contentType: string, dataset: string, creator: string, dateCreated: any, meta?: any | null, signedUrl: string, signedUrlExpiration: number } }; + export type DeleteEntryMutationVariables = Types.Exact<{ entry: Types.Scalars['ID']['input']; }>; @@ -64,6 +71,50 @@ export function useEntryForDatasetLazyQuery(baseOptions?: Apollo.LazyQueryHookOp export type EntryForDatasetQueryHookResult = ReturnType; export type EntryForDatasetLazyQueryHookResult = ReturnType; export type EntryForDatasetQueryResult = Apollo.QueryResult; +export const EntryFromIdDocument = gql` + query entryFromID($entry: ID!) { + entryFromID(entry: $entry) { + _id + organization + entryID + contentType + dataset + creator + dateCreated + meta + signedUrl + signedUrlExpiration + } +} + `; + +/** + * __useEntryFromIdQuery__ + * + * To run a query within a React component, call `useEntryFromIdQuery` and pass it any options that fit your needs. + * When your component renders, `useEntryFromIdQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useEntryFromIdQuery({ + * variables: { + * entry: // value for 'entry' + * }, + * }); + */ +export function useEntryFromIdQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(EntryFromIdDocument, options); + } +export function useEntryFromIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(EntryFromIdDocument, options); + } +export type EntryFromIdQueryHookResult = ReturnType; +export type EntryFromIdLazyQueryHookResult = ReturnType; +export type EntryFromIdQueryResult = Apollo.QueryResult; export const DeleteEntryDocument = gql` mutation deleteEntry($entry: ID!) { deleteEntry(entry: $entry) diff --git a/packages/client/src/graphql/graphql.ts b/packages/client/src/graphql/graphql.ts index cff59a77..e8007aa6 100644 --- a/packages/client/src/graphql/graphql.ts +++ b/packages/client/src/graphql/graphql.ts @@ -338,6 +338,7 @@ export type Query = { __typename?: 'Query'; datasetExists: Scalars['Boolean']['output']; entryForDataset: Array; + entryFromID: Entry; exists: Scalars['Boolean']['output']; findStudies: Array; /** Get the presigned URL for where to upload the CSV against */ @@ -372,6 +373,11 @@ export type QueryEntryForDatasetArgs = { }; +export type QueryEntryFromIdArgs = { + entry: Scalars['ID']['input']; +}; + + export type QueryExistsArgs = { name: Scalars['String']['input']; }; diff --git a/packages/server/src/entry/resolvers/entry.resolver.ts b/packages/server/src/entry/resolvers/entry.resolver.ts index 1d43c561..495f303c 100644 --- a/packages/server/src/entry/resolvers/entry.resolver.ts +++ b/packages/server/src/entry/resolvers/entry.resolver.ts @@ -14,13 +14,15 @@ import { OrganizationContext } from '../../organization/organization.context'; import { Organization } from '../../organization/organization.model'; import { EntryPipe } from '../pipes/entry.pipe'; import { OrganizationGuard } from '../../organization/organization.guard'; +import { DatasetService } from '../../dataset/dataset.service'; @UseGuards(JwtAuthGuard, OrganizationGuard) @Resolver(() => Entry) export class EntryResolver { constructor( private readonly entryService: EntryService, - @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer + @Inject(CASBIN_PROVIDER) private readonly enforcer: casbin.Enforcer, + private readonly datasetService: DatasetService ) {} @Query(() => [Entry]) @@ -35,6 +37,22 @@ export class EntryResolver { return this.entryService.findForDataset(dataset); } + @Query(() => Entry) + async entryFromID( + @Args('entry', { type: () => ID }, EntryPipe) entry: Entry, + @TokenContext() user: TokenPayload + ): Promise { + const dataset = await this.datasetService.findById(entry.dataset); + if (!dataset) { + throw new Error('Dataset not found for entry'); + } + if (!(await this.enforcer.enforce(user.user_id, DatasetPermissions.READ, dataset._id))) { + throw new UnauthorizedException('User cannot read entries on this dataset'); + } + + return entry; + } + @ResolveField(() => String) async signedUrl(@Parent() entry: Entry, @TokenContext() user: TokenPayload): Promise { if (!(await this.enforcer.enforce(user.user_id, DatasetPermissions.READ, entry.dataset))) { From 365a49777d6e93b88bc94479933d7094a7f1b8f3 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 15:58:00 -0500 Subject: [PATCH 14/15] Working video record visualization --- .../tag/view/TagGridView.component.tsx | 4 +++- .../tag/view/VideoGridView.component.tsx | 21 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 0a2d558e..26ebcd97 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 { getNumericCols, numericTest } from './NumericGridView.component'; import { getSliderCols, sliderTest } from './SliderGridView.component'; import { getBoolCols, booleanTest } from './BooleanGridView.component'; import { aslLexTest, getAslLexCols } from './AslLexGridView.component'; +import { getVideoCols, videoViewTest } from './VideoGridView.component'; export interface TagGridViewProps { study: Study; @@ -26,7 +27,8 @@ export const TagGridView: React.FC = ({ study }) => { { tester: numericTest, getGridColDefs: getNumericCols }, { tester: sliderTest, getGridColDefs: getSliderCols }, { tester: booleanTest, getGridColDefs: getBoolCols }, - { tester: aslLexTest, getGridColDefs: getAslLexCols } + { tester: aslLexTest, getGridColDefs: getAslLexCols }, + { tester: videoViewTest, getGridColDefs: getVideoCols } ]; const getTagsResults = useGetTagsQuery({ variables: { study: study._id } }); diff --git a/packages/client/src/components/tag/view/VideoGridView.component.tsx b/packages/client/src/components/tag/view/VideoGridView.component.tsx index e898ecfe..f4d95eae 100644 --- a/packages/client/src/components/tag/view/VideoGridView.component.tsx +++ b/packages/client/src/components/tag/view/VideoGridView.component.tsx @@ -1,10 +1,26 @@ 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'; 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]) + return ( - 'hello world' + <> + { entry && } + ) } @@ -29,7 +45,8 @@ export const getVideoCols: GetGridColDefs = (uischema, schema, property) => { columns.push({ field: `${property}-video-${i+1}`, headerName: `${property}: ${i18next.t('common.video')} ${i + 1}`, - renderCell: (params) => params.row.data && params.row.data[property] && + width: 300, + renderCell: (params) => params.row.data && params.row.data[property] && }) } From 97b18243ecdeb9b03f8f85b4567be138573431a1 Mon Sep 17 00:00:00 2001 From: cbolles Date: Fri, 23 Feb 2024 15:58:30 -0500 Subject: [PATCH 15/15] Fix formatting --- .../src/components/EntryView.component.tsx | 1 - .../src/components/VideoView.component.tsx | 1 - .../tag/view/AslLexGridView.component.tsx | 53 ++++++++++++++----- .../tag/view/BooleanGridView.component.tsx | 22 +++++--- .../tag/view/FreeTextGridView.component.tsx | 26 +++++---- .../tag/view/NumericGridView.component.tsx | 26 +++++---- .../tag/view/SliderGridView.component.tsx | 22 +++++--- .../tag/view/TagGridView.component.tsx | 36 +++++++------ .../tag/view/VideoGridView.component.tsx | 34 ++++++++---- 9 files changed, 144 insertions(+), 77 deletions(-) diff --git a/packages/client/src/components/EntryView.component.tsx b/packages/client/src/components/EntryView.component.tsx index 59071e5f..553ae1ef 100644 --- a/packages/client/src/components/EntryView.component.tsx +++ b/packages/client/src/components/EntryView.component.tsx @@ -21,7 +21,6 @@ const getEntryView = (props: EntryViewProps) => { return

Placeholder

; }; - const ImageEntryView: React.FC = (props) => { return ( diff --git a/packages/client/src/components/VideoView.component.tsx b/packages/client/src/components/VideoView.component.tsx index e36f7d21..faf0d5a4 100644 --- a/packages/client/src/components/VideoView.component.tsx +++ b/packages/client/src/components/VideoView.component.tsx @@ -93,4 +93,3 @@ export const VideoEntryView: React.FC = (props) => { ); }; - diff --git a/packages/client/src/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx index bab5b882..fae73e6a 100644 --- a/packages/client/src/components/tag/view/AslLexGridView.component.tsx +++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx @@ -7,7 +7,9 @@ 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 } }); + const lexiconByKeyResult = useLexiconByKeyQuery({ + variables: { lexicon: import.meta.env.VITE_ASL_LEXICON_ID, key: data } + }); useEffect(() => { if (lexiconByKeyResult.data) { @@ -17,19 +19,30 @@ const AslLexGridViewVideo: React.FC = ({ data }) => { return ( <> - {videoUrl && } + {videoUrl && ( + + )} ); -} +}; const AslLexGridViewKey: React.FC = ({ data }) => { return 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 } }); + const lexiconByKeyResult = useLexiconByKeyQuery({ + variables: { lexicon: import.meta.env.VITE_ASL_LEXICON_ID, key: data } + }); useEffect(() => { if (lexiconByKeyResult.data) { @@ -38,14 +51,18 @@ const AslLexGridViewPrimary: React.FC = ({ data }) => { }, [lexiconByKeyResult]); return primary || ''; -} +}; export const aslLexTest: TagViewTest = (uischema, _schema, _context) => { - if (uischema.options != undefined && uischema.options.customType != undefined && uischema.options.customType == 'asl-lex') { + if ( + uischema.options != undefined && + uischema.options.customType != undefined && + uischema.options.customType == 'asl-lex' + ) { return 5; } return NOT_APPLICABLE; -} +}; export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { return [ @@ -53,17 +70,29 @@ export const getAslLexCols: GetGridColDefs = (uischema, schema, property) => { field: `${property}-video`, headerName: `${property}: ${i18next.t('common.video')}`, width: 300, - renderCell: (params) => params.row.data && params.row.data[property] && + renderCell: (params) => + params.row.data && + params.row.data[property] && ( + + ) }, { field: `${property}-key`, headerName: `${property}: ${i18next.t('common.key')}`, - renderCell: (params) => params.row.data && params.row.data[property] && + renderCell: (params) => + params.row.data && + params.row.data[property] && ( + + ) }, { field: `${property}-primary`, headerName: `${property}: ${i18next.t('common.primary')}`, - renderCell: (params) => params.row.data && params.row.data[property] && + renderCell: (params) => + params.row.data && + params.row.data[property] && ( + + ) } ]; -} +}; diff --git a/packages/client/src/components/tag/view/BooleanGridView.component.tsx b/packages/client/src/components/tag/view/BooleanGridView.component.tsx index f5f23fbe..74a3b834 100644 --- a/packages/client/src/components/tag/view/BooleanGridView.component.tsx +++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx @@ -5,19 +5,25 @@ import { Checkbox } from '@mui/material'; /** Visualize basic text data in a grid view */ const BooleanGridView: React.FC = ({ data }) => { return ; -} +}; export const booleanTest: TagViewTest = (uischema, schema, context) => { if (materialBooleanControlTester(uischema, schema, context) !== NOT_APPLICABLE) { return 2; } return NOT_APPLICABLE; -} +}; export const getBoolCols: GetGridColDefs = (uischema, schema, property) => { - return [{ - field: property, - headerName: property, - renderCell: (params) => params.row.data && params.row.data[property] && - }] -} + return [ + { + field: property, + headerName: property, + renderCell: (params) => + params.row.data && + params.row.data[property] && ( + + ) + } + ]; +}; diff --git a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx index 697a2885..509441c1 100644 --- a/packages/client/src/components/tag/view/FreeTextGridView.component.tsx +++ b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx @@ -1,22 +1,28 @@ -import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from "../../../types/TagColumnView"; -import { materialAnyOfStringOrEnumControlTester } from "@jsonforms/material-renderers"; +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView'; +import { materialAnyOfStringOrEnumControlTester } from '@jsonforms/material-renderers'; /** Visualize basic text data in a grid view */ const FreeTextGridView: React.FC = ({ data }) => { return data; -} +}; export const freeTextTest: TagViewTest = (uischema, schema, context) => { if (materialAnyOfStringOrEnumControlTester(uischema, schema, context) !== NOT_APPLICABLE) { return 1; } return NOT_APPLICABLE; -} +}; export const getTextCols: GetGridColDefs = (uischema, schema, property) => { - return [{ - field: property, - headerName: property, - renderCell: (params) => params.row.data && params.row.data[property] && - }] -} + return [ + { + field: property, + headerName: property, + renderCell: (params) => + params.row.data && + params.row.data[property] && ( + + ) + } + ]; +}; diff --git a/packages/client/src/components/tag/view/NumericGridView.component.tsx b/packages/client/src/components/tag/view/NumericGridView.component.tsx index 119ef1a8..2aa8b549 100644 --- a/packages/client/src/components/tag/view/NumericGridView.component.tsx +++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx @@ -1,22 +1,28 @@ -import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from "../../../types/TagColumnView"; -import { materialNumberControlTester } from "@jsonforms/material-renderers"; +import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView'; +import { materialNumberControlTester } from '@jsonforms/material-renderers'; /** Visualize basic text data in a grid view */ const NumericGridView: React.FC = ({ data }) => { return data; -} +}; export const numericTest: TagViewTest = (uischema, schema, context) => { if (materialNumberControlTester(uischema, schema, context) !== NOT_APPLICABLE) { return 2; } return NOT_APPLICABLE; -} +}; export const getNumericCols: GetGridColDefs = (uischema, schema, property) => { - return [{ - field: property, - headerName: property, - renderCell: (params) => params.row.data && params.row.data[property] && - }] -} + return [ + { + field: property, + headerName: property, + renderCell: (params) => + params.row.data && + params.row.data[property] && ( + + ) + } + ]; +}; diff --git a/packages/client/src/components/tag/view/SliderGridView.component.tsx b/packages/client/src/components/tag/view/SliderGridView.component.tsx index 3fac266f..8b125950 100644 --- a/packages/client/src/components/tag/view/SliderGridView.component.tsx +++ b/packages/client/src/components/tag/view/SliderGridView.component.tsx @@ -4,19 +4,25 @@ import { materialSliderControlTester } from '@jsonforms/material-renderers'; /** Visualize basic text data in a grid view */ const SliderGridView: React.FC = ({ data }) => { return data; -} +}; export const sliderTest: TagViewTest = (uischema, schema, context) => { if (materialSliderControlTester(uischema, schema, context) !== NOT_APPLICABLE) { return 2; } return NOT_APPLICABLE; -} +}; export const getSliderCols: GetGridColDefs = (uischema, schema, property) => { - return [{ - field: property, - headerName: property, - renderCell: (params) => params.row.data && params.row.data[property] && - }] -} + return [ + { + field: property, + headerName: property, + renderCell: (params) => + params.row.data && + params.row.data[property] && ( + + ) + } + ]; +}; diff --git a/packages/client/src/components/tag/view/TagGridView.component.tsx b/packages/client/src/components/tag/view/TagGridView.component.tsx index 26ebcd97..426f356b 100644 --- a/packages/client/src/components/tag/view/TagGridView.component.tsx +++ b/packages/client/src/components/tag/view/TagGridView.component.tsx @@ -22,7 +22,7 @@ export const TagGridView: React.FC = ({ study }) => { const { t } = useTranslation(); const [tags, setTags] = useState([]); - const tagColumnViews: { tester: TagViewTest, getGridColDefs: GetGridColDefs }[] = [ + const tagColumnViews: { tester: TagViewTest; getGridColDefs: GetGridColDefs }[] = [ { tester: freeTextTest, getGridColDefs: getTextCols }, { tester: numericTest, getGridColDefs: getNumericCols }, { tester: sliderTest, getGridColDefs: getSliderCols }, @@ -57,25 +57,29 @@ export const TagGridView: React.FC = ({ study }) => { ]; // Generate the dynamic columns for the grid - const dataColunms: GridColDef[] = Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties).map((property: string) => { - const fieldSchema = study.tagSchema.dataSchema.properties[property]; - const fieldUiSchema = study.tagSchema.uiSchema.elements.find((element: any) => element.scope === `#/properties/${property}`); + const dataColunms: GridColDef[] = Object.getOwnPropertyNames(study.tagSchema.dataSchema.properties) + .map((property: string) => { + const fieldSchema = study.tagSchema.dataSchema.properties[property]; + const fieldUiSchema = study.tagSchema.uiSchema.elements.find( + (element: any) => element.scope === `#/properties/${property}` + ); - if (!fieldSchema || !fieldUiSchema) { - throw new Error(`Could not find schema for property ${property}`); - } + if (!fieldSchema || !fieldUiSchema) { + throw new Error(`Could not find schema for property ${property}`); + } - const context = { rootSchema: study.tagSchema.dataSchema, config: {} }; - const reactNode = tagColumnViews - .filter((view) => view.tester(fieldUiSchema, fieldSchema, context)) - .sort((a, b) => b.tester(fieldUiSchema, fieldSchema, context) - a.tester(fieldUiSchema, fieldSchema, context)); + const context = { rootSchema: study.tagSchema.dataSchema, config: {} }; + const reactNode = tagColumnViews + .filter((view) => view.tester(fieldUiSchema, fieldSchema, context)) + .sort((a, b) => b.tester(fieldUiSchema, fieldSchema, context) - a.tester(fieldUiSchema, fieldSchema, context)); - if (reactNode.length === 0) { - throw new Error(`No matching view for property ${property}`); - } + if (reactNode.length === 0) { + throw new Error(`No matching view for property ${property}`); + } - return reactNode[0].getGridColDefs(fieldUiSchema, fieldSchema, property); - }).flat(); + return reactNode[0].getGridColDefs(fieldUiSchema, fieldSchema, property); + }) + .flat(); return ( = ({ data }) => { const [entry, setEntry] = useState(null); - const entryFromIdResult = useEntryFromIdQuery({ variables: { entry: data }}); + const entryFromIdResult = useEntryFromIdQuery({ variables: { entry: data } }); useEffect(() => { if (entryFromIdResult.data) { setEntry(entryFromIdResult.data.entryFromID); } - - }, [entryFromIdResult]) + }, [entryFromIdResult]); return ( <> - { entry && } + {entry && ( + + )} - ) -} + ); +}; export const videoViewTest: TagViewTest = (uischema, _schema, _context) => { if (uischema.options && uischema.options.customType && uischema.options.customType === 'video') { return 5; } return NOT_APPLICABLE; -} +}; export const getVideoCols: GetGridColDefs = (uischema, schema, property) => { const minVideos = uischema.options!.minimumRequired!; @@ -43,12 +51,16 @@ export const getVideoCols: GetGridColDefs = (uischema, schema, property) => { for (let i = 0; i < maxVideos; i++) { columns.push({ - field: `${property}-video-${i+1}`, + field: `${property}-video-${i + 1}`, headerName: `${property}: ${i18next.t('common.video')} ${i + 1}`, width: 300, - renderCell: (params) => params.row.data && params.row.data[property] && - }) + renderCell: (params) => + params.row.data && + params.row.data[property] && ( + + ) + }); } return columns; -} +};