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:
- Backend authorization: The frontend calls an XRPC endpoint to validate permissions and compute version numbers
- 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:
| Section | Fields | Description |
|---|---|---|
| Metadata | Title, abstract, keywords | Core document information |
| Authors | Author list, affiliations, ORCID | Authorship and attribution |
| Fields | Field classifications | Academic discipline categorization |
| Publication | Status, DOI, journal | Publication metadata |
| Files | PDF, supplementary materials | Document files |
| Supplementary | Code, data, appendices | Additional materials |
| Facets | PMEST classification | Faceted classification values |
| Review | Changelog, version bump | Version 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:
| Prop | Type | Description |
|---|---|---|
| eprint | EprintEditData | Current eprint data |
| canEdit | boolean | Whether user has edit permission |
| onSuccess | () => void | Callback after successful edit |
| children | ReactNode | Trigger 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:
| Prop | Type | Description |
|---|---|---|
| title | string | Eprint title for confirmation |
| uri | string | AT-URI of eprint |
| canDelete | boolean | Whether deletion is allowed |
| isPending | boolean | Whether delete operation is running |
| onConfirm | () => void | Callback when deletion is confirmed |
| children | ReactNode | Trigger 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:
| Prop | Type | Description |
|---|---|---|
| value | 'major' | 'minor' | 'patch' | Selected version bump type |
| onChange | (value: VersionBumpType) => void | Selection change handler |
| currentVersion | string | Current version for display |
| disabled | boolean | Whether 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:
| Prop | Type | Description |
|---|---|---|
| value | ChangelogFormData | Current changelog state |
| onChange | (value: ChangelogFormData) => void | State change handler |
| showReviewFields | boolean | Show review reference fields |
| disabled | boolean | Disable all inputs |
| className | string | Additional 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:
| Prop | Type | Description |
|---|---|---|
| eprintUri | string | AT-URI of the eprint |
| className | string | Additional 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:
| Prop | Type | Description |
|---|---|---|
| eprint | EprintData | Eprint with optional paperDid |
| children | ReactNode | Content to render when authenticated |
| onAuthenticated | () => void | Callback when paper auth succeeds |
Behavior:
- If
eprint.paperDidis undefined, children render directly - If
eprint.paperDidis set, showsPaperAuthPromptuntil 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:
| Prop | Type | Description |
|---|---|---|
| paperDid | string | DID of paper account |
| onSuccess | () => void | Callback on successful auth |
| onError | (error: Error) => void | Callback 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:
| Prop | Type | Description |
|---|---|---|
| uri | string | AT-URI of the record to migrate |
| fields | string[] | List of fields needing migration |
| onMigrated | () => void | Callback after successful migration |
| className | string | Additional 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 descriptionstatus: 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.tsxweb/components/eprints/delete-dialog.test.tsxweb/components/eprints/changelog-form.test.tsxweb/components/eprints/version-history.test.tsxweb/components/eprints/paper-auth-gate.test.tsxweb/lib/hooks/use-eprint-mutations.test.ts
Related Documentation
- Editing Eprints (User Guide): End-user documentation
- Lexicons Reference: Record schemas
- XRPC Endpoints: Backend API reference
- Frontend Development: General frontend architecture