mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(mod queue): reset board filter on navigation (#1166)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user