mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 09:31:13 -05:00
614 lines
13 KiB
Markdown
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.
|