diff --git a/client/modules/_hooks/src/datafiles/nees/index.ts b/client/modules/_hooks/src/datafiles/nees/index.ts index 98ae74a209..a9b8ef53da 100644 --- a/client/modules/_hooks/src/datafiles/nees/index.ts +++ b/client/modules/_hooks/src/datafiles/nees/index.ts @@ -1,2 +1,5 @@ export { useNeesListing } from './useNeesListing'; export type { TNeesListingItem } from './useNeesListing'; + +export { useNeesDetails } from './useNeesDetails'; +export type { TNeesDetailsItem } from './useNeesDetails'; diff --git a/client/modules/_hooks/src/datafiles/nees/useNeesDetails.ts b/client/modules/_hooks/src/datafiles/nees/useNeesDetails.ts new file mode 100644 index 0000000000..319fd00c86 --- /dev/null +++ b/client/modules/_hooks/src/datafiles/nees/useNeesDetails.ts @@ -0,0 +1,112 @@ +import { useQuery } from '@tanstack/react-query'; +import apiClient from '../../apiClient'; + +export type TNeesDetailsItem = { + agavePath: string; + children: Record[]; + deleted: boolean; + format: string; + length: number; + name: string; + path: string; + permissions: string; + system: string; + systemID: string; + type: string; + metadata: { + experiments: TNeesExperimentMetadata[]; + project: TNeesProjectMetadata; + }; +}; + +export type TNeesExperimentMetadata = { + creators: { + lastName: string; + firstName: string; + }[]; + doi: string; + startDate: string; + endDate: string; + description: string; + title: string; + deleted: boolean; + material: { + materials: string[]; + component: string; + }[]; + facility: { + country: string; + state: string; + name: string; + }[]; + equipment: { + equipment: string; + component: string; + equipmentClass: string; + facility: string; + }[]; + path: string; + sensors: string[]; + type: string; + specimenType: { + name: string; + }[]; + name: string; +}; + +export type TNeesProjectMetadata = { + description: string; + endDate: string; + startDate: string; + facility: { + country: string; + state: string; + name: string; + }[]; + name: string; + organization: { + country: string; + state: string; + name: string; + }[]; + pis: { + firstName: string; + lastName: string; + }[]; + project: string; + publications: { + authors: string[]; + title: string; + }[]; + system: string; + title: string; + sponsor: { + url: string; + name: string; + }[]; + systemId: string; +}; + +async function getNeesDetails({ + neesId, + signal, +}: { + neesId: string; + signal: AbortSignal; +}) { + const resp = await apiClient.get( + `/api/projects/nees-publication/${neesId}/`, + { + signal, + } + ); + return resp.data; +} + +export function useNeesDetails(neesId: string) { + return useQuery({ + queryKey: ['datafiles', 'nees', 'details', neesId], + queryFn: ({ signal }) => getNeesDetails({ neesId, signal }), + enabled: !!neesId, + }); +} diff --git a/client/modules/_hooks/src/datafiles/useFileListingRouteParams.ts b/client/modules/_hooks/src/datafiles/useFileListingRouteParams.ts index 6cfe9fdef6..b0d5f706ed 100644 --- a/client/modules/_hooks/src/datafiles/useFileListingRouteParams.ts +++ b/client/modules/_hooks/src/datafiles/useFileListingRouteParams.ts @@ -24,7 +24,7 @@ export function useFileListingRouteParams() { api, scheme, system: system ?? '', - path: encodeURIComponent(path ?? ''), + path: path ?? '', }; } diff --git a/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx b/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx index e38e86ba0a..096e50550f 100644 --- a/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx +++ b/client/modules/datafiles/src/DatafilesBreadcrumb/DatafilesBreadcrumb.tsx @@ -6,27 +6,18 @@ import { getSystemRootDisplayName, useAuthenticatedUser } from '@client/hooks'; function getPathRoutes( baseRoute: string, path: string = '', - systemRoot: string = '', - systemRootAlias?: string + relativeTo: string = '' ) { - const pathComponents = decodeURIComponent(path.replace(systemRoot, '')) + const pathComponents = path + .replace(relativeTo, '') .split('/') .filter((p) => !!p); - - const systemRootBreadcrumb = { - path: `${baseRoute}/${systemRoot}`, - title: systemRootAlias ?? 'Data Files', - }; - - return [ - systemRootBreadcrumb, - ...pathComponents.slice(systemRoot ? 1 : 0).map((comp, i) => ({ - title: comp, - path: `${baseRoute}/${systemRoot}${encodeURIComponent( - '/' + pathComponents.slice(0, i + 1).join('/') - )}`, - })), - ]; + return pathComponents.map((comp, i) => ({ + title: comp, + path: `${baseRoute}/${encodeURIComponent(relativeTo)}${encodeURIComponent( + '/' + pathComponents.slice(0, i + 1).join('/') + )}`, + })); } export const DatafilesBreadcrumb: React.FC< @@ -36,7 +27,6 @@ export const DatafilesBreadcrumb: React.FC< baseRoute: string; systemRoot: string; systemRootAlias?: string; - skipBreadcrumbs?: number; // Number of path elements to skip when generating breadcrumbs } & BreadcrumbProps > = ({ initialBreadcrumbs, @@ -44,14 +34,11 @@ export const DatafilesBreadcrumb: React.FC< baseRoute, systemRoot, systemRootAlias, - skipBreadcrumbs, ...props }) => { const breadcrumbItems = [ ...initialBreadcrumbs, - ...getPathRoutes(baseRoute, path, systemRoot, systemRootAlias).slice( - skipBreadcrumbs ?? 0 - ), + ...getPathRoutes(baseRoute, path, systemRoot), ]; return ( @@ -87,15 +74,20 @@ export const BaseFileListingBreadcrumb: React.FC< ...props }) => { const { user } = useAuthenticatedUser(); - + const rootAlias = systemRootAlias || getSystemRootDisplayName(api, system); + const systemRoot = isUserHomeSystem(system) ? '/' + user?.username : ''; return ( diff --git a/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx b/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx index 95fae57d4d..60e3cc075e 100644 --- a/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx +++ b/client/modules/datafiles/src/DatafilesModal/CopyModal/CopyModal.tsx @@ -256,7 +256,7 @@ export const CopyModal: React.FC<{ = ({ + children, +}) => { + const [expanderRef, setExpanderRef] = useState(null); + const [expanded, setExpanded] = useState(false); + const [expandable, setExpandable] = useState(false); + + const expanderRefCallback = useCallback( + (node: HTMLElement) => { + if (node !== null) setExpanderRef(node); + }, + [setExpanderRef] + ); + + useEffect(() => { + const ro = new ResizeObserver((entries) => { + for (const entry of entries) { + setExpandable(entry.target.scrollHeight > entry.target.clientHeight); + } + }); + expanderRef && ro.observe(expanderRef); + return () => { + ro.disconnect(); + }; + }, [setExpandable, expanderRef]); + + return ( +
+ + {children} + + {(expandable || expanded) && ( + + )} +
+ ); +}; + +export const NeesDetails: React.FC<{ neesId: string }> = ({ neesId }) => { + const { data } = useNeesDetails(neesId); + const neesProjectData = data?.metadata.project; + const neesExperiments = data?.metadata.experiments; + const numDOIs = neesExperiments?.filter((exp) => !!exp.doi).length || 0; + const routeParams = useParams(); + const path = routeParams.path ?? data?.path; + + const neesCitations = neesExperiments + ?.filter((exp) => !!exp.doi) + .map((u) => { + const authors = u.creators + ?.map((a) => a.lastName + ', ' + a.firstName) + .join('; '); + const doi = u.doi; + const doiUrl = 'https://doi.org/' + doi; + const year = u.endDate + ? u.endDate.split('T')[0].split('-')[0] + : u.startDate.split('T')[0].split('-')[0]; + + return ( +
+ {authors}, ({year}), "{u.title}", DesignSafe-CI [publisher], doi:{' '} + {doi} +
+ {doiUrl} + +
+ ); + }); + + const doiList = () => { + Modal.info({ + title: 'DOIs', + content: neesCitations, + width: 600, + }); + }; + + const experimentsList = neesExperiments?.map((exp) => { + return ( +
+ +
{exp.name}
+ + + + + + + + + + + + + + + {exp.doi ? ( + + + + + ) : ( + + )} + {exp.doi ? ( + + + + + ) : ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + +
Title{exp.title}
Creators + {exp.creators + ? exp.creators?.map((c) => ( +
+ {c.firstName} {c.lastName} +
+ )) + : 'No Creators Listed'} +
DOI{exp.doi}
Citation + {exp.creators + ?.map( + (author) => author.lastName + ', ' + author.firstName + ) + .join('; ')} + , ( + {exp.endDate + ? exp.endDate.split('T')[0].split('-')[0] + : exp.startDate.split('T')[0].split('-')[0]} + ), "{exp.title}", DesignSafe-CI [publisher], doi: {exp.doi} +
Type{exp.type}
Description + {exp.description ? ( + {exp.description} + ) : ( + 'No Description' + )} +
Start Date{exp.startDate}
End Date{exp.endDate ? exp.endDate : 'No End Date'}
Equipment + + + {exp.equipment ? ( + + + + + + + ) : ( + + + + )} + + + {exp.equipment?.map((eq) => ( + + + + + + + ))} + +
EquipmentComponentEquipment ClassFacility
No Equipment Listed
{eq.equipment}{eq.component}{eq.equipmentClass}{eq.facility}
+
Material + {exp.material + ? exp.material?.map((mat) => ( +
+
{mat.component}:
+
+ {mat.materials?.map((mats) => ( +
{mats}
+ ))} +
+
+
+ )) + : 'No Materials Listed '} +
+
+ +
+ ); + }); + + const neesFiles = ( + <> + { + return ( + + {obj.title} + + ); + }} + /> + + + ); + + return ( + <> +
+

+ {neesProjectData?.name}: {neesProjectData?.title} +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +
PIs + {neesProjectData?.pis + ? neesProjectData?.pis.map((u) => ( +
+ {u.firstName} {u.lastName} +
+ )) + : 'No PIs Listed'} +
+
+ + + + + +
Organizations + {neesProjectData?.organization + ? neesProjectData?.organization.map((u) => ( +
+ {u.name} {u.state}, {u.country} +
+ )) + : 'No Organizations Listed'} +
+
+ + + +
+ + + + + +
NEES ID{neesProjectData?.name}
+
+ + + + + +
Sponsors + {neesProjectData?.sponsor + ? neesProjectData?.sponsor?.map((u) => ( +
+ + {u.name} + +
+ )) + : 'No Sponsors Listed'} +
+
+ + + +
+ + + + + + + +
Project TypeNEES
+
+ + + + + + + +
Start Date + {neesProjectData?.startDate + ? neesProjectData?.startDate + : 'No Start Date'} +
+
+ + + +
+ + {numDOIs > 0 ? ( + + + + + ) : ( + + )} + +
DOIs + +
+ + + Description: + + {neesProjectData?.description} + +
+
+ +
+ + ); +}; diff --git a/client/modules/datafiles/src/nees/index.ts b/client/modules/datafiles/src/nees/index.ts index ec5ae9b297..544ad7dd59 100644 --- a/client/modules/datafiles/src/nees/index.ts +++ b/client/modules/datafiles/src/nees/index.ts @@ -1 +1,2 @@ export { NeesListing } from './NeesListing'; +export { NeesDetails } from './NeesDetails'; diff --git a/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.tsx b/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.tsx index 3309cc8e11..e31d49a67c 100644 --- a/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.tsx +++ b/client/modules/datafiles/src/projects/ProjectCitation/ProjectCitation.tsx @@ -6,7 +6,8 @@ export const ProjectCitation: React.FC<{ }> = ({ projectId, entityUuid }) => { const { data } = useProjectDetail(projectId); const entityDetails = data?.entities.find((e) => e.uuid === entityUuid); - const authors = entityDetails?.value.authors ?? []; + const authors = + entityDetails?.value.authors?.filter((a) => a.fname && a.lname) ?? []; if (!data || !entityDetails) return null; return (
diff --git a/client/src/datafiles/datafilesRouter.tsx b/client/src/datafiles/datafilesRouter.tsx index ec036dce18..7df6828c64 100644 --- a/client/src/datafiles/datafilesRouter.tsx +++ b/client/src/datafiles/datafilesRouter.tsx @@ -81,7 +81,7 @@ const datafilesRouter = createBrowserRouter( { path: ':neesid', element: , - children: [{ path: ':path', element: }], + children: [{ path: ':path', element: }], }, ], }, diff --git a/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx b/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx index 269286e60f..0fce687adf 100644 --- a/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx +++ b/client/src/datafiles/layouts/nees/NeesDetailLayout.tsx @@ -1,10 +1,19 @@ import React from 'react'; -import { Outlet } from 'react-router-dom'; +import { Layout } from 'antd'; +import { useParams } from 'react-router-dom'; +import { NeesDetails } from '@client/datafiles'; export const NeesDetailLayout: React.FC = () => { + const { neesid } = useParams(); + if (!neesid) return null; + const nees = neesid?.split('.')[0]; + return ( -
- Placeholder for the NEES detail view. -
+ +
Placeholder for the NEES buttons.
+
+ +
+
); }; diff --git a/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx b/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx index 8b0b06f4a8..cc36e2a2fb 100644 --- a/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx +++ b/client/src/datafiles/layouts/projects/ProjectCurationLayout.tsx @@ -167,7 +167,9 @@ export const ProjectCurationLayout: React.FC = () => {
{ <> { path={path ?? ''} baseRoute={`/public/designsafe.storage.published/${projectId}`} systemRootAlias={projectId} - systemRoot={projectId} - skipBreadcrumbs={1} + systemRoot={`/${projectId}`} itemRender={(obj) => { return ( diff --git a/designsafe/apps/api/datafiles/operations/tapis_operations.py b/designsafe/apps/api/datafiles/operations/tapis_operations.py index 3a86d84bb6..0b9909c3f3 100644 --- a/designsafe/apps/api/datafiles/operations/tapis_operations.py +++ b/designsafe/apps/api/datafiles/operations/tapis_operations.py @@ -49,7 +49,7 @@ def listing(client, system, path, offset=0, limit=100, *args, **kwargs): 'type': 'dir' if f.type == 'dir' else 'file', 'format': 'folder' if f.type == 'dir' else 'raw', 'mimeType': f.mimeType, - 'path': f.path, + 'path': f"/{f.path}", 'name': f.name, 'length': f.size, 'lastModified': f.lastModified,