Skip to main content

Frontend Eprint Lifecycle Components

This guide covers the React components and hooks for eprint editing, versioning, and deletion.

Architecture Overview

The eprint lifecycle features follow a two-step authorization pattern:

  1. Backend authorization: The frontend calls an XRPC endpoint to validate permissions and compute version numbers
  2. PDS write: The frontend uses the ATProto agent to write to the user's (or paper's) PDS

This design keeps authorization logic on the backend while maintaining ATProto's principle that users control their own data.

Section-Based Editing

The eprint edit page (/eprints/edit/[...uri]) uses a section-based UI that organizes editable fields into logical groups:

SectionFieldsDescription
MetadataTitle, abstract, keywordsCore document information
AuthorsAuthor list, affiliations, ORCIDAuthorship and attribution
FieldsField classificationsAcademic discipline categorization
PublicationStatus, DOI, journalPublication metadata
FilesPDF, supplementary materialsDocument files
SupplementaryCode, data, appendicesAdditional materials
FacetsPMEST classificationFaceted classification values
ReviewChangelog, version bumpVersion control

Each section can be expanded or collapsed, with unsaved changes indicated by a badge.

React Query Hooks

All lifecycle operations use TanStack Query mutations located in web/lib/hooks/use-eprint-mutations.ts.

useUpdateEprint

Authorizes an eprint update and returns the new version info.

import { useUpdateEprint, formatVersion } from '@/lib/hooks/use-eprint-mutations';

function EditEprintForm({ eprint }) {
const { mutateAsync: updateEprint, isPending, error } = useUpdateEprint();
const agent = useAgent();

const handleSubmit = async (values) => {
// Step 1: Get authorization and new version from backend
const authResult = await updateEprint({
uri: eprint.uri,
versionBump: values.versionBump,
title: values.title,
keywords: values.keywords,
changelog: values.changelog,
});

// Step 2: Update record in PDS with optimistic concurrency control
await agent.com.atproto.repo.putRecord({
repo: eprint.repo,
collection: eprint.collection,
rkey: eprint.rkey,
record: { ...currentRecord, version: authResult.version },
swapRecord: authResult.expectedCid,
});
};

return <form onSubmit={handleSubmit}>...</form>;
}

useDeleteEprint

Authorizes an eprint deletion.

import { useDeleteEprint } from '@/lib/hooks/use-eprint-mutations';

function DeleteEprintButton({ eprint }) {
const { mutateAsync: deleteEprint, isPending } = useDeleteEprint();
const agent = useAgent();

const handleDelete = async () => {
// Step 1: Authorize deletion
await deleteEprint({ uri: eprint.uri });

// Step 2: Delete record from PDS
await agent.com.atproto.repo.deleteRecord({
repo: eprint.repo,
collection: eprint.collection,
rkey: eprint.rkey,
});
};

return <button onClick={handleDelete} disabled={isPending}>Delete</button>;
}

useEprintPermissions

Determines if the current user can modify an eprint.

import { useEprintPermissions } from '@/lib/hooks/use-eprint-mutations';

function EprintActions({ eprint, userDid }) {
const { canModify, requiresPaperAuth, reason } = useEprintPermissions(eprint, userDid);

if (!canModify) {
return <span title={reason}>Editing disabled</span>;
}

if (requiresPaperAuth) {
return <PaperAuthGate eprint={eprint}><EditButton /></PaperAuthGate>;
}

return <EditButton />;
}

useEprintChangelogs

Fetches paginated changelogs for an eprint.

import { useEprintChangelogs } from '@/lib/hooks/use-eprint-mutations';

function VersionHistory({ eprintUri }) {
const { data, isLoading, error } = useEprintChangelogs(eprintUri, {
limit: 20,
});

if (isLoading) return <VersionHistorySkeleton />;
if (error) return <ChangelogError error={error} />;

return (
<ul>
{data?.changelogs.map((changelog) => (
<VersionEntry key={changelog.uri} changelog={changelog} />
))}
</ul>
);
}

formatVersion

Formats a semantic version object as a display string.

import { formatVersion } from '@/lib/hooks/use-eprint-mutations';

formatVersion({ major: 1, minor: 2, patch: 3 });
// Returns: "1.2.3"

