From 13a49629838bfc0c1ec54ab4ac12e9b2032534bd Mon Sep 17 00:00:00 2001 From: zzacharo Date: Thu, 9 Jan 2025 18:07:39 +0100 Subject: [PATCH] pids: improve UI for optional DOI --- .../controls/PublishButton/PublishButton.js | 9 +- .../deposit/fields/Identifiers/PIDField.js | 634 ------------------ .../Identifiers/PIDField/OptionalPIDField.js | 243 +++++++ .../Identifiers/PIDField/PIDFieldCmp.js | 70 ++ .../Identifiers/PIDField/RequiredPIDField.js | 201 ++++++ .../components/ManagedIdentifierCmp.js | 138 ++++ .../components/ManagedUnmanagedSwitch.js | 72 ++ .../PIDField/components/OptionalDOIoptions.js | 120 ++++ .../PIDField/components/ReservePIDBtn.js | 50 ++ .../components/UnmanagedIdentifierCmp.js | 80 +++ .../PIDField/components/UnreservePIDBtn.js | 50 ++ .../PIDField/components/helpers.js | 14 + .../Identifiers/PIDField/components/index.js | 14 + .../fields/Identifiers/PIDField/index.js | 8 + .../src/deposit/state/actions/deposit.js | 10 - .../src/deposit/state/reducers/deposit.js | 5 +- invenio_rdm_records/config.py | 22 + .../services/components/pids.py | 101 +-- 18 files changed, 1127 insertions(+), 714 deletions(-) delete mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/OptionalPIDField.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/PIDFieldCmp.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/RequiredPIDField.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ManagedIdentifierCmp.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ManagedUnmanagedSwitch.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/OptionalDOIoptions.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ReservePIDBtn.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/UnmanagedIdentifierCmp.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/UnreservePIDBtn.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/helpers.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/index.js create mode 100644 invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/index.js diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/controls/PublishButton/PublishButton.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/controls/PublishButton/PublishButton.js index 2e9082af2..4b0fee166 100644 --- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/controls/PublishButton/PublishButton.js +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/controls/PublishButton/PublishButton.js @@ -32,8 +32,8 @@ class PublishButtonComponent extends Component { handlePublish = (event, handleSubmit, publishWithoutCommunity) => { const { setSubmitContext } = this.context; - const { formik, raiseDOINeededButNotReserved, isDOIRequired } = this.props; - const noINeedDOI = formik?.values?.noINeedDOI; + const { formik, raiseDOINeededButNotReserved, isDOIRequired, noINeedDOI } = + this.props; // Check for explicit DOI reservation via the "GET DOI button" only when DOI is // optional in the instance's settings. If it is required, backend will automatically // mint one even if it was not explicitly reserved @@ -92,6 +92,8 @@ class PublishButtonComponent extends Component { formik, publishModalExtraContent, raiseDOINeededButNotReserved, + noINeedDOI, + isDOIRequired, ...ui } = this.props; const { isConfirmModalOpen } = this.state; @@ -166,6 +168,7 @@ PublishButtonComponent.propTypes = { filesState: PropTypes.object, raiseDOINeededButNotReserved: PropTypes.func.isRequired, isDOIRequired: PropTypes.bool, + noINeedDOI: PropTypes.bool, }; PublishButtonComponent.defaultProps = { @@ -175,6 +178,7 @@ PublishButtonComponent.defaultProps = { publishModalExtraContent: undefined, filesState: undefined, isDOIRequired: undefined, + noINeedDOI: undefined, }; const mapStateToProps = (state) => ({ @@ -182,6 +186,7 @@ const mapStateToProps = (state) => ({ publishModalExtraContent: state.deposit.config.publish_modal_extra, filesState: state.files, isDOIRequired: state.deposit.config.is_doi_required, + noINeedDOI: state.deposit.noINeedDOI, }); export const PublishButton = connect(mapStateToProps, (dispatch) => { diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField.js deleted file mode 100644 index 74e63df37..000000000 --- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField.js +++ /dev/null @@ -1,634 +0,0 @@ -// This file is part of Invenio-RDM-Records -// Copyright (C) 2020-2023 CERN. -// Copyright (C) 2020-2022 Northwestern University. -// -// Invenio-RDM-Records is free software; you can redistribute it and/or modify it -// under the terms of the MIT License; see LICENSE file for more details. - -import { i18next } from "@translations/invenio_rdm_records/i18next"; -import { FastField, Field, getIn } from "formik"; -import _debounce from "lodash/debounce"; -import PropTypes from "prop-types"; -import React, { Component } from "react"; -import { FieldLabel } from "react-invenio-forms"; -import { connect } from "react-redux"; -import { Form, Popup, Radio } from "semantic-ui-react"; -import { - DepositFormSubmitActions, - DepositFormSubmitContext, -} from "../../api/DepositFormSubmitContext"; -import { DISCARD_PID_STARTED, RESERVE_PID_STARTED } from "../../state/types"; - -const PROVIDER_EXTERNAL = "external"; -const UPDATE_PID_DEBOUNCE_MS = 200; - -const getFieldErrors = (form, fieldPath) => { - return ( - getIn(form.errors, fieldPath, null) || getIn(form.initialErrors, fieldPath, null) - ); -}; - -/** - * Button component to reserve a PID. - */ -class ReservePIDBtn extends Component { - render() { - const { disabled, handleReservePID, label, loading, fieldError } = this.props; - return ( - - {({ form: formik }) => ( - handleReservePID(e, formik)} - content={label} - error={fieldError} - /> - )} - - ); - } -} - -ReservePIDBtn.propTypes = { - disabled: PropTypes.bool, - handleReservePID: PropTypes.func.isRequired, - fieldError: PropTypes.object, - label: PropTypes.string.isRequired, - loading: PropTypes.bool, -}; - -ReservePIDBtn.defaultProps = { - disabled: false, - loading: false, - fieldError: null, -}; - -/** - * Button component to unreserve a PID. - */ -class UnreservePIDBtn extends Component { - render() { - const { disabled, handleDiscardPID, label, loading } = this.props; - return ( - - {({ form: formik }) => ( - handleDiscardPID(e, formik)} - size="mini" - /> - )} - - } - /> - ); - } -} - -UnreservePIDBtn.propTypes = { - disabled: PropTypes.bool, - handleDiscardPID: PropTypes.func.isRequired, - label: PropTypes.string.isRequired, - loading: PropTypes.bool, -}; - -UnreservePIDBtn.defaultProps = { - disabled: false, - loading: false, -}; - -/** - * Manage radio buttons choices between managed - * and unmanaged PID. - */ -class ManagedUnmanagedSwitch extends Component { - handleChange = (e, { value }) => { - const { onManagedUnmanagedChange } = this.props; - const isManagedSelected = value === "managed"; - const isNoNeedSelected = value === "notneeded"; - onManagedUnmanagedChange(isManagedSelected, isNoNeedSelected); - }; - - render() { - const { disabled, isManagedSelected, isNoNeedSelected, pidLabel, required } = - this.props; - - return ( - - - {i18next.t("Do you already have a {{pidLabel}} for this upload?", { - pidLabel: pidLabel, - })} - - - - - - - - {!required && ( - - - - )} - - ); - } -} - -ManagedUnmanagedSwitch.propTypes = { - disabled: PropTypes.bool, - isManagedSelected: PropTypes.bool.isRequired, - isNoNeedSelected: PropTypes.bool.isRequired, - onManagedUnmanagedChange: PropTypes.func.isRequired, - pidLabel: PropTypes.string, - required: PropTypes.bool.isRequired, -}; - -ManagedUnmanagedSwitch.defaultProps = { - disabled: false, - pidLabel: undefined, -}; - -/** - * Render identifier field and reserve/unreserve - * button components for managed PID. - */ -class ManagedIdentifierComponent extends Component { - static contextType = DepositFormSubmitContext; - - handleReservePID = (event, formik) => { - const { pidType } = this.props; - const { setSubmitContext } = this.context; - setSubmitContext(DepositFormSubmitActions.RESERVE_PID, { - pidType: pidType, - }); - formik.handleSubmit(event); - }; - - handleDiscardPID = (event, formik) => { - const { pidType } = this.props; - const { setSubmitContext } = this.context; - setSubmitContext(DepositFormSubmitActions.DISCARD_PID, { - pidType: pidType, - }); - formik.handleSubmit(event); - }; - - render() { - const { - actionState, - actionStateExtra, - btnLabelDiscardPID, - btnLabelGetPID, - disabled, - helpText, - identifier, - pidPlaceholder, - pidType, - form, - fieldPath, - } = this.props; - const hasIdentifier = identifier !== ""; - - const ReserveBtn = ( - - ); - - const UnreserveBtn = ( - - ); - - return ( - <> - - {hasIdentifier ? ( - - - - ) : ( - - - - )} - - {identifier ? UnreserveBtn : ReserveBtn} - - {helpText && } - - ); - } -} - -ManagedIdentifierComponent.propTypes = { - btnLabelGetPID: PropTypes.string.isRequired, - disabled: PropTypes.bool, - helpText: PropTypes.string, - identifier: PropTypes.string.isRequired, - btnLabelDiscardPID: PropTypes.string.isRequired, - pidPlaceholder: PropTypes.string.isRequired, - pidType: PropTypes.string.isRequired, - form: PropTypes.object.isRequired, - fieldPath: PropTypes.string.isRequired, - /* from Redux */ - actionState: PropTypes.string, - actionStateExtra: PropTypes.object, -}; - -ManagedIdentifierComponent.defaultProps = { - disabled: false, - helpText: null, - /* from Redux */ - actionState: "", - actionStateExtra: {}, -}; - -const mapStateToProps = (state) => ({ - actionState: state.deposit.actionState, - actionStateExtra: state.deposit.actionStateExtra, -}); - -const ManagedIdentifierCmp = connect(mapStateToProps, null)(ManagedIdentifierComponent); - -/** - * Render identifier field to allow user to input - * the unmanaged PID. - */ -class UnmanagedIdentifierCmp extends Component { - constructor(props) { - super(props); - - const { identifier } = props; - - this.state = { - localIdentifier: identifier, - }; - } - - componentDidUpdate(prevProps) { - // called after the form field is updated and therefore re-rendered. - const { identifier } = this.props; - if (identifier !== prevProps.identifier) { - this.handleIdentifierUpdate(identifier); - } - } - - handleIdentifierUpdate = (newIdentifier) => { - this.setState({ localIdentifier: newIdentifier }); - }; - - onChange = (value) => { - const { onIdentifierChanged } = this.props; - this.setState({ localIdentifier: value }, () => onIdentifierChanged(value)); - }; - - render() { - const { localIdentifier } = this.state; - const { form, fieldPath, helpText, pidPlaceholder, disabled } = this.props; - const fieldError = getFieldErrors(form, fieldPath); - return ( - <> - - this.onChange(value)} - value={localIdentifier} - placeholder={pidPlaceholder} - width={16} - error={fieldError} - disabled={disabled} - /> - - {helpText && } - - ); - } -} - -UnmanagedIdentifierCmp.propTypes = { - form: PropTypes.object.isRequired, - fieldPath: PropTypes.string.isRequired, - helpText: PropTypes.string, - identifier: PropTypes.string.isRequired, - onIdentifierChanged: PropTypes.func.isRequired, - pidPlaceholder: PropTypes.string.isRequired, - disabled: PropTypes.bool, -}; - -UnmanagedIdentifierCmp.defaultProps = { - helpText: null, - disabled: false, -}; - -/** - * Render managed or unamanged PID fields and update - * Formik form on input changed. - * The field value has the following format: - * { 'doi': { identifier: '', provider: '', client: '' } } - */ -class CustomPIDField extends Component { - constructor(props) { - super(props); - - const { canBeManaged, canBeUnmanaged, record, field } = this.props; - this.canBeManagedAndUnmanaged = canBeManaged && canBeUnmanaged; - const value = field?.value; - const isInternalProvider = value?.provider !== PROVIDER_EXTERNAL; - const isDraft = record?.is_draft === true; - const hasIdentifier = value?.identifier; - const isManagedSelected = - isDraft && hasIdentifier && isInternalProvider ? true : undefined; - - this.state = { - isManagedSelected: isManagedSelected, - isNoNeedSelected: undefined, - }; - } - - onExternalIdentifierChanged = (identifier) => { - const { form, fieldPath } = this.props; - - const pid = { - identifier: identifier, - provider: PROVIDER_EXTERNAL, - }; - - this.debounced && this.debounced.cancel(); - this.debounced = _debounce(() => { - form.setFieldValue(fieldPath, pid); - }, UPDATE_PID_DEBOUNCE_MS); - this.debounced(); - }; - - render() { - const { isManagedSelected, isNoNeedSelected } = this.state; - const { - btnLabelDiscardPID, - btnLabelGetPID, - canBeManaged, - canBeUnmanaged, - form, - fieldPath, - fieldLabel, - isEditingPublishedRecord, - managedHelpText, - pidLabel, - pidIcon, - pidPlaceholder, - required, - unmanagedHelpText, - pidType, - field, - record, - } = this.props; - - let { doiDefaultSelection } = this.props; - - const value = field.value || {}; - const currentIdentifier = value.identifier || ""; - const currentProvider = value.provider || ""; - - let managedIdentifier = "", - unmanagedIdentifier = ""; - if (currentIdentifier !== "") { - const isProviderExternal = currentProvider === PROVIDER_EXTERNAL; - managedIdentifier = !isProviderExternal ? currentIdentifier : ""; - unmanagedIdentifier = isProviderExternal ? currentIdentifier : ""; - } - - const hasManagedIdentifier = managedIdentifier !== ""; - const hasUnmanagedIdentifier = unmanagedIdentifier !== ""; - const doi = record?.pids?.doi?.identifier || ""; - const parentDoi = record.parent?.pids?.doi?.identifier || ""; - - const hasDoi = doi !== ""; - const hasParentDoi = parentDoi !== ""; - const isDoiCreated = currentIdentifier !== ""; - const isDraft = record.is_draft; - - const _isUnmanagedSelected = - isManagedSelected === undefined - ? hasUnmanagedIdentifier || - (currentIdentifier === "" && doiDefaultSelection === "yes") - : !isManagedSelected; - - const _isManagedSelected = - isManagedSelected === undefined - ? hasManagedIdentifier || - (currentIdentifier === "" && doiDefaultSelection === "no") // i.e pids: {} - : isManagedSelected; - - const _isNoNeedSelected = - isNoNeedSelected === undefined - ? (!_isManagedSelected && !_isUnmanagedSelected) || - (isDraft !== true && - currentIdentifier === "" && - doiDefaultSelection === "not_needed") - : isNoNeedSelected; - - const fieldError = getFieldErrors(form, fieldPath); - - return ( - <> - - - - - {this.canBeManagedAndUnmanaged && ( - { - if (userSelectedManaged) { - form.setFieldValue("pids", {}); - if (!required) { - // We set the - form.setFieldValue("noINeedDOI", true); - } - } else if (userSelectedNoNeed) { - form.setFieldValue("pids", {}); - form.setFieldValue("noINeedDOI", false); - } else { - this.onExternalIdentifierChanged(""); - form.setFieldValue("noINeedDOI", false); - } - form.setFieldError(fieldPath, false); - this.setState({ - isManagedSelected: userSelectedManaged, - isNoNeedSelected: userSelectedNoNeed, - }); - }} - pidLabel={pidLabel} - required={required} - /> - )} - - {canBeManaged && _isManagedSelected && ( - - )} - - {canBeUnmanaged && (!_isManagedSelected || _isNoNeedSelected) && ( - { - this.onExternalIdentifierChanged(identifier); - }} - form={form} - fieldPath={fieldPath} - pidPlaceholder={pidPlaceholder} - helpText={unmanagedHelpText} - disabled={_isNoNeedSelected || isEditingPublishedRecord} - /> - )} - - ); - } -} - -CustomPIDField.propTypes = { - field: PropTypes.object, - form: PropTypes.object.isRequired, - btnLabelDiscardPID: PropTypes.string.isRequired, - btnLabelGetPID: PropTypes.string.isRequired, - canBeManaged: PropTypes.bool.isRequired, - canBeUnmanaged: PropTypes.bool.isRequired, - fieldPath: PropTypes.string.isRequired, - fieldLabel: PropTypes.string.isRequired, - isEditingPublishedRecord: PropTypes.bool.isRequired, - managedHelpText: PropTypes.string, - pidIcon: PropTypes.string.isRequired, - pidLabel: PropTypes.string.isRequired, - pidPlaceholder: PropTypes.string.isRequired, - pidType: PropTypes.string.isRequired, - required: PropTypes.bool.isRequired, - unmanagedHelpText: PropTypes.string, - record: PropTypes.object.isRequired, - doiDefaultSelection: PropTypes.object.isRequired, -}; - -CustomPIDField.defaultProps = { - managedHelpText: null, - unmanagedHelpText: null, - field: undefined, -}; - -/** - * Render the PIDField using a custom Formik component - */ -export class PIDField extends Component { - constructor(props) { - super(props); - - this.validatePropValues(); - } - - validatePropValues = () => { - const { canBeManaged, canBeUnmanaged, fieldPath } = this.props; - - if (!canBeManaged && !canBeUnmanaged) { - throw Error(`${fieldPath} must be managed, unmanaged or both.`); - } - }; - - render() { - const { fieldPath } = this.props; - - return ; - } -} - -PIDField.propTypes = { - btnLabelDiscardPID: PropTypes.string, - btnLabelGetPID: PropTypes.string, - canBeManaged: PropTypes.bool, - canBeUnmanaged: PropTypes.bool, - fieldPath: PropTypes.string.isRequired, - fieldLabel: PropTypes.string.isRequired, - isEditingPublishedRecord: PropTypes.bool.isRequired, - managedHelpText: PropTypes.string, - pidIcon: PropTypes.string, - pidLabel: PropTypes.string.isRequired, - pidPlaceholder: PropTypes.string, - pidType: PropTypes.string.isRequired, - required: PropTypes.bool, - unmanagedHelpText: PropTypes.string, - record: PropTypes.object.isRequired, - doiDefaultSelection: PropTypes.object.isRequired, -}; - -PIDField.defaultProps = { - btnLabelDiscardPID: "Discard", - btnLabelGetPID: "Reserve", - canBeManaged: true, - canBeUnmanaged: true, - managedHelpText: null, - pidIcon: "barcode", - pidPlaceholder: "", - required: false, - unmanagedHelpText: null, -}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/OptionalPIDField.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/OptionalPIDField.js new file mode 100644 index 000000000..f69e0a87a --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/OptionalPIDField.js @@ -0,0 +1,243 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2025 CERN. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import _debounce from "lodash/debounce"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { FieldLabel } from "react-invenio-forms"; +import { Form } from "semantic-ui-react"; +import { + ManagedIdentifierCmp, + OptionalDOIoptions, + UnmanagedIdentifierCmp, +} from "./components"; +import { getFieldErrors } from "./components/helpers"; +import { SET_DOI_NEEDED } from "../../../state/types"; +import { set } from "lodash"; + +const PROVIDER_EXTERNAL = "external"; +const UPDATE_PID_DEBOUNCE_MS = 200; + +/** + * Render managed or unamanged PID fields and update + * Formik form on input changed. + * The field value has the following format: + * { 'doi': { identifier: '', provider: '', client: '' } } + */ +class OptionalPIDFieldCmp extends Component { + constructor(props) { + super(props); + + const { canBeManaged, canBeUnmanaged, record, field } = this.props; + this.canBeManagedAndUnmanaged = canBeManaged && canBeUnmanaged; + const value = field?.value; + const isInternalProvider = value?.provider !== PROVIDER_EXTERNAL; + const isDraft = record?.is_draft === true; + const hasIdentifier = value?.identifier; + const isManagedSelected = + isDraft && hasIdentifier && isInternalProvider ? true : undefined; + + this.state = { + isManagedSelected: isManagedSelected, + isNoNeedSelected: undefined, + }; + } + + onExternalIdentifierChanged = (identifier) => { + const { form, fieldPath } = this.props; + + const pid = { + identifier: identifier, + provider: PROVIDER_EXTERNAL, + }; + + this.debounced && this.debounced.cancel(); + this.debounced = _debounce(() => { + form.setFieldValue(fieldPath, pid); + }, UPDATE_PID_DEBOUNCE_MS); + this.debounced(); + }; + + render() { + const { isManagedSelected, isNoNeedSelected } = this.state; + const { + btnLabelDiscardPID, + btnLabelGetPID, + canBeManaged, + canBeUnmanaged, + form, + fieldPath, + fieldLabel, + isEditingPublishedRecord, + managedHelpText, + pidLabel, + pidIcon, + pidPlaceholder, + required, + unmanagedHelpText, + pidType, + field, + record, + optionalDOItransitions, + setNoINeedDOI, + } = this.props; + + let { doiDefaultSelection } = this.props; + + const value = field.value || {}; + const currentIdentifier = value.identifier || ""; + const currentProvider = value.provider || ""; + + let managedIdentifier = "", + unmanagedIdentifier = ""; + if (currentIdentifier !== "") { + const isProviderExternal = currentProvider === PROVIDER_EXTERNAL; + managedIdentifier = !isProviderExternal ? currentIdentifier : ""; + unmanagedIdentifier = isProviderExternal ? currentIdentifier : ""; + } + + const hasManagedIdentifier = managedIdentifier !== ""; + const hasUnmanagedIdentifier = unmanagedIdentifier !== ""; + const doi = record?.pids?.doi?.identifier || ""; + const parentDoi = record.parent?.pids?.doi?.identifier || ""; + + const hasDoi = doi !== ""; + const hasParentDoi = parentDoi !== ""; + const isDraft = record.is_draft; + + const _isUnmanagedSelected = + isManagedSelected === undefined + ? hasUnmanagedIdentifier || + (currentIdentifier === "" && doiDefaultSelection === "yes") + : !isManagedSelected; + + const _isManagedSelected = + isManagedSelected === undefined + ? hasManagedIdentifier || + (currentIdentifier === "" && doiDefaultSelection === "no") // i.e pids: {} + : isManagedSelected; + + const _isNoNeedSelected = + isNoNeedSelected === undefined + ? (!_isManagedSelected && !_isUnmanagedSelected) || + (isDraft !== true && + currentIdentifier === "" && + doiDefaultSelection === "not_needed") + : isNoNeedSelected; + + const fieldError = getFieldErrors(form, fieldPath); + + return ( + <> + + + + + {this.canBeManagedAndUnmanaged && ( + { + if (userSelectedManaged) { + form.setFieldValue("pids", {}); + if (!required) { + // We set the value as required so we can validate the action on submit + setNoINeedDOI(true); + } + } else if (userSelectedNoNeed) { + form.setFieldValue("pids", {}); + setNoINeedDOI(false); + } else { + this.onExternalIdentifierChanged(""); + setNoINeedDOI(false); + } + form.setFieldError(fieldPath, false); + this.setState({ + isManagedSelected: userSelectedManaged, + isNoNeedSelected: userSelectedNoNeed, + }); + }} + pidLabel={pidLabel} + required={required} + /> + )} + + {canBeManaged && _isManagedSelected && ( + + )} + + {canBeUnmanaged && (!_isManagedSelected || _isNoNeedSelected) && ( + { + this.onExternalIdentifierChanged(identifier); + }} + form={form} + fieldPath={fieldPath} + pidPlaceholder={pidPlaceholder} + helpText={unmanagedHelpText} + disabled={_isNoNeedSelected} + /> + )} + + ); + } +} + +OptionalPIDFieldCmp.propTypes = { + field: PropTypes.object, + form: PropTypes.object.isRequired, + btnLabelDiscardPID: PropTypes.string.isRequired, + btnLabelGetPID: PropTypes.string.isRequired, + canBeManaged: PropTypes.bool.isRequired, + canBeUnmanaged: PropTypes.bool.isRequired, + fieldPath: PropTypes.string.isRequired, + fieldLabel: PropTypes.string.isRequired, + isEditingPublishedRecord: PropTypes.bool.isRequired, + managedHelpText: PropTypes.string, + pidIcon: PropTypes.string.isRequired, + pidLabel: PropTypes.string.isRequired, + pidPlaceholder: PropTypes.string.isRequired, + pidType: PropTypes.string.isRequired, + required: PropTypes.bool.isRequired, + unmanagedHelpText: PropTypes.string, + record: PropTypes.object.isRequired, + doiDefaultSelection: PropTypes.object.isRequired, + optionalDOItransitions: PropTypes.array.isRequired, + setNoINeedDOI: PropTypes.func.isRequired, +}; + +OptionalPIDFieldCmp.defaultProps = { + managedHelpText: null, + unmanagedHelpText: null, + field: undefined, +}; + +export const OptionalPIDField = connect(null, (dispatch) => { + return { + setNoINeedDOI: (value) => + dispatch({ + type: SET_DOI_NEEDED, + payload: { noINeedDOI: value }, + }), + }; +})(OptionalPIDFieldCmp); diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/PIDFieldCmp.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/PIDFieldCmp.js new file mode 100644 index 000000000..439ca657e --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/PIDFieldCmp.js @@ -0,0 +1,70 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import { FastField } from "formik"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { RequiredPIDField } from "./RequiredPIDField"; +import { OptionalPIDField } from "./OptionalPIDField"; + +/** + * Render the PIDField using a custom Formik component + */ +export class PIDField extends Component { + constructor(props) { + super(props); + + this.validatePropValues(); + } + + validatePropValues = () => { + const { canBeManaged, canBeUnmanaged, fieldPath } = this.props; + + if (!canBeManaged && !canBeUnmanaged) { + throw Error(`${fieldPath} must be managed, unmanaged or both.`); + } + }; + + render() { + const { fieldPath, required } = this.props; + const cmp = required ? RequiredPIDField : OptionalPIDField; + + return ; + } +} + +PIDField.propTypes = { + btnLabelDiscardPID: PropTypes.string, + btnLabelGetPID: PropTypes.string, + canBeManaged: PropTypes.bool, + canBeUnmanaged: PropTypes.bool, + fieldPath: PropTypes.string.isRequired, + fieldLabel: PropTypes.string.isRequired, + isEditingPublishedRecord: PropTypes.bool.isRequired, + managedHelpText: PropTypes.string, + pidIcon: PropTypes.string, + pidLabel: PropTypes.string.isRequired, + pidPlaceholder: PropTypes.string, + pidType: PropTypes.string.isRequired, + required: PropTypes.bool, + unmanagedHelpText: PropTypes.string, + record: PropTypes.object.isRequired, + doiDefaultSelection: PropTypes.object.isRequired, + optionalDOItransitions: PropTypes.array.isRequired, +}; + +PIDField.defaultProps = { + btnLabelDiscardPID: "Discard", + btnLabelGetPID: "Reserve", + canBeManaged: true, + canBeUnmanaged: true, + managedHelpText: null, + pidIcon: "barcode", + pidPlaceholder: "", + required: false, + unmanagedHelpText: null, +}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/RequiredPIDField.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/RequiredPIDField.js new file mode 100644 index 000000000..6988562a1 --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/RequiredPIDField.js @@ -0,0 +1,201 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2025 CERN. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import _debounce from "lodash/debounce"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { FieldLabel } from "react-invenio-forms"; +import { Form } from "semantic-ui-react"; +import { + ManagedIdentifierCmp, + ManagedUnmanagedSwitch, + UnmanagedIdentifierCmp, +} from "./components"; +import { getFieldErrors } from "./components/helpers"; + +const PROVIDER_EXTERNAL = "external"; +const UPDATE_PID_DEBOUNCE_MS = 200; + +/** + * Render managed or unamanged PID fields and update + * Formik form on input changed. + * The field value has the following format: + * { 'doi': { identifier: '', provider: '', client: '' } } + */ +export class RequiredPIDField extends Component { + constructor(props) { + super(props); + + const { canBeManaged, canBeUnmanaged, record, field } = this.props; + this.canBeManagedAndUnmanaged = canBeManaged && canBeUnmanaged; + const value = field?.value; + const isInternalProvider = value?.provider !== PROVIDER_EXTERNAL; + const isDraft = record?.is_draft === true; + const hasIdentifier = value?.identifier; + const isManagedSelected = + isDraft && hasIdentifier && isInternalProvider ? true : undefined; + + this.state = { + isManagedSelected: isManagedSelected, + }; + } + + onExternalIdentifierChanged = (identifier) => { + const { form, fieldPath } = this.props; + + const pid = { + identifier: identifier, + provider: PROVIDER_EXTERNAL, + }; + + this.debounced && this.debounced.cancel(); + this.debounced = _debounce(() => { + form.setFieldValue(fieldPath, pid); + }, UPDATE_PID_DEBOUNCE_MS); + this.debounced(); + }; + + render() { + const { isManagedSelected } = this.state; + const { + btnLabelDiscardPID, + btnLabelGetPID, + canBeManaged, + canBeUnmanaged, + form, + fieldPath, + fieldLabel, + isEditingPublishedRecord, + managedHelpText, + pidLabel, + pidIcon, + pidPlaceholder, + required, + unmanagedHelpText, + pidType, + field, + record, + } = this.props; + + let { doiDefaultSelection } = this.props; + + const value = field.value || {}; + const currentIdentifier = value.identifier || ""; + const currentProvider = value.provider || ""; + + let managedIdentifier = "", + unmanagedIdentifier = ""; + if (currentIdentifier !== "") { + const isProviderExternal = currentProvider === PROVIDER_EXTERNAL; + managedIdentifier = !isProviderExternal ? currentIdentifier : ""; + unmanagedIdentifier = isProviderExternal ? currentIdentifier : ""; + } + + const hasManagedIdentifier = managedIdentifier !== ""; + const doi = record?.pids?.doi?.identifier || ""; + const parentDoi = record.parent?.pids?.doi?.identifier || ""; + + const hasDoi = doi !== ""; + const hasParentDoi = parentDoi !== ""; + const isDoiCreated = currentIdentifier !== ""; + + const _isManagedSelected = + isManagedSelected === undefined + ? hasManagedIdentifier || + (currentIdentifier === "" && doiDefaultSelection === "no") // i.e pids: {} + : isManagedSelected; + + const fieldError = getFieldErrors(form, fieldPath); + + return ( + <> + + + + + {this.canBeManagedAndUnmanaged && ( + { + if (userSelectedManaged) { + form.setFieldValue("pids", {}); + } else { + this.onExternalIdentifierChanged(""); + } + form.setFieldError(fieldPath, false); + this.setState({ + isManagedSelected: userSelectedManaged, + }); + }} + pidLabel={pidLabel} + /> + )} + + {canBeManaged && _isManagedSelected && ( + + )} + + {canBeUnmanaged && !_isManagedSelected && ( + { + this.onExternalIdentifierChanged(identifier); + }} + form={form} + fieldPath={fieldPath} + pidPlaceholder={pidPlaceholder} + helpText={unmanagedHelpText} + /> + )} + + ); + } +} + +RequiredPIDField.propTypes = { + field: PropTypes.object, + form: PropTypes.object.isRequired, + btnLabelDiscardPID: PropTypes.string.isRequired, + btnLabelGetPID: PropTypes.string.isRequired, + canBeManaged: PropTypes.bool.isRequired, + canBeUnmanaged: PropTypes.bool.isRequired, + fieldPath: PropTypes.string.isRequired, + fieldLabel: PropTypes.string.isRequired, + isEditingPublishedRecord: PropTypes.bool.isRequired, + managedHelpText: PropTypes.string, + pidIcon: PropTypes.string.isRequired, + pidLabel: PropTypes.string.isRequired, + pidPlaceholder: PropTypes.string.isRequired, + pidType: PropTypes.string.isRequired, + required: PropTypes.bool.isRequired, + unmanagedHelpText: PropTypes.string, + record: PropTypes.object.isRequired, + doiDefaultSelection: PropTypes.object.isRequired, +}; + +RequiredPIDField.defaultProps = { + managedHelpText: null, + unmanagedHelpText: null, + field: undefined, +}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ManagedIdentifierCmp.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ManagedIdentifierCmp.js new file mode 100644 index 000000000..e5497a562 --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ManagedIdentifierCmp.js @@ -0,0 +1,138 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { Form } from "semantic-ui-react"; +import { connect } from "react-redux"; +import { UnreservePIDBtn } from "./UnreservePIDBtn"; +import { ReservePIDBtn } from "./ReservePIDBtn"; +import { + DepositFormSubmitActions, + DepositFormSubmitContext, +} from "../../../../api/DepositFormSubmitContext"; +import { DISCARD_PID_STARTED, RESERVE_PID_STARTED } from "../../../../state/types"; +import { getFieldErrors } from "./helpers"; + +/** + * Render identifier field and reserve/unreserve + * button components for managed PID. + */ +class ManagedIdentifierComponent extends Component { + static contextType = DepositFormSubmitContext; + + handleReservePID = (event, formik) => { + const { pidType } = this.props; + const { setSubmitContext } = this.context; + setSubmitContext(DepositFormSubmitActions.RESERVE_PID, { + pidType: pidType, + }); + formik.handleSubmit(event); + }; + + handleDiscardPID = (event, formik) => { + const { pidType } = this.props; + const { setSubmitContext } = this.context; + setSubmitContext(DepositFormSubmitActions.DISCARD_PID, { + pidType: pidType, + }); + formik.handleSubmit(event); + }; + + render() { + const { + actionState, + actionStateExtra, + btnLabelDiscardPID, + btnLabelGetPID, + disabled, + helpText, + identifier, + pidPlaceholder, + pidType, + form, + fieldPath, + } = this.props; + const hasIdentifier = identifier !== ""; + + const ReserveBtn = ( + + ); + + const UnreserveBtn = ( + + ); + + return ( + <> + + {hasIdentifier ? ( + + + + ) : ( + + + + )} + + {identifier ? UnreserveBtn : ReserveBtn} + + {helpText && } + + ); + } +} + +ManagedIdentifierComponent.propTypes = { + btnLabelGetPID: PropTypes.string.isRequired, + disabled: PropTypes.bool, + helpText: PropTypes.string, + identifier: PropTypes.string.isRequired, + btnLabelDiscardPID: PropTypes.string.isRequired, + pidPlaceholder: PropTypes.string.isRequired, + pidType: PropTypes.string.isRequired, + form: PropTypes.object.isRequired, + fieldPath: PropTypes.string.isRequired, + /* from Redux */ + actionState: PropTypes.string, + actionStateExtra: PropTypes.object, +}; + +ManagedIdentifierComponent.defaultProps = { + disabled: false, + helpText: null, + /* from Redux */ + actionState: "", + actionStateExtra: {}, +}; + +const mapStateToProps = (state) => ({ + actionState: state.deposit.actionState, + actionStateExtra: state.deposit.actionStateExtra, +}); + +export const ManagedIdentifierCmp = connect( + mapStateToProps, + null +)(ManagedIdentifierComponent); diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ManagedUnmanagedSwitch.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ManagedUnmanagedSwitch.js new file mode 100644 index 000000000..e868fd580 --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ManagedUnmanagedSwitch.js @@ -0,0 +1,72 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import { i18next } from "@translations/invenio_rdm_records/i18next"; + +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { Form, Radio } from "semantic-ui-react"; + +/** + * Manage radio buttons choices between managed + * and unmanaged PID. + */ +export class ManagedUnmanagedSwitch extends Component { + handleChange = (e, { value }) => { + const { onManagedUnmanagedChange } = this.props; + const isManagedSelected = value === "managed"; + onManagedUnmanagedChange(isManagedSelected); + }; + + render() { + const { disabled, isManagedSelected, pidLabel } = this.props; + + return ( + + + {i18next.t("Do you already have a {{pidLabel}} for this upload?", { + pidLabel: pidLabel, + })} + + + + + + + + + ); + } +} + +ManagedUnmanagedSwitch.propTypes = { + disabled: PropTypes.bool, + isManagedSelected: PropTypes.bool.isRequired, + onManagedUnmanagedChange: PropTypes.func.isRequired, + pidLabel: PropTypes.string, +}; + +ManagedUnmanagedSwitch.defaultProps = { + disabled: false, + pidLabel: undefined, +}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/OptionalDOIoptions.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/OptionalDOIoptions.js new file mode 100644 index 000000000..19da2feb7 --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/OptionalDOIoptions.js @@ -0,0 +1,120 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2025 CERN. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import { i18next } from "@translations/invenio_rdm_records/i18next"; + +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { Form, Radio, Popup } from "semantic-ui-react"; +import _isEmpty from "lodash/isEmpty"; + +/** + * Manage radio buttons choices between managed (i.e. datacite), unmanaged (i.e. external) and no need for a PID. + */ +export class OptionalDOIoptions extends Component { + handleChange = (e, { value }) => { + const { onManagedUnmanagedChange } = this.props; + const isManagedSelected = value === "managed"; + const isNoNeedSelected = value === "notneeded"; + onManagedUnmanagedChange(isManagedSelected, isNoNeedSelected); + }; + + _render = (cmp, shouldWrapPopup, message) => + shouldWrapPopup ? : cmp; + + render() { + const { isManagedSelected, isNoNeedSelected, pidLabel, optionalDOItransitions } = + this.props; + + const allDOIoptionsAllowed = _isEmpty(optionalDOItransitions); + const isUnManagedDisabled = + isManagedSelected || + (!allDOIoptionsAllowed && + !optionalDOItransitions.allowed_providers.includes("external")); + const isNoNeedDisabled = + isManagedSelected || + (!allDOIoptionsAllowed && + !optionalDOItransitions.allowed_providers.includes("not_needed")); + // The locally managed DOI is disabled either if the external provider is allowed or if the no need option is allowed + const isManagedDisabled = + !allDOIoptionsAllowed && (!isUnManagedDisabled || !isNoNeedDisabled); + + const yesIHaveOne = ( + + + + ); + + const noINeedOne = ( + + + + ); + + const noNeed = ( + + + + ); + + return ( + + + {i18next.t("Do you already have a {{pidLabel}} for this upload?", { + pidLabel: pidLabel, + })} + + {this._render( + yesIHaveOne, + isUnManagedDisabled && !isManagedSelected, + optionalDOItransitions?.message + )} + {this._render(noINeedOne, isManagedDisabled, optionalDOItransitions?.message)} + {this._render( + noNeed, + isNoNeedDisabled && !isManagedSelected, + optionalDOItransitions?.message + )} + + ); + } +} + +OptionalDOIoptions.propTypes = { + isManagedSelected: PropTypes.bool.isRequired, + isNoNeedSelected: PropTypes.bool.isRequired, + onManagedUnmanagedChange: PropTypes.func.isRequired, + pidLabel: PropTypes.string, + optionalDOItransitions: PropTypes.array.isRequired, +}; + +OptionalDOIoptions.defaultProps = { + pidLabel: undefined, +}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ReservePIDBtn.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ReservePIDBtn.js new file mode 100644 index 000000000..07e498fda --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/ReservePIDBtn.js @@ -0,0 +1,50 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import { Field } from "formik"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { Form } from "semantic-ui-react"; + +/** + * Button component to reserve a PID. + */ + +export class ReservePIDBtn extends Component { + render() { + const { disabled, handleReservePID, label, loading, fieldError } = this.props; + return ( + + {({ form: formik }) => ( + handleReservePID(e, formik)} + content={label} + error={fieldError} + /> + )} + + ); + } +} + +ReservePIDBtn.propTypes = { + disabled: PropTypes.bool, + handleReservePID: PropTypes.func.isRequired, + fieldError: PropTypes.object, + label: PropTypes.string.isRequired, + loading: PropTypes.bool, +}; + +ReservePIDBtn.defaultProps = { + disabled: false, + loading: false, + fieldError: null, +}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/UnmanagedIdentifierCmp.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/UnmanagedIdentifierCmp.js new file mode 100644 index 000000000..9b996c850 --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/UnmanagedIdentifierCmp.js @@ -0,0 +1,80 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { Form } from "semantic-ui-react"; +import { getFieldErrors } from "./helpers"; + +/** + * Render identifier field to allow user to input + * the unmanaged PID. + */ +export class UnmanagedIdentifierCmp extends Component { + constructor(props) { + super(props); + + const { identifier } = props; + + this.state = { + localIdentifier: identifier, + }; + } + + componentDidUpdate(prevProps) { + // called after the form field is updated and therefore re-rendered. + const { identifier } = this.props; + if (identifier !== prevProps.identifier) { + this.handleIdentifierUpdate(identifier); + } + } + + handleIdentifierUpdate = (newIdentifier) => { + this.setState({ localIdentifier: newIdentifier }); + }; + + onChange = (value) => { + const { onIdentifierChanged } = this.props; + this.setState({ localIdentifier: value }, () => onIdentifierChanged(value)); + }; + + render() { + const { localIdentifier } = this.state; + const { form, fieldPath, helpText, pidPlaceholder, disabled } = this.props; + const fieldError = getFieldErrors(form, fieldPath); + return ( + <> + + this.onChange(value)} + value={localIdentifier} + placeholder={pidPlaceholder} + width={16} + error={fieldError} + disabled={disabled} + /> + + {helpText && } + + ); + } +} + +UnmanagedIdentifierCmp.propTypes = { + form: PropTypes.object.isRequired, + fieldPath: PropTypes.string.isRequired, + helpText: PropTypes.string, + identifier: PropTypes.string.isRequired, + onIdentifierChanged: PropTypes.func.isRequired, + pidPlaceholder: PropTypes.string.isRequired, + disabled: PropTypes.bool, +}; + +UnmanagedIdentifierCmp.defaultProps = { + helpText: null, + disabled: false, +}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/UnreservePIDBtn.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/UnreservePIDBtn.js new file mode 100644 index 000000000..c91e22587 --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/UnreservePIDBtn.js @@ -0,0 +1,50 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import { Field } from "formik"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { Form, Popup } from "semantic-ui-react"; + +/** + * Button component to unreserve a PID. + */ +export class UnreservePIDBtn extends Component { + render() { + const { disabled, handleDiscardPID, label, loading } = this.props; + return ( + + {({ form: formik }) => ( + handleDiscardPID(e, formik)} + size="mini" + /> + )} + + } + /> + ); + } +} + +UnreservePIDBtn.propTypes = { + disabled: PropTypes.bool, + handleDiscardPID: PropTypes.func.isRequired, + label: PropTypes.string.isRequired, + loading: PropTypes.bool, +}; + +UnreservePIDBtn.defaultProps = { + disabled: false, + loading: false, +}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/helpers.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/helpers.js new file mode 100644 index 000000000..0aa86445d --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/helpers.js @@ -0,0 +1,14 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import { getIn } from "formik"; + +export const getFieldErrors = (form, fieldPath) => { + return ( + getIn(form.errors, fieldPath, null) || getIn(form.initialErrors, fieldPath, null) + ); +}; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/index.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/index.js new file mode 100644 index 000000000..5537ecdba --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/index.js @@ -0,0 +1,14 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +export { ReservePIDBtn } from "./ReservePIDBtn"; +export { UnreservePIDBtn } from "./UnreservePIDBtn"; +export { ManagedUnmanagedSwitch } from "./ManagedUnmanagedSwitch"; +export { ManagedIdentifierCmp } from "./ManagedIdentifierCmp"; +export { UnmanagedIdentifierCmp } from "./UnmanagedIdentifierCmp"; +export { RequiredPIDField } from "../RequiredPIDField"; +export { OptionalDOIoptions } from "./OptionalDOIoptions"; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/index.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/index.js new file mode 100644 index 000000000..59761bd11 --- /dev/null +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/index.js @@ -0,0 +1,8 @@ +// This file is part of Invenio-RDM-Records +// Copyright (C) 2020-2023 CERN. +// Copyright (C) 2020-2022 Northwestern University. +// +// Invenio-RDM-Records is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +export { PIDField } from "./PIDFieldCmp"; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/state/actions/deposit.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/state/actions/deposit.js index 393898bbd..5a3d4897d 100644 --- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/state/actions/deposit.js +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/state/actions/deposit.js @@ -153,16 +153,6 @@ export const save = (draft) => { type: DRAFT_SAVE_SUCCEEDED, payload: { data: response.data }, }); - - if (draft.noINeedDOI) { - // Save the choice that user selected that DOI is needed. This is used to validate - // if user has reserved a DOI before clicking publish. This check is valid when - // DOI is optional - dispatch({ - type: SET_DOI_NEEDED, - payload: { noINeedDOI: draft.noINeedDOI }, - }); - } }; }; diff --git a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/state/reducers/deposit.js b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/state/reducers/deposit.js index 60e6ac2fb..656eab700 100644 --- a/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/state/reducers/deposit.js +++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/state/reducers/deposit.js @@ -340,12 +340,9 @@ const depositReducer = (state = {}, action) => { }; } case SET_DOI_NEEDED: { - const recordCopy = { - ...state.record, - }; return { ...state, - record: { ...recordCopy, ...action.payload }, + noINeedDOI: action.payload.noINeedDOI, }; } default: diff --git a/invenio_rdm_records/config.py b/invenio_rdm_records/config.py index fdb96df32..5522de6ae 100644 --- a/invenio_rdm_records/config.py +++ b/invenio_rdm_records/config.py @@ -437,6 +437,28 @@ def always_valid(identifier): RDM_ALLOW_EXTERNAL_DOI_VERSIONING = True """Allow records with external DOIs to be versioned.""" +RDM_OPTIONAL_DOI_TRANSITIONS = { + "datacite": { + "allowed_providers": ["datacite"], + "message": _( + "A previous version used a DOI registered from {sitename}. This version must also use a DOI from {sitename}." + ), + }, + "external": { + "allowed_providers": ["external", "not_needed"], + "message": _( + "A previous version was published with a DOI from an external provider or without one. You cannot use a DOI registered from {sitename} for this version." + ), + }, + "not_needed": { + "allowed_providers": ["external", "not_needed"], + "message": _( + "A previous version was published with a DOI from an external provider or without one. You cannot use a DOI registered from {sitename} for this version." + ), + }, +} +"""Optional DOI transitions for versioning. The allowed providers are the ones that can be used for the new version and when you edit a published record.""" + # Configuration for the DataCiteClient used by the DataCitePIDProvider DATACITE_ENABLED = False diff --git a/invenio_rdm_records/services/components/pids.py b/invenio_rdm_records/services/components/pids.py index 2a35312ed..83de01540 100644 --- a/invenio_rdm_records/services/components/pids.py +++ b/invenio_rdm_records/services/components/pids.py @@ -22,68 +22,40 @@ from ..pids.tasks import register_or_update_pid -class PIDsComponent(ServiceComponent): - """Service component for PIDs.""" +def _get_optional_doi_transitions(record): + """Reusable method to validate optional DOI.""" + RDM_OPTIONAL_DOI_TRANSITIONS = current_app.config.get( + "RDM_OPTIONAL_DOI_TRANSITIONS", {} + ) + if record: + record_pids = record.get("pids", {}) + record_provider = record_pids.get("doi", {}).get("provider", "not_needed") + return RDM_OPTIONAL_DOI_TRANSITIONS.get(record_provider, {}) + return {} - ALLOWED_DOI_PROVIDERS_TRANSITIONS = { - "datacite": { - "allowed_providers": ["datacite"], - "message": _( - "A previous version used a DOI registered from {sitename}. This version must also use a DOI from {sitename}." - ), - }, - "external": { - "allowed_providers": ["external", "not_needed"], - "message": _( - "A previous version was published with a DOI from an external provider or without one. You cannot use a DOI registered from {sitename} for this version." - ), - }, - "not_needed": { - "allowed_providers": ["external", "not_needed"], - "message": _( - "A previous version was published with a DOI from an external provider or without one. You cannot use a DOI registered from {sitename} for this version." - ), - }, - } - def _validate_doi_transition( - self, new_provider, previous_published_provider, errors=None - ): - """If DOI is not required then we validate allowed DOI providers. +class PIDsComponent(ServiceComponent): + """Service component for PIDs.""" - Each new version that is published must follow the ALLOWED_DOI_PROVIDERS_TRANSITIONS. - """ - sitename = current_app.config.get("THEME_SITENAME", "this repository") + def _validate_optional_doi(self, record, previous_published_record, errors=None): + """.Validate optional DOI.""" sitename = current_app.config.get("THEME_SITENAME", "this repository") - valid_transitions = self.ALLOWED_DOI_PROVIDERS_TRANSITIONS.get( - previous_published_provider, {} - ) - if new_provider not in valid_transitions.get("allowed_providers", []): - error_message = { - "field": "pids.doi", - "messages": [ - valid_transitions.get("message").format(sitename=sitename) - ], - } - - if errors is not None: - errors.append(error_message) - else: - raise ValidationErrorWithMessageAsList(message=[error_message]) - - def _validate_optional_doi(self, record, previous_published, errors=None): - """Reusable method to validate optional DOI.""" - if previous_published: - previous_published_pids = previous_published.get("pids", {}) + doi_transitions = _get_optional_doi_transitions(previous_published_record) + if doi_transitions: doi_pid = [pid for pid in record.pids.values() if "doi" in record.pids] - previous_published_provider = previous_published_pids.get("doi", {}).get( - "provider", "not_needed" - ) new_provider = "not_needed" if not doi_pid else doi_pid[0]["provider"] - self._validate_doi_transition( - new_provider, previous_published_provider, errors - ) + if new_provider not in doi_transitions.get("allowed_providers", []): + error_message = { + "field": "pids.doi", + "messages": [ + doi_transitions.get("message").format(sitename=sitename) + ], + } + if errors is not None: + errors.append(error_message) + else: + raise ValidationErrorWithMessageAsList(message=[error_message]) def create(self, identity, data=None, record=None, errors=None): """This method is called on draft creation. @@ -110,10 +82,12 @@ def update_draft(self, identity, data=None, record=None, errors=None): doi_required = "doi" in required_schemes can_manage_dois = self.service.check_permission(identity, "pid_manage") if not doi_required and not can_manage_dois: - previous_published = self.service.record_cls.get_latest_published_by_parent( - record.parent + previous_published_record = ( + self.service.record_cls.get_latest_published_by_parent(record.parent) + ) + self._validate_optional_doi( + record, previous_published_record, errors=errors ) - self._validate_optional_doi(record, previous_published, errors) self.service.pids.pid_manager.validate(pids_data, record, errors) record.pids = pids_data @@ -158,11 +132,10 @@ def publish(self, identity, draft=None, record=None): # for any version of the record that will be published if draft.parent.get("pids", {}).get("doi"): required_schemes.add("doi") - - previous_published = ( + previous_published_record = ( self.service.record_cls.get_previous_published_by_parent(record.parent) ) - self._validate_optional_doi(draft, previous_published) + self._validate_optional_doi(draft, previous_published_record) self.service.pids.pid_manager.validate(draft_pids, draft, raise_errors=True) @@ -257,12 +230,12 @@ def publish(self, identity, draft=None, record=None): # Check if a doi was added in the draft and create a parent DOI independently if # doi is required. - # Note: we don't have to check explicitely to the parent DOI creation only for - # datacite provider because we pass a `condition_func` below that it omits the - # minting if the pid selected is external if draft.get("pids", {}).get("doi"): required_schemes.add("doi") + # Note: we don't have explicitly to check for minting a parent DOI only for the + # managed provider because we pass a `condition_func` below that it omits the + # minting if the pid selected is external conditional_schemes = self.service.config.parent_pids_conditional for scheme in set(required_schemes): condition_func = conditional_schemes.get(scheme)