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:
@@ -621,11 +621,11 @@ describe('PostForm', () => {
|
||||
expect(textarea).toBeTruthy();
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, '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');
|
||||
expect(delayedOptionsError?.className).toContain('formError');
|
||||
|
||||
@@ -636,7 +636,7 @@ describe('PostForm', () => {
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
expect(testState.publishPostOptions.content).toBe('fortune body<span class="fortune" style="color:#fd4d32"><br><br><b>Your fortune: Excellent Luck</b></span>');
|
||||
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
@@ -660,7 +660,7 @@ describe('PostForm', () => {
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
||||
await waitForOptionsValidation();
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'silly fortune');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
@@ -684,7 +684,7 @@ describe('PostForm', () => {
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'dice+1d6+3');
|
||||
await waitForOptionsValidation();
|
||||
|
||||
expect(container.textContent).not.toContain('unsupported options');
|
||||
expect(container.textContent).not.toContain('Unsupported options');
|
||||
|
||||
await dispatchInput(textarea as HTMLTextAreaElement, 'dice body');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
@@ -705,10 +705,12 @@ describe('PostForm', () => {
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, '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 body');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
@@ -717,6 +719,24 @@ describe('PostForm', () => {
|
||||
expect(testState.publishPostOptions.content).toBe('plain body');
|
||||
});
|
||||
|
||||
it('combines unavailable and unknown options in one clear post-form message', async () => {
|
||||
testState.resolvedCommunityAddress = 'politically-incorrect.bso';
|
||||
testState.directories = [...testState.directories, { address: 'politically-incorrect.bso', features: {}, title: '/pol/ - Politically Incorrect' }];
|
||||
|
||||
await renderPostForm('/pol');
|
||||
await clickByText(container, 'start_new_thread');
|
||||
|
||||
const table = container.querySelector('table');
|
||||
const optionsInput = table?.querySelector<HTMLInputElement>('input[aria-label="options"]');
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, 'sage fortune');
|
||||
await waitForOptionsValidation();
|
||||
|
||||
expect(container.textContent).toContain('Unsupported options: sage, 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/');
|
||||
});
|
||||
|
||||
it('treats dice rolls as unsupported outside /tg/ and /qst/', async () => {
|
||||
testState.resolvedCommunityAddress = 'random-nsfw.bso';
|
||||
|
||||
@@ -728,10 +748,12 @@ describe('PostForm', () => {
|
||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||
|
||||
await dispatchInput(optionsInput as HTMLInputElement, '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');
|
||||
await clickByText(table as HTMLTableElement, 'post');
|
||||
|
||||
@@ -233,6 +233,12 @@
|
||||
color: red;
|
||||
}
|
||||
|
||||
.error a,
|
||||
.error a:visited {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.formError {
|
||||
margin: 8px 0;
|
||||
padding: 6px 5px;
|
||||
|
||||
@@ -10,13 +10,14 @@ import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation
|
||||
import {
|
||||
type DiceRoll,
|
||||
type FortuneEntry,
|
||||
type PostOptionsValidationError,
|
||||
POST_OPTIONS_VALIDATION_DELAY_MS,
|
||||
getContentWithPostOptionState as getContentWithOptions,
|
||||
getNonokoPendingRouteState,
|
||||
getPostOptionsDirectoryCode,
|
||||
getUnsupportedPostOptionsMessage,
|
||||
getPostOptionsValidationError,
|
||||
hasNonokoOption,
|
||||
isUnsupportedPostOptionsMessage,
|
||||
isPostOptionsValidationError,
|
||||
} from '../../lib/utils/post-options-utils';
|
||||
import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
|
||||
import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/utils/url-utils';
|
||||
@@ -39,6 +40,7 @@ import useMediaHostingStore from '../../stores/use-media-hosting-store';
|
||||
import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
||||
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
|
||||
import LoadingEllipsis from '../loading-ellipsis';
|
||||
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
|
||||
import styles from './post-form.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import debounce from 'lodash/debounce';
|
||||
@@ -446,7 +448,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const showBbcodeToolbar = hasModQueueAccessRole(accountRole) || (!effectiveBoardAddress && isInModView && accountCommunityAddresses.length > 0);
|
||||
|
||||
const [lengthError, setLengthError] = useState<string | null>(null);
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const [formError, setFormError] = useState<string | PostOptionsValidationError | null>(null);
|
||||
const [isBbcodePreviewing, setIsBbcodePreviewing] = useState(false);
|
||||
const [bbcodePreviewContent, setBbcodePreviewContent] = useState('');
|
||||
|
||||
@@ -463,7 +465,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
|
||||
const checkPostOptions = useRef(
|
||||
debounce((options: string, directoryCode: string | undefined) => {
|
||||
const nextOptionsError = getUnsupportedPostOptionsMessage(options, directoryCode);
|
||||
const nextOptionsError = getPostOptionsValidationError(options, directoryCode);
|
||||
if (nextOptionsError) {
|
||||
setFormError(nextOptionsError);
|
||||
}
|
||||
@@ -487,6 +489,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
checkPostOptions.cancel();
|
||||
fortuneEntryRef.current = null;
|
||||
diceRollRef.current = null;
|
||||
setFormError(null);
|
||||
setIsBbcodePreviewing(false);
|
||||
setBbcodePreviewContent('');
|
||||
};
|
||||
@@ -504,7 +507,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
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);
|
||||
|
||||
checkContentLength.cancel();
|
||||
@@ -600,7 +603,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const handleOptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const options = e.target.value;
|
||||
handleContentValueChange(textRef.current?.value || '', options);
|
||||
setFormError((currentError) => (isUnsupportedPostOptionsMessage(currentError) ? null : currentError));
|
||||
setFormError((currentError) => (isPostOptionsValidationError(currentError) ? null : currentError));
|
||||
checkPostOptions(options, postOptionsDirectoryCode);
|
||||
};
|
||||
|
||||
@@ -618,7 +621,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
const onPublishReply = () => {
|
||||
const currentUrl = urlRef.current?.value.trim() || '';
|
||||
const currentOptions = optionsRef.current?.value || '';
|
||||
const currentOptionsError = getUnsupportedPostOptionsMessage(currentOptions, postOptionsDirectoryCode);
|
||||
const currentOptionsError = getPostOptionsValidationError(currentOptions, postOptionsDirectoryCode);
|
||||
const publishContent = getContentWithOptions(textRef.current?.value || '', currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||
|
||||
checkContentLength.cancel();
|
||||
@@ -746,7 +749,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
||||
</tbody>
|
||||
</table>
|
||||
{showBbcodeToolbar ? <div className={`${styles.error} ${styles.formError}`}>warning: posting as moderator</div> : null}
|
||||
{formError && <div className={`${styles.error} ${styles.formError}`}>{formError}</div>}
|
||||
{formError ? (
|
||||
<div className={`${styles.error} ${styles.formError}`}>
|
||||
{isPostOptionsValidationError(formError) ? <PostOptionsErrorMessage error={formError} directories={directories} /> : formError}
|
||||
</div>
|
||||
) : null}
|
||||
{publishPostError && <div className={`${styles.error} ${styles.formError}`}>{publishPostError}</div>}
|
||||
{publishReplyError && <div className={`${styles.error} ${styles.formError}`}>{publishReplyError}</div>}
|
||||
{publishReplyStateMessage && <div className={styles.status}>{publishReplyStateMessage}</div>}
|
||||
|
||||
Reference in New Issue
Block a user