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..944cbe0c1
--- /dev/null
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/OptionalPIDField.js
@@ -0,0 +1,228 @@
+// 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,
+ OptionalDOIoptions,
+ 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 OptionalPIDField 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,
+ } = 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
+ 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}
+ />
+ )}
+ >
+ );
+ }
+}
+
+OptionalPIDField.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,
+};
+
+OptionalPIDField.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/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..217b2358d
--- /dev/null
+++ b/invenio_rdm_records/assets/semantic-ui/js/invenio_rdm_records/src/deposit/fields/Identifiers/PIDField/components/OptionalDOIoptions.js
@@ -0,0 +1,114 @@
+// 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 =
+ !allDOIoptionsAllowed &&
+ !optionalDOItransitions.allowed_providers.includes("external");
+ const isNoNeedDisabled =
+ !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,
+ optionalDOItransitions?.message
+ )}
+ {this._render(noINeedOne, isManagedDisabled, optionalDOItransitions?.message)}
+ {this._render(noNeed, isNoNeedDisabled, 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/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..ad2cb1608 100644
--- a/invenio_rdm_records/services/components/pids.py
+++ b/invenio_rdm_records/services/components/pids.py
@@ -22,69 +22,47 @@
from ..pids.tasks import register_or_update_pid
-class PIDsComponent(ServiceComponent):
- """Service component for PIDs."""
+def _get_optional_doi_transitions(service, record):
+ """Reusable method to validate optional DOI."""
+ RDM_OPTIONAL_DOI_TRANSITIONS = current_app.config.get(
+ "RDM_OPTIONAL_DOI_TRANSITIONS", {}
+ )
+ previous_published = service.record_cls.get_latest_published_by_parent(
+ record.parent
+ )
+ if previous_published:
+ previous_published_pids = previous_published.get("pids", {})
+ previous_published_provider = previous_published_pids.get("doi", {}).get(
+ "provider", "not_needed"
+ )
+ return RDM_OPTIONAL_DOI_TRANSITIONS.get(previous_published_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, 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", []):
+ doi_transitions = _get_optional_doi_transitions(self.service, record)
+ doi_pid = None
+ if record.pids:
+ doi_pid = [pid for pid in record.pids.values() if "doi" in record.pids]
+ new_provider = "not_needed" if not doi_pid else doi_pid[0]["provider"]
+ if doi_transitions and new_provider not in doi_transitions.get(
+ "allowed_providers", []
+ ):
error_message = {
"field": "pids.doi",
- "messages": [
- valid_transitions.get("message").format(sitename=sitename)
- ],
+ "messages": [doi_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_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
- )
-
def create(self, identity, data=None, record=None, errors=None):
"""This method is called on draft creation.
@@ -110,10 +88,7 @@ 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
- )
- self._validate_optional_doi(record, previous_published, errors)
+ self._validate_optional_doi(record, errors=errors)
self.service.pids.pid_manager.validate(pids_data, record, errors)
record.pids = pids_data
@@ -159,10 +134,7 @@ def publish(self, identity, draft=None, record=None):
if draft.parent.get("pids", {}).get("doi"):
required_schemes.add("doi")
- previous_published = (
- self.service.record_cls.get_previous_published_by_parent(record.parent)
- )
- self._validate_optional_doi(draft, previous_published)
+ self._validate_optional_doi(record)
self.service.pids.pid_manager.validate(draft_pids, draft, raise_errors=True)