mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 13:11:12 -05:00
feat: Implement versioning documentation
This commit is contained in:
613
docs/versioning/FRONTEND.md
Normal file
613
docs/versioning/FRONTEND.md
Normal file
@@ -0,0 +1,613 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user