mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post): link board-specific option errors to supported boards
When fortune or dice options are used on the wrong board, show which boards support them with clickable links in the post form and reply modal.
This commit is contained in:
@@ -141,6 +141,7 @@ vi.mock('../../../stores/use-media-hosting-store', () => ({
|
||||
vi.mock('../../../hooks/use-directories', () => ({
|
||||
findDirectoryByAddress: (directories: Array<{ address: string; features?: Record<string, unknown>; title?: string }>, address: string | undefined) =>
|
||||
directories.find((entry) => entry.address === address),
|
||||
useDirectories: () => Object.values(testState.directoryByAddress),
|
||||
useDirectoryByAddress: (address: string) => testState.directoryByAddress[address],
|
||||
normalizeBoardAddress: (address: string) => address.replace(/\.(bso|eth)$/, ''),
|
||||
}));
|
||||
@@ -508,11 +509,11 @@ describe('ReplyModal', () => {
|
||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput, 'x y z');
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
|
||||
await waitForOptionsValidation();
|
||||
expect(container.textContent).toContain('unsupported options: x, y, z');
|
||||
const delayedOptionsError = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'unsupported options: x, y, z');
|
||||
expect(container.textContent).toContain('Unsupported options: x, y, z.');
|
||||
const delayedOptionsError = Array.from(container.querySelectorAll('div')).find((element) => element.textContent === 'Unsupported options: x, y, z.');
|
||||
expect(delayedOptionsError?.className).toContain('error');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'reply body');
|
||||
@@ -522,7 +523,7 @@ describe('ReplyModal', () => {
|
||||
|
||||
await dispatchInput(optionsInput, 'fortune');
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({
|
||||
content: 'reply body<span class="fortune" style="color:#fd4d32"><br><br><b>Your fortune: Excellent Luck</b></span>',
|
||||
});
|
||||
@@ -547,7 +548,7 @@ describe('ReplyModal', () => {
|
||||
await dispatchInput(optionsInput, 'fortune');
|
||||
await waitForOptionsValidation();
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'silly reply');
|
||||
await clickButtonByText('post');
|
||||
@@ -572,7 +573,7 @@ describe('ReplyModal', () => {
|
||||
await dispatchInput(optionsInput, 'dice+2d6');
|
||||
await waitForOptionsValidation();
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'dice reply');
|
||||
await clickButtonByText('post');
|
||||
@@ -594,10 +595,12 @@ describe('ReplyModal', () => {
|
||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput, 'dice+1d6');
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
|
||||
await waitForOptionsValidation();
|
||||
expect(container.textContent).toContain('unsupported options: dice+1d6');
|
||||
expect(container.textContent).toContain('Unsupported options: dice+1d6. Option "dice+1d6" is supported on: /qst/, /tg/.');
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="/qst"]')?.textContent).toBe('/qst/');
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="/tg"]')?.textContent).toBe('/tg/');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'plain dice reply');
|
||||
await clickButtonByText('post');
|
||||
@@ -616,10 +619,12 @@ describe('ReplyModal', () => {
|
||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput, 'fortune');
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
|
||||
await waitForOptionsValidation();
|
||||
expect(container.textContent).toContain('unsupported options: fortune');
|
||||
expect(container.textContent).toContain('Unsupported options: fortune. Option "fortune" is supported on: /b/, /s5s/.');
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="/b"]')?.textContent).toBe('/b/');
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="/s5s"]')?.textContent).toBe('/s5s/');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'plain reply');
|
||||
await clickButtonByText('post');
|
||||
|
||||
@@ -155,6 +155,12 @@
|
||||
text-shadow: 0 1px rgba(0, 0, 0, 0.20);
|
||||
}
|
||||
|
||||
.error a,
|
||||
.error a:visited {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status {
|
||||
width: 294px;
|
||||
font-family: monospace;
|
||||
|
||||
@@ -7,12 +7,13 @@ import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation
|
||||
import {
|
||||
type DiceRoll,
|
||||
type FortuneEntry,
|
||||
type PostOptionsValidationError,
|
||||
POST_OPTIONS_VALIDATION_DELAY_MS,
|
||||
getContentWithPostOptionState as getContentWithOptions,
|
||||
getPostOptionsDirectoryCode,
|
||||
getUnsupportedPostOptionsMessage,
|
||||
getPostOptionsValidationError,
|
||||
hasNonokoOption,
|
||||
isUnsupportedPostOptionsMessage,
|
||||
isPostOptionsValidationError,
|
||||
} from '../../lib/utils/post-options-utils';
|
||||
import { getPublishURLFilename, isValidPublishURL } from '../../lib/utils/url-utils';
|
||||
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
|
||||
@@ -21,7 +22,7 @@ import useSelectedTextStore from '../../stores/use-selected-text-store';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
import { getShowUploadControls, isWebRuntime } from '../../lib/media-hosting/show-upload-controls';
|
||||
import useMediaHostingStore from '../../stores/use-media-hosting-store';
|
||||
import { useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
import { findDirectoryByAddress, useDirectories } from '../../hooks/use-directories';
|
||||
import usePublishReply from '../../hooks/use-publish-reply';
|
||||
import useIsMobile from '../../hooks/use-is-mobile';
|
||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
||||
@@ -29,6 +30,7 @@ import { useCommunityField } from '../../hooks/use-stable-community';
|
||||
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
|
||||
import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
||||
import LoadingEllipsis from '../loading-ellipsis';
|
||||
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
|
||||
import styles from './reply-modal.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import debounce from 'lodash/debounce';
|
||||
@@ -56,7 +58,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const isInAllView = isAllView(location.pathname);
|
||||
const isInModView = isModView(location.pathname);
|
||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
||||
const directoryEntry = useDirectoryByAddress(communityAddress);
|
||||
const directories = useDirectories();
|
||||
const directoryEntry = findDirectoryByAddress(directories, communityAddress);
|
||||
const showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
|
||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||
@@ -98,7 +101,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const quoteInsertNumber = useReplyModalStore((state) => state.quoteInsertNumber);
|
||||
const quoteInsertSelectedText = useReplyModalStore((state) => state.quoteInsertSelectedText);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | PostOptionsValidationError | null>(null);
|
||||
const [lengthError, setLengthError] = useState<string | null>(null);
|
||||
const [url, setUrl] = useState('');
|
||||
const [isBbcodePreviewing, setIsBbcodePreviewing] = useState(false);
|
||||
@@ -118,7 +121,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
|
||||
const checkPostOptionsRef = useRef(
|
||||
debounce((options: string, directoryCode: string | undefined) => {
|
||||
const nextOptionsError = getUnsupportedPostOptionsMessage(options, directoryCode);
|
||||
const nextOptionsError = getPostOptionsValidationError(options, directoryCode);
|
||||
if (nextOptionsError) {
|
||||
setLengthError(null);
|
||||
setError(nextOptionsError);
|
||||
@@ -130,7 +133,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const currentContent = textRef.current?.value || '';
|
||||
const currentUrl = urlRef.current?.value.trim() || '';
|
||||
const currentOptions = optionsRef.current?.value || '';
|
||||
const currentOptionsError = getUnsupportedPostOptionsMessage(currentOptions, postOptionsDirectoryCode);
|
||||
const currentOptionsError = getPostOptionsValidationError(currentOptions, postOptionsDirectoryCode);
|
||||
const publishContent = getContentWithOptions(currentContent, currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||
|
||||
checkContentLengthRef.current.cancel();
|
||||
@@ -345,7 +348,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
const handleOptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const options = e.target.value;
|
||||
handleContentValueChange(textRef.current?.value || '', undefined, undefined, options);
|
||||
setError((currentError) => (isUnsupportedPostOptionsMessage(currentError) ? null : currentError));
|
||||
setError((currentError) => (isPostOptionsValidationError(currentError) ? null : currentError));
|
||||
checkPostOptionsRef.current(options, postOptionsDirectoryCode);
|
||||
};
|
||||
|
||||
@@ -556,7 +559,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
||||
{lengthError ? (
|
||||
<div className={styles.error}>{lengthError}</div>
|
||||
) : error ? (
|
||||
<div className={styles.error}>{error}</div>
|
||||
<div className={styles.error}>{isPostOptionsValidationError(error) ? <PostOptionsErrorMessage error={error} directories={directories} /> : error}</div>
|
||||
) : (
|
||||
publishReplyError && <div className={styles.error}>{publishReplyError}</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user