Document Workflow
Learn how to implement a complete document workflow from loading to submission using the MIMS SDK.
Overview
A typical document workflow involves these stages:
- Load - Fetch document from API and display in viewer
- View - Navigate pages and zoom
- Edit - Fill in form fields, add signatures
- Validate - Check required fields are completed
- Submit - Send completed document to the API
Complete Implementation
1
Create Document Service
Create a service to handle document API calls:
lib/document-service.tstypescript
import type { DocumentMetadata, AnyFieldDefinition } from '@mims/sdk-react';
export interface DocumentWithFields {
metadata: DocumentMetadata;
documentUrl: string;
fields: AnyFieldDefinition[];
}
export async function fetchDocument(
documentId: string,
apiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>
): Promise<DocumentWithFields> {
return apiRequest<DocumentWithFields>(`/sdk/documents/${documentId}`);
}
export async function submitDocument(
payload: DocumentSubmissionPayload,
apiRequest: <T>(endpoint: string, options?: RequestInit) => Promise<T>
): Promise<{ success: boolean; submissionId: string }> {
return apiRequest('/sdk/documents/submit', {
method: 'POST',
body: JSON.stringify(payload),
});
}2
Create Document Container
Build a container component that manages the document lifecycle:
components/DocumentContainer.tsxtsx
'use client';
import { useEffect, useState } from 'react';
import {
PDFViewer,
useSDK,
useDocumentStore,
type DocumentSubmissionPayload
} from '@mims/sdk-react';
import { fetchDocument, submitDocument } from '@/lib/document-service';
interface DocumentContainerProps {
documentId: string;
onComplete?: (submissionId: string) => void;
onError?: (error: Error) => void;
}
export function DocumentContainer({
documentId,
onComplete,
onError
}: DocumentContainerProps) {
const { apiRequest, isInitialized } = useSDK();
const [documentUrl, setDocumentUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Store actions
const initDocument = useDocumentStore((s) => s.initDocument);
const addField = useDocumentStore((s) => s.addField);
const reset = useDocumentStore((s) => s.reset);
// Load document on mount
useEffect(() => {
if (!isInitialized) return;
async function loadDocument() {
try {
setLoading(true);
setError(null);
const doc = await fetchDocument(documentId, apiRequest);
// Initialize document store
initDocument(documentId, doc.documentUrl);
setDocumentUrl(doc.documentUrl);
// Add predefined fields
doc.fields.forEach((field) => addField(field));
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to load document';
setError(message);
onError?.(err instanceof Error ? err : new Error(message));
} finally {
setLoading(false);
}
}
loadDocument();
// Cleanup on unmount
return () => reset();
}, [documentId, isInitialized, apiRequest, initDocument, addField, reset, onError]);
if (loading) {
return <DocumentSkeleton />;
}
if (error) {
return <DocumentError message={error} onRetry={() => window.location.reload()} />;
}
if (!documentUrl) {
return null;
}
return (
<div className="document-container">
<DocumentToolbar documentId={documentId} onComplete={onComplete} />
<PDFViewer documentUrl={documentUrl} />
</div>
);
}
function DocumentSkeleton() {
return (
<div className="animate-pulse">
<div className="h-12 bg-neutral-200 rounded mb-4" />
<div className="aspect-[8.5/11] bg-neutral-200 rounded" />
</div>
);
}
function DocumentError({ message, onRetry }: { message: string; onRetry: () => void }) {
return (
<div className="text-center py-12">
<p className="text-red-600 mb-4">{message}</p>
<button onClick={onRetry} className="px-4 py-2 bg-black text-white rounded">
Try Again
</button>
</div>
);
}3
Create Document Toolbar
Build toolbar with navigation, zoom, and submit functionality:
components/DocumentToolbar.tsxtsx
'use client';
import { useState } from 'react';
import { useSDK, useDocument, useDocumentStore } from '@mims/sdk-react';
import { submitDocument } from '@/lib/document-service';
interface DocumentToolbarProps {
documentId: string;
onComplete?: (submissionId: string) => void;
}
export function DocumentToolbar({ documentId, onComplete }: DocumentToolbarProps) {
const { apiRequest } = useSDK();
const { currentPage, totalPages, isDirty } = useDocument();
const goToPage = useDocumentStore((s) => s.goToPage);
const validateFields = useDocumentStore((s) => s.validateFields);
const getSubmissionPayload = useDocumentStore((s) => s.getSubmissionPayload);
const renderContext = useDocumentStore((s) => s.renderContext);
const setRenderContext = useDocumentStore((s) => s.setRenderContext);
const [submitting, setSubmitting] = useState(false);
const handleZoom = (delta: number) => {
const newScale = Math.max(0.5, Math.min(3, renderContext.scale + delta));
setRenderContext({ scale: newScale });
};
const handleSubmit = async () => {
// Validate all fields
if (!validateFields()) {
alert('Please complete all required fields');
return;
}
const payload = getSubmissionPayload();
if (!payload) return;
setSubmitting(true);
try {
const result = await submitDocument(payload, apiRequest);
onComplete?.(result.submissionId);
} catch (error) {
console.error('Submit failed:', error);
alert('Failed to submit document. Please try again.');
} finally {
setSubmitting(false);
}
};
return (
<div className="flex items-center justify-between p-4 border-b">
{/* Navigation */}
<div className="flex items-center gap-2">
<button
onClick={() => goToPage(currentPage - 1)}
disabled={currentPage <= 1}
className="p-2 disabled:opacity-50"
>
← Prev
</button>
<span>{currentPage} / {totalPages}</span>
<button
onClick={() => goToPage(currentPage + 1)}
disabled={currentPage >= totalPages}
className="p-2 disabled:opacity-50"
>
Next →
</button>
</div>
{/* Zoom */}
<div className="flex items-center gap-2">
<button onClick={() => handleZoom(-0.25)} className="p-2">−</button>
<span>{Math.round(renderContext.scale * 100)}%</span>
<button onClick={() => handleZoom(0.25)} className="p-2">+</button>
</div>
{/* Submit */}
<button
onClick={handleSubmit}
disabled={submitting || !isDirty}
className="px-4 py-2 bg-emerald-500 text-white rounded disabled:opacity-50"
>
{submitting ? 'Submitting...' : 'Submit Document'}
</button>
</div>
);
}4
Create Page Component
Use the container in your page:
app/documents/[id]/page.tsxtsx
import { MIMSProvider } from '@mims/sdk-react';
import { DocumentContainer } from '@/components/DocumentContainer';
import { redirect } from 'next/navigation';
interface Props {
params: { id: string };
}
export default function DocumentPage({ params }: Props) {
const handleComplete = (submissionId: string) => {
// Redirect to success page
redirect(`/documents/${params.id}/complete?submission=${submissionId}`);
};
return (
<MIMSProvider
apiKey={process.env.NEXT_PUBLIC_MIMS_API_KEY!}
onError={(error) => console.error('SDK Error:', error)}
>
<div className="container mx-auto py-8">
<DocumentContainer
documentId={params.id}
onComplete={handleComplete}
/>
</div>
</MIMSProvider>
);
}Workflow States
Track document workflow states for better UX:
Workflow State Machinetsx
type WorkflowState =
| 'loading'
| 'viewing'
| 'editing'
| 'validating'
| 'submitting'
| 'complete'
| 'error';
function useWorkflowState() {
const [state, setState] = useState<WorkflowState>('loading');
const { isLoading, error, isDirty } = useDocument();
const selectedField = useSelectedField();
useEffect(() => {
if (isLoading) {
setState('loading');
} else if (error) {
setState('error');
} else if (selectedField) {
setState('editing');
} else if (isDirty) {
setState('viewing');
}
}, [isLoading, error, isDirty, selectedField]);
return { state, setState };
}Progress Tracking
Show users their progress through required fields:
Progress Indicatortsx
function DocumentProgress() {
const fields = useDocumentStore((s) => s.fields);
const requiredFields = Array.from(fields.values())
.filter(f => f.definition.required);
const completedFields = requiredFields
.filter(f => f.value !== null);
const progress = requiredFields.length > 0
? (completedFields.length / requiredFields.length) * 100
: 100;
return (
<div className="progress-container">
<div className="flex justify-between mb-2">
<span>Progress</span>
<span>{completedFields.length} / {requiredFields.length} fields</span>
</div>
<div className="h-2 bg-neutral-200 rounded-full overflow-hidden">
<div
className="h-full bg-emerald-500 transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
);
}Best Practice
Always validate fields before submission and provide clear feedback about which fields need attention.
Error Recovery
Implement error recovery for failed submissions:
Error Recoverytsx
function useSubmitWithRetry() {
const { apiRequest } = useSDK();
const getSubmissionPayload = useDocumentStore((s) => s.getSubmissionPayload);
const [attempts, setAttempts] = useState(0);
const maxAttempts = 3;
const submit = async () => {
const payload = getSubmissionPayload();
if (!payload) throw new Error('No document loaded');
for (let i = 0; i < maxAttempts; i++) {
try {
setAttempts(i + 1);
const result = await apiRequest('/sdk/documents/submit', {
method: 'POST',
body: JSON.stringify(payload),
});
return result;
} catch (error) {
if (i === maxAttempts - 1) throw error;
// Wait before retry (exponential backoff)
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
}
}
};
return { submit, attempts, maxAttempts };
}