fix(mod queue): reset board filter on navigation (#1166)

This commit is contained in:
Tommaso Casaburi
2026-06-09 16:06:13 +07:00
committed by GitHub
parent 2e58961cf1
commit a3ff6b2e33
4 changed files with 114 additions and 40 deletions
@@ -95,9 +95,9 @@ describe('persisted extra stores', () => {
alertThresholdUnit: 'hours',
dismissedCommentCids: [],
queuedCommentHistory: [],
selectedBoardFilter: 'music.eth',
viewMode: 'feed',
});
expect(useModQueueStore.getState()).not.toHaveProperty('selectedBoardFilter');
expect(useModQueueStore.getState().getAlertThresholdSeconds()).toBe(10_800);
useModQueueStore.getState().setAlertThreshold(15, 'minutes');
@@ -105,7 +105,6 @@ describe('persisted extra stores', () => {
useModQueueStore.getState().dismissCommentFromQueue('approved-cid');
useModQueueStore.getState().rememberCommentsInQueue([{ cid: 'approved-cid', approved: true, content: 'approved body' }]);
useModQueueStore.getState().rememberCommentsInQueue([{ cid: 'rejected-cid', approved: false, content: 'rejected body' }]);
useModQueueStore.getState().setSelectedBoardFilter('tech.eth');
useModQueueStore.getState().setViewMode('compact');
expect(useModQueueStore.getState()).toMatchObject({
@@ -116,9 +115,9 @@ describe('persisted extra stores', () => {
{ cid: 'rejected-cid', approved: false, content: 'rejected body' },
{ cid: 'approved-cid', approved: true, content: 'approved body' },
],
selectedBoardFilter: 'tech.eth',
viewMode: 'compact',
});
expect(localStorage.getItem('mod-queue-storage')).not.toContain('selectedBoardFilter');
expect(useModQueueStore.getState().getAlertThresholdSeconds()).toBe(900);
});
+2 -12
View File
@@ -11,12 +11,10 @@ interface ModQueueState {
alertThresholdUnit: AlertThresholdUnit;
dismissedCommentCids: string[];
queuedCommentHistory: QueuedCommentSnapshot[];
selectedBoardFilter: string | null;
viewMode: ModQueueViewMode;
dismissCommentFromQueue: (cid: string) => void;
rememberCommentsInQueue: (comments: QueuedCommentSnapshot[]) => void;
setAlertThreshold: (value: number, unit: AlertThresholdUnit) => void;
setSelectedBoardFilter: (boardAddress: string | null) => void;
setViewMode: (viewMode: ModQueueViewMode) => void;
// Helper to get threshold in seconds for calculations
getAlertThresholdSeconds: () => number;
@@ -34,10 +32,7 @@ interface OldPersistedState {
}
// Type for persisted data (without methods)
type PersistedModQueueData = Pick<
ModQueueState,
'alertThresholdValue' | 'alertThresholdUnit' | 'dismissedCommentCids' | 'queuedCommentHistory' | 'selectedBoardFilter' | 'viewMode'
>;
type PersistedModQueueData = Pick<ModQueueState, 'alertThresholdValue' | 'alertThresholdUnit' | 'dismissedCommentCids' | 'queuedCommentHistory' | 'viewMode'>;
const useModQueueStore = create<ModQueueState>()(
persist(
@@ -69,10 +64,8 @@ const useModQueueStore = create<ModQueueState>()(
return { queuedCommentHistory: [...rememberedByCid.values()].slice(0, MAX_QUEUE_HISTORY_COMMENTS) };
}),
selectedBoardFilter: null,
viewMode: 'feed',
setAlertThreshold: (value, unit) => set({ alertThresholdValue: value, alertThresholdUnit: unit }),
setSelectedBoardFilter: (boardAddress) => set({ selectedBoardFilter: boardAddress }),
setViewMode: (viewMode) => set({ viewMode }),
getAlertThresholdSeconds: () => {
const { alertThresholdValue, alertThresholdUnit } = get();
@@ -81,7 +74,7 @@ const useModQueueStore = create<ModQueueState>()(
}),
{
name: 'mod-queue-storage',
version: 2,
version: 3,
// Migrate old alertThresholdHours format to new alertThresholdValue/alertThresholdUnit format
migrate: (persistedState, version): ModQueueState => {
const state = persistedState as OldPersistedState;
@@ -91,7 +84,6 @@ const useModQueueStore = create<ModQueueState>()(
alertThresholdUnit: 'hours' as AlertThresholdUnit,
dismissedCommentCids: state.dismissedCommentCids ?? [],
queuedCommentHistory: state.queuedCommentHistory ?? [],
selectedBoardFilter: state.selectedBoardFilter ?? null,
viewMode: state.viewMode ?? 'feed',
};
// Zustand will merge this with the store definition (which includes methods)
@@ -103,7 +95,6 @@ const useModQueueStore = create<ModQueueState>()(
alertThresholdUnit: state.alertThresholdUnit ?? 'hours',
dismissedCommentCids: state.dismissedCommentCids ?? [],
queuedCommentHistory: state.queuedCommentHistory ?? [],
selectedBoardFilter: state.selectedBoardFilter ?? null,
viewMode: state.viewMode ?? 'feed',
};
return current as ModQueueState;
@@ -113,7 +104,6 @@ const useModQueueStore = create<ModQueueState>()(
alertThresholdUnit: state.alertThresholdUnit,
dismissedCommentCids: state.dismissedCommentCids,
queuedCommentHistory: state.queuedCommentHistory,
selectedBoardFilter: state.selectedBoardFilter,
viewMode: state.viewMode,
}),
},
@@ -1,7 +1,7 @@
import * as React from 'react';
import { createElement } from 'react';
import { createRoot, type Root } from 'react-dom/client';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { Link, MemoryRouter, Route, Routes, useNavigate } from 'react-router-dom';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import ModQueueView from '../mod-queue';
@@ -31,7 +31,6 @@ const testState = vi.hoisted(() => ({
queuedCommentHistory: [] as TestComment[],
rememberCommentsInQueueMock: vi.fn(),
resetMock: vi.fn(),
selectedBoardFilter: null as string | null,
setResetFunctionMock: vi.fn(),
viewMode: 'compact' as 'compact' | 'feed',
}));
@@ -42,8 +41,6 @@ const getModQueueState = () => ({
getAlertThresholdSeconds: () => 6 * 60 * 60,
queuedCommentHistory: testState.queuedCommentHistory,
rememberCommentsInQueue: testState.rememberCommentsInQueueMock,
selectedBoardFilter: testState.selectedBoardFilter,
setSelectedBoardFilter: vi.fn(),
setViewMode: vi.fn(),
viewMode: testState.viewMode,
});
@@ -161,6 +158,13 @@ vi.mock('../../../hooks/use-current-time', () => ({
}));
vi.mock('../../../hooks/use-directories', () => ({
findDirectoryByAddress: (directories: typeof testState.directories, address: string) =>
directories.find((directory) =>
[directory.address, directory.directoryCode, directory.title].some(
(value) => typeof value === 'string' && value.replace(/(\.bso|\.eth)$/, '') === address.replace(/(\.bso|\.eth)$/, ''),
),
),
normalizeBoardAddress: (address: string) => address.replace(/(\.bso|\.eth)$/, ''),
useDirectories: () => testState.directories,
}));
@@ -176,17 +180,17 @@ vi.mock('../../../components/error-display/error-display', () => ({
default: ({ error }: { error?: Error }) => createElement('div', { 'data-testid': 'error-display' }, error?.message || 'error'),
}));
vi.mock('../../../components/footer', () => ({
vi.mock('../../../components/footer/footer', () => ({
PageFooterDesktop: ({ firstRow }: { firstRow: React.ReactNode }) => createElement('div', { 'data-testid': 'footer-desktop' }, firstRow),
PageFooterMobile: ({ children }: { children: React.ReactNode }) => createElement('div', { 'data-testid': 'footer-mobile' }, children),
StyleOnlyFooterFirstRow: () => createElement('div', { 'data-testid': 'style-footer-row' }),
}));
vi.mock('../../../components/loading-ellipsis', () => ({
vi.mock('../../../components/loading-ellipsis/loading-ellipsis', () => ({
default: ({ string }: { string: string }) => createElement('div', { 'data-testid': 'loading-ellipsis' }, string),
}));
vi.mock('../../../components/tooltip', () => ({
vi.mock('../../../components/tooltip/tooltip', () => ({
default: ({ children }: { children: React.ReactNode }) => createElement(React.Fragment, {}, children),
}));
@@ -208,6 +212,16 @@ vi.mock('../../post/post', () => ({
let container: HTMLDivElement;
let root: Root;
const ModQueueWithLeaveButton = () => {
const navigate = useNavigate();
return createElement(
React.Fragment,
{},
createElement('button', { type: 'button', 'data-testid': 'leave-route', onClick: () => navigate('/other') }, 'leave'),
createElement(ModQueueView),
);
};
const renderModQueue = async () => {
await act(async () => {
root.render(
@@ -220,15 +234,36 @@ const renderModQueue = async () => {
});
};
const renderModQueueWithOtherRoute = async () => {
await act(async () => {
root.render(
createElement(
MemoryRouter,
{ initialEntries: ['/mod/queue'] },
createElement(
Routes,
{},
createElement(Route, { path: '/mod/queue', element: createElement(ModQueueWithLeaveButton) }),
createElement(Route, {
path: '/other',
element: createElement('div', {}, createElement(Link, { to: '/mod/queue' }, 'queue'), createElement('span', {}, 'other route')),
}),
),
),
);
});
};
describe('ModQueueView', () => {
beforeEach(() => {
vi.clearAllMocks();
testState.accountCommunityAddresses = ['music-posting.eth'];
testState.directories = [{ address: 'music-posting.eth', directoryCode: 'mu', title: '/mu/ - Music' }];
testState.dismissedCommentCids = [];
testState.feed = [];
testState.hasMore = false;
testState.isMobile = false;
testState.queuedCommentHistory = [];
testState.selectedBoardFilter = null;
testState.viewMode = 'compact';
container = document.createElement('div');
@@ -265,6 +300,58 @@ describe('ModQueueView', () => {
expect(text.indexOf('No.')).toBeLessThan(text.indexOf('queue_is_empty'));
});
it('resets the board summary selection to all after leaving and returning to the route', async () => {
testState.accountCommunityAddresses = ['music-posting.eth', 'sports-posting.eth'];
testState.directories = [
{ address: 'music-posting.eth', directoryCode: 'mu', title: '/mu/ - Music' },
{ address: 'sports-posting.eth', directoryCode: 'sp', title: '/sp/ - Sports' },
];
testState.feed = [
{
cid: 'music-pending',
communityAddress: 'music-posting.eth',
content: 'music pending body',
pendingApproval: true,
timestamp: 90_000,
},
{
cid: 'sports-pending',
communityAddress: 'sports-posting.eth',
content: 'sports pending body',
pendingApproval: true,
timestamp: 90_000,
},
];
await renderModQueueWithOtherRoute();
const musicButton = Array.from(container.querySelectorAll<HTMLButtonElement>('button')).find((button) => button.textContent?.includes('mu'));
expect(musicButton).toBeTruthy();
await act(async () => {
musicButton?.click();
});
expect(Array.from(container.querySelectorAll<HTMLButtonElement>('button')).find((button) => button.textContent?.includes('mu'))?.className).toContain(
'boardSummaryLinkSelected',
);
await act(async () => {
container.querySelector<HTMLButtonElement>('[data-testid="leave-route"]')?.click();
});
expect(container.textContent).toContain('other route');
await act(async () => {
container.querySelector<HTMLAnchorElement>('a[href="/mod/queue"]')?.click();
});
const allButton = Array.from(container.querySelectorAll<HTMLButtonElement>('button')).find((button) => button.textContent?.includes('all'));
expect(allButton?.className).toContain('boardSummaryLinkSelected');
expect(Array.from(container.querySelectorAll<HTMLButtonElement>('button')).find((button) => button.textContent?.includes('mu'))?.className).not.toContain(
'boardSummaryLinkSelected',
);
});
it('opens a full floating post preview from a compact excerpt hover', async () => {
testState.feed = [
{
+15 -17
View File
@@ -9,7 +9,7 @@ import { Virtuoso } from 'react-virtuoso';
import styles from './mod-queue.module.css';
import postStyles from '../post/post.module.css';
import useModQueueStore from '../../stores/use-mod-queue-store';
import LoadingEllipsis from '../../components/loading-ellipsis';
import LoadingEllipsis from '../../components/loading-ellipsis/loading-ellipsis';
import ErrorDisplay from '../../components/error-display/error-display';
import { useFeedStateString } from '../../hooks/use-state-string';
import { getCommunityAddress, getBoardPath, areSameBoardAddress } from '../../lib/utils/route-utils';
@@ -40,7 +40,7 @@ import {
getVisibleQueuedCommentHistory,
shouldKeepQueuedCommentHistory,
} from '../../lib/utils/mod-queue-utils';
import Tooltip from '../../components/tooltip';
import Tooltip from '../../components/tooltip/tooltip';
import { useCommunityIdentifier, useCommunityIdentifiers } from '../../hooks/use-community-identifiers';
import useIsMobile from '../../hooks/use-is-mobile';
import { useCurrentTime } from '../../hooks/use-current-time';
@@ -48,7 +48,7 @@ import { Post } from '../post/post';
import { canAccessBoardModQueue, hasModQueueAccessRole } from '../../lib/utils/mod-access';
import capitalize from 'lodash/capitalize';
import lowerCase from 'lodash/lowerCase';
import { PageFooterDesktop, PageFooterMobile, StyleOnlyFooterFirstRow } from '../../components/footer';
import { PageFooterDesktop, PageFooterMobile, StyleOnlyFooterFirstRow } from '../../components/footer/footer';
import footerStyles from '../../components/footer/footer.module.css';
import { useModeratedCommunityAddresses } from '../../hooks/use-moderated-community-addresses';
@@ -263,15 +263,7 @@ const ModQueueExcerptPreviewLink = ({ comment, excerpt, postUrl, postUrlState }:
{excerpt}
</Link>
) : (
<span
title={excerpt}
ref={setReferenceNode}
tabIndex={0}
onMouseEnter={openPreview}
onFocus={openPreview}
onMouseLeave={closePreview}
onBlur={closePreview}
>
<span title={excerpt} ref={setReferenceNode} tabIndex={0} onMouseEnter={openPreview} onFocus={openPreview} onMouseLeave={closePreview} onBlur={closePreview}>
{excerpt}
</span>
)}
@@ -687,6 +679,8 @@ interface ModQueueBoardSummaryProps {
feed: Comment[];
directories: DirectoryCommunity[];
accountCommunityAddresses: string[];
selectedBoardFilter: string | null;
setSelectedBoardFilter: (boardAddress: string | null) => void;
}
const ModQueueBoardCount = ({ normal, urgent }: { normal: number; urgent: number }) => {
@@ -710,10 +704,8 @@ const ModQueueBoardCount = ({ normal, urgent }: { normal: number; urgent: number
);
};
const ModQueueBoardSummary = ({ feed, directories, accountCommunityAddresses }: ModQueueBoardSummaryProps) => {
const ModQueueBoardSummary = ({ feed, directories, accountCommunityAddresses, selectedBoardFilter, setSelectedBoardFilter }: ModQueueBoardSummaryProps) => {
const { t } = useTranslation();
const selectedBoardFilter = useModQueueStore((state) => state.selectedBoardFilter);
const setSelectedBoardFilter = useModQueueStore((state) => state.setSelectedBoardFilter);
const getAlertThresholdSeconds = useModQueueStore((state) => state.getAlertThresholdSeconds);
const currentTime = useCurrentTime();
const alertThresholdSeconds = getAlertThresholdSeconds();
@@ -936,7 +928,7 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp
const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProps) => {
const { t } = useTranslation();
const params = useParams();
const selectedBoardFilter = useModQueueStore((state) => state.selectedBoardFilter);
const [selectedBoardFilter, setSelectedBoardFilter] = useState<string | null>(null);
const viewMode = useModQueueStore((state) => state.viewMode);
const dismissedCommentCids = useModQueueStore((state) => state.dismissedCommentCids);
const queuedCommentHistory = useModQueueStore((state) => state.queuedCommentHistory);
@@ -1098,7 +1090,13 @@ const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProp
{!resolvedAddress && (
<div className={styles.controls}>
<div className={styles.controlsLeft}>
<ModQueueBoardSummary feed={feed} directories={directories} accountCommunityAddresses={accountCommunityAddresses} />
<ModQueueBoardSummary
feed={feed}
directories={directories}
accountCommunityAddresses={accountCommunityAddresses}
selectedBoardFilter={selectedBoardFilter}
setSelectedBoardFilter={setSelectedBoardFilter}
/>
</div>
</div>
)}