diff --git a/apps/admin-x-settings/src/admin-x-ds/global/chrome/DesktopChromeHeader.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/chrome/DesktopChromeHeader.stories.tsx index ce9fd80de7..517da15bd6 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/chrome/DesktopChromeHeader.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/chrome/DesktopChromeHeader.stories.tsx @@ -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: , - toolbarCenter: { + toolbarCenter: { alert(value); }} />, toolbarRight: } -}; \ No newline at end of file +}; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/MultiSelect.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/MultiSelect.tsx index 40c3ad775c..efb854e66a 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/MultiSelect.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/MultiSelect.tsx @@ -48,7 +48,7 @@ const DropdownIndicator: React.FC> = ({children, ...optionProps}) => ( - {children} + {children} ); diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/Select.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/Select.stories.tsx index baf27fcf34..272f0f70c1 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/Select.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/Select.stories.tsx @@ -86,7 +86,7 @@ export const WithSelectedOption: Story = { export const WithCallback: Story = { args: { options: selectOptions, - onSelect: (value: string) => { + onSelect: (value) => { alert(value); } } diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/Select.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/Select.tsx index dcc7308a9f..76645cd5c7 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/Select.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/Select.tsx @@ -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 { 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 & {clearBg: boolean}> = ({clearBg, ...props}) => ( + + + +); + +const Option: React.FC> = ({children, ...optionProps}) => ( + + {children} + {optionProps.data.hint && {optionProps.data.hint}} + +); + const Select: React.FC = ({ title, hideTitle, @@ -48,25 +73,20 @@ const Select: React.FC = ({ 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) => { - 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 = ({ 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) { + return ; + }; + }, [clearBg]); + + const individualOptions = options.flatMap((option) => { + if ('options' in option) { + return option.options; + } + return option; + }); const select = ( <> {title && {title}} - - {prompt && {prompt}} - {options.map(option => ( - 'options' in option ? - - {option.options.map(child => ( - - {child.label} - - ))} - : - - {option.label} - - ))} - + + 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} + /> {hint && {hint}} > diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx index 91b06a13aa..cbc2044451 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/TextField.stories.tsx @@ -97,11 +97,12 @@ export const WithDropdown: Story = { rightPlaceholder: ( {}} /> ) diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/URLSelect.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/URLSelect.stories.tsx deleted file mode 100644 index 823d463337..0000000000 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/URLSelect.stories.tsx +++ /dev/null @@ -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; - -export default meta; -type Story = StoryObj; - -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: () => {} - } -}; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/URLSelect.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/URLSelect.tsx deleted file mode 100644 index bf9451e068..0000000000 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/URLSelect.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import Select, {SelectProps} from './Select'; -import clsx from 'clsx'; - -const URLSelect: React.FC = (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 ( - - ); -}; - -export default URLSelect; diff --git a/apps/admin-x-settings/src/admin-x-ds/global/modal/PreviewModal.tsx b/apps/admin-x-settings/src/admin-x-ds/global/modal/PreviewModal.tsx index 0fbbd02e19..1d030b2c4c 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/modal/PreviewModal.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/modal/PreviewModal.tsx @@ -108,7 +108,7 @@ export const PreviewModalContent: React.FC = ({ let toolbarLeft = (<>>); if (previewToolbarURLs) { toolbarLeft = ( - + url && onSelectURL?.(url)} /> ); } else if (previewToolbarTabs) { toolbarLeft = = ({keywords}) => { selectedOption={selectedOption} title="Default Newsletter recipients" onSelect={(value) => { - setDefaultRecipientValue(value); + if (value) { + setDefaultRecipientValue(value); + } }} /> {(selectedOption === 'segment') && ( diff --git a/apps/admin-x-settings/src/components/settings/email/Mailgun.tsx b/apps/admin-x-settings/src/components/settings/email/Mailgun.tsx index ab6a2ff037..7d2760b0cb 100644 --- a/apps/admin-x-settings/src/components/settings/email/Mailgun.tsx +++ b/apps/admin-x-settings/src/components/settings/email/Mailgun.tsx @@ -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); }} /> = ({keywords}) => { }; }); - const handleTimezoneChange = (value: string) => { - updateSetting('timezone', value); + const handleTimezoneChange = (value?: string) => { + updateSetting('timezone', value || null); }; const viewContent = ( diff --git a/apps/admin-x-settings/src/components/settings/membership/Access.tsx b/apps/admin-x-settings/src/components/settings/membership/Access.tsx index e41c4ca978..a2cf886ea5 100644 --- a/apps/admin-x-settings/src/components/settings/membership/Access.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/Access.tsx @@ -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); }} /> = ({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); }} /> diff --git a/apps/admin-x-settings/src/components/settings/membership/TipsOrDonations.tsx b/apps/admin-x-settings/src/components/settings/membership/TipsOrDonations.tsx index 00750b1629..9b7ef55321 100644 --- a/apps/admin-x-settings/src/components/settings/membership/TipsOrDonations.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/TipsOrDonations.tsx @@ -98,10 +98,11 @@ const TipsOrDonations: React.FC<{ keywords: string[] }> = ({keywords}) => { rightPlaceholder={( updateSetting('donations_currency', currency)} + onSelect={currency => updateSetting('donations_currency', currency || 'USD')} /> )} title='Suggested amount' diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/LookAndFeel.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/LookAndFeel.tsx index 8d638828f9..3f573135e8 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/LookAndFeel.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/LookAndFeel.tsx @@ -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') && diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/PortalLinks.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/PortalLinks.tsx index 053c5c0368..8c992d8908 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/PortalLinks.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/PortalLinks.tsx @@ -83,7 +83,9 @@ const PortalLinks: React.FC = () => { options={tierOptions} selectedOption={selectedTier} onSelect={(value) => { - setSelectedTier(value); + if (value) { + setSelectedTier(value); + } }} /> diff --git a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx index 16be05fa75..26e03dfb77 100644 --- a/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/tiers/TierDetailModal.tsx @@ -163,8 +163,9 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => { updateForm(state => ({...state, currency}))} diff --git a/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemeSettings.tsx b/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemeSettings.tsx index 85d7fe3cbb..7ae511c303 100644 --- a/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemeSettings.tsx +++ b/apps/admin-x-settings/src/components/settings/site/designAndBranding/ThemeSettings.tsx @@ -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': diff --git a/apps/admin-x-settings/test/e2e/advanced/integrations/custom.test.ts b/apps/admin-x-settings/test/e2e/advanced/integrations/custom.test.ts index 8952ab3a6b..56ed9d75f6 100644 --- a/apps/admin-x-settings/test/e2e/advanced/integrations/custom.test.ts +++ b/apps/admin-x-settings/test/e2e/advanced/integrations/custom.test.ts @@ -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/); diff --git a/apps/admin-x-settings/test/e2e/email/defaultRecipients.test.ts b/apps/admin-x-settings/test/e2e/email/defaultRecipients.test.ts index 27a9681467..df3e402746 100644 --- a/apps/admin-x-settings/test/e2e/email/defaultRecipients.test.ts +++ b/apps/admin-x-settings/test/e2e/email/defaultRecipients.test.ts @@ -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(); diff --git a/apps/admin-x-settings/test/e2e/email/newsletters.test.ts b/apps/admin-x-settings/test/e2e/email/newsletters.test.ts index bd553651de..af0504ad7c 100644 --- a/apps/admin-x-settings/test/e2e/email/newsletters.test.ts +++ b/apps/admin-x-settings/test/e2e/email/newsletters.test.ts @@ -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(); diff --git a/apps/admin-x-settings/test/e2e/general/timeZone.test.ts b/apps/admin-x-settings/test/e2e/general/timeZone.test.ts index 8cabfbc03b..fe1599f3e9 100644 --- a/apps/admin-x-settings/test/e2e/general/timeZone.test.ts +++ b/apps/admin-x-settings/test/e2e/general/timeZone.test.ts @@ -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'} ] }); }); diff --git a/apps/admin-x-settings/test/e2e/membership/access.test.ts b/apps/admin-x-settings/test/e2e/membership/access.test.ts index d60bb8cf61..85846acc25 100644 --- a/apps/admin-x-settings/test/e2e/membership/access.test.ts +++ b/apps/admin-x-settings/test/e2e/membership/access.test.ts @@ -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(); diff --git a/apps/admin-x-settings/test/e2e/site/design.test.ts b/apps/admin-x-settings/test/e2e/site/design.test.ts index fa98dd5216..6ee62c012d 100644 --- a/apps/admin-x-settings/test/e2e/site/design.test.ts +++ b/apps/admin-x-settings/test/e2e/site/design.test.ts @@ -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'}; diff --git a/apps/admin-x-settings/test/utils/e2e.ts b/apps/admin-x-settings/test/utils/e2e.ts index 74d2dd6494..56bfab1eff 100644 --- a/apps/admin-x-settings/test/utils/e2e.ts +++ b/apps/admin-x-settings/test/utils/e2e.ts @@ -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(); +}