formatVersion({ major: 2, minor: 0, patch: 0, prerelease: 'draft' });
// Returns: "2.0.0-draft"

UI Components

EprintEditDialog

Dialog for editing eprint metadata and creating new versions.

Location: web/components/eprints/eprint-edit-dialog.tsx

import { EprintEditDialog } from '@/components/eprints/eprint-edit-dialog';

<EprintEditDialog
eprint={{
uri: 'at://did:plc:abc/pub.chive.eprint.submission/123',
rkey: '123',
collection: 'pub.chive.eprint.submission',
title: 'My Eprint',
keywords: ['machine learning', 'nlp'],
version: { major: 1, minor: 0, patch: 0 },
repo: 'did:plc:abc',
}}
canEdit={true}
onSuccess={() => refetch()}
>
<Button variant="outline">Edit Eprint</Button>
</EprintEditDialog>

Props:

PropTypeDescription
eprintEprintEditDataCurrent eprint data
canEditbooleanWhether user has edit permission
onSuccess() => voidCallback after successful edit
childrenReactNodeTrigger element (optional)

DeleteEprintDialog

Confirmation dialog for eprint deletion.

Location: web/components/eprints/delete-dialog.tsx

import { DeleteEprintDialog } from '@/components/eprints/delete-dialog';

<DeleteEprintDialog
title={eprint.title}
uri={eprint.uri}
canDelete={canModify}
isPending={isDeleting}
onConfirm={handleDelete}
>
<Button variant="destructive">Delete</Button>
</DeleteEprintDialog>

Props:

PropTypeDescription
titlestringEprint title for confirmation
uristringAT-URI of eprint
canDeletebooleanWhether deletion is allowed
isPendingbooleanWhether delete operation is running
onConfirm() => voidCallback when deletion is confirmed
childrenReactNodeTrigger element (optional)

VersionSelector

Radio group for selecting version bump type.

Location: web/components/eprints/version-selector.tsx

import { VersionSelector } from '@/components/eprints/version-selector';

const [versionBump, setVersionBump] = useState<VersionBumpType>('patch');

<VersionSelector
value={versionBump}
onChange={setVersionBump}
currentVersion="1.2.3"
disabled={false}
/>

Props:

PropTypeDescription
value'major' | 'minor' | 'patch'Selected version bump type
onChange(value: VersionBumpType) => voidSelection change handler
currentVersionstringCurrent version for display
disabledbooleanWhether selector is disabled

ChangelogForm

Form for creating structured changelogs.

Location: web/components/eprints/changelog-form.tsx

import { ChangelogForm, type ChangelogFormData } from '@/components/eprints/changelog-form';

const [changelog, setChangelog] = useState<ChangelogFormData>({
summary: '',
sections: [],
reviewerResponse: undefined,
});

<ChangelogForm
value={changelog}
onChange={setChangelog}
showReviewFields={isRespondingToReview}
disabled={isSubmitting}
/>

Props:

PropTypeDescription
valueChangelogFormDataCurrent changelog state
onChange(value: ChangelogFormData) => voidState change handler
showReviewFieldsbooleanShow review reference fields
disabledbooleanDisable all inputs
classNamestringAdditional CSS classes

VersionHistory

Timeline display of version history with expandable changelogs.

Location: web/components/eprints/version-history.tsx

import { VersionHistory } from '@/components/eprints/version-history';

<VersionHistory eprintUri="at://did:plc:abc/pub.chive.eprint.submission/123" />

Props:

PropTypeDescription
eprintUristringAT-URI of the eprint
classNamestringAdditional CSS classes

PaperAuthGate

Wrapper that requires paper account authentication for paper-centric eprints.

Location: web/components/eprints/paper-auth-gate.tsx

import { PaperAuthGate } from '@/components/eprints/paper-auth-gate';

<PaperAuthGate
eprint={eprint}
onAuthenticated={() => console.log('Paper auth successful')}
>
<EditEprintDialog eprint={eprint} canEdit={true} />
</PaperAuthGate>

Props:

PropTypeDescription
eprintEprintDataEprint with optional paperDid
childrenReactNodeContent to render when authenticated
onAuthenticated() => voidCallback when paper auth succeeds

