Skip to content

Commit 348b56d

Browse files
committed
fix: loading phrases in record browser
1 parent 3f19a77 commit 348b56d

File tree

4 files changed

+104
-76
lines changed

4 files changed

+104
-76
lines changed

frontend/src/features/admin/components/AdminPanel/useAdminApi.js

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import axios from "axios";
2-
import { useCallback } from "react";
2+
import { useCallback, useMemo } from "react";
33
import { API_ENDPOINTS } from '../../../../shared/constants/constants';
44
import { useDebouncedApiCall } from '../../../../hooks/useDebounce';
55

@@ -9,9 +9,10 @@ let cacheTimestamp = null;
99
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
1010

1111
export function useAdminApi({ token, setRows, setTotalRows, setDashboard, setError, setToken, setIsLogged }) {
12-
const authHeader = token
12+
// Memoize so all downstream useCallbacks only recreate when token actually changes
13+
const authHeader = useMemo(() => token
1314
? { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }
14-
: {};
15+
: {}, [token]);
1516

1617
// Helper function to handle authentication errors
1718
const handleAuthError = useCallback((response) => {
@@ -33,7 +34,7 @@ export function useAdminApi({ token, setRows, setTotalRows, setDashboard, setErr
3334
if (filterCategory) url += `&category=${encodeURIComponent(filterCategory)}`;
3435
if (searchTerm && searchTerm.trim()) url += `&search=${encodeURIComponent(searchTerm.trim())}`;
3536
if (languageSetId) url += `&language_set_id=${languageSetId}`;
36-
37+
3738
const response = await fetch(url, { headers: authHeader });
3839
if (!response.ok) {
3940
if (handleAuthError(response)) {
@@ -47,20 +48,24 @@ export function useAdminApi({ token, setRows, setTotalRows, setDashboard, setErr
4748
return response.json();
4849
}, [authHeader, handleAuthError]);
4950

50-
const {
51-
call: debouncedFetchRows,
52-
isLoading: isFetchingRows,
53-
showRateLimit: showFetchRateLimit
51+
// Stable callbacks — memoized so useDebouncedApiCall doesn't recreate on every render
52+
const onFetchSuccess = useCallback((data) => {
53+
setRows(data.rows || data);
54+
setTotalRows(data.total || data.length || 0);
55+
setError("");
56+
}, [setRows, setTotalRows, setError]);
57+
58+
const onFetchError = useCallback((err) => {
59+
setError(err.message);
60+
}, [setError]);
61+
62+
const {
63+
call: debouncedFetchRows,
64+
isLoading: isFetchingRows,
65+
showRateLimit: showFetchRateLimit
5466
} = useDebouncedApiCall(fetchRowsApiCall, 750, {
55-
onSuccess: (data) => {
56-
setRows(data.rows || data);
57-
setTotalRows(data.total || data.length || 0);
58-
setDashboard(false);
59-
setError("");
60-
},
61-
onError: (err) => {
62-
setError(err.message);
63-
}
67+
onSuccess: onFetchSuccess,
68+
onError: onFetchError,
6469
});
6570

6671
const fetchRows = useCallback((offset, limit, filterCategory, searchTerm, languageSetId) => {
@@ -234,7 +239,7 @@ export function useAdminApi({ token, setRows, setTotalRows, setDashboard, setErr
234239
const response = await fetch(`${API_ENDPOINTS.ADMIN_BATCH_ADD_CATEGORY}?language_set_id=${languageSetId}`, {
235240
method: 'POST',
236241
headers: authHeader,
237-
body: JSON.stringify({
242+
body: JSON.stringify({
238243
row_ids: rowIds,
239244
category: category.trim()
240245
})
@@ -270,7 +275,7 @@ export function useAdminApi({ token, setRows, setTotalRows, setDashboard, setErr
270275
const response = await fetch(`${API_ENDPOINTS.ADMIN_BATCH_REMOVE_CATEGORY}?language_set_id=${languageSetId}`, {
271276
method: 'POST',
272277
headers: authHeader,
273-
body: JSON.stringify({
278+
body: JSON.stringify({
274279
row_ids: rowIds,
275280
category: category.trim()
276281
})
@@ -312,13 +317,13 @@ export function useAdminApi({ token, setRows, setTotalRows, setDashboard, setErr
312317
return response.json();
313318
}, [authHeader, handleAuthError]);
314319

315-
return {
316-
fetchRows,
317-
handleLogin,
318-
handleSave,
319-
handleExportTxt,
320-
clearDb,
321-
handleDelete,
320+
return {
321+
fetchRows,
322+
handleLogin,
323+
handleSave,
324+
handleExportTxt,
325+
clearDb,
326+
handleDelete,
322327
fetchCategories,
323328
invalidateCategoriesCache,
324329
handleBatchDelete,

frontend/src/features/admin/components/BrowseRecords/BrowseRecordsContainer.jsx

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export default function BrowseRecordsContainer({
3434
const [offsetInput, setOffsetInput] = useState(0);
3535
const [searchTerm, setSearchTerm] = useState('');
3636
const [filterCategory, setFilterCategory] = useState('');
37-
const [loading, setLoading] = useState(false);
3837

3938
// Row Editing State
4039
const [newRow, setNewRow] = useState(null);
@@ -52,15 +51,16 @@ export default function BrowseRecordsContainer({
5251

5352
// API Hook
5453
const {
55-
fetchRows: originalFetchRows,
54+
fetchRows,
5655
handleSave,
5756
handleExportTxt,
5857
clearDb,
5958
handleDelete: deleteRowApi,
6059
handleBatchDelete,
6160
handleBatchAddCategory,
6261
handleBatchRemoveCategory,
63-
invalidateCategoriesCache
62+
invalidateCategoriesCache,
63+
isFetchingRows,
6464
} = useAdminApi({
6565
token,
6666
setRows,
@@ -71,20 +71,6 @@ export default function BrowseRecordsContainer({
7171
setIsLogged
7272
});
7373

74-
// Wrap fetchRows to handle loading state
75-
const fetchRows = useCallback((...args) => {
76-
setLoading(true);
77-
originalFetchRows(...args);
78-
}, [originalFetchRows]);
79-
80-
// Clear loading state when rows change
81-
useEffect(() => {
82-
if (loading) {
83-
const timer = setTimeout(() => setLoading(false), 100);
84-
return () => clearTimeout(timer);
85-
}
86-
}, [rows, loading]);
87-
8874
// Initial Fetch & Updates
8975
useEffect(() => {
9076
if (selectedLanguageSetId) {
@@ -303,7 +289,7 @@ export default function BrowseRecordsContainer({
303289
<BrowseRecordsView
304290
rows={rows}
305291
totalRows={totalRows}
306-
loading={loading}
292+
loading={isFetchingRows}
307293
error={error}
308294

309295
offset={offset}

frontend/src/features/admin/components/BrowseRecords/BrowseRecordsView.jsx

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
useTheme,
1717
Snackbar,
1818
Alert,
19-
IconButton
19+
IconButton,
20+
CircularProgress,
21+
Fade,
2022
} from '@mui/material';
2123
import TranslateIcon from '@mui/icons-material/Translate';
2224
import CategoryIcon from '@mui/icons-material/Category';
@@ -343,28 +345,50 @@ const BrowseRecordsView = ({
343345
)
344346
}
345347
{/* Data Table */}
346-
<AdminTable
347-
rows={rows}
348-
onSaveRow={handleInlineSave}
349-
onDeleteRow={handleInlineDelete}
350-
totalRows={totalRows}
351-
searchTerm={searchTerm}
352-
onSearchChange={handleSearchChange}
353-
isLoading={loading}
354-
batchMode={batchMode}
355-
selectedRows={selectedRows}
356-
onRowSelectionChange={handleRowSelectionChange}
357-
onBatchModeToggle={handleBatchModeToggle}
358-
onAddNewRow={handleStartAddRow}
359-
newRow={newRow}
360-
onNewRowChange={handleNewRowFieldChange}
361-
onCancelNewRow={handleCancelNewRow}
362-
onConfirmNewRow={handleConfirmNewRow}
363-
isSavingNewRow={isSavingNewRow}
364-
canAddNewRow={canAddNewRow}
365-
categoryOptions={categories}
366-
compactMode={isLayoutCompact}
367-
/>
348+
<Box sx={{ position: 'relative' }}>
349+
<AdminTable
350+
rows={rows}
351+
onSaveRow={handleInlineSave}
352+
onDeleteRow={handleInlineDelete}
353+
totalRows={totalRows}
354+
searchTerm={searchTerm}
355+
onSearchChange={handleSearchChange}
356+
isLoading={loading}
357+
batchMode={batchMode}
358+
selectedRows={selectedRows}
359+
onRowSelectionChange={handleRowSelectionChange}
360+
onBatchModeToggle={handleBatchModeToggle}
361+
onAddNewRow={handleStartAddRow}
362+
newRow={newRow}
363+
onNewRowChange={handleNewRowFieldChange}
364+
onCancelNewRow={handleCancelNewRow}
365+
onConfirmNewRow={handleConfirmNewRow}
366+
isSavingNewRow={isSavingNewRow}
367+
canAddNewRow={canAddNewRow}
368+
categoryOptions={categories}
369+
compactMode={isLayoutCompact}
370+
/>
371+
<Fade in={loading} unmountOnExit>
372+
<Box
373+
sx={{
374+
position: 'absolute',
375+
inset: 0,
376+
display: 'flex',
377+
alignItems: 'center',
378+
justifyContent: 'center',
379+
backgroundColor: (theme) =>
380+
theme.palette.mode === 'dark'
381+
? 'rgba(0, 0, 0, 0.45)'
382+
: 'rgba(255, 255, 255, 0.65)',
383+
backdropFilter: 'blur(2px)',
384+
borderRadius: 1,
385+
zIndex: 10,
386+
}}
387+
>
388+
<CircularProgress size={48} />
389+
</Box>
390+
</Fade>
391+
</Box>
368392
{/* Pagination */}
369393
<Box sx={{ mt: 3 }}>
370394
<PaginationControls

frontend/src/hooks/useDebounce.js

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const useDebounce = (callback, delay, deps = []) => {
1313

1414
const debouncedFn = useCallback((...args) => {
1515
setIsDebouncing(true);
16-
16+
1717
if (timeoutRef.current) {
1818
clearTimeout(timeoutRef.current);
1919
}
@@ -75,36 +75,49 @@ export const useDebouncedApiCall = (apiCall, delay = 750, options = {}) => {
7575
const [isLoading, setIsLoading] = useState(false);
7676
const [error, setError] = useState(null);
7777
const [showRateLimit, setShowRateLimit] = useState(false);
78-
const { onSuccess, onError } = options;
78+
79+
// Use refs for callbacks and loading flag so they don't need to be in
80+
// dependency arrays — prevents cascade re-creation on every state change
81+
const isLoadingRef = useRef(false);
82+
const onSuccessRef = useRef(options.onSuccess);
83+
const onErrorRef = useRef(options.onError);
84+
const apiCallRef = useRef(apiCall);
85+
86+
// Keep refs up-to-date without triggering re-renders
87+
onSuccessRef.current = options.onSuccess;
88+
onErrorRef.current = options.onError;
89+
apiCallRef.current = apiCall;
7990

8091
const debouncedCall = useCallback(async (...args) => {
81-
if (isLoading) {
92+
if (isLoadingRef.current) {
8293
setShowRateLimit(true);
8394
setTimeout(() => setShowRateLimit(false), 3000);
8495
return;
8596
}
8697

98+
isLoadingRef.current = true;
8799
setIsLoading(true);
88100
setError(null);
89101

90102
try {
91-
const result = await apiCall(...args);
92-
if (onSuccess) onSuccess(result);
103+
const result = await apiCallRef.current(...args);
104+
if (onSuccessRef.current) onSuccessRef.current(result);
93105
return result;
94106
} catch (err) {
95107
setError(err);
96108
if (err.response?.status === 429) {
97109
setShowRateLimit(true);
98110
setTimeout(() => setShowRateLimit(false), 4000);
99111
}
100-
if (onError) onError(err);
112+
if (onErrorRef.current) onErrorRef.current(err);
101113
throw err;
102114
} finally {
115+
isLoadingRef.current = false;
103116
setIsLoading(false);
104117
}
105-
}, [apiCall, isLoading, onSuccess, onError]);
118+
}, []); // stable — uses refs for all external values
106119

107-
const { debouncedFn: call } = useDebounce(debouncedCall, delay);
120+
const { debouncedFn: call, isDebouncing } = useDebounce(debouncedCall, delay);
108121

109-
return { call, isLoading, error, showRateLimit };
122+
return { call, isLoading: isLoading || isDebouncing, error, showRateLimit };
110123
};

0 commit comments

Comments
 (0)