Document Change History Architecture
Overview
The Document Change History feature provides a timeline view of all changes made to a document, including creation, title changes, content modifications, moves, state changes, and deletions. Users can view previous versions and restore them if needed.
Architecture Principles
This implementation follows best practices for:
- ✅ Testability: Complex logic extracted into pure utility functions and custom hooks
- ✅ Extensibility: Easy to add new change types without touching existing code
- ✅ Type Safety: Full TypeScript support with proper type definitions
- ✅ Error Handling: Graceful degradation with loading and error states
- ✅ User Experience: Relative timestamps with full date on hover, smooth interactions
Directory Structure
changeHistory/
├── ARCHITECTURE.md # This file
├── hooks/
│ └── useParentDocumentTitle.ts # Custom hook for fetching parent titles
├── utils/
│ └── changeUtils.ts # Pure utility functions for data extraction
├── changeMessages/
│ ├── ChangeMessageComponents.tsx # Individual message components + router
│ └── README.md # Guide for adding new change types
├── DocumentChangeHistoryDrawer.tsx # Main drawer container
├── DocumentHistoryTimeline.tsx # Timeline list component
├── DocumentChangeTimelineContent.tsx # Individual timeline entry
├── DocumentChangeTimelineDot.tsx # User avatar for each change
└── PreviousVersionModal.tsx # View/restore previous content
Component Hierarchy
DocumentChangeHistoryDrawer
└── DocumentHistoryTimeline
└── Timeline (from alchemy-components)
├── DocumentChangeTimelineDot (for each item)
└── DocumentChangeTimelineContent (for each item)
├── ChangeMessage (router component)
│ ├── CreatedMessage
│ ├── TitleChangedMessage
│ ├── TextChangedMessage
│ ├── StateChangedMessage
│ ├── ParentChangedMessage (uses useParentDocumentTitle hook)
│ ├── DeletedMessage
│ └── DefaultMessage
└── PreviousVersionModal (conditional, for text changes)
Key Features
1. Testable Utilities
extractChangeDetails(details)
Converts GraphQL StringMapEntry[] to Record<string, string>
- Input: Array of {key, value} objects
- Output: Simple key-value object
- Testable: Pure function, no dependencies
- Location:
utils/changeUtils.ts
// Easy to unit test
const details = [
{ key: 'oldTitle', value: 'Old' },
{ key: 'newTitle', value: 'New' },
];
const result = extractChangeDetails(details);
// { oldTitle: 'Old', newTitle: 'New' }
getActorDisplayName(actor, getDisplayName)
Extracts actor display name with fallback
- Input: Actor object + display name function
- Output: String name or 'System'
- Testable: Can mock the getDisplayName function
- Location:
utils/changeUtils.ts
2. Custom Hooks
useParentDocumentTitle(parentUrn)
Fetches parent document title with proper state handling
- Returns:
{ title, loading, error } - States:
- Loading: returns
'...' - Error: returns
'Unknown Document'+ logs error - Success: returns actual title
- No URN: returns
'...'
- Loading: returns
- Testable: Can be tested with mocked GraphQL queries
- Location:
hooks/useParentDocumentTitle.ts
3. Error & Loading States
| Component | Loading Handled | Error Handled | Notes |
|---|---|---|---|
useParentDocumentTitle | ✅ | ✅ | Shows '...' while loading, 'Unknown Document' on error |
PreviousVersionModal | ✅ | ✅ | Disables buttons during restore, shows error message |
DocumentHistoryTimeline | ✅ | ✅ | Shows loading skeleton, empty state message |
4. User Experience
Timestamps
- Relative: "2 minutes ago", "3 days ago" (using dayjs.fromNow())
- Absolute: Hover shows full timestamp "March 15, 2024 2:30:45 PM"
- Implementation: Popover with formatted timestamp
Change Messages
All messages follow the pattern: {ActorName} {action} {target}
- ✅ Actor names in bold
- ✅ Important values (titles, parent names) in bold
- ✅ Interactive elements (links) styled appropriately
Restore Flow
- Click "See previous version" → Opens modal
- Review old content
- Click "Restore" → Shows confirmation
- Confirm → Restores content + refetches + closes modals
- Error handling → Shows error message, keeps modals open
Adding a New Change Type
See changeMessages/README.md for detailed instructions. Quick summary:
- Update GraphQL schema (backend)
- Update event generator (backend, if needed)
- Create message component:
export const MyNewMessage: React.FC<ActorWithDetailsProps> = ({ actorName, details }) => (
<ActionText>
<ActorName>{actorName}</ActorName> did something
</ActionText>
); - Add to router:
case DocumentChangeType.MyNewType:
return <MyNewMessage actorName={actorName} details={details} />; - Run
yarn generateto update types
GraphQL Integration
Query
query getDocumentChangeHistory($urn: String!, $limit: Int) {
document(urn: $urn) {
changeHistory(limit: $limit) {
changeType
description
actor {
urn
type
username
info
editableProperties
}
timestamp
details {
key
value
}
}
}
}
Mutation (for restore)
mutation updateDocumentContents($input: UpdateDocumentContentsInput!) {
updateDocumentContents(input: $input)
}
Testing Strategy
Unit Tests (Recommended)
changeUtils.tsfunctions (pure functions, easy to test)- Individual message components (can pass mock data)
useParentDocumentTitlehook (mock GraphQL responses)
Integration Tests
- Full timeline rendering with mock data
- Restore flow (mock mutations)
- Error handling scenarios
E2E Tests
- Create document → view history → see creation event
- Edit title → view history → see title change with new title
- Edit content → view history → see content change → restore previous version
- Move document → view history → see move with parent name
Performance Considerations
Optimizations
- Conditional Query: Parent title only fetched when needed
- Skip Flag: GraphQL queries skipped when URN is empty
- Memoization:
detailsobject memoized in component - Code Splitting: Large modal only loaded when needed
Data Loading
- Timeline loads up to 100 most recent changes
- Each change with a parent triggers a separate query (could be optimized with batching if needed)
- Modal content is part of change details (no additional fetch needed)
Future Enhancements
Potential Improvements
- Diff View: Show inline diffs for content changes (not just full previous version)
- Batch Parent Queries: Load all parent titles in one query
- Infinite Scroll: Load more changes on demand
- Filtering: Filter by change type, date range, or actor
- Comparison: Compare any two versions side-by-side
- Annotations: Add comments to specific changes
Extensibility Points
- Add new change types in
ChangeMessageComponents.tsx - Add new data fetching hooks in
hooks/ - Add new utilities in
utils/ - Customize message rendering per change type
Code Quality Metrics
- Lines of Code: ~600 total
- Number of Components: 11
- Number of Hooks: 1 custom
- Number of Utilities: 2
- TypeScript Coverage: 100%
- Linting Errors: 0
- Type Errors: 0
Dependencies
Internal
@app/entityV2/document- Document queries and mutations@app/useEntityRegistry- Entity display names@app/sharedV2/modals/ConfirmationModal- Confirmation dialogs@src/alchemy-components- UI components (Timeline, Popover, Button, etc.)
External
dayjs- Date formatting and relative timeantd- Modal componentreact- Component frameworkstyled-components- Styling
Maintenance Guide
Common Tasks
Update message text: Edit the appropriate component in changeMessages/ChangeMessageComponents.tsx
Change timestamp format: Modify dayjs.format() calls in DocumentChangeTimelineContent.tsx
Add new detail field: Update backend event generator → regenerate types → use in message component
Customize styling: Update styled-components in respective files
Debugging
Timeline not showing: Check GraphQL query response in DevTools Network tab
Parent name shows '...': Check if parent URN is valid and document exists
Restore not working: Check mutation response and refetch behavior
Wrong actor name: Verify actor data in change history response