mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
fix(admin-bulk-actions): plug stale-closure bug in columns useMemo
Header checkbox for 'select all subscriptions' read allVisibleSubscriptionIds, subscriptionSelection, isMultiTariff, toggleAllSubscriptions from a stale closure because those values were declared AFTER the columns useMemo. The warned-about deps were absent, so the header only refreshed by accident when expandedRows or getFilteredSubs changed. Relocate filteredUsers / allVisibleSubscriptionIds / toggleAllSubscriptions above the columns block, list them as deps. Header now reflects current selection without piggybacking on unrelated re-renders. Same commit clears the matching AdminInfoPageEditor warning with an explicit eslint-disable-line comment — the activeLocale omission was already intentional (initial-content lock), just not silenced.
This commit is contained in:
@@ -1778,6 +1778,66 @@ export default function AdminBulkActions() {
|
|||||||
[tariffFilter, trialOnly],
|
[tariffFilter, trialOnly],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Selection-related derivations live here so the columns useMemo (below)
|
||||||
|
// can list them as deps without forward-reference warnings.
|
||||||
|
// When multiple tariffs are selected, filter users client-side
|
||||||
|
// (server only supports single tariff_id filter)
|
||||||
|
const filteredUsers = useMemo(() => {
|
||||||
|
let result = users;
|
||||||
|
if (trialOnly) {
|
||||||
|
result = result.filter(
|
||||||
|
(u) => u.subscription_is_trial || (u.subscriptions ?? []).some((s) => s.status === 'trial'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, [users, trialOnly]);
|
||||||
|
|
||||||
|
const allVisibleSubscriptionIds = useMemo(() => {
|
||||||
|
const ids: number[] = [];
|
||||||
|
for (const user of filteredUsers) {
|
||||||
|
const subs = user.subscriptions ?? [];
|
||||||
|
const filtered = getFilteredSubs(subs);
|
||||||
|
for (const sub of filtered) {
|
||||||
|
ids.push(sub.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}, [filteredUsers, getFilteredSubs]);
|
||||||
|
|
||||||
|
const toggleAllSubscriptions = useCallback(() => {
|
||||||
|
const allSelected =
|
||||||
|
allVisibleSubscriptionIds.length > 0 &&
|
||||||
|
allVisibleSubscriptionIds.every((id) => subscriptionSelection[id]);
|
||||||
|
|
||||||
|
if (allSelected) {
|
||||||
|
const next: Record<number, boolean> = {};
|
||||||
|
for (const key of Object.keys(subscriptionSelection)) {
|
||||||
|
const id = Number(key);
|
||||||
|
if (!allVisibleSubscriptionIds.includes(id)) {
|
||||||
|
next[id] = subscriptionSelection[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setSubscriptionSelection(next);
|
||||||
|
} else {
|
||||||
|
const next = { ...subscriptionSelection };
|
||||||
|
for (const id of allVisibleSubscriptionIds) {
|
||||||
|
next[id] = true;
|
||||||
|
}
|
||||||
|
setSubscriptionSelection(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-expand rows that have filtered subs
|
||||||
|
const expanded: Record<number, boolean> = { ...expandedRows };
|
||||||
|
for (const user of users) {
|
||||||
|
const subs = user.subscriptions ?? [];
|
||||||
|
const filtered = getFilteredSubs(subs);
|
||||||
|
if (filtered.length > 1) {
|
||||||
|
expanded[user.id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setExpandedRows(expanded);
|
||||||
|
}, [allVisibleSubscriptionIds, subscriptionSelection, expandedRows, users, getFilteredSubs]);
|
||||||
|
|
||||||
const handleOpenAction = (type: BulkActionType) => {
|
const handleOpenAction = (type: BulkActionType) => {
|
||||||
setModal({ open: true, action: type, loading: false, result: null, progress: null });
|
setModal({ open: true, action: type, loading: false, result: null, progress: null });
|
||||||
};
|
};
|
||||||
@@ -2125,72 +2185,19 @@ export default function AdminBulkActions() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[t, formatWithCurrency, expandedRows, toggleExpandRow, getFilteredSubs],
|
[
|
||||||
|
t,
|
||||||
|
formatWithCurrency,
|
||||||
|
expandedRows,
|
||||||
|
toggleExpandRow,
|
||||||
|
getFilteredSubs,
|
||||||
|
allVisibleSubscriptionIds,
|
||||||
|
isMultiTariff,
|
||||||
|
subscriptionSelection,
|
||||||
|
toggleAllSubscriptions,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
// When multiple tariffs are selected, filter users client-side
|
|
||||||
// (server only supports single tariff_id filter)
|
|
||||||
const filteredUsers = useMemo(() => {
|
|
||||||
let result = users;
|
|
||||||
|
|
||||||
// Trial-only filter: show users with trial subscription
|
|
||||||
if (trialOnly) {
|
|
||||||
result = result.filter(
|
|
||||||
(u) => u.subscription_is_trial || (u.subscriptions ?? []).some((s) => s.status === 'trial'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, [users, trialOnly]);
|
|
||||||
|
|
||||||
const allVisibleSubscriptionIds = useMemo(() => {
|
|
||||||
const ids: number[] = [];
|
|
||||||
for (const user of filteredUsers) {
|
|
||||||
const subs = user.subscriptions ?? [];
|
|
||||||
const filtered = getFilteredSubs(subs);
|
|
||||||
for (const sub of filtered) {
|
|
||||||
ids.push(sub.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}, [filteredUsers, getFilteredSubs]);
|
|
||||||
|
|
||||||
const toggleAllSubscriptions = useCallback(() => {
|
|
||||||
const allSelected =
|
|
||||||
allVisibleSubscriptionIds.length > 0 &&
|
|
||||||
allVisibleSubscriptionIds.every((id) => subscriptionSelection[id]);
|
|
||||||
|
|
||||||
if (allSelected) {
|
|
||||||
// Deselect all
|
|
||||||
const next: Record<number, boolean> = {};
|
|
||||||
for (const key of Object.keys(subscriptionSelection)) {
|
|
||||||
const id = Number(key);
|
|
||||||
if (!allVisibleSubscriptionIds.includes(id)) {
|
|
||||||
next[id] = subscriptionSelection[id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setSubscriptionSelection(next);
|
|
||||||
} else {
|
|
||||||
// Select all visible
|
|
||||||
const next = { ...subscriptionSelection };
|
|
||||||
for (const id of allVisibleSubscriptionIds) {
|
|
||||||
next[id] = true;
|
|
||||||
}
|
|
||||||
setSubscriptionSelection(next);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-expand rows that have filtered subs
|
|
||||||
const expanded: Record<number, boolean> = { ...expandedRows };
|
|
||||||
for (const user of users) {
|
|
||||||
const subs = user.subscriptions ?? [];
|
|
||||||
const filtered = getFilteredSubs(subs);
|
|
||||||
if (filtered.length > 1) {
|
|
||||||
expanded[user.id] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setExpandedRows(expanded);
|
|
||||||
}, [allVisibleSubscriptionIds, subscriptionSelection, expandedRows, users, getFilteredSubs]);
|
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: filteredUsers,
|
data: filteredUsers,
|
||||||
columns,
|
columns,
|
||||||
|
|||||||
@@ -981,7 +981,10 @@ export default function AdminInfoPageEditor() {
|
|||||||
const initialContent = pageData.content[activeLocale] ?? pageData.content['ru'] ?? '';
|
const initialContent = pageData.content[activeLocale] ?? pageData.content['ru'] ?? '';
|
||||||
editor.commands.setContent(initialContent);
|
editor.commands.setContent(initialContent);
|
||||||
editorPopulated.current = true;
|
editorPopulated.current = true;
|
||||||
}, [pageData, editor]); // activeLocale intentionally omitted
|
// activeLocale intentionally omitted — initial content is locked at editor
|
||||||
|
// mount; subsequent locale changes are routed through switchLocale().
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [pageData, editor]);
|
||||||
|
|
||||||
// Auto-generate slug from Russian title
|
// Auto-generate slug from Russian title
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user