Skip to content

Commit

Permalink
task/DES-2685: Add API endpoint and frontend view for project listing (
Browse files Browse the repository at this point in the history
…#1167)

* Add API endpoint and frontend view for project listing

* style/linting fixes

* Working form fields for base project metadata

* proof of concept populating form with data

* fix cases where setting form values would break the input

* Add project detail endpoint and confirm compatibility with form

* file listings and details in project workdir

* curation tree renders

* Add dropdown to select entities to add to the tree

* Add project header with form modal

* Add curation UI for project tree and file associations

* add curation and publication preview layouts
  • Loading branch information
jarosenb authored Feb 26, 2024
1 parent f944462 commit 4701e11
Show file tree
Hide file tree
Showing 72 changed files with 3,939 additions and 46 deletions.
1 change: 1 addition & 0 deletions client/modules/_hooks/src/datafiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export {
usePathDisplayName,
getSystemRootDisplayName,
} from './usePathDisplayName';
export * from './projects';
14 changes: 14 additions & 0 deletions client/modules/_hooks/src/datafiles/projects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export * from './types';
export { useProjectListing } from './useProjectListing';
export {
useProjectDetail,
useFileAssociations,
useFileTags,
} from './useProjectDetail';
export { useProjectPreview } from './useProjectPreview';
export { useProjectEntityReorder } from './useProjectEntityReorder';
export { useAddEntityToTree } from './useAddEntityToTree';
export { useRemoveEntityFromTree } from './useRemoveEntityFromTree';
export { useAddFileAssociation } from './useAddFileAssociation';
export { useRemoveFileAssociation } from './useRemoveFileAssociation';
export { useSetFileTags } from './useSetFileTags';
118 changes: 118 additions & 0 deletions client/modules/_hooks/src/datafiles/projects/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
export type TProjectUser = {
fname: string;
lname: string;
email: string;
inst: string;
role: 'pi' | 'co_pi' | 'team_member' | 'guest';
username?: string;
};

export type TProjectAward = {
name: string;
number: string;
fundingSource: string;
};

export type TReferencedWork = {
title: string;
doi: string;
hrefType: 'doi' | 'url';
};

export type TAssociatedProject = {
type: 'Context' | 'Linked Dataset' | 'Cited By';
title: string;
href: string;
hrefType: 'doi' | 'url';
};

export type TFileTag = {
tagName: string;
path: string;
};

export type TFileObj = {
system: string;
name: string;
path: string;
type: 'file' | 'dir';
length?: number;
lastModified?: string;
};

export type THazmapperMap = {
name: string;
uuid: string;
path: string;
deployment: string;
href?: string;
};

export type TDropdownValue = {
id: string;
name: string;
};

export type TNHEvent = {
eventName: string;
eventStart: string;
eventEnd: string;
location: string;
latitude: string;
longitude: string;
};

export type TBaseProjectValue = {
projectId: string;
projectType:
| 'other'
| 'experimental'
| 'simulation'
| 'hybrid_simulation'
| 'field_recon'
| 'field_reconnaissance'
| 'None';

title: string;
description: string;
users: TProjectUser[];
dataTypes?: TDropdownValue[];
authors: TProjectUser[];

awardNumbers: TProjectAward[];
associatedProjects: TAssociatedProject[];
referencedData: TReferencedWork[];
keywords: string[];
nhEvents: TNHEvent[];
nhTypes: TDropdownValue[];
frTypes?: TDropdownValue[];
facilities: TDropdownValue[];

dois: string[];
fileObjs: TFileObj[];
fileTags: TFileTag[];
};

type TEntityValue = {
title: string;
description?: string;
authors?: TProjectUser[];
fileObjs?: TFileObj[];
fileTags: TFileTag[];
};

export type TProjectMeta = {
uuid: string;
name: string;
created: string;
lastUpdated: string;
};

export type TBaseProject = TProjectMeta & {
name: 'designsafe.project';
value: TBaseProjectValue;
};

export type TEntityMeta = TProjectMeta & {
value: TEntityValue;
};
22 changes: 22 additions & 0 deletions client/modules/_hooks/src/datafiles/projects/useAddEntityToTree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '../../apiClient';

async function addEntity(projectId: string, nodeId: string, uuid: string) {
const res = await apiClient.post(
`/api/projects/v2/${projectId}/entities/associations/${nodeId}/`,
{ uuid }
);
return res.data;
}

export function useAddEntityToTree(projectId: string, nodeId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ uuid }: { uuid: string }) =>
addEntity(projectId, nodeId, uuid),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: ['datafiles', 'projects', 'detail', projectId],
}),
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '../../apiClient';
import { TFileObj } from './types';

async function addFileAssociation(
projectId: string,
entityUuid: string,
fileObjs: TFileObj[]
) {
const res = await apiClient.post(
`/api/projects/v2/${projectId}/entities/${entityUuid}/files/`,
{ fileObjs }
);
return res.data;
}