Behavior:

  • If eprint.paperDid is undefined, children render directly
  • If eprint.paperDid is set, shows PaperAuthPrompt until authenticated

PaperAuthPrompt

UI for initiating paper account authentication.

Location: web/components/eprints/paper-auth-prompt.tsx

import { PaperAuthPrompt } from '@/components/eprints/paper-auth-prompt';

<PaperAuthPrompt
paperDid="did:plc:paper123"
onSuccess={() => setAuthenticated(true)}
onError={(err) => toast.error(err.message)}
/>

Props:

PropTypeDescription
paperDidstringDID of paper account
onSuccess() => voidCallback on successful auth
onError(error: Error) => voidCallback on auth failure (optional)

SchemaMigrationBanner

Banner component that prompts users to update records using outdated schema formats.

Location: web/components/migrations/migration-banner.tsx

import { SchemaMigrationBanner } from '@/components/migrations/migration-banner';

<SchemaMigrationBanner
uri={eprint.uri}
fields={['title', 'abstract']}
onMigrated={() => refetch()}
/>

Props:

PropTypeDescription
uristringAT-URI of the record to migrate
fieldsstring[]List of fields needing migration
onMigrated() => voidCallback after successful migration
classNamestringAdditional CSS classes (optional)

The banner appears when needsSchemaMigration() returns true for a record. It shows which fields need updating and provides a one-click migration action.

QuickEditPencil

Inline edit button for quick title editing without opening the full edit dialog.

Location: web/components/eprints/eprint-header.tsx

The pencil icon appears next to the title when the user has edit permissions. Clicking it opens an inline text input for immediate title changes.

Data Types

EprintEditData

Data required for the edit dialog.

interface EprintEditData {
uri: string; // AT-URI of the eprint
rkey: string; // Record key
collection: string; // Collection NSID
title: string; // Current title
keywords?: string[]; // Current keywords
version?: SemanticVersion; // Current version
repo: string; // DID of repository owner
}

ChangelogFormData

Changelog state for the form.

interface ChangelogFormData {
summary?: string;
sections: ChangelogSection[];
reviewerResponse?: string;
}

interface ChangelogSection {
category: ChangelogCategory;
items: ChangelogItem[];
}

interface ChangelogItem {
description: string;
changeType?: ChangeType;
location?: string;
reviewReference?: string;
}

VersionBumpType

type VersionBumpType = 'major' | 'minor' | 'patch';

SemanticVersion

interface SemanticVersion {
major: number;
minor: number;
patch: number;
prerelease?: string;
}

Query Key Management

The hooks use a query key factory for cache management.

import { changelogKeys, eprintKeys } from '@/lib/hooks/use-eprint-mutations';

// Invalidate all changelog queries
queryClient.invalidateQueries({ queryKey: changelogKeys.all });

// Invalidate changelogs for a specific eprint
queryClient.invalidateQueries({ queryKey: changelogKeys.list(eprintUri) });

// Invalidate a specific eprint
queryClient.invalidateQueries({ queryKey: eprintKeys.detail(uri) });

Error Handling

All mutations throw APIError on failure. The error includes:

  • message: Human-readable error description
  • status: HTTP status code (if available)
  • endpoint: XRPC endpoint that failed
import { APIError } from '@/lib/errors';

try {
await updateEprint({ uri, versionBump: 'minor' });
} catch (error) {
if (error instanceof APIError) {
if (error.message.includes('swapRecord')) {
toast.error('Update conflict: record was modified');
} else if (error.message.includes('Unauthorized')) {
toast.error('Not authorized to edit this eprint');
} else {
toast.error(`Failed to update: ${error.message}`);
}
}
}

Testing

Components include comprehensive test coverage. Run tests with:

pnpm test:unit web/components/eprints
pnpm test:unit web/lib/hooks/use-eprint-mutations.test.ts

Key test files:

  • web/components/eprints/eprint-edit-dialog.test.tsx
  • web/components/eprints/delete-dialog.test.tsx
  • web/components/eprints/changelog-form.test.tsx
  • web/components/eprints/version-history.test.tsx
  • web/components/eprints/paper-auth-gate.test.tsx
  • web/lib/hooks/use-eprint-mutations.test.ts