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:
@@ -1,18 +1,40 @@
|
||||
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', () => {
|
||||
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', () => {
|
||||
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('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', () => {
|
||||
expect(getNonokoPendingRouteState(7)).toEqual({ nonokoPendingAccountCommentIndex: 7 });
|
||||
expect(getNonokoPendingAccountCommentIndex({ nonokoPendingAccountCommentIndex: 7 })).toBe(7);
|
||||
|
||||
@@ -19,6 +19,20 @@ export interface DiceRoll {
|
||||
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 {
|
||||
directoryCode?: string;
|
||||
title?: string;
|
||||
@@ -88,27 +102,62 @@ const isSupportedPostOption = (option: string, directoryCode: string | undefined
|
||||
return !!parseDiceOption(option) && !!directoryCode && DICE_DIRECTORY_CODES.has(directoryCode);
|
||||
};
|
||||
|
||||
const getUnsupportedPostOptions = (value: string, directoryCode: string | undefined): string[] => {
|
||||
let hasDiceOption = false;
|
||||
const getSupportedDirectoryCodes = (option: string): string[] => {
|
||||
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);
|
||||
if (diceOption && directoryCode && DICE_DIRECTORY_CODES.has(directoryCode)) {
|
||||
const isExtraDiceOption = hasDiceOption;
|
||||
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 => {
|
||||
const unsupportedOptions = getUnsupportedPostOptions(value, directoryCode);
|
||||
return unsupportedOptions.length > 0 ? `unsupported options: ${unsupportedOptions.join(', ')}` : null;
|
||||
};
|
||||
const error = getPostOptionsValidationError(value, directoryCode);
|
||||
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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user