a4d0f0c35d
Check Cross-Plugin Imports / check (push) Has been cancelled
- D-GEN: RestoreRegistry mit RestoreConfig (model_class, restore_permission, excluded_fields, special_handler) - D-HOOK: history_hooks.py mit register_history_hooks() für after_create/update/delete - D-CORE: Company create+update record_history in companies.py - D-PLUG: Task/Calendar/DMS record_history in services/routes - D-SOFT: Alle registrierten Entitäten haben deleted_at + un-delete via Registry - D-MAIL: Mail special_handler (IMAP Trash-Move, Folder-Verify) + record_history in delete/move - D-TRASH: GET /entity-history/trash (filterbar, paginiert) + Frontend Trash.tsx - D-TOAST: UndoToast.tsx (5s Auto-Dismiss, useUndoToast Hook) - D-HIST-UI: HistoryPanel.tsx (Timeline, Diff-View, Restore-Button) - D-BULK: POST /entity-history/bulk-restore mit partial_success Semantik - D-RET: POST /entity-history/retention/archive (GDPR hard-delete >90 Tage) - D-TEST: 26 Tests in test_restore_registry.py, alle grün - D-DOC: test-strategy.md + security_kernel.md aktualisiert Backend: 10 Dateien, Frontend: 7 Dateien, Tests: 1 Datei, Docs: 3 Dateien 26/26 Tests passed, TSC 0 errors, App import 492 routes
120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
/**
|
|
* Entity History hooks — undo/restore/trash functionality.
|
|
*/
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { apiGet, apiPost, apiClient } from './client';
|
|
|
|
export interface EntityHistoryEntry {
|
|
id: string;
|
|
entity_type: string;
|
|
entity_id: string;
|
|
action: 'create' | 'update' | 'delete';
|
|
snapshot_before: Record<string, unknown> | null;
|
|
snapshot_after: Record<string, unknown> | null;
|
|
changes: Record<string, { old: unknown; new: unknown }> | null;
|
|
user_id: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface EntityHistoryList {
|
|
items: EntityHistoryEntry[];
|
|
total: number;
|
|
}
|
|
|
|
export interface TrashItem {
|
|
history_id: string;
|
|
entity_type: string;
|
|
entity_id: string;
|
|
snapshot_before: Record<string, unknown> | null;
|
|
deleted_at: string | null;
|
|
user_id: string | null;
|
|
}
|
|
|
|
export interface TrashListResponse {
|
|
items: TrashItem[];
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
}
|
|
|
|
export interface BulkRestoreResultItem {
|
|
history_id: string;
|
|
success: boolean;
|
|
entity_type: string | null;
|
|
entity_id: string | null;
|
|
error: string | null;
|
|
}
|
|
|
|
export interface BulkRestoreResponse {
|
|
total: number;
|
|
succeeded: number;
|
|
failed: number;
|
|
results: BulkRestoreResultItem[];
|
|
partial_success: boolean;
|
|
}
|
|
|
|
export function useEntityHistory(entityType?: string, entityId?: string, limit = 50) {
|
|
return useQuery({
|
|
queryKey: ['entityHistory', entityType, entityId, limit],
|
|
queryFn: () =>
|
|
apiGet<EntityHistoryList>(`/entity-history/${entityType}/${entityId}?limit=${limit}`),
|
|
enabled: !!entityType && !!entityId,
|
|
});
|
|
}
|
|
|
|
export function useTrashList(entityType?: string, limit = 50, offset = 0) {
|
|
const params = new URLSearchParams();
|
|
params.set('limit', String(limit));
|
|
params.set('offset', String(offset));
|
|
if (entityType) {
|
|
params.set('entity_type', entityType);
|
|
}
|
|
return useQuery({
|
|
queryKey: ['trash', entityType ?? 'all', limit, offset],
|
|
queryFn: () => apiGet<TrashListResponse>(`/entity-history/trash?${params.toString()}`),
|
|
});
|
|
}
|
|
|
|
export function useRestoreFromHistory() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (historyId: string) =>
|
|
apiClient.post('/entity-history/restore', { history_id: historyId }).then(r => r.data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['entityHistory'] });
|
|
queryClient.invalidateQueries({ queryKey: ['trash'] });
|
|
queryClient.invalidateQueries({ queryKey: ['contacts'] });
|
|
queryClient.invalidateQueries({ queryKey: ['contact'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useBulkRestore() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (historyIds: string[]) =>
|
|
apiPost<BulkRestoreResponse>('/entity-history/bulk-restore', { history_ids: historyIds }),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['entityHistory'] });
|
|
queryClient.invalidateQueries({ queryKey: ['trash'] });
|
|
queryClient.invalidateQueries({ queryKey: ['contacts'] });
|
|
queryClient.invalidateQueries({ queryKey: ['contact'] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUndoLastAction() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ entityType, entityId }: { entityType: string; entityId: string }) =>
|
|
apiPost(`/entity-history/undo/${entityType}/${entityId}`, {}),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ['entityHistory'] });
|
|
queryClient.invalidateQueries({ queryKey: ['trash'] });
|
|
queryClient.invalidateQueries({ queryKey: ['contacts'] });
|
|
queryClient.invalidateQueries({ queryKey: ['contact'] });
|
|
},
|
|
});
|
|
}
|