bugfix(PRM-01)
This commit is contained in:
parent
dc174f570c
commit
5dd4741a0f
3 changed files with 148 additions and 22 deletions
|
|
@ -118,15 +118,11 @@ export function DeleteActionButton<T = any>({
|
|||
const success = await handleDelete(itemId);
|
||||
|
||||
if (success) {
|
||||
// If we used optimistic removal, don't refetch immediately
|
||||
// The item is already removed from UI, and refetch might bring it back
|
||||
if (removeOptimistically) {
|
||||
// Only refetch if there was an error or if we need to sync other changes
|
||||
// For now, we trust the optimistic removal worked
|
||||
} else {
|
||||
// No optimistic removal, refetch immediately
|
||||
refetch();
|
||||
}
|
||||
// Always refetch after a successful delete. The server has actually
|
||||
// removed the row, so fresh data won't bring it back — and this is
|
||||
// what re-syncs pagination.totalItems (and clears any optimistic
|
||||
// hidden-row state maintained by FormGeneratorTable).
|
||||
refetch();
|
||||
onSuccess?.(row);
|
||||
} else {
|
||||
// Refetch to restore the item in case of failure
|
||||
|
|
|
|||
|
|
@ -339,7 +339,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
|||
onRefresh,
|
||||
className = '',
|
||||
getRowDataAttributes,
|
||||
hookData,
|
||||
hookData: hookDataProp,
|
||||
emptyMessage,
|
||||
apiEndpoint,
|
||||
groupBy,
|
||||
|
|
@ -356,6 +356,108 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
|||
// When only onDelete is provided, use it for multi-delete too so Delete stays visible with 2+ selected
|
||||
const onDeleteMultiple = onDeleteMultipleProp ?? (onDelete ? (rows: T[]) => rows.forEach((r) => onDelete(r)) : undefined);
|
||||
const currentLanguage = useMemo(() => contextLanguage || 'en', [contextLanguage]);
|
||||
|
||||
// ── Optimistic row hiding + adjusted header count ───────────────────────
|
||||
// We synthesize `removeOptimistically` at the FormGenerator layer so every
|
||||
// page gets instant delete feedback (row hidden + "N Einträge" decremented)
|
||||
// regardless of whether the underlying hook implements it.
|
||||
//
|
||||
// 1. `optimisticallyDeletedIds` tracks which rows are currently hidden.
|
||||
// 2. `displayData` below is filtered to exclude those IDs.
|
||||
// 3. `pagination.totalItems`/`totalPages` are reduced by the set size.
|
||||
// 4. The set is cleared whenever a fresh `pagination` reference arrives
|
||||
// from the hook (i.e. after a successful refetch establishes server
|
||||
// truth).
|
||||
const [optimisticallyDeletedIds, setOptimisticallyDeletedIds] = useState<Set<string>>(() => new Set());
|
||||
const previousPaginationRef = useRef<any>(hookDataProp?.pagination);
|
||||
useEffect(() => {
|
||||
if (hookDataProp?.pagination !== previousPaginationRef.current) {
|
||||
previousPaginationRef.current = hookDataProp?.pagination;
|
||||
if (optimisticallyDeletedIds.size > 0) setOptimisticallyDeletedIds(new Set());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [hookDataProp?.pagination]);
|
||||
|
||||
// Snapshot of the current table state so a "naked" refetch() call (e.g.
|
||||
// from DeleteActionButton) still includes page/pageSize/filters/sort/search.
|
||||
// Without this, the hook issues a no-params GET which, for paginated
|
||||
// endpoints, returns `pagination: null` and leaves `totalItems` stale.
|
||||
const tableStateRef = useRef({
|
||||
page: 1,
|
||||
pageSize: pageSize,
|
||||
search: '',
|
||||
filters: {} as Record<string, any>,
|
||||
sort: [] as Array<{ key: string; direction: 'asc' | 'desc' }>,
|
||||
});
|
||||
|
||||
const hookData = useMemo(() => {
|
||||
if (!hookDataProp) return hookDataProp;
|
||||
|
||||
const origRemove =
|
||||
hookDataProp.removeOptimistically || hookDataProp.removeFileOptimistically;
|
||||
const wrappedRemove = (id: string) => {
|
||||
if (origRemove) origRemove(id);
|
||||
setOptimisticallyDeletedIds(prev => {
|
||||
const next = new Set(prev);
|
||||
next.add(String(id));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const wrappedRefetch = hookDataProp.refetch
|
||||
? async (params?: any) => {
|
||||
const hasPaginationInfo =
|
||||
params && (params.page !== undefined || params.pageSize !== undefined);
|
||||
if (hasPaginationInfo) {
|
||||
return await hookDataProp.refetch(params);
|
||||
}
|
||||
const s = tableStateRef.current;
|
||||
const finalParams: any = {
|
||||
page: s.page,
|
||||
pageSize: s.pageSize,
|
||||
...(params || {}),
|
||||
};
|
||||
if (s.search && s.search.trim()) finalParams.search = s.search.trim();
|
||||
const activeFilters: Record<string, any> = {};
|
||||
Object.entries(s.filters).forEach(([k, v]) => {
|
||||
if (v !== undefined && v !== '') activeFilters[k] = v;
|
||||
});
|
||||
if (Object.keys(activeFilters).length) finalParams.filters = activeFilters;
|
||||
if (s.sort.length) {
|
||||
finalParams.sort = s.sort.map(sc => ({ field: sc.key, direction: sc.direction }));
|
||||
}
|
||||
return await hookDataProp.refetch(finalParams);
|
||||
}
|
||||
: hookDataProp.refetch;
|
||||
|
||||
const origPagination = hookDataProp.pagination;
|
||||
const hasNumericTotal =
|
||||
origPagination && typeof origPagination.totalItems === 'number';
|
||||
const delta = -optimisticallyDeletedIds.size;
|
||||
const adjustedTotalItems = hasNumericTotal
|
||||
? Math.max(0, origPagination.totalItems + delta)
|
||||
: undefined;
|
||||
const adjustedTotalPages =
|
||||
hasNumericTotal && origPagination.pageSize
|
||||
? Math.max(1, Math.ceil((adjustedTotalItems ?? 0) / origPagination.pageSize))
|
||||
: origPagination?.totalPages;
|
||||
|
||||
const adjustedPagination = hasNumericTotal
|
||||
? {
|
||||
...origPagination,
|
||||
totalItems: adjustedTotalItems,
|
||||
totalPages: adjustedTotalPages,
|
||||
}
|
||||
: origPagination;
|
||||
|
||||
return {
|
||||
...hookDataProp,
|
||||
removeOptimistically: wrappedRemove,
|
||||
removeFileOptimistically: wrappedRemove,
|
||||
refetch: wrappedRefetch,
|
||||
pagination: adjustedPagination,
|
||||
};
|
||||
}, [hookDataProp, optimisticallyDeletedIds]);
|
||||
// Use provided columns from Pydantic attribute definitions
|
||||
// NO AUTO-DETECTION - columns must come from backend attribute definitions
|
||||
// Use a ref to cache columns so they persist across data changes (e.g., when filtering)
|
||||
|
|
@ -441,6 +543,18 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
|||
return () => clearTimeout(timer);
|
||||
}, [searchTerm]);
|
||||
|
||||
// Keep tableStateRef in sync so a "naked" refetch() call (from e.g.
|
||||
// DeleteActionButton) can inject the current page/pageSize/filters/sort.
|
||||
useEffect(() => {
|
||||
tableStateRef.current = {
|
||||
page: currentPage,
|
||||
pageSize: currentPageSize,
|
||||
search: debouncedSearchTerm,
|
||||
filters,
|
||||
sort: sortConfigs,
|
||||
};
|
||||
}, [currentPage, currentPageSize, debouncedSearchTerm, filters, sortConfigs]);
|
||||
|
||||
// Call backend when filters/search/sort/pagination change
|
||||
useEffect(() => {
|
||||
if (!supportsBackendPagination || !hookData?.refetch) return;
|
||||
|
|
@ -740,9 +854,13 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
|||
return value.length > 8 ? `${value.substring(0, 8)}...` : value;
|
||||
}, [fkCache]);
|
||||
|
||||
// Data is already filtered, sorted, and paginated by the backend
|
||||
// No client-side processing needed
|
||||
const displayData = data;
|
||||
// Data is already filtered, sorted, and paginated by the backend.
|
||||
// Client-side only filters out rows that were just optimistically deleted
|
||||
// so the UI updates instantly before the server's next refetch response.
|
||||
const displayData = useMemo(() => {
|
||||
if (optimisticallyDeletedIds.size === 0) return data;
|
||||
return data.filter(row => !optimisticallyDeletedIds.has(String(row?.[_idField])));
|
||||
}, [data, optimisticallyDeletedIds, _idField]);
|
||||
|
||||
// Grouping: Group data by groupBy field if specified
|
||||
const groupedData = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* Follows the pattern established in AdminUsersPage/WorkflowsPage.
|
||||
*/
|
||||
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { usePrompts, usePromptOperations } from '../../hooks/usePrompts';
|
||||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||||
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
|
||||
|
|
@ -49,10 +49,22 @@ export const PromptsPage: React.FC = () => {
|
|||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [editingPrompt, setEditingPrompt] = useState<Prompt | null>(null);
|
||||
|
||||
// ── Table refetch wrapper (stable signature used by FormGeneratorTable) ──
|
||||
const _tableRefetch = useCallback(async (params?: any) => {
|
||||
await refetch(params);
|
||||
}, [refetch]);
|
||||
|
||||
// ── Refresh-All for the header "Aktualisieren" button ────────────────────
|
||||
// Forces a paginated request so the cache key matches what the table uses
|
||||
// internally. This guarantees fresh (non-cached) data is pulled in.
|
||||
const _refreshAll = useCallback(async () => {
|
||||
await _tableRefetch({ page: 1, pageSize: 25 });
|
||||
}, [_tableRefetch]);
|
||||
|
||||
// Initial fetch
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, []);
|
||||
_tableRefetch({ page: 1, pageSize: 25 });
|
||||
}, [_tableRefetch]);
|
||||
|
||||
// Generate columns from attributes - exclude ID fields from display
|
||||
const columns = useMemo(() => {
|
||||
|
|
@ -114,7 +126,7 @@ export const PromptsPage: React.FC = () => {
|
|||
});
|
||||
if (result?.success) {
|
||||
setShowCreateModal(false);
|
||||
refetch();
|
||||
_refreshAll();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -127,7 +139,7 @@ export const PromptsPage: React.FC = () => {
|
|||
});
|
||||
if (result.success) {
|
||||
setEditingPrompt(null);
|
||||
refetch();
|
||||
_refreshAll();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -135,7 +147,7 @@ export const PromptsPage: React.FC = () => {
|
|||
const handleDelete = async (prompt: Prompt) => {
|
||||
const success = await handlePromptDelete(prompt.id);
|
||||
if (success) {
|
||||
refetch();
|
||||
_refreshAll();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -152,7 +164,7 @@ export const PromptsPage: React.FC = () => {
|
|||
<div className={styles.errorContainer}>
|
||||
<span className={styles.errorIcon}>⚠️</span>
|
||||
<p className={styles.errorMessage}>{t('Fehler beim Laden der Prompts: {detail}', { detail: String(error) })}</p>
|
||||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
||||
<button className={styles.secondaryButton} onClick={() => _refreshAll()}>
|
||||
<FaSync /> {t('Erneut versuchen')}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -170,7 +182,7 @@ export const PromptsPage: React.FC = () => {
|
|||
<div className={styles.headerActions}>
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={() => refetch()}
|
||||
onClick={() => _refreshAll()}
|
||||
disabled={loading}
|
||||
>
|
||||
<FaSync className={loading ? 'spinning' : ''} /> {t('Aktualisieren')}
|
||||
|
|
@ -217,7 +229,7 @@ export const PromptsPage: React.FC = () => {
|
|||
]}
|
||||
onDelete={handleDelete}
|
||||
hookData={{
|
||||
refetch,
|
||||
refetch: _tableRefetch,
|
||||
permissions,
|
||||
pagination,
|
||||
handleDelete: handlePromptDelete,
|
||||
|
|
|
|||
Loading…
Reference in a new issue