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();
|
expect(textarea).toBeTruthy();
|
||||||
|
|
||||||
await dispatchInput(optionsInput as HTMLInputElement, 'x y z');
|
await dispatchInput(optionsInput as HTMLInputElement, 'x y z');
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await waitForOptionsValidation();
|
await waitForOptionsValidation();
|
||||||
expect(container.textContent).toContain('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');
|
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('error');
|
||||||
expect(delayedOptionsError?.className).toContain('formError');
|
expect(delayedOptionsError?.className).toContain('formError');
|
||||||
|
|
||||||
@@ -636,7 +636,7 @@ describe('PostForm', () => {
|
|||||||
|
|
||||||
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
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>');
|
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');
|
await clickByText(table as HTMLTableElement, 'post');
|
||||||
@@ -660,7 +660,7 @@ describe('PostForm', () => {
|
|||||||
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
||||||
await waitForOptionsValidation();
|
await waitForOptionsValidation();
|
||||||
|
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await dispatchInput(textarea as HTMLTextAreaElement, 'silly fortune');
|
await dispatchInput(textarea as HTMLTextAreaElement, 'silly fortune');
|
||||||
await clickByText(table as HTMLTableElement, 'post');
|
await clickByText(table as HTMLTableElement, 'post');
|
||||||
@@ -684,7 +684,7 @@ describe('PostForm', () => {
|
|||||||
await dispatchInput(optionsInput as HTMLInputElement, 'dice+1d6+3');
|
await dispatchInput(optionsInput as HTMLInputElement, 'dice+1d6+3');
|
||||||
await waitForOptionsValidation();
|
await waitForOptionsValidation();
|
||||||
|
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await dispatchInput(textarea as HTMLTextAreaElement, 'dice body');
|
await dispatchInput(textarea as HTMLTextAreaElement, 'dice body');
|
||||||
await clickByText(table as HTMLTableElement, 'post');
|
await clickByText(table as HTMLTableElement, 'post');
|
||||||
@@ -705,10 +705,12 @@ describe('PostForm', () => {
|
|||||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||||
|
|
||||||
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
await dispatchInput(optionsInput as HTMLInputElement, 'fortune');
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await waitForOptionsValidation();
|
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 dispatchInput(textarea as HTMLTextAreaElement, 'plain body');
|
||||||
await clickByText(table as HTMLTableElement, 'post');
|
await clickByText(table as HTMLTableElement, 'post');
|
||||||
@@ -717,6 +719,24 @@ describe('PostForm', () => {
|
|||||||
expect(testState.publishPostOptions.content).toBe('plain body');
|
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 () => {
|
it('treats dice rolls as unsupported outside /tg/ and /qst/', async () => {
|
||||||
testState.resolvedCommunityAddress = 'random-nsfw.bso';
|
testState.resolvedCommunityAddress = 'random-nsfw.bso';
|
||||||
|
|
||||||
@@ -728,10 +748,12 @@ describe('PostForm', () => {
|
|||||||
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
const textarea = table?.querySelector<HTMLTextAreaElement>('textarea');
|
||||||
|
|
||||||
await dispatchInput(optionsInput as HTMLInputElement, 'dice+1d6');
|
await dispatchInput(optionsInput as HTMLInputElement, 'dice+1d6');
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await waitForOptionsValidation();
|
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 dispatchInput(textarea as HTMLTextAreaElement, 'plain dice');
|
||||||
await clickByText(table as HTMLTableElement, 'post');
|
await clickByText(table as HTMLTableElement, 'post');
|
||||||
|
|||||||
@@ -233,6 +233,12 @@
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error a,
|
||||||
|
.error a:visited {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.formError {
|
.formError {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
padding: 6px 5px;
|
padding: 6px 5px;
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation
|
|||||||
import {
|
import {
|
||||||
type DiceRoll,
|
type DiceRoll,
|
||||||
type FortuneEntry,
|
type FortuneEntry,
|
||||||
|
type PostOptionsValidationError,
|
||||||
POST_OPTIONS_VALIDATION_DELAY_MS,
|
POST_OPTIONS_VALIDATION_DELAY_MS,
|
||||||
getContentWithPostOptionState as getContentWithOptions,
|
getContentWithPostOptionState as getContentWithOptions,
|
||||||
getNonokoPendingRouteState,
|
getNonokoPendingRouteState,
|
||||||
getPostOptionsDirectoryCode,
|
getPostOptionsDirectoryCode,
|
||||||
getUnsupportedPostOptionsMessage,
|
getPostOptionsValidationError,
|
||||||
hasNonokoOption,
|
hasNonokoOption,
|
||||||
isUnsupportedPostOptionsMessage,
|
isPostOptionsValidationError,
|
||||||
} from '../../lib/utils/post-options-utils';
|
} from '../../lib/utils/post-options-utils';
|
||||||
import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
|
import { truncateWithEllipsisInMiddle } from '../../lib/utils/string-utils';
|
||||||
import { getPublishURLFilename, isValidPublishURL, isValidURL } from '../../lib/utils/url-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 BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
||||||
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
|
import BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
|
||||||
import LoadingEllipsis from '../loading-ellipsis';
|
import LoadingEllipsis from '../loading-ellipsis';
|
||||||
|
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
|
||||||
import styles from './post-form.module.css';
|
import styles from './post-form.module.css';
|
||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import debounce from 'lodash/debounce';
|
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 showBbcodeToolbar = hasModQueueAccessRole(accountRole) || (!effectiveBoardAddress && isInModView && accountCommunityAddresses.length > 0);
|
||||||
|
|
||||||
const [lengthError, setLengthError] = useState<string | null>(null);
|
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 [isBbcodePreviewing, setIsBbcodePreviewing] = useState(false);
|
||||||
const [bbcodePreviewContent, setBbcodePreviewContent] = useState('');
|
const [bbcodePreviewContent, setBbcodePreviewContent] = useState('');
|
||||||
|
|
||||||
@@ -463,7 +465,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
|
|
||||||
const checkPostOptions = useRef(
|
const checkPostOptions = useRef(
|
||||||
debounce((options: string, directoryCode: string | undefined) => {
|
debounce((options: string, directoryCode: string | undefined) => {
|
||||||
const nextOptionsError = getUnsupportedPostOptionsMessage(options, directoryCode);
|
const nextOptionsError = getPostOptionsValidationError(options, directoryCode);
|
||||||
if (nextOptionsError) {
|
if (nextOptionsError) {
|
||||||
setFormError(nextOptionsError);
|
setFormError(nextOptionsError);
|
||||||
}
|
}
|
||||||
@@ -487,6 +489,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
checkPostOptions.cancel();
|
checkPostOptions.cancel();
|
||||||
fortuneEntryRef.current = null;
|
fortuneEntryRef.current = null;
|
||||||
diceRollRef.current = null;
|
diceRollRef.current = null;
|
||||||
|
setFormError(null);
|
||||||
setIsBbcodePreviewing(false);
|
setIsBbcodePreviewing(false);
|
||||||
setBbcodePreviewContent('');
|
setBbcodePreviewContent('');
|
||||||
};
|
};
|
||||||
@@ -504,7 +507,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
const currentContent = textRef.current?.value || '';
|
const currentContent = textRef.current?.value || '';
|
||||||
const currentUrl = urlRef.current?.value.trim() || '';
|
const currentUrl = urlRef.current?.value.trim() || '';
|
||||||
const currentOptions = optionsRef.current?.value || '';
|
const currentOptions = optionsRef.current?.value || '';
|
||||||
const currentOptionsError = getUnsupportedPostOptionsMessage(currentOptions, postOptionsDirectoryCode);
|
const currentOptionsError = getPostOptionsValidationError(currentOptions, postOptionsDirectoryCode);
|
||||||
const publishContent = getContentWithOptions(currentContent, currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
const publishContent = getContentWithOptions(currentContent, currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||||
|
|
||||||
checkContentLength.cancel();
|
checkContentLength.cancel();
|
||||||
@@ -600,7 +603,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
const handleOptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleOptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const options = e.target.value;
|
const options = e.target.value;
|
||||||
handleContentValueChange(textRef.current?.value || '', options);
|
handleContentValueChange(textRef.current?.value || '', options);
|
||||||
setFormError((currentError) => (isUnsupportedPostOptionsMessage(currentError) ? null : currentError));
|
setFormError((currentError) => (isPostOptionsValidationError(currentError) ? null : currentError));
|
||||||
checkPostOptions(options, postOptionsDirectoryCode);
|
checkPostOptions(options, postOptionsDirectoryCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -618,7 +621,7 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
const onPublishReply = () => {
|
const onPublishReply = () => {
|
||||||
const currentUrl = urlRef.current?.value.trim() || '';
|
const currentUrl = urlRef.current?.value.trim() || '';
|
||||||
const currentOptions = optionsRef.current?.value || '';
|
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);
|
const publishContent = getContentWithOptions(textRef.current?.value || '', currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||||
|
|
||||||
checkContentLength.cancel();
|
checkContentLength.cancel();
|
||||||
@@ -746,7 +749,11 @@ const PostFormTable = ({ closeForm, postCid }: { closeForm: () => void; postCid:
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{showBbcodeToolbar ? <div className={`${styles.error} ${styles.formError}`}>warning: posting as moderator</div> : null}
|
{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>}
|
{publishPostError && <div className={`${styles.error} ${styles.formError}`}>{publishPostError}</div>}
|
||||||
{publishReplyError && <div className={`${styles.error} ${styles.formError}`}>{publishReplyError}</div>}
|
{publishReplyError && <div className={`${styles.error} ${styles.formError}`}>{publishReplyError}</div>}
|
||||||
{publishReplyStateMessage && <div className={styles.status}>{publishReplyStateMessage}</div>}
|
{publishReplyStateMessage && <div className={styles.status}>{publishReplyStateMessage}</div>}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import type { DirectoryCommunity } from '../../hooks/use-directories';
|
||||||
|
import type { PostOptionsValidationError } from '../../lib/utils/post-options-utils';
|
||||||
|
|
||||||
|
interface PostOptionsErrorMessageProps {
|
||||||
|
directories?: DirectoryCommunity[];
|
||||||
|
error: PostOptionsValidationError;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMPTY_DIRECTORIES: DirectoryCommunity[] = [];
|
||||||
|
const DIRECTORY_TITLE_CODE_PATTERN = /^\/([^/]+)\//;
|
||||||
|
const DIRECTORY_TITLE_LABEL_PATTERN = /^\/[^/]+\//;
|
||||||
|
|
||||||
|
const getDirectoryCode = (directory: DirectoryCommunity): string | undefined => directory.directoryCode ?? directory.title?.match(DIRECTORY_TITLE_CODE_PATTERN)?.[1];
|
||||||
|
|
||||||
|
const getDirectoryLabel = (code: string, directories: DirectoryCommunity[]): string => {
|
||||||
|
const directory = directories.find((entry) => getDirectoryCode(entry) === code);
|
||||||
|
return directory?.title?.match(DIRECTORY_TITLE_LABEL_PATTERN)?.[0] ?? `/${code}/`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DirectoryLinks = ({ codes, directories }: { codes: string[]; directories: DirectoryCommunity[] }) => (
|
||||||
|
<>
|
||||||
|
{codes.map((code, index) => {
|
||||||
|
const separator = index === 0 ? '' : ', ';
|
||||||
|
return (
|
||||||
|
<Fragment key={code}>
|
||||||
|
{separator}
|
||||||
|
<Link to={`/${code}`}>{getDirectoryLabel(code, directories)}</Link>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const PostOptionsErrorMessage = ({ directories = EMPTY_DIRECTORIES, error }: PostOptionsErrorMessageProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
Unsupported options: {error.unsupportedOptions.join(', ')}.
|
||||||
|
{error.supportedDirectoryCodesByOption.map(({ option, directoryCodes }) => (
|
||||||
|
<Fragment key={option}>
|
||||||
|
{' '}
|
||||||
|
Option "{option}" is supported on: <DirectoryLinks codes={directoryCodes} directories={directories} />.
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostOptionsErrorMessage;
|
||||||
@@ -141,6 +141,7 @@ vi.mock('../../../stores/use-media-hosting-store', () => ({
|
|||||||
vi.mock('../../../hooks/use-directories', () => ({
|
vi.mock('../../../hooks/use-directories', () => ({
|
||||||
findDirectoryByAddress: (directories: Array<{ address: string; features?: Record<string, unknown>; title?: string }>, address: string | undefined) =>
|
findDirectoryByAddress: (directories: Array<{ address: string; features?: Record<string, unknown>; title?: string }>, address: string | undefined) =>
|
||||||
directories.find((entry) => entry.address === address),
|
directories.find((entry) => entry.address === address),
|
||||||
|
useDirectories: () => Object.values(testState.directoryByAddress),
|
||||||
useDirectoryByAddress: (address: string) => testState.directoryByAddress[address],
|
useDirectoryByAddress: (address: string) => testState.directoryByAddress[address],
|
||||||
normalizeBoardAddress: (address: string) => address.replace(/\.(bso|eth)$/, ''),
|
normalizeBoardAddress: (address: string) => address.replace(/\.(bso|eth)$/, ''),
|
||||||
}));
|
}));
|
||||||
@@ -508,11 +509,11 @@ describe('ReplyModal', () => {
|
|||||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
||||||
|
|
||||||
await dispatchInput(optionsInput, 'x y z');
|
await dispatchInput(optionsInput, 'x y z');
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await waitForOptionsValidation();
|
await waitForOptionsValidation();
|
||||||
expect(container.textContent).toContain('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');
|
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('error');
|
||||||
|
|
||||||
await dispatchInput(textarea as HTMLTextAreaElement, 'reply body');
|
await dispatchInput(textarea as HTMLTextAreaElement, 'reply body');
|
||||||
@@ -522,7 +523,7 @@ describe('ReplyModal', () => {
|
|||||||
|
|
||||||
await dispatchInput(optionsInput, 'fortune');
|
await dispatchInput(optionsInput, 'fortune');
|
||||||
|
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({
|
expect(testState.setPublishReplyOptionsMock).toHaveBeenCalledWith({
|
||||||
content: 'reply body<span class="fortune" style="color:#fd4d32"><br><br><b>Your fortune: Excellent Luck</b></span>',
|
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 dispatchInput(optionsInput, 'fortune');
|
||||||
await waitForOptionsValidation();
|
await waitForOptionsValidation();
|
||||||
|
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await dispatchInput(textarea as HTMLTextAreaElement, 'silly reply');
|
await dispatchInput(textarea as HTMLTextAreaElement, 'silly reply');
|
||||||
await clickButtonByText('post');
|
await clickButtonByText('post');
|
||||||
@@ -572,7 +573,7 @@ describe('ReplyModal', () => {
|
|||||||
await dispatchInput(optionsInput, 'dice+2d6');
|
await dispatchInput(optionsInput, 'dice+2d6');
|
||||||
await waitForOptionsValidation();
|
await waitForOptionsValidation();
|
||||||
|
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await dispatchInput(textarea as HTMLTextAreaElement, 'dice reply');
|
await dispatchInput(textarea as HTMLTextAreaElement, 'dice reply');
|
||||||
await clickButtonByText('post');
|
await clickButtonByText('post');
|
||||||
@@ -594,10 +595,12 @@ describe('ReplyModal', () => {
|
|||||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
||||||
|
|
||||||
await dispatchInput(optionsInput, 'dice+1d6');
|
await dispatchInput(optionsInput, 'dice+1d6');
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await waitForOptionsValidation();
|
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 dispatchInput(textarea as HTMLTextAreaElement, 'plain dice reply');
|
||||||
await clickButtonByText('post');
|
await clickButtonByText('post');
|
||||||
@@ -616,10 +619,12 @@ describe('ReplyModal', () => {
|
|||||||
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
const textarea = container.querySelector<HTMLTextAreaElement>('textarea');
|
||||||
|
|
||||||
await dispatchInput(optionsInput, 'fortune');
|
await dispatchInput(optionsInput, 'fortune');
|
||||||
expect(container.textContent).not.toContain('unsupported options');
|
expect(container.textContent).not.toContain('Unsupported options');
|
||||||
|
|
||||||
await waitForOptionsValidation();
|
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 dispatchInput(textarea as HTMLTextAreaElement, 'plain reply');
|
||||||
await clickButtonByText('post');
|
await clickButtonByText('post');
|
||||||
|
|||||||
@@ -155,6 +155,12 @@
|
|||||||
text-shadow: 0 1px rgba(0, 0, 0, 0.20);
|
text-shadow: 0 1px rgba(0, 0, 0, 0.20);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error a,
|
||||||
|
.error a:visited {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
width: 294px;
|
width: 294px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import { getExpiringMediaLinkAlert } from '../../lib/utils/media-link-validation
|
|||||||
import {
|
import {
|
||||||
type DiceRoll,
|
type DiceRoll,
|
||||||
type FortuneEntry,
|
type FortuneEntry,
|
||||||
|
type PostOptionsValidationError,
|
||||||
POST_OPTIONS_VALIDATION_DELAY_MS,
|
POST_OPTIONS_VALIDATION_DELAY_MS,
|
||||||
getContentWithPostOptionState as getContentWithOptions,
|
getContentWithPostOptionState as getContentWithOptions,
|
||||||
getPostOptionsDirectoryCode,
|
getPostOptionsDirectoryCode,
|
||||||
getUnsupportedPostOptionsMessage,
|
getPostOptionsValidationError,
|
||||||
hasNonokoOption,
|
hasNonokoOption,
|
||||||
isUnsupportedPostOptionsMessage,
|
isPostOptionsValidationError,
|
||||||
} from '../../lib/utils/post-options-utils';
|
} from '../../lib/utils/post-options-utils';
|
||||||
import { getPublishURLFilename, isValidPublishURL } from '../../lib/utils/url-utils';
|
import { getPublishURLFilename, isValidPublishURL } from '../../lib/utils/url-utils';
|
||||||
import { hasModQueueAccessRole } from '../../lib/utils/mod-access';
|
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 useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||||
import { getShowUploadControls, isWebRuntime } from '../../lib/media-hosting/show-upload-controls';
|
import { getShowUploadControls, isWebRuntime } from '../../lib/media-hosting/show-upload-controls';
|
||||||
import useMediaHostingStore from '../../stores/use-media-hosting-store';
|
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 usePublishReply from '../../hooks/use-publish-reply';
|
||||||
import useIsMobile from '../../hooks/use-is-mobile';
|
import useIsMobile from '../../hooks/use-is-mobile';
|
||||||
import { useFileUpload } from '../../hooks/use-file-upload';
|
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 BbcodeEditorToolbar, { BbcodePreview } from '../bbcode-editor-toolbar/bbcode-editor-toolbar';
|
||||||
import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
import BoardOfflineAlert from '../board-offline-alert/board-offline-alert';
|
||||||
import LoadingEllipsis from '../loading-ellipsis';
|
import LoadingEllipsis from '../loading-ellipsis';
|
||||||
|
import PostOptionsErrorMessage from '../post-options-error-message/post-options-error-message';
|
||||||
import styles from './reply-modal.module.css';
|
import styles from './reply-modal.module.css';
|
||||||
import capitalize from 'lodash/capitalize';
|
import capitalize from 'lodash/capitalize';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
@@ -56,7 +58,8 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
const isInAllView = isAllView(location.pathname);
|
const isInAllView = isAllView(location.pathname);
|
||||||
const isInModView = isModView(location.pathname);
|
const isInModView = isModView(location.pathname);
|
||||||
const isInSubscriptionsView = isSubscriptionsView(location.pathname, params);
|
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 showSpoilerForReply = directoryEntry?.features?.noSpoilerReplies !== true;
|
||||||
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
const postOptionsDirectoryCode = getPostOptionsDirectoryCode(directoryEntry, location.pathname);
|
||||||
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
const requirePostLinkIsMediaFeature = directoryEntry?.features?.requirePostLinkIsMedia;
|
||||||
@@ -98,7 +101,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
const quoteInsertNumber = useReplyModalStore((state) => state.quoteInsertNumber);
|
const quoteInsertNumber = useReplyModalStore((state) => state.quoteInsertNumber);
|
||||||
const quoteInsertSelectedText = useReplyModalStore((state) => state.quoteInsertSelectedText);
|
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 [lengthError, setLengthError] = useState<string | null>(null);
|
||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
const [isBbcodePreviewing, setIsBbcodePreviewing] = useState(false);
|
const [isBbcodePreviewing, setIsBbcodePreviewing] = useState(false);
|
||||||
@@ -118,7 +121,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
|
|
||||||
const checkPostOptionsRef = useRef(
|
const checkPostOptionsRef = useRef(
|
||||||
debounce((options: string, directoryCode: string | undefined) => {
|
debounce((options: string, directoryCode: string | undefined) => {
|
||||||
const nextOptionsError = getUnsupportedPostOptionsMessage(options, directoryCode);
|
const nextOptionsError = getPostOptionsValidationError(options, directoryCode);
|
||||||
if (nextOptionsError) {
|
if (nextOptionsError) {
|
||||||
setLengthError(null);
|
setLengthError(null);
|
||||||
setError(nextOptionsError);
|
setError(nextOptionsError);
|
||||||
@@ -130,7 +133,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
const currentContent = textRef.current?.value || '';
|
const currentContent = textRef.current?.value || '';
|
||||||
const currentUrl = urlRef.current?.value.trim() || '';
|
const currentUrl = urlRef.current?.value.trim() || '';
|
||||||
const currentOptions = optionsRef.current?.value || '';
|
const currentOptions = optionsRef.current?.value || '';
|
||||||
const currentOptionsError = getUnsupportedPostOptionsMessage(currentOptions, postOptionsDirectoryCode);
|
const currentOptionsError = getPostOptionsValidationError(currentOptions, postOptionsDirectoryCode);
|
||||||
const publishContent = getContentWithOptions(currentContent, currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
const publishContent = getContentWithOptions(currentContent, currentOptions, fortuneEntryRef, diceRollRef, postOptionsDirectoryCode);
|
||||||
|
|
||||||
checkContentLengthRef.current.cancel();
|
checkContentLengthRef.current.cancel();
|
||||||
@@ -345,7 +348,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
const handleOptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleOptionsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const options = e.target.value;
|
const options = e.target.value;
|
||||||
handleContentValueChange(textRef.current?.value || '', undefined, undefined, options);
|
handleContentValueChange(textRef.current?.value || '', undefined, undefined, options);
|
||||||
setError((currentError) => (isUnsupportedPostOptionsMessage(currentError) ? null : currentError));
|
setError((currentError) => (isPostOptionsValidationError(currentError) ? null : currentError));
|
||||||
checkPostOptionsRef.current(options, postOptionsDirectoryCode);
|
checkPostOptionsRef.current(options, postOptionsDirectoryCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -556,7 +559,7 @@ const ReplyModal = ({ closeModal, showReplyModal, parentCid, parentNumber, threa
|
|||||||
{lengthError ? (
|
{lengthError ? (
|
||||||
<div className={styles.error}>{lengthError}</div>
|
<div className={styles.error}>{lengthError}</div>
|
||||||
) : error ? (
|
) : 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>
|
publishReplyError && <div className={styles.error}>{publishReplyError}</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,18 +1,40 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { getNonokoPendingAccountCommentIndex, getNonokoPendingRouteState, getUnsupportedPostOptionsMessage, hasNonokoOption } from '../post-options-utils';
|
import {
|
||||||
|
getNonokoPendingAccountCommentIndex,
|
||||||
|
getNonokoPendingRouteState,
|
||||||
|
getPostOptionsValidationError,
|
||||||
|
getUnsupportedPostOptionsMessage,
|
||||||
|
hasNonokoOption,
|
||||||
|
} from '../post-options-utils';
|
||||||
|
|
||||||
describe('post-options-utils', () => {
|
describe('post-options-utils', () => {
|
||||||
it('rejects additional dice options instead of dropping them', () => {
|
it('rejects additional dice options instead of dropping them', () => {
|
||||||
expect(getUnsupportedPostOptionsMessage('dice+1d6 dice+1d20', 'qst')).toBe('unsupported options: dice+1d20');
|
expect(getUnsupportedPostOptionsMessage('dice+1d6 dice+1d20', 'qst')).toBe('Unsupported options: dice+1d20.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('supports nonoko while keeping sage unsupported', () => {
|
it('supports nonoko while keeping sage unsupported', () => {
|
||||||
expect(getUnsupportedPostOptionsMessage('nonoko', undefined)).toBeNull();
|
expect(getUnsupportedPostOptionsMessage('nonoko', undefined)).toBeNull();
|
||||||
expect(getUnsupportedPostOptionsMessage('sage', 'b')).toBe('unsupported options: sage');
|
expect(getUnsupportedPostOptionsMessage('sage', 'b')).toBe('Unsupported options: sage.');
|
||||||
expect(hasNonokoOption('fortune nonoko')).toBe(true);
|
expect(hasNonokoOption('fortune nonoko')).toBe(true);
|
||||||
expect(hasNonokoOption('nonokosage')).toBe(false);
|
expect(hasNonokoOption('nonokosage')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('describes board-specific options with supported directories', () => {
|
||||||
|
expect(getPostOptionsValidationError('fortune', 'mu')).toEqual({
|
||||||
|
unsupportedOptions: ['fortune'],
|
||||||
|
supportedDirectoryCodesByOption: [{ option: 'fortune', directoryCodes: ['b', 's5s'] }],
|
||||||
|
});
|
||||||
|
expect(getUnsupportedPostOptionsMessage('fortune', 'mu')).toBe('Unsupported options: fortune. Option "fortune" is supported on: /b/, /s5s/.');
|
||||||
|
expect(getUnsupportedPostOptionsMessage('sage fortune', 'pol')).toBe('Unsupported options: sage, fortune. Option "fortune" is supported on: /b/, /s5s/.');
|
||||||
|
expect(getPostOptionsValidationError('fortune dice+1d6', 'mu')).toEqual({
|
||||||
|
unsupportedOptions: ['fortune', 'dice+1d6'],
|
||||||
|
supportedDirectoryCodesByOption: [
|
||||||
|
{ option: 'fortune', directoryCodes: ['b', 's5s'] },
|
||||||
|
{ option: 'dice+1d6', directoryCodes: ['qst', 'tg'] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('reads the nonoko pending account comment index from direct and wrapped route state', () => {
|
it('reads the nonoko pending account comment index from direct and wrapped route state', () => {
|
||||||
expect(getNonokoPendingRouteState(7)).toEqual({ nonokoPendingAccountCommentIndex: 7 });
|
expect(getNonokoPendingRouteState(7)).toEqual({ nonokoPendingAccountCommentIndex: 7 });
|
||||||
expect(getNonokoPendingAccountCommentIndex({ nonokoPendingAccountCommentIndex: 7 })).toBe(7);
|
expect(getNonokoPendingAccountCommentIndex({ nonokoPendingAccountCommentIndex: 7 })).toBe(7);
|
||||||
|
|||||||
@@ -19,6 +19,20 @@ export interface DiceRoll {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PostOptionsValidationError {
|
||||||
|
unsupportedOptions: string[];
|
||||||
|
supportedDirectoryCodesByOption: Array<{ option: string; directoryCodes: string[] }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isPostOptionsValidationError = (error: unknown): error is PostOptionsValidationError => {
|
||||||
|
if (!error || typeof error !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = error as Partial<PostOptionsValidationError>;
|
||||||
|
return Array.isArray(value.unsupportedOptions) && Array.isArray(value.supportedDirectoryCodesByOption);
|
||||||
|
};
|
||||||
|
|
||||||
interface PostOptionsDirectory {
|
interface PostOptionsDirectory {
|
||||||
directoryCode?: string;
|
directoryCode?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -88,27 +102,62 @@ const isSupportedPostOption = (option: string, directoryCode: string | undefined
|
|||||||
return !!parseDiceOption(option) && !!directoryCode && DICE_DIRECTORY_CODES.has(directoryCode);
|
return !!parseDiceOption(option) && !!directoryCode && DICE_DIRECTORY_CODES.has(directoryCode);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUnsupportedPostOptions = (value: string, directoryCode: string | undefined): string[] => {
|
const getSupportedDirectoryCodes = (option: string): string[] => {
|
||||||
let hasDiceOption = false;
|
if (option === 'fortune') {
|
||||||
|
return [...FORTUNE_DIRECTORY_CODES];
|
||||||
|
}
|
||||||
|
|
||||||
return parsePostOptions(value).filter((option) => {
|
return parseDiceOption(option) ? [...DICE_DIRECTORY_CODES] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPostOptionsValidationError = (value: string, directoryCode: string | undefined): PostOptionsValidationError | null => {
|
||||||
|
let hasDiceOption = false;
|
||||||
|
const unsupportedOptions: string[] = [];
|
||||||
|
const supportedDirectoryCodesByOption: Array<{ option: string; directoryCodes: string[] }> = [];
|
||||||
|
|
||||||
|
for (const option of parsePostOptions(value)) {
|
||||||
const diceOption = parseDiceOption(option);
|
const diceOption = parseDiceOption(option);
|
||||||
if (diceOption && directoryCode && DICE_DIRECTORY_CODES.has(directoryCode)) {
|
if (diceOption && directoryCode && DICE_DIRECTORY_CODES.has(directoryCode)) {
|
||||||
const isExtraDiceOption = hasDiceOption;
|
const isExtraDiceOption = hasDiceOption;
|
||||||
hasDiceOption = true;
|
hasDiceOption = true;
|
||||||
return isExtraDiceOption;
|
if (isExtraDiceOption) {
|
||||||
|
unsupportedOptions.push(option);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return !isSupportedPostOption(option, directoryCode);
|
if (isSupportedPostOption(option, directoryCode)) {
|
||||||
});
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionDirectoryCodes = getSupportedDirectoryCodes(option);
|
||||||
|
if (optionDirectoryCodes.length > 0) {
|
||||||
|
unsupportedOptions.push(option);
|
||||||
|
if (!supportedDirectoryCodesByOption.some((entry) => entry.option === option)) {
|
||||||
|
supportedDirectoryCodesByOption.push({ option, directoryCodes: optionDirectoryCodes });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unsupportedOptions.push(option);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return unsupportedOptions.length > 0 ? { unsupportedOptions, supportedDirectoryCodesByOption } : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUnsupportedPostOptionsMessage = (value: string, directoryCode: string | undefined): string | null => {
|
export const getUnsupportedPostOptionsMessage = (value: string, directoryCode: string | undefined): string | null => {
|
||||||
const unsupportedOptions = getUnsupportedPostOptions(value, directoryCode);
|
const error = getPostOptionsValidationError(value, directoryCode);
|
||||||
return unsupportedOptions.length > 0 ? `unsupported options: ${unsupportedOptions.join(', ')}` : null;
|
if (!error) {
|
||||||
};
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export const isUnsupportedPostOptionsMessage = (message: string | null): boolean => message?.startsWith('unsupported options:') === true;
|
let message = `Unsupported options: ${error.unsupportedOptions.join(', ')}.`;
|
||||||
|
|
||||||
|
for (const { option, directoryCodes } of error.supportedDirectoryCodesByOption) {
|
||||||
|
message += ` Option "${option}" is supported on: ${directoryCodes.map((code) => `/${code}/`).join(', ')}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
export const hasNonokoOption = (value: string): boolean => parsePostOptions(value).includes('nonoko');
|
export const hasNonokoOption = (value: string): boolean => parsePostOptions(value).includes('nonoko');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user