diff --git a/client/src/components/DataFiles/DataFiles.jsx b/client/src/components/DataFiles/DataFiles.jsx index ff84ace104..756e2884d8 100644 --- a/client/src/components/DataFiles/DataFiles.jsx +++ b/client/src/components/DataFiles/DataFiles.jsx @@ -114,9 +114,8 @@ const DataFilesSwitch = React.memo(() => { { - const decodedPath = getDecodedPath(params.path); - + return ( { - // encode path to handle special characters path = path.split('/').map(encodeURIComponent).join('/'); diff --git a/client/src/components/DataFiles/DataFilesListing/DataFilesListingCells.jsx b/client/src/components/DataFiles/DataFilesListing/DataFilesListingCells.jsx index b5eb54c607..c9fbbe989d 100644 --- a/client/src/components/DataFiles/DataFilesListing/DataFilesListingCells.jsx +++ b/client/src/components/DataFiles/DataFilesListing/DataFilesListingCells.jsx @@ -84,8 +84,9 @@ export const FileNavCell = React.memo( if (!basePath) basePath = isPublic ? '/public-data' : '/workbench/data'; // encoding for % and # in path. Done twice due to react-router encoding bug. fixed in react router v6 - path = path.replace(/%/g, encodeURIComponent(encodeURIComponent('%'))) - .replace(/#/g, encodeURIComponent(encodeURIComponent('#'))); + path = path + .replace(/%/g, encodeURIComponent(encodeURIComponent('%'))) + .replace(/#/g, encodeURIComponent(encodeURIComponent('#'))); return ( <> diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesAddProjectModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesAddProjectModal.jsx index 2552aa5abe..3edde65def 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesAddProjectModal.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesAddProjectModal.jsx @@ -88,7 +88,9 @@ const DataFilesAddProjectModal = () => { username: member.user.username, access: member.access, })), - metadata: DataFilesAddProjectModalAddon ? { title, description, ...values } : null, + metadata: DataFilesAddProjectModalAddon + ? { title, description, ...values } + : null, onCreate, }, }); @@ -153,7 +155,9 @@ const DataFilesAddProjectModal = () => {
} - description={'The title should be descriptive and distinctive from related publications.'} + description={ + 'The title should be descriptive and distinctive from related publications.' + } /> {!!minDescriptionLength && ( { type="textarea" /> )} - {DataFilesAddProjectModalAddon && } + {DataFilesAddProjectModalAddon && ( + + )} { ); }); -export default DataFilesCopyModal; \ No newline at end of file +export default DataFilesCopyModal; diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesFormModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesFormModal.jsx index aafb52b653..3f4a0f9b60 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesFormModal.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesFormModal.jsx @@ -1,6 +1,12 @@ import React, { useCallback } from 'react'; import { useDispatch, useSelector, shallowEqual } from 'react-redux'; -import { Modal, ModalHeader, ModalBody, ModalFooter, FormText } from 'reactstrap'; +import { + Modal, + ModalHeader, + ModalBody, + ModalFooter, + FormText, +} from 'reactstrap'; import { DynamicForm } from '_common/Form/DynamicForm'; import { Formik, Form } from 'formik'; import * as Yup from 'yup'; @@ -14,7 +20,9 @@ const DataFilesFormModal = () => { const location = useLocation(); const reloadPage = (updatedPath = '') => { - const match = location.pathname.match(/^\/workbench\/data\/tapis\/[^\/]+\/[^\/]+\/[^\/]+/); + const match = location.pathname.match( + /^\/workbench\/data\/tapis\/[^\/]+\/[^\/]+\/[^\/]+/ + ); if (!match) return; const projectUrl = match[0]; @@ -73,7 +81,6 @@ const DataFilesFormModal = () => { const validationSchema = Yup.object().shape({ ...(form?.form_fields ?? []).reduce((schema, field) => { - let validator; if (field.type === 'number') { @@ -120,7 +127,7 @@ const DataFilesFormModal = () => { .max( field.validation?.max ?? Infinity, `${field.label} must be less than or equal to ${field.validation?.max} characters` - ) + ); } schema[field.name] = validator; @@ -149,10 +156,7 @@ const DataFilesFormModal = () => { {form?.description && ( - + {form.description} )} diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesPreviewModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesPreviewModal.jsx index f462a4bd6d..682712b472 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesPreviewModal.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesPreviewModal.jsx @@ -103,9 +103,11 @@ const DataFilesPreviewModal = () => { File Preview: {params.name} - {DataFilesPreviewModalAddon && !isLoading && params.scheme === 'projects' && ( - - )} + {DataFilesPreviewModalAddon && + !isLoading && + params.scheme === 'projects' && ( + + )} {(isLoading || (previewUsingHref && isFrameLoading)) && (
diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesProjectEditDescriptionModal.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesProjectEditDescriptionModal.jsx index 5b6dd85b91..9fd95af28d 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesProjectEditDescriptionModal.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesProjectEditDescriptionModal.jsx @@ -87,110 +87,114 @@ const DataFilesProjectEditDescriptionModal = () => { [projectId, dispatch] ); - const [validationSchema, setValidationSchema] = useState(Yup.object().shape({ - title: Yup.string() - .min(3, 'Title must be at least 3 characters') - .max(maxTitleLength, `Title must be at most ${maxTitleLength} characters`) - .required('Please enter a title.'), - description: Yup.string() - .min( - minDescriptionLength, - `Description must be at least ${minDescriptionLength} characters` - ) - .when([], { - is: () => minDescriptionLength > 0, - then: (schema) => schema.required('Please enter a description.'), - otherwise: (schema) => schema.notRequired(), + const [validationSchema, setValidationSchema] = useState( + Yup.object().shape({ + title: Yup.string() + .min(3, 'Title must be at least 3 characters') + .max( + maxTitleLength, + `Title must be at most ${maxTitleLength} characters` + ) + .required('Please enter a title.'), + description: Yup.string() + .min( + minDescriptionLength, + `Description must be at least ${minDescriptionLength} characters` + ) + .when([], { + is: () => minDescriptionLength > 0, + then: (schema) => schema.required('Please enter a description.'), + otherwise: (schema) => schema.notRequired(), + }), + ...(enableWorkspaceKeywords && { + keywords: Yup.array().of(Yup.string()), }), - ...(enableWorkspaceKeywords && { - keywords: Yup.array().of(Yup.string() - ), - }), - })); + }) + ); return ( {/* */} - - {({ isValid, dirty }) => ( -
- - Edit Dataset - - + + {({ isValid, dirty }) => ( + + + Edit Dataset + + + + Title{' '} + + (Maximum {maxTitleLength} characters) + +
+ } + /> + {!!minDescriptionLength && ( - Title{' '} + Description{' '} - (Maximum {maxTitleLength} characters) + (Minimum {minDescriptionLength} characters) } + type="textarea" + className={styles['description-textarea']} /> - {!!minDescriptionLength && ( - - Description{' '} - - (Minimum {minDescriptionLength} characters) - - - } - type="textarea" - className={styles['description-textarea']} - /> - )} - {!!enableWorkspaceKeywords && ( - Keywords} - type="textarea" - className={styles['description-textarea']} - /> - )} - {DataFilesProjectEditDescriptionModalAddon && ( - + )} + {!!enableWorkspaceKeywords && ( + Keywords} + type="textarea" + className={styles['description-textarea']} + /> + )} + {DataFilesProjectEditDescriptionModalAddon && ( + + )} +
+ {updatingError && ( + + Something went wrong. + )} -
- {updatingError && ( - - Something went wrong. - - )} - -
- + +
+
)} diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesProjectTreeModal.module.scss b/client/src/components/DataFiles/DataFilesModals/DataFilesProjectTreeModal.module.scss index 7012ef5ce7..7bb930b9cf 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesProjectTreeModal.module.scss +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesProjectTreeModal.module.scss @@ -1,4 +1,4 @@ .modal-body { - overflow: auto; - max-height: 80vh; + overflow: auto; + max-height: 80vh; } diff --git a/client/src/components/DataFiles/DataFilesModals/DataFilesUploadModalListing/DataFilesUploadModalListingTable.jsx b/client/src/components/DataFiles/DataFilesModals/DataFilesUploadModalListing/DataFilesUploadModalListingTable.jsx index 2fcaa25c7c..5612872051 100644 --- a/client/src/components/DataFiles/DataFilesModals/DataFilesUploadModalListing/DataFilesUploadModalListingTable.jsx +++ b/client/src/components/DataFiles/DataFilesModals/DataFilesUploadModalListing/DataFilesUploadModalListingTable.jsx @@ -50,7 +50,9 @@ function DataFilesUploadModalListingTable({ const { params } = useFileListing('FilesListing'); const portalName = useSelector((state) => state.workbench.portalName); - const { DataFilesUploadModalListingTableAddon } = useAddonComponents({ portalName }); + const { DataFilesUploadModalListingTableAddon } = useAddonComponents({ + portalName, + }); return (
@@ -71,20 +73,21 @@ function DataFilesUploadModalListingTable({ - {DataFilesUploadModalListingTableAddon && params.scheme === 'projects' && ( - - setUploadedFiles((prevFiles) => - prevFiles.map((f) => - f.id === fileId - ? { ...f, is_advanced_image_file: value } - : f + {DataFilesUploadModalListingTableAddon && + params.scheme === 'projects' && ( + + setUploadedFiles((prevFiles) => + prevFiles.map((f) => + f.id === fileId + ? { ...f, is_advanced_image_file: value } + : f + ) ) - ) - } - /> - )} + } + /> + )} diff --git a/client/src/components/DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing.jsx b/client/src/components/DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing.jsx index 1b962ac101..957936889b 100644 --- a/client/src/components/DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing.jsx +++ b/client/src/components/DataFiles/DataFilesProjectFileListing/DataFilesProjectFileListing.jsx @@ -66,7 +66,10 @@ const DataFilesProjectFileListing = ({ ? member.user.username === state.authenticatedUser?.user?.username : { access: null } ) - .map((currentUser) => currentUser.access === 'owner' || currentUser.access === 'edit')[0] + .map( + (currentUser) => + currentUser.access === 'owner' || currentUser.access === 'edit' + )[0] ); const readOnlyTeam = useSelector((state) => { diff --git a/client/src/components/Publications/PublicationDetailPublicView.jsx b/client/src/components/Publications/PublicationDetailPublicView.jsx index 446115f9fe..2127f25922 100644 --- a/client/src/components/Publications/PublicationDetailPublicView.jsx +++ b/client/src/components/Publications/PublicationDetailPublicView.jsx @@ -13,11 +13,9 @@ import NotificationToast from '../Toasts'; import DataFilesUnavailDownloadModal from '../DataFiles/DataFilesModals/DataFilesUnavailDownloadModal'; import DataFilesPublicationDownloadModal from '../DataFiles/DataFilesModals/DataFilesPublicationDownloadModal'; import { getDecodedPath } from '../../utils/datafilesUtil'; -import { PUBLICATIONS } from '../../constants/routes' - +import { PUBLICATIONS } from '../../constants/routes'; function PublicationDetailPublicView({ params }) { - const decodedPath = getDecodedPath(params.path); return ( diff --git a/client/src/components/Publications/PublicationsPublicView.jsx b/client/src/components/Publications/PublicationsPublicView.jsx index 95003305ca..e0f0feb25f 100644 --- a/client/src/components/Publications/PublicationsPublicView.jsx +++ b/client/src/components/Publications/PublicationsPublicView.jsx @@ -12,4 +12,4 @@ function PublicationsPublicView() { ); } -export default PublicationsPublicView \ No newline at end of file +export default PublicationsPublicView; diff --git a/client/src/components/Workbench/AppRouter.jsx b/client/src/components/Workbench/AppRouter.jsx index 3a9d551ef9..4ab8c9b7fb 100644 --- a/client/src/components/Workbench/AppRouter.jsx +++ b/client/src/components/Workbench/AppRouter.jsx @@ -11,7 +11,12 @@ import GoogleDrivePrivacyPolicy from '../ManageAccount/GoogleDrivePrivacyPolicy' import SiteSearch from '../SiteSearch'; import UserNewsBrowse from '../UserNews/UserNewsBrowse'; import UserNewsDetail from '../UserNews/UserNewsDetail'; -import { PublishedDatasetsBrowse, PublishedDatasetDetail, PublishedDatasetEntityDetail, PublishedDatasetsLayout } from '../_custom/drp/PublishedDatasets'; +import { + PublishedDatasetsBrowse, + PublishedDatasetDetail, + PublishedDatasetEntityDetail, + PublishedDatasetsLayout, +} from '../_custom/drp/PublishedDatasets'; function AppRouter() { const dispatch = useDispatch(); @@ -51,35 +56,40 @@ function AppRouter() { - { return ( - ) + ); }} /> { return ( - + - ) + ); }} /> { - return ( - - - - ) + return ( + + + + ); }} /> diff --git a/client/src/components/_common/CMSBreadcrumbs/CMSBreadcrumbs.jsx b/client/src/components/_common/CMSBreadcrumbs/CMSBreadcrumbs.jsx index 6060b87ac8..6b79303a03 100644 --- a/client/src/components/_common/CMSBreadcrumbs/CMSBreadcrumbs.jsx +++ b/client/src/components/_common/CMSBreadcrumbs/CMSBreadcrumbs.jsx @@ -15,10 +15,11 @@ export default function CMSBreadcrumbs({ breadcrumbs = [] }) { // Default breadcrumbs if none provided (maintains backward compatibility) const defaultBreadcrumbs = [ - { name: "Browse Datasets", href: ROUTES.PUBLICATIONS } + { name: 'Browse Datasets', href: ROUTES.PUBLICATIONS }, ]; - const crumbsToRender = breadcrumbs.length > 0 ? breadcrumbs : defaultBreadcrumbs; + const crumbsToRender = + breadcrumbs.length > 0 ? breadcrumbs : defaultBreadcrumbs; return ( <> diff --git a/client/src/components/_common/Form/DynamicForm/DynamicForm.jsx b/client/src/components/_common/Form/DynamicForm/DynamicForm.jsx index 81dc052fac..e65587a373 100644 --- a/client/src/components/_common/Form/DynamicForm/DynamicForm.jsx +++ b/client/src/components/_common/Form/DynamicForm/DynamicForm.jsx @@ -23,7 +23,7 @@ const DynamicForm = ({ initialFormFields, onChange }) => { modifiedField ) => { const { dependency } = field; - const filteredOptions = field.options.filter(option => { + const filteredOptions = field.options.filter((option) => { if (option.value === 'other') { return true; } @@ -204,18 +204,18 @@ const DynamicForm = ({ initialFormFields, onChange }) => { ); }) : // shows only filtered fields - field.filteredOptions - ? field.filteredOptions.map((option) => ( - - )) - : // shows all fields - field.options.map((option) => ( - - ))} + field.filteredOptions + ? field.filteredOptions.map((option) => ( + + )) + : // shows all fields + field.options.map((option) => ( + + ))} ); // uses FieldArray from formik to handle array fields. arrayHelpers from FieldArray is used to add and remove fields @@ -230,7 +230,9 @@ const DynamicForm = ({ initialFormFields, onChange }) => {

{field.label}

{field.description && ( -

{field.description}

+

+ {field.description} +

)}
{values[field.name]?.map((_, index) => ( @@ -241,10 +243,14 @@ const DynamicForm = ({ initialFormFields, onChange }) => { { /> {field?.file_name && ( - Uploaded File:  - + Uploaded File:  + {field.file_name} diff --git a/client/src/components/_common/Wizard/Wizard.tsx b/client/src/components/_common/Wizard/Wizard.tsx index cec40dea78..7f084b021e 100644 --- a/client/src/components/_common/Wizard/Wizard.tsx +++ b/client/src/components/_common/Wizard/Wizard.tsx @@ -159,13 +159,9 @@ function Wizard({ steps, memo, formSubmit }: WizardProps) { const { goToStep } = stepWizardProps; - useEffect( - () => { - goToStep && goToStep(1); - }, - /* eslint-disable-next-line */ - [memo] - ); + useEffect(() => { + goToStep && goToStep(1); + }, [memo]); return ( diff --git a/client/src/components/_custom/drp/DataFilesAddProjectModalAddon/DataFilesAddProjectModalAddon.jsx b/client/src/components/_custom/drp/DataFilesAddProjectModalAddon/DataFilesAddProjectModalAddon.jsx index 79b67255ea..69e83011be 100644 --- a/client/src/components/_custom/drp/DataFilesAddProjectModalAddon/DataFilesAddProjectModalAddon.jsx +++ b/client/src/components/_custom/drp/DataFilesAddProjectModalAddon/DataFilesAddProjectModalAddon.jsx @@ -4,8 +4,7 @@ import { fetchUtil } from 'utils/fetchUtil'; import { DynamicForm } from '_common/Form/DynamicForm'; const DataFilesAddProjectModalAddon = () => { - - const getProjectFormAddon = async() => { + const getProjectFormAddon = async () => { const response = await fetchUtil({ url: '/api/forms', params: { @@ -14,7 +13,7 @@ const DataFilesAddProjectModalAddon = () => { }); return response; - } + }; const useProjectFormAddon = () => { const query = useQuery({ @@ -22,7 +21,7 @@ const DataFilesAddProjectModalAddon = () => { queryFn: getProjectFormAddon, }); return query; - } + }; const { data: form, isLoading } = useProjectFormAddon(); diff --git a/client/src/components/_custom/drp/DataFilesManageProjectModalAddon/DataFilesManageProjectModalAddon.jsx b/client/src/components/_custom/drp/DataFilesManageProjectModalAddon/DataFilesManageProjectModalAddon.jsx index a9c5c0c7ac..2bfcaf020e 100644 --- a/client/src/components/_custom/drp/DataFilesManageProjectModalAddon/DataFilesManageProjectModalAddon.jsx +++ b/client/src/components/_custom/drp/DataFilesManageProjectModalAddon/DataFilesManageProjectModalAddon.jsx @@ -162,11 +162,12 @@ const DataFilesManageProjectModalAddon = ({ projectId }) => { {!readOnlyTeam && ( <> -
+
- Collaborators who do not have TACC accounts. These authors won’t have access to the portal but will be included as authors. + Collaborators who do not have TACC accounts. These authors won’t + have access to the portal but will be included as authors.
{ const dispatch = useDispatch(); const history = useHistory(); const location = useLocation(); - const [isAdvancedImageFile, setIsAdvancedImageFile] = useState(metadata?.is_advanced_image_file ?? false); + const [isAdvancedImageFile, setIsAdvancedImageFile] = useState( + metadata?.is_advanced_image_file ?? false + ); const [expandIsOpen, setExpandIsOpen] = useState(false); // regex from old digitalrocks portal @@ -36,9 +44,11 @@ const DataFilesPreviewModalAddon = ({ metadata }) => { const { ...file } = useSelector((state) => state.files.modalProps.preview); - const { is_review_project, is_published_project } = useSelector((state) => state.projects.metadata); + const { is_review_project, is_published_project } = useSelector( + (state) => state.projects.metadata + ); - const getEditFileForm = async() => { + const getEditFileForm = async () => { const response = await fetchUtil({ url: 'api/forms', params: { @@ -47,7 +57,7 @@ const DataFilesPreviewModalAddon = ({ metadata }) => { }); return response; - } + }; const useEditFileForm = () => { const query = useQuery({ @@ -55,7 +65,7 @@ const DataFilesPreviewModalAddon = ({ metadata }) => { queryFn: getEditFileForm, }); return query; - } + }; const { data: form, isLoading } = useEditFileForm(); @@ -76,7 +86,7 @@ const DataFilesPreviewModalAddon = ({ metadata }) => { history.replace(location.pathname); }; - const onMetadataRemove = ( resetForm ) => { + const onMetadataRemove = (resetForm) => { resetForm(); setIsAdvancedImageFile(false); @@ -89,7 +99,7 @@ const DataFilesPreviewModalAddon = ({ metadata }) => { selectedFile: file, }, }); - } + }; const handleSubmit = (values) => { Object.keys(values).forEach((key) => { @@ -112,60 +122,65 @@ const DataFilesPreviewModalAddon = ({ metadata }) => { <> {!isLoading && !is_review_project && !is_published_project && (
- {!isAdvancedImageFile && ( -
- -
- )} + {!isAdvancedImageFile && ( +
+ +
+ )} {form && isAdvancedImageFile && ( - - {({ resetForm }) => ( + detail="Metadata" + isOpenDefault={expandIsOpen} + message={ + <> + + {({ resetForm }) => (
-
- +
+ +
+ } + /> + {form?.footer && ( +
+ +
- } - /> - {form?.footer && ( -
- - -
- )} - - )} -
- - } - /> + )} + + )} + + + } + /> )}
)} - ); + ); }; export default DataFilesPreviewModalAddon; diff --git a/client/src/components/_custom/drp/DataFilesProjectEditDescriptionModalAddon/DataFilesProjectEditDescriptionModalAddon.jsx b/client/src/components/_custom/drp/DataFilesProjectEditDescriptionModalAddon/DataFilesProjectEditDescriptionModalAddon.jsx index e8c9804e9d..364c653245 100644 --- a/client/src/components/_custom/drp/DataFilesProjectEditDescriptionModalAddon/DataFilesProjectEditDescriptionModalAddon.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectEditDescriptionModalAddon/DataFilesProjectEditDescriptionModalAddon.jsx @@ -90,7 +90,7 @@ const DataFilesProjectEditDescriptionModalAddon = ({ setValidationSchema }) => { .max( subField.validation?.max ?? Infinity, `${subField.label} must be less than or equal to ${subField.validation?.max} characters` - ) + ); } return subAcc; diff --git a/client/src/components/_custom/drp/DataFilesProjectFileListingMetadataAddon/DataFilesProjectFileListingMetadataAddon.jsx b/client/src/components/_custom/drp/DataFilesProjectFileListingMetadataAddon/DataFilesProjectFileListingMetadataAddon.jsx index b51a36ecd8..1ae3c25544 100644 --- a/client/src/components/_custom/drp/DataFilesProjectFileListingMetadataAddon/DataFilesProjectFileListingMetadataAddon.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectFileListingMetadataAddon/DataFilesProjectFileListingMetadataAddon.jsx @@ -19,7 +19,9 @@ const DataFilesProjectFileListingMetadataAddon = ({ const dispatch = useDispatch(); const { portalName } = useSelector((state) => state.workbench); - const { value: tree, error } = useSelector((state) => state.publications.tree); + const { value: tree, error } = useSelector( + (state) => state.publications.tree + ); const { loading } = useFileListing('FilesListing'); useEffect(() => { @@ -50,7 +52,9 @@ const DataFilesProjectFileListingMetadataAddon = ({ ), license: license ?? 'None', ...(doi && { doi }), - ...(keywords && { keywords: Array.isArray(keywords) ? keywords.join(', ') : keywords }), + ...(keywords && { + keywords: Array.isArray(keywords) ? keywords.join(', ') : keywords, + }), ...(cover_image && { cover_image }), ...(file_url && { file_url }), }; @@ -82,7 +86,8 @@ const DataFilesProjectFileListingMetadataAddon = ({ return ( <> - {!loading && tree && + {!loading && + tree && (folderMetadata ? ( <> {!!folderMetadata.description && ( diff --git a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ProjectDescription.jsx b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ProjectDescription.jsx index 126f159d53..6024857ac0 100644 --- a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ProjectDescription.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ProjectDescription.jsx @@ -52,7 +52,12 @@ const ProjectDescription = ({ project }) => { if (project.cover_image) { projectData['Cover Image'] = ( - + {project.cover_image.split('/').pop()} ); @@ -168,7 +173,9 @@ const ProjectDescription = ({ project }) => {

Dataset metadata has the following errors:

    {Object.keys(errors).map((key) => ( -
  • {errors[key]}
  • +
  • + {errors[key]} +
  • ))}
@@ -180,11 +187,11 @@ const ProjectDescription = ({ project }) => { const validateProjectMetadata = (values) => { const errors = {}; - + if (!values.title) { errors.title = 'Title is required'; } - + if (!values.description) { errors.description = 'Description is required'; } @@ -192,7 +199,7 @@ const validateProjectMetadata = (values) => { if (!values.cover_image) { errors.cover_image = 'Cover image is required'; } - + return errors; }; diff --git a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ProjectTreeView.jsx b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ProjectTreeView.jsx index 9a642d8557..8b39ab5752 100644 --- a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ProjectTreeView.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ProjectTreeView.jsx @@ -1,10 +1,5 @@ import React, { useEffect, useState, useCallback } from 'react'; -import { - Button, - ShowMore, - Section, - Icon, -} from '_common'; +import { Button, ShowMore, Section, Icon } from '_common'; import { TreeItem2 as TreeItem, SimpleTreeView } from '@mui/x-tree-view'; import styles from './DataFilesProjectPublishWizard.module.scss'; import DataDisplay from '../../utils/DataDisplay/DataDisplay'; @@ -21,14 +16,14 @@ const theme = createTheme({ MuiTreeItem2: { styleOverrides: { root: { - "& > .MuiTreeItem-content.Mui-selected": { + '& > .MuiTreeItem-content.Mui-selected': { backgroundColor: 'transparent', - } + }, }, - } - } - } -}) + }, + }, + }, +}); export const ProjectTreeView = ({ projectId, readOnly = false }) => { const history = useHistory(); @@ -40,12 +35,13 @@ export const ProjectTreeView = ({ projectId, readOnly = false }) => { const [tree, setTree] = useState([]); - const { dynamicFormModal, previewModal, projectTreeModal, metadata } = useSelector((state) => ({ - dynamicFormModal: state.files.modals.dynamicform, - previewModal: state.files.modals.preview, - projectTreeModal: state.files.modals.projectTree, - metadata: state.projects.metadata, - })); + const { dynamicFormModal, previewModal, projectTreeModal, metadata } = + useSelector((state) => ({ + dynamicFormModal: state.files.modals.dynamicform, + previewModal: state.files.modals.preview, + projectTreeModal: state.files.modals.projectTree, + metadata: state.projects.metadata, + })); const fetchTree = useCallback(async () => { if (projectId) { @@ -77,7 +73,7 @@ export const ProjectTreeView = ({ projectId, readOnly = false }) => { const findNodeByPath = (nodes, targetPath, parentIds = []) => { if (!nodes || !Array.isArray(nodes)) return null; - targetPath = targetPath.replace(/\/+$/, '') + targetPath = targetPath.replace(/\/+$/, ''); for (const node of nodes) { const currentPath = (node.path || '').replace(/\/+$/, ''); @@ -89,13 +85,21 @@ export const ProjectTreeView = ({ projectId, readOnly = false }) => { // search in children if (node.children && node.children.length > 0) { - const result = findNodeByPath(node.children, targetPath, currentParentIds); + const result = findNodeByPath( + node.children, + targetPath, + currentParentIds + ); if (result) return result; } // search in fileObjs if (node.fileObjs && node.fileObjs.length > 0) { - const result = findNodeByPath(node.fileObjs, targetPath, currentParentIds); + const result = findNodeByPath( + node.fileObjs, + targetPath, + currentParentIds + ); if (result) return result; } } @@ -107,14 +111,14 @@ export const ProjectTreeView = ({ projectId, readOnly = false }) => { if (tree && tree.length > 0) { const regex = /^.*?\/projects\/[^/]+\/[^/]+/; const baseUrlMatch = location.pathname.match(regex); - + if (baseUrlMatch) { const baseUrl = baseUrlMatch[0]; const nodePath = location.pathname.substring(baseUrl.length + 1); - + // Find the node by path and get all parent IDs const parentIds = findNodeByPath(tree, nodePath); - + if (parentIds && parentIds.length > 0) { setExpandedNodes(parentIds); } else { @@ -187,7 +191,6 @@ export const ProjectTreeView = ({ projectId, readOnly = false }) => { }; const onGoTo = (node) => { - const regex = /^.*?\/projects\/[^/]+\/[^/]+/; const baseUrl = location.pathname.match(regex)[0]; @@ -212,8 +215,7 @@ export const ProjectTreeView = ({ projectId, readOnly = false }) => { .join(' '); const renderTree = (node) => { - - let treeItemSlots; + let treeItemSlots; if (node.children && node.children.length > 0) { treeItemSlots = { @@ -227,103 +229,103 @@ export const ProjectTreeView = ({ projectId, readOnly = false }) => { } return ( - <> -
-
- - {node.label ?? node.name} - {node.metadata.data_type && ( - - {formatDatatype(node.metadata.data_type)} - - )} -
- } - classes={{ - label: styles['tree-label'], - }} - slots={treeItemSlots} - > - {expandedNodes.includes(node.id) && node.id !== 'NODE_ROOT' && ( -
-
- {(!readOnly || node.metadata.data_type === 'file') && ( - - )} - {(!readOnly || node.metadata.data_type === 'file') && ( - | - )} - {( - + <> +
+
+ + {node.label ?? node.name} + {node.metadata.data_type && ( + + {formatDatatype(node.metadata.data_type)} + )}
-
- - {node.metadata.description} - - + } + classes={{ + label: styles['tree-label'], + }} + slots={treeItemSlots} + > + {expandedNodes.includes(node.id) && node.id !== 'NODE_ROOT' && ( +
+
+ {(!readOnly || node.metadata.data_type === 'file') && ( + + )} + {(!readOnly || node.metadata.data_type === 'file') && ( + | + )} + { + + } +
+
+ + {node.metadata.description} + + +
-
- )} - {Array.isArray(node.fileObjs) && - node.fileObjs.map((fileObj) => ( - - {renderTree(fileObj)} - - ))} - {Array.isArray(node.children) && - node.children.map((child) => ( - - {renderTree(child)} - - ))} - -
-
- - ); -} + )} + {Array.isArray(node.fileObjs) && + node.fileObjs.map((fileObj) => ( + + {renderTree(fileObj)} + + ))} + {Array.isArray(node.children) && + node.children.map((child) => ( + + {renderTree(child)} + + ))} + + + + + ); + }; - return (tree && - tree.length > 0 && ( - - - {tree.map((node) => ( - - {renderTree(node)} - - ))} - - - )); -}; \ No newline at end of file + return ( + tree && + tree.length > 0 && ( + + + {tree.map((node) => ( + {renderTree(node)} + ))} + + + ) + ); +}; diff --git a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/PublicationInstructions.jsx b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/PublicationInstructions.jsx index 10bc8a3c43..9052ed97c4 100644 --- a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/PublicationInstructions.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/PublicationInstructions.jsx @@ -12,7 +12,8 @@ const PublicationInstructions = () => { You are requesting to publish this project. By publishing your project, it will be available to anyone to view and download the project data and metadata. - Please note: Once a project is published, any changes to published files/data requires a new version + Please note: Once a project is published, any changes to + published files/data requires a new version

You will begin the process of reviewing your data publication. This diff --git a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewAuthors.jsx b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewAuthors.jsx index 3a9ca024d5..5d017c0f92 100644 --- a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewAuthors.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewAuthors.jsx @@ -1,9 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { - Button, - SectionTableWrapper, - Section, -} from '_common'; +import { Button, SectionTableWrapper, Section } from '_common'; import styles from './DataFilesProjectPublishWizard.module.scss'; import ReorderUserList from '../../utils/ReorderUserList/ReorderUserList'; import ProjectMembersList from '../../utils/ProjectMembersList/ProjectMembersList'; @@ -27,7 +23,9 @@ const ACMCitation = ({ project, authors }) => { const authorString = authors .map((a) => `${a.first_name} ${a.last_name}`) .join(', '); - const createdDate = new Date(project.publication_date || project.created).toLocaleDateString('en-US', { + const createdDate = new Date( + project.publication_date || project.created + ).toLocaleDateString('en-US', { month: 'long', year: 'numeric', }); @@ -72,7 +70,9 @@ const BibTeXCitation = ({ project, authors }) => { const projectUrl = project.doi ? `https://www.doi.org/${project.doi}` : `DOI link will appear after publication`; - const year = new Date(project.publication_date || project.created).getFullYear(); + const year = new Date( + project.publication_date || project.created + ).getFullYear(); return (

{`@misc{dataset,
@@ -90,11 +90,14 @@ export const MLACitation = ({ project, authors }) => {
   const authorString = authors
     .map((a) => `${a.last_name}, ${a.first_name}`)
     .join(', ');
-  const createdDate = new Date(project.publication_date).toLocaleDateString('en-GB', {
-    day: 'numeric',
-    month: 'short',
-    year: 'numeric',
-  });
+  const createdDate = new Date(project.publication_date).toLocaleDateString(
+    'en-GB',
+    {
+      day: 'numeric',
+      month: 'short',
+      year: 'numeric',
+    }
+  );
   const accessDate = new Date().toLocaleDateString('en-GB', {
     day: 'numeric',
     month: 'short',
@@ -103,7 +106,8 @@ export const MLACitation = ({ project, authors }) => {
 
   return (
     
- {`${authorString}. "${project.title}."`} Digital Porous Media Portal,{' '} + {`${authorString}. "${project.title}."`}{' '} + Digital Porous Media Portal,{' '} {` Digital Porous Media, ${createdDate}, `} {` Accessed ${accessDate}.`} @@ -123,8 +127,7 @@ const IEEECitation = ({ project, authors }) => { return (
{`[1] ${authorString}, "${project.title}",`}{' '} - Digital Porous Media Portal,{' '} - {` ${year}. [Online]. Available: `} + Digital Porous Media Portal, {` ${year}. [Online]. Available: `} {`. [Accessed: ${day}-${month}-${year}]`}
@@ -160,7 +163,11 @@ export const Citations = ({ project, authors }) => (
); -const ReviewAuthors = ({ project, onAuthorsUpdate, isReviewProject = false }) => { +const ReviewAuthors = ({ + project, + onAuthorsUpdate, + isReviewProject = false, +}) => { const [authors, setAuthors] = useState([]); const [members, setMembers] = useState([]); @@ -184,13 +191,16 @@ const ReviewAuthors = ({ project, onAuthorsUpdate, isReviewProject = false }) => const projectMembers = project.members || []; const guestUsers = project.guest_users || []; - const initialAuthors = isReviewProject && project.authors.length > 0 ? project.authors : [ - ...projectMembers.map((member) => ({ - ...member.user, - isOwner: member.access === 'owner', - })), - ...guestUsers, - ]; + const initialAuthors = + isReviewProject && project.authors.length > 0 + ? project.authors + : [ + ...projectMembers.map((member) => ({ + ...member.user, + isOwner: member.access === 'owner', + })), + ...guestUsers, + ]; setAuthors(initialAuthors); setMembers([]); @@ -263,9 +273,19 @@ const ReviewAuthors = ({ project, onAuthorsUpdate, isReviewProject = false }) => ); }; -export const ReviewAuthorsStep = ({ project, onAuthorsUpdate, isReviewProject = false }) => ({ +export const ReviewAuthorsStep = ({ + project, + onAuthorsUpdate, + isReviewProject = false, +}) => ({ id: 'project_authors', name: 'Review Authors and Citations', - render: , + render: ( + + ), initialValues: {}, }); diff --git a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewProjectStructure.jsx b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewProjectStructure.jsx index a7ee4abca2..4840222ae1 100644 --- a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewProjectStructure.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewProjectStructure.jsx @@ -63,10 +63,16 @@ export const ReviewProjectStructure = ({ projectTree }) => {

Dataset structure has the following errors:

    {Object.keys(errors).map((key) => ( -
  • {errors[key]}
  • +
  • + {errors[key]} +
  • ))}
-

Please delete {Object.keys(errors).length === 1 ? 'entity' : 'entities'} or upload files to continue

+

+ Please delete{' '} + {Object.keys(errors).length === 1 ? 'entity' : 'entities'} or + upload files to continue +

)}
@@ -95,39 +101,40 @@ export const ReviewProjectStructure = ({ projectTree }) => { const validateFolder = (node) => { const errors = []; - + const hasFileObjs = (node) => { // Check if the current node has fileObjs if (node.fileObjs && node.fileObjs.length > 0) { return true; } - + // Check if any children have fileObjs if (node.children && node.children.length > 0) { - return node.children.some(child => hasFileObjs(child)); + return node.children.some((child) => hasFileObjs(child)); } - + return false; - } - + }; + // Process the current node if (!hasFileObjs(node)) { - errors.push(`Entity "${node.label}" (path: /${node.path}) has no files in itself or any of its child entities.`); + errors.push( + `Entity "${node.label}" (path: /${node.path}) has no files in itself or any of its child entities.` + ); } - + // Recursively validate all children if (node.children && node.children.length > 0) { - node.children.forEach(child => { + node.children.forEach((child) => { const childErrors = validateFolder(child); errors.push(...childErrors); }); } - + return errors; -} +}; const validateProjectStructure = (tree) => { - const validationErrors = []; tree.forEach((node) => { @@ -135,7 +142,7 @@ const validateProjectStructure = (tree) => { if (nodeErrors.length > 0) { validationErrors.push(...nodeErrors); } - }) + }); const errors = {}; @@ -143,12 +150,11 @@ const validateProjectStructure = (tree) => { validationErrors.forEach((error, index) => { const errorKey = `folder_${index}`; errors[errorKey] = error; - } - ); + }); } return errors; -} +}; export const ReviewProjectStructureStep = ({ projectTree }) => ({ id: 'project_structure', diff --git a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/SubmitPublicationRequest.jsx b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/SubmitPublicationRequest.jsx index 4f2f9c3e38..daec23857d 100644 --- a/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/SubmitPublicationRequest.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/SubmitPublicationRequest.jsx @@ -46,14 +46,13 @@ const SubmitPublicationRequest = ({ callbackUrl }) => { }); }, [values]); - const onSubmit = () => { setValues({ ...values, formSubmitted: true, }); submitForm(); - } + }; return ( { const [submitDisabled, setSubmitDisabled] = useState(false); - const { canPublish = false } = useSelector((state) => state.workbench.config) || {}; + const { canPublish = false } = + useSelector((state) => state.workbench.config) || {}; const { isApproveLoading, diff --git a/client/src/components/_custom/drp/DataFilesProjectReview/DataFilesProjectReview.jsx b/client/src/components/_custom/drp/DataFilesProjectReview/DataFilesProjectReview.jsx index 65ae24479e..5d1c8f9777 100644 --- a/client/src/components/_custom/drp/DataFilesProjectReview/DataFilesProjectReview.jsx +++ b/client/src/components/_custom/drp/DataFilesProjectReview/DataFilesProjectReview.jsx @@ -50,7 +50,11 @@ const DataFilesProjectReview = ({ rootSystem, system }) => { const wizardSteps = [ ProjectDescriptionStep({ project: metadata }), ReviewProjectStructureStep({ projectTree: tree }), - ReviewAuthorsStep({ project: metadata, onAuthorsUpdate: () => {}, isReviewProject: true }), + ReviewAuthorsStep({ + project: metadata, + onAuthorsUpdate: () => {}, + isReviewProject: true, + }), SubmitPublicationReviewStep({ callbackUrl: `${ROUTES.WORKBENCH}${ROUTES.DATA}/tapis/projects/${rootSystem}`, }), diff --git a/client/src/components/_custom/drp/DataFilesUploadModalAddon/DataFilesUploadModalAddon.jsx b/client/src/components/_custom/drp/DataFilesUploadModalAddon/DataFilesUploadModalAddon.jsx index d7a09f280e..01fd6344bc 100644 --- a/client/src/components/_custom/drp/DataFilesUploadModalAddon/DataFilesUploadModalAddon.jsx +++ b/client/src/components/_custom/drp/DataFilesUploadModalAddon/DataFilesUploadModalAddon.jsx @@ -47,10 +47,11 @@ const DataFilesUploadModalAddon = ({ uploadedFiles, setUploadedFiles }) => { if (uploadedFile.data.name === file.data.name) { return { ...uploadedFile, - metadata: { + metadata: { name: uploadedFile.data.name, is_advanced_image_file: uploadedFile.is_advanced_image_file, - ...values }, + ...values, + }, }; } return uploadedFile; diff --git a/client/src/components/_custom/drp/DataFilesUploadModalListingTableAddon/DataFilesUploadModalListingTableAddon.jsx b/client/src/components/_custom/drp/DataFilesUploadModalListingTableAddon/DataFilesUploadModalListingTableAddon.jsx index acfe6f09d0..1d9f56542f 100644 --- a/client/src/components/_custom/drp/DataFilesUploadModalListingTableAddon/DataFilesUploadModalListingTableAddon.jsx +++ b/client/src/components/_custom/drp/DataFilesUploadModalListingTableAddon/DataFilesUploadModalListingTableAddon.jsx @@ -3,23 +3,24 @@ import { useQuery } from '@tanstack/react-query'; import { fetchUtil } from 'utils/fetchUtil'; import styles from './DataFilesUploadModalListingTableAddon.module.scss'; +const DataFilesUploadModalListingTableAddon = ({ + file, + onToggleAdvancedImageFile, +}) => { + return ( + <> + + { + onToggleAdvancedImageFile(file.id, e.target.checked); + }} + className={styles['input']} + /> + Advanced Image File + + + ); +}; -const DataFilesUploadModalListingTableAddon = ({ file, onToggleAdvancedImageFile }) => { - - return ( - <> - - { - onToggleAdvancedImageFile(file.id, e.target.checked); - }} - className={styles['input']} - /> - Advanced Image File - - - ) -} - -export default DataFilesUploadModalListingTableAddon; \ No newline at end of file +export default DataFilesUploadModalListingTableAddon; diff --git a/client/src/components/_custom/drp/DataFilesUploadModalListingTableAddon/DataFilesUploadModalListingTableAddon.module.scss b/client/src/components/_custom/drp/DataFilesUploadModalListingTableAddon/DataFilesUploadModalListingTableAddon.module.scss index 5ed84dd8b4..e0dc97711a 100644 --- a/client/src/components/_custom/drp/DataFilesUploadModalListingTableAddon/DataFilesUploadModalListingTableAddon.module.scss +++ b/client/src/components/_custom/drp/DataFilesUploadModalListingTableAddon/DataFilesUploadModalListingTableAddon.module.scss @@ -1,8 +1,8 @@ .input { - margin-right: 5px; + margin-right: 5px; } .span { - display: flex; - align-items: center; + display: flex; + align-items: center; } diff --git a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetBreadcrumbs.jsx b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetBreadcrumbs.jsx index 860b87c914..698d19991b 100644 --- a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetBreadcrumbs.jsx +++ b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetBreadcrumbs.jsx @@ -6,10 +6,13 @@ import * as ROUTES from '../../../../constants/routes'; import { findNodeInTreeById } from '../utils/utils'; const PublishedDatasetBreadcrumbs = ({ params }) => { - const dispatch = useDispatch(); const portalName = useSelector((state) => state.workbench.portalName); - const { value: tree, loading: treeLoading, error: treeError } = useSelector((state) => state.publications.tree); + const { + value: tree, + loading: treeLoading, + error: treeError, + } = useSelector((state) => state.publications.tree); const [breadcrumbs, setBreadcrumbs] = useState([]); useEffect(() => { @@ -25,21 +28,19 @@ const PublishedDatasetBreadcrumbs = ({ params }) => { useEffect(() => { const buildBreadcrumbs = () => { - const crumbs = [ - { name: "Browse Datasets", href: ROUTES.PUBLICATIONS } - ]; + const crumbs = [{ name: 'Browse Datasets', href: ROUTES.PUBLICATIONS }]; if (params?.page_type === 'datasetDetail') { crumbs.push({ name: tree.label }); } - + if (params?.page_type === 'entityDetail') { - crumbs.push({ - name: tree.label, - href: `${ROUTES.PUBLICATIONS}/${params.system}` + crumbs.push({ + name: tree.label, + href: `${ROUTES.PUBLICATIONS}/${params.system}`, }); - crumbs.push({ - name: findNodeInTreeById(tree, params.entity_id)?.label || 'Entity' + crumbs.push({ + name: findNodeInTreeById(tree, params.entity_id)?.label || 'Entity', }); } diff --git a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetDetail.jsx b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetDetail.jsx index fb4d557e5c..f3d0d9e972 100644 --- a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetDetail.jsx +++ b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetDetail.jsx @@ -2,7 +2,10 @@ import React, { useEffect, useCallback, useState } from 'react'; import PropTypes from 'prop-types'; import { Section, Button, LoadingSpinner } from '_common'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; -import { MLACitation, APACitation } from '../DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewAuthors'; +import { + MLACitation, + APACitation, +} from '../DataFilesProjectPublish/DataFilesProjectPublishWizardSteps/ReviewAuthors'; import * as ROUTES from '../../../../constants/routes'; import { Link } from 'react-router-dom'; import NameWithDesc from '../utils/NameWithDesc/NameWithDesc'; @@ -13,278 +16,313 @@ import { EXCLUDED_METADATA_FIELDS } from '../constants/metadataFields'; const BASE_ASSET_URL = 'https://web.corral.tacc.utexas.edu/digitalporousmedia'; function formatPublicationLink(link) { - if (!link) return null; + if (!link) return null; - const doiPattern = /^10\.\d+\/.+$/; + const doiPattern = /^10\.\d+\/.+$/; - if (doiPattern.test(link)) { - return `https://doi.org/${link}`; - } + if (doiPattern.test(link)) { + return `https://doi.org/${link}`; + } - return link; + return link; } function TreeNode({ node, system }) { - const hasChildren = node.children && node.children.length > 0; + const hasChildren = node.children && node.children.length > 0; - const nodeNameWithDesc = ( - - {formatLabel(node.name.split('.').pop())} - - ); + const nodeNameWithDesc = ( + + {formatLabel(node.name.split('.').pop())} + + ); - return ( - <> - {node.metadata && node.metadata.data_type === 'sample' ? ( -
  • -
    - - - {nodeNameWithDesc} - {' '}{formatLabel(node.label)} - - -

    {node.metadata?.description}

    - - - {Object.entries(node.metadata).map(([key, value]) => { - if (EXCLUDED_METADATA_FIELDS.includes(key)) return null; + return ( + <> + {node.metadata && node.metadata.data_type === 'sample' ? ( +
  • +
    + + + {nodeNameWithDesc} + {formatLabel(node.label)} + + +

    {node.metadata?.description}

    +
  • + + {Object.entries(node.metadata).map(([key, value]) => { + if (EXCLUDED_METADATA_FIELDS.includes(key)) return null; - // TODO: Add description to key if needed by PI - // const keyNameWithDesc = ( - // {formatLabel(key)} - // ); + // TODO: Add description to key if needed by PI + // const keyNameWithDesc = ( + // {formatLabel(key)} + // ); - return ( - - - - - ); - })} - -
    {formatLabel(key)} - {formatLabel(value)} -
    -
    -
  • - ) : ( -
  • - - {formatLabel(node.name.split('.').pop())} - {' '}{formatLabel(node.label)} - -
  • - )} - {hasChildren && ( -
      - {node.children.map((child) => ( - - ))} -
    - )} - - ); + return ( + + {formatLabel(key)} + + {formatLabel(value)} + + + ); + })} + + + + + ) : ( +
  • + + {formatLabel(node.name.split('.').pop())} + {formatLabel(node.label)} + +
  • + )} + {hasChildren && ( +
      + {node.children.map((child) => ( + + ))} +
    + )} + + ); } function PublishedDatasetDetail({ params }) { + const dispatch = useDispatch(); - const dispatch = useDispatch(); - - const { system } = params; - const [projectId, setProjectId] = useState(null); - const portalName = useSelector((state) => state.workbench.portalName); - const { value: tree, loading, error } = useSelector((state) => state.publications.tree); - const metadata = useSelector((state) => state.projects.metadata); - const imageUrl = `${BASE_ASSET_URL}/${metadata.cover_image}`; + const { system } = params; + const [projectId, setProjectId] = useState(null); + const portalName = useSelector((state) => state.workbench.portalName); + const { + value: tree, + loading, + error, + } = useSelector((state) => state.publications.tree); + const metadata = useSelector((state) => state.projects.metadata); + const imageUrl = `${BASE_ASSET_URL}/${metadata.cover_image}`; - useEffect(() => { - dispatch({ - type: 'PROJECTS_GET_METADATA', - payload: system, - }); - setProjectId(system.split('.').pop()); - }, [system]); + useEffect(() => { + dispatch({ + type: 'PROJECTS_GET_METADATA', + payload: system, + }); + setProjectId(system.split('.').pop()); + }, [system]); - useEffect(() => { - if (system && portalName && !error) { - dispatch({ - type: 'PUBLICATIONS_GET_TREE', - payload: { portalName, system }, - }); - } - }, [system, portalName, error]); + useEffect(() => { + if (system && portalName && !error) { + dispatch({ + type: 'PUBLICATIONS_GET_TREE', + payload: { portalName, system }, + }); + } + }, [system, portalName, error]); - return ( -
    - {(metadata.loading || loading) ? ( - - ) : (metadata.error || error) ? ( -
    - Error loading data. Please try again. + return ( +
    + {metadata.loading || loading ? ( + + ) : metadata.error || error ? ( +
    + Error loading data. Please try again. +
    + ) : ( + metadata?.title && ( + <> +
    +

    + {metadata.title} +
    + + +
  • + + Download Dataset{' '} + +
  • +
  • + + Download Metadata{' '} + +
  • +
    - ) : ( - metadata?.title && ( - <> -
    -

    - {metadata.title} -
    -

    +
    +

    Cite This Dataset

    +

    + +

    +

    + Download Citation:{' '} + + Other Formats + +

    +
    +
    +
    +
    + {metadata.cover_image && ( + + {metadata.title} + + )} +
    +
    +

    {metadata.description}

    + + + + + + + {metadata.authors.length > 1 && ( + + + + + )} + + + + + + + + + + + + + {metadata.keywords && ( + + + + + )} + {metadata.related_publications && + metadata.related_publications.length > 0 && ( + + + + + )} + +
    Author + {metadata.authors[0].first_name}{' '} + {metadata.authors[0].last_name}{' '} + {metadata.institution + ? `(${metadata.institution})` + : ''} +
    Collaborators + {metadata.authors.slice(1).map((author, index) => ( + <> + {index > 0 &&
    } + {`${author.first_name} ${author.last_name}`} + + ))} +
    Published + {new Date( + metadata.publication_date + ).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + })} +
    License + {metadata.license} +
    + Digital Object Identifier + {metadata.doi}
    Keywords + {metadata.keywords} +
    + Related Publications + + {metadata.related_publications.map( + (publication, index) => ( + <> + {index > 0 &&
    } + - ↓ - {' '} - Download - - -
  • - - Download Dataset{' '} - -
  • -
  • - - Download Metadata{' '} - -
  • -
    - - - -
    -

    Cite This Dataset

    -

    - -

    -

    - Download Citation:{' '} - - Other Formats - -

    -
    -
    -
    -
    - {metadata.cover_image && ( - - {metadata.title} + {publication.publication_title} - )} -
    -
    -

    {metadata.description}

    - - - - - - - {metadata.authors.length > 1 && ( - - - - - )} - - - - - - - - - - - - - {metadata.keywords && ( - - - - - )} - {metadata.related_publications && metadata.related_publications.length > 0 && ( - - - - - )} - -
    - Author - - {metadata.authors[0].first_name} {metadata.authors[0].last_name} {metadata.institution ? `(${metadata.institution})` : ''} -
    - Collaborators - - {metadata.authors.slice(1).map((author, index) => ( - <> - {index > 0 &&
    } - {`${author.first_name} ${author.last_name}`} - - ))} -
    - Published - - {new Date(metadata.publication_date).toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - })} -
    - License - - {metadata.license} -
    - Digital Object Identifier - - {metadata.doi} -
    - Keywords - - {metadata.keywords} -
    - Related Publications - - {metadata.related_publications.map((publication, index) => ( - <> - {index > 0 &&
    } - {publication.publication_title} - - ))} -
    -
    -
    -
    -
    -

    Files and Metadata

    - {tree?.children?.length > 0 ? ( -
      - {tree.children.map((child, index) => ( - - ))} -
    - ) : ( -

    No files available

    - )} -
    -
    -
    - - ))} - - ); + + ) + )} +
    +
    +
    +
    +
    +

    Files and Metadata

    + {tree?.children?.length > 0 ? ( +
      + {tree.children.map((child, index) => ( + + ))} +
    + ) : ( +

    No files available

    + )} +
    +
    +
    + + ) + )} +

    + ); } -export default PublishedDatasetDetail; \ No newline at end of file +export default PublishedDatasetDetail; diff --git a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetEntityDetail.jsx b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetEntityDetail.jsx index 450a363df2..c6ff916603 100644 --- a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetEntityDetail.jsx +++ b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetEntityDetail.jsx @@ -6,316 +6,391 @@ import { useSelector, useDispatch } from 'react-redux'; import createSizeString from 'utils/sizeFormat'; import styles from './PublishedDatasetsLayout.module.css'; import NameWithDesc from '../utils/NameWithDesc/NameWithDesc'; -import { formatLabel, findNodeInTreeById, findNodeInTree, getTooltipDescription } from '../utils/utils'; +import { + formatLabel, + findNodeInTreeById, + findNodeInTree, + getTooltipDescription, +} from '../utils/utils'; import { EXCLUDED_METADATA_FIELDS } from '../constants/metadataFields'; const BASE_ASSET_URL = 'https://web.corral.tacc.utexas.edu/digitalporousmedia'; -const excludedImageMetadataFields = ['is_advanced_image_file', 'data_type', 'name']; +const excludedImageMetadataFields = [ + 'is_advanced_image_file', + 'data_type', + 'name', +]; function PublishedDatasetEntityDetail({ params }) { + const dispatch = useDispatch(); + const location = useLocation(); - const dispatch = useDispatch(); - const location = useLocation(); - - const { system, entity_type: entityType, entity_id: entityID } = params; - const projectId = system.split('.').pop(); - - const projectUrl = `${BASE_ASSET_URL}/${projectId}`; - const portalName = useSelector((state) => state.workbench.portalName); - - const { value: tree, loading, error } = useSelector((state) => state.publications.tree); - const [selectedEntity, setSelectedEntity] = useState(null); - const [fileGroups, setFileGroups] = useState([]); - const [currentPage, setCurrentPage] = useState(1); - - const itemsPerPage = 5; - - const paginationData = useMemo(() => { - const totalPages = Math.ceil(fileGroups.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const currentFileGroups = fileGroups.slice(startIndex, endIndex); - - return { - totalPages, - startIndex, - endIndex, - currentFileGroups - }; - }, [fileGroups, currentPage]); - - - const groupFilesByBaseName = (files) => { - const processedFileSuffixes = ['.thumb.jpg', '.histogram.jpg', '.histogram.csv', '.gif', '.jpg']; - - const groups = files.reduce((map, file) => { - const suffix = processedFileSuffixes.find(suf => file.name.endsWith(suf)); - const baseName = suffix - ? file.name.slice(0, -suffix.length) - : file.name; - - if (!map[baseName]) { - map[baseName] = { raw: null, processed: [] }; - } - - if (suffix) { - map[baseName].processed.push(file); - } else { - map[baseName].raw = file; - } - - return map; - }, {}); - - // Handle standalone processed files (e.g., standalone .gif files) - // If a group has no raw file but has processed files, treat the first processed file as raw - Object.values(groups).forEach(group => { - if (!group.raw && group.processed.length > 0) { - group.raw = group.processed[0]; - group.processed = group.processed.slice(1); - } - }); - - return Object.values(groups); - }; + const { system, entity_type: entityType, entity_id: entityID } = params; + const projectId = system.split('.').pop(); + + const projectUrl = `${BASE_ASSET_URL}/${projectId}`; + const portalName = useSelector((state) => state.workbench.portalName); + + const { + value: tree, + loading, + error, + } = useSelector((state) => state.publications.tree); + const [selectedEntity, setSelectedEntity] = useState(null); + const [fileGroups, setFileGroups] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + + const itemsPerPage = 5; + + const paginationData = useMemo(() => { + const totalPages = Math.ceil(fileGroups.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + const currentFileGroups = fileGroups.slice(startIndex, endIndex); - useEffect(() => { - if (system && portalName && !error) { - dispatch({ - type: 'PUBLICATIONS_GET_TREE', - payload: { portalName, system }, - }); - } - }, [system, portalName]); - - useEffect(() => { - if (tree && !loading && !error && entityID) { - const entity = findNodeInTreeById(tree, entityID); - setSelectedEntity(entity); - } - }, [tree, loading, error, entityID]); - - useEffect(() => { - if (selectedEntity) { - const groups = groupFilesByBaseName(selectedEntity.fileObjs); - setFileGroups(groups); - setCurrentPage(1); // Reset to first page when data changes - } - }, [selectedEntity]); - - const handlePageChange = useCallback((page) => { - setCurrentPage(page); - }, []); - - const entityTypeWithDesc = ( - - {formatLabel(entityType)} - - ); - - const getDigitalDatasetLink = (digitalDataset) => { - // Only construct link if digitalDataset is a valid UUID - - const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - - const urlRegex = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/; - - //if referenced digital dataset is a valid url, make it a link - if (urlRegex.test(digitalDataset)) { - return {digitalDataset}; - } - else if (!uuidRegex.test(digitalDataset)) { - return formatLabel(digitalDataset); - } - - const digitalDatasetEntity = findNodeInTree(tree, digitalDataset); - - if (digitalDatasetEntity) { - const index = location.pathname.indexOf(system) + system.length; - const id = digitalDatasetEntity.id.split('_').pop(); - const digitalDatasetUrl = `${location.pathname.slice(0, index)}/digital_dataset/${id}`; - return {digitalDatasetEntity.label}; - } - - return formatLabel(digitalDataset); + return { + totalPages, + startIndex, + endIndex, + currentFileGroups, }; + }, [fileGroups, currentPage]); + + const groupFilesByBaseName = (files) => { + const processedFileSuffixes = [ + '.thumb.jpg', + '.histogram.jpg', + '.histogram.csv', + '.gif', + '.jpg', + ]; + + const groups = files.reduce((map, file) => { + const suffix = processedFileSuffixes.find((suf) => + file.name.endsWith(suf) + ); + const baseName = suffix ? file.name.slice(0, -suffix.length) : file.name; + + if (!map[baseName]) { + map[baseName] = { raw: null, processed: [] }; + } + + if (suffix) { + map[baseName].processed.push(file); + } else { + map[baseName].raw = file; + } + + return map; + }, {}); + + // Handle standalone processed files (e.g., standalone .gif files) + // If a group has no raw file but has processed files, treat the first processed file as raw + Object.values(groups).forEach((group) => { + if (!group.raw && group.processed.length > 0) { + group.raw = group.processed[0]; + group.processed = group.processed.slice(1); + } + }); + + return Object.values(groups); + }; + + useEffect(() => { + if (system && portalName && !error) { + dispatch({ + type: 'PUBLICATIONS_GET_TREE', + payload: { portalName, system }, + }); + } + }, [system, portalName]); + + useEffect(() => { + if (tree && !loading && !error && entityID) { + const entity = findNodeInTreeById(tree, entityID); + setSelectedEntity(entity); + } + }, [tree, loading, error, entityID]); + + useEffect(() => { + if (selectedEntity) { + const groups = groupFilesByBaseName(selectedEntity.fileObjs); + setFileGroups(groups); + setCurrentPage(1); // Reset to first page when data changes + } + }, [selectedEntity]); + + const handlePageChange = useCallback((page) => { + setCurrentPage(page); + }, []); + + const entityTypeWithDesc = ( + + {formatLabel(entityType)} + + ); + + const getDigitalDatasetLink = (digitalDataset) => { + // Only construct link if digitalDataset is a valid UUID + + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - return ( + const urlRegex = + /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/; + + //if referenced digital dataset is a valid url, make it a link + if (urlRegex.test(digitalDataset)) { + return ( + + {digitalDataset} + + ); + } else if (!uuidRegex.test(digitalDataset)) { + return formatLabel(digitalDataset); + } + + const digitalDatasetEntity = findNodeInTree(tree, digitalDataset); + + if (digitalDatasetEntity) { + const index = location.pathname.indexOf(system) + system.length; + const id = digitalDatasetEntity.id.split('_').pop(); + const digitalDatasetUrl = `${location.pathname.slice(0, index)}/digital_dataset/${id}`; + return {digitalDatasetEntity.label}; + } + + return formatLabel(digitalDataset); + }; + + return ( + <> + {loading || !selectedEntity ? ( + + ) : error ? ( +
    + Error loading data. Please try again. +
    + ) : ( <> - {loading || !selectedEntity ? ( - - ) : error ? ( -
    - Error loading data. Please try again. -
    - ) : ( - <> -
    -

    - {entityTypeWithDesc}{` `} - {selectedEntity.label} -

    -

    {selectedEntity?.description}

    - - - {Object.entries(selectedEntity.metadata).map(([key, value]) => { - - if (key === 'digital_dataset') { - return ( - - - - - ); - } - - if (EXCLUDED_METADATA_FIELDS.includes(key)) return null; - - // TODO: Add description to key if needed by PI - // const keyNameWithDesc = ( - // {formatLabel(key)} - // ); - - return ( - - - - - ); - })} - +
    +

    + {entityTypeWithDesc} + {` `} + {selectedEntity.label} +

    +

    {selectedEntity?.description}

    +
    Reference Digital Dataset{getDigitalDatasetLink(value)}
    {formatLabel(key)} - {formatLabel(value)} -
    + + {Object.entries(selectedEntity.metadata).map(([key, value]) => { + if (key === 'digital_dataset') { + return ( + + + + + ); + } + + if (EXCLUDED_METADATA_FIELDS.includes(key)) return null; + + // TODO: Add description to key if needed by PI + // const keyNameWithDesc = ( + // {formatLabel(key)} + // ); + + return ( + + + + + ); + })} + +
    + Reference Digital Dataset + + {getDigitalDatasetLink(value)} +
    {formatLabel(key)} + {formatLabel(value)} +
    +
    + +
    +
      + {paginationData.currentFileGroups.map(({ raw, processed }) => { + const thumbnailFile = + processed.find((file) => file.name.endsWith('.thumb.jpg')) || + processed.find((file) => file.name.endsWith('.jpg')); + const gifFile = processed.find((file) => + file.name.endsWith('.gif') + ); + const histogramFile = processed.find((file) => + file.name.endsWith('.histogram.jpg') + ); + const histogramCsvFile = processed.find((file) => + file.name.endsWith('.histogram.csv') + ); + + let imageSrc; + + if (thumbnailFile) { + // Use thumbnail if available + imageSrc = `${projectUrl}/${thumbnailFile.path}`; + } else if ( + raw.value?.isAdvancedImageFile !== true && + processed.length === 0 + ) { + const validImageExtensions = [ + '.jpg', + '.jpeg', + '.png', + '.gif', + '.bmp', + '.webp', + '.svg', + ]; + const isValidImage = validImageExtensions.some((ext) => + raw.name.toLowerCase().endsWith(ext) + ); + + if (isValidImage) { + imageSrc = `${projectUrl}/${raw.path}`; + } else { + imageSrc = `${BASE_ASSET_URL}/media/default/cover_image/default_logo.png`; + } + } else { + imageSrc = `${BASE_ASSET_URL}/media/default/cover_image/default_logo.png`; + } + + return ( +
    • + {`Preview +

      + {raw.name} +
      + {raw.length ? createSizeString(raw.length) : ''} +

      + + + {Object.entries(raw.metadata).map(([key, value]) => { + if (excludedImageMetadataFields.includes(key)) + return null; + return ( + + + + + ); + })} +
      + {formatLabel(key)} + + {formatLabel(value)} +
      -
    - -
    -
      - {paginationData.currentFileGroups.map(({ raw, processed }) => { - const thumbnailFile = - processed.find(file => file.name.endsWith('.thumb.jpg')) || - processed.find(file => file.name.endsWith('.jpg')); - const gifFile = processed.find(file => file.name.endsWith('.gif')); - const histogramFile = processed.find(file => file.name.endsWith('.histogram.jpg')); - const histogramCsvFile = processed.find(file => file.name.endsWith('.histogram.csv')); - - let imageSrc; - - if (thumbnailFile) { - // Use thumbnail if available - imageSrc = `${projectUrl}/${thumbnailFile.path}`; - } else if (raw.value?.isAdvancedImageFile !== true && processed.length === 0) { - const validImageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']; - const isValidImage = validImageExtensions.some(ext => - raw.name.toLowerCase().endsWith(ext) - ); - - if (isValidImage) { - imageSrc = `${projectUrl}/${raw.path}`; - } else { - imageSrc = `${BASE_ASSET_URL}/media/default/cover_image/default_logo.png`; - } - } else { - imageSrc = `${BASE_ASSET_URL}/media/default/cover_image/default_logo.png`; - } - - return ( -
    • - {`Preview -

      - {raw.name} -
      - {raw.length ? createSizeString(raw.length) : ''} -

      - - - {Object.entries(raw.metadata).map(([key, value]) => { - if (excludedImageMetadataFields.includes(key)) return null; - return ( - - - - - ); - })} - -
      {formatLabel(key)}{formatLabel(value)}
      - -

      - -

      -
    • - - Download File{' '} - -
    • - - {thumbnailFile && ( -
    • - - View Thumbnail - -
    • - )} - - {gifFile && ( -
    • - - View GIF - -
    • - )} - - {histogramFile && ( -
    • - - Histogram - -
    • - )} - - {histogramCsvFile && ( -
    • - - Histogram (CSV) - -
    • - )} -
      -

      -
    • - ); - })} -
    -
    -
    - -
    - - )} - - ) +

    + +

    +
  • + + Download File{' '} + +
  • + + {thumbnailFile && ( +
  • + + View Thumbnail + +
  • + )} + + {gifFile && ( +
  • + + View GIF + +
  • + )} + + {histogramFile && ( +
  • + + Histogram + +
  • + )} + + {histogramCsvFile && ( +
  • + + Histogram (CSV) + +
  • + )} +
    +

    + + ); + })} + + +
    + +
    + + )} + + ); } PublishedDatasetEntityDetail.propTypes = { - params: PropTypes.object.isRequired, + params: PropTypes.object.isRequired, }; -export default PublishedDatasetEntityDetail; \ No newline at end of file +export default PublishedDatasetEntityDetail; diff --git a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetsBrowse.jsx b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetsBrowse.jsx index eb60d6904c..593db15715 100644 --- a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetsBrowse.jsx +++ b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetsBrowse.jsx @@ -12,111 +12,134 @@ import styles from './PublishedDatasetsBrowse.module.css'; const BASE_ASSET_URL = 'https://web.corral.tacc.utexas.edu/digitalporousmedia'; function PublishedDatasetsBrowse() { - const dispatch = useDispatch(); - const { debug: isDebug } = useSelector((state) => state.workbench.config); - - const [filteredPublications, setFilteredPublications] = useState([]); - const [filteredPublicationsLoading, setFilteredPublicationsLoading] = useState(false); - const query = queryStringParser.parse(useLocation().search); - - const { error, loading, publications } = useSelector( - (state) => state.publications.listing - ); - - const systems = useSelector( - (state) => state.systems.storage.configuration.filter((s) => !s.hidden), - shallowEqual - ); - - const selectedSystem = systems.find( - (s) => s.scheme === 'projects' && s.publicationProject === true - ); - - useEffect(() => { - dispatch({ - type: 'PUBLICATIONS_GET_PUBLICATIONS', - payload: { - queryString: query.query_string, - system: selectedSystem?.system, - }, + const dispatch = useDispatch(); + const { debug: isDebug } = useSelector((state) => state.workbench.config); + + const [filteredPublications, setFilteredPublications] = useState([]); + const [filteredPublicationsLoading, setFilteredPublicationsLoading] = + useState(false); + const query = queryStringParser.parse(useLocation().search); + + const { error, loading, publications } = useSelector( + (state) => state.publications.listing + ); + + const systems = useSelector( + (state) => state.systems.storage.configuration.filter((s) => !s.hidden), + shallowEqual + ); + + const selectedSystem = systems.find( + (s) => s.scheme === 'projects' && s.publicationProject === true + ); + + useEffect(() => { + dispatch({ + type: 'PUBLICATIONS_GET_PUBLICATIONS', + payload: { + queryString: query.query_string, + system: selectedSystem?.system, + }, + }); + }, [dispatch, query.query_string]); + + // Workaround to filter out publications that don't have a cover image + // Mainly done so we can test pprd properly + useEffect(() => { + if (!publications) return; + + // Only perform image validation in pprd and dev environment + if (isDebug) { + setFilteredPublicationsLoading(true); + Promise.all( + publications.map((pub) => { + return new Promise((resolve) => { + if ( + !pub.cover_image || + pub.cover_image.includes( + 'media/default/cover_image/default_logo.png' + ) + ) { + return resolve(null); + } + + const img = new Image(); + img.onload = () => resolve(pub); + img.onerror = () => resolve(null); + img.src = `${BASE_ASSET_URL}/${pub.cover_image}`; + }); + }) + ) + .then((results) => { + setFilteredPublications(results.filter(Boolean)); + setFilteredPublicationsLoading(false); + }) + .catch((error) => { + console.error(error); + setFilteredPublicationsLoading(false); }); - }, [dispatch, query.query_string]); - - // Workaround to filter out publications that don't have a cover image - // Mainly done so we can test pprd properly - useEffect(() => { - if (!publications) return; - - // Only perform image validation in pprd and dev environment - if (isDebug) { - setFilteredPublicationsLoading(true); - Promise.all( - publications.map(pub => { - return new Promise(resolve => { - if (!pub.cover_image || pub.cover_image.includes('media/default/cover_image/default_logo.png')) { - return resolve(null); - } - - const img = new Image(); - img.onload = () => resolve(pub); - img.onerror = () => resolve(null); - img.src = `${BASE_ASSET_URL}/${pub.cover_image}`; - }); - }) - ).then(results => { - setFilteredPublications(results.filter(Boolean)); - setFilteredPublicationsLoading(false); - }).catch(error => { - console.error(error); - setFilteredPublicationsLoading(false); - }); - } else { - // In production, use all publications without validation - setFilteredPublications(publications); - } - }, [publications]); - - - return ( -
    -
    -

    Browse Datasets

    - -
    - {loading || filteredPublicationsLoading ? ( - - ) : ( - filteredPublications.length > 0 && ( -
    - {filteredPublications.map((publication) => { - - const coverImage = publication.cover_image; - - const thumbnailFile = `${BASE_ASSET_URL}/${coverImage}`; - - return ( -
  • -

    {publication.title}

    -

    - {publication.authors[0].first_name} {publication.authors[0].last_name} -

    -

    - View Dataset -

    - {publication.title} -
  • - )})} -
    - ))} -
    - ); + } else { + // In production, use all publications without validation + setFilteredPublications(publications); + } + }, [publications]); + + return ( +
    +
    +

    Browse Datasets

    + +
    + {loading || filteredPublicationsLoading ? ( + + ) : ( + filteredPublications.length > 0 && ( +
    + {filteredPublications.map((publication) => { + const coverImage = publication.cover_image; + + const thumbnailFile = `${BASE_ASSET_URL}/${coverImage}`; + + return ( +
  • +

    {publication.title}

    +

    + + {publication.authors[0].first_name}{' '} + {publication.authors[0].last_name} + +

    +

    + + View Dataset + +

    + {publication.title} +
  • + ); + })} +
    + ) + )} +
    + ); } export default PublishedDatasetsBrowse; diff --git a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetsLayout.jsx b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetsLayout.jsx index 6458e2af18..71ce203516 100644 --- a/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetsLayout.jsx +++ b/client/src/components/_custom/drp/PublishedDatasets/PublishedDatasetsLayout.jsx @@ -6,18 +6,17 @@ import './PublishedDatasetsLayout.global.css'; import PublishedDatasetBreadcrumbs from './PublishedDatasetBreadcrumbs'; function PublishedDatasetsLayout({ children, params }) { - - return ( - - - {children} - - ); + return ( + + + {children} + + ); } PublishedDatasetsLayout.propTypes = { - children: PropTypes.node.isRequired, - params: PropTypes.object.isRequired, + children: PropTypes.node.isRequired, + params: PropTypes.object.isRequired, }; -export default PublishedDatasetsLayout; \ No newline at end of file +export default PublishedDatasetsLayout; diff --git a/client/src/components/_custom/drp/PublishedDatasets/index.js b/client/src/components/_custom/drp/PublishedDatasets/index.js index 688a2563d6..af586a019d 100644 --- a/client/src/components/_custom/drp/PublishedDatasets/index.js +++ b/client/src/components/_custom/drp/PublishedDatasets/index.js @@ -2,4 +2,4 @@ export { default as PublishedDatasetsBrowse } from './PublishedDatasetsBrowse'; export { default as PublishedDatasetDetail } from './PublishedDatasetDetail'; export { default as PublishedDatasetEntityDetail } from './PublishedDatasetEntityDetail'; export { default as PublishedDatasetsLayout } from './PublishedDatasetsLayout'; -export { default as PublishedDatasetBreadcrumbs } from './PublishedDatasetBreadcrumbs'; \ No newline at end of file +export { default as PublishedDatasetBreadcrumbs } from './PublishedDatasetBreadcrumbs'; diff --git a/client/src/components/_custom/drp/constants/metadataFields.js b/client/src/components/_custom/drp/constants/metadataFields.js index f8af1e3e38..822cd666cc 100644 --- a/client/src/components/_custom/drp/constants/metadataFields.js +++ b/client/src/components/_custom/drp/constants/metadataFields.js @@ -8,4 +8,4 @@ export const EXCLUDED_METADATA_FIELDS = [ 'cover_image', 'file_url', 'uuid', -]; \ No newline at end of file +]; diff --git a/client/src/components/_custom/drp/utils/DataDisplay/DataDisplay.jsx b/client/src/components/_custom/drp/utils/DataDisplay/DataDisplay.jsx index 56367417bb..5c4a86b4ed 100644 --- a/client/src/components/_custom/drp/utils/DataDisplay/DataDisplay.jsx +++ b/client/src/components/_custom/drp/utils/DataDisplay/DataDisplay.jsx @@ -83,9 +83,9 @@ const DataDisplay = ({ if (entity) { const index = location.pathname.indexOf(system) + system.length; const url = `${location.pathname.slice(0, index)}/${entity.path}`; - + processedData = processedData.filter((entry) => entry.label !== label); - + processedData.unshift({ label, value: ( diff --git a/client/src/components/_custom/drp/utils/hooks/useDrpDatasetModals.js b/client/src/components/_custom/drp/utils/hooks/useDrpDatasetModals.js index f3ea597b58..72edf89740 100644 --- a/client/src/components/_custom/drp/utils/hooks/useDrpDatasetModals.js +++ b/client/src/components/_custom/drp/utils/hooks/useDrpDatasetModals.js @@ -7,14 +7,17 @@ const useDrpDatasetModals = ( portalName, useReloadCallback = true ) => { - const folderData = useSelector( (state) => state.files.folderMetadata.FilesListing ); let sampleUUID = ''; if (folderData && folderData.data_type === 'sample') { sampleUUID = folderData.uuid; - } else if (folderData && (folderData.data_type === 'digital_dataset' || folderData.data_type === 'analysis_data')) { + } else if ( + folderData && + (folderData.data_type === 'digital_dataset' || + folderData.data_type === 'analysis_data') + ) { sampleUUID = folderData.sample; } diff --git a/client/src/components/_custom/drp/utils/utils.js b/client/src/components/_custom/drp/utils/utils.js index c057615835..16e9ac9c51 100644 --- a/client/src/components/_custom/drp/utils/utils.js +++ b/client/src/components/_custom/drp/utils/utils.js @@ -1,79 +1,79 @@ // Format a key to be displayed as a label // Example: "digital_dataset" -> "Digital Dataset" export const formatLabel = (key) => { - // Return as-is if not a string - if (typeof key !== 'string') { - return key; + // Return as-is if not a string + if (typeof key !== 'string') { + return key; + } + + // Check if it's a valid URL + const isValidUrl = (str) => { + try { + const url = new URL(str); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; } + }; - // Check if it's a valid URL - const isValidUrl = (str) => { - try { - const url = new URL(str); - return url.protocol === 'http:' || url.protocol === 'https:'; - } catch { - return false; - } - }; + if (isValidUrl(key)) { + return key; + } - if (isValidUrl(key)) { - return key; - } - - // Handle camelCase by inserting spaces before uppercase letters - const withSpaces = key.replace(/([a-z])([A-Z])/g, '$1 $2'); - - // Split by underscores and spaces, then capitalize each word - return withSpaces - .split(/[_\s]+/) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' ') - .replace('Uri', 'URI'); // Fix for URI + // Handle camelCase by inserting spaces before uppercase letters + const withSpaces = key.replace(/([a-z])([A-Z])/g, '$1 $2'); + + // Split by underscores and spaces, then capitalize each word + return withSpaces + .split(/[_\s]+/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' ') + .replace('Uri', 'URI'); // Fix for URI }; // Find a node in a tree structure by UUID // Returns the node if found, null otherwise export const findNodeInTree = (node, uuid) => { - //upon refresh, node is null so returning null - if (node === null) { - return null; - } + //upon refresh, node is null so returning null + if (node === null) { + return null; + } - if (node?.uuid === uuid) { - return node; - } - if (node.children) { - for (const child of node.children) { - const result = findNodeInTree(child, uuid); - if (result) return result; - } + if (node?.uuid === uuid) { + return node; + } + if (node.children) { + for (const child of node.children) { + const result = findNodeInTree(child, uuid); + if (result) return result; } - return null; + } + return null; }; export const findNodeInTreeById = (node, id) => { - const nodeUuid = node?.id?.split('_').pop(); - if (nodeUuid === id) { - return node; - } - if (node.children) { - for (const child of node.children) { - const result = findNodeInTreeById(child, id); - if (result) return result; - } + const nodeUuid = node?.id?.split('_').pop(); + if (nodeUuid === id) { + return node; + } + if (node.children) { + for (const child of node.children) { + const result = findNodeInTreeById(child, id); + if (result) return result; } - return null; + } + return null; }; export const getTooltipDescription = (key) => { - switch (key) { - case 'sample': - return 'A sample of a porous material. It can be a core, fibrous material, fuel cell, etc.'; - case 'digital_dataset': - return 'All images corresponding to the sample, that do not contain any analysis results. One expectation is segmentation, which can be included in the digital dataset.'; - case 'analysis_dataset': - return 'Analysis datasets are connected to "Sample" entities. They can also be linked to "Digital Dataset" entities if the analysis is conducted on the digital dataset.' - default: - return key; - } -}; \ No newline at end of file + switch (key) { + case 'sample': + return 'A sample of a porous material. It can be a core, fibrous material, fuel cell, etc.'; + case 'digital_dataset': + return 'All images corresponding to the sample, that do not contain any analysis results. One expectation is segmentation, which can be included in the digital dataset.'; + case 'analysis_dataset': + return 'Analysis datasets are connected to "Sample" entities. They can also be linked to "Digital Dataset" entities if the analysis is conducted on the digital dataset.'; + default: + return key; + } +}; diff --git a/client/src/hooks/datafiles/mutations/useMove.ts b/client/src/hooks/datafiles/mutations/useMove.ts index ea768ca094..26d46307f7 100644 --- a/client/src/hooks/datafiles/mutations/useMove.ts +++ b/client/src/hooks/datafiles/mutations/useMove.ts @@ -11,7 +11,7 @@ export async function moveFileUtil({ path, destSystem, destPath, - metadata + metadata, }: { api: string; scheme: string; diff --git a/client/src/hooks/datafiles/mutations/useRename.ts b/client/src/hooks/datafiles/mutations/useRename.ts index 513bd6c924..5fc20e1f53 100644 --- a/client/src/hooks/datafiles/mutations/useRename.ts +++ b/client/src/hooks/datafiles/mutations/useRename.ts @@ -68,7 +68,9 @@ function useRename() { system: selectedFile.system, path: '/' + selectedFile.path, newName, - metadata: selectedFile.metadata ? {...selectedFile.metadata, name: newName} : null, + metadata: selectedFile.metadata + ? { ...selectedFile.metadata, name: newName } + : null, }, { onSuccess: (resp) => { diff --git a/client/src/hooks/datafiles/useAddonComponents.js b/client/src/hooks/datafiles/useAddonComponents.js index 83c9e25a22..a12e76c5e7 100644 --- a/client/src/hooks/datafiles/useAddonComponents.js +++ b/client/src/hooks/datafiles/useAddonComponents.js @@ -20,10 +20,11 @@ const useAddonComponents = ({ portalName }) => { const loadAddonComponents = async () => { try { const modules = await Promise.all( - addons.map((addonName) => - import( - `../../components/_custom/${portalName.toLowerCase()}/${addonName}/${addonName}.jsx` - ) + addons.map( + (addonName) => + import( + `../../components/_custom/${portalName.toLowerCase()}/${addonName}/${addonName}.jsx` + ) ) ); diff --git a/client/src/redux/reducers/publications.reducers.js b/client/src/redux/reducers/publications.reducers.js index 0a88af6528..1bbbd885fb 100644 --- a/client/src/redux/reducers/publications.reducers.js +++ b/client/src/redux/reducers/publications.reducers.js @@ -169,7 +169,7 @@ export default function publications(state = initialState, action) { error: action.payload, loading: false, }, - }; + }; default: return state; } diff --git a/client/src/redux/sagas/datafiles.sagas.js b/client/src/redux/sagas/datafiles.sagas.js index bc8b9b54d5..deff34e4c4 100644 --- a/client/src/redux/sagas/datafiles.sagas.js +++ b/client/src/redux/sagas/datafiles.sagas.js @@ -109,13 +109,16 @@ export async function fetchFilesUtil( nextPageToken, }); - path = path.split('/').map(p => { - try { - return encodeURIComponent(decodeURIComponent(p)); - } catch { - return encodeURIComponent(p); - } - }).join('/'); + path = path + .split('/') + .map((p) => { + try { + return encodeURIComponent(decodeURIComponent(p)); + } catch { + return encodeURIComponent(p); + } + }) + .join('/'); const url = removeDuplicateSlashes( `/api/datafiles/${api}/${operation}/${scheme}/${system}/${path}/?${q}` diff --git a/client/src/redux/sagas/projects.sagas.js b/client/src/redux/sagas/projects.sagas.js index dca400effd..f78853cfbe 100644 --- a/client/src/redux/sagas/projects.sagas.js +++ b/client/src/redux/sagas/projects.sagas.js @@ -64,7 +64,6 @@ export function* showSharedWorkspaces(action) { } export async function fetchCreateProject(project) { - const formData = new FormData(); const { file, ...projectMetadata } = project.metadata; // Exclude the file @@ -79,7 +78,7 @@ export async function fetchCreateProject(project) { .forEach(([key, value]) => { formData.append(key, value); }); - + const result = await fetchUtil({ url: `/api/projects/`, method: 'POST', @@ -183,7 +182,6 @@ export function* setMember(action) { } export async function setTitleDescriptionUtil(projectId, data) { - const formData = new FormData(); const { file, ...projectMetadata } = data.metadata; // Exclude the file diff --git a/client/src/redux/sagas/publications.sagas.js b/client/src/redux/sagas/publications.sagas.js index fdde34c576..af2a1899be 100644 --- a/client/src/redux/sagas/publications.sagas.js +++ b/client/src/redux/sagas/publications.sagas.js @@ -210,7 +210,11 @@ export function* getTree(action) { type: 'PUBLICATIONS_GET_TREE_STARTED', }); try { - const tree = yield call(fetchTree, action.payload.portalName, action.payload.system); + const tree = yield call( + fetchTree, + action.payload.portalName, + action.payload.system + ); yield put({ type: 'PUBLICATIONS_GET_TREE_SUCCESS', payload: tree, diff --git a/client/src/utils/datafilesUtil.js b/client/src/utils/datafilesUtil.js index 9b8884e7d8..b9c4cbb03c 100644 --- a/client/src/utils/datafilesUtil.js +++ b/client/src/utils/datafilesUtil.js @@ -1,9 +1,8 @@ - export const getDecodedPath = (path) => { - if (!path) return '/'; - try { - return path.split('/').map(decodeURIComponent).join('/'); - } catch { - return path; - } -}; \ No newline at end of file + if (!path) return '/'; + try { + return path.split('/').map(decodeURIComponent).join('/'); + } catch { + return path; + } +}; diff --git a/server/conf/docker/docker-compose-dev.all.debug.yml b/server/conf/docker/docker-compose-dev.all.debug.yml index 8ed1b3b566..fac9012b67 100644 --- a/server/conf/docker/docker-compose-dev.all.debug.yml +++ b/server/conf/docker/docker-compose-dev.all.debug.yml @@ -32,11 +32,13 @@ services: command: ["memcached"] elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:7.17.0 + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.28 ulimits: - memlock: -1 + memlock: + soft: -1 + hard: -1 environment: - - ES_HEAP_SIZE:1g + - ES_JAVA_OPTS=-Xms1g -Xmx1g - discovery.type=single-node volumes: - ../elasticsearch/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml diff --git a/server/portal/apps/public_data/views.py b/server/portal/apps/public_data/views.py index e38a1156c4..e2ff70bb48 100644 --- a/server/portal/apps/public_data/views.py +++ b/server/portal/apps/public_data/views.py @@ -1,5 +1,41 @@ +import networkx as nx from django.views.generic.base import TemplateView from django.conf import settings +from portal.apps.publications.models import Publication + + +def get_google_scholar_context(project_id): + """Get context info for Google Scholar/Datacite""" + pub = Publication.objects.get(project_id=project_id) + pub_tree = nx.node_link_graph(pub.tree) + latest_version = max( + pub_tree.nodes[node]["version"] for node in pub_tree.successors("NODE_ROOT") + ) + published_ents = [node for node in pub_tree.successors("NODE_ROOT") + if pub_tree.nodes[node]["version"] == latest_version] + + datacite_json_list = [] + scholar_meta = {} + scholar_meta["keywords"] = ", ".join(pub.value.get("keywords", [])) + scholar_meta["citation_keywords"] = pub.value.get("keywords", []) + scholar_meta["entities"] = [] + for ent in published_ents: + ent_meta = pub_tree.nodes[ent] + entity_scholar_data = { + "title": ent_meta["value"]["title"], + "description": ent_meta["value"].get("description"), + "doi": ent_meta["value"].get("dois", [])[0], + "authors": ent_meta["value"].get("authors", []), + "publication_date": ent_meta["publicationDate"] + } + scholar_meta["entities"].append(entity_scholar_data) + + + datacite_json_list.append(get_datacite_json(pub_tree, + ent_meta["uuid"], + latest_version)) + pub_title = pub.value["title"] + return scholar_meta, datacite_json_list, pub_title class IndexView(TemplateView): @@ -10,6 +46,14 @@ class IndexView(TemplateView): def get_context_data(self, **kwargs): context = super(IndexView, self).get_context_data(**kwargs) + try: + scholar_context, datacite_context, title = get_google_scholar_context(kwargs['project_id']) + context['dc_context'] = [json.dumps(ctx) for ctx in datacite_context] + context['scholar_context'] = scholar_context + context['citation_title'] = f"{kwargs['project_id']} | {title}" + except Exception: + # If we can't generate DataCite JSON, render the page without meta tags. + pass context['setup_complete'] = False if self.request.user.is_anonymous \ else self.request.user.profile.setup_complete context['DEBUG'] = settings.DEBUG diff --git a/server/portal/apps/publications/models.py b/server/portal/apps/publications/models.py index a9bba80765..44ccfb040a 100644 --- a/server/portal/apps/publications/models.py +++ b/server/portal/apps/publications/models.py @@ -32,7 +32,8 @@ class Status(models.TextChoices): def __str__(self): return f'Review for {self.review_project.project_id}' - + + class Publication(models.Model): project_id = models.CharField(max_length=100, primary_key=True, editable=False) @@ -50,4 +51,5 @@ class Publication(models.Model): tree = models.JSONField( encoder=DjangoJSONEncoder, help_text=("JSON document containing the serialized publication tree"), - ) \ No newline at end of file + ) + diff --git a/server/portal/apps/workbench/templates/portal/apps/workbench/index.html b/server/portal/apps/workbench/templates/portal/apps/workbench/index.html index 4e81c34bf4..4ab7a1c390 100644 --- a/server/portal/apps/workbench/templates/portal/apps/workbench/index.html +++ b/server/portal/apps/workbench/templates/portal/apps/workbench/index.html @@ -1,5 +1,37 @@ {% extends "base.html" %} {% load static %} +{% block google_citation_meta %} + + + + {% for keyword in scholar_context.citation_keywords %} + + {% endfor %} + {% for entity in scholar_context.entities %} + + + + + {% for author in entity.authors %} + + + {% endfor %} + + + + + + + + + {% endfor %} + + {% for dc_json in dc_context %} + + {% endfor %} +{% endblock %} {% block title %} Workbench {% endblock %} {% block head_extra %} diff --git a/server/portal/apps/workbench/templates/portal/apps/workbench/index.j2 b/server/portal/apps/workbench/templates/portal/apps/workbench/index.j2 index df67ff2acc..f2bdaf41b1 100644 --- a/server/portal/apps/workbench/templates/portal/apps/workbench/index.j2 +++ b/server/portal/apps/workbench/templates/portal/apps/workbench/index.j2 @@ -1,5 +1,37 @@ {% extends "base.html" %} {% load static %} +{% block google_citation_meta %} + + + + {% for keyword in scholar_context.citation_keywords %} + + {% endfor %} + {% for entity in scholar_context.entities %} + + + + + {% for author in entity.authors %} + + + {% endfor %} + + + + + + + + + {% endfor %} + + {% for dc_json in dc_context %} + + {% endfor %} +{% endblock %} {% block title %} Workbench {% endblock %} {% block head_extra %} diff --git a/server/portal/templates/base.html b/server/portal/templates/base.html index 1bd775fa6d..574f6e38fb 100644 --- a/server/portal/templates/base.html +++ b/server/portal/templates/base.html @@ -28,8 +28,32 @@ {% endfor %} {% endif %} - {% block styles %}{% endblock %} + + + + + {% block google_citation_meta %} + + + + + + + + + + + + + + + + + + + + {% endblock %}