mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Updated select to use react-select (#18164)
refs https://github.com/TryGhost/Product/issues/3832 --- ### <samp>🤖 Generated by Copilot at adffb67</samp> This file updates the global form select component to use a custom `ReactSelect` component with better performance and style.
This commit is contained in:
parent
353c565739
commit
343535116c
@ -3,8 +3,7 @@ import type {Meta, StoryObj} from '@storybook/react';
|
||||
import Button from '../Button';
|
||||
import ButtonGroup from '../ButtonGroup';
|
||||
import DesktopChromeHeader from './DesktopChromeHeader';
|
||||
import URLSelect from '../form/URLSelect';
|
||||
import {SelectOption} from '../form/Select';
|
||||
import Select, {SelectOption} from '../form/Select';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Chrome / Desktop Header',
|
||||
@ -54,7 +53,7 @@ const selectOptions: SelectOption[] = [
|
||||
export const CustomToolbar: Story = {
|
||||
args: {
|
||||
toolbarLeft: <Button icon='arrow-left' link={true} size='sm' />,
|
||||
toolbarCenter: <URLSelect options={selectOptions} onSelect={(value: string) => {
|
||||
toolbarCenter: <Select options={selectOptions} onSelect={(value) => {
|
||||
alert(value);
|
||||
}} />,
|
||||
toolbarRight: <ButtonGroup
|
||||
@ -64,4 +63,4 @@ export const CustomToolbar: Story = {
|
||||
]}
|
||||
/>
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -48,7 +48,7 @@ const DropdownIndicator: React.FC<DropdownIndicatorProps<MultiSelectOption, true
|
||||
|
||||
const Option: React.FC<OptionProps<MultiSelectOption, true>> = ({children, ...optionProps}) => (
|
||||
<components.Option {...optionProps}>
|
||||
<span data-testid="multiselect-option">{children}</span>
|
||||
<span data-testid="select-option">{children}</span>
|
||||
</components.Option>
|
||||
);
|
||||
|
||||
|
@ -86,7 +86,7 @@ export const WithSelectedOption: Story = {
|
||||
export const WithCallback: Story = {
|
||||
args: {
|
||||
options: selectOptions,
|
||||
onSelect: (value: string) => {
|
||||
onSelect: (value) => {
|
||||
alert(value);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React, {useId} from 'react';
|
||||
import React, {useId, useMemo} from 'react';
|
||||
import ReactSelect, {DropdownIndicatorProps, OptionProps, Props, components} from 'react-select';
|
||||
|
||||
import Heading from '../Heading';
|
||||
import Hint from '../Hint';
|
||||
@ -7,6 +8,7 @@ import clsx from 'clsx';
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
key?: string;
|
||||
className?: string;
|
||||
}
|
||||
@ -17,25 +19,48 @@ export interface SelectOptionGroup {
|
||||
options: SelectOption[];
|
||||
}
|
||||
|
||||
export interface SelectProps {
|
||||
export interface SelectControlClasses {
|
||||
control?: string;
|
||||
valueContainer?: string;
|
||||
placeHolder?: string;
|
||||
menu?: string;
|
||||
option?: string;
|
||||
noOptionsMessage?: string;
|
||||
groupHeading?: string;
|
||||
}
|
||||
|
||||
export interface SelectProps extends Props<SelectOption, false> {
|
||||
title?: string;
|
||||
hideTitle?: boolean;
|
||||
size?: 'xs' | 'md';
|
||||
prompt?: string;
|
||||
options: SelectOption[] | SelectOptionGroup[];
|
||||
selectedOption?: string
|
||||
onSelect: (value: string) => void;
|
||||
onSelect: (value: string | undefined) => void;
|
||||
error?:boolean;
|
||||
hint?: React.ReactNode;
|
||||
clearBg?: boolean;
|
||||
border?: boolean;
|
||||
fullWidth?: boolean;
|
||||
containerClassName?: string;
|
||||
selectClassName?: string;
|
||||
optionClassName?: string;
|
||||
controlClasses?: SelectControlClasses;
|
||||
unstyled?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const DropdownIndicator: React.FC<DropdownIndicatorProps<SelectOption, false> & {clearBg: boolean}> = ({clearBg, ...props}) => (
|
||||
<components.DropdownIndicator {...props}>
|
||||
<div className={`absolute top-[14px] block h-2 w-2 rotate-45 border-[1px] border-l-0 border-t-0 border-grey-900 content-[''] dark:border-grey-400 ${clearBg ? 'right-0' : 'right-4'} `}></div>
|
||||
</components.DropdownIndicator>
|
||||
);
|
||||
|
||||
const Option: React.FC<OptionProps<SelectOption, false>> = ({children, ...optionProps}) => (
|
||||
<components.Option {...optionProps}>
|
||||
<span data-testid="select-option">{children}</span>
|
||||
{optionProps.data.hint && <span className="block text-xs text-grey-700 dark:text-grey-300">{optionProps.data.hint}</span>}
|
||||
</components.Option>
|
||||
);
|
||||
|
||||
const Select: React.FC<SelectProps> = ({
|
||||
title,
|
||||
hideTitle,
|
||||
@ -48,25 +73,20 @@ const Select: React.FC<SelectProps> = ({
|
||||
hint,
|
||||
clearBg = true,
|
||||
border = true,
|
||||
fullWidth = true,
|
||||
containerClassName,
|
||||
selectClassName,
|
||||
optionClassName,
|
||||
controlClasses,
|
||||
unstyled,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
...props
|
||||
}) => {
|
||||
const id = useId();
|
||||
|
||||
const handleOptionChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
onSelect(event.target.value);
|
||||
};
|
||||
|
||||
let containerClasses = '';
|
||||
if (!unstyled) {
|
||||
containerClasses = clsx(
|
||||
'relative w-full after:pointer-events-none dark:text-white',
|
||||
`after:absolute after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-[''] dark:after:border-grey-500`,
|
||||
size === 'xs' ? 'after:top-[6px]' : 'after:top-[14px]',
|
||||
clearBg ? 'after:right-0' : 'after:right-4',
|
||||
'dark:text-white',
|
||||
fullWidth && 'w-full',
|
||||
disabled && 'opacity-40'
|
||||
);
|
||||
}
|
||||
@ -75,53 +95,66 @@ const Select: React.FC<SelectProps> = ({
|
||||
containerClassName
|
||||
);
|
||||
|
||||
let selectClasses = '';
|
||||
if (!unstyled) {
|
||||
selectClasses = clsx(
|
||||
size === 'xs' ? 'h-6 py-0 pr-3 text-xs' : 'h-10 py-2 pr-5',
|
||||
'w-full appearance-none outline-none',
|
||||
const customClasses = {
|
||||
control: clsx(
|
||||
controlClasses?.control,
|
||||
'min-h-[40px] w-full cursor-pointer appearance-none outline-none dark:text-white',
|
||||
size === 'xs' ? 'py-0 text-xs' : 'py-2',
|
||||
border && 'border-b',
|
||||
!clearBg && 'bg-grey-75 px-[10px]',
|
||||
error ? '!border-red' : 'border-grey-500 focus:border-black dark:border-grey-800 dark:focus:border-grey-500',
|
||||
disabled ? 'cursor-auto' : 'cursor-pointer hover:border-grey-700',
|
||||
!clearBg && 'bg-grey-75 px-[10px] dark:bg-grey-950',
|
||||
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 dark:border-grey-800 dark:hover:border-grey-700',
|
||||
(title && !clearBg) && 'mt-2'
|
||||
);
|
||||
}
|
||||
selectClasses = clsx(
|
||||
selectClasses,
|
||||
selectClassName
|
||||
);
|
||||
),
|
||||
valueContainer: clsx('gap-1', controlClasses?.valueContainer),
|
||||
placeHolder: clsx('text-grey-500 dark:text-grey-800', controlClasses?.placeHolder),
|
||||
menu: clsx(
|
||||
'z-50 rounded-b bg-white py-2 shadow dark:border dark:border-grey-900 dark:bg-black',
|
||||
size === 'xs' && 'text-xs',
|
||||
controlClasses?.menu
|
||||
),
|
||||
option: clsx('px-3 py-[6px] hover:cursor-pointer hover:bg-grey-100 dark:text-white dark:hover:bg-grey-900', controlClasses?.option),
|
||||
noOptionsMessage: clsx('p-3 text-grey-600', controlClasses?.noOptionsMessage),
|
||||
groupHeading: clsx('px-3 py-[6px] text-2xs font-semibold uppercase tracking-wide text-grey-700', controlClasses?.groupHeading)
|
||||
};
|
||||
|
||||
const optionClasses = optionClassName;
|
||||
const dropdownIndicatorComponent = useMemo(() => {
|
||||
return function DropdownIndicatorComponent(ddiProps: DropdownIndicatorProps<SelectOption, false>) {
|
||||
return <DropdownIndicator {...ddiProps} clearBg={clearBg} />;
|
||||
};
|
||||
}, [clearBg]);
|
||||
|
||||
const individualOptions = options.flatMap((option) => {
|
||||
if ('options' in option) {
|
||||
return option.options;
|
||||
}
|
||||
return option;
|
||||
});
|
||||
|
||||
const select = (
|
||||
<>
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={selectedOption || !prompt ? true : false} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
<div className={containerClasses}>
|
||||
<select className={selectClasses} disabled={disabled} id={id} value={selectedOption} onChange={handleOptionChange}>
|
||||
{prompt && <option className={optionClasses} value="" disabled selected>{prompt}</option>}
|
||||
{options.map(option => (
|
||||
'options' in option ?
|
||||
<optgroup key={option.key || option.label} label={option.label}>
|
||||
{option.options.map(child => (
|
||||
<option
|
||||
key={child.key || child.value}
|
||||
className={clsx(optionClasses, child.className)}
|
||||
value={child.value}
|
||||
>
|
||||
{child.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup> :
|
||||
<option
|
||||
key={option.key || option.value}
|
||||
className={clsx(optionClasses, option.className)}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ReactSelect<SelectOption, false>
|
||||
classNames={{
|
||||
menuList: () => 'z-50',
|
||||
valueContainer: () => customClasses.valueContainer,
|
||||
control: () => customClasses.control,
|
||||
placeholder: () => customClasses.placeHolder,
|
||||
menu: () => customClasses.menu,
|
||||
option: () => customClasses.option,
|
||||
noOptionsMessage: () => customClasses.noOptionsMessage,
|
||||
groupHeading: () => customClasses.groupHeading
|
||||
}}
|
||||
components={{DropdownIndicator: dropdownIndicatorComponent, Option}}
|
||||
inputId={id}
|
||||
isClearable={false}
|
||||
options={options}
|
||||
placeholder={prompt ? prompt : ''}
|
||||
value={individualOptions.find(option => option.value === selectedOption)}
|
||||
unstyled
|
||||
onChange={option => onSelect(option?.value)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
</>
|
||||
|
@ -97,11 +97,12 @@ export const WithDropdown: Story = {
|
||||
rightPlaceholder: (
|
||||
<Select
|
||||
border={false}
|
||||
containerClassName='w-14'
|
||||
fullWidth={false}
|
||||
options={[
|
||||
{label: 'USD', value: 'usd'},
|
||||
{label: 'EUR', value: 'eur'}
|
||||
]}
|
||||
selectClassName='w-auto'
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
)
|
||||
|
@ -1,28 +0,0 @@
|
||||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import URLSelect from './URLSelect';
|
||||
import {SelectOption} from './Select';
|
||||
|
||||
const meta = {
|
||||
title: 'Experimental / URL Select',
|
||||
component: URLSelect,
|
||||
tags: ['autodocs']
|
||||
} satisfies Meta<typeof URLSelect>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof URLSelect>;
|
||||
|
||||
const selectOptions: SelectOption[] = [
|
||||
{value: 'homepage', label: 'Homepage'},
|
||||
{value: 'post', label: 'Post'},
|
||||
{value: 'page', label: 'Page'},
|
||||
{value: 'tag-archive', label: 'Tag archive'},
|
||||
{value: 'author-archive', label: 'Author archive'}
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
options: selectOptions,
|
||||
onSelect: () => {}
|
||||
}
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import Select, {SelectProps} from './Select';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const URLSelect: React.FC<SelectProps> = (props) => {
|
||||
const selectClasses = clsx(
|
||||
`!h-[unset] w-full appearance-none rounded-full border border-grey-100 bg-white px-3 py-1 text-sm`
|
||||
);
|
||||
|
||||
const containerClasses = clsx(
|
||||
'relative w-full max-w-[380px] self-center after:pointer-events-none',
|
||||
`after:absolute after:right-4 after:top-[9px] after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-['']`
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
containerClassName={containerClasses}
|
||||
selectClassName={selectClasses}
|
||||
unstyled={true}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default URLSelect;
|
@ -108,7 +108,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
let toolbarLeft = (<></>);
|
||||
if (previewToolbarURLs) {
|
||||
toolbarLeft = (
|
||||
<Select options={previewToolbarURLs!} selectedOption={selectedURL} onSelect={onSelectURL!} />
|
||||
<Select options={previewToolbarURLs!} selectedOption={selectedURL} onSelect={url => url && onSelectURL?.(url)} />
|
||||
);
|
||||
} else if (previewToolbarTabs) {
|
||||
toolbarLeft = <TabView
|
||||
|
@ -18,18 +18,23 @@ type RefipientValueArgs = {
|
||||
|
||||
const RECIPIENT_FILTER_OPTIONS = [{
|
||||
label: 'Whoever has access to the post',
|
||||
hint: 'Free posts to everyone, premium posts sent to paid members',
|
||||
value: 'visibility'
|
||||
}, {
|
||||
label: 'All members',
|
||||
hint: 'Everyone who is subscribed to newsletter updates, whether free or paid members',
|
||||
value: 'all-members'
|
||||
}, {
|
||||
label: 'Paid-members only',
|
||||
hint: 'People who have a premium subscription',
|
||||
value: 'paid-only'
|
||||
}, {
|
||||
label: 'Specific people',
|
||||
hint: 'Only people with any of the selected tiers or labels',
|
||||
value: 'segment'
|
||||
}, {
|
||||
label: 'Usually nobody',
|
||||
hint: 'Newsletters are off for new posts, but can be enabled when needed',
|
||||
value: 'none'
|
||||
}];
|
||||
|
||||
@ -166,7 +171,9 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
selectedOption={selectedOption}
|
||||
title="Default Newsletter recipients"
|
||||
onSelect={(value) => {
|
||||
setDefaultRecipientValue(value);
|
||||
if (value) {
|
||||
setDefaultRecipientValue(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{(selectedOption === 'segment') && (
|
||||
|
@ -6,8 +6,7 @@ import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {getSettingValues} from '../../../api/settings';
|
||||
import {useEditSettings} from '../../../api/settings';
|
||||
import {getSettingValues, useEditSettings} from '../../../api/settings';
|
||||
|
||||
const MAILGUN_REGIONS = [
|
||||
{label: '🇺🇸 US', value: 'https://api.mailgun.net/v3'},
|
||||
@ -67,7 +66,7 @@ const MailGun: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
selectedOption={mailgunRegion}
|
||||
title="Mailgun region"
|
||||
onSelect={(value) => {
|
||||
updateSetting('mailgun_base_url', value);
|
||||
updateSetting('mailgun_base_url', value || null);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
|
@ -55,8 +55,8 @@ const TimeZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
};
|
||||
});
|
||||
|
||||
const handleTimezoneChange = (value: string) => {
|
||||
updateSetting('timezone', value);
|
||||
const handleTimezoneChange = (value?: string) => {
|
||||
updateSetting('timezone', value || null);
|
||||
};
|
||||
|
||||
const viewContent = (
|
||||
|
@ -10,22 +10,62 @@ import {getSettingValues} from '../../../api/settings';
|
||||
import {useBrowseTiers} from '../../../api/tiers';
|
||||
|
||||
const MEMBERS_SIGNUP_ACCESS_OPTIONS = [
|
||||
{value: 'all', label: 'Anyone can sign up'},
|
||||
{value: 'invite', label: 'Only people I invite'},
|
||||
{value: 'none', label: 'Nobody'}
|
||||
{
|
||||
value: 'all',
|
||||
label: 'Anyone can sign up',
|
||||
hint: 'All visitors will be able to subscribe and sign in'
|
||||
},
|
||||
{
|
||||
value: 'invite',
|
||||
label: 'Only people I invite',
|
||||
hint: 'People can sign in from your site but won\'t be able to sign up'
|
||||
},
|
||||
{
|
||||
value: 'none',
|
||||
label: 'Nobody',
|
||||
hint: 'Disable all member features, including newsletters'
|
||||
}
|
||||
];
|
||||
|
||||
const DEFAULT_CONTENT_VISIBILITY_OPTIONS = [
|
||||
{value: 'public', label: 'Public'},
|
||||
{value: 'members', label: 'Members only'},
|
||||
{value: 'paid', label: 'Paid-members only'},
|
||||
{value: 'tiers', label: 'Specific tiers'}
|
||||
{
|
||||
value: 'public',
|
||||
label: 'Public',
|
||||
hint: 'All site visitors to your site, no login required'
|
||||
},
|
||||
{
|
||||
value: 'members',
|
||||
label: 'Members only',
|
||||
hint: 'All logged-in members'
|
||||
},
|
||||
{
|
||||
value: 'paid',
|
||||
label: 'Paid-members only',
|
||||
hint: 'Only logged-in members with an active Stripe subscription'
|
||||
},
|
||||
{
|
||||
value: 'tiers',
|
||||
label: 'Specific tiers',
|
||||
hint: 'Members with any of the selected tiers'
|
||||
}
|
||||
];
|
||||
|
||||
const COMMENTS_ENABLED_OPTIONS = [
|
||||
{value: 'all', label: 'All members'},
|
||||
{value: 'paid', label: 'Paid-members only'},
|
||||
{value: 'off', label: 'Nobody'}
|
||||
{
|
||||
value: 'all',
|
||||
label: 'All members',
|
||||
hint: 'Logged-in members'
|
||||
},
|
||||
{
|
||||
value: 'paid',
|
||||
label: 'Paid-members only',
|
||||
hint: 'Only logged-in members with an active Stripe subscription'
|
||||
},
|
||||
{
|
||||
value: 'off',
|
||||
label: 'Nobody',
|
||||
hint: 'Disable commenting completely'
|
||||
}
|
||||
];
|
||||
|
||||
const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
@ -98,7 +138,7 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
selectedOption={membersSignupAccess}
|
||||
title="Subscription access"
|
||||
onSelect={(value) => {
|
||||
updateSetting('members_signup_access', value);
|
||||
updateSetting('members_signup_access', value || null);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
@ -107,7 +147,7 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
selectedOption={defaultContentVisibility}
|
||||
title="Default post access"
|
||||
onSelect={(value) => {
|
||||
updateSetting('default_content_visibility', value);
|
||||
updateSetting('default_content_visibility', value || null);
|
||||
}}
|
||||
/>
|
||||
{defaultContentVisibility === 'tiers' && (
|
||||
@ -126,7 +166,7 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
selectedOption={commentsEnabled}
|
||||
title="Commenting"
|
||||
onSelect={(value) => {
|
||||
updateSetting('comments_enabled', value);
|
||||
updateSetting('comments_enabled', value || null);
|
||||
}}
|
||||
/>
|
||||
</SettingGroupContent>
|
||||
|
@ -98,10 +98,11 @@ const TipsOrDonations: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
rightPlaceholder={(
|
||||
<Select
|
||||
border={false}
|
||||
containerClassName='w-14'
|
||||
fullWidth={false}
|
||||
options={currencySelectGroups()}
|
||||
selectClassName='w-auto'
|
||||
selectedOption={donationsCurrency}
|
||||
onSelect={currency => updateSetting('donations_currency', currency)}
|
||||
onSelect={currency => updateSetting('donations_currency', currency || 'USD')}
|
||||
/>
|
||||
)}
|
||||
title='Suggested amount'
|
||||
|
@ -77,7 +77,7 @@ const LookAndFeel: React.FC<{
|
||||
]}
|
||||
selectedOption={portalButtonStyle as string}
|
||||
title='Portal button style'
|
||||
onSelect={option => updateSetting('portal_button_style', option)}
|
||||
onSelect={option => updateSetting('portal_button_style', option || null)}
|
||||
/>
|
||||
{portalButtonStyle?.toString()?.includes('icon') &&
|
||||
<div className='flex flex-col gap-2'>
|
||||
|
@ -83,7 +83,9 @@ const PortalLinks: React.FC = () => {
|
||||
options={tierOptions}
|
||||
selectedOption={selectedTier}
|
||||
onSelect={(value) => {
|
||||
setSelectedTier(value);
|
||||
if (value) {
|
||||
setSelectedTier(value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -163,8 +163,9 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
<div className='w-10'>
|
||||
<Select
|
||||
border={false}
|
||||
containerClassName='font-medium'
|
||||
controlClasses={{menu: 'w-14'}}
|
||||
options={currencySelectGroups()}
|
||||
selectClassName='font-medium'
|
||||
selectedOption={formState.currency}
|
||||
size='xs'
|
||||
onSelect={currency => updateForm(state => ({...state, currency}))}
|
||||
|
@ -49,7 +49,7 @@ const ThemeSetting: React.FC<{
|
||||
options={setting.options.map(option => ({label: option, value: option}))}
|
||||
selectedOption={setting.value}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
onSelect={value => setSetting(value)}
|
||||
onSelect={value => setSetting(value || null)}
|
||||
/>
|
||||
);
|
||||
case 'color':
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Integration, IntegrationsResponseType} from '../../../../src/api/integrations';
|
||||
import {Webhook, WebhooksResponseType} from '../../../../src/api/webhooks';
|
||||
import {chooseOptionInSelect, globalDataRequests, limitRequests, mockApi, responseFixtures} from '../../../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {globalDataRequests, limitRequests, mockApi, responseFixtures} from '../../../utils/e2e';
|
||||
|
||||
test.describe('Custom integrations', async () => {
|
||||
test('Supports creating an integration and adding webhooks', async ({page}) => {
|
||||
@ -146,10 +146,8 @@ test.describe('Custom integrations', async () => {
|
||||
|
||||
await webhookModal.getByLabel('Name').fill('My webhook');
|
||||
await webhookModal.getByLabel('Target URL').fill('https://example.com');
|
||||
await webhookModal.getByLabel('Event').selectOption('Post created');
|
||||
await chooseOptionInSelect(webhookModal.getByLabel('Event'), 'Post created');
|
||||
|
||||
// Playwright fails unless you click twice, for some reason (timing issue with validations?)
|
||||
await webhookModal.getByRole('button', {name: 'Add'}).click();
|
||||
await webhookModal.getByRole('button', {name: 'Add'}).click();
|
||||
|
||||
await expect(modal).toHaveText(/My webhook/);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {chooseOptionInSelect, globalDataRequests, mockApi, responseFixtures, updatedSettingsResponse} from '../../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {globalDataRequests, mockApi, responseFixtures, updatedSettingsResponse} from '../../utils/e2e';
|
||||
|
||||
test.describe('Default recipient settings', async () => {
|
||||
test('Supports editing default recipients', async ({page}) => {
|
||||
@ -18,7 +18,7 @@ test.describe('Default recipient settings', async () => {
|
||||
await expect(section.getByText('Whoever has access to the post')).toHaveCount(1);
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
await section.getByLabel('Default newsletter recipients').selectOption('All members');
|
||||
await chooseOptionInSelect(section.getByLabel('Default newsletter recipients'), 'All members');
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
expect(lastApiRequests.editSettings?.body).toEqual({
|
||||
@ -29,7 +29,7 @@ test.describe('Default recipient settings', async () => {
|
||||
});
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
await section.getByLabel('Default newsletter recipients').selectOption('Usually nobody');
|
||||
await chooseOptionInSelect(section.getByLabel('Default newsletter recipients'), 'Usually nobody');
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
expect(lastApiRequests.editSettings?.body).toEqual({
|
||||
@ -40,7 +40,7 @@ test.describe('Default recipient settings', async () => {
|
||||
});
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
await section.getByLabel('Default newsletter recipients').selectOption('Paid-members only');
|
||||
await chooseOptionInSelect(section.getByLabel('Default newsletter recipients'), 'Paid-members only');
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(section.getByLabel('Default newsletter recipients')).toHaveCount(0);
|
||||
@ -79,12 +79,12 @@ test.describe('Default recipient settings', async () => {
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
await section.getByLabel('Default newsletter recipients').selectOption({label: 'Specific people'});
|
||||
await chooseOptionInSelect(section.getByLabel('Default newsletter recipients'), 'Specific people');
|
||||
await section.getByLabel('Filter').click();
|
||||
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'Basic Supporter'}).click();
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'first-label'}).click();
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'First offer'}).click();
|
||||
await section.locator('[data-testid="select-option"]', {hasText: 'Basic Supporter'}).click();
|
||||
await section.locator('[data-testid="select-option"]', {hasText: 'first-label'}).click();
|
||||
await section.locator('[data-testid="select-option"]', {hasText: 'First offer'}).click();
|
||||
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {chooseOptionInSelect, globalDataRequests, limitRequests, mockApi, responseFixtures} from '../../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {globalDataRequests, limitRequests, mockApi, responseFixtures} from '../../utils/e2e';
|
||||
|
||||
test.describe('Newsletter settings', async () => {
|
||||
test('Supports creating a new newsletter', async ({page}) => {
|
||||
@ -75,7 +75,7 @@ test.describe('Newsletter settings', async () => {
|
||||
await modal.getByPlaceholder('Weekly Roundup').fill('Updated newsletter');
|
||||
|
||||
await modal.getByRole('tab', {name: 'Design'}).click();
|
||||
await modal.getByLabel('Body style').selectOption({value: 'sans_serif'});
|
||||
await chooseOptionInSelect(modal.getByLabel('Body style'), 'Clean sans-serif');
|
||||
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import {chooseOptionInSelect, globalDataRequests, mockApi, updatedSettingsResponse} from '../../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {globalDataRequests, mockApi, updatedSettingsResponse} from '../../utils/e2e';
|
||||
|
||||
test.describe('Time zone settings', async () => {
|
||||
test('Supports editing the time zone', async ({page}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
|
||||
{key: 'timezone', value: 'Asia/Tokyo'}
|
||||
{key: 'timezone', value: 'America/Anchorage'}
|
||||
])}
|
||||
}});
|
||||
|
||||
@ -18,17 +18,17 @@ test.describe('Time zone settings', async () => {
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
await section.getByLabel('Site timezone').selectOption('Asia/Tokyo');
|
||||
await chooseOptionInSelect(section.getByLabel('Site timezone'), '(GMT -9:00) Alaska');
|
||||
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
await expect(section.getByLabel('Site timezone')).toHaveCount(0);
|
||||
|
||||
await expect(section.getByText('Asia/Tokyo')).toHaveCount(1);
|
||||
await expect(section.getByText('America/Anchorage')).toHaveCount(1);
|
||||
|
||||
expect(lastApiRequests.editSettings?.body).toEqual({
|
||||
settings: [
|
||||
{key: 'timezone', value: 'Asia/Tokyo'}
|
||||
{key: 'timezone', value: 'America/Anchorage'}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {chooseOptionInSelect, globalDataRequests, mockApi, responseFixtures, updatedSettingsResponse} from '../../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {globalDataRequests, mockApi, responseFixtures, updatedSettingsResponse} from '../../utils/e2e';
|
||||
|
||||
test.describe('Access settings', async () => {
|
||||
test('Supports editing access', async ({page}) => {
|
||||
@ -22,9 +22,9 @@ test.describe('Access settings', async () => {
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
await section.getByLabel('Subscription access').selectOption({label: 'Only people I invite'});
|
||||
await section.getByLabel('Default post access').selectOption({label: 'Members only'});
|
||||
await section.getByLabel('Commenting').selectOption({label: 'All members'});
|
||||
await chooseOptionInSelect(section.getByLabel('Subscription access'), 'Only people I invite');
|
||||
await chooseOptionInSelect(section.getByLabel('Default post access'), /^Members only$/);
|
||||
await chooseOptionInSelect(section.getByLabel('Commenting'), 'All members');
|
||||
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
@ -59,11 +59,11 @@ test.describe('Access settings', async () => {
|
||||
|
||||
await section.getByRole('button', {name: 'Edit'}).click();
|
||||
|
||||
await section.getByLabel('Default post access').selectOption({label: 'Specific tiers'});
|
||||
await chooseOptionInSelect(section.getByLabel('Default post access'), 'Specific tiers');
|
||||
await section.getByLabel('Select tiers').click();
|
||||
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'Basic Supporter'}).click();
|
||||
await section.locator('[data-testid="multiselect-option"]', {hasText: 'Ultimate Starlight Diamond Tier'}).click();
|
||||
await section.locator('[data-testid="select-option"]', {hasText: 'Basic Supporter'}).click();
|
||||
await section.locator('[data-testid="select-option"]', {hasText: 'Ultimate Starlight Diamond Tier'}).click();
|
||||
|
||||
await section.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {chooseOptionInSelect, globalDataRequests, mockApi, mockSitePreview, responseFixtures} from '../../utils/e2e';
|
||||
import {expect, test} from '@playwright/test';
|
||||
import {globalDataRequests, mockApi, mockSitePreview, responseFixtures} from '../../utils/e2e';
|
||||
|
||||
test.describe('Design settings', async () => {
|
||||
test('Working with the preview', async ({page}) => {
|
||||
@ -129,7 +129,7 @@ test.describe('Design settings', async () => {
|
||||
|
||||
await modal.getByRole('tab', {name: 'Site wide'}).click();
|
||||
|
||||
await modal.getByLabel('Navigation layout').selectOption('Logo in the middle');
|
||||
await chooseOptionInSelect(modal.getByLabel('Navigation layout'), 'Logo in the middle');
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
|
||||
const expectedSettings = {navigation_layout: 'Logo in the middle'};
|
||||
|
@ -3,9 +3,9 @@ import {ConfigResponseType} from '../../src/api/config';
|
||||
import {CustomThemeSettingsResponseType} from '../../src/api/customThemeSettings';
|
||||
import {InvitesResponseType} from '../../src/api/invites';
|
||||
import {LabelsResponseType} from '../../src/api/labels';
|
||||
import {Locator, Page} from '@playwright/test';
|
||||
import {NewslettersResponseType} from '../../src/api/newsletters';
|
||||
import {OffersResponseType} from '../../src/api/offers';
|
||||
import {Page} from '@playwright/test';
|
||||
import {RolesResponseType} from '../../src/api/roles';
|
||||
import {SettingsResponseType} from '../../src/api/settings';
|
||||
import {SiteResponseType} from '../../src/api/site';
|
||||
@ -146,3 +146,8 @@ export async function mockSitePreview({page, url, response}: {page: Page, url: s
|
||||
|
||||
return lastRequest;
|
||||
}
|
||||
|
||||
export async function chooseOptionInSelect(select: Locator, optionText: string | RegExp) {
|
||||
await select.click();
|
||||
await select.page().locator('[data-testid="select-option"]', {hasText: optionText}).click();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user