From c5af43fef2a601433cf8fb62ea55dd8da5849b03 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Thu, 25 Jan 2024 18:31:39 -0700 Subject: [PATCH 01/10] CORE-1967 Initial Local Contexts label display Initial Local Contexts label display in Data Listings and Metadata Templates. --- config/default.yaml | 2 + src/components/data/listing/TableView.js | 152 ++++++++++++------ .../metadata/LocalContextsLabelDisplay.js | 104 ++++++++++++ src/components/metadata/templates/index.js | 13 ++ src/server/api/metadata.js | 12 +- src/server/configuration.js | 8 + src/serviceFacades/metadata.js | 12 ++ 7 files changed, 257 insertions(+), 46 deletions(-) create mode 100644 src/components/metadata/LocalContextsLabelDisplay.js diff --git a/config/default.yaml b/config/default.yaml index 73e50e5ba..259e78fdc 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -78,6 +78,8 @@ services: base: https://www.ebi.ac.uk/ols/api/select unified_astronomy_thesaurus: base: https://vocabs.ands.org.au/repository/api/lda/aas/the-unified-astronomy-thesaurus/current/concept.json + local_contexts: + base: https://sandbox.localcontextshub.org/api/v1 grouper: allUsers: GrouperAll diff --git a/src/components/data/listing/TableView.js b/src/components/data/listing/TableView.js index 5840ac43a..fbfc80b28 100644 --- a/src/components/data/listing/TableView.js +++ b/src/components/data/listing/TableView.js @@ -5,6 +5,7 @@ */ import React, { Fragment, useState } from "react"; +import { useQuery } from "react-query"; import { useTranslation } from "i18n"; import CustomizeColumns from "./CustomizeColumns"; import dataFields from "../dataFields"; @@ -26,12 +27,21 @@ import DECheckbox from "components/utils/DECheckbox"; import EmptyTable from "components/table/EmptyTable"; import { formatDate } from "components/utils/DateFormatter"; +import LocalContextsLabelDisplay from "components/metadata/LocalContextsLabelDisplay"; +import ResourceTypes from "components/models/ResourceTypes"; + import InstantLaunchButton from "components/instantlaunches"; import { defaultInstantLaunch } from "serviceFacades/instantlaunches"; +import { + FILESYSTEM_METADATA_QUERY_KEY, + getFilesystemMetadata, +} from "serviceFacades/metadata"; + import { alpha, Paper, + Stack, Table, TableBody, TableCell, @@ -43,6 +53,89 @@ import makeStyles from "@mui/styles/makeStyles"; import RowDotMenu from "./RowDotMenu"; +function ResourceNameCell({ + rowId, + resource, + instantLaunch, + computeLimitExceeded, + handlePathChange, +}) { + const theme = useTheme(); + const [localContextsProjectURI, setLocalContextsProjectURI] = useState(); + + const resourceId = resource.id; + const isFolder = resource.type === ResourceTypes.FOLDER; + + useQuery({ + queryKey: [FILESYSTEM_METADATA_QUERY_KEY, { dataId: resourceId }], + queryFn: () => getFilesystemMetadata({ dataId: resourceId }), + enabled: resourceId && isFolder, + onSuccess: (metadata) => { + const { avus } = metadata; + + const rightsURI = avus + ?.find((avu) => avu.attr === "LocalContexts") + ?.avus?.find( + (childAVU) => childAVU.attr === "rightsURI" + )?.value; + + if (rightsURI) { + setLocalContextsProjectURI(rightsURI); + } + }, + onError: (error) => + console.log( + "Unable to fetch metadata for folder " + resource.label, + error + ), // fail silently. + }); + + return ( + + + { + handlePathChange( + resource.path, + resource.type, + resourceId + ); + }} + > + {resource.label} + + {localContextsProjectURI && ( + + )} + + {instantLaunch && ( + + )} + + + ); +} + function SizeCell({ resource }) { return {formatFileSize(resource.fileSize)}; } @@ -161,7 +254,6 @@ function TableView(props) { const dataRecordFields = dataFields(t); const tableId = buildID(baseId, ids.LISTING_TABLE); const trashPath = useBaseTrashPath(); - const theme = useTheme(); const [displayColumns, setDisplayColumns] = useState( getLocalStorageCols(rowDotMenuVisibility, dataRecordFields) || @@ -310,7 +402,8 @@ function TableView(props) { listing.map((resource, index) => { const resourceName = resource.label; const resourceId = resource.id; - const resourcePath = resource.path; + const rowId = buildID(tableId, resourceName); + const isSelected = selected.indexOf(resourceId) !== -1; const isInvalid = @@ -337,7 +430,7 @@ function TableView(props) { role="checkbox" tabIndex={0} hover - id={buildID(tableId, resourceName)} + id={rowId} key={resourceId} selected={isSelected} aria-checked={isSelected} @@ -354,8 +447,7 @@ function TableView(props) { checked={isSelected} tabIndex={0} id={buildID( - tableId, - resourceName, + rowId, ids.checkbox )} onChange={(event) => @@ -380,42 +472,15 @@ function TableView(props) { type={resource.type} /> - - { - handlePathChange( - resourcePath, - resource.type, - resource.id - ); - }} - > - {resource.label} - - - {instantLaunch && ( - - )} - + {getColumnDetails(displayColumns).map( (column, index) => ( @@ -431,10 +496,7 @@ function TableView(props) { {rowDotMenuVisibility && ( + size === "large" + ? theme.spacing(8) + : size === "small" + ? theme.spacing(3) + : theme.spacing(5); + +const LocalContextsLabel = ({ baseId, label, size = "medium" }) => { + const [dialogOpen, setDialogOpen] = React.useState(false); + const theme = useTheme(); + const labelURI = label.svg_url || label.img_url; + + return ( + <> + + setDialogOpen(true)}> + {label.name} + + + setDialogOpen(false)} + > + {label.default_text} + + + ); +}; + +const LocalContextsLabelDisplay = ({ rightsURI, size = "medium" }) => { + // Remove any trailing slash from the rightsURI + // and take the final part of the path as the project ID. + const projectID = rightsURI?.replace(/\/$/, "").split("/").at(-1); + + const { data: project } = useQuery({ + queryKey: [ + LOCAL_CONTEXTS_QUERY_KEY, + projectID && { + projectID, + }, + ], + queryFn: () => + getLocalContextsProject({ + projectID, + }), + enabled: !!projectID, + onError: (error) => { + console.log("Error fetch Local Contexts project.", { + rightsURI, + error, + }); + }, + }); + + const labels = [ + ...(project?.notice || []), + ...(project?.bclabels || []), + ...(project?.tklabels || []), + ].filter((url) => url); + + return ( + + {labels.map((label) => ( + + ))} + + ); +}; + +export default LocalContextsLabelDisplay; diff --git a/src/components/metadata/templates/index.js b/src/components/metadata/templates/index.js index 8f9ca08a6..f1dfef807 100644 --- a/src/components/metadata/templates/index.js +++ b/src/components/metadata/templates/index.js @@ -43,6 +43,8 @@ import FormCheckboxStringValue from "components/forms/FormCheckboxStringValue"; import AstroThesaurusSearchField from "./AstroThesaurusSearchField"; import OntologyLookupServiceSearchField from "./OntologyLookupServiceSearchField"; + +import LocalContextsLabelDisplay from "../LocalContextsLabelDisplay"; import SlideUpTransition from "../SlideUpTransition"; import { @@ -316,6 +318,17 @@ const MetadataTemplateAttributeForm = (props) => { const avuField = ( + {attribute.name === "LocalContexts" && ( + + childAVU.attr === + "rightsURI" + )?.value + } + /> + )} { + const localContextsHandler = externalHandler({ + method: "GET", + url: `${localContextsURL}/projects/${req.params.id}`, + }); + + return localContextsHandler(req, res); + }); + return api; } diff --git a/src/server/configuration.js b/src/server/configuration.js index 00ee1d6af..f372200f4 100644 --- a/src/server/configuration.js +++ b/src/server/configuration.js @@ -289,6 +289,14 @@ export const olsURL = config.get("services.ontology_lookup_service.base"); */ export const uatURL = config.get("services.unified_astronomy_thesaurus.base"); +/** + * The Local Contexts Hub API. + * https://github.com/localcontexts/localcontextshub/wiki/API-Documentation + * + * @type {string} + */ +export const localContextsURL = config.get("services.local_contexts.base"); + /** * The base URL for the User Portal's API */ diff --git a/src/serviceFacades/metadata.js b/src/serviceFacades/metadata.js index 27942a863..a3c8a0e7e 100644 --- a/src/serviceFacades/metadata.js +++ b/src/serviceFacades/metadata.js @@ -12,6 +12,7 @@ const FILESYSTEM_METADATA_TEMPLATE_LISTING_QUERY_KEY = "fetchFilesystemMetadataTemplateListingKey"; const SEARCH_OLS_QUERY_KEY = "searchOntologyLookupServiceKey"; const SEARCH_UAT_QUERY_KEY = "searchUnifiedAstronomyThesaurusKey"; +const LOCAL_CONTEXTS_QUERY_KEY = "localContextsKey"; function getFilesystemMetadataTemplateListing() { return callApi({ @@ -138,10 +139,20 @@ function searchUnifiedAstronomyThesaurus({ searchTerm, orderBy }) { .then((apiResponse) => apiResponse.data); } +function getLocalContextsProject({ projectID }) { + return axiosInstance + .request({ + url: `/api/local-contexts/projects/${projectID}`, + method: "GET", + }) + .then((apiResponse) => apiResponse.data); +} + export { FILESYSTEM_METADATA_QUERY_KEY, FILESYSTEM_METADATA_TEMPLATE_QUERY_KEY, FILESYSTEM_METADATA_TEMPLATE_LISTING_QUERY_KEY, + LOCAL_CONTEXTS_QUERY_KEY, SEARCH_OLS_QUERY_KEY, SEARCH_UAT_QUERY_KEY, getFilesystemMetadata, @@ -150,6 +161,7 @@ export { saveFilesystemMetadata, setFilesystemMetadata, applyBulkMetadataFromFile, + getLocalContextsProject, searchOntologyLookupService, searchUnifiedAstronomyThesaurus, }; From 423ba1164690e5cf2ef4b2aad791a2be756f8f13 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Fri, 26 Jan 2024 17:41:38 -0700 Subject: [PATCH 02/10] CORE-1967 Update LocalContexts details dialog display Add the LocalContexts label/notice default text to a card that also includes the label/notice icon. --- .../metadata/LocalContextsLabelDisplay.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/components/metadata/LocalContextsLabelDisplay.js b/src/components/metadata/LocalContextsLabelDisplay.js index bd46ba1c3..6a03e56fb 100644 --- a/src/components/metadata/LocalContextsLabelDisplay.js +++ b/src/components/metadata/LocalContextsLabelDisplay.js @@ -2,6 +2,9 @@ import React from "react"; import { useQuery } from "react-query"; import { + Card, + CardContent, + CardMedia, IconButton, Stack, Tooltip, @@ -45,7 +48,22 @@ const LocalContextsLabel = ({ baseId, label, size = "medium" }) => { title={label.name} onClose={() => setDialogOpen(false)} > - {label.default_text} + + + + + {label.default_text} + + + ); From 7e64843e81d59baca2b0acd032f7776db4ea157e Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Wed, 14 Feb 2024 19:26:11 -0700 Subject: [PATCH 03/10] CORE-1967 Fix parsing labels from localcontextshub API responses --- src/components/metadata/LocalContextsLabelDisplay.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/metadata/LocalContextsLabelDisplay.js b/src/components/metadata/LocalContextsLabelDisplay.js index 6a03e56fb..4cac48e6c 100644 --- a/src/components/metadata/LocalContextsLabelDisplay.js +++ b/src/components/metadata/LocalContextsLabelDisplay.js @@ -87,7 +87,7 @@ const LocalContextsLabelDisplay = ({ rightsURI, size = "medium" }) => { }), enabled: !!projectID, onError: (error) => { - console.log("Error fetch Local Contexts project.", { + console.log("Error fetching Local Contexts project.", { rightsURI, error, }); @@ -96,9 +96,9 @@ const LocalContextsLabelDisplay = ({ rightsURI, size = "medium" }) => { const labels = [ ...(project?.notice || []), - ...(project?.bclabels || []), - ...(project?.tklabels || []), - ].filter((url) => url); + ...(project?.bc_labels || []), + ...(project?.tk_labels || []), + ].filter((label) => label); return ( Date: Wed, 14 Feb 2024 19:44:47 -0700 Subject: [PATCH 04/10] CORE-1967 Update LocalContexts details dialog and icon display Improved the LocalContexts label/notice icon sizing. Added a link to the LocalContextsHub project in the details dialog. --- public/static/locales/en/localcontexts.json | 3 + .../metadata/LocalContextsLabelDisplay.js | 60 ++++++++++++++----- src/pages/data/ds/[...pathItems].js | 1 + 3 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 public/static/locales/en/localcontexts.json diff --git a/public/static/locales/en/localcontexts.json b/public/static/locales/en/localcontexts.json new file mode 100644 index 000000000..bca3b5533 --- /dev/null +++ b/public/static/locales/en/localcontexts.json @@ -0,0 +1,3 @@ +{ + "projectMoreInfoWithLink": "For more information on the Local Contexts project for this data, please visit the project's page: {{projectTitle}}" +} diff --git a/src/components/metadata/LocalContextsLabelDisplay.js b/src/components/metadata/LocalContextsLabelDisplay.js index 4cac48e6c..5976b30c3 100644 --- a/src/components/metadata/LocalContextsLabelDisplay.js +++ b/src/components/metadata/LocalContextsLabelDisplay.js @@ -12,7 +12,10 @@ import { useTheme, } from "@mui/material"; +import { Trans, useTranslation } from "i18n"; + import DEDialog from "components/utils/DEDialog"; +import ExternalLink from "components/utils/ExternalLink"; import { LOCAL_CONTEXTS_QUERY_KEY, @@ -26,7 +29,9 @@ const sizeToSpacing = (size, theme) => ? theme.spacing(3) : theme.spacing(5); -const LocalContextsLabel = ({ baseId, label, size = "medium" }) => { +const LocalContextsLabel = ({ baseId, label, project, size = "medium" }) => { + const { t } = useTranslation("localcontexts"); + const [dialogOpen, setDialogOpen] = React.useState(false); const theme = useTheme(); const labelURI = label.svg_url || label.img_url; @@ -38,7 +43,8 @@ const LocalContextsLabel = ({ baseId, label, size = "medium" }) => { {label.name} @@ -48,19 +54,44 @@ const LocalContextsLabel = ({ baseId, label, size = "medium" }) => { title={label.name} onClose={() => setDialogOpen(false)} > - - + + + + + + {label.label_text || label.default_text} + + + + - - {label.default_text} + + , + ProjectLink: ( + + ), + }} + /> @@ -113,6 +144,7 @@ const LocalContextsLabelDisplay = ({ rightsURI, size = "medium" }) => { baseId={label.unique_id || label.img_url} size={size} label={label} + project={project} /> ))} diff --git a/src/pages/data/ds/[...pathItems].js b/src/pages/data/ds/[...pathItems].js index 3c220d55a..7f02db92a 100644 --- a/src/pages/data/ds/[...pathItems].js +++ b/src/pages/data/ds/[...pathItems].js @@ -205,6 +205,7 @@ export async function getServerSideProps(context) { title, ...(await serverSideTranslations(locale, [ "data", + "localcontexts", "metadata", "upload", "urlImport", From c0082a35ca65b862cd7f51c4d982e7ea1d22283f Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Thu, 22 Feb 2024 19:56:11 -0700 Subject: [PATCH 05/10] CORE-1967 Auto-populate LocalContexts metadata in templates Added a custom LocalContextsField for metadata templates that only displays a URL field for a Local Contexts Hub project URL, and auto-populates DataCite metadata and child AVUs from the Local Contexts Hub API response. --- public/static/locales/en/localcontexts.json | 3 + src/components/data/listing/TableView.js | 33 ++- .../metadata/LocalContextsLabelDisplay.js | 32 +-- .../metadata/templates/LocalContextsField.js | 241 ++++++++++++++++++ src/components/metadata/templates/index.js | 49 ++-- .../models/metadata/LocalContexts.js | 14 + 6 files changed, 317 insertions(+), 55 deletions(-) create mode 100644 src/components/metadata/templates/LocalContextsField.js create mode 100644 src/components/models/metadata/LocalContexts.js diff --git a/public/static/locales/en/localcontexts.json b/public/static/locales/en/localcontexts.json index bca3b5533..d9e66b599 100644 --- a/public/static/locales/en/localcontexts.json +++ b/public/static/locales/en/localcontexts.json @@ -1,3 +1,6 @@ { + "localContextsAttrDefaultValue": "The \"{{projectTitle}}\" project has Labels and/or Notices applied through the Local Contexts Hub. For more information, refer to the project page: {{projectPage}}", + "localContextsHubError": "Error fetching Local Contexts Hub project information.", + "localContextsHubProjectURI": "Local Contexts Hub Project URI", "projectMoreInfoWithLink": "For more information on the Local Contexts project for this data, please visit the project's page: {{projectTitle}}" } diff --git a/src/components/data/listing/TableView.js b/src/components/data/listing/TableView.js index fbfc80b28..d071e451a 100644 --- a/src/components/data/listing/TableView.js +++ b/src/components/data/listing/TableView.js @@ -1,5 +1,5 @@ /** - * @author aramsey + * @author aramsey, psarando * * A component intended for showing a data listing in a table format. */ @@ -28,6 +28,10 @@ import EmptyTable from "components/table/EmptyTable"; import { formatDate } from "components/utils/DateFormatter"; import LocalContextsLabelDisplay from "components/metadata/LocalContextsLabelDisplay"; +import { + LocalContextsAttrs, + parseProjectID, +} from "components/models/metadata/LocalContexts"; import ResourceTypes from "components/models/ResourceTypes"; import InstantLaunchButton from "components/instantlaunches"; @@ -36,6 +40,8 @@ import { defaultInstantLaunch } from "serviceFacades/instantlaunches"; import { FILESYSTEM_METADATA_QUERY_KEY, getFilesystemMetadata, + getLocalContextsProject, + LOCAL_CONTEXTS_QUERY_KEY, } from "serviceFacades/metadata"; import { @@ -74,9 +80,10 @@ function ResourceNameCell({ const { avus } = metadata; const rightsURI = avus - ?.find((avu) => avu.attr === "LocalContexts") + ?.find((avu) => avu.attr === LocalContextsAttrs.LOCAL_CONTEXTS) ?.avus?.find( - (childAVU) => childAVU.attr === "rightsURI" + (childAVU) => + childAVU.attr === LocalContextsAttrs.RIGHTS_URI )?.value; if (rightsURI) { @@ -90,6 +97,19 @@ function ResourceNameCell({ ), // fail silently. }); + const projectID = parseProjectID(localContextsProjectURI); + + const { data: project } = useQuery({ + queryKey: [LOCAL_CONTEXTS_QUERY_KEY, projectID], + queryFn: () => getLocalContextsProject({ projectID }), + enabled: URL.canParse(localContextsProjectURI), + onError: (error) => + console.log("Error fetching Local Contexts project.", { + localContextsProjectURI, + error, + }), // fail silently. + }); + return ( {resource.label} - {localContextsProjectURI && ( - + {localContextsProjectURI && project && ( + )} {instantLaunch && ( diff --git a/src/components/metadata/LocalContextsLabelDisplay.js b/src/components/metadata/LocalContextsLabelDisplay.js index 5976b30c3..11a9daa4a 100644 --- a/src/components/metadata/LocalContextsLabelDisplay.js +++ b/src/components/metadata/LocalContextsLabelDisplay.js @@ -1,6 +1,5 @@ import React from "react"; -import { useQuery } from "react-query"; import { Card, CardContent, @@ -17,11 +16,6 @@ import { Trans, useTranslation } from "i18n"; import DEDialog from "components/utils/DEDialog"; import ExternalLink from "components/utils/ExternalLink"; -import { - LOCAL_CONTEXTS_QUERY_KEY, - getLocalContextsProject, -} from "serviceFacades/metadata"; - const sizeToSpacing = (size, theme) => size === "large" ? theme.spacing(8) @@ -100,31 +94,7 @@ const LocalContextsLabel = ({ baseId, label, project, size = "medium" }) => { ); }; -const LocalContextsLabelDisplay = ({ rightsURI, size = "medium" }) => { - // Remove any trailing slash from the rightsURI - // and take the final part of the path as the project ID. - const projectID = rightsURI?.replace(/\/$/, "").split("/").at(-1); - - const { data: project } = useQuery({ - queryKey: [ - LOCAL_CONTEXTS_QUERY_KEY, - projectID && { - projectID, - }, - ], - queryFn: () => - getLocalContextsProject({ - projectID, - }), - enabled: !!projectID, - onError: (error) => { - console.log("Error fetching Local Contexts project.", { - rightsURI, - error, - }); - }, - }); - +const LocalContextsLabelDisplay = ({ project, size = "medium" }) => { const labels = [ ...(project?.notice || []), ...(project?.bc_labels || []), diff --git a/src/components/metadata/templates/LocalContextsField.js b/src/components/metadata/templates/LocalContextsField.js new file mode 100644 index 000000000..378b601ce --- /dev/null +++ b/src/components/metadata/templates/LocalContextsField.js @@ -0,0 +1,241 @@ +/** + * This custom metadata template field displays only a Local Contexts Hub + * project URL field, and auto-populates DataCite metadata and child AVUs using + * the project ID parsed from the URL and the Local Contexts Hub API response. + * + * @author psarando + */ +import React from "react"; + +import { useQuery } from "react-query"; + +import { useTranslation } from "i18n"; + +import LocalContextsLabelDisplay from "../LocalContextsLabelDisplay"; + +import ErrorTypographyWithDialog from "components/error/ErrorTypographyWithDialog"; +import getFormError from "components/forms/getFormError"; +import { + LocalContextsAttrs, + parseProjectID, +} from "components/models/metadata/LocalContexts"; + +import { + LOCAL_CONTEXTS_QUERY_KEY, + getLocalContextsProject, +} from "serviceFacades/metadata"; + +import { Skeleton, TextField } from "@mui/material"; + +const findAVU = (avus, attr) => avus?.find((avu) => avu.attr === attr); + +const LocalContextsField = ({ + avu, + onUpdate, + helperText, + form: { setFieldValue, ...form }, + field: { value, onChange, ...field }, + ...props +}) => { + const { t } = useTranslation("localcontexts"); + + const [rightsURIAVU, setRightsURIAVU] = React.useState( + () => + findAVU(avu.avus, LocalContextsAttrs.RIGHTS_URI) || { + attr: LocalContextsAttrs.RIGHTS_URI, + value: "", + unit: "", + } + ); + + const [projectHubURI, setProjectHubURI] = React.useState( + rightsURIAVU.value + ); + + const [projectHubError, setProjectHubError] = React.useState(); + + const [rightsIDSchemeURIAVU] = React.useState(() => { + let rightsIDScheme = findAVU( + avu.avus, + LocalContextsAttrs.RIGHTS_ID_SCHEME + ); + + if ( + rightsIDScheme?.value !== LocalContextsAttrs.RIGHTS_ID_SCHEME_VALUE + ) { + rightsIDScheme = { + attr: LocalContextsAttrs.RIGHTS_ID_SCHEME, + value: LocalContextsAttrs.RIGHTS_ID_SCHEME_VALUE, + unit: "", + }; + } + + return rightsIDScheme; + }); + + const [schemeURIAVU] = React.useState(() => { + let schemeURI = findAVU(avu.avus, LocalContextsAttrs.SCHEME_URI); + + if (schemeURI?.value !== LocalContextsAttrs.SCHEME_URI_VALUE) { + schemeURI = { + attr: LocalContextsAttrs.SCHEME_URI, + value: LocalContextsAttrs.SCHEME_URI_VALUE, + unit: "", + }; + } + + return schemeURI; + }); + + const projectID = parseProjectID(projectHubURI); + + const { data: project, isFetching } = useQuery({ + queryKey: [LOCAL_CONTEXTS_QUERY_KEY, projectID], + queryFn: () => + getLocalContextsProject({ + projectID, + }), + enabled: URL.canParse(projectHubURI), + onSuccess: (project) => { + let newValue = avu.value || ""; + + const projectLabels = [ + ...(project?.notice?.filter((label) => label?.name) || []), + ...(project?.bc_labels?.filter((label) => label?.name) || []), + ...(project?.tk_labels?.filter((label) => label?.name) || []), + ]; + + if (projectLabels.length === 1) { + newValue = + projectLabels[0].label_text || + projectLabels[0].default_text; + } else { + newValue = t("localContextsAttrDefaultValue", { + projectTitle: project?.title, + projectPage: project?.project_page, + }); + } + + const newLabels = projectLabels.map((label) => label.name); + + let newAVUs = avu.avus || []; + + const currentLabels = newAVUs + .filter( + (childAVU) => childAVU.attr === LocalContextsAttrs.RIGHTS_ID + ) + ?.map((childAVU) => childAVU.value); + + const missingLabels = newLabels.filter( + (label) => !currentLabels.includes(label) + ); + + const extraLabels = currentLabels.filter( + (label) => !newLabels.includes(label) + ); + + const requiresUpdate = + avu.value !== newValue || + missingLabels.length > 0 || + extraLabels.length > 0; + + if (requiresUpdate) { + newAVUs = newAVUs.filter( + (childAVU) => + childAVU.attr !== LocalContextsAttrs.RIGHTS_URI && + childAVU.attr !== LocalContextsAttrs.RIGHTS_ID_SCHEME && + childAVU.attr !== LocalContextsAttrs.SCHEME_URI && + (childAVU.attr !== LocalContextsAttrs.RIGHTS_ID || + newLabels.includes(childAVU.value)) + ); + + newAVUs = [ + ...newAVUs, + rightsURIAVU, + schemeURIAVU, + rightsIDSchemeURIAVU, + ...missingLabels.map((label) => ({ + attr: LocalContextsAttrs.RIGHTS_ID, + value: label, + unit: "", + })), + ]; + + onUpdate({ ...avu, value: newValue, avus: newAVUs }); + } + }, + onError: (error) => { + setProjectHubError( + + ); + }, + }); + + const updateProjectHubURI = (uri) => { + setProjectHubURI(uri); + setProjectHubError(null); + + let newAVUs = avu.avus || []; + + if (rightsURIAVU.value !== uri) { + const newRightsURIAVU = { + attr: LocalContextsAttrs.RIGHTS_URI, + value: uri, + unit: "", + }; + setRightsURIAVU(newRightsURIAVU); + + newAVUs = newAVUs.filter( + (childAVU) => + childAVU.attr !== LocalContextsAttrs.RIGHTS_URI && + childAVU.attr !== LocalContextsAttrs.RIGHTS_ID_SCHEME && + childAVU.attr !== LocalContextsAttrs.SCHEME_URI + ); + + newAVUs = [ + ...newAVUs, + newRightsURIAVU, + schemeURIAVU, + rightsIDSchemeURIAVU, + ]; + + onUpdate({ ...avu, avus: newAVUs }); + } + }; + + const { touched, errors } = form; + const errorMsg = + getFormError(field.name, touched, errors) || projectHubError; + + return ( + <> + updateProjectHubURI(event?.target?.value)} + /> + {isFetching ? ( + + + + ) : ( + + )} + + ); +}; + +export default LocalContextsField; diff --git a/src/components/metadata/templates/index.js b/src/components/metadata/templates/index.js index f1dfef807..718b850af 100644 --- a/src/components/metadata/templates/index.js +++ b/src/components/metadata/templates/index.js @@ -43,8 +43,8 @@ import FormCheckboxStringValue from "components/forms/FormCheckboxStringValue"; import AstroThesaurusSearchField from "./AstroThesaurusSearchField"; import OntologyLookupServiceSearchField from "./OntologyLookupServiceSearchField"; +import LocalContextsField from "./LocalContextsField"; -import LocalContextsLabelDisplay from "../LocalContextsLabelDisplay"; import SlideUpTransition from "../SlideUpTransition"; import { @@ -68,6 +68,7 @@ import { } from "@mui/icons-material"; import { Skeleton } from "@mui/material"; +import { LocalContextsAttrs } from "components/models/metadata/LocalContexts"; const useStyles = makeStyles(styles); @@ -272,6 +273,17 @@ const MetadataTemplateAttributeForm = (props) => { return null; } + const isLocalContextsAttr = + attribute.name === + LocalContextsAttrs.LOCAL_CONTEXTS; + if (isLocalContextsAttr) { + FieldComponent = LocalContextsField; + fieldProps.avu = avu; + fieldProps.onUpdate = (avu) => { + arrayHelpers.replace(index, avu); + }; + } + const avuFieldName = `${field}.avus[${index}]`; const avuError = getFormError( avuFieldName, @@ -300,7 +312,8 @@ const MetadataTemplateAttributeForm = (props) => { ); - const childAVUs = attribute.attributes && + const childAVUs = !isLocalContextsAttr && + attribute.attributes && attribute.attributes.length > 0 && ( { const avuField = ( - {attribute.name === "LocalContexts" && ( - - childAVU.attr === - "rightsURI" - )?.value - } - /> - )} { const avuArrayErrors = []; avus.forEach((avu, avuIndex) => { const avuErrors = {}; - const value = avu.value; const attrTemplate = attributeMap[avu.attr]; if (!attrTemplate) { return; } - if (attrTemplate.required && value === "") { + let attrType = attrTemplate.type; + let value = avu.value; + + const isLocalContexts = + avu.attr === LocalContextsAttrs.LOCAL_CONTEXTS; + if (isLocalContexts) { + const rightsURIAVU = avu.avus?.find( + (childAVU) => + childAVU.attr === LocalContextsAttrs.RIGHTS_URI + ); + + attrType = AttributeTypes.URL; + value = rightsURIAVU?.value; + } + + const isRequired = isLocalContexts || attrTemplate.required; + if (isRequired && value === "") { avuErrors.value = t("required"); avuErrors.error = true; avuArrayErrors[avuIndex] = avuErrors; } else if (value) { - switch (attrTemplate.type) { + switch (attrType) { case AttributeTypes.NUMBER: case AttributeTypes.INTEGER: const numVal = Number(value); diff --git a/src/components/models/metadata/LocalContexts.js b/src/components/models/metadata/LocalContexts.js new file mode 100644 index 000000000..487ea6192 --- /dev/null +++ b/src/components/models/metadata/LocalContexts.js @@ -0,0 +1,14 @@ +// Remove any trailing slash from the rightsURI +// and take the final part of the path as the project ID. +export const parseProjectID = (projectHubURI) => + projectHubURI?.replace(/\/$/, "").split("/").at(-1); + +export const LocalContextsAttrs = { + LOCAL_CONTEXTS: "LocalContexts", + RIGHTS_URI: "rightsURI", + RIGHTS_ID: "rightsIdentifier", + RIGHTS_ID_SCHEME: "rightsIdentifierScheme", + RIGHTS_ID_SCHEME_VALUE: "Local Contexts", + SCHEME_URI: "schemeURI", + SCHEME_URI_VALUE: "https://localcontexts.org", +}; From 8bc45c22cef02d8a4960a048ed55e7faf7b05143 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Tue, 27 Feb 2024 20:41:59 -0700 Subject: [PATCH 06/10] CORE-1967 Add Local Contexts label display to folder's listing page --- public/static/locales/en/localcontexts.json | 1 + src/components/data/listing/Listing.js | 74 ++++++++++++++++++++- 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/public/static/locales/en/localcontexts.json b/public/static/locales/en/localcontexts.json index d9e66b599..71fee72bd 100644 --- a/public/static/locales/en/localcontexts.json +++ b/public/static/locales/en/localcontexts.json @@ -1,4 +1,5 @@ { + "dataHasLocalContextsLabelsNotices": "This data has associated Local Contexts labels or notices.", "localContextsAttrDefaultValue": "The \"{{projectTitle}}\" project has Labels and/or Notices applied through the Local Contexts Hub. For more information, refer to the project page: {{projectPage}}", "localContextsHubError": "Error fetching Local Contexts Hub project information.", "localContextsHubProjectURI": "Local Contexts Hub Project URI", diff --git a/src/components/data/listing/Listing.js b/src/components/data/listing/Listing.js index 646a5be7e..5b149402e 100644 --- a/src/components/data/listing/Listing.js +++ b/src/components/data/listing/Listing.js @@ -55,6 +55,13 @@ import { DEFAULTS_MAPPING_QUERY_KEY, } from "serviceFacades/instantlaunches"; +import { + getFilesystemMetadata, + FILESYSTEM_METADATA_QUERY_KEY, + LOCAL_CONTEXTS_QUERY_KEY, + getLocalContextsProject, +} from "serviceFacades/metadata"; + import { announce } from "components/announcer/CyVerseAnnouncer"; import { ERROR, INFO } from "components/announcer/AnnouncerConstants"; import buildID from "components/utils/DebugIDUtil"; @@ -64,7 +71,7 @@ import { useBagAddItems } from "serviceFacades/bags"; import { useQueryClient, useMutation, useQuery } from "react-query"; -import { Button, Typography, useTheme } from "@mui/material"; +import { Button, Stack, Typography, useTheme } from "@mui/material"; import DEDialog from "components/utils/DEDialog"; import PublicLinks from "../PublicLinks"; import constants from "../../../constants"; @@ -82,6 +89,12 @@ import { } from "serviceFacades/dashboard"; import { getUserQuota } from "common/resourceUsage"; +import LocalContextsLabelDisplay from "components/metadata/LocalContextsLabelDisplay"; +import { + LocalContextsAttrs, + parseProjectID, +} from "components/models/metadata/LocalContexts"; + function Listing(props) { const { baseId, @@ -102,7 +115,7 @@ function Listing(props) { } = props; const [config] = useConfig(); - const { t } = useTranslation(["data", "common"]); + const { t } = useTranslation(["data", "common", "localcontexts"]); const [userProfile] = useUserProfile(); const uploadTracker = useUploadTrackingState(); @@ -112,6 +125,7 @@ function Listing(props) { const [selected, setSelected] = useState([]); const [lastSelectIndex, setLastSelectIndex] = useState(-1); const [data, setData] = useState({ total: 0, listing: [] }); + const [localContextsProjectURI, setLocalContextsProjectURI] = useState(); const [detailsEnabled, setDetailsEnabled] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false); const [advancedSearchOpen, setAdvancedSearchOpen] = useState(false); @@ -204,6 +218,8 @@ function Listing(props) { path, }); setData({ + id: respData?.id, + label: respData?.label, total: respData?.total, permission: respData?.permission, listing: [ @@ -217,6 +233,8 @@ function Listing(props) { })), ].map((i) => camelcaseit(i)), // camelcase the fields for each object, for consistency. }); + + setLocalContextsProjectURI(null); }, }); @@ -234,6 +252,42 @@ function Listing(props) { { exact: true } ); + useQuery({ + queryKey: [FILESYSTEM_METADATA_QUERY_KEY, { dataId: data?.id }], + queryFn: () => getFilesystemMetadata({ dataId: data?.id }), + enabled: !!data?.id, + onSuccess: (metadata) => { + const { avus } = metadata; + + const rightsURI = avus + ?.find((avu) => avu.attr === LocalContextsAttrs.LOCAL_CONTEXTS) + ?.avus?.find( + (childAVU) => + childAVU.attr === LocalContextsAttrs.RIGHTS_URI + )?.value; + + setLocalContextsProjectURI(rightsURI); + }, + onError: (error) => + console.log( + "Unable to fetch metadata for folder " + data?.label, + error + ), // fail silently. + }); + + const projectID = parseProjectID(localContextsProjectURI); + + const { data: localContextsProject } = useQuery({ + queryKey: [LOCAL_CONTEXTS_QUERY_KEY, projectID], + queryFn: () => getLocalContextsProject({ projectID }), + enabled: URL.canParse(localContextsProjectURI), + onError: (error) => + console.log("Error fetching Local Contexts project.", { + localContextsProjectURI, + error, + }), // fail silently. + }); + const { isFetching: isFetchingDefaultsMapping } = useQuery({ queryKey: [DEFAULTS_MAPPING_QUERY_KEY], queryFn: getDefaultsMapping, @@ -324,6 +378,7 @@ function Listing(props) { useEffect(() => { setSelected([]); + setLocalContextsProjectURI(null); }, [path, rowsPerPage, orderBy, order, page, uploadsCompleted]); const viewUploadQueue = useCallback(() => { @@ -677,6 +732,21 @@ function Listing(props) { onMoveSelected={onMoveSelected} uploadsEnabled={uploadsEnabled} /> + {localContextsProjectURI && localContextsProject && ( + + + }} + /> + + + + )} {!isGridView && ( Date: Wed, 28 Feb 2024 17:49:02 -0700 Subject: [PATCH 07/10] CORE-1967 Replace usage of URL.canParse Chromatic is throwing errors that URL.canParse is not a function, even though CanIUse and MDN docs indicate there's wide support for it. --- src/components/data/listing/Listing.js | 2 +- src/components/data/listing/TableView.js | 2 +- src/components/metadata/templates/LocalContextsField.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/data/listing/Listing.js b/src/components/data/listing/Listing.js index 5b149402e..d32adeaa8 100644 --- a/src/components/data/listing/Listing.js +++ b/src/components/data/listing/Listing.js @@ -280,7 +280,7 @@ function Listing(props) { const { data: localContextsProject } = useQuery({ queryKey: [LOCAL_CONTEXTS_QUERY_KEY, projectID], queryFn: () => getLocalContextsProject({ projectID }), - enabled: URL.canParse(localContextsProjectURI), + enabled: !!localContextsProjectURI, onError: (error) => console.log("Error fetching Local Contexts project.", { localContextsProjectURI, diff --git a/src/components/data/listing/TableView.js b/src/components/data/listing/TableView.js index d071e451a..f8a07c381 100644 --- a/src/components/data/listing/TableView.js +++ b/src/components/data/listing/TableView.js @@ -102,7 +102,7 @@ function ResourceNameCell({ const { data: project } = useQuery({ queryKey: [LOCAL_CONTEXTS_QUERY_KEY, projectID], queryFn: () => getLocalContextsProject({ projectID }), - enabled: URL.canParse(localContextsProjectURI), + enabled: !!localContextsProjectURI, onError: (error) => console.log("Error fetching Local Contexts project.", { localContextsProjectURI, diff --git a/src/components/metadata/templates/LocalContextsField.js b/src/components/metadata/templates/LocalContextsField.js index 378b601ce..c5ddb4462 100644 --- a/src/components/metadata/templates/LocalContextsField.js +++ b/src/components/metadata/templates/LocalContextsField.js @@ -87,6 +87,8 @@ const LocalContextsField = ({ return schemeURI; }); + const { touched, errors } = form; + const fieldError = getFormError(field.name, touched, errors); const projectID = parseProjectID(projectHubURI); const { data: project, isFetching } = useQuery({ @@ -95,7 +97,7 @@ const LocalContextsField = ({ getLocalContextsProject({ projectID, }), - enabled: URL.canParse(projectHubURI), + enabled: projectHubURI && !fieldError, onSuccess: (project) => { let newValue = avu.value || ""; @@ -206,9 +208,7 @@ const LocalContextsField = ({ } }; - const { touched, errors } = form; - const errorMsg = - getFormError(field.name, touched, errors) || projectHubError; + const errorMsg = fieldError || projectHubError; return ( <> From 44da182bfb43a3faad75fca55e9f36196bbd92c9 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Wed, 28 Feb 2024 17:53:52 -0700 Subject: [PATCH 08/10] CORE-1967 Minor metadata template form refactoring --- src/components/metadata/templates/index.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/metadata/templates/index.js b/src/components/metadata/templates/index.js index 718b850af..2a68a0010 100644 --- a/src/components/metadata/templates/index.js +++ b/src/components/metadata/templates/index.js @@ -680,10 +680,7 @@ const MetadataTemplateView = (props) => { attributes .filter((attribute) => attribute.required) .forEach((attribute) => { - if ( - avus.filter((avu) => avu.attr === attribute.name) - .length < 1 - ) { + if (!avus.find((avu) => avu.attr === attribute.name)) { avus.push(newAVU(attribute)); } @@ -825,12 +822,11 @@ const MetadataTemplateView = (props) => { const attrTemplate = attributeMap[avu.attr]; const isNumberAttr = - attrTemplate && - (attrTemplate.type === AttributeTypes.NUMBER || - attrTemplate.type === AttributeTypes.INTEGER); + attrTemplate?.type === AttributeTypes.NUMBER || + attrTemplate?.type === AttributeTypes.INTEGER; const isGroupingAttr = - attrTemplate && attrTemplate.type === AttributeTypes.GROUPING; + attrTemplate?.type === AttributeTypes.GROUPING; const hasChildAVUs = avu.avus && avu.avus.length > 0; From 92639a195edfd94e0b4f6e2b0b85a04d6e87beeb Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Wed, 28 Feb 2024 17:55:01 -0700 Subject: [PATCH 09/10] CORE-1967 Enable dirty flag in Metadata form back button --- src/components/metadata/form/MetadataFormToolbar.js | 3 ++- src/components/metadata/form/index.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/metadata/form/MetadataFormToolbar.js b/src/components/metadata/form/MetadataFormToolbar.js index bfacf66f9..a7614c470 100644 --- a/src/components/metadata/form/MetadataFormToolbar.js +++ b/src/components/metadata/form/MetadataFormToolbar.js @@ -38,6 +38,7 @@ const MetadataFormToolbar = (props) => { const { baseId, title, + dirty, saveDisabled, showSave, onSave, @@ -59,7 +60,7 @@ const MetadataFormToolbar = (props) => { return ( - + { setShowImportConfirmationDialog(true) } targetResource={targetResource} + dirty={dirty} /> {irodsAVUs?.length > 0 && ( From d9b1b70cd3776f1773657cc6d741cc053b8eecde Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Wed, 28 Feb 2024 20:09:04 -0700 Subject: [PATCH 10/10] CORE-1967 Fix warning for isDisabled in FormSearchField Probably broken by MUI v5 Autocomplete upgrade in #554 --- src/components/forms/FormSearchField.js | 2 ++ src/components/metadata/templates/index.js | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/forms/FormSearchField.js b/src/components/forms/FormSearchField.js index 2c8bfcc1f..78d3f79f6 100644 --- a/src/components/forms/FormSearchField.js +++ b/src/components/forms/FormSearchField.js @@ -18,6 +18,7 @@ const FormSearchField = ({ form: { setFieldValue, ...form }, valueKey, labelKey, + readOnly, ...props }) => { const [searchValue, setSearchValue] = React.useState(value); @@ -48,6 +49,7 @@ const FormSearchField = ({ return ( option[labelKey] === value[labelKey] } diff --git a/src/components/metadata/templates/index.js b/src/components/metadata/templates/index.js index 2a68a0010..34e8174de 100644 --- a/src/components/metadata/templates/index.js +++ b/src/components/metadata/templates/index.js @@ -241,7 +241,7 @@ const MetadataTemplateAttributeForm = (props) => { fieldProps = { ...fieldProps, searchAstroThesaurusTerms, - isDisabled: !writable, + readOnly: !writable, }; break; @@ -251,7 +251,7 @@ const MetadataTemplateAttributeForm = (props) => { ...fieldProps, searchOLSTerms, attribute, - isDisabled: !writable, + readOnly: !writable, }; break;