export function useAddFileAssociation(projectId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
fileObjs,
entityUuid,
}: {
fileObjs: TFileObj[];
entityUuid: string;
}) => addFileAssociation(projectId, entityUuid, fileObjs),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: ['datafiles', 'projects', 'detail', projectId],
}),
});
}
73 changes: 73 additions & 0 deletions client/modules/_hooks/src/datafiles/projects/useProjectDetail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useQuery } from '@tanstack/react-query';
import apiClient from '../../apiClient';
import { TBaseProject, TEntityMeta, TFileTag } from './types';
import { useMemo } from 'react';

type TProjectDetailResponse = {
baseProject: TBaseProject;
entities: TEntityMeta[];
tree: unknown;
};

async function getProjectDetail({
projectId,
signal,
}: {
projectId: string;
signal: AbortSignal;
}) {
const resp = await apiClient.get<TProjectDetailResponse>(
`/api/projects/v2/${projectId}/`,
{
signal,
}
);
return resp.data;
}

export function useProjectDetail(projectId: string) {
return useQuery({
queryKey: ['datafiles', 'projects', 'detail', projectId],
queryFn: ({ signal }) => getProjectDetail({ projectId, signal }),
enabled: !!projectId,
});
}

export function useFileAssociations(projectId: string) {
/*Return a record mapping file paths to an array of entities containing those paths.*/
const { data } = useProjectDetail(projectId);

const memoizedFileMapping = useMemo(() => {
const entities = data?.entities ?? [];
const fileMapping: Record<string, TEntityMeta[]> = {};
entities.forEach((entity) => {
const fileObjs = entity.value.fileObjs ?? [];
fileObjs.forEach((fileObj) => {
const entityList = fileMapping[fileObj.path] ?? [];
entityList.push(entity);
fileMapping[fileObj.path] = entityList;
});
});
return fileMapping;
}, [data]);

return memoizedFileMapping;
}

export function useFileTags(projectId: string) {
/*Return a record mapping file paths to an array of entities containing those paths.*/
const { data } = useProjectDetail(projectId);

const memoizedFileMapping = useMemo(() => {
const entities = data?.entities ?? [];
const tagMapping: Record<string, TFileTag[]> = {};
entities.forEach((entity) => {
const fileTags = entity.value.fileTags ?? [];
tagMapping[entity.uuid] = fileTags;
});

return tagMapping;
}, [data]);

return memoizedFileMapping;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '../../apiClient';

async function reorderEntitity(
projectId: string,
nodeId: string,
order: number
) {
const res = await apiClient.put(
`/api/projects/v2/${projectId}/entities/ordering/`,
{ nodeId, order }
);
return res.data;
}

export function useProjectEntityReorder(projectId: string, nodeId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ order }: { order: number }) =>
reorderEntitity(projectId, nodeId, order),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: ['datafiles', 'projects', 'detail', projectId],
}),
});
}
34 changes: 34 additions & 0 deletions client/modules/_hooks/src/datafiles/projects/useProjectListing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useQuery } from '@tanstack/react-query';
import apiClient from '../../apiClient';
import { TBaseProject } from './types';

export type TProjectListingResponse = {
total: number;
result: TBaseProject[];
};

async function getProjectListing({
page = 1,
limit = 100,
signal,
}: {
page: number;
limit: number;
signal: AbortSignal;
}) {
const resp = await apiClient.get<TProjectListingResponse>(
'/api/projects/v2',
{
signal,
params: { offset: (page - 1) * limit, limit },
}
);
return resp.data;
}

export function useProjectListing(page: number, limit: number) {
return useQuery({
queryKey: ['datafiles', 'projects', 'listing', page, limit],
queryFn: ({ signal }) => getProjectListing({ page, limit, signal }),
});
}
33 changes: 33 additions & 0 deletions client/modules/_hooks/src/datafiles/projects/useProjectPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useQuery } from '@tanstack/react-query';
import apiClient from '../../apiClient';
import { TBaseProject, TEntityMeta } from './types';

type TProjectPreviewResponse = {
baseProject: TBaseProject;
entities: TEntityMeta[];
tree: unknown;
};

async function getProjectPreview({
projectId,
signal,
}: {
projectId: string;
signal: AbortSignal;
}) {
const resp = await apiClient.get<TProjectPreviewResponse>(
`/api/projects/v2/${projectId}/preview/`,
{
signal,
}
);
return resp.data;
}

export function useProjectPreview(projectId: string) {
return useQuery({
queryKey: ['datafiles', 'projects', 'detail', projectId, 'preview'],
queryFn: ({ signal }) => getProjectPreview({ projectId, signal }),
enabled: !!projectId,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import apiClient from '../../apiClient';

async function removeEntity(projectId: string, nodeId: string) {
const res = await apiClient.delete(
`/api/projects/v2/${projectId}/entities/associations/${nodeId}/`
);
return res.data;
}

export function useRemoveEntityFromTree(projectId: string, nodeId: string) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => removeEntity(projectId, nodeId),
onSuccess: () =>
queryClient.invalidateQueries({
queryKey: ['datafiles', 'projects', 'detail', projectId],
}),
});
}
Loading

0 comments on commit 4701e11

Please sign in to comment.