Skip to main content
Version: 1.5.0

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 '...'
  • Testable: Can be tested with mocked GraphQL queries
  • Location: hooks/useParentDocumentTitle.ts

3. Error & Loading States

ComponentLoading HandledError HandledNotes
useParentDocumentTitleShows '...' while loading, 'Unknown Document' on error
PreviousVersionModalDisables buttons during restore, shows error message
DocumentHistoryTimelineShows 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

  1. Click "See previous version" → Opens modal
  2. Review old content
  3. Click "Restore" → Shows confirmation
  4. Confirm → Restores content + refetches + closes modals
  5. Error handling → Shows error message, keeps modals open

Adding a New Change Type

See changeMessages/README.md for detailed instructions. Quick summary:

  1. Update GraphQL schema (backend)
  2. Update event generator (backend, if needed)
  3. Create message component:
    export const MyNewMessage: React.FC<ActorWithDetailsProps> = ({ actorName, details }) => (
    <ActionText>
    <ActorName>{actorName}</ActorName> did something
    </ActionText>
    );
  4. Add to router:
    case DocumentChangeType.MyNewType:
    return <MyNewMessage actorName={actorName} details={details} />;
  5. Run yarn generate to 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

  • changeUtils.ts functions (pure functions, easy to test)
  • Individual message components (can pass mock data)
  • useParentDocumentTitle hook (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

  1. Conditional Query: Parent title only fetched when needed
  2. Skip Flag: GraphQL queries skipped when URN is empty
  3. Memoization: details object memoized in component
  4. 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

  1. Diff View: Show inline diffs for content changes (not just full previous version)
  2. Batch Parent Queries: Load all parent titles in one query
  3. Infinite Scroll: Load more changes on demand
  4. Filtering: Filter by change type, date range, or actor
  5. Comparison: Compare any two versions side-by-side
  6. 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 time
  • antd - Modal component
  • react - Component framework
  • styled-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