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:
Jono M 2023-09-18 14:51:59 +01:00 committed by GitHub
parent 353c565739
commit 343535116c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 203 additions and 170 deletions

View File

@ -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 = {
]}
/>
}
};
};

View File

@ -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>
);

View File

@ -86,7 +86,7 @@ export const WithSelectedOption: Story = {
export const WithCallback: Story = {
args: {
options: selectOptions,
onSelect: (value: string) => {
onSelect: (value) => {
alert(value);
}
}

View File

@ -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>}
</>

View File

@ -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={() => {}}
/>
)

View File

@ -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: () => {}
}
};

View File

@ -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;

View File

@ -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

View File

@ -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') && (

View File

@ -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

View File

@ -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 = (

View File

@ -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>

View File

@ -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'

View File

@ -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'>

View File

@ -83,7 +83,9 @@ const PortalLinks: React.FC = () => {
options={tierOptions}
selectedOption={selectedTier}
onSelect={(value) => {
setSelectedTier(value);
if (value) {
setSelectedTier(value);
}
}}
/>
</div>

View File

@ -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}))}

View File

@ -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':

View File

@ -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/);

View File

@ -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();

View File

@ -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();

View File

@ -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'}
]
});
});

View File

@ -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();

View File

@ -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'};

View File

@ -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();
}