diff --git a/packages/client/public/locales/en/translation.json b/packages/client/public/locales/en/translation.json
index 1a4631d1..a2e6ef80 100644
--- a/packages/client/public/locales/en/translation.json
+++ b/packages/client/public/locales/en/translation.json
@@ -20,7 +20,11 @@
"view": "View",
"entryId": "Entry ID",
"login": "Login",
- "clear": "clear"
+ "clear": "clear",
+ "complete": "complete",
+ "video": "video",
+ "key": "key",
+ "primary": "primay"
},
"languages": {
"en": "English",
@@ -40,7 +44,7 @@
"newStudy": "New Study",
"studyControl": "Study Control",
"entryControl": "Entry Control",
- "downloadTags": "Download Tags",
+ "viewTags": "Download Tags",
"datasets": "Datasets",
"datasetControl": "Dataset Control",
"projectAccess": "Project Access",
@@ -117,6 +121,9 @@
"login": {
"selectOrg": "Select an Organization to Login",
"redirectToOrg": "Redirect to Organization Login"
+ },
+ "tagView": {
+ "originalEntry": "Original Entry"
}
}
}
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/EntryView.component.tsx b/packages/client/src/components/EntryView.component.tsx
index 2be4d788..553ae1ef 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,91 +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/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/VideoView.component.tsx b/packages/client/src/components/VideoView.component.tsx
new file mode 100644
index 00000000..faf0d5a4
--- /dev/null
+++ b/packages/client/src/components/VideoView.component.tsx
@@ -0,0 +1,95 @@
+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/components/tag/view/AslLexGridView.component.tsx b/packages/client/src/components/tag/view/AslLexGridView.component.tsx
new file mode 100644
index 00000000..fae73e6a
--- /dev/null
+++ b/packages/client/src/components/tag/view/AslLexGridView.component.tsx
@@ -0,0 +1,98 @@
+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 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) => {
+ 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}-video`,
+ headerName: `${property}: ${i18next.t('common.video')}`,
+ width: 300,
+ 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] && (
+
+ )
+ },
+ {
+ field: `${property}-primary`,
+ headerName: `${property}: ${i18next.t('common.primary')}`,
+ 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
new file mode 100644
index 00000000..74a3b834
--- /dev/null
+++ b/packages/client/src/components/tag/view/BooleanGridView.component.tsx
@@ -0,0 +1,29 @@
+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 */
+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] && (
+
+ )
+ }
+ ];
+};
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..509441c1
--- /dev/null
+++ b/packages/client/src/components/tag/view/FreeTextGridView.component.tsx
@@ -0,0 +1,28 @@
+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] && (
+
+ )
+ }
+ ];
+};
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..2aa8b549
--- /dev/null
+++ b/packages/client/src/components/tag/view/NumericGridView.component.tsx
@@ -0,0 +1,28 @@
+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] && (
+
+ )
+ }
+ ];
+};
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..8b125950
--- /dev/null
+++ b/packages/client/src/components/tag/view/SliderGridView.component.tsx
@@ -0,0 +1,28 @@
+import { TagColumnViewProps, TagViewTest, NOT_APPLICABLE, GetGridColDefs } from '../../../types/TagColumnView';
+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] && (
+
+ )
+ }
+ ];
+};
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..426f356b
--- /dev/null
+++ b/packages/client/src/components/tag/view/TagGridView.component.tsx
@@ -0,0 +1,92 @@
+import { useTranslation } from 'react-i18next';
+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 { freeTextTest, getTextCols } from './FreeTextGridView.component';
+import { EntryView } from '../../EntryView.component';
+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';
+import { getVideoCols, videoViewTest } from './VideoGridView.component';
+
+export interface TagGridViewProps {
+ study: Study;
+}
+
+export const TagGridView: React.FC = ({ study }) => {
+ const { t } = useTranslation();
+ const [tags, setTags] = useState([]);
+
+ const tagColumnViews: { tester: TagViewTest; getGridColDefs: GetGridColDefs }[] = [
+ { tester: freeTextTest, getGridColDefs: getTextCols },
+ { tester: numericTest, getGridColDefs: getNumericCols },
+ { tester: sliderTest, getGridColDefs: getSliderCols },
+ { tester: booleanTest, getGridColDefs: getBoolCols },
+ { tester: aslLexTest, getGridColDefs: getAslLexCols },
+ { tester: videoViewTest, getGridColDefs: getVideoCols }
+ ];
+
+ const getTagsResults = useGetTagsQuery({ variables: { study: study._id } });
+
+ useEffect(() => {
+ if (getTagsResults.data) {
+ setTags(getTagsResults.data.getTags);
+ }
+ }, [getTagsResults.data]);
+
+ const entryColumns: GridColDef[] = [
+ {
+ field: 'entryView',
+ 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];
+ 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 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}`);
+ }
+
+ return reactNode[0].getGridColDefs(fieldUiSchema, fieldSchema, property);
+ })
+ .flat();
+
+ return (
+ 'auto'}
+ rows={tags}
+ columns={entryColumns.concat(tagMetaColumns).concat(dataColunms)}
+ getRowId={(row) => row._id}
+ />
+ );
+};
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..b611d491
--- /dev/null
+++ b/packages/client/src/components/tag/view/VideoGridView.component.tsx
@@ -0,0 +1,66 @@
+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 (
+ <>
+ {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!;
+
+ 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}`,
+ width: 300,
+ renderCell: (params) =>
+ params.row.data &&
+ params.row.data[property] && (
+
+ )
+ });
+ }
+
+ return columns;
+};
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 4833e4ce..e8007aa6 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,21 +334,11 @@ 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'];
entryForDataset: Array;
+ entryFromID: Entry;
exists: Scalars['Boolean']['output'];
findStudies: Array;
/** Get the presigned URL for where to upload the CSV against */
@@ -575,25 +348,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;
};
@@ -608,6 +373,11 @@ export type QueryEntryForDatasetArgs = {
};
+export type QueryEntryFromIdArgs = {
+ entry: Scalars['ID']['input'];
+};
+
+
export type QueryExistsArgs = {
name: Scalars['String']['input'];
};
@@ -640,11 +410,6 @@ export type QueryGetEntryUploadUrlArgs = {
};
-export type QueryGetProjectArgs = {
- id: Scalars['String']['input'];
-};
-
-
export type QueryGetProjectPermissionsArgs = {
project: Scalars['ID']['input'];
};
@@ -661,18 +426,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 +454,6 @@ export type QueryProjectExistsArgs = {
};
-export type QueryProjectUsersArgs = {
- projectId: Scalars['String']['input'];
-};
-
-
export type QueryStudyExistsArgs = {
name: Scalars['String']['input'];
project: Scalars['ID']['input'];
@@ -714,13 +464,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 +550,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/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
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/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..09879e1e
--- /dev/null
+++ b/packages/client/src/pages/studies/TagView.tsx
@@ -0,0 +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/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/EntryView.ts b/packages/client/src/types/EntryView.ts
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/client/src/types/TagColumnView.ts b/packages/client/src/types/TagColumnView.ts
new file mode 100644
index 00000000..5ea54104
--- /dev/null
+++ b/packages/client/src/types/TagColumnView.ts
@@ -0,0 +1,20 @@
+import { JsonSchema, TesterContext, UISchemaElement } from '@jsonforms/core';
+import { GridColDef } from '@mui/x-data-grid';
+
+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 type GetGridColDefs = (uischema: UISchemaElement, schema: JsonSchema, property: string) => GridColDef[];
+
+/**
+ * Test to see if a given field can be transformed into a tag column view.
+ */
+export type TagViewTest = (uischema: UISchemaElement, schema: JsonSchema, context: TesterContext) => number;
+export const NOT_APPLICABLE = -1;
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))) {
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 });
}