From b09ab10d62dde29398e085b217411c1f83e17df5 Mon Sep 17 00:00:00 2001 From: Paul Sarando Date: Thu, 22 Feb 2024 19:56:11 -0700 Subject: [PATCH] CORE-1967 WIP auto-populate LocalContexts metadata in templates --- public/static/locales/en/localcontexts.json | 2 + src/components/data/listing/TableView.js | 33 ++- .../metadata/LocalContextsLabelDisplay.js | 32 +-- .../metadata/templates/LocalContextsField.js | 217 ++++++++++++++++++ src/components/metadata/templates/index.js | 46 ++-- .../models/metadata/LocalContexts.js | 14 ++ 6 files changed, 290 insertions(+), 54 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..8ddce94df 100644 --- a/public/static/locales/en/localcontexts.json +++ b/public/static/locales/en/localcontexts.json @@ -1,3 +1,5 @@ { + "localContextsAttrDefaultValue": "The \"{{projectTitle}}\" project has Labels and/or Notices applied through the Local Contexts Hub. For more information, refer to the project page: {{projectPage}}", + "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..e0b2d18ea 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.RIGHTS_ALIAS) ?.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..a9625c4d4 --- /dev/null +++ b/src/components/metadata/templates/LocalContextsField.js @@ -0,0 +1,217 @@ +/** + * @author psarando + */ +import React from "react"; + +import { useQuery } from "react-query"; + +import { useTranslation } from "i18n"; + +import LocalContextsLabelDisplay from "../LocalContextsLabelDisplay"; + +import FormTextField from "components/forms/FormTextField"; +import { + LocalContextsAttrs, + parseProjectID, +} from "components/models/metadata/LocalContexts"; + +import { + LOCAL_CONTEXTS_QUERY_KEY, + getLocalContextsProject, +} from "serviceFacades/metadata"; + +import { Skeleton } from "@mui/material"; + +const findAVU = (avus, attr) => avus?.find((avu) => avu.attr === attr); + +const LocalContextsField = ({ + avu, + onUpdate, + 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 [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 notices = + project?.notice?.filter((label) => label?.name) || []; + const bc_labels = + project?.bc_labels?.filter((label) => label?.name) || []; + const tk_labels = + project?.tk_labels?.filter((label) => label?.name) || []; + + const labels = [...bc_labels, ...tk_labels]; + + const newLabels = [...notices, ...labels]; + + if (newLabels.length === 1) { + newValue = newLabels[0].label_text || newLabels[0].default_text; + } else { + newValue = t("localContextsAttrDefaultValue", { + projectTitle: project?.title, + projectPage: project?.project_page, + }); + } + + 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.name) + ); + + if (missingLabels.length > 0) { + 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.find( + (label) => label.name === childAVU.value + )) + ); + + newAVUs = [ + ...newAVUs, + rightsURIAVU, + schemeURIAVU, + rightsIDSchemeURIAVU, + ...missingLabels.map((label) => ({ + attr: LocalContextsAttrs.RIGHTS_ID, + value: label.name, + unit: "", + })), + ]; + } + + if (avu.value !== newValue || avu.avus !== newAVUs) { + onUpdate({ ...avu, value: newValue, avus: newAVUs }); + } + }, + onError: (error) => { + console.log("Error fetching Local Contexts project.", { + projectHubURI, + error, + }); + }, + }); + + const updateProjectHubURI = (uri) => { + setProjectHubURI(uri); + + 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 }); + } + }; + + return ( + <> + updateProjectHubURI(event?.target?.value)} + {...props} + label={t("localContextsHubProjectURI")} + required + /> + {isFetching ? ( + + + + ) : ( + + )} + + ); +}; + +export default LocalContextsField; diff --git a/src/components/metadata/templates/index.js b/src/components/metadata/templates/index.js index f1dfef807..a568ec684 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.RIGHTS_ALIAS; + if (isLocalContextsAttr) { + FieldComponent = LocalContextsField; + fieldProps.avu = avu; + fieldProps.onUpdate = (avu) => { + arrayHelpers.replace(index, avu); + }; + } + const avuFieldName = `${field}.avus[${index}]`; const avuError = getFormError( avuFieldName, @@ -318,17 +330,6 @@ const MetadataTemplateAttributeForm = (props) => { 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.RIGHTS_ALIAS; + 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..56d8c0fd8 --- /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 = { + RIGHTS_ALIAS: "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", +};