mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
Merge branch 'codex/fix/option-board-support-alert'
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