fix(mod queue): keep approved items from sticking

This commit is contained in:
Tommaso Casaburi
2026-04-19 13:45:24 +07:00
parent df01cbfb49
commit 8957f0439e
15 changed files with 792 additions and 83 deletions
@@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest';
import { formatErrorForDisplay } from '../error-utils';
describe('error utils', () => {
it('returns plain string errors unchanged', () => {
expect(formatErrorForDisplay('plain failure')).toBe('plain failure');
});
it('appends structured details to the message', () => {
expect(
formatErrorForDisplay({
details: {
plebpubsub: 'timeout',
pubsubprovider: 'connection refused',
},
message: 'All pubsub providers throw an error and unable to publish or subscribe',
}),
).toBe('All pubsub providers throw an error and unable to publish or subscribe: plebpubsub: timeout; pubsubprovider: connection refused');
});
it('falls back to cause when details are missing', () => {
expect(
formatErrorForDisplay({
cause: {
provider: 'plebpubsub',
reason: 'timeout',
},
message: 'publish failed',
}),
).toBe('publish failed: provider: plebpubsub; reason: timeout');
});
});
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest';
import { filterVisibleModQueueFeed, getModQueueCommentRoute, getQueuedCommentRouteState } from '../mod-queue-utils';
describe('mod queue utils', () => {
it('keeps only comments still awaiting approval', () => {
const feed = [
{ cid: 'pending', communityAddress: 'tech.eth', pendingApproval: true },
{ cid: 'approved', approved: true, communityAddress: 'tech.eth', pendingApproval: true },
{ cid: 'rejected', approved: false, communityAddress: 'tech.eth', pendingApproval: true },
{ cid: 'removed', communityAddress: 'tech.eth', pendingApproval: true, removed: true },
{ cid: 'published', communityAddress: 'tech.eth', pendingApproval: false },
];
expect(filterVisibleModQueueFeed(feed, null)).toEqual([{ cid: 'pending', communityAddress: 'tech.eth', pendingApproval: true }]);
});
it('applies the selected board filter after removing non-pending items', () => {
const feed = [
{ cid: 'tech-pending', communityAddress: 'tech.eth', pendingApproval: true },
{ cid: 'g-pending', communityAddress: 'g.eth', pendingApproval: true },
{ cid: 'tech-approved', approved: true, communityAddress: 'tech.eth', pendingApproval: true },
];
expect(filterVisibleModQueueFeed(feed, 'tech.eth')).toEqual([{ cid: 'tech-pending', communityAddress: 'tech.eth', pendingApproval: true }]);
});
it('builds excerpt routes from the comment permalink cid', () => {
expect(getModQueueCommentRoute('g', 'reply-cid')).toBe('/g/thread/reply-cid');
expect(getModQueueCommentRoute('g', undefined)).toBeUndefined();
expect(getModQueueCommentRoute(undefined, 'reply-cid')).toBeUndefined();
});
it('serializes queued comment route state for reply links', () => {
expect(
getQueuedCommentRouteState({
cid: 'reply-cid',
communityAddress: 'g.eth',
content: 'pending reply body',
parentCid: 'thread-cid',
pendingApproval: true,
postCid: 'thread-cid',
}),
).toEqual({
queuedComment: {
approved: undefined,
author: undefined,
cid: 'reply-cid',
commentModeration: undefined,
communityAddress: 'g.eth',
content: 'pending reply body',
deleted: undefined,
link: undefined,
linkHeight: undefined,
linkWidth: undefined,
number: undefined,
parentCid: 'thread-cid',
pendingApproval: true,
postCid: 'thread-cid',
reason: undefined,
removed: undefined,
replyCount: undefined,
threadCid: undefined,
thumbnailUrl: undefined,
timestamp: undefined,
title: undefined,
},
});
});
});
+64
View File
@@ -0,0 +1,64 @@
type ErrorLike = {
cause?: unknown;
details?: unknown;
message?: unknown;
};
const normalizeUnknownErrorPart = (value: unknown): string | undefined => {
if (value === null || value === undefined) {
return undefined;
}
if (typeof value === 'string') {
return value.trim() || undefined;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
if (Array.isArray(value)) {
const parts = value.map(normalizeUnknownErrorPart).filter(Boolean);
return parts.length ? parts.join('; ') : undefined;
}
if (typeof value === 'object') {
const entries = Object.entries(value as Record<string, unknown>)
.map(([key, entryValue]) => {
const normalizedValue = normalizeUnknownErrorPart(entryValue);
return normalizedValue ? `${key}: ${normalizedValue}` : undefined;
})
.filter(Boolean);
if (entries.length) {
return entries.join('; ');
}
try {
return JSON.stringify(value);
} catch {
return undefined;
}
}
return undefined;
};
export const formatErrorForDisplay = (error: unknown): string | undefined => {
if (!error) {
return undefined;
}
const normalizedString = normalizeUnknownErrorPart(error);
if (typeof error === 'string') {
return normalizedString;
}
const { cause, details, message } = error as ErrorLike;
const normalizedMessage = normalizeUnknownErrorPart(message);
const normalizedDetails = normalizeUnknownErrorPart(details);
const normalizedCause = normalizeUnknownErrorPart(cause);
const detailParts = [normalizedDetails, normalizedCause].filter(Boolean);
if (normalizedMessage && detailParts.length) {
const detailText = detailParts.join('; ');
return normalizedMessage.includes(detailText) ? normalizedMessage : `${normalizedMessage}: ${detailText}`;
}
return normalizedMessage || detailParts[0] || normalizedString;
};
+104
View File
@@ -0,0 +1,104 @@
import { Comment } from '@bitsocialnet/bitsocial-react-hooks';
import { getCommentCommunityAddress } from './comment-utils';
import { isPendingApprovalAwaiting } from './pending-approval-moderation';
import { getThreadTopNavigationState } from './thread-scroll-utils';
type ModQueueCommentLike = {
approved?: boolean;
author?: Comment['author'];
cid?: string;
commentModeration?: Comment['commentModeration'];
communityAddress?: string;
content?: Comment['content'];
deleted?: Comment['deleted'];
link?: Comment['link'];
linkHeight?: Comment['linkHeight'];
linkWidth?: Comment['linkWidth'];
number?: Comment['number'];
parentCid?: Comment['parentCid'];
pendingApproval?: boolean;
postCid?: Comment['postCid'];
reason?: Comment['reason'];
removed?: boolean;
replyCount?: Comment['replyCount'];
threadCid?: Comment['threadCid'];
thumbnailUrl?: Comment['thumbnailUrl'];
timestamp?: Comment['timestamp'];
title?: Comment['title'];
};
export type QueuedCommentRouteState = {
scrollThreadContainerCid?: string;
queuedComment?: {
approved?: Comment['approved'];
author?: Comment['author'];
cid?: Comment['cid'];
commentModeration?: Comment['commentModeration'];
communityAddress?: string;
content?: Comment['content'];
deleted?: Comment['deleted'];
link?: Comment['link'];
linkHeight?: Comment['linkHeight'];
linkWidth?: Comment['linkWidth'];
number?: Comment['number'];
parentCid?: Comment['parentCid'];
pendingApproval?: Comment['pendingApproval'];
postCid?: Comment['postCid'];
reason?: Comment['reason'];
removed?: Comment['removed'];
replyCount?: Comment['replyCount'];
threadCid?: Comment['threadCid'];
thumbnailUrl?: Comment['thumbnailUrl'];
timestamp?: Comment['timestamp'];
title?: Comment['title'];
};
};
export const getModQueueCommentRoute = (boardPath: string | undefined, commentCid: string | undefined): string | undefined =>
boardPath && commentCid ? `/${boardPath}/thread/${commentCid}` : undefined;
export const getQueuedCommentRouteState = (comment: ModQueueCommentLike | undefined): QueuedCommentRouteState | undefined => {
if (!comment?.cid) {
return undefined;
}
return {
...(comment.parentCid ? {} : getThreadTopNavigationState(comment.cid)),
queuedComment: {
approved: comment.approved,
author: comment.author,
cid: comment.cid,
commentModeration: comment.commentModeration,
communityAddress: getCommentCommunityAddress(comment),
content: comment.content,
deleted: comment.deleted,
link: comment.link,
linkHeight: comment.linkHeight,
linkWidth: comment.linkWidth,
number: comment.number,
parentCid: comment.parentCid,
pendingApproval: comment.pendingApproval,
postCid: comment.postCid,
reason: comment.reason,
removed: comment.removed,
replyCount: comment.replyCount,
threadCid: comment.threadCid,
thumbnailUrl: comment.thumbnailUrl,
timestamp: comment.timestamp,
title: comment.title,
},
};
};
export const filterVisibleModQueueFeed = <T extends ModQueueCommentLike>(feed: T[], selectedBoardFilter: string | null): T[] =>
feed.filter((comment) => {
if (!isPendingApprovalAwaiting(comment)) {
return false;
}
if (!selectedBoardFilter) {
return true;
}
return getCommentCommunityAddress(comment) === selectedBoardFilter;
});