mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
fix(post options): link sage warning to FAQ
This commit is contained in:
@@ -7,7 +7,7 @@ import { usePostPageNumber } from '../../hooks/use-post-page-number';
|
||||
import { useDirectories, useDirectoryByAddress } from '../../hooks/use-directories';
|
||||
import { useAccountCommunityAddresses } from '../../hooks/use-account-community-addresses';
|
||||
import { useFilteredDirectoryAddresses } from '../../hooks/use-filtered-directory-addresses';
|
||||
import { getBoardPath, isDirectoryRoute, isFlashBoardRoute } from '../../lib/utils/route-utils';
|
||||
import { getBoardPath, isDirectoryRoute } from '../../lib/utils/route-utils';
|
||||
import { useResolvedCommunityAddress } from '../../hooks/use-resolved-community-address';
|
||||
import { useCommunityIdentifier } from '../../hooks/use-community-identifiers';
|
||||
import useSafeAccountComment from '../../hooks/use-safe-account-comment';
|
||||
@@ -30,6 +30,7 @@ import Tooltip from '../tooltip';
|
||||
import { ModQueueButton } from '../../views/mod-queue/mod-queue';
|
||||
import { isCommentArchived } from '../../lib/utils/comment-moderation-utils';
|
||||
import { getSearchWithTimeFilter, getTimeFilterOptionLabel } from '../../lib/utils/time-filter-utils';
|
||||
import { shouldShowCatalogButton } from './catalog-button-utils';
|
||||
import styles from './board-buttons.module.css';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
|
||||
@@ -45,17 +46,6 @@ interface BoardButtonsProps {
|
||||
|
||||
const EMPTY_COMMUNITY_ADDRESSES: string[] = [];
|
||||
|
||||
export const shouldShowCatalogButton = (
|
||||
boardIdentifier: string | undefined,
|
||||
directories: ReturnType<typeof useDirectories>,
|
||||
{ isInAllView, isInSubscriptionsView, isInModView }: Pick<BoardButtonsProps, 'isInAllView' | 'isInSubscriptionsView' | 'isInModView'>,
|
||||
): boolean => {
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
return true;
|
||||
}
|
||||
return !isFlashBoardRoute(boardIdentifier, directories);
|
||||
};
|
||||
|
||||
const getMultiboardPath = ({
|
||||
isInAllView,
|
||||
isInCatalogView,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { DirectoryCommunity } from '../../hooks/use-directories';
|
||||
import { isFlashBoardRoute } from '../../lib/utils/route-utils';
|
||||
|
||||
interface CatalogButtonVisibilityContext {
|
||||
isInAllView?: boolean;
|
||||
isInSubscriptionsView?: boolean;
|
||||
isInModView?: boolean;
|
||||
}
|
||||
|
||||
export const shouldShowCatalogButton = (
|
||||
boardIdentifier: string | undefined,
|
||||
directories: DirectoryCommunity[],
|
||||
{ isInAllView, isInSubscriptionsView, isInModView }: CatalogButtonVisibilityContext,
|
||||
): boolean => {
|
||||
if (isInAllView || isInSubscriptionsView || isInModView) {
|
||||
return true;
|
||||
}
|
||||
return !isFlashBoardRoute(boardIdentifier, directories);
|
||||
};
|
||||
@@ -70,7 +70,6 @@ vi.mock('../../board-buttons/board-buttons', () => ({
|
||||
isInModView?: boolean;
|
||||
isInSubscriptionsView?: boolean;
|
||||
}) => createElement('button', { 'data-testid': 'catalog-button', type: 'button' }, `${address}|${isInAllView}|${isInSubscriptionsView}|${isInModView}`),
|
||||
shouldShowCatalogButton: () => true,
|
||||
PostPageStats: () => createElement('div', { 'data-testid': 'post-page-stats' }, 'post-page-stats'),
|
||||
RefreshButton: () => createElement('button', { type: 'button' }, 'refresh-button'),
|
||||
ReturnButton: ({
|
||||
@@ -88,6 +87,10 @@ vi.mock('../../board-buttons/board-buttons', () => ({
|
||||
UpdateButton: () => createElement('button', { type: 'button' }, 'update-button'),
|
||||
}));
|
||||
|
||||
vi.mock('../../board-buttons/catalog-button-utils', () => ({
|
||||
shouldShowCatalogButton: () => true,
|
||||
}));
|
||||
|
||||
vi.mock('../../../stores/use-reply-modal-store', () => ({
|
||||
default: () => ({
|
||||
openReplyModalEmpty: testState.openReplyModalEmptyMock,
|
||||
|
||||
@@ -8,13 +8,13 @@ import {
|
||||
CatalogSearchResultsLabel,
|
||||
ReturnButton,
|
||||
CatalogButton,
|
||||
shouldShowCatalogButton,
|
||||
TopButton,
|
||||
UpdateButton,
|
||||
AutoButton,
|
||||
PostPageStats,
|
||||
RefreshButton,
|
||||
} from '../board-buttons/board-buttons';
|
||||
import { shouldShowCatalogButton } from '../board-buttons/catalog-button-utils';
|
||||
import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils';
|
||||
import useReplyModalStore from '../../stores/use-reply-modal-store';
|
||||
import useThreadLiveUpdatesStore from '../../stores/use-thread-live-updates-store';
|
||||
|
||||
@@ -930,7 +930,8 @@ describe('PostForm', () => {
|
||||
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.textContent).toContain('Unsupported options: sage [learn why], fortune. Option "fortune" is supported on: /b/, /s5s/.');
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="/faq#sage"]')?.textContent).toBe('learn why');
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="/b"]')?.textContent).toBe('/b/');
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="/s5s"]')?.textContent).toBe('/s5s/');
|
||||
});
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Fragment } from 'react';
|
||||
import { HashLink } from 'react-router-hash-link';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { DirectoryCommunity } from '../../hooks/use-directories';
|
||||
import type { PostOptionsValidationError } from '../../lib/utils/post-options-utils';
|
||||
import { SAGE_FAQ_LINK_LABEL, SAGE_FAQ_PATH, SAGE_OPTION, type PostOptionsValidationError } from '../../lib/utils/post-options-utils';
|
||||
|
||||
interface PostOptionsErrorMessageProps {
|
||||
directories?: DirectoryCommunity[];
|
||||
@@ -33,10 +34,33 @@ const DirectoryLinks = ({ codes, directories }: { codes: string[]; directories:
|
||||
</>
|
||||
);
|
||||
|
||||
const UnsupportedOption = ({ option }: { option: string }) => (
|
||||
<>
|
||||
{option}
|
||||
{option === SAGE_OPTION && (
|
||||
<>
|
||||
{' '}
|
||||
[<HashLink to={SAGE_FAQ_PATH}>{SAGE_FAQ_LINK_LABEL}</HashLink>]
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
const UnsupportedOptions = ({ options }: { options: string[] }) => (
|
||||
<>
|
||||
{options.map((option, index) => (
|
||||
<Fragment key={`${option}-${index}`}>
|
||||
{index === 0 ? '' : ', '}
|
||||
<UnsupportedOption option={option} />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const PostOptionsErrorMessage = ({ directories = EMPTY_DIRECTORIES, error }: PostOptionsErrorMessageProps) => {
|
||||
return (
|
||||
<>
|
||||
Unsupported options: {error.unsupportedOptions.join(', ')}.
|
||||
Unsupported options: <UnsupportedOptions options={error.unsupportedOptions} />.
|
||||
{error.supportedDirectoryCodesByOption.map(({ option, directoryCodes }) => (
|
||||
<Fragment key={option}>
|
||||
{' '}
|
||||
|
||||
@@ -657,6 +657,21 @@ describe('ReplyModal', () => {
|
||||
randomSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('links the unsupported sage option to its FAQ entry in reply modal', async () => {
|
||||
testState.openEmpty = true;
|
||||
testState.selectedText = '';
|
||||
|
||||
await renderReplyModal('/b/thread/post-1', 'random-nsfw.bso');
|
||||
|
||||
const optionsInput = container.querySelectorAll<HTMLInputElement>('input[type="text"]')[1];
|
||||
|
||||
await dispatchInput(optionsInput, 'sage');
|
||||
await waitForOptionsValidation();
|
||||
|
||||
expect(container.textContent).toContain('Unsupported options: sage [learn why].');
|
||||
expect(container.querySelector<HTMLAnchorElement>('a[href="/faq#sage"]')?.textContent).toBe('learn why');
|
||||
});
|
||||
|
||||
it('supports fortune on the /s5s/ route when directory metadata is not loaded', async () => {
|
||||
const randomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.25);
|
||||
testState.openEmpty = true;
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('post-options-utils', () => {
|
||||
|
||||
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 [learn why].');
|
||||
expect(hasNonokoOption('fortune nonoko')).toBe(true);
|
||||
expect(hasNonokoOption('nonokosage')).toBe(false);
|
||||
});
|
||||
@@ -25,7 +25,7 @@ describe('post-options-utils', () => {
|
||||
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(getUnsupportedPostOptionsMessage('sage fortune', 'pol')).toBe('Unsupported options: sage [learn why], fortune. Option "fortune" is supported on: /b/, /s5s/.');
|
||||
expect(getPostOptionsValidationError('fortune dice+1d6', 'mu')).toEqual({
|
||||
unsupportedOptions: ['fortune', 'dice+1d6'],
|
||||
supportedDirectoryCodesByOption: [
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const POST_OPTIONS_VALIDATION_DELAY_MS = 700;
|
||||
export const SAGE_FAQ_LINK_LABEL = 'learn why';
|
||||
export const SAGE_FAQ_PATH = '/faq#sage';
|
||||
export const SAGE_OPTION = 'sage';
|
||||
const FORTUNE_DIRECTORY_CODES = new Set(['b', 's5s']);
|
||||
const DICE_DIRECTORY_CODES = new Set(['qst', 'tg']);
|
||||
const POST_OPTION_ROUTE_DIRECTORY_CODES = new Set([...FORTUNE_DIRECTORY_CODES, ...DICE_DIRECTORY_CODES]);
|
||||
@@ -150,7 +153,8 @@ export const getUnsupportedPostOptionsMessage = (value: string, directoryCode: s
|
||||
return null;
|
||||
}
|
||||
|
||||
let message = `Unsupported options: ${error.unsupportedOptions.join(', ')}.`;
|
||||
const unsupportedOptionsLabel = error.unsupportedOptions.map((option) => (option === SAGE_OPTION ? `${option} [${SAGE_FAQ_LINK_LABEL}]` : option)).join(', ');
|
||||
let message = `Unsupported options: ${unsupportedOptionsLabel}.`;
|
||||
|
||||
for (const { option, directoryCodes } of error.supportedDirectoryCodesByOption) {
|
||||
message += ` Option "${option}" is supported on: ${directoryCodes.map((code) => `/${code}/`).join(', ')}.`;
|
||||
|
||||
@@ -79,6 +79,7 @@ vi.mock('../../../hooks/use-is-mobile', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/board-buttons/board-buttons', () => ({
|
||||
BracketedCatalogButton: () => createElement('span', null, '[', createElement('button', { type: 'button' }, 'catalog'), ']'),
|
||||
BottomButton: () => createElement('button', { type: 'button' }, 'bottom'),
|
||||
CatalogButton: () => createElement('button', { type: 'button' }, 'catalog'),
|
||||
ReturnButton: () => createElement('button', { type: 'button' }, 'return'),
|
||||
|
||||
@@ -68,6 +68,7 @@ vi.mock('@bitsocial/bitsocial-react-hooks', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../../components/board-buttons/board-buttons', () => ({
|
||||
BracketedCatalogButton: () => createElement('span', null, '[', createElement('a', null, 'catalog'), ']'),
|
||||
BottomButton: () => createElement('button', { type: 'button' }, 'bottom'),
|
||||
CatalogButton: () => createElement('a', null, 'catalog'),
|
||||
ReturnButton: () => createElement('a', null, 'return'),
|
||||
|
||||
Reference in New Issue
Block a user