mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(pending-post): preserve sparse retry routes
This commit is contained in:
@@ -16,6 +16,7 @@ const testState = vi.hoisted(() => ({
|
|||||||
lastPublishOptions: undefined as Record<string, any> | undefined,
|
lastPublishOptions: undefined as Record<string, any> | undefined,
|
||||||
navigateMock: vi.fn(),
|
navigateMock: vi.fn(),
|
||||||
publishCommentMock: vi.fn(async () => undefined),
|
publishCommentMock: vi.fn(async () => undefined),
|
||||||
|
publishIndex: undefined as number | undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('react-router-dom', () => ({
|
vi.mock('react-router-dom', () => ({
|
||||||
@@ -28,6 +29,7 @@ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
|||||||
testState.lastPublishOptions = options;
|
testState.lastPublishOptions = options;
|
||||||
return {
|
return {
|
||||||
abandonPublish: testState.abandonPublishMock,
|
abandonPublish: testState.abandonPublishMock,
|
||||||
|
index: testState.publishIndex,
|
||||||
publishComment: testState.publishCommentMock,
|
publishComment: testState.publishCommentMock,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -79,10 +81,19 @@ const renderHook = (post = failedPost, deleteRedirectPath?: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const flushEffects = async (count = 4) => {
|
||||||
|
for (let i = 0; i < count; i += 1) {
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
describe('useDeleteFailedPost', () => {
|
describe('useDeleteFailedPost', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
testState.lastPublishOptions = undefined;
|
testState.lastPublishOptions = undefined;
|
||||||
|
testState.publishIndex = undefined;
|
||||||
vi.stubGlobal('alert', testState.alertMock);
|
vi.stubGlobal('alert', testState.alertMock);
|
||||||
useChallengesStore.setState({ challenges: [] });
|
useChallengesStore.setState({ challenges: [] });
|
||||||
|
|
||||||
@@ -142,6 +153,20 @@ describe('useDeleteFailedPost', () => {
|
|||||||
expect(testState.publishCommentMock).toHaveBeenCalledTimes(1);
|
expect(testState.publishCommentMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redirects retry publishes to the new pending row', async () => {
|
||||||
|
testState.publishCommentMock.mockImplementationOnce(async () => {
|
||||||
|
testState.publishIndex = 12;
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await latestValue.onRetryFailedPost();
|
||||||
|
});
|
||||||
|
renderHook();
|
||||||
|
await flushEffects();
|
||||||
|
|
||||||
|
expect(testState.navigateMock).toHaveBeenCalledWith('/pending/12', { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
it('redirects after deleting a failed post when a redirect path is provided', async () => {
|
it('redirects after deleting a failed post when a redirect path is provided', async () => {
|
||||||
renderHook(failedPost, '/mu');
|
renderHook(failedPost, '/mu');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ChallengeVerification, Comment, PublishCommentOptions, deleteComment, usePublishComment } from '@bitsocial/bitsocial-react-hooks';
|
import { ChallengeVerification, Comment, PublishCommentOptions, deleteComment, usePublishComment } from '@bitsocial/bitsocial-react-hooks';
|
||||||
import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils';
|
import { alertChallengeVerificationFailed } from '../lib/utils/challenge-utils';
|
||||||
@@ -71,6 +71,7 @@ export const getFailedPostRetryPublishOptions = (post?: FailedPost): PublishComm
|
|||||||
const useDeleteFailedPost = (post?: FailedPost, deleteRedirectPath?: string) => {
|
const useDeleteFailedPost = (post?: FailedPost, deleteRedirectPath?: string) => {
|
||||||
const [isDeletingFailedPost, setIsDeletingFailedPost] = useState(false);
|
const [isDeletingFailedPost, setIsDeletingFailedPost] = useState(false);
|
||||||
const [isRetryingFailedPost, setIsRetryingFailedPost] = useState(false);
|
const [isRetryingFailedPost, setIsRetryingFailedPost] = useState(false);
|
||||||
|
const [isRetryRedirectPending, setIsRetryRedirectPending] = useState(false);
|
||||||
const addChallenge = useChallengesStore((state) => state.addChallenge);
|
const addChallenge = useChallengesStore((state) => state.addChallenge);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const abandonPublishRef = useRef<(() => Promise<void>) | undefined>();
|
const abandonPublishRef = useRef<(() => Promise<void>) | undefined>();
|
||||||
@@ -93,15 +94,25 @@ const useDeleteFailedPost = (post?: FailedPost, deleteRedirectPath?: string) =>
|
|||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
console.error('Failed to retry failed post:', error);
|
console.error('Failed to retry failed post:', error);
|
||||||
|
setIsRetryRedirectPending(false);
|
||||||
alert(`Failed to retry post: ${error.message}`);
|
alert(`Failed to retry post: ${error.message}`);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
[abandonCurrentPublish, addChallenge, retryPublishOptions],
|
[abandonCurrentPublish, addChallenge, retryPublishOptions],
|
||||||
);
|
);
|
||||||
const { abandonPublish, publishComment } = usePublishComment(publishOptionsWithCallbacks);
|
const { abandonPublish, index: retryPostIndex, publishComment } = usePublishComment(publishOptionsWithCallbacks);
|
||||||
abandonPublishRef.current = abandonPublish;
|
abandonPublishRef.current = abandonPublish;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isRetryRedirectPending || typeof retryPostIndex !== 'number') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRetryRedirectPending(false);
|
||||||
|
navigate(`/pending/${retryPostIndex}`, { replace: true });
|
||||||
|
}, [isRetryRedirectPending, navigate, retryPostIndex]);
|
||||||
|
|
||||||
const onDeleteFailedPost = useCallback(() => {
|
const onDeleteFailedPost = useCallback(() => {
|
||||||
if (isDeletingFailedPost || isRetryingFailedPost || !canDeleteFailedPost) {
|
if (isDeletingFailedPost || isRetryingFailedPost || !canDeleteFailedPost) {
|
||||||
return;
|
return;
|
||||||
@@ -140,12 +151,14 @@ const useDeleteFailedPost = (post?: FailedPost, deleteRedirectPath?: string) =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsRetryingFailedPost(true);
|
setIsRetryingFailedPost(true);
|
||||||
|
setIsRetryRedirectPending(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteComment(targetComment);
|
await deleteComment(targetComment);
|
||||||
await publishComment();
|
await publishComment();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to retry failed post:', error);
|
console.error('Failed to retry failed post:', error);
|
||||||
|
setIsRetryRedirectPending(false);
|
||||||
alert(`Failed to retry post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
alert(`Failed to retry post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
} finally {
|
} finally {
|
||||||
setIsRetryingFailedPost(false);
|
setIsRetryingFailedPost(false);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const act = (React as { act?: (cb: () => void | Promise<void>) => void | Promise
|
|||||||
type TestComment = {
|
type TestComment = {
|
||||||
cid?: string;
|
cid?: string;
|
||||||
communityAddress?: string;
|
communityAddress?: string;
|
||||||
|
index?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const testState = vi.hoisted(() => ({
|
const testState = vi.hoisted(() => ({
|
||||||
@@ -136,6 +137,29 @@ describe('PendingPost', () => {
|
|||||||
expect(testState.navigateMock).toHaveBeenCalledWith('/not-found', { replace: true });
|
expect(testState.navigateMock).toHaveBeenCalledWith('/not-found', { replace: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps sparse pending account comment indices addressable', async () => {
|
||||||
|
testState.accountCommentIndex = '1';
|
||||||
|
testState.accountComments = [{ index: 1 }];
|
||||||
|
testState.post = {
|
||||||
|
communityAddress: 'music-posting.eth',
|
||||||
|
index: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
await renderPendingPost();
|
||||||
|
|
||||||
|
expect(container.querySelector('[data-testid="post-view"]')?.textContent).toBe('no-post');
|
||||||
|
expect(testState.navigateMock).not.toHaveBeenCalledWith('/not-found', { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects missing sparse pending account comment indices to not found', async () => {
|
||||||
|
testState.accountCommentIndex = '0';
|
||||||
|
testState.accountComments = [{ index: 1 }];
|
||||||
|
|
||||||
|
await renderPendingPost();
|
||||||
|
|
||||||
|
expect(testState.navigateMock).toHaveBeenCalledWith('/not-found', { replace: true });
|
||||||
|
});
|
||||||
|
|
||||||
it('redirects resolved pending posts to the canonical thread route', async () => {
|
it('redirects resolved pending posts to the canonical thread route', async () => {
|
||||||
testState.accountCommentIndex = '1';
|
testState.accountCommentIndex = '1';
|
||||||
testState.accountComments = [{}, {}];
|
testState.accountComments = [{}, {}];
|
||||||
|
|||||||
@@ -7,6 +7,30 @@ import { getCommentCommunityAddress } from '../../lib/utils/comment-utils';
|
|||||||
import { getBoardPath } from '../../lib/utils/route-utils';
|
import { getBoardPath } from '../../lib/utils/route-utils';
|
||||||
import { Post } from '../post';
|
import { Post } from '../post';
|
||||||
|
|
||||||
|
type PendingAccountComment = {
|
||||||
|
index?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasPendingAccountCommentIndex = (accountComments: PendingAccountComment[] | undefined, accountCommentIndex: number) => {
|
||||||
|
if (!accountComments || accountComments.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasExplicitIndices = false;
|
||||||
|
for (const accountComment of accountComments) {
|
||||||
|
if (typeof accountComment?.index !== 'number') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasExplicitIndices = true;
|
||||||
|
if (accountComment.index === accountCommentIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasExplicitIndices ? false : accountCommentIndex < accountComments.length;
|
||||||
|
};
|
||||||
|
|
||||||
const PendingPost = () => {
|
const PendingPost = () => {
|
||||||
const { accountComments } = useAccountComments();
|
const { accountComments } = useAccountComments();
|
||||||
const { accountCommentIndex } = useParams<{ accountCommentIndex?: string }>();
|
const { accountCommentIndex } = useParams<{ accountCommentIndex?: string }>();
|
||||||
@@ -23,7 +47,7 @@ const PendingPost = () => {
|
|||||||
(hasNormalizedAccountCommentIndex &&
|
(hasNormalizedAccountCommentIndex &&
|
||||||
normalizedAccountCommentIndex >= 0 &&
|
normalizedAccountCommentIndex >= 0 &&
|
||||||
Number.isInteger(normalizedAccountCommentIndex) &&
|
Number.isInteger(normalizedAccountCommentIndex) &&
|
||||||
(accountComments?.length === 0 || normalizedAccountCommentIndex < accountComments.length));
|
hasPendingAccountCommentIndex(accountComments, normalizedAccountCommentIndex));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isValidAccountCommentIndex) {
|
if (!isValidAccountCommentIndex) {
|
||||||
|
|||||||
Reference in New Issue
Block a user