Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions client/components/Deposit/Deposit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { assert, exists } from 'utils/assert';
import { PUBPUB_DOI_PREFIX } from 'utils/crossref/communities';

import DataciteDeposit from './DataciteDeposit';
import DepositStatusCallout from './DepositStatusCallout';
import './deposit.scss';

import { UpdateDoi } from './UpdateDoi';
Expand Down Expand Up @@ -145,10 +146,18 @@ export default function Deposit(props: Props) {

const firstIntraWorkRelationship = resource && getFirstIntraWorkRelationship(resource);
const disabledDueToNoReleases = 'pub' in props && props.pub.releases?.length === 0;
// Deliberately still keyed on "a deposit exists" rather than "a deposit
// succeeded": a failed deposit does NOT unlock the DOI suffix again. Deposit
// state is per attempt, so a rejected *update* to a work whose DOI Crossref
// already registered also reads as failed, and letting the suffix be edited
// there would strand a live DOI pointing at nothing. The affordance for a
// failure is fixing the metadata and re-submitting, not renaming the DOI.
const crossrefDepositRecordId =
'pub' in props
? props.pub.crossrefDepositRecordId
: props.collection.crossrefDepositRecordId;
const depositRecord =
'pub' in props ? props.pub.crossrefDepositRecord : props.collection.crossrefDepositRecord;

let children: React.ReactNode;

Expand All @@ -171,6 +180,14 @@ export default function Deposit(props: Props) {
children = (
<>
{unapprovedWarning}
{!justSetDoi && (
<DepositStatusCallout
status={depositRecord?.status}
error={depositRecord?.error}
lastCheckedAt={depositRecord?.lastCheckedAt}
registrarName="DataCite"
/>
)}
{'pub' in props && resource && firstIntraWorkRelationship && (
<p>
This Pub will be cited as a member of the{' '}
Expand Down
95 changes: 95 additions & 0 deletions client/components/Deposit/DepositStatusCallout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';

import { Callout } from '@blueprintjs/core';

import { getDoiDisplay } from 'utils/crossref/depositStatus';

type Props = {
status?: string | null;
/** Registrar (or Doily) failure text for the last attempt, shown verbatim. */
error?: string | null;
lastCheckedAt?: string | Date | null;
registrarName?: string;
};

const formatCheckedAt = (lastCheckedAt?: string | Date | null) => {
if (!lastCheckedAt) {
return null;
}
const date = new Date(lastCheckedAt);
if (Number.isNaN(date.getTime())) {
return null;
}
return date.toLocaleString();
};

/**
* What the registrar actually said about a deposit, for the manage-level DOI UI.
*
* Before Doily pushed outcomes, PubPub could only say "deposited", which meant
* "we POSTed it and were not told no". So the two states worth an affordance
* (still in flight, and rejected) had no way to appear at all. Renders nothing
* for a legacy row: a deposit with no recorded state is the pre-Doily normal and
* must look exactly as it always did rather than sprouting a scary callout.
*/
export default function DepositStatusCallout(props: Props) {
const { status, error, lastCheckedAt, registrarName = 'Crossref' } = props;
const display = getDoiDisplay(status);
const checkedAt = formatCheckedAt(lastCheckedAt);
const checkedAtLine = checkedAt ? (
<p className="deposit-status-checked">As of {checkedAt}.</p>
) : null;

if (display === 'legacy') {
return null;
}

if (display === 'registered') {
return (
<Callout intent="success" icon="tick" title={`Registered with ${registrarName}`}>
<p>The DOI is registered and resolves to this Pub.</p>
{checkedAtLine}
</Callout>
);
}

if (display === 'unverified') {
return (
<Callout intent="warning" title="Registration not confirmed">
<p>
This deposit was submitted and {registrarName} did not report a failure, but the
registration could not be confirmed. The DOI is displayed as normal. If it does
not resolve, submit the deposit again.
</p>
{error && <p>{error}</p>}
{checkedAtLine}
</Callout>
);
}

if (display === 'pending') {
return (
<Callout intent="primary" icon="time" title="Registration in progress">
<p>
The deposit is with {registrarName}, which usually rules on it within a few
hours. Until it confirms, this DOI is left out of the Pub page, the citations
and the metadata search engines read, so that nobody is sent to a link that does
not resolve yet. Nothing more to do here: reload this page to see the outcome.
</p>
{checkedAtLine}
</Callout>
);
}

return (
<Callout intent="danger" icon="error" title={`${registrarName} rejected this deposit`}>
<p>
The DOI is not registered, so it is left out of the Pub page, the citations and the
metadata search engines read. Correct what the message below points at, then submit
the deposit again.
</p>
{error && <p className="deposit-status-error">{error}</p>}
{checkedAtLine}
</Callout>
);
}
5 changes: 4 additions & 1 deletion client/components/Deposit/SubmitDepositButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ const buttonTextByStatus = {
[SubmitDepositStatus.Previewing]: 'Generating Preview',
[SubmitDepositStatus.Previewed]: 'Submit Deposit',
[SubmitDepositStatus.Depositing]: 'Depositing',
[SubmitDepositStatus.Deposited]: 'DOI Deposited',
// Not "DOI Deposited": the registrar has only accepted the batch at this
// point and can still reject the record. The outcome is reported by the
// status callout next to this button, which is the only thing that knows it.
[SubmitDepositStatus.Deposited]: 'Deposit Submitted',
};

const getButtonText = (status: SubmitDepositStatus, depositRecord?: DepositRecord) => {
Expand Down
1 change: 1 addition & 0 deletions client/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { default as DashboardRowListing } from './DashboardRow/DashboardRowListi
export { default as DatePicker } from './DatePicker/DatePicker';
export { default as DataciteDeposit } from './Deposit/DataciteDeposit';
export { default as Deposit } from './Deposit/Deposit';
export { default as DepositStatusCallout } from './Deposit/DepositStatusCallout';
export {
DevCommunitySwitcherMenu,
DevCommunitySwitcherMenuItems,
Expand Down
36 changes: 29 additions & 7 deletions client/containers/DashboardSettings/PubSettings/Doi.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import React, { Component } from 'react';
import { Button, Callout, Collapse, FormGroup, InputGroup } from '@blueprintjs/core';

import { apiFetch } from 'client/utils/apiFetch';
import { AssignDoi } from 'components';
import { AssignDoi, DepositStatusCallout } from 'components';
import { getPrimaryCollection } from 'utils/collections/primary';
import { getSchemaForKind } from 'utils/collections/schemas';
import { PUBPUB_DOI_PREFIX } from 'utils/crossref/communities';
import { getDoiDisplay } from 'utils/crossref/depositStatus';
import { isDoi } from 'utils/crossref/parseDoi';
import {
findParentEdgeByRelationTypes,
Expand Down Expand Up @@ -341,18 +342,30 @@ class Doi extends Component<Props, State> {
return null;
}

getDepositState() {
// Null for a viewer without manage permission (pubSanitize withholds the
// record), which lands on 'legacy' and renders as it always has.
return this.props.pubData.crossrefDepositRecord ?? null;
}

renderStatusMessage() {
const { pubData } = this.props;
const { justSetDoi } = this.state;
const depositState = this.getDepositState();
// A deposit with no recorded state is the pre-Doily normal: keep the exact
// sentence this page has always shown rather than implying the DOI is
// unconfirmed.
const hasDepositState = getDoiDisplay(depositState?.status) !== 'legacy';

if (justSetDoi) {
return (
<Callout intent="success" title="Success!">
<p>Successfully submitted a DOI registration for this Pub.</p>
<Callout intent="success" title="Deposit submitted">
<p>The DOI registration for this Pub has been submitted to Crossref.</p>
<p>
Registration may take a few hours to complete in Crossref&apos;s system. If
DOI URLs do not work immediately, the registration is likely still
processing.
Crossref usually rules on a deposit within a few hours. Until it confirms
the record, this DOI is left out of the Pub page, the citations and the
metadata search engines read. Reload this page to see the outcome, which
will include Crossref&apos;s error message if the deposit was rejected.
</p>
</Callout>
);
Expand All @@ -376,7 +389,16 @@ class Doi extends Component<Props, State> {
to Crossref.
</Callout>
)}
{pubData.crossrefDepositRecordId && <p>This Pub has been deposited to Crossref.</p>}
{pubData.crossrefDepositRecordId && !hasDepositState && (
<p>This Pub has been deposited to Crossref.</p>
)}
{pubData.crossrefDepositRecordId && hasDepositState && (
<DepositStatusCallout
status={depositState?.status}
error={depositState?.error}
lastCheckedAt={depositState?.lastCheckedAt}
/>
)}
{this.isDoiEditableWithoutRelations() && this.findSupplementTo() && (
<Callout intent="warning">
The DOI for this Pub is not editable because it is a{' '}
Expand Down
41 changes: 38 additions & 3 deletions client/containers/Pub/PubHeader/details/PubDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import dateFormat from 'dateformat';
import { ClickToCopyButton, ContributorsList } from 'components';
import { collectionUrl } from 'utils/canonicalUrls';
import { getAllPubContributors } from 'utils/contributors';
import { getDoiDisplay, isDoiPublic } from 'utils/crossref/depositStatus';
import { usePageContext } from 'utils/hooks';
import {
getPubCreatedDate,
Expand All @@ -21,7 +22,10 @@ import SmallHeaderButton from '../SmallHeaderButton';
import './pubDetails.scss';

type Props = {
pubData: Pub;
pubData: Pub & {
crossrefDepositStatus?: string | null;
crossrefDepositEverRegistered?: boolean;
};
onCloseHeaderDetails: (...args: any[]) => any;
communityData: {};
};
Expand All @@ -31,7 +35,21 @@ const PubDetails = (props: Props) => {
const { collectionPubs } = pubData;
const contributors = getAllPubContributors(pubData, 'contributors');
const { scopeData } = usePageContext();
const { canView } = scopeData.activePermissions;
const { canView, canManage } = scopeData.activePermissions;

// A DOI whose deposit has not been confirmed is not a link we may ask a
// reader to click: doi.org answers 404 until the registrar has the record.
// Pending still shows the DOI (it is the pub's assigned identifier, and it
// will start resolving) but as text, not a promise. A rejected deposit is
// shown only to someone who can do something about it, with the way there.
const doiDisplay = getDoiDisplay(pubData.crossrefDepositStatus);
// A DOI that ever registered still resolves even when the latest attempt was
// rejected, so it stays printable. doiDisplay above still reports 'failed',
// which is what drives the manager-only caveat.
const doiIsPublic = isDoiPublic(
pubData.crossrefDepositStatus,
pubData.crossrefDepositEverRegistered,
);

const createdAt = getPubCreatedDate(pubData);
const publishedDateString = getPubPublishedDateString(pubData);
Expand Down Expand Up @@ -76,7 +94,7 @@ const PubDetails = (props: Props) => {
)}
</div>
<div className="section citation-and-doi">
{pubData.doi && (
{pubData.doi && doiIsPublic && (
<React.Fragment>
<h6 className="pub-header-themed-secondary">DOI</h6>{' '}
<ClickToCopyButton
Expand All @@ -88,6 +106,23 @@ const PubDetails = (props: Props) => {
</ClickToCopyButton>
</React.Fragment>
)}
{pubData.doi && doiDisplay === 'pending' && (
<React.Fragment>
<h6 className="pub-header-themed-secondary">DOI</h6>{' '}
<div>
{pubData.doi} <i>(registration pending)</i>
</div>
</React.Fragment>
)}
{pubData.doi && doiDisplay === 'failed' && canManage && (
<React.Fragment>
<h6 className="pub-header-themed-secondary">DOI</h6>{' '}
<div>
{pubData.doi} <i>(registration failed)</i>{' '}
<a href={`/dash/pub/${pubData.slug}/settings`}>Resubmit</a>
</div>
</React.Fragment>
)}
<CitationsPreview pubData={pubData} />
</div>
<div className="section collections">
Expand Down
Loading
Loading