Files
thrilltrack-explorer/docs/versioning/FRONTEND.md
2025-10-15 17:54:53 +00:00

614 lines
13 KiB
Markdown

# Frontend Integration Guide
**How to use versioning in React components**
## Overview
The versioning system provides React hooks and components for displaying version history, comparing versions, and rolling back changes. All components are fully typed with TypeScript discriminated unions.
## Type System
### Core Types
```typescript
// Entity type union
type EntityType = 'park' | 'ride' | 'company' | 'ride_model';
// Change type enum
type ChangeType = 'created' | 'updated' | 'deleted' | 'restored' | 'archived';
// Base version metadata
interface BaseVersion {
version_id: string;
version_number: number;
created_at: string;
created_by: string | null;
change_type: ChangeType;
change_reason: string | null;
submission_id: string | null;
is_current: boolean;
}
```
### Entity-Specific Versions
The system uses discriminated unions for type-safe version handling:
```typescript
// Discriminated union based on entity type
type EntityVersion =
| ParkVersion
| RideVersion
| CompanyVersion
| RideModelVersion;
// Example: Park version
interface ParkVersion extends BaseVersion {
park_id: string;
name: string;
slug: string;
description: string | null;
park_type: string;
status: string;
opening_date: string | null;
closing_date: string | null;
location_id: string | null;
operator_id: string | null;
property_owner_id: string | null;
// ... all park fields
}
```
### Version Diff
```typescript
interface VersionDiff {
[fieldName: string]: {
from: any;
to: any;
changed: boolean;
};
}
```
## React Hooks
### useEntityVersions
**Purpose:** Fetch and manage version history for an entity.
#### Usage
```typescript
import { useEntityVersions } from '@/hooks/useEntityVersions';
const MyComponent = ({ parkId }: { parkId: string }) => {
const {
versions,
currentVersion,
loading,
fieldHistory,
fetchVersions,
fetchFieldHistory,
compareVersions,
rollbackToVersion,
} = useEntityVersions('park', parkId);
// versions: Array of all versions
// currentVersion: Latest version with is_current = true
// loading: Boolean loading state
// fieldHistory: Array of field changes for selected version
};
```
#### Return Type
```typescript
interface UseEntityVersionsReturn {
versions: EntityVersion[];
currentVersion: EntityVersion | null;
loading: boolean;
fieldHistory: FieldChange[];
fetchVersions: () => Promise<void>;
fetchFieldHistory: (versionId: string) => Promise<void>;
compareVersions: (fromId: string, toId: string) => Promise<VersionDiff | null>;
rollbackToVersion: (versionId: string, reason: string) => Promise<boolean>;
}
```
#### Methods
**fetchVersions()**
- Fetches all versions for the entity
- Automatically called on mount and entity ID change
- Returns: `Promise<void>`
**fetchFieldHistory(versionId)**
- Fetches detailed field changes for a specific version
- Populates `fieldHistory` state
- Parameters:
- `versionId: string` - Version to analyze
- Returns: `Promise<void>`
**compareVersions(fromId, toId)**
- Compares two versions using database function
- Parameters:
- `fromId: string` - Older version ID
- `toId: string` - Newer version ID
- Returns: `Promise<VersionDiff | null>`
**rollbackToVersion(versionId, reason)**
- Restores entity to a previous version
- Creates new version with `change_type='restored'`
- Requires moderator role
- Parameters:
- `versionId: string` - Target version to restore
- `reason: string` - Reason for rollback
- Returns: `Promise<boolean>` - Success status
### useVersionComparison
**Purpose:** Compare two specific versions and get diff.
#### Usage
```typescript
import { useVersionComparison } from '@/hooks/useVersionComparison';
const ComparisonView = () => {
const { diff, loading, error } = useVersionComparison(
'park',
fromVersionId,
toVersionId
);
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return (
<div>
{Object.entries(diff || {}).map(([field, change]) => (
<FieldDiff
key={field}
field={field}
from={change.from}
to={change.to}
/>
))}
</div>
);
};
```
#### Return Type
```typescript
interface UseVersionComparisonReturn {
diff: VersionDiff | null;
loading: boolean;
error: string | null;
}
```
## Components
### VersionIndicator
**Purpose:** Display current version badge on entity pages.
#### Props
```typescript
interface VersionIndicatorProps {
entityType: EntityType;
entityId: string;
entityName: string;
compact?: boolean; // Compact mode shows only version number
}
```
#### Usage
```typescript
import { VersionIndicator } from '@/components/versioning/VersionIndicator';
// Full mode (with "Last edited" timestamp)
<VersionIndicator
entityType="park"
entityId={park.id}
entityName={park.name}
/>
// Compact mode (badge only)
<VersionIndicator
entityType="park"
entityId={park.id}
entityName={park.name}
compact
/>
```
#### Output
- Displays: "Version 3 · Last edited 2 hours ago"
- Clicking opens `EntityVersionHistory` dialog
- Compact mode: Just "v3" badge
### EntityVersionHistory
**Purpose:** Display complete version timeline with comparison and rollback.
#### Props
```typescript
interface EntityVersionHistoryProps {
entityType: EntityType;
entityId: string;
entityName: string;
}
```
#### Usage
```typescript
import { EntityVersionHistory } from '@/components/versioning/EntityVersionHistory';
<Dialog>
<DialogContent>
<EntityVersionHistory
entityType="park"
entityId={park.id}
entityName={park.name}
/>
</DialogContent>
</Dialog>
```
#### Features
- Timeline of all versions
- Select two versions to compare
- Rollback to any previous version
- User attribution and timestamps
- Change type badges (created, updated, restored)
### VersionComparisonDialog
**Purpose:** Side-by-side diff view of two versions.
#### Props
```typescript
interface VersionComparisonDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
entityType: EntityType;
fromVersionId: string | null;
toVersionId: string | null;
}
```
#### Usage
```typescript
import { VersionComparisonDialog } from '@/components/versioning/VersionComparisonDialog';
<VersionComparisonDialog
open={showComparison}
onOpenChange={setShowComparison}
entityType="park"
fromVersionId={selectedVersions[0]}
toVersionId={selectedVersions[1]}
/>
```
#### Output
- Field-by-field comparison
- Highlights added, removed, and modified fields
- Color-coded changes (green=added, red=removed, yellow=modified)
- Formatted values (dates, JSON, etc.)
### RollbackDialog
**Purpose:** Confirm and execute version rollback.
#### Props
```typescript
interface RollbackDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
entityType: EntityType;
entityId: string;
targetVersion: EntityVersion;
onRollback: (versionId: string, reason: string) => Promise<boolean>;
}
```
#### Usage
```typescript
import { RollbackDialog } from '@/components/versioning/RollbackDialog';
<RollbackDialog
open={showRollback}
onOpenChange={setShowRollback}
entityType="park"
entityId={park.id}
targetVersion={selectedVersion}
onRollback={rollbackToVersion}
/>
```
#### Features
- Requires rollback reason (text input)
- Shows preview of target version
- Confirms action with user
- Displays success/error toast
## Integration Examples
### Adding Version History to Entity Pages
```typescript
// Example: ParkDetail.tsx
import { VersionIndicator } from '@/components/versioning/VersionIndicator';
import { EntityHistoryTabs } from '@/components/history/EntityHistoryTabs';
const ParkDetail = () => {
const { park } = usePark();
return (
<div>
{/* Header with version indicator */}
<div className="flex justify-between items-center">
<h1>{park.name}</h1>
<VersionIndicator
entityType="park"
entityId={park.id}
entityName={park.name}
/>
</div>
{/* Tabs with history */}
<Tabs>
<TabsList>
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="history">History</TabsTrigger>
</TabsList>
<TabsContent value="details">
{/* Entity details */}
</TabsContent>
<TabsContent value="history">
<EntityHistoryTabs
entityType="park"
entityId={park.id}
entityName={park.name}
events={[]} // Event history
formerNames={park.former_names}
currentName={park.name}
/>
</TabsContent>
</Tabs>
</div>
);
};
```
### Custom Version List
```typescript
const CustomVersionList = ({ parkId }: { parkId: string }) => {
const { versions, loading } = useEntityVersions('park', parkId);
if (loading) return <Skeleton />;
return (
<ul>
{versions.map((version) => (
<li key={version.version_id}>
<Badge>{version.change_type}</Badge>
<span>Version {version.version_number}</span>
<span>{formatDate(version.created_at)}</span>
<UserAvatar userId={version.created_by} />
</li>
))}
</ul>
);
};
```
### Version Comparison Widget
```typescript
const VersionComparison = ({
entityType,
versionIds
}: {
entityType: EntityType;
versionIds: [string, string]
}) => {
const { diff, loading } = useVersionComparison(
entityType,
versionIds[0],
versionIds[1]
);
if (loading) return <Spinner />;
const changedFields = Object.entries(diff || {}).filter(
([_, change]) => change.changed
);
return (
<Card>
<CardHeader>
<CardTitle>{changedFields.length} fields changed</CardTitle>
</CardHeader>
<CardContent>
{changedFields.map(([field, change]) => (
<div key={field} className="grid grid-cols-2 gap-4">
<div>
<Label>{field}</Label>
<pre>{JSON.stringify(change.from, null, 2)}</pre>
</div>
<div>
<Label>{field}</Label>
<pre>{JSON.stringify(change.to, null, 2)}</pre>
</div>
</div>
))}
</CardContent>
</Card>
);
};
```
## Real-Time Updates
The `useEntityVersions` hook automatically subscribes to real-time changes via Supabase:
```typescript
// Automatic subscription in useEntityVersions
useEffect(() => {
const channel = supabase
.channel(`${tableName}:${entityId}`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: tableName,
filter: `${entityType}_id=eq.${entityId}`,
},
() => {
fetchVersions(); // Refetch on change
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [entityType, entityId]);
```
## Type Guards
Use type guards to narrow discriminated unions:
```typescript
function isParkVersion(version: EntityVersion): version is ParkVersion {
return 'park_id' in version;
}
function isRideVersion(version: EntityVersion): version is RideVersion {
return 'ride_id' in version;
}
// Usage
if (isParkVersion(version)) {
console.log(version.park_type); // TypeScript knows this is a park
}
```
## Error Handling
All hooks handle errors gracefully:
```typescript
const { versions, loading } = useEntityVersions('park', parkId);
// Loading state
if (loading) return <Skeleton />;
// Error state (versions will be empty array)
if (versions.length === 0) {
return <EmptyState message="No version history available" />;
}
// Success state
return <VersionList versions={versions} />;
```
## Best Practices
### DO
✅ Use `VersionIndicator` on all entity detail pages
✅ Add history tab to entity pages
✅ Let hooks manage state and subscriptions
✅ Use type guards for discriminated unions
✅ Handle loading and error states
### DON'T
❌ Manually query version tables (use hooks)
❌ Bypass type system with `any`
❌ Forget to cleanup subscriptions
❌ Display sensitive data (created_by IDs) to public
## Performance Optimization
### Pagination
For entities with many versions, implement pagination:
```typescript
const { versions } = useEntityVersions('park', parkId);
const [page, setPage] = useState(1);
const pageSize = 10;
const paginatedVersions = versions.slice(
(page - 1) * pageSize,
page * pageSize
);
```
### Lazy Loading
Only load version details when needed:
```typescript
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
const { fieldHistory, fetchFieldHistory } = useEntityVersions('park', parkId);
const handleVersionClick = (versionId: string) => {
setSelectedVersion(versionId);
fetchFieldHistory(versionId); // Load on demand
};
```
## Testing
### Example Unit Test
```typescript
import { renderHook, waitFor } from '@testing-library/react';
import { useEntityVersions } from '@/hooks/useEntityVersions';
describe('useEntityVersions', () => {
it('fetches versions on mount', async () => {
const { result } = renderHook(() =>
useEntityVersions('park', 'test-park-id')
);
expect(result.current.loading).toBe(true);
await waitFor(() => {
expect(result.current.loading).toBe(false);
expect(result.current.versions.length).toBeGreaterThan(0);
});
});
});
```
## Troubleshooting
See [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) for common issues and solutions.