mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 21:33:24 +03:00
Refactored limit=all queries to be paginated in AdminX (#18324)
refs https://github.com/TryGhost/Product/issues/3832 --- ### <samp>🤖 Generated by Copilot at 0095d23</samp> The pull request adds support for asynchronous and creatable select inputs in various components, using the `react-select` and `@tanstack/react-query` libraries. It also adds pagination features to the newsletters and tiers lists, using a `Button` component and infinite queries. It refactors and fixes the type and null handling of the select inputs and their options, using the `SelectOption` type and the `useFilterableApi` and `debounce` hooks. It removes some unnecessary props from the browse queries, and adds a new custom hook `useDefaultRecipientsOptions` for the default recipients component. It updates the stories and modals to use the new select inputs and options.
This commit is contained in:
parent
c4773b946b
commit
0e35baaf01
@ -1,9 +1,11 @@
|
|||||||
|
import AsyncCreatableSelect from 'react-select/async-creatable';
|
||||||
|
import AsyncSelect from 'react-select/async';
|
||||||
import CreatableSelect from 'react-select/creatable';
|
import CreatableSelect from 'react-select/creatable';
|
||||||
import Heading from '../Heading';
|
import Heading from '../Heading';
|
||||||
import Hint from '../Hint';
|
import Hint from '../Hint';
|
||||||
import React, {useId, useMemo} from 'react';
|
import React, {useId, useMemo} from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {DropdownIndicatorProps, GroupBase, MultiValue, OptionProps, OptionsOrGroups, default as ReactSelect, components} from 'react-select';
|
import {DropdownIndicatorProps, GroupBase, MultiValue, OptionProps, OptionsOrGroups, Props, default as ReactSelect, components} from 'react-select';
|
||||||
|
|
||||||
export type MultiSelectColor = 'grey' | 'black' | 'green' | 'pink';
|
export type MultiSelectColor = 'grey' | 'black' | 'green' | 'pink';
|
||||||
type FieldStyles = 'text' | 'dropdown';
|
type FieldStyles = 'text' | 'dropdown';
|
||||||
@ -14,9 +16,22 @@ export type MultiSelectOption = {
|
|||||||
color?: MultiSelectColor;
|
color?: MultiSelectColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MultiSelectProps {
|
export type LoadOptions = (inputValue: string, callback: (options: OptionsOrGroups<MultiSelectOption, GroupBase<MultiSelectOption>>) => void) => void
|
||||||
|
|
||||||
|
type MultiSelectOptionProps = {
|
||||||
|
async: true;
|
||||||
|
defaultOptions: boolean | OptionsOrGroups<MultiSelectOption, GroupBase<MultiSelectOption>>;
|
||||||
|
loadOptions: LoadOptions;
|
||||||
|
options?: never;
|
||||||
|
} | {
|
||||||
|
async?: false;
|
||||||
options: OptionsOrGroups<MultiSelectOption, GroupBase<MultiSelectOption>>;
|
options: OptionsOrGroups<MultiSelectOption, GroupBase<MultiSelectOption>>;
|
||||||
values: MultiSelectOption[];
|
defaultOptions?: never;
|
||||||
|
loadOptions?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MultiSelectProps = MultiSelectOptionProps & {
|
||||||
|
values: MultiValue<MultiSelectOption>;
|
||||||
title?: string;
|
title?: string;
|
||||||
clearBg?: boolean;
|
clearBg?: boolean;
|
||||||
error?: boolean;
|
error?: boolean;
|
||||||
@ -70,7 +85,10 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
fieldStyle = 'dropdown',
|
fieldStyle = 'dropdown',
|
||||||
hint = '',
|
hint = '',
|
||||||
|
async,
|
||||||
options,
|
options,
|
||||||
|
defaultOptions,
|
||||||
|
loadOptions,
|
||||||
values,
|
values,
|
||||||
onChange,
|
onChange,
|
||||||
canCreate = false,
|
canCreate = false,
|
||||||
@ -110,60 +128,37 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
return (ddiProps: DropdownIndicatorProps<MultiSelectOption, true>) => <DropdownIndicator {...ddiProps} clearBg={clearBg} fieldStyle={fieldStyle} />;
|
return (ddiProps: DropdownIndicatorProps<MultiSelectOption, true>) => <DropdownIndicator {...ddiProps} clearBg={clearBg} fieldStyle={fieldStyle} />;
|
||||||
}, [clearBg, fieldStyle]);
|
}, [clearBg, fieldStyle]);
|
||||||
|
|
||||||
|
const commonOptions: Props<MultiSelectOption, true> = {
|
||||||
|
classNames: {
|
||||||
|
menuList: () => 'z-50',
|
||||||
|
valueContainer: () => customClasses.valueContainer,
|
||||||
|
control: () => customClasses.control,
|
||||||
|
placeholder: () => customClasses.placeHolder,
|
||||||
|
menu: () => customClasses.menu,
|
||||||
|
option: () => customClasses.option,
|
||||||
|
multiValue: ({data}) => customClasses.multiValue(data.color),
|
||||||
|
noOptionsMessage: () => customClasses.noOptionsMessage,
|
||||||
|
groupHeading: () => customClasses.groupHeading
|
||||||
|
},
|
||||||
|
closeMenuOnSelect: false,
|
||||||
|
components: {DropdownIndicator: dropdownIndicatorComponent, Option},
|
||||||
|
inputId: id,
|
||||||
|
isClearable: false,
|
||||||
|
placeholder: placeholder ? placeholder : '',
|
||||||
|
value: values,
|
||||||
|
isMulti: true,
|
||||||
|
unstyled: true,
|
||||||
|
onChange,
|
||||||
|
...props
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col'>
|
<div className='flex flex-col'>
|
||||||
{title && <Heading htmlFor={id} grey useLabelTag>{title}</Heading>}
|
{title && <Heading htmlFor={id} grey useLabelTag>{title}</Heading>}
|
||||||
{
|
{
|
||||||
canCreate ?
|
async ?
|
||||||
<CreatableSelect
|
(canCreate ? <AsyncCreatableSelect {...commonOptions} defaultOptions={defaultOptions} loadOptions={loadOptions} /> : <AsyncSelect {...commonOptions} defaultOptions={defaultOptions} loadOptions={loadOptions} />) :
|
||||||
classNames={{
|
(canCreate ? <CreatableSelect {...commonOptions} options={options} /> : <ReactSelect {...commonOptions} options={options} />)
|
||||||
menuList: () => 'z-50',
|
|
||||||
valueContainer: () => customClasses.valueContainer,
|
|
||||||
control: () => customClasses.control,
|
|
||||||
placeholder: () => customClasses.placeHolder,
|
|
||||||
menu: () => customClasses.menu,
|
|
||||||
option: () => customClasses.option,
|
|
||||||
multiValue: ({data}) => customClasses.multiValue(data.color),
|
|
||||||
noOptionsMessage: () => customClasses.noOptionsMessage,
|
|
||||||
groupHeading: () => customClasses.groupHeading
|
|
||||||
}}
|
|
||||||
closeMenuOnSelect={false}
|
|
||||||
components={{DropdownIndicator: dropdownIndicatorComponent, Option}}
|
|
||||||
inputId={id}
|
|
||||||
isClearable={false}
|
|
||||||
options={options}
|
|
||||||
placeholder={placeholder ? placeholder : ''}
|
|
||||||
value={values}
|
|
||||||
isMulti
|
|
||||||
unstyled
|
|
||||||
onChange={onChange}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<ReactSelect
|
|
||||||
classNames={{
|
|
||||||
menuList: () => 'z-50',
|
|
||||||
valueContainer: () => customClasses.valueContainer,
|
|
||||||
control: () => customClasses.control,
|
|
||||||
placeholder: () => customClasses.placeHolder,
|
|
||||||
menu: () => customClasses.menu,
|
|
||||||
option: () => customClasses.option,
|
|
||||||
multiValue: ({data}) => customClasses.multiValue(data.color),
|
|
||||||
noOptionsMessage: () => customClasses.noOptionsMessage,
|
|
||||||
groupHeading: () => customClasses.groupHeading
|
|
||||||
}}
|
|
||||||
closeMenuOnSelect={false}
|
|
||||||
components={{DropdownIndicator: dropdownIndicatorComponent, Option}}
|
|
||||||
inputId={id}
|
|
||||||
isClearable={false}
|
|
||||||
options={options}
|
|
||||||
placeholder={placeholder ? placeholder : ''}
|
|
||||||
value={values}
|
|
||||||
isMulti
|
|
||||||
unstyled
|
|
||||||
onChange={onChange}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,7 +78,7 @@ export const WithSelectedOption: Story = {
|
|||||||
args: {
|
args: {
|
||||||
title: 'Title',
|
title: 'Title',
|
||||||
options: selectOptions,
|
options: selectOptions,
|
||||||
selectedOption: 'option-3',
|
selectedOption: selectOptions.find(option => option.value === 'option-3'),
|
||||||
hint: 'Here\'s some hint'
|
hint: 'Here\'s some hint'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, {useId, useMemo} from 'react';
|
import AsyncSelect from 'react-select/async';
|
||||||
import ReactSelect, {ClearIndicatorProps, DropdownIndicatorProps, OptionProps, Props, components} from 'react-select';
|
|
||||||
|
|
||||||
import Heading from '../Heading';
|
import Heading from '../Heading';
|
||||||
import Hint from '../Hint';
|
import Hint from '../Hint';
|
||||||
import Icon from '../Icon';
|
import Icon from '../Icon';
|
||||||
|
import React, {useId, useMemo} from 'react';
|
||||||
|
import ReactSelect, {ClearIndicatorProps, DropdownIndicatorProps, GroupBase, OptionProps, OptionsOrGroups, Props, components} from 'react-select';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export interface SelectOption {
|
export interface SelectOption {
|
||||||
@ -31,14 +31,28 @@ export interface SelectControlClasses {
|
|||||||
clearIndicator?: string;
|
clearIndicator?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectProps extends Props<SelectOption, false> {
|
export type LoadOptions = (inputValue: string, callback: (options: OptionsOrGroups<SelectOption, GroupBase<SelectOption>>) => void) => void
|
||||||
|
|
||||||
|
type SelectOptionProps = {
|
||||||
|
async: true;
|
||||||
|
defaultOptions: boolean | OptionsOrGroups<SelectOption, GroupBase<SelectOption>>;
|
||||||
|
loadOptions: LoadOptions;
|
||||||
|
options?: never;
|
||||||
|
} | {
|
||||||
|
async?: false;
|
||||||
|
options: OptionsOrGroups<SelectOption, GroupBase<SelectOption>>;
|
||||||
|
defaultOptions?: never;
|
||||||
|
loadOptions?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectProps = Props<SelectOption, false> & SelectOptionProps & {
|
||||||
|
async?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
hideTitle?: boolean;
|
hideTitle?: boolean;
|
||||||
size?: 'xs' | 'md';
|
size?: 'xs' | 'md';
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
options: SelectOption[] | SelectOptionGroup[];
|
selectedOption?: SelectOption
|
||||||
selectedOption?: string
|
onSelect: (option: SelectOption | null) => void;
|
||||||
onSelect: (value: string | undefined) => void;
|
|
||||||
error?:boolean;
|
error?:boolean;
|
||||||
hint?: React.ReactNode;
|
hint?: React.ReactNode;
|
||||||
clearBg?: boolean;
|
clearBg?: boolean;
|
||||||
@ -71,6 +85,7 @@ const Option: React.FC<OptionProps<SelectOption, false>> = ({children, ...option
|
|||||||
);
|
);
|
||||||
|
|
||||||
const Select: React.FC<SelectProps> = ({
|
const Select: React.FC<SelectProps> = ({
|
||||||
|
async,
|
||||||
title,
|
title,
|
||||||
hideTitle,
|
hideTitle,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
@ -133,19 +148,8 @@ const Select: React.FC<SelectProps> = ({
|
|||||||
};
|
};
|
||||||
}, [clearBg]);
|
}, [clearBg]);
|
||||||
|
|
||||||
const individualOptions = options.flatMap((option) => {
|
const customProps = {
|
||||||
if ('options' in option) {
|
classNames: {
|
||||||
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}>
|
|
||||||
<ReactSelect<SelectOption, false>
|
|
||||||
classNames={{
|
|
||||||
menuList: () => 'z-[300]',
|
menuList: () => 'z-[300]',
|
||||||
valueContainer: () => customClasses.valueContainer,
|
valueContainer: () => customClasses.valueContainer,
|
||||||
control: () => customClasses.control,
|
control: () => customClasses.control,
|
||||||
@ -155,17 +159,25 @@ const Select: React.FC<SelectProps> = ({
|
|||||||
noOptionsMessage: () => customClasses.noOptionsMessage,
|
noOptionsMessage: () => customClasses.noOptionsMessage,
|
||||||
groupHeading: () => customClasses.groupHeading,
|
groupHeading: () => customClasses.groupHeading,
|
||||||
clearIndicator: () => customClasses.clearIndicator
|
clearIndicator: () => customClasses.clearIndicator
|
||||||
}}
|
},
|
||||||
components={{DropdownIndicator: dropdownIndicatorComponent, Option, ClearIndicator}}
|
components: {DropdownIndicator: dropdownIndicatorComponent, Option, ClearIndicator},
|
||||||
inputId={id}
|
inputId: id,
|
||||||
isClearable={false}
|
isClearable: false,
|
||||||
options={options}
|
options: options,
|
||||||
placeholder={prompt ? prompt : ''}
|
placeholder: prompt ? prompt : '',
|
||||||
value={individualOptions.find(option => option.value === selectedOption)}
|
value: selectedOption,
|
||||||
unstyled
|
unstyled,
|
||||||
onChange={option => onSelect(option?.value)}
|
onChange: onSelect
|
||||||
{...props}
|
};
|
||||||
/>
|
|
||||||
|
const select = (
|
||||||
|
<>
|
||||||
|
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={selectedOption || !prompt ? true : false} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||||
|
<div className={containerClasses}>
|
||||||
|
{async ?
|
||||||
|
<AsyncSelect<SelectOption, false> {...customProps} {...props} /> :
|
||||||
|
<ReactSelect<SelectOption, false> {...customProps} {...props} />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
||||||
</>
|
</>
|
||||||
|
@ -108,7 +108,11 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
|||||||
let toolbarLeft = (<></>);
|
let toolbarLeft = (<></>);
|
||||||
if (previewToolbarURLs) {
|
if (previewToolbarURLs) {
|
||||||
toolbarLeft = (
|
toolbarLeft = (
|
||||||
<Select options={previewToolbarURLs!} selectedOption={selectedURL} onSelect={url => url && onSelectURL?.(url)} />
|
<Select
|
||||||
|
options={previewToolbarURLs!}
|
||||||
|
selectedOption={previewToolbarURLs!.find(option => option.value === selectedURL)}
|
||||||
|
onSelect={option => option && onSelectURL?.(option.value)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else if (previewToolbarTabs) {
|
} else if (previewToolbarTabs) {
|
||||||
toolbarLeft = <TabView
|
toolbarLeft = <TabView
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {ExternalLink, InternalLink} from '../components/providers/RoutingProvider';
|
import {ExternalLink, InternalLink} from '../components/providers/RoutingProvider';
|
||||||
import {InfiniteData} from '@tanstack/react-query';
|
import {InfiniteData} from '@tanstack/react-query';
|
||||||
import {JSONObject} from './config';
|
import {JSONObject} from './config';
|
||||||
import {Meta, createInfiniteQuery} from '../utils/apiRequests';
|
import {Meta, createInfiniteQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
||||||
import {createMutation} from '../utils/apiRequests';
|
import {createMutation} from '../utils/api/hooks';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createQuery} from '../utils/apiRequests';
|
import {createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type JSONValue = string|number|boolean|null|Date|JSONObject|JSONArray;
|
export type JSONValue = string|number|boolean|null|Date|JSONObject|JSONArray;
|
||||||
export interface JSONObject { [key: string]: JSONValue }
|
export interface JSONObject { [key: string]: JSONValue }
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Setting} from './settings';
|
import {Setting} from './settings';
|
||||||
import {createMutation, createQuery} from '../utils/apiRequests';
|
import {createMutation, createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
type CustomThemeSettingData =
|
type CustomThemeSettingData =
|
||||||
{ type: 'text', value: string | null, default: string | null } |
|
{ type: 'text', value: string | null, default: string | null } |
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createMutation} from '../utils/apiRequests';
|
import {createMutation} from '../utils/api/hooks';
|
||||||
import {downloadFromEndpoint} from '../utils/helpers';
|
import {downloadFromEndpoint} from '../utils/helpers';
|
||||||
|
|
||||||
export const useImportContent = createMutation<unknown, File>({
|
export const useImportContent = createMutation<unknown, File>({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createMutation} from '../utils/apiRequests';
|
import {Meta, createMutation} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type emailVerification = {
|
export type emailVerification = {
|
||||||
token: string;
|
token: string;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {useFetchApi} from '../utils/apiRequests';
|
import {useFetchApi} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type GhostSiteResponse = {
|
export type GhostSiteResponse = {
|
||||||
site: {
|
site: {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createMutation} from '../utils/apiRequests';
|
import {createMutation} from '../utils/api/hooks';
|
||||||
|
|
||||||
export interface FilesResponseType {
|
export interface FilesResponseType {
|
||||||
files: {
|
files: {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createMutation} from '../utils/apiRequests';
|
import {createMutation} from '../utils/api/hooks';
|
||||||
|
|
||||||
export interface ImagesResponseType {
|
export interface ImagesResponseType {
|
||||||
images: {
|
images: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {APIKey} from './apiKeys';
|
import {APIKey} from './apiKeys';
|
||||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
|
||||||
import {Webhook} from './webhooks';
|
import {Webhook} from './webhooks';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export interface UserInvite {
|
export interface UserInvite {
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createQuery} from '../utils/apiRequests';
|
import {Meta, createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type Label = {
|
export type Label = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -17,6 +17,5 @@ const dataType = 'LabelsResponseType';
|
|||||||
|
|
||||||
export const useBrowseLabels = createQuery<LabelsResponseType>({
|
export const useBrowseLabels = createQuery<LabelsResponseType>({
|
||||||
dataType,
|
dataType,
|
||||||
path: '/labels/',
|
path: '/labels/'
|
||||||
defaultSearchParams: {limit: 'all'}
|
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createQuery} from '../utils/apiRequests';
|
import {Meta, createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type Member = {
|
export type Member = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createPaginatedQuery} from '../utils/apiRequests';
|
import {Meta, createPaginatedQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type Mention = {
|
export type Mention = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
import {InfiniteData} from '@tanstack/react-query';
|
||||||
|
import {Meta, createInfiniteQuery, createMutation} from '../utils/api/hooks';
|
||||||
|
import {insertToQueryCache, updateQueryCache} from '../utils/api/updateQueries';
|
||||||
|
|
||||||
export type Newsletter = {
|
export type Newsletter = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -46,10 +48,24 @@ export interface NewslettersResponseType {
|
|||||||
|
|
||||||
const dataType = 'NewslettersResponseType';
|
const dataType = 'NewslettersResponseType';
|
||||||
|
|
||||||
export const useBrowseNewsletters = createQuery<NewslettersResponseType>({
|
export const useBrowseNewsletters = createInfiniteQuery<NewslettersResponseType & {isEnd: boolean}>({
|
||||||
dataType,
|
dataType,
|
||||||
path: '/newsletters/',
|
path: '/newsletters/',
|
||||||
defaultSearchParams: {include: 'count.active_members,count.posts', limit: 'all'}
|
defaultSearchParams: {include: 'count.active_members,count.posts', limit: '20'},
|
||||||
|
defaultNextPageParams: (lastPage, otherParams) => ({
|
||||||
|
...otherParams,
|
||||||
|
page: (lastPage.meta?.pagination.next || 1).toString()
|
||||||
|
}),
|
||||||
|
returnData: (originalData) => {
|
||||||
|
const {pages} = originalData as InfiniteData<NewslettersResponseType>;
|
||||||
|
const newsletters = pages.flatMap(page => page.newsletters);
|
||||||
|
|
||||||
|
return {
|
||||||
|
newsletters: newsletters,
|
||||||
|
meta: pages.at(-1)!.meta,
|
||||||
|
isEnd: pages.at(-1)!.newsletters.length < (pages.at(-1)!.meta?.pagination.limit || 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAddNewsletter = createMutation<NewslettersResponseType, Partial<Newsletter> & {opt_in_existing: boolean}>({
|
export const useAddNewsletter = createMutation<NewslettersResponseType, Partial<Newsletter> & {opt_in_existing: boolean}>({
|
||||||
@ -60,10 +76,7 @@ export const useAddNewsletter = createMutation<NewslettersResponseType, Partial<
|
|||||||
searchParams: payload => ({opt_in_existing: payload.opt_in_existing.toString(), include: 'count.active_members,count.posts'}),
|
searchParams: payload => ({opt_in_existing: payload.opt_in_existing.toString(), include: 'count.active_members,count.posts'}),
|
||||||
updateQueries: {
|
updateQueries: {
|
||||||
dataType,
|
dataType,
|
||||||
update: (newData, currentData) => (currentData && {
|
update: insertToQueryCache('newsletters')
|
||||||
...(currentData as NewslettersResponseType),
|
|
||||||
newsletters: (currentData as NewslettersResponseType).newsletters.concat(newData.newsletters)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -78,12 +91,6 @@ export const useEditNewsletter = createMutation<NewslettersEditResponseType, New
|
|||||||
defaultSearchParams: {include: 'count.active_members,count.posts'},
|
defaultSearchParams: {include: 'count.active_members,count.posts'},
|
||||||
updateQueries: {
|
updateQueries: {
|
||||||
dataType,
|
dataType,
|
||||||
update: (newData, currentData) => (currentData && {
|
update: updateQueryCache('newsletters')
|
||||||
...(currentData as NewslettersResponseType),
|
|
||||||
newsletters: (currentData as NewslettersResponseType).newsletters.map((newsletter) => {
|
|
||||||
const newNewsletter = newData.newsletters.find(({id}) => id === newsletter.id);
|
|
||||||
return newNewsletter || newsletter;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {apiUrl, useFetchApi} from '../utils/apiRequests';
|
import {apiUrl, useFetchApi} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type OembedResponse = {
|
export type OembedResponse = {
|
||||||
metadata: {
|
metadata: {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createQuery} from '../utils/apiRequests';
|
import {Meta, createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type Offer = {
|
export type Offer = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -30,6 +30,5 @@ const dataType = 'OffersResponseType';
|
|||||||
|
|
||||||
export const useBrowseOffers = createQuery<OffersResponseType>({
|
export const useBrowseOffers = createQuery<OffersResponseType>({
|
||||||
dataType,
|
dataType,
|
||||||
path: '/offers/',
|
path: '/offers/'
|
||||||
defaultSearchParams: {limit: 'all'}
|
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createQuery} from '../utils/apiRequests';
|
import {Meta, createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type Post = {
|
export type Post = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {InfiniteData} from '@tanstack/react-query';
|
import {InfiniteData} from '@tanstack/react-query';
|
||||||
import {Meta, apiUrl, createInfiniteQuery, createMutation, useFetchApi} from '../utils/apiRequests';
|
import {Meta, apiUrl, createInfiniteQuery, createMutation, useFetchApi} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type Recommendation = {
|
export type Recommendation = {
|
||||||
id: string
|
id: string
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createMutation} from '../utils/apiRequests';
|
import {createMutation} from '../utils/api/hooks';
|
||||||
import {downloadFromEndpoint} from '../utils/helpers';
|
import {downloadFromEndpoint} from '../utils/helpers';
|
||||||
|
|
||||||
export const useUploadRedirects = createMutation<unknown, File>({
|
export const useUploadRedirects = createMutation<unknown, File>({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createQuery} from '../utils/apiRequests';
|
import {createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type ReferrerHistoryItem = {
|
export type ReferrerHistoryItem = {
|
||||||
date: string,
|
date: string,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createQuery} from '../utils/apiRequests';
|
import {Meta, createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type UserRoleType = 'Owner' | 'Administrator' | 'Editor' | 'Author' | 'Contributor';
|
export type UserRoleType = 'Owner' | 'Administrator' | 'Editor' | 'Author' | 'Contributor';
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createMutation} from '../utils/apiRequests';
|
import {createMutation} from '../utils/api/hooks';
|
||||||
import {downloadFromEndpoint} from '../utils/helpers';
|
import {downloadFromEndpoint} from '../utils/helpers';
|
||||||
|
|
||||||
export const useUploadRoutes = createMutation<unknown, File>({
|
export const useUploadRoutes = createMutation<unknown, File>({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Config} from './config';
|
import {Config} from './config';
|
||||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createQuery} from '../utils/apiRequests';
|
import {createQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createMutation} from '../utils/apiRequests';
|
import {createMutation} from '../utils/api/hooks';
|
||||||
|
|
||||||
export const useTestSlack = createMutation<unknown, null>({
|
export const useTestSlack = createMutation<unknown, null>({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta, createMutation, createPaginatedQuery} from '../utils/apiRequests';
|
import {Meta, createMutation, createPaginatedQuery} from '../utils/api/hooks';
|
||||||
|
|
||||||
export type staffToken = {
|
export type staffToken = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {createMutation, createQuery} from '../utils/apiRequests';
|
import {createMutation, createQuery} from '../utils/api/hooks';
|
||||||
import {customThemeSettingsDataType} from './customThemeSettings';
|
import {customThemeSettingsDataType} from './customThemeSettings';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
import {InfiniteData} from '@tanstack/react-query';
|
||||||
|
import {Meta, createInfiniteQuery, createMutation} from '../utils/api/hooks';
|
||||||
|
import {updateQueryCache} from '../utils/api/updateQueries';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
@ -29,11 +31,23 @@ export interface TiersResponseType {
|
|||||||
|
|
||||||
const dataType = 'TiersResponseType';
|
const dataType = 'TiersResponseType';
|
||||||
|
|
||||||
export const useBrowseTiers = createQuery<TiersResponseType>({
|
export const useBrowseTiers = createInfiniteQuery<TiersResponseType & {isEnd: boolean}>({
|
||||||
dataType,
|
dataType,
|
||||||
path: '/tiers/',
|
path: '/tiers/',
|
||||||
defaultSearchParams: {
|
defaultSearchParams: {limit: '20'},
|
||||||
limit: 'all'
|
defaultNextPageParams: (lastPage, otherParams) => ({
|
||||||
|
...otherParams,
|
||||||
|
page: (lastPage.meta?.pagination.next || 1).toString()
|
||||||
|
}),
|
||||||
|
returnData: (originalData) => {
|
||||||
|
const {pages} = originalData as InfiniteData<TiersResponseType>;
|
||||||
|
const tiers = pages.flatMap(page => page.tiers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tiers,
|
||||||
|
meta: pages.at(-1)!.meta,
|
||||||
|
isEnd: pages.at(-1)!.tiers.length < (pages.at(-1)!.meta?.pagination.limit || 0)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,13 +65,7 @@ export const useEditTier = createMutation<TiersResponseType, Tier>({
|
|||||||
body: tier => ({tiers: [tier]}),
|
body: tier => ({tiers: [tier]}),
|
||||||
updateQueries: {
|
updateQueries: {
|
||||||
dataType,
|
dataType,
|
||||||
update: (newData, currentData) => (currentData && {
|
update: updateQueryCache('tiers')
|
||||||
...(currentData as TiersResponseType),
|
|
||||||
tiers: (currentData as TiersResponseType).tiers.map((tier) => {
|
|
||||||
const newTier = newData.tiers.find(({id}) => id === tier.id);
|
|
||||||
return newTier || tier;
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
import {InfiniteData} from '@tanstack/react-query';
|
||||||
|
import {Meta, createInfiniteQuery, createMutation, createQuery} from '../utils/api/hooks';
|
||||||
import {UserRole} from './roles';
|
import {UserRole} from './roles';
|
||||||
|
import {deleteFromQueryCache, updateQueryCache} from '../utils/api/updateQueries';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
@ -62,18 +64,24 @@ export interface DeleteUserResponse {
|
|||||||
|
|
||||||
const dataType = 'UsersResponseType';
|
const dataType = 'UsersResponseType';
|
||||||
|
|
||||||
const updateUsers = (newData: UsersResponseType, currentData: unknown) => ({
|
export const useBrowseUsers = createInfiniteQuery<UsersResponseType & {isEnd: boolean}>({
|
||||||
...(currentData as UsersResponseType),
|
|
||||||
users: (currentData as UsersResponseType).users.map((user) => {
|
|
||||||
const newUser = newData.users.find(({id}) => id === user.id);
|
|
||||||
return newUser || user;
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
export const useBrowseUsers = createQuery<UsersResponseType>({
|
|
||||||
dataType,
|
dataType,
|
||||||
path: '/users/',
|
path: '/users/',
|
||||||
defaultSearchParams: {limit: 'all', include: 'roles'}
|
defaultSearchParams: {limit: '100', include: 'roles'},
|
||||||
|
defaultNextPageParams: (lastPage, otherParams) => ({
|
||||||
|
...otherParams,
|
||||||
|
page: (lastPage.meta?.pagination.next || 1).toString()
|
||||||
|
}),
|
||||||
|
returnData: (originalData) => {
|
||||||
|
const {pages} = originalData as InfiniteData<UsersResponseType>;
|
||||||
|
const users = pages.flatMap(page => page.users);
|
||||||
|
|
||||||
|
return {
|
||||||
|
users: users,
|
||||||
|
meta: pages.at(-1)!.meta,
|
||||||
|
isEnd: pages.at(-1)!.users.length < (pages.at(-1)!.meta?.pagination.limit || 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useCurrentUser = createQuery<User>({
|
export const useCurrentUser = createQuery<User>({
|
||||||
@ -90,7 +98,7 @@ export const useEditUser = createMutation<UsersResponseType, User>({
|
|||||||
searchParams: () => ({include: 'roles'}),
|
searchParams: () => ({include: 'roles'}),
|
||||||
updateQueries: {
|
updateQueries: {
|
||||||
dataType,
|
dataType,
|
||||||
update: updateUsers
|
update: updateQueryCache('users')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,10 +107,7 @@ export const useDeleteUser = createMutation<DeleteUserResponse, string>({
|
|||||||
path: id => `/users/${id}/`,
|
path: id => `/users/${id}/`,
|
||||||
updateQueries: {
|
updateQueries: {
|
||||||
dataType,
|
dataType,
|
||||||
update: (_, currentData, id) => ({
|
update: deleteFromQueryCache('users')
|
||||||
...(currentData as UsersResponseType),
|
|
||||||
users: (currentData as UsersResponseType).users.filter(user => user.id !== id)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -129,7 +134,7 @@ export const useMakeOwner = createMutation<UsersResponseType, string>({
|
|||||||
}),
|
}),
|
||||||
updateQueries: {
|
updateQueries: {
|
||||||
dataType,
|
dataType,
|
||||||
update: updateUsers
|
update: updateQueryCache('users')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
||||||
import {Meta, createMutation} from '../utils/apiRequests';
|
import {Meta, createMutation} from '../utils/api/hooks';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
|
||||||
|
@ -11,10 +11,13 @@ import Popover from '../../../admin-x-ds/global/Popover';
|
|||||||
import Select, {SelectOption} from '../../../admin-x-ds/global/form/Select';
|
import Select, {SelectOption} from '../../../admin-x-ds/global/form/Select';
|
||||||
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||||
import ToggleGroup from '../../../admin-x-ds/global/form/ToggleGroup';
|
import ToggleGroup from '../../../admin-x-ds/global/form/ToggleGroup';
|
||||||
|
import useFilterableApi from '../../../hooks/useFilterableApi';
|
||||||
import useRouting from '../../../hooks/useRouting';
|
import useRouting from '../../../hooks/useRouting';
|
||||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
|
||||||
import {Action, getActionTitle, getContextResource, getLinkTarget, isBulkAction, useBrowseActions} from '../../../api/actions';
|
import {Action, getActionTitle, getContextResource, getLinkTarget, isBulkAction, useBrowseActions} from '../../../api/actions';
|
||||||
|
import {LoadOptions} from '../../../admin-x-ds/global/form/MultiSelect';
|
||||||
import {RoutingModalProps} from '../../providers/RoutingProvider';
|
import {RoutingModalProps} from '../../providers/RoutingProvider';
|
||||||
|
import {User} from '../../../api/users';
|
||||||
|
import {debounce} from '../../../utils/debounce';
|
||||||
import {generateAvatarColor, getInitials} from '../../../utils/helpers';
|
import {generateAvatarColor, getInitials} from '../../../utils/helpers';
|
||||||
import {useCallback, useState} from 'react';
|
import {useCallback, useState} from 'react';
|
||||||
|
|
||||||
@ -73,15 +76,19 @@ const HistoryFilter: React.FC<{
|
|||||||
toggleResourceType: (resource: string, included: boolean) => void;
|
toggleResourceType: (resource: string, included: boolean) => void;
|
||||||
}> = ({excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => {
|
}> = ({excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => {
|
||||||
const {updateRoute} = useRouting();
|
const {updateRoute} = useRouting();
|
||||||
const {users} = useStaffUsers();
|
const usersApi = useFilterableApi<User, 'users', 'name'>({path: '/users/', filterKey: 'name', responseKey: 'users'});
|
||||||
|
|
||||||
|
const loadOptions: LoadOptions = async (input, callback) => {
|
||||||
|
const users = await usersApi.loadData(input);
|
||||||
|
callback(users.map(user => ({label: user.name, value: user.id})));
|
||||||
|
};
|
||||||
|
|
||||||
const [searchedStaff, setSearchStaff] = useState<SelectOption | null>();
|
const [searchedStaff, setSearchStaff] = useState<SelectOption | null>();
|
||||||
|
|
||||||
const resetStaff = () => {
|
const resetStaff = () => {
|
||||||
setSearchStaff(null);
|
setSearchStaff(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const userOptions = users.map(user => ({label: user.name, value: user.id}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-4'>
|
<div className='flex items-center gap-4'>
|
||||||
<Popover position='right' trigger={<Button color='outline' label='Filter' size='sm' />}>
|
<Popover position='right' trigger={<Button color='outline' label='Filter' size='sm' />}>
|
||||||
@ -102,14 +109,16 @@ const HistoryFilter: React.FC<{
|
|||||||
</Popover>
|
</Popover>
|
||||||
<div className='w-[200px]'>
|
<div className='w-[200px]'>
|
||||||
<Select
|
<Select
|
||||||
options={userOptions}
|
loadOptions={debounce(loadOptions, 500)}
|
||||||
placeholder='Search staff'
|
placeholder='Search staff'
|
||||||
value={searchedStaff}
|
value={searchedStaff}
|
||||||
|
async
|
||||||
|
defaultOptions
|
||||||
isClearable
|
isClearable
|
||||||
onSelect={(value) => {
|
onSelect={(option) => {
|
||||||
if (value) {
|
if (option) {
|
||||||
setSearchStaff(userOptions.find(option => option.value === value)!);
|
setSearchStaff(option);
|
||||||
updateRoute(`history/view/${value}`);
|
updateRoute(`history/view/${option.value}`);
|
||||||
} else {
|
} else {
|
||||||
resetStaff();
|
resetStaff();
|
||||||
updateRoute('history/view');
|
updateRoute('history/view');
|
||||||
|
@ -8,7 +8,7 @@ import NoValueLabel from '../../../admin-x-ds/global/NoValueLabel';
|
|||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||||
import TabView from '../../../admin-x-ds/global/TabView';
|
import TabView from '../../../admin-x-ds/global/TabView';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import useRouting from '../../../hooks/useRouting';
|
import useRouting from '../../../hooks/useRouting';
|
||||||
import {ReactComponent as AmpIcon} from '../../../assets/icons/amp.svg';
|
import {ReactComponent as AmpIcon} from '../../../assets/icons/amp.svg';
|
||||||
import {ReactComponent as FirstPromoterIcon} from '../../../assets/icons/firstpromoter.svg';
|
import {ReactComponent as FirstPromoterIcon} from '../../../assets/icons/firstpromoter.svg';
|
||||||
|
@ -4,7 +4,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
||||||
import {RoutingModalProps} from '../../../providers/RoutingProvider';
|
import {RoutingModalProps} from '../../../providers/RoutingProvider';
|
||||||
|
@ -4,7 +4,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {ReactComponent as Icon} from '../../../../assets/icons/amp.svg';
|
import {ReactComponent as Icon} from '../../../../assets/icons/amp.svg';
|
||||||
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
||||||
|
@ -7,7 +7,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import WebhooksTable from './WebhooksTable';
|
import WebhooksTable from './WebhooksTable';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useForm from '../../../../hooks/useForm';
|
import useForm from '../../../../hooks/useForm';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {APIKey, useRefreshAPIKey} from '../../../../api/apiKeys';
|
import {APIKey, useRefreshAPIKey} from '../../../../api/apiKeys';
|
||||||
|
@ -4,7 +4,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {ReactComponent as Icon} from '../../../../assets/icons/firstpromoter.svg';
|
import {ReactComponent as Icon} from '../../../../assets/icons/firstpromoter.svg';
|
||||||
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
||||||
|
@ -4,7 +4,7 @@ import IntegrationHeader from './IntegrationHeader';
|
|||||||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import pinturaScreenshot from '../../../../assets/images/pintura-screenshot.png';
|
import pinturaScreenshot from '../../../../assets/images/pintura-screenshot.png';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {ReactComponent as Icon} from '../../../../assets/icons/pintura.svg';
|
import {ReactComponent as Icon} from '../../../../assets/icons/pintura.svg';
|
||||||
|
@ -3,7 +3,7 @@ import IntegrationHeader from './IntegrationHeader';
|
|||||||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {ReactComponent as Icon} from '../../../../assets/icons/unsplash.svg';
|
import {ReactComponent as Icon} from '../../../../assets/icons/unsplash.svg';
|
||||||
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
||||||
|
@ -4,7 +4,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Select from '../../../../admin-x-ds/global/form/Select';
|
import Select from '../../../../admin-x-ds/global/form/Select';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import useForm from '../../../../hooks/useForm';
|
import useForm from '../../../../hooks/useForm';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
@ -94,11 +94,11 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
|
|||||||
hint={errors.event}
|
hint={errors.event}
|
||||||
options={webhookEventOptions}
|
options={webhookEventOptions}
|
||||||
prompt='Select an event'
|
prompt='Select an event'
|
||||||
selectedOption={formState.event}
|
selectedOption={webhookEventOptions.flatMap(group => group.options).find(option => option.value === formState.event)}
|
||||||
title='Event'
|
title='Event'
|
||||||
hideTitle
|
hideTitle
|
||||||
onSelect={(event) => {
|
onSelect={(option) => {
|
||||||
updateForm(state => ({...state, event}));
|
updateForm(state => ({...state, event: option?.value}));
|
||||||
clearError('event');
|
clearError('event');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -6,7 +6,7 @@ import TableCell from '../../../../admin-x-ds/global/TableCell';
|
|||||||
import TableHead from '../../../../admin-x-ds/global/TableHead';
|
import TableHead from '../../../../admin-x-ds/global/TableHead';
|
||||||
import TableRow from '../../../../admin-x-ds/global/TableRow';
|
import TableRow from '../../../../admin-x-ds/global/TableRow';
|
||||||
import WebhookModal from './WebhookModal';
|
import WebhookModal from './WebhookModal';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import {Integration} from '../../../../api/integrations';
|
import {Integration} from '../../../../api/integrations';
|
||||||
import {getWebhookEventLabel} from './webhookEventOptions';
|
import {getWebhookEventLabel} from './webhookEventOptions';
|
||||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||||
|
@ -6,7 +6,7 @@ import List from '../../../../admin-x-ds/global/List';
|
|||||||
import ListItem from '../../../../admin-x-ds/global/ListItem';
|
import ListItem from '../../../../admin-x-ds/global/ListItem';
|
||||||
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {ReactComponent as ArrowRightIcon} from '../../../../admin-x-ds/assets/icons/arrow-right.svg';
|
import {ReactComponent as ArrowRightIcon} from '../../../../admin-x-ds/assets/icons/arrow-right.svg';
|
||||||
import {ReactComponent as Icon} from '../../../../assets/icons/zapier.svg';
|
import {ReactComponent as Icon} from '../../../../assets/icons/zapier.svg';
|
||||||
|
@ -4,7 +4,7 @@ import FileUpload from '../../../../admin-x-ds/global/form/FileUpload';
|
|||||||
import LabItem from './LabItem';
|
import LabItem from './LabItem';
|
||||||
import List from '../../../../admin-x-ds/global/List';
|
import List from '../../../../admin-x-ds/global/List';
|
||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {downloadRedirects, useUploadRedirects} from '../../../../api/redirects';
|
import {downloadRedirects, useUploadRedirects} from '../../../../api/redirects';
|
||||||
import {downloadRoutes, useUploadRoutes} from '../../../../api/routes';
|
import {downloadRoutes, useUploadRoutes} from '../../../../api/routes';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import {ConfigResponseType, configDataType} from '../../../../api/config';
|
import {ConfigResponseType, configDataType} from '../../../../api/config';
|
||||||
import {getSettingValue, useEditSettings} from '../../../../api/settings';
|
import {getSettingValue, useEditSettings} from '../../../../api/settings';
|
||||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||||
|
@ -5,7 +5,7 @@ import LabItem from './LabItem';
|
|||||||
import List from '../../../../admin-x-ds/global/List';
|
import List from '../../../../admin-x-ds/global/List';
|
||||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import {downloadAllContent, useDeleteAllContent, useImportContent} from '../../../../api/db';
|
import {downloadAllContent, useDeleteAllContent, useImportContent} from '../../../../api/db';
|
||||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||||
import {useQueryClient} from '@tanstack/react-query';
|
import {useQueryClient} from '@tanstack/react-query';
|
||||||
|
@ -3,13 +3,11 @@ import React, {useState} from 'react';
|
|||||||
import Select from '../../../admin-x-ds/global/form/Select';
|
import Select from '../../../admin-x-ds/global/form/Select';
|
||||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||||
|
import useDefaultRecipientsOptions from './useDefaultRecipientsOptions';
|
||||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||||
import {GroupBase, MultiValue} from 'react-select';
|
import {MultiValue} from 'react-select';
|
||||||
import {getOptionLabel} from '../../../utils/helpers';
|
import {getOptionLabel} from '../../../utils/helpers';
|
||||||
import {getSettingValues} from '../../../api/settings';
|
import {getSettingValues} from '../../../api/settings';
|
||||||
import {useBrowseLabels} from '../../../api/labels';
|
|
||||||
import {useBrowseOffers} from '../../../api/offers';
|
|
||||||
import {useBrowseTiers} from '../../../api/tiers';
|
|
||||||
import {withErrorBoundary} from '../../../admin-x-ds/global/ErrorBoundary';
|
import {withErrorBoundary} from '../../../admin-x-ds/global/ErrorBoundary';
|
||||||
|
|
||||||
type RefipientValueArgs = {
|
type RefipientValueArgs = {
|
||||||
@ -39,16 +37,6 @@ const RECIPIENT_FILTER_OPTIONS = [{
|
|||||||
value: 'none'
|
value: 'none'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const SIMPLE_SEGMENT_OPTIONS: MultiSelectOption[] = [{
|
|
||||||
label: 'Free members',
|
|
||||||
value: 'status:free',
|
|
||||||
color: 'green'
|
|
||||||
}, {
|
|
||||||
label: 'Paid members',
|
|
||||||
value: 'status:-free',
|
|
||||||
color: 'pink'
|
|
||||||
}];
|
|
||||||
|
|
||||||
function getDefaultRecipientValue({
|
function getDefaultRecipientValue({
|
||||||
defaultEmailRecipients,
|
defaultEmailRecipients,
|
||||||
defaultEmailRecipientsFilter
|
defaultEmailRecipientsFilter
|
||||||
@ -88,9 +76,7 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
defaultEmailRecipientsFilter
|
defaultEmailRecipientsFilter
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const {data: {tiers} = {}} = useBrowseTiers();
|
const {loadOptions, selectedSegments, setSelectedSegments} = useDefaultRecipientsOptions(selectedOption, defaultEmailRecipientsFilter);
|
||||||
const {data: {labels} = {}} = useBrowseLabels();
|
|
||||||
const {data: {offers} = {}} = useBrowseOffers();
|
|
||||||
|
|
||||||
const setDefaultRecipientValue = (value: string) => {
|
const setDefaultRecipientValue = (value: string) => {
|
||||||
if (['visibility', 'disabled'].includes(value)) {
|
if (['visibility', 'disabled'].includes(value)) {
|
||||||
@ -115,34 +101,9 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
setSelectedOption(value);
|
setSelectedOption(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const segmentOptionGroups: GroupBase<MultiSelectOption>[] = [
|
const updateSelectedSegments = (selected: MultiValue<MultiSelectOption>) => {
|
||||||
{
|
setSelectedSegments(selected);
|
||||||
options: SIMPLE_SEGMENT_OPTIONS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Active Tiers',
|
|
||||||
options: tiers?.filter(({active, type}) => active && type !== 'free').map(tier => ({value: tier.id, label: tier.name, color: 'black'})) || []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Archived Tiers',
|
|
||||||
options: tiers?.filter(({active}) => !active).map(tier => ({value: tier.id, label: tier.name, color: 'black'})) || []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Labels',
|
|
||||||
options: labels?.map(label => ({value: `label:${label.slug}`, label: label.name, color: 'grey'})) || []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Offers',
|
|
||||||
options: offers?.map(offer => ({value: `offer_redemptions:${offer.id}`, label: offer.name, color: 'black'})) || []
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const filters = defaultEmailRecipientsFilter?.split(',') || [];
|
|
||||||
const selectedSegments = segmentOptionGroups
|
|
||||||
.flatMap(({options}) => options)
|
|
||||||
.filter(({value}) => filters.includes(value));
|
|
||||||
|
|
||||||
const setSelectedSegments = (selected: MultiValue<MultiSelectOption>) => {
|
|
||||||
if (selected.length) {
|
if (selected.length) {
|
||||||
const selectedGroups = selected?.map(({value}) => value).join(',');
|
const selectedGroups = selected?.map(({value}) => value).join(',');
|
||||||
updateSetting('editor_default_email_recipients_filter', selectedGroups);
|
updateSetting('editor_default_email_recipients_filter', selectedGroups);
|
||||||
@ -169,21 +130,23 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
<Select
|
<Select
|
||||||
hint='Who should be able to subscribe to your site?'
|
hint='Who should be able to subscribe to your site?'
|
||||||
options={RECIPIENT_FILTER_OPTIONS}
|
options={RECIPIENT_FILTER_OPTIONS}
|
||||||
selectedOption={selectedOption}
|
selectedOption={RECIPIENT_FILTER_OPTIONS.find(option => option.value === selectedOption)}
|
||||||
title="Default Newsletter recipients"
|
title="Default Newsletter recipients"
|
||||||
onSelect={(value) => {
|
onSelect={(option) => {
|
||||||
if (value) {
|
if (option) {
|
||||||
setDefaultRecipientValue(value);
|
setDefaultRecipientValue(option.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{(selectedOption === 'segment') && (
|
{(selectedOption === 'segment') && selectedSegments && (
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
options={segmentOptionGroups.filter(group => group.options.length > 0)}
|
loadOptions={loadOptions}
|
||||||
title='Filter'
|
title='Filter'
|
||||||
values={selectedSegments}
|
values={selectedSegments}
|
||||||
|
async
|
||||||
clearBg
|
clearBg
|
||||||
onChange={setSelectedSegments}
|
defaultOptions
|
||||||
|
onChange={updateSelectedSegments}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</SettingGroupContent>
|
</SettingGroupContent>
|
||||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||||
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import useRouting from '../../../hooks/useRouting';
|
import useRouting from '../../../hooks/useRouting';
|
||||||
import {Setting, getSettingValues, useEditSettings} from '../../../api/settings';
|
import {Setting, getSettingValues, useEditSettings} from '../../../api/settings';
|
||||||
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||||
|
@ -5,7 +5,7 @@ import Select from '../../../admin-x-ds/global/form/Select';
|
|||||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||||
import {getSettingValues, useEditSettings} from '../../../api/settings';
|
import {getSettingValues, useEditSettings} from '../../../api/settings';
|
||||||
import {withErrorBoundary} from '../../../admin-x-ds/global/ErrorBoundary';
|
import {withErrorBoundary} from '../../../admin-x-ds/global/ErrorBoundary';
|
||||||
@ -65,10 +65,10 @@ const MailGun: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
<div className='grid grid-cols-[120px_auto] gap-x-3 gap-y-6'>
|
<div className='grid grid-cols-[120px_auto] gap-x-3 gap-y-6'>
|
||||||
<Select
|
<Select
|
||||||
options={MAILGUN_REGIONS}
|
options={MAILGUN_REGIONS}
|
||||||
selectedOption={mailgunRegion}
|
selectedOption={MAILGUN_REGIONS.find(option => option.value === mailgunRegion)}
|
||||||
title="Mailgun region"
|
title="Mailgun region"
|
||||||
onSelect={(value) => {
|
onSelect={(option) => {
|
||||||
updateSetting('mailgun_base_url', value || null);
|
updateSetting('mailgun_base_url', option?.value || null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -13,7 +13,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
updateRoute('newsletters/add');
|
updateRoute('newsletters/add');
|
||||||
};
|
};
|
||||||
const [selectedTab, setSelectedTab] = useState('active-newsletters');
|
const [selectedTab, setSelectedTab] = useState('active-newsletters');
|
||||||
const {data: {newsletters} = {}} = useBrowseNewsletters();
|
const {data: {newsletters, isEnd} = {}, fetchNextPage} = useBrowseNewsletters();
|
||||||
|
|
||||||
const buttons = (
|
const buttons = (
|
||||||
<Button color='green' label='Add newsletter' link linkWithPadding onClick={() => {
|
<Button color='green' label='Add newsletter' link linkWithPadding onClick={() => {
|
||||||
@ -43,6 +43,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
title='Newsletters'
|
title='Newsletters'
|
||||||
>
|
>
|
||||||
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
|
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
|
||||||
|
{isEnd === false && <Button label='Load more' link onClick={() => fetchNextPage()} />}
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -6,7 +6,7 @@ import React, {useEffect} from 'react';
|
|||||||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useForm from '../../../../hooks/useForm';
|
import useForm from '../../../../hooks/useForm';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
import {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
||||||
|
@ -19,7 +19,7 @@ import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
|||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
|
import ToggleGroup from '../../../../admin-x-ds/global/form/ToggleGroup';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
import useFeatureFlag from '../../../../hooks/useFeatureFlag';
|
||||||
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
|
import useForm, {ErrorMessages} from '../../../../hooks/useForm';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
@ -160,7 +160,12 @@ const Sidebar: React.FC<{
|
|||||||
onChange={e => updateNewsletter({sender_email: e.target.value})}
|
onChange={e => updateNewsletter({sender_email: e.target.value})}
|
||||||
onKeyDown={() => clearError('sender_email')}
|
onKeyDown={() => clearError('sender_email')}
|
||||||
/>
|
/>
|
||||||
<Select options={replyToEmails} selectedOption={newsletter.sender_reply_to} title="Reply-to email" onSelect={value => updateNewsletter({sender_reply_to: value})}/>
|
<Select
|
||||||
|
options={replyToEmails}
|
||||||
|
selectedOption={replyToEmails.find(option => option.value === newsletter.sender_reply_to)}
|
||||||
|
title="Reply-to email"
|
||||||
|
onSelect={option => updateNewsletter({sender_reply_to: option?.value})}
|
||||||
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
<Form className='mt-6' gap='sm' margins='lg' title='Member settings'>
|
<Form className='mt-6' gap='sm' margins='lg' title='Member settings'>
|
||||||
<Toggle
|
<Toggle
|
||||||
@ -308,8 +313,8 @@ const Sidebar: React.FC<{
|
|||||||
<Select
|
<Select
|
||||||
disabled={!newsletter.show_post_title_section}
|
disabled={!newsletter.show_post_title_section}
|
||||||
options={fontOptions}
|
options={fontOptions}
|
||||||
selectedOption={newsletter.title_font_category}
|
selectedOption={fontOptions.find(option => option.value === newsletter.title_font_category)}
|
||||||
onSelect={value => updateNewsletter({title_font_category: value})}
|
onSelect={option => updateNewsletter({title_font_category: option?.value})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ButtonGroup buttons={[
|
<ButtonGroup buttons={[
|
||||||
@ -359,9 +364,9 @@ const Sidebar: React.FC<{
|
|||||||
/>}
|
/>}
|
||||||
<Select
|
<Select
|
||||||
options={fontOptions}
|
options={fontOptions}
|
||||||
selectedOption={newsletter.body_font_category}
|
selectedOption={fontOptions.find(option => option.value === newsletter.body_font_category)}
|
||||||
title='Body style'
|
title='Body style'
|
||||||
onSelect={value => updateNewsletter({body_font_category: value})}
|
onSelect={option => updateNewsletter({body_font_category: option?.value})}
|
||||||
/>
|
/>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={newsletter.show_feature_image}
|
checked={newsletter.show_feature_image}
|
||||||
@ -510,9 +515,15 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
|
|||||||
};
|
};
|
||||||
|
|
||||||
const NewsletterDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
const NewsletterDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
||||||
const {data: {newsletters} = {}} = useBrowseNewsletters();
|
const {data: {newsletters, isEnd} = {}, fetchNextPage} = useBrowseNewsletters();
|
||||||
const newsletter = newsletters?.find(({id}) => id === params?.id);
|
const newsletter = newsletters?.find(({id}) => id === params?.id);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!newsletter && !isEnd) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [fetchNextPage, isEnd, newsletter]);
|
||||||
|
|
||||||
if (newsletter) {
|
if (newsletter) {
|
||||||
return <NewsletterDetailModalContent newsletter={newsletter} onlyOne={newsletters!.length === 1} />;
|
return <NewsletterDetailModalContent newsletter={newsletter} onlyOne={newsletters!.length === 1} />;
|
||||||
} else {
|
} else {
|
||||||
|
@ -0,0 +1,100 @@
|
|||||||
|
import useFilterableApi from '../../../hooks/useFilterableApi';
|
||||||
|
import {GroupBase, MultiValue} from 'react-select';
|
||||||
|
import {Label} from '../../../api/labels';
|
||||||
|
import {LoadOptions, MultiSelectOption} from '../../../admin-x-ds/global/form/MultiSelect';
|
||||||
|
import {Offer} from '../../../api/offers';
|
||||||
|
import {Tier} from '../../../api/tiers';
|
||||||
|
import {debounce} from '../../../utils/debounce';
|
||||||
|
import {isObjectId} from '../../../utils/helpers';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
const SIMPLE_SEGMENT_OPTIONS: MultiSelectOption[] = [{
|
||||||
|
label: 'Free members',
|
||||||
|
value: 'status:free',
|
||||||
|
color: 'green'
|
||||||
|
}, {
|
||||||
|
label: 'Paid members',
|
||||||
|
value: 'status:-free',
|
||||||
|
color: 'pink'
|
||||||
|
}];
|
||||||
|
|
||||||
|
const useDefaultRecipientsOptions = (selectedOption: string, defaultEmailRecipientsFilter?: string | null) => {
|
||||||
|
const tiers = useFilterableApi<Tier, 'tiers', 'name'>({path: '/tiers/', filterKey: 'name', responseKey: 'tiers'});
|
||||||
|
const labels = useFilterableApi<Label, 'labels', 'name'>({path: '/labels/', filterKey: 'name', responseKey: 'labels'});
|
||||||
|
const offers = useFilterableApi<Offer, 'offers', 'name'>({path: '/offers/', filterKey: 'name', responseKey: 'offers'});
|
||||||
|
|
||||||
|
const [selectedSegments, setSelectedSegments] = useState<MultiValue<MultiSelectOption> | null>(null);
|
||||||
|
|
||||||
|
const tierOption = (tier: Tier): MultiSelectOption => ({value: tier.id, label: tier.name, color: 'black'});
|
||||||
|
const labelOption = (label: Label): MultiSelectOption => ({value: `label:${label.slug}`, label: label.name, color: 'grey'});
|
||||||
|
const offerOption = (offer: Offer): MultiSelectOption => ({value: `offer_redemptions:${offer.id}`, label: offer.name, color: 'black'});
|
||||||
|
|
||||||
|
const loadOptions: LoadOptions = async (input, callback) => {
|
||||||
|
const [tiersData, labelsData, offersData] = await Promise.all([tiers.loadData(input), labels.loadData(input), offers.loadData(input)]);
|
||||||
|
|
||||||
|
const segmentOptionGroups: GroupBase<MultiSelectOption>[] = [
|
||||||
|
{
|
||||||
|
options: SIMPLE_SEGMENT_OPTIONS.filter(({label}) => label.toLowerCase().includes(input.toLowerCase()))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Active Tiers',
|
||||||
|
options: tiersData.filter(({active, type}) => active && type !== 'free').map(tierOption) || []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Archived Tiers',
|
||||||
|
options: tiersData.filter(({active}) => !active).map(tierOption) || []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Labels',
|
||||||
|
options: labelsData.map(labelOption) || []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Offers',
|
||||||
|
options: offersData.map(offerOption) || []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (selectedSegments === null) {
|
||||||
|
initSelectedSegments();
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(segmentOptionGroups.filter(group => group.options.length > 0));
|
||||||
|
};
|
||||||
|
|
||||||
|
const initSelectedSegments = async () => {
|
||||||
|
const filters = defaultEmailRecipientsFilter?.split(',') || [];
|
||||||
|
const tierIds: string[] = [], labelIds: string[] = [], offerIds: string[] = [];
|
||||||
|
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (filter.startsWith('label:')) {
|
||||||
|
labelIds.push(filter.replace('label:', ''));
|
||||||
|
} else if (filter.startsWith('offer_redemptions:')) {
|
||||||
|
offerIds.push(filter.replace('offer_redemptions:', ''));
|
||||||
|
} else if (isObjectId(filter)) {
|
||||||
|
tierIds.push(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = await Promise.all([
|
||||||
|
tiers.loadInitialValues(tierIds).then(data => data.map(tierOption)),
|
||||||
|
labels.loadInitialValues(labelIds).then(data => data.map(labelOption)),
|
||||||
|
offers.loadInitialValues(offerIds).then(data => data.map(offerOption))
|
||||||
|
]).then(results => results.flat());
|
||||||
|
|
||||||
|
setSelectedSegments(filters.map(filter => options.find(option => option.value === filter)!));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedOption === 'segment') {
|
||||||
|
loadOptions('', () => {});
|
||||||
|
}
|
||||||
|
}, [selectedOption]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadOptions: debounce(loadOptions, 500),
|
||||||
|
selectedSegments,
|
||||||
|
setSelectedSegments
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useDefaultRecipientsOptions;
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||||
import {ReactComponent as FacebookLogo} from '../../../admin-x-ds/assets/images/facebook-logo.svg';
|
import {ReactComponent as FacebookLogo} from '../../../admin-x-ds/assets/images/facebook-logo.svg';
|
||||||
|
@ -2,7 +2,7 @@ import Modal from '../../../admin-x-ds/global/modal/Modal';
|
|||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import Radio from '../../../admin-x-ds/global/form/Radio';
|
import Radio from '../../../admin-x-ds/global/form/Radio';
|
||||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import useRouting from '../../../hooks/useRouting';
|
import useRouting from '../../../hooks/useRouting';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
|
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
|
||||||
|
@ -49,7 +49,7 @@ const TimeZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
|
|
||||||
const [publicationTimezone] = getSettingValues(localSettings, ['timezone']) as string[];
|
const [publicationTimezone] = getSettingValues(localSettings, ['timezone']) as string[];
|
||||||
|
|
||||||
const timezoneOptions = timezoneData.map((tzOption: TimezoneDataDropdownOption) => {
|
const timezoneOptions: Array<{value: string; label: string}> = timezoneData.map((tzOption: TimezoneDataDropdownOption) => {
|
||||||
return {
|
return {
|
||||||
value: tzOption.name,
|
value: tzOption.name,
|
||||||
label: tzOption.label
|
label: tzOption.label
|
||||||
@ -76,9 +76,9 @@ const TimeZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
<Select
|
<Select
|
||||||
hint={<Hint timezone={publicationTimezone} />}
|
hint={<Hint timezone={publicationTimezone} />}
|
||||||
options={timezoneOptions}
|
options={timezoneOptions}
|
||||||
selectedOption={publicationTimezone}
|
selectedOption={timezoneOptions.find(option => option.value === publicationTimezone)}
|
||||||
title="Site timezone"
|
title="Site timezone"
|
||||||
onSelect={handleTimezoneChange}
|
onSelect={option => handleTimezoneChange(option?.value)}
|
||||||
/>
|
/>
|
||||||
</SettingGroupContent>
|
</SettingGroupContent>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||||
import {ReactComponent as TwitterLogo} from '../../../admin-x-ds/assets/images/twitter-logo.svg';
|
import {ReactComponent as TwitterLogo} from '../../../admin-x-ds/assets/images/twitter-logo.svg';
|
||||||
|
@ -15,7 +15,7 @@ import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupConten
|
|||||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||||
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import useFeatureFlag from '../../../hooks/useFeatureFlag';
|
import useFeatureFlag from '../../../hooks/useFeatureFlag';
|
||||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||||
import useRouting from '../../../hooks/useRouting';
|
import useRouting from '../../../hooks/useRouting';
|
||||||
@ -722,9 +722,15 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const UserDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
const UserDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
||||||
const {users} = useStaffUsers();
|
const {users, hasNextPage, fetchNextPage} = useStaffUsers();
|
||||||
const user = users.find(({slug}) => slug === params?.slug);
|
const user = users.find(({slug}) => slug === params?.slug);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!user && !hasNextPage) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [fetchNextPage, hasNextPage, user]);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return <UserDetailModalContent user={user} />;
|
return <UserDetailModalContent user={user} />;
|
||||||
} else {
|
} else {
|
||||||
|
@ -7,7 +7,7 @@ import React, {useState} from 'react';
|
|||||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||||
import TabView from '../../../admin-x-ds/global/TabView';
|
import TabView from '../../../admin-x-ds/global/TabView';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import useRouting from '../../../hooks/useRouting';
|
import useRouting from '../../../hooks/useRouting';
|
||||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||||
import {User, hasAdminAccess, isContributorUser, isEditorUser} from '../../../api/users';
|
import {User, hasAdminAccess, isContributorUser, isEditorUser} from '../../../api/users';
|
||||||
|
@ -2,7 +2,7 @@ import Button from '../../../../admin-x-ds/global/Button';
|
|||||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||||
import SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import {User, useUpdatePassword} from '../../../../api/users';
|
import {User, useUpdatePassword} from '../../../../api/users';
|
||||||
import {ValidationError} from '../../../../utils/errors';
|
import {ValidationError} from '../../../../utils/errors';
|
||||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||||
|
@ -136,19 +136,19 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
<Select
|
<Select
|
||||||
hint='Who should be able to subscribe to your site?'
|
hint='Who should be able to subscribe to your site?'
|
||||||
options={MEMBERS_SIGNUP_ACCESS_OPTIONS}
|
options={MEMBERS_SIGNUP_ACCESS_OPTIONS}
|
||||||
selectedOption={membersSignupAccess}
|
selectedOption={MEMBERS_SIGNUP_ACCESS_OPTIONS.find(option => option.value === membersSignupAccess)}
|
||||||
title="Subscription access"
|
title="Subscription access"
|
||||||
onSelect={(value) => {
|
onSelect={(option) => {
|
||||||
updateSetting('members_signup_access', value || null);
|
updateSetting('members_signup_access', option?.value || null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
hint='When a new post is created, who should have access?'
|
hint='When a new post is created, who should have access?'
|
||||||
options={DEFAULT_CONTENT_VISIBILITY_OPTIONS}
|
options={DEFAULT_CONTENT_VISIBILITY_OPTIONS}
|
||||||
selectedOption={defaultContentVisibility}
|
selectedOption={DEFAULT_CONTENT_VISIBILITY_OPTIONS.find(option => option.value === defaultContentVisibility)}
|
||||||
title="Default post access"
|
title="Default post access"
|
||||||
onSelect={(value) => {
|
onSelect={(option) => {
|
||||||
updateSetting('default_content_visibility', value || null);
|
updateSetting('default_content_visibility', option?.value || null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{defaultContentVisibility === 'tiers' && (
|
{defaultContentVisibility === 'tiers' && (
|
||||||
@ -164,10 +164,10 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
<Select
|
<Select
|
||||||
hint='Who can comment on posts?'
|
hint='Who can comment on posts?'
|
||||||
options={COMMENTS_ENABLED_OPTIONS}
|
options={COMMENTS_ENABLED_OPTIONS}
|
||||||
selectedOption={commentsEnabled}
|
selectedOption={COMMENTS_ENABLED_OPTIONS.find(option => option.value === commentsEnabled)}
|
||||||
title="Commenting"
|
title="Commenting"
|
||||||
onSelect={(value) => {
|
onSelect={(option) => {
|
||||||
updateSetting('comments_enabled', value || null);
|
updateSetting('comments_enabled', option?.value || null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingGroupContent>
|
</SettingGroupContent>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import Button from '../../../admin-x-ds/global/Button';
|
||||||
import React, {useState} from 'react';
|
import React, {useState} from 'react';
|
||||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||||
import StripeButton from '../../../admin-x-ds/settings/StripeButton';
|
import StripeButton from '../../../admin-x-ds/settings/StripeButton';
|
||||||
@ -26,7 +27,7 @@ const StripeConnectedButton: React.FC<{className?: string; onClick: () => void;}
|
|||||||
const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||||
const [selectedTab, setSelectedTab] = useState('active-tiers');
|
const [selectedTab, setSelectedTab] = useState('active-tiers');
|
||||||
const {settings, config} = useGlobalData();
|
const {settings, config} = useGlobalData();
|
||||||
const {data: {tiers} = {}} = useBrowseTiers();
|
const {data: {tiers, isEnd} = {}, fetchNextPage} = useBrowseTiers();
|
||||||
const activeTiers = getActiveTiers(tiers || []);
|
const activeTiers = getActiveTiers(tiers || []);
|
||||||
const archivedTiers = getArchivedTiers(tiers || []);
|
const archivedTiers = getArchivedTiers(tiers || []);
|
||||||
const {updateRoute} = useRouting();
|
const {updateRoute} = useRouting();
|
||||||
@ -80,6 +81,7 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{content}
|
{content}
|
||||||
|
{isEnd === false && <Button label='Load more' link onClick={() => fetchNextPage()} />}
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -102,8 +102,8 @@ const TipsOrDonations: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||||||
containerClassName='w-14'
|
containerClassName='w-14'
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
options={currencySelectGroups()}
|
options={currencySelectGroups()}
|
||||||
selectedOption={donationsCurrency}
|
selectedOption={currencySelectGroups().flatMap(group => group.options).find(option => option.value === donationsCurrency)}
|
||||||
onSelect={currency => updateSetting('donations_currency', currency || 'USD')}
|
onSelect={option => updateSetting('donations_currency', option?.value || 'USD')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
title='Suggested amount'
|
title='Suggested amount'
|
||||||
|
@ -8,7 +8,6 @@ import {MultiSelectOption} from '../../../../admin-x-ds/global/form/MultiSelect'
|
|||||||
import {MultiValue} from 'react-select';
|
import {MultiValue} from 'react-select';
|
||||||
import {generateCode} from '../../../../utils/generateEmbedCode';
|
import {generateCode} from '../../../../utils/generateEmbedCode';
|
||||||
import {getSettingValues} from '../../../../api/settings';
|
import {getSettingValues} from '../../../../api/settings';
|
||||||
import {useBrowseLabels} from '../../../../api/labels';
|
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||||
|
|
||||||
@ -25,7 +24,6 @@ const EmbedSignupFormModal = NiceModal.create(() => {
|
|||||||
const {config} = useGlobalData();
|
const {config} = useGlobalData();
|
||||||
const {localSettings, siteData} = useSettingGroup();
|
const {localSettings, siteData} = useSettingGroup();
|
||||||
const [accentColor, title, description, locale, labs, icon] = getSettingValues<string>(localSettings, ['accent_color', 'title', 'description', 'locale', 'labs', 'icon']);
|
const [accentColor, title, description, locale, labs, icon] = getSettingValues<string>(localSettings, ['accent_color', 'title', 'description', 'locale', 'labs', 'icon']);
|
||||||
const {data: labels} = useBrowseLabels();
|
|
||||||
const [customColor, setCustomColor] = useState<{active: boolean}>({active: false});
|
const [customColor, setCustomColor] = useState<{active: boolean}>({active: false});
|
||||||
|
|
||||||
if (labs) {
|
if (labs) {
|
||||||
@ -113,7 +111,6 @@ const EmbedSignupFormModal = NiceModal.create(() => {
|
|||||||
handleLabelClick={addSelectedLabel}
|
handleLabelClick={addSelectedLabel}
|
||||||
handleLayoutSelect={setSelectedLayout}
|
handleLayoutSelect={setSelectedLayout}
|
||||||
isCopied={isCopied}
|
isCopied={isCopied}
|
||||||
labels={labels?.labels || []}
|
|
||||||
selectedColor={selectedColor}
|
selectedColor={selectedColor}
|
||||||
selectedLabels={selectedLabels}
|
selectedLabels={selectedLabels}
|
||||||
selectedLayout={selectedLayout}
|
selectedLayout={selectedLayout}
|
||||||
|
@ -3,13 +3,15 @@ import ColorIndicator from '../../../../admin-x-ds/global/form/ColorIndicator';
|
|||||||
import ColorPicker from '../../../../admin-x-ds/global/form/ColorPicker';
|
import ColorPicker from '../../../../admin-x-ds/global/form/ColorPicker';
|
||||||
import Form from '../../../../admin-x-ds/global/form/Form';
|
import Form from '../../../../admin-x-ds/global/form/Form';
|
||||||
import Heading from '../../../../admin-x-ds/global/Heading';
|
import Heading from '../../../../admin-x-ds/global/Heading';
|
||||||
import MultiSelect, {MultiSelectOption} from '../../../../admin-x-ds/global/form/MultiSelect';
|
import MultiSelect, {LoadOptions, MultiSelectOption} from '../../../../admin-x-ds/global/form/MultiSelect';
|
||||||
import Radio from '../../../../admin-x-ds/global/form/Radio';
|
import Radio from '../../../../admin-x-ds/global/form/Radio';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import StickyFooter from '../../../../admin-x-ds/global/StickyFooter';
|
import StickyFooter from '../../../../admin-x-ds/global/StickyFooter';
|
||||||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||||
|
import useFilterableApi from '../../../../hooks/useFilterableApi';
|
||||||
import {Label} from '../../../../api/labels';
|
import {Label} from '../../../../api/labels';
|
||||||
import {MultiValue} from 'react-select';
|
import {MultiValue} from 'react-select';
|
||||||
|
import {debounce} from '../../../../utils/debounce';
|
||||||
|
|
||||||
export type SelectedLabelTypes = {
|
export type SelectedLabelTypes = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -20,7 +22,6 @@ type SidebarProps = {
|
|||||||
selectedColor?: string;
|
selectedColor?: string;
|
||||||
accentColor?: string;
|
accentColor?: string;
|
||||||
handleColorToggle: (e: string) => void;
|
handleColorToggle: (e: string) => void;
|
||||||
labels?: Label[];
|
|
||||||
handleLabelClick: (selected: MultiValue<MultiSelectOption>) => void;
|
handleLabelClick: (selected: MultiValue<MultiSelectOption>) => void;
|
||||||
selectedLabels?: SelectedLabelTypes[];
|
selectedLabels?: SelectedLabelTypes[];
|
||||||
embedScript: string;
|
embedScript: string;
|
||||||
@ -36,7 +37,6 @@ const EmbedSignupSidebar: React.FC<SidebarProps> = ({selectedLayout,
|
|||||||
accentColor,
|
accentColor,
|
||||||
handleColorToggle,
|
handleColorToggle,
|
||||||
selectedColor,
|
selectedColor,
|
||||||
labels,
|
|
||||||
selectedLabels,
|
selectedLabels,
|
||||||
handleLabelClick,
|
handleLabelClick,
|
||||||
embedScript,
|
embedScript,
|
||||||
@ -45,12 +45,13 @@ const EmbedSignupSidebar: React.FC<SidebarProps> = ({selectedLayout,
|
|||||||
customColor,
|
customColor,
|
||||||
setCustomColor,
|
setCustomColor,
|
||||||
isCopied}) => {
|
isCopied}) => {
|
||||||
const labelOptions = labels ? labels.map((l) => {
|
const {loadData} = useFilterableApi<Label>({path: '/labels/', filterKey: 'name', responseKey: 'labels'});
|
||||||
return {
|
|
||||||
label: l?.name,
|
const loadOptions: LoadOptions = async (input, callback) => {
|
||||||
value: l?.name
|
const labels = await loadData(input);
|
||||||
|
callback(labels.map(label => ({label: label.name, value: label.name})));
|
||||||
};
|
};
|
||||||
}).filter(Boolean) : [];
|
|
||||||
return (
|
return (
|
||||||
<div className='flex h-[calc(100vh-16vmin)] max-h-[645px] flex-col justify-between overflow-y-scroll border-l border-grey-200 p-6 pb-0 dark:border-grey-900'>
|
<div className='flex h-[calc(100vh-16vmin)] max-h-[645px] flex-col justify-between overflow-y-scroll border-l border-grey-200 p-6 pb-0 dark:border-grey-900'>
|
||||||
<div>
|
<div>
|
||||||
@ -125,12 +126,14 @@ const EmbedSignupSidebar: React.FC<SidebarProps> = ({selectedLayout,
|
|||||||
}
|
}
|
||||||
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
canCreate={true}
|
|
||||||
hint='Will be applied to all members signing up via this form'
|
hint='Will be applied to all members signing up via this form'
|
||||||
options={labelOptions}
|
loadOptions={debounce(loadOptions, 500)}
|
||||||
placeholder='Pick one or more labels (optional)'
|
placeholder='Pick one or more labels (optional)'
|
||||||
title='Labels at signup'
|
title='Labels at signup'
|
||||||
values={selectedLabels || []}
|
values={selectedLabels || []}
|
||||||
|
async
|
||||||
|
canCreate
|
||||||
|
defaultOptions
|
||||||
onChange={handleLabelClick}
|
onChange={handleLabelClick}
|
||||||
/>
|
/>
|
||||||
<TextArea
|
<TextArea
|
||||||
|
@ -7,7 +7,7 @@ import Select from '../../../../admin-x-ds/global/form/Select';
|
|||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import {ReactComponent as PortalIcon1} from '../../../../assets/icons/portal-icon-1.svg';
|
import {ReactComponent as PortalIcon1} from '../../../../assets/icons/portal-icon-1.svg';
|
||||||
import {ReactComponent as PortalIcon2} from '../../../../assets/icons/portal-icon-2.svg';
|
import {ReactComponent as PortalIcon2} from '../../../../assets/icons/portal-icon-2.svg';
|
||||||
import {ReactComponent as PortalIcon3} from '../../../../assets/icons/portal-icon-3.svg';
|
import {ReactComponent as PortalIcon3} from '../../../../assets/icons/portal-icon-3.svg';
|
||||||
@ -67,6 +67,12 @@ const LookAndFeel: React.FC<{
|
|||||||
setUploadedIcon(undefined);
|
setUploadedIcon(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const portalButtonOptions = [
|
||||||
|
{value: 'icon-and-text', label: 'Icon and text'},
|
||||||
|
{value: 'icon-only', label: 'Icon only'},
|
||||||
|
{value: 'text-only', label: 'Text only'}
|
||||||
|
];
|
||||||
|
|
||||||
return <div className='mt-7'><Form>
|
return <div className='mt-7'><Form>
|
||||||
<Toggle
|
<Toggle
|
||||||
checked={Boolean(portalButton)}
|
checked={Boolean(portalButton)}
|
||||||
@ -75,14 +81,10 @@ const LookAndFeel: React.FC<{
|
|||||||
onChange={e => updateSetting('portal_button', e.target.checked)}
|
onChange={e => updateSetting('portal_button', e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
options={[
|
options={portalButtonOptions}
|
||||||
{value: 'icon-and-text', label: 'Icon and text'},
|
selectedOption={portalButtonOptions.find(option => option.value === portalButtonStyle)}
|
||||||
{value: 'icon-only', label: 'Icon only'},
|
|
||||||
{value: 'text-only', label: 'Text only'}
|
|
||||||
]}
|
|
||||||
selectedOption={portalButtonStyle as string}
|
|
||||||
title='Portal button style'
|
title='Portal button style'
|
||||||
onSelect={option => updateSetting('portal_button_style', option || null)}
|
onSelect={option => updateSetting('portal_button_style', option?.value || null)}
|
||||||
/>
|
/>
|
||||||
{portalButtonStyle?.toString()?.includes('icon') &&
|
{portalButtonStyle?.toString()?.includes('icon') &&
|
||||||
<div className='flex flex-col gap-2'>
|
<div className='flex flex-col gap-2'>
|
||||||
|
@ -81,10 +81,10 @@ const PortalLinks: React.FC = () => {
|
|||||||
<span className='inline-block w-[240px] shrink-0'>Tier</span>
|
<span className='inline-block w-[240px] shrink-0'>Tier</span>
|
||||||
<Select
|
<Select
|
||||||
options={tierOptions}
|
options={tierOptions}
|
||||||
selectedOption={selectedTier}
|
selectedOption={tierOptions.find(option => option.value === selectedTier)}
|
||||||
onSelect={(value) => {
|
onSelect={(option) => {
|
||||||
if (value) {
|
if (option) {
|
||||||
setSelectedTier(value);
|
setSelectedTier(option?.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -6,7 +6,7 @@ import PortalPreview from './PortalPreview';
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import SignupOptions from './SignupOptions';
|
import SignupOptions from './SignupOptions';
|
||||||
import TabView, {Tab} from '../../../../admin-x-ds/global/TabView';
|
import TabView, {Tab} from '../../../../admin-x-ds/global/TabView';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useForm, {Dirtyable} from '../../../../hooks/useForm';
|
import useForm, {Dirtyable} from '../../../../hooks/useForm';
|
||||||
import useQueryParams from '../../../../hooks/useQueryParams';
|
import useQueryParams from '../../../../hooks/useQueryParams';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
|
@ -13,7 +13,7 @@ import StripeLogo from '../../../../assets/images/stripe-emblem.svg';
|
|||||||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||||
import {JSONError} from '../../../../utils/errors';
|
import {JSONError} from '../../../../utils/errors';
|
||||||
@ -64,7 +64,7 @@ const Connect: React.FC = () => {
|
|||||||
|
|
||||||
const saveTier = async () => {
|
const saveTier = async () => {
|
||||||
const {data} = await fetchActiveTiers();
|
const {data} = await fetchActiveTiers();
|
||||||
const tier = data?.tiers[0];
|
const tier = data?.pages[0].tiers[0];
|
||||||
|
|
||||||
if (tier) {
|
if (tier) {
|
||||||
tier.monthly_price = 500;
|
tier.monthly_price = 500;
|
||||||
|
@ -13,7 +13,7 @@ import TextField from '../../../../admin-x-ds/global/form/TextField';
|
|||||||
import TierDetailPreview from './TierDetailPreview';
|
import TierDetailPreview from './TierDetailPreview';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import URLTextField from '../../../../admin-x-ds/global/form/URLTextField';
|
import URLTextField from '../../../../admin-x-ds/global/form/URLTextField';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useForm from '../../../../hooks/useForm';
|
import useForm from '../../../../hooks/useForm';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||||
@ -223,9 +223,9 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
|||||||
containerClassName='font-medium'
|
containerClassName='font-medium'
|
||||||
controlClasses={{menu: 'w-14'}}
|
controlClasses={{menu: 'w-14'}}
|
||||||
options={currencySelectGroups()}
|
options={currencySelectGroups()}
|
||||||
selectedOption={formState.currency}
|
selectedOption={currencySelectGroups().flatMap(group => group.options).find(option => option.value === formState.currency)}
|
||||||
size='xs'
|
size='xs'
|
||||||
onSelect={currency => updateForm(state => ({...state, currency}))}
|
onSelect={option => updateForm(state => ({...state, currency: option?.value}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -339,15 +339,21 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TierDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
const TierDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
||||||
const {data: {tiers} = {}} = useBrowseTiers();
|
const {data: {tiers, isEnd} = {}, fetchNextPage} = useBrowseTiers();
|
||||||
|
|
||||||
let tier: Tier | undefined;
|
let tier: Tier | undefined;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (params?.id && !tier && !isEnd) {
|
||||||
|
fetchNextPage();
|
||||||
|
}
|
||||||
|
}, [fetchNextPage, isEnd, params?.id, tier]);
|
||||||
|
|
||||||
if (params?.id) {
|
if (params?.id) {
|
||||||
tier = tiers?.find(({id}) => id === params?.id);
|
tier = tiers?.find(({id}) => id === params?.id);
|
||||||
|
|
||||||
if (!tier) {
|
if (!tier) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import StickyFooter from '../../../admin-x-ds/global/StickyFooter';
|
|||||||
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
import TabView, {Tab} from '../../../admin-x-ds/global/TabView';
|
||||||
import ThemePreview from './designAndBranding/ThemePreview';
|
import ThemePreview from './designAndBranding/ThemePreview';
|
||||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import useForm from '../../../hooks/useForm';
|
import useForm from '../../../hooks/useForm';
|
||||||
import useRouting from '../../../hooks/useRouting';
|
import useRouting from '../../../hooks/useRouting';
|
||||||
import {CustomThemeSetting, useBrowseCustomThemeSettings, useEditCustomThemeSettings} from '../../../api/customThemeSettings';
|
import {CustomThemeSetting, useBrowseCustomThemeSettings, useEditCustomThemeSettings} from '../../../api/customThemeSettings';
|
||||||
|
@ -12,7 +12,7 @@ import React, {useEffect, useState} from 'react';
|
|||||||
import TabView from '../../../admin-x-ds/global/TabView';
|
import TabView from '../../../admin-x-ds/global/TabView';
|
||||||
import ThemeInstalledModal from './theme/ThemeInstalledModal';
|
import ThemeInstalledModal from './theme/ThemeInstalledModal';
|
||||||
import ThemePreview from './theme/ThemePreview';
|
import ThemePreview from './theme/ThemePreview';
|
||||||
import handleError from '../../../utils/handleError';
|
import handleError from '../../../utils/api/handleError';
|
||||||
import useRouting from '../../../hooks/useRouting';
|
import useRouting from '../../../hooks/useRouting';
|
||||||
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
|
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
|
||||||
import {InstalledTheme, Theme, ThemesInstallResponseType, useBrowseThemes, useInstallTheme, useUploadTheme} from '../../../api/themes';
|
import {InstalledTheme, Theme, ThemesInstallResponseType, useBrowseThemes, useInstallTheme, useUploadTheme} from '../../../api/themes';
|
||||||
|
@ -6,7 +6,7 @@ import React, {useRef, useState} from 'react';
|
|||||||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import UnsplashSearchModal from '../../../../utils/unsplash/UnsplashSearchModal';
|
import UnsplashSearchModal from '../../../../utils/unsplash/UnsplashSearchModal';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import usePinturaEditor from '../../../../hooks/usePinturaEditor';
|
import usePinturaEditor from '../../../../hooks/usePinturaEditor';
|
||||||
import {SettingValue, getSettingValues} from '../../../../api/settings';
|
import {SettingValue, getSettingValues} from '../../../../api/settings';
|
||||||
import {debounce} from '../../../../utils/debounce';
|
import {debounce} from '../../../../utils/debounce';
|
||||||
|
@ -7,7 +7,7 @@ import Select from '../../../../admin-x-ds/global/form/Select';
|
|||||||
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
import SettingGroupContent from '../../../../admin-x-ds/settings/SettingGroupContent';
|
||||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import {CustomThemeSetting} from '../../../../api/customThemeSettings';
|
import {CustomThemeSetting} from '../../../../api/customThemeSettings';
|
||||||
import {getImageUrl, useUploadImage} from '../../../../api/images';
|
import {getImageUrl, useUploadImage} from '../../../../api/images';
|
||||||
import {humanizeSettingKey} from '../../../../api/settings';
|
import {humanizeSettingKey} from '../../../../api/settings';
|
||||||
@ -52,9 +52,9 @@ const ThemeSetting: React.FC<{
|
|||||||
<Select
|
<Select
|
||||||
hint={setting.description}
|
hint={setting.description}
|
||||||
options={setting.options.map(option => ({label: option, value: option}))}
|
options={setting.options.map(option => ({label: option, value: option}))}
|
||||||
selectedOption={setting.value}
|
selectedOption={{label: setting.value, value: setting.value}}
|
||||||
title={humanizeSettingKey(setting.key)}
|
title={humanizeSettingKey(setting.key)}
|
||||||
onSelect={value => setSetting(value || null)}
|
onSelect={option => setSetting(option?.value || null)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'color':
|
case 'color':
|
||||||
|
@ -3,7 +3,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import RecommendationReasonForm from './RecommendationReasonForm';
|
import RecommendationReasonForm from './RecommendationReasonForm';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useForm from '../../../../hooks/useForm';
|
import useForm from '../../../../hooks/useForm';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {EditOrAddRecommendation, useAddRecommendation} from '../../../../api/recommendations';
|
import {EditOrAddRecommendation, useAddRecommendation} from '../../../../api/recommendations';
|
||||||
|
@ -3,7 +3,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
|||||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import RecommendationReasonForm from './RecommendationReasonForm';
|
import RecommendationReasonForm from './RecommendationReasonForm';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import useForm from '../../../../hooks/useForm';
|
import useForm from '../../../../hooks/useForm';
|
||||||
import useRouting from '../../../../hooks/useRouting';
|
import useRouting from '../../../../hooks/useRouting';
|
||||||
import {Recommendation, useDeleteRecommendation, useEditRecommendation} from '../../../../api/recommendations';
|
import {Recommendation, useDeleteRecommendation, useEditRecommendation} from '../../../../api/recommendations';
|
||||||
|
@ -6,7 +6,7 @@ import Menu from '../../../../admin-x-ds/global/Menu';
|
|||||||
import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
|
import ModalPage from '../../../../admin-x-ds/global/modal/ModalPage';
|
||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
|
import {Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, useActivateTheme, useDeleteTheme} from '../../../../api/themes';
|
||||||
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
|
import {downloadFile, getGhostPaths} from '../../../../utils/helpers';
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import List from '../../../../admin-x-ds/global/List';
|
|||||||
import ListItem from '../../../../admin-x-ds/global/ListItem';
|
import ListItem from '../../../../admin-x-ds/global/ListItem';
|
||||||
import NiceModal from '@ebay/nice-modal-react';
|
import NiceModal from '@ebay/nice-modal-react';
|
||||||
import React, {ReactNode, useState} from 'react';
|
import React, {ReactNode, useState} from 'react';
|
||||||
import handleError from '../../../../utils/handleError';
|
import handleError from '../../../../utils/api/handleError';
|
||||||
import {ConfirmationModalContent} from '../../../../admin-x-ds/global/modal/ConfirmationModal';
|
import {ConfirmationModalContent} from '../../../../admin-x-ds/global/modal/ConfirmationModal';
|
||||||
import {InstalledTheme, ThemeProblem, useActivateTheme} from '../../../../api/themes';
|
import {InstalledTheme, ThemeProblem, useActivateTheme} from '../../../../api/themes';
|
||||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||||
|
66
apps/admin-x-settings/src/hooks/useFilterableApi.ts
Normal file
66
apps/admin-x-settings/src/hooks/useFilterableApi.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {Meta, apiUrl, useFetchApi} from '../utils/api/hooks';
|
||||||
|
import {useRef} from 'react';
|
||||||
|
|
||||||
|
const escapeNqlString = (value: string) => {
|
||||||
|
return '\'' + value.replace(/'/g, '\\\'') + '\'';
|
||||||
|
};
|
||||||
|
|
||||||
|
const useFilterableApi = <
|
||||||
|
Data extends {id: string} & {[Key in FilterKey]: string},
|
||||||
|
ResponseKey extends string = string,
|
||||||
|
FilterKey extends string = string
|
||||||
|
>({path, filterKey, responseKey, limit = 20}: {
|
||||||
|
path: string
|
||||||
|
filterKey: FilterKey
|
||||||
|
responseKey: ResponseKey
|
||||||
|
limit?: number
|
||||||
|
}) => {
|
||||||
|
const fetchApi = useFetchApi();
|
||||||
|
|
||||||
|
const result = useRef<{
|
||||||
|
data?: Data[];
|
||||||
|
allLoaded?: boolean;
|
||||||
|
lastInput?: string;
|
||||||
|
}>({});
|
||||||
|
|
||||||
|
const loadData = async (input: string) => {
|
||||||
|
if ((result.current.allLoaded || result.current.lastInput === input) && result.current.data) {
|
||||||
|
return result.current.data.filter(item => item[filterKey]?.toLowerCase().includes(input.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchApi<{meta?: Meta} & {[k in ResponseKey]: Data[]}>(apiUrl(path, {
|
||||||
|
filter: input ? `${filterKey}:~${escapeNqlString(input)}` : '',
|
||||||
|
limit: limit.toString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
result.current.data = response[responseKey];
|
||||||
|
result.current.allLoaded = !input && !response.meta?.pagination.next;
|
||||||
|
result.current.lastInput = input;
|
||||||
|
|
||||||
|
return response[responseKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadData,
|
||||||
|
|
||||||
|
loadInitialValues: async (ids: string[]) => {
|
||||||
|
await loadData('');
|
||||||
|
|
||||||
|
const data = [...(result.current.data || [])];
|
||||||
|
const missingIds = ids.filter(id => !result.current.data?.find(({id: dataId}) => dataId === id));
|
||||||
|
|
||||||
|
if (missingIds.length) {
|
||||||
|
const additionalData = await fetchApi<{meta?: Meta} & {[k in ResponseKey]: Data[]}>(apiUrl(path, {
|
||||||
|
filter: `id:[${missingIds.join(',')}]`,
|
||||||
|
limit: 'all'
|
||||||
|
}));
|
||||||
|
|
||||||
|
data.push(...additionalData[responseKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids.map(id => data.find(({id: dataId}) => dataId === id)!);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useFilterableApi;
|
@ -61,7 +61,7 @@ export const useLimiter = () => {
|
|||||||
enabled: false
|
enabled: false
|
||||||
});
|
});
|
||||||
const {refetch: fetchNewsletters} = useBrowseNewsletters({
|
const {refetch: fetchNewsletters} = useBrowseNewsletters({
|
||||||
searchParams: {filter: 'status:active', limit: 'all'},
|
searchParams: {filter: 'status:active', limit: '1'},
|
||||||
enabled: false
|
enabled: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -74,16 +74,17 @@ export const useLimiter = () => {
|
|||||||
}, [config.hostSettings?.billing]);
|
}, [config.hostSettings?.billing]);
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const limits = config.hostSettings?.limits as LimiterLimits;
|
if (!LimitService || !config.hostSettings?.limits || isLoading) {
|
||||||
|
|
||||||
if (!LimitService || !limits || isLoading) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const limits = {...config.hostSettings.limits} as LimiterLimits;
|
||||||
const limiter = new LimitService();
|
const limiter = new LimitService();
|
||||||
|
|
||||||
if (limits.staff) {
|
if (limits.staff) {
|
||||||
limits.staff.currentCountQuery = async () => {
|
limits.staff.currentCountQuery = async () => {
|
||||||
|
// useStaffUsers will only return the first 100 users by default, but we can assume
|
||||||
|
// that either there's no limit or the limit is <100
|
||||||
const staffUsers = users.filter(u => u.status !== 'inactive' && !contributorUsers.includes(u));
|
const staffUsers = users.filter(u => u.status !== 'inactive' && !contributorUsers.includes(u));
|
||||||
const staffInvites = invites.filter(i => i.role !== 'Contributor');
|
const staffInvites = invites.filter(i => i.role !== 'Contributor');
|
||||||
|
|
||||||
@ -100,8 +101,8 @@ export const useLimiter = () => {
|
|||||||
|
|
||||||
if (limits.newsletters) {
|
if (limits.newsletters) {
|
||||||
limits.newsletters.currentCountQuery = async () => {
|
limits.newsletters.currentCountQuery = async () => {
|
||||||
const {data: {newsletters} = {newsletters: []}} = await fetchNewsletters();
|
const {data: {pages} = {pages: []}} = await fetchNewsletters();
|
||||||
return newsletters?.length || 0;
|
return pages[0].meta?.pagination.total || 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Meta} from '../utils/apiRequests';
|
import {Meta} from '../utils/api/hooks';
|
||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
export interface PaginationData {
|
export interface PaginationData {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, {useEffect, useRef, useState} from 'react';
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
import handleError from '../utils/handleError';
|
import handleError from '../utils/api/handleError';
|
||||||
import useForm, {ErrorMessages, SaveState} from './useForm';
|
import useForm, {ErrorMessages, SaveState} from './useForm';
|
||||||
import useGlobalDirtyState from './useGlobalDirtyState';
|
import useGlobalDirtyState from './useGlobalDirtyState';
|
||||||
import {Setting, SettingValue, useEditSettings} from '../api/settings';
|
import {Setting, SettingValue, useEditSettings} from '../api/settings';
|
||||||
|
@ -14,6 +14,8 @@ export type UsersHook = {
|
|||||||
contributorUsers: User[];
|
contributorUsers: User[];
|
||||||
currentUser: User|null;
|
currentUser: User|null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
hasNextPage?: boolean;
|
||||||
|
fetchNextPage: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getUsersByRole(users: User[], role: string): User[] {
|
function getUsersByRole(users: User[], role: string): User[] {
|
||||||
@ -30,7 +32,7 @@ function getOwnerUser(users: User[]): User {
|
|||||||
|
|
||||||
const useStaffUsers = (): UsersHook => {
|
const useStaffUsers = (): UsersHook => {
|
||||||
const {currentUser} = useGlobalData();
|
const {currentUser} = useGlobalData();
|
||||||
const {data: {users} = {users: []}, isLoading: usersLoading} = useBrowseUsers();
|
const {data: {users, isEnd} = {users: []}, isLoading: usersLoading, fetchNextPage} = useBrowseUsers();
|
||||||
const {data: {invites} = {invites: []}, isLoading: invitesLoading} = useBrowseInvites();
|
const {data: {invites} = {invites: []}, isLoading: invitesLoading} = useBrowseInvites();
|
||||||
const {data: {roles} = {}, isLoading: rolesLoading} = useBrowseRoles();
|
const {data: {roles} = {}, isLoading: rolesLoading} = useBrowseRoles();
|
||||||
|
|
||||||
@ -58,7 +60,9 @@ const useStaffUsers = (): UsersHook => {
|
|||||||
contributorUsers,
|
contributorUsers,
|
||||||
currentUser,
|
currentUser,
|
||||||
invites: mappedInvites,
|
invites: mappedInvites,
|
||||||
isLoading: usersLoading || invitesLoading || rolesLoading
|
isLoading: usersLoading || invitesLoading || rolesLoading,
|
||||||
|
hasNextPage: isEnd,
|
||||||
|
fetchNextPage
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import {APIError, ValidationError} from './errors';
|
import {APIError, ValidationError} from '../errors';
|
||||||
import {showToast} from '../admin-x-ds/global/Toast';
|
import {showToast} from '../../admin-x-ds/global/Toast';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generic error handling for API calls. This is enabled by default for queries (can be disabled by
|
* Generic error handling for API calls. This is enabled by default for queries (can be disabled by
|
@ -1,4 +1,4 @@
|
|||||||
import {APIError, EmailError, ErrorResponse, HostLimitError, JSONError, MaintenanceError, RequestEntityTooLargeError, ServerUnreachableError, ThemeValidationError, UnsupportedMediaTypeError, ValidationError, VersionMismatchError} from './errors';
|
import {APIError, EmailError, ErrorResponse, HostLimitError, JSONError, MaintenanceError, RequestEntityTooLargeError, ServerUnreachableError, ThemeValidationError, UnsupportedMediaTypeError, ValidationError, VersionMismatchError} from '../errors';
|
||||||
|
|
||||||
const handleResponse = async (response: Response) => {
|
const handleResponse = async (response: Response) => {
|
||||||
if (response.status === 0) {
|
if (response.status === 0) {
|
@ -1,12 +1,12 @@
|
|||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
import handleError from './handleError';
|
import handleError from './handleError';
|
||||||
import handleResponse from './handleResponse';
|
import handleResponse from './handleResponse';
|
||||||
import {APIError, MaintenanceError, ServerUnreachableError, TimeoutError} from './errors';
|
import {APIError, MaintenanceError, ServerUnreachableError, TimeoutError} from '../errors';
|
||||||
import {QueryClient, UseInfiniteQueryOptions, UseQueryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
import {QueryClient, UseInfiniteQueryOptions, UseQueryOptions, useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query';
|
||||||
import {getGhostPaths} from './helpers';
|
import {getGhostPaths} from '../helpers';
|
||||||
import {useEffect, useMemo} from 'react';
|
import {useEffect, useMemo} from 'react';
|
||||||
import {usePage, usePagination} from '../hooks/usePagination';
|
import {usePage, usePagination} from '../../hooks/usePagination';
|
||||||
import {useSentryDSN, useServices} from '../components/providers/ServiceProvider';
|
import {useSentryDSN, useServices} from '../../components/providers/ServiceProvider';
|
||||||
|
|
||||||
export interface Meta {
|
export interface Meta {
|
||||||
pagination: {
|
pagination: {
|
||||||
@ -33,7 +33,8 @@ export const useFetchApi = () => {
|
|||||||
const {ghostVersion} = useServices();
|
const {ghostVersion} = useServices();
|
||||||
const sentrydsn = useSentryDSN();
|
const sentrydsn = useSentryDSN();
|
||||||
|
|
||||||
return async (endpoint: string | URL, options: RequestOptions = {}) => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
return async <Response = any>(endpoint: string | URL, options: RequestOptions = {}) => {
|
||||||
// By default, we set the Content-Type header to application/json
|
// By default, we set the Content-Type header to application/json
|
||||||
const defaultHeaders: Record<string, string> = {
|
const defaultHeaders: Record<string, string> = {
|
||||||
'app-pragma': 'no-cache',
|
'app-pragma': 'no-cache',
|
||||||
@ -91,7 +92,7 @@ export const useFetchApi = () => {
|
|||||||
Sentry.captureMessage('Request took multiple attempts', {extra: {attempts, retryingMs, endpoint: endpoint.toString()}});
|
Sentry.captureMessage('Request took multiple attempts', {extra: {attempts, retryingMs, endpoint: endpoint.toString()}});
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleResponse(response);
|
return handleResponse(response) as Response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
retryingMs = Date.now() - startTime;
|
retryingMs = Date.now() - startTime;
|
||||||
|
|
||||||
@ -160,7 +161,7 @@ export const createQuery = <ResponseData>(options: QueryOptions<ResponseData>) =
|
|||||||
|
|
||||||
const data = useMemo(() => (
|
const data = useMemo(() => (
|
||||||
(result.data && options.returnData) ? options.returnData(result.data) : result.data)
|
(result.data && options.returnData) ? options.returnData(result.data) : result.data)
|
||||||
, [result]);
|
, [result.data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (result.error && query.defaultErrorHandler !== false) {
|
if (result.error && query.defaultErrorHandler !== false) {
|
||||||
@ -217,25 +218,28 @@ export const createPaginatedQuery = <ResponseData extends {meta?: Meta}>(options
|
|||||||
|
|
||||||
type InfiniteQueryOptions<ResponseData> = Omit<QueryOptions<ResponseData>, 'returnData'> & {
|
type InfiniteQueryOptions<ResponseData> = Omit<QueryOptions<ResponseData>, 'returnData'> & {
|
||||||
returnData: NonNullable<QueryOptions<ResponseData>['returnData']>
|
returnData: NonNullable<QueryOptions<ResponseData>['returnData']>
|
||||||
|
defaultNextPageParams?: (data: ResponseData, params: Record<string, string>) => Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type InfiniteQueryHookOptions<ResponseData> = UseInfiniteQueryOptions<ResponseData> & {
|
type InfiniteQueryHookOptions<ResponseData> = UseInfiniteQueryOptions<ResponseData> & {
|
||||||
searchParams?: Record<string, string>;
|
searchParams?: Record<string, string>;
|
||||||
defaultErrorHandler?: boolean;
|
defaultErrorHandler?: boolean;
|
||||||
getNextPageParams: (data: ResponseData, params: Record<string, string>) => Record<string, string>|undefined;
|
getNextPageParams?: (data: ResponseData, params: Record<string, string>) => Record<string, string> | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createInfiniteQuery = <ResponseData>(options: InfiniteQueryOptions<ResponseData>) => ({searchParams, getNextPageParams, ...query}: InfiniteQueryHookOptions<ResponseData>) => {
|
export const createInfiniteQuery = <ResponseData>(options: InfiniteQueryOptions<ResponseData>) => ({searchParams, getNextPageParams, ...query}: InfiniteQueryHookOptions<ResponseData> = {}) => {
|
||||||
const fetchApi = useFetchApi();
|
const fetchApi = useFetchApi();
|
||||||
|
|
||||||
|
const nextPageParams = getNextPageParams || options.defaultNextPageParams || (() => ({}));
|
||||||
|
|
||||||
const result = useInfiniteQuery<ResponseData>({
|
const result = useInfiniteQuery<ResponseData>({
|
||||||
queryKey: [options.dataType, apiUrl(options.path, searchParams || options.defaultSearchParams)],
|
queryKey: [options.dataType, apiUrl(options.path, searchParams || options.defaultSearchParams)],
|
||||||
queryFn: ({pageParam}) => fetchApi(apiUrl(options.path, pageParam || searchParams || options.defaultSearchParams)),
|
queryFn: ({pageParam}) => fetchApi(apiUrl(options.path, pageParam || searchParams || options.defaultSearchParams)),
|
||||||
getNextPageParam: data => getNextPageParams(data, searchParams || options.defaultSearchParams || {}),
|
getNextPageParam: data => nextPageParams(data, searchParams || options.defaultSearchParams || {}),
|
||||||
...query
|
...query
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = useMemo(() => result.data && options.returnData(result.data), [result]);
|
const data = useMemo(() => result.data && options.returnData(result.data), [result.data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (result.error && query.defaultErrorHandler !== false) {
|
if (result.error && query.defaultErrorHandler !== false) {
|
||||||
@ -280,7 +284,7 @@ const mutate = <ResponseData, Payload>({fetchApi, path, payload, searchParams, o
|
|||||||
requestBody = JSON.stringify(generatedBody);
|
requestBody = JSON.stringify(generatedBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fetchApi(url, {
|
return fetchApi<ResponseData>(url, {
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
...requestOptions
|
...requestOptions
|
||||||
});
|
});
|
86
apps/admin-x-settings/src/utils/api/updateQueries.ts
Normal file
86
apps/admin-x-settings/src/utils/api/updateQueries.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import {InfiniteData} from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const insertToQueryCache = <ResponseData>(field: string, recordsToInsert?: (response: ResponseData) => unknown[]) => {
|
||||||
|
return (newData: ResponseData, currentData: unknown) => {
|
||||||
|
if (!currentData) {
|
||||||
|
return currentData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRecords = recordsToInsert || ((response: ResponseData) => (response as Record<string, unknown[]>)[field]);
|
||||||
|
|
||||||
|
if (typeof currentData === 'object' && 'pages' in currentData) {
|
||||||
|
const {pages} = currentData as InfiniteData<ResponseData>;
|
||||||
|
const lastPage = pages.at(-1)!;
|
||||||
|
return {
|
||||||
|
...currentData,
|
||||||
|
pages: pages.slice(0, -1).concat({
|
||||||
|
...lastPage,
|
||||||
|
[field]: (lastPage as Record<string, unknown[]>)[field].concat(getRecords(newData))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentData,
|
||||||
|
[field]: (currentData as Record<string, unknown[]>)[field].concat(getRecords(newData))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateQueryCache = <ResponseData>(field: string, updatedRecords?: (response: ResponseData) => Record<string, unknown>) => {
|
||||||
|
return (newData: ResponseData, currentData: unknown) => {
|
||||||
|
if (!currentData) {
|
||||||
|
return currentData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRecords = updatedRecords || ((response: ResponseData) => {
|
||||||
|
const records = (response as Record<string, {id: string}[]>)[field];
|
||||||
|
|
||||||
|
return records.reduce((result, record) => ({...result, [record.id]: record}), {} as Record<string, unknown>);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = getRecords(newData);
|
||||||
|
|
||||||
|
if (typeof currentData === 'object' && 'pages' in currentData) {
|
||||||
|
const {pages} = currentData as InfiniteData<ResponseData>;
|
||||||
|
return {
|
||||||
|
...currentData,
|
||||||
|
pages: pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
[field]: (page as Record<string, {id: string}[]>)[field].map(current => updated[current.id] || current)
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentData,
|
||||||
|
[field]: (currentData as Record<string, {id: string}[]>)[field].map(current => updated[current.id] || current)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteFromQueryCache = <ResponseData, Payload>(field: string, idsFromPayload?: (payload: Payload) => string[]) => {
|
||||||
|
return (_: ResponseData, currentData: unknown, payload: Payload) => {
|
||||||
|
if (!currentData) {
|
||||||
|
return currentData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedIds = idsFromPayload?.(payload) || [payload as string];
|
||||||
|
|
||||||
|
if (typeof currentData === 'object' && 'pages' in currentData) {
|
||||||
|
const {pages} = currentData as InfiniteData<ResponseData>;
|
||||||
|
return {
|
||||||
|
...currentData,
|
||||||
|
pages: pages.map(page => ({
|
||||||
|
...page,
|
||||||
|
[field]: (page as Record<string, {id: string}[]>)[field].filter(current => !deletedIds.includes(current.id))
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...currentData,
|
||||||
|
[field]: (currentData as Record<string, {id: string}[]>)[field].filter(current => !deletedIds.includes(current.id))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
@ -78,3 +78,7 @@ export function downloadFromEndpoint(path: string) {
|
|||||||
export function numberWithCommas(x: number) {
|
export function numberWithCommas(x: number) {
|
||||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isObjectId(value: string) {
|
||||||
|
return /^[a-z0-9]{24}$/.test(value);
|
||||||
|
}
|
||||||
|
@ -58,9 +58,9 @@ test.describe('Default recipient settings', async () => {
|
|||||||
test('Supports selecting specific tiers, labels and offers', async ({page}) => {
|
test('Supports selecting specific tiers, labels and offers', async ({page}) => {
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseTiers: {method: 'GET', path: '/tiers/?limit=all', response: responseFixtures.tiers},
|
browseTiers: {method: 'GET', path: '/tiers/?filter=&limit=20', response: responseFixtures.tiers},
|
||||||
browseLabels: {method: 'GET', path: '/labels/?limit=all', response: responseFixtures.labels},
|
browseLabels: {method: 'GET', path: '/labels/?filter=&limit=20', response: responseFixtures.labels},
|
||||||
browseOffers: {method: 'GET', path: '/offers/?limit=all', response: responseFixtures.offers},
|
browseOffers: {method: 'GET', path: '/offers/?filter=&limit=20', response: responseFixtures.offers},
|
||||||
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
|
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
|
||||||
{
|
{
|
||||||
key: 'editor_default_email_recipients',
|
key: 'editor_default_email_recipients',
|
||||||
|
@ -5,7 +5,7 @@ test.describe('Newsletter settings', async () => {
|
|||||||
test('Supports creating a new newsletter', async ({page}) => {
|
test('Supports creating a new newsletter', async ({page}) => {
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=all', response: responseFixtures.newsletters},
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||||
addNewsletter: {method: 'POST', path: '/newsletters/?opt_in_existing=true&include=count.active_members%2Ccount.posts', response: {newsletters: [{
|
addNewsletter: {method: 'POST', path: '/newsletters/?opt_in_existing=true&include=count.active_members%2Ccount.posts', response: {newsletters: [{
|
||||||
id: 'new-newsletter',
|
id: 'new-newsletter',
|
||||||
name: 'New newsletter',
|
name: 'New newsletter',
|
||||||
@ -48,7 +48,7 @@ test.describe('Newsletter settings', async () => {
|
|||||||
test('Supports updating a newsletter', async ({page}) => {
|
test('Supports updating a newsletter', async ({page}) => {
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=all', response: responseFixtures.newsletters},
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||||
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||||
newsletters: [{
|
newsletters: [{
|
||||||
...responseFixtures.newsletters.newsletters[0],
|
...responseFixtures.newsletters.newsletters[0],
|
||||||
@ -93,7 +93,7 @@ test.describe('Newsletter settings', async () => {
|
|||||||
test('Displays a prompt when email verification is required', async ({page}) => {
|
test('Displays a prompt when email verification is required', async ({page}) => {
|
||||||
await mockApi({page, requests: {
|
await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=all', response: responseFixtures.newsletters},
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||||
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||||
newsletters: [responseFixtures.newsletters.newsletters[0]],
|
newsletters: [responseFixtures.newsletters.newsletters[0]],
|
||||||
meta: {
|
meta: {
|
||||||
@ -120,7 +120,7 @@ test.describe('Newsletter settings', async () => {
|
|||||||
test('Supports archiving newsletters', async ({page}) => {
|
test('Supports archiving newsletters', async ({page}) => {
|
||||||
const activate = await mockApi({page, requests: {
|
const activate = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=all', response: responseFixtures.newsletters},
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||||
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[1].id}/?include=count.active_members%2Ccount.posts`, response: {
|
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[1].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||||
newsletters: [{
|
newsletters: [{
|
||||||
...responseFixtures.newsletters.newsletters[1],
|
...responseFixtures.newsletters.newsletters[1],
|
||||||
@ -157,7 +157,7 @@ test.describe('Newsletter settings', async () => {
|
|||||||
|
|
||||||
const archive = await mockApi({page, requests: {
|
const archive = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=all', response: responseFixtures.newsletters},
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters},
|
||||||
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
editNewsletter: {method: 'PUT', path: `/newsletters/${responseFixtures.newsletters.newsletters[0].id}/?include=count.active_members%2Ccount.posts`, response: {
|
||||||
newsletters: [{
|
newsletters: [{
|
||||||
...responseFixtures.newsletters.newsletters[0],
|
...responseFixtures.newsletters.newsletters[0],
|
||||||
@ -206,7 +206,7 @@ test.describe('Newsletter settings', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=all', response: responseFixtures.newsletters}
|
browseNewsletters: {method: 'GET', path: '/newsletters/?include=count.active_members%2Ccount.posts&limit=20', response: responseFixtures.newsletters}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
@ -7,7 +7,7 @@ test.describe('User actions', async () => {
|
|||||||
|
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||||
users: [{
|
users: [{
|
||||||
...userToEdit,
|
...userToEdit,
|
||||||
@ -50,7 +50,7 @@ test.describe('User actions', async () => {
|
|||||||
|
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: {
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: {
|
||||||
users: [
|
users: [
|
||||||
...responseFixtures.users.users.filter(user => user.email !== 'author@test.com'),
|
...responseFixtures.users.users.filter(user => user.email !== 'author@test.com'),
|
||||||
{
|
{
|
||||||
@ -103,7 +103,7 @@ test.describe('User actions', async () => {
|
|||||||
|
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
deleteUser: {method: 'DELETE', path: `/users/${authorUser.id}/`, response: {}}
|
deleteUser: {method: 'DELETE', path: `/users/${authorUser.id}/`, response: {}}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@ -151,7 +151,7 @@ test.describe('User actions', async () => {
|
|||||||
|
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
editUser: {method: 'PUT', path: /^\/users\/\w{24}\/\?include=roles$/, response: responseFixtures.users},
|
editUser: {method: 'PUT', path: /^\/users\/\w{24}\/\?include=roles$/, response: responseFixtures.users},
|
||||||
makeOwner: {method: 'PUT', path: '/users/owner/', response: makeOwnerResponse}
|
makeOwner: {method: 'PUT', path: '/users/owner/', response: makeOwnerResponse}
|
||||||
}});
|
}});
|
||||||
@ -206,7 +206,7 @@ test.describe('User actions', async () => {
|
|||||||
await mockApi({page, requests: {
|
await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
...limitRequests,
|
...limitRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: {
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: {
|
||||||
users: [
|
users: [
|
||||||
...responseFixtures.users.users.filter(user => user.email !== 'author@test.com'),
|
...responseFixtures.users.users.filter(user => user.email !== 'author@test.com'),
|
||||||
{
|
{
|
||||||
|
@ -8,7 +8,7 @@ test.describe('User invitations', async () => {
|
|||||||
|
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
browseInvites: {method: 'GET', path: '/invites/', response: responseFixtures.invites},
|
browseInvites: {method: 'GET', path: '/invites/', response: responseFixtures.invites},
|
||||||
browseRoles: {method: 'GET', path: '/roles/?limit=all', response: responseFixtures.roles},
|
browseRoles: {method: 'GET', path: '/roles/?limit=all', response: responseFixtures.roles},
|
||||||
browseAssignableRoles: {method: 'GET', path: '/roles/?limit=all&permissions=assign', response: responseFixtures.roles},
|
browseAssignableRoles: {method: 'GET', path: '/roles/?limit=all&permissions=assign', response: responseFixtures.roles},
|
||||||
@ -61,7 +61,7 @@ test.describe('User invitations', async () => {
|
|||||||
test('Supports resending invitations', async ({page}) => {
|
test('Supports resending invitations', async ({page}) => {
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
browseInvites: {method: 'GET', path: '/invites/', response: responseFixtures.invites},
|
browseInvites: {method: 'GET', path: '/invites/', response: responseFixtures.invites},
|
||||||
deleteInvite: {method: 'DELETE', path: `/invites/${responseFixtures.invites.invites[0].id}/`, response: {}},
|
deleteInvite: {method: 'DELETE', path: `/invites/${responseFixtures.invites.invites[0].id}/`, response: {}},
|
||||||
addInvite: {method: 'POST', path: '/invites/', response: responseFixtures.invites}
|
addInvite: {method: 'POST', path: '/invites/', response: responseFixtures.invites}
|
||||||
@ -97,7 +97,7 @@ test.describe('User invitations', async () => {
|
|||||||
test('Supports revoking invitations', async ({page}) => {
|
test('Supports revoking invitations', async ({page}) => {
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
browseInvites: {method: 'GET', path: '/invites/', response: responseFixtures.invites},
|
browseInvites: {method: 'GET', path: '/invites/', response: responseFixtures.invites},
|
||||||
deleteInvite: {method: 'DELETE', path: `/invites/${responseFixtures.invites.invites[0].id}/`, response: {}}
|
deleteInvite: {method: 'DELETE', path: `/invites/${responseFixtures.invites.invites[0].id}/`, response: {}}
|
||||||
}});
|
}});
|
||||||
|
@ -7,7 +7,7 @@ test.describe('User profile', async () => {
|
|||||||
|
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||||
users: [{
|
users: [{
|
||||||
...userToEdit,
|
...userToEdit,
|
||||||
@ -68,7 +68,7 @@ test.describe('User profile', async () => {
|
|||||||
test('Supports changing password', async ({page}) => {
|
test('Supports changing password', async ({page}) => {
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
updatePassword: {method: 'PUT', path: '/users/password/', response: {}}
|
updatePassword: {method: 'PUT', path: '/users/password/', response: {}}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
@ -109,7 +109,7 @@ test.describe('User profile', async () => {
|
|||||||
|
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
uploadImage: {method: 'POST', path: '/images/upload/', response: {images: [{url: 'http://example.com/image.png', ref: null}]}},
|
uploadImage: {method: 'POST', path: '/images/upload/', response: {images: [{url: 'http://example.com/image.png', ref: null}]}},
|
||||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||||
users: [{
|
users: [{
|
||||||
|
@ -5,7 +5,7 @@ test.describe('User roles', async () => {
|
|||||||
test('Shows users under their role', async ({page}) => {
|
test('Shows users under their role', async ({page}) => {
|
||||||
await mockApi({page, requests: {
|
await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users}
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@ -42,7 +42,7 @@ test.describe('User roles', async () => {
|
|||||||
|
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
browseRoles: {method: 'GET', path: '/roles/?limit=all', response: responseFixtures.roles},
|
browseRoles: {method: 'GET', path: '/roles/?limit=all', response: responseFixtures.roles},
|
||||||
browseAssignableRoles: {method: 'GET', path: '/roles/?limit=all&permissions=assign', response: responseFixtures.roles},
|
browseAssignableRoles: {method: 'GET', path: '/roles/?limit=all&permissions=assign', response: responseFixtures.roles},
|
||||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||||
|
@ -46,7 +46,7 @@ test.describe('Access settings', async () => {
|
|||||||
test('Supports selecting specific tiers', async ({page}) => {
|
test('Supports selecting specific tiers', async ({page}) => {
|
||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseTiers: {method: 'GET', path: '/tiers/?limit=all', response: responseFixtures.tiers},
|
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: responseFixtures.tiers},
|
||||||
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
|
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
|
||||||
{key: 'default_content_visibility', value: 'tiers'},
|
{key: 'default_content_visibility', value: 'tiers'},
|
||||||
{key: 'default_content_visibility_tiers', value: JSON.stringify(responseFixtures.tiers.tiers.map(tier => tier.id))}
|
{key: 'default_content_visibility_tiers', value: JSON.stringify(responseFixtures.tiers.tiers.map(tier => tier.id))}
|
||||||
|
@ -13,7 +13,7 @@ test.describe('Tier settings', async () => {
|
|||||||
await mockApi({page, requests: {
|
await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
||||||
browseTiers: {method: 'GET', path: '/tiers/?limit=all', response: responseFixtures.tiers}
|
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: responseFixtures.tiers}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@ -55,7 +55,7 @@ test.describe('Tier settings', async () => {
|
|||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
addTier: {method: 'POST', path: '/tiers/', response: {tiers: [newTier]}},
|
addTier: {method: 'POST', path: '/tiers/', response: {tiers: [newTier]}},
|
||||||
// This request will be reloaded after the new tier is added
|
// This request will be reloaded after the new tier is added
|
||||||
browseTiers: {method: 'GET', path: '/tiers/?limit=all', response: {tiers: [...responseFixtures.tiers.tiers, newTier]}}
|
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: {tiers: [...responseFixtures.tiers.tiers, newTier]}}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||||
@ -77,7 +77,7 @@ test.describe('Tier settings', async () => {
|
|||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
||||||
browseTiers: {method: 'GET', path: '/tiers/?limit=all', response: responseFixtures.tiers},
|
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: responseFixtures.tiers},
|
||||||
editTier: {method: 'PUT', path: `/tiers/${responseFixtures.tiers.tiers[1].id}/`, response: {
|
editTier: {method: 'PUT', path: `/tiers/${responseFixtures.tiers.tiers[1].id}/`, response: {
|
||||||
tiers: [{
|
tiers: [{
|
||||||
...responseFixtures.tiers.tiers[1],
|
...responseFixtures.tiers.tiers[1],
|
||||||
@ -140,7 +140,7 @@ test.describe('Tier settings', async () => {
|
|||||||
const {lastApiRequests} = await mockApi({page, requests: {
|
const {lastApiRequests} = await mockApi({page, requests: {
|
||||||
...globalDataRequests,
|
...globalDataRequests,
|
||||||
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
browseSettings: {...globalDataRequests.browseSettings, response: settingsWithStripe},
|
||||||
browseTiers: {method: 'GET', path: '/tiers/?limit=all', response: responseFixtures.tiers},
|
browseTiers: {method: 'GET', path: '/tiers/?limit=20', response: responseFixtures.tiers},
|
||||||
editTier: {method: 'PUT', path: `/tiers/${responseFixtures.tiers.tiers[0].id}/`, response: {
|
editTier: {method: 'PUT', path: `/tiers/${responseFixtures.tiers.tiers[0].id}/`, response: {
|
||||||
tiers: [{
|
tiers: [{
|
||||||
...responseFixtures.tiers.tiers[0],
|
...responseFixtures.tiers.tiers[0],
|
||||||
|
@ -55,10 +55,10 @@ export const globalDataRequests = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const limitRequests = {
|
export const limitRequests = {
|
||||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: responseFixtures.users},
|
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: responseFixtures.users},
|
||||||
browseInvites: {method: 'GET', path: '/invites/', response: responseFixtures.invites},
|
browseInvites: {method: 'GET', path: '/invites/', response: responseFixtures.invites},
|
||||||
browseRoles: {method: 'GET', path: '/roles/?limit=all', response: responseFixtures.roles},
|
browseRoles: {method: 'GET', path: '/roles/?limit=all', response: responseFixtures.roles},
|
||||||
browseNewslettersLimit: {method: 'GET', path: '/newsletters/?filter=status%3Aactive&limit=all', response: responseFixtures.newsletters}
|
browseNewslettersLimit: {method: 'GET', path: '/newsletters/?filter=status%3Aactive&limit=1', response: responseFixtures.newsletters}
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function mockApi<Requests extends Record<string, MockRequestConfig>>({page, requests}: {page: Page, requests: Requests}) {
|
export async function mockApi<Requests extends Record<string, MockRequestConfig>>({page, requests}: {page: Page, requests: Requests}) {
|
||||||
|
Loading…
Reference in New Issue
Block a user