mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-27 21:03:29 +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 Heading from '../Heading';
|
||||
import Hint from '../Hint';
|
||||
import React, {useId, useMemo} from 'react';
|
||||
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';
|
||||
type FieldStyles = 'text' | 'dropdown';
|
||||
@ -14,9 +16,22 @@ export type MultiSelectOption = {
|
||||
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>>;
|
||||
values: MultiSelectOption[];
|
||||
defaultOptions?: never;
|
||||
loadOptions?: never;
|
||||
}
|
||||
|
||||
type MultiSelectProps = MultiSelectOptionProps & {
|
||||
values: MultiValue<MultiSelectOption>;
|
||||
title?: string;
|
||||
clearBg?: boolean;
|
||||
error?: boolean;
|
||||
@ -70,7 +85,10 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
size = 'md',
|
||||
fieldStyle = 'dropdown',
|
||||
hint = '',
|
||||
async,
|
||||
options,
|
||||
defaultOptions,
|
||||
loadOptions,
|
||||
values,
|
||||
onChange,
|
||||
canCreate = false,
|
||||
@ -110,60 +128,37 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
return (ddiProps: DropdownIndicatorProps<MultiSelectOption, true>) => <DropdownIndicator {...ddiProps} clearBg={clearBg} fieldStyle={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 (
|
||||
<div className='flex flex-col'>
|
||||
{title && <Heading htmlFor={id} grey useLabelTag>{title}</Heading>}
|
||||
{
|
||||
canCreate ?
|
||||
<CreatableSelect
|
||||
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}
|
||||
/>
|
||||
:
|
||||
<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}
|
||||
/>
|
||||
async ?
|
||||
(canCreate ? <AsyncCreatableSelect {...commonOptions} defaultOptions={defaultOptions} loadOptions={loadOptions} /> : <AsyncSelect {...commonOptions} defaultOptions={defaultOptions} loadOptions={loadOptions} />) :
|
||||
(canCreate ? <CreatableSelect {...commonOptions} options={options} /> : <ReactSelect {...commonOptions} options={options} />)
|
||||
}
|
||||
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
</div>
|
||||
|
@ -78,7 +78,7 @@ export const WithSelectedOption: Story = {
|
||||
args: {
|
||||
title: 'Title',
|
||||
options: selectOptions,
|
||||
selectedOption: 'option-3',
|
||||
selectedOption: selectOptions.find(option => option.value === 'option-3'),
|
||||
hint: 'Here\'s some hint'
|
||||
}
|
||||
};
|
||||
|
@ -1,9 +1,9 @@
|
||||
import React, {useId, useMemo} from 'react';
|
||||
import ReactSelect, {ClearIndicatorProps, DropdownIndicatorProps, OptionProps, Props, components} from 'react-select';
|
||||
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import Heading from '../Heading';
|
||||
import Hint from '../Hint';
|
||||
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';
|
||||
|
||||
export interface SelectOption {
|
||||
@ -31,14 +31,28 @@ export interface SelectControlClasses {
|
||||
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;
|
||||
hideTitle?: boolean;
|
||||
size?: 'xs' | 'md';
|
||||
prompt?: string;
|
||||
options: SelectOption[] | SelectOptionGroup[];
|
||||
selectedOption?: string
|
||||
onSelect: (value: string | undefined) => void;
|
||||
selectedOption?: SelectOption
|
||||
onSelect: (option: SelectOption | null) => void;
|
||||
error?:boolean;
|
||||
hint?: React.ReactNode;
|
||||
clearBg?: boolean;
|
||||
@ -71,6 +85,7 @@ const Option: React.FC<OptionProps<SelectOption, false>> = ({children, ...option
|
||||
);
|
||||
|
||||
const Select: React.FC<SelectProps> = ({
|
||||
async,
|
||||
title,
|
||||
hideTitle,
|
||||
size = 'md',
|
||||
@ -133,39 +148,36 @@ const Select: React.FC<SelectProps> = ({
|
||||
};
|
||||
}, [clearBg]);
|
||||
|
||||
const individualOptions = options.flatMap((option) => {
|
||||
if ('options' in option) {
|
||||
return option.options;
|
||||
}
|
||||
return option;
|
||||
});
|
||||
const customProps = {
|
||||
classNames: {
|
||||
menuList: () => 'z-[300]',
|
||||
valueContainer: () => customClasses.valueContainer,
|
||||
control: () => customClasses.control,
|
||||
placeholder: () => customClasses.placeHolder,
|
||||
menu: () => customClasses.menu,
|
||||
option: () => customClasses.option,
|
||||
noOptionsMessage: () => customClasses.noOptionsMessage,
|
||||
groupHeading: () => customClasses.groupHeading,
|
||||
clearIndicator: () => customClasses.clearIndicator
|
||||
},
|
||||
components: {DropdownIndicator: dropdownIndicatorComponent, Option, ClearIndicator},
|
||||
inputId: id,
|
||||
isClearable: false,
|
||||
options: options,
|
||||
placeholder: prompt ? prompt : '',
|
||||
value: selectedOption,
|
||||
unstyled,
|
||||
onChange: onSelect
|
||||
};
|
||||
|
||||
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]',
|
||||
valueContainer: () => customClasses.valueContainer,
|
||||
control: () => customClasses.control,
|
||||
placeholder: () => customClasses.placeHolder,
|
||||
menu: () => customClasses.menu,
|
||||
option: () => customClasses.option,
|
||||
noOptionsMessage: () => customClasses.noOptionsMessage,
|
||||
groupHeading: () => customClasses.groupHeading,
|
||||
clearIndicator: () => customClasses.clearIndicator
|
||||
}}
|
||||
components={{DropdownIndicator: dropdownIndicatorComponent, Option, ClearIndicator}}
|
||||
inputId={id}
|
||||
isClearable={false}
|
||||
options={options}
|
||||
placeholder={prompt ? prompt : ''}
|
||||
value={individualOptions.find(option => option.value === selectedOption)}
|
||||
unstyled
|
||||
onChange={option => onSelect(option?.value)}
|
||||
{...props}
|
||||
/>
|
||||
{async ?
|
||||
<AsyncSelect<SelectOption, false> {...customProps} {...props} /> :
|
||||
<ReactSelect<SelectOption, false> {...customProps} {...props} />
|
||||
}
|
||||
</div>
|
||||
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
</>
|
||||
|
@ -108,7 +108,11 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
||||
let toolbarLeft = (<></>);
|
||||
if (previewToolbarURLs) {
|
||||
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) {
|
||||
toolbarLeft = <TabView
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {ExternalLink, InternalLink} from '../components/providers/RoutingProvider';
|
||||
import {InfiniteData} from '@tanstack/react-query';
|
||||
import {JSONObject} from './config';
|
||||
import {Meta, createInfiniteQuery} from '../utils/apiRequests';
|
||||
import {Meta, createInfiniteQuery} from '../utils/api/hooks';
|
||||
|
||||
// Types
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {createMutation} from '../utils/api/hooks';
|
||||
|
||||
// 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 interface JSONObject { [key: string]: JSONValue }
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Setting} from './settings';
|
||||
import {createMutation, createQuery} from '../utils/apiRequests';
|
||||
import {createMutation, createQuery} from '../utils/api/hooks';
|
||||
|
||||
type CustomThemeSettingData =
|
||||
{ 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';
|
||||
|
||||
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 = {
|
||||
token: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {useFetchApi} from '../utils/apiRequests';
|
||||
import {useFetchApi} from '../utils/api/hooks';
|
||||
|
||||
export type GhostSiteResponse = {
|
||||
site: {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {createMutation} from '../utils/api/hooks';
|
||||
|
||||
export interface FilesResponseType {
|
||||
files: {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {createMutation} from '../utils/api/hooks';
|
||||
|
||||
export interface ImagesResponseType {
|
||||
images: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {APIKey} from './apiKeys';
|
||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
||||
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
|
||||
import {Webhook} from './webhooks';
|
||||
|
||||
// Types
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
||||
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
|
||||
|
||||
export interface UserInvite {
|
||||
created_at: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Meta, createQuery} from '../utils/apiRequests';
|
||||
import {Meta, createQuery} from '../utils/api/hooks';
|
||||
|
||||
export type Label = {
|
||||
id: string;
|
||||
@ -17,6 +17,5 @@ const dataType = 'LabelsResponseType';
|
||||
|
||||
export const useBrowseLabels = createQuery<LabelsResponseType>({
|
||||
dataType,
|
||||
path: '/labels/',
|
||||
defaultSearchParams: {limit: 'all'}
|
||||
path: '/labels/'
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Meta, createQuery} from '../utils/apiRequests';
|
||||
import {Meta, createQuery} from '../utils/api/hooks';
|
||||
|
||||
export type Member = {
|
||||
id: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Meta, createPaginatedQuery} from '../utils/apiRequests';
|
||||
import {Meta, createPaginatedQuery} from '../utils/api/hooks';
|
||||
|
||||
export type Mention = {
|
||||
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 = {
|
||||
id: string;
|
||||
@ -46,10 +48,24 @@ export interface NewslettersResponseType {
|
||||
|
||||
const dataType = 'NewslettersResponseType';
|
||||
|
||||
export const useBrowseNewsletters = createQuery<NewslettersResponseType>({
|
||||
export const useBrowseNewsletters = createInfiniteQuery<NewslettersResponseType & {isEnd: boolean}>({
|
||||
dataType,
|
||||
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}>({
|
||||
@ -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'}),
|
||||
updateQueries: {
|
||||
dataType,
|
||||
update: (newData, currentData) => (currentData && {
|
||||
...(currentData as NewslettersResponseType),
|
||||
newsletters: (currentData as NewslettersResponseType).newsletters.concat(newData.newsletters)
|
||||
})
|
||||
update: insertToQueryCache('newsletters')
|
||||
}
|
||||
});
|
||||
|
||||
@ -78,12 +91,6 @@ export const useEditNewsletter = createMutation<NewslettersEditResponseType, New
|
||||
defaultSearchParams: {include: 'count.active_members,count.posts'},
|
||||
updateQueries: {
|
||||
dataType,
|
||||
update: (newData, currentData) => (currentData && {
|
||||
...(currentData as NewslettersResponseType),
|
||||
newsletters: (currentData as NewslettersResponseType).newsletters.map((newsletter) => {
|
||||
const newNewsletter = newData.newsletters.find(({id}) => id === newsletter.id);
|
||||
return newNewsletter || newsletter;
|
||||
})
|
||||
})
|
||||
update: updateQueryCache('newsletters')
|
||||
}
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {apiUrl, useFetchApi} from '../utils/apiRequests';
|
||||
import {apiUrl, useFetchApi} from '../utils/api/hooks';
|
||||
|
||||
export type OembedResponse = {
|
||||
metadata: {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Meta, createQuery} from '../utils/apiRequests';
|
||||
import {Meta, createQuery} from '../utils/api/hooks';
|
||||
|
||||
export type Offer = {
|
||||
id: string;
|
||||
@ -30,6 +30,5 @@ const dataType = 'OffersResponseType';
|
||||
|
||||
export const useBrowseOffers = createQuery<OffersResponseType>({
|
||||
dataType,
|
||||
path: '/offers/',
|
||||
defaultSearchParams: {limit: 'all'}
|
||||
path: '/offers/'
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Meta, createQuery} from '../utils/apiRequests';
|
||||
import {Meta, createQuery} from '../utils/api/hooks';
|
||||
|
||||
export type Post = {
|
||||
id: string;
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 = {
|
||||
id: string
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {createMutation} from '../utils/api/hooks';
|
||||
import {downloadFromEndpoint} from '../utils/helpers';
|
||||
|
||||
export const useUploadRedirects = createMutation<unknown, File>({
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createQuery} from '../utils/apiRequests';
|
||||
import {createQuery} from '../utils/api/hooks';
|
||||
|
||||
export type ReferrerHistoryItem = {
|
||||
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';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {createMutation} from '../utils/api/hooks';
|
||||
import {downloadFromEndpoint} from '../utils/helpers';
|
||||
|
||||
export const useUploadRoutes = createMutation<unknown, File>({
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {Config} from './config';
|
||||
import {Meta, createMutation, createQuery} from '../utils/apiRequests';
|
||||
import {Meta, createMutation, createQuery} from '../utils/api/hooks';
|
||||
|
||||
// Types
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createQuery} from '../utils/apiRequests';
|
||||
import {createQuery} from '../utils/api/hooks';
|
||||
|
||||
// Types
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createMutation} from '../utils/apiRequests';
|
||||
import {createMutation} from '../utils/api/hooks';
|
||||
|
||||
export const useTestSlack = createMutation<unknown, null>({
|
||||
method: 'POST',
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Meta, createMutation, createPaginatedQuery} from '../utils/apiRequests';
|
||||
import {Meta, createMutation, createPaginatedQuery} from '../utils/api/hooks';
|
||||
|
||||
export type staffToken = {
|
||||
id: string;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {createMutation, createQuery} from '../utils/apiRequests';
|
||||
import {createMutation, createQuery} from '../utils/api/hooks';
|
||||
import {customThemeSettingsDataType} from './customThemeSettings';
|
||||
|
||||
// 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
|
||||
|
||||
@ -29,11 +31,23 @@ export interface TiersResponseType {
|
||||
|
||||
const dataType = 'TiersResponseType';
|
||||
|
||||
export const useBrowseTiers = createQuery<TiersResponseType>({
|
||||
export const useBrowseTiers = createInfiniteQuery<TiersResponseType & {isEnd: boolean}>({
|
||||
dataType,
|
||||
path: '/tiers/',
|
||||
defaultSearchParams: {
|
||||
limit: 'all'
|
||||
defaultSearchParams: {limit: '20'},
|
||||
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]}),
|
||||
updateQueries: {
|
||||
dataType,
|
||||
update: (newData, currentData) => (currentData && {
|
||||
...(currentData as TiersResponseType),
|
||||
tiers: (currentData as TiersResponseType).tiers.map((tier) => {
|
||||
const newTier = newData.tiers.find(({id}) => id === tier.id);
|
||||
return newTier || tier;
|
||||
})
|
||||
})
|
||||
update: updateQueryCache('tiers')
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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 {deleteFromQueryCache, updateQueryCache} from '../utils/api/updateQueries';
|
||||
|
||||
// Types
|
||||
|
||||
@ -62,18 +64,24 @@ export interface DeleteUserResponse {
|
||||
|
||||
const dataType = 'UsersResponseType';
|
||||
|
||||
const updateUsers = (newData: UsersResponseType, currentData: unknown) => ({
|
||||
...(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>({
|
||||
export const useBrowseUsers = createInfiniteQuery<UsersResponseType & {isEnd: boolean}>({
|
||||
dataType,
|
||||
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>({
|
||||
@ -90,7 +98,7 @@ export const useEditUser = createMutation<UsersResponseType, User>({
|
||||
searchParams: () => ({include: 'roles'}),
|
||||
updateQueries: {
|
||||
dataType,
|
||||
update: updateUsers
|
||||
update: updateQueryCache('users')
|
||||
}
|
||||
});
|
||||
|
||||
@ -99,10 +107,7 @@ export const useDeleteUser = createMutation<DeleteUserResponse, string>({
|
||||
path: id => `/users/${id}/`,
|
||||
updateQueries: {
|
||||
dataType,
|
||||
update: (_, currentData, id) => ({
|
||||
...(currentData as UsersResponseType),
|
||||
users: (currentData as UsersResponseType).users.filter(user => user.id !== id)
|
||||
})
|
||||
update: deleteFromQueryCache('users')
|
||||
}
|
||||
});
|
||||
|
||||
@ -129,7 +134,7 @@ export const useMakeOwner = createMutation<UsersResponseType, string>({
|
||||
}),
|
||||
updateQueries: {
|
||||
dataType,
|
||||
update: updateUsers
|
||||
update: updateQueryCache('users')
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {IntegrationsResponseType, integrationsDataType} from './integrations';
|
||||
import {Meta, createMutation} from '../utils/apiRequests';
|
||||
import {Meta, createMutation} from '../utils/api/hooks';
|
||||
|
||||
// Types
|
||||
|
||||
|
@ -11,10 +11,13 @@ import Popover from '../../../admin-x-ds/global/Popover';
|
||||
import Select, {SelectOption} from '../../../admin-x-ds/global/form/Select';
|
||||
import Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||
import ToggleGroup from '../../../admin-x-ds/global/form/ToggleGroup';
|
||||
import useFilterableApi from '../../../hooks/useFilterableApi';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||
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 {User} from '../../../api/users';
|
||||
import {debounce} from '../../../utils/debounce';
|
||||
import {generateAvatarColor, getInitials} from '../../../utils/helpers';
|
||||
import {useCallback, useState} from 'react';
|
||||
|
||||
@ -73,15 +76,19 @@ const HistoryFilter: React.FC<{
|
||||
toggleResourceType: (resource: string, included: boolean) => void;
|
||||
}> = ({excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => {
|
||||
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 resetStaff = () => {
|
||||
setSearchStaff(null);
|
||||
};
|
||||
|
||||
const userOptions = users.map(user => ({label: user.name, value: user.id}));
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-4'>
|
||||
<Popover position='right' trigger={<Button color='outline' label='Filter' size='sm' />}>
|
||||
@ -102,14 +109,16 @@ const HistoryFilter: React.FC<{
|
||||
</Popover>
|
||||
<div className='w-[200px]'>
|
||||
<Select
|
||||
options={userOptions}
|
||||
loadOptions={debounce(loadOptions, 500)}
|
||||
placeholder='Search staff'
|
||||
value={searchedStaff}
|
||||
async
|
||||
defaultOptions
|
||||
isClearable
|
||||
onSelect={(value) => {
|
||||
if (value) {
|
||||
setSearchStaff(userOptions.find(option => option.value === value)!);
|
||||
updateRoute(`history/view/${value}`);
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setSearchStaff(option);
|
||||
updateRoute(`history/view/${option.value}`);
|
||||
} else {
|
||||
resetStaff();
|
||||
updateRoute('history/view');
|
||||
|
@ -8,7 +8,7 @@ import NoValueLabel from '../../../admin-x-ds/global/NoValueLabel';
|
||||
import React, {useState} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
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 {ReactComponent as AmpIcon} from '../../../assets/icons/amp.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 React, {useEffect, useState} from 'react';
|
||||
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 {HostLimitError, useLimiter} from '../../../../hooks/useLimiter';
|
||||
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 TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
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 {ReactComponent as Icon} from '../../../../assets/icons/amp.svg';
|
||||
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 TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import WebhooksTable from './WebhooksTable';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import handleError from '../../../../utils/api/handleError';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
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 TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
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 {ReactComponent as Icon} from '../../../../assets/icons/firstpromoter.svg';
|
||||
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 NiceModal from '@ebay/nice-modal-react';
|
||||
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 useRouting from '../../../../hooks/useRouting';
|
||||
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 NiceModal from '@ebay/nice-modal-react';
|
||||
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 {ReactComponent as Icon} from '../../../../assets/icons/unsplash.svg';
|
||||
import {Setting, getSettingValues, useEditSettings} from '../../../../api/settings';
|
||||
|
@ -4,7 +4,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import Select from '../../../../admin-x-ds/global/form/Select';
|
||||
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 useForm from '../../../../hooks/useForm';
|
||||
import validator from 'validator';
|
||||
@ -94,11 +94,11 @@ const WebhookModal: React.FC<WebhookModalProps> = ({webhook, integrationId}) =>
|
||||
hint={errors.event}
|
||||
options={webhookEventOptions}
|
||||
prompt='Select an event'
|
||||
selectedOption={formState.event}
|
||||
selectedOption={webhookEventOptions.flatMap(group => group.options).find(option => option.value === formState.event)}
|
||||
title='Event'
|
||||
hideTitle
|
||||
onSelect={(event) => {
|
||||
updateForm(state => ({...state, event}));
|
||||
onSelect={(option) => {
|
||||
updateForm(state => ({...state, event: option?.value}));
|
||||
clearError('event');
|
||||
}}
|
||||
/>
|
||||
|
@ -6,7 +6,7 @@ import TableCell from '../../../../admin-x-ds/global/TableCell';
|
||||
import TableHead from '../../../../admin-x-ds/global/TableHead';
|
||||
import TableRow from '../../../../admin-x-ds/global/TableRow';
|
||||
import WebhookModal from './WebhookModal';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import handleError from '../../../../utils/api/handleError';
|
||||
import {Integration} from '../../../../api/integrations';
|
||||
import {getWebhookEventLabel} from './webhookEventOptions';
|
||||
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 Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||
import NiceModal from '@ebay/nice-modal-react';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import handleError from '../../../../utils/api/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {ReactComponent as ArrowRightIcon} from '../../../../admin-x-ds/assets/icons/arrow-right.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 List from '../../../../admin-x-ds/global/List';
|
||||
import React, {useState} from 'react';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import handleError from '../../../../utils/api/handleError';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
import {downloadRedirects, useUploadRedirects} from '../../../../api/redirects';
|
||||
import {downloadRoutes, useUploadRoutes} from '../../../../api/routes';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
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 {getSettingValue, useEditSettings} from '../../../../api/settings';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
|
@ -5,7 +5,7 @@ import LabItem from './LabItem';
|
||||
import List from '../../../../admin-x-ds/global/List';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-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 {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
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 SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import useDefaultRecipientsOptions from './useDefaultRecipientsOptions';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {GroupBase, MultiValue} from 'react-select';
|
||||
import {MultiValue} from 'react-select';
|
||||
import {getOptionLabel} from '../../../utils/helpers';
|
||||
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';
|
||||
|
||||
type RefipientValueArgs = {
|
||||
@ -39,16 +37,6 @@ const RECIPIENT_FILTER_OPTIONS = [{
|
||||
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({
|
||||
defaultEmailRecipients,
|
||||
defaultEmailRecipientsFilter
|
||||
@ -88,9 +76,7 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
defaultEmailRecipientsFilter
|
||||
}));
|
||||
|
||||
const {data: {tiers} = {}} = useBrowseTiers();
|
||||
const {data: {labels} = {}} = useBrowseLabels();
|
||||
const {data: {offers} = {}} = useBrowseOffers();
|
||||
const {loadOptions, selectedSegments, setSelectedSegments} = useDefaultRecipientsOptions(selectedOption, defaultEmailRecipientsFilter);
|
||||
|
||||
const setDefaultRecipientValue = (value: string) => {
|
||||
if (['visibility', 'disabled'].includes(value)) {
|
||||
@ -115,34 +101,9 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
setSelectedOption(value);
|
||||
};
|
||||
|
||||
const segmentOptionGroups: GroupBase<MultiSelectOption>[] = [
|
||||
{
|
||||
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 updateSelectedSegments = (selected: MultiValue<MultiSelectOption>) => {
|
||||
setSelectedSegments(selected);
|
||||
|
||||
const filters = defaultEmailRecipientsFilter?.split(',') || [];
|
||||
const selectedSegments = segmentOptionGroups
|
||||
.flatMap(({options}) => options)
|
||||
.filter(({value}) => filters.includes(value));
|
||||
|
||||
const setSelectedSegments = (selected: MultiValue<MultiSelectOption>) => {
|
||||
if (selected.length) {
|
||||
const selectedGroups = selected?.map(({value}) => value).join(',');
|
||||
updateSetting('editor_default_email_recipients_filter', selectedGroups);
|
||||
@ -169,21 +130,23 @@ const DefaultRecipients: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<Select
|
||||
hint='Who should be able to subscribe to your site?'
|
||||
options={RECIPIENT_FILTER_OPTIONS}
|
||||
selectedOption={selectedOption}
|
||||
selectedOption={RECIPIENT_FILTER_OPTIONS.find(option => option.value === selectedOption)}
|
||||
title="Default Newsletter recipients"
|
||||
onSelect={(value) => {
|
||||
if (value) {
|
||||
setDefaultRecipientValue(value);
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setDefaultRecipientValue(option.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{(selectedOption === 'segment') && (
|
||||
{(selectedOption === 'segment') && selectedSegments && (
|
||||
<MultiSelect
|
||||
options={segmentOptionGroups.filter(group => group.options.length > 0)}
|
||||
loadOptions={loadOptions}
|
||||
title='Filter'
|
||||
values={selectedSegments}
|
||||
async
|
||||
clearBg
|
||||
onChange={setSelectedSegments}
|
||||
defaultOptions
|
||||
onChange={updateSelectedSegments}
|
||||
/>
|
||||
)}
|
||||
</SettingGroupContent>
|
||||
|
@ -4,7 +4,7 @@ import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
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 {Setting, getSettingValues, useEditSettings} from '../../../api/settings';
|
||||
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 SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
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 {getSettingValues, useEditSettings} from '../../../api/settings';
|
||||
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'>
|
||||
<Select
|
||||
options={MAILGUN_REGIONS}
|
||||
selectedOption={mailgunRegion}
|
||||
selectedOption={MAILGUN_REGIONS.find(option => option.value === mailgunRegion)}
|
||||
title="Mailgun region"
|
||||
onSelect={(value) => {
|
||||
updateSetting('mailgun_base_url', value || null);
|
||||
onSelect={(option) => {
|
||||
updateSetting('mailgun_base_url', option?.value || null);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
|
@ -13,7 +13,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
updateRoute('newsletters/add');
|
||||
};
|
||||
const [selectedTab, setSelectedTab] = useState('active-newsletters');
|
||||
const {data: {newsletters} = {}} = useBrowseNewsletters();
|
||||
const {data: {newsletters, isEnd} = {}, fetchNextPage} = useBrowseNewsletters();
|
||||
|
||||
const buttons = (
|
||||
<Button color='green' label='Add newsletter' link linkWithPadding onClick={() => {
|
||||
@ -43,6 +43,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
title='Newsletters'
|
||||
>
|
||||
<TabView selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
|
||||
{isEnd === false && <Button label='Load more' link onClick={() => fetchNextPage()} />}
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import React, {useEffect} from 'react';
|
||||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
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 useRouting from '../../../../hooks/useRouting';
|
||||
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 Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
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 useForm, {ErrorMessages} from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
@ -160,7 +160,12 @@ const Sidebar: React.FC<{
|
||||
onChange={e => updateNewsletter({sender_email: e.target.value})}
|
||||
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 className='mt-6' gap='sm' margins='lg' title='Member settings'>
|
||||
<Toggle
|
||||
@ -308,8 +313,8 @@ const Sidebar: React.FC<{
|
||||
<Select
|
||||
disabled={!newsletter.show_post_title_section}
|
||||
options={fontOptions}
|
||||
selectedOption={newsletter.title_font_category}
|
||||
onSelect={value => updateNewsletter({title_font_category: value})}
|
||||
selectedOption={fontOptions.find(option => option.value === newsletter.title_font_category)}
|
||||
onSelect={option => updateNewsletter({title_font_category: option?.value})}
|
||||
/>
|
||||
</div>
|
||||
<ButtonGroup buttons={[
|
||||
@ -359,9 +364,9 @@ const Sidebar: React.FC<{
|
||||
/>}
|
||||
<Select
|
||||
options={fontOptions}
|
||||
selectedOption={newsletter.body_font_category}
|
||||
selectedOption={fontOptions.find(option => option.value === newsletter.body_font_category)}
|
||||
title='Body style'
|
||||
onSelect={value => updateNewsletter({body_font_category: value})}
|
||||
onSelect={option => updateNewsletter({body_font_category: option?.value})}
|
||||
/>
|
||||
<Toggle
|
||||
checked={newsletter.show_feature_image}
|
||||
@ -510,9 +515,15 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b
|
||||
};
|
||||
|
||||
const NewsletterDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
||||
const {data: {newsletters} = {}} = useBrowseNewsletters();
|
||||
const {data: {newsletters, isEnd} = {}, fetchNextPage} = useBrowseNewsletters();
|
||||
const newsletter = newsletters?.find(({id}) => id === params?.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (!newsletter && !isEnd) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [fetchNextPage, isEnd, newsletter]);
|
||||
|
||||
if (newsletter) {
|
||||
return <NewsletterDetailModalContent newsletter={newsletter} onlyOne={newsletters!.length === 1} />;
|
||||
} 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 SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
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 useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
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 Radio from '../../../admin-x-ds/global/form/Radio';
|
||||
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 validator from 'validator';
|
||||
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 timezoneOptions = timezoneData.map((tzOption: TimezoneDataDropdownOption) => {
|
||||
const timezoneOptions: Array<{value: string; label: string}> = timezoneData.map((tzOption: TimezoneDataDropdownOption) => {
|
||||
return {
|
||||
value: tzOption.name,
|
||||
label: tzOption.label
|
||||
@ -76,9 +76,9 @@ const TimeZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<Select
|
||||
hint={<Hint timezone={publicationTimezone} />}
|
||||
options={timezoneOptions}
|
||||
selectedOption={publicationTimezone}
|
||||
selectedOption={timezoneOptions.find(option => option.value === publicationTimezone)}
|
||||
title="Site timezone"
|
||||
onSelect={handleTimezoneChange}
|
||||
onSelect={option => handleTimezoneChange(option?.value)}
|
||||
/>
|
||||
</SettingGroupContent>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import handleError from '../../../utils/api/handleError';
|
||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
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 Toggle from '../../../admin-x-ds/global/form/Toggle';
|
||||
import clsx from 'clsx';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import handleError from '../../../utils/api/handleError';
|
||||
import useFeatureFlag from '../../../hooks/useFeatureFlag';
|
||||
import usePinturaEditor from '../../../hooks/usePinturaEditor';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
@ -722,9 +722,15 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => {
|
||||
};
|
||||
|
||||
const UserDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
||||
const {users} = useStaffUsers();
|
||||
const {users, hasNextPage, fetchNextPage} = useStaffUsers();
|
||||
const user = users.find(({slug}) => slug === params?.slug);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user && !hasNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [fetchNextPage, hasNextPage, user]);
|
||||
|
||||
if (user) {
|
||||
return <UserDetailModalContent user={user} />;
|
||||
} else {
|
||||
|
@ -7,7 +7,7 @@ import React, {useState} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import TabView from '../../../admin-x-ds/global/TabView';
|
||||
import clsx from 'clsx';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import handleError from '../../../utils/api/handleError';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import useStaffUsers from '../../../hooks/useStaffUsers';
|
||||
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 SettingGroup from '../../../../admin-x-ds/settings/SettingGroup';
|
||||
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 {ValidationError} from '../../../../utils/errors';
|
||||
import {showToast} from '../../../../admin-x-ds/global/Toast';
|
||||
|
@ -136,19 +136,19 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<Select
|
||||
hint='Who should be able to subscribe to your site?'
|
||||
options={MEMBERS_SIGNUP_ACCESS_OPTIONS}
|
||||
selectedOption={membersSignupAccess}
|
||||
selectedOption={MEMBERS_SIGNUP_ACCESS_OPTIONS.find(option => option.value === membersSignupAccess)}
|
||||
title="Subscription access"
|
||||
onSelect={(value) => {
|
||||
updateSetting('members_signup_access', value || null);
|
||||
onSelect={(option) => {
|
||||
updateSetting('members_signup_access', option?.value || null);
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
hint='When a new post is created, who should have access?'
|
||||
options={DEFAULT_CONTENT_VISIBILITY_OPTIONS}
|
||||
selectedOption={defaultContentVisibility}
|
||||
selectedOption={DEFAULT_CONTENT_VISIBILITY_OPTIONS.find(option => option.value === defaultContentVisibility)}
|
||||
title="Default post access"
|
||||
onSelect={(value) => {
|
||||
updateSetting('default_content_visibility', value || null);
|
||||
onSelect={(option) => {
|
||||
updateSetting('default_content_visibility', option?.value || null);
|
||||
}}
|
||||
/>
|
||||
{defaultContentVisibility === 'tiers' && (
|
||||
@ -164,10 +164,10 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
<Select
|
||||
hint='Who can comment on posts?'
|
||||
options={COMMENTS_ENABLED_OPTIONS}
|
||||
selectedOption={commentsEnabled}
|
||||
selectedOption={COMMENTS_ENABLED_OPTIONS.find(option => option.value === commentsEnabled)}
|
||||
title="Commenting"
|
||||
onSelect={(value) => {
|
||||
updateSetting('comments_enabled', value || null);
|
||||
onSelect={(option) => {
|
||||
updateSetting('comments_enabled', option?.value || null);
|
||||
}}
|
||||
/>
|
||||
</SettingGroupContent>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import React, {useState} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
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 [selectedTab, setSelectedTab] = useState('active-tiers');
|
||||
const {settings, config} = useGlobalData();
|
||||
const {data: {tiers} = {}} = useBrowseTiers();
|
||||
const {data: {tiers, isEnd} = {}, fetchNextPage} = useBrowseTiers();
|
||||
const activeTiers = getActiveTiers(tiers || []);
|
||||
const archivedTiers = getArchivedTiers(tiers || []);
|
||||
const {updateRoute} = useRouting();
|
||||
@ -80,6 +81,7 @@ const Tiers: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
</div>
|
||||
|
||||
{content}
|
||||
{isEnd === false && <Button label='Load more' link onClick={() => fetchNextPage()} />}
|
||||
</SettingGroup>
|
||||
);
|
||||
};
|
||||
|
@ -102,8 +102,8 @@ const TipsOrDonations: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
containerClassName='w-14'
|
||||
fullWidth={false}
|
||||
options={currencySelectGroups()}
|
||||
selectedOption={donationsCurrency}
|
||||
onSelect={currency => updateSetting('donations_currency', currency || 'USD')}
|
||||
selectedOption={currencySelectGroups().flatMap(group => group.options).find(option => option.value === donationsCurrency)}
|
||||
onSelect={option => updateSetting('donations_currency', option?.value || 'USD')}
|
||||
/>
|
||||
)}
|
||||
title='Suggested amount'
|
||||
|
@ -8,7 +8,6 @@ import {MultiSelectOption} from '../../../../admin-x-ds/global/form/MultiSelect'
|
||||
import {MultiValue} from 'react-select';
|
||||
import {generateCode} from '../../../../utils/generateEmbedCode';
|
||||
import {getSettingValues} from '../../../../api/settings';
|
||||
import {useBrowseLabels} from '../../../../api/labels';
|
||||
import {useEffect, useState} from 'react';
|
||||
import {useGlobalData} from '../../../providers/GlobalDataProvider';
|
||||
|
||||
@ -25,7 +24,6 @@ const EmbedSignupFormModal = NiceModal.create(() => {
|
||||
const {config} = useGlobalData();
|
||||
const {localSettings, siteData} = useSettingGroup();
|
||||
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});
|
||||
|
||||
if (labs) {
|
||||
@ -113,7 +111,6 @@ const EmbedSignupFormModal = NiceModal.create(() => {
|
||||
handleLabelClick={addSelectedLabel}
|
||||
handleLayoutSelect={setSelectedLayout}
|
||||
isCopied={isCopied}
|
||||
labels={labels?.labels || []}
|
||||
selectedColor={selectedColor}
|
||||
selectedLabels={selectedLabels}
|
||||
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 Form from '../../../../admin-x-ds/global/form/Form';
|
||||
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 React from 'react';
|
||||
import StickyFooter from '../../../../admin-x-ds/global/StickyFooter';
|
||||
import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
||||
import useFilterableApi from '../../../../hooks/useFilterableApi';
|
||||
import {Label} from '../../../../api/labels';
|
||||
import {MultiValue} from 'react-select';
|
||||
import {debounce} from '../../../../utils/debounce';
|
||||
|
||||
export type SelectedLabelTypes = {
|
||||
label: string;
|
||||
@ -20,7 +22,6 @@ type SidebarProps = {
|
||||
selectedColor?: string;
|
||||
accentColor?: string;
|
||||
handleColorToggle: (e: string) => void;
|
||||
labels?: Label[];
|
||||
handleLabelClick: (selected: MultiValue<MultiSelectOption>) => void;
|
||||
selectedLabels?: SelectedLabelTypes[];
|
||||
embedScript: string;
|
||||
@ -36,7 +37,6 @@ const EmbedSignupSidebar: React.FC<SidebarProps> = ({selectedLayout,
|
||||
accentColor,
|
||||
handleColorToggle,
|
||||
selectedColor,
|
||||
labels,
|
||||
selectedLabels,
|
||||
handleLabelClick,
|
||||
embedScript,
|
||||
@ -45,12 +45,13 @@ const EmbedSignupSidebar: React.FC<SidebarProps> = ({selectedLayout,
|
||||
customColor,
|
||||
setCustomColor,
|
||||
isCopied}) => {
|
||||
const labelOptions = labels ? labels.map((l) => {
|
||||
return {
|
||||
label: l?.name,
|
||||
value: l?.name
|
||||
};
|
||||
}).filter(Boolean) : [];
|
||||
const {loadData} = useFilterableApi<Label>({path: '/labels/', filterKey: 'name', responseKey: 'labels'});
|
||||
|
||||
const loadOptions: LoadOptions = async (input, callback) => {
|
||||
const labels = await loadData(input);
|
||||
callback(labels.map(label => ({label: label.name, value: label.name})));
|
||||
};
|
||||
|
||||
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>
|
||||
@ -125,12 +126,14 @@ const EmbedSignupSidebar: React.FC<SidebarProps> = ({selectedLayout,
|
||||
}
|
||||
|
||||
<MultiSelect
|
||||
canCreate={true}
|
||||
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)'
|
||||
title='Labels at signup'
|
||||
values={selectedLabels || []}
|
||||
async
|
||||
canCreate
|
||||
defaultOptions
|
||||
onChange={handleLabelClick}
|
||||
/>
|
||||
<TextArea
|
||||
|
@ -7,7 +7,7 @@ import Select from '../../../../admin-x-ds/global/form/Select';
|
||||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
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 PortalIcon2} from '../../../../assets/icons/portal-icon-2.svg';
|
||||
import {ReactComponent as PortalIcon3} from '../../../../assets/icons/portal-icon-3.svg';
|
||||
@ -67,6 +67,12 @@ const LookAndFeel: React.FC<{
|
||||
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>
|
||||
<Toggle
|
||||
checked={Boolean(portalButton)}
|
||||
@ -75,14 +81,10 @@ const LookAndFeel: React.FC<{
|
||||
onChange={e => updateSetting('portal_button', e.target.checked)}
|
||||
/>
|
||||
<Select
|
||||
options={[
|
||||
{value: 'icon-and-text', label: 'Icon and text'},
|
||||
{value: 'icon-only', label: 'Icon only'},
|
||||
{value: 'text-only', label: 'Text only'}
|
||||
]}
|
||||
selectedOption={portalButtonStyle as string}
|
||||
options={portalButtonOptions}
|
||||
selectedOption={portalButtonOptions.find(option => option.value === portalButtonStyle)}
|
||||
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') &&
|
||||
<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>
|
||||
<Select
|
||||
options={tierOptions}
|
||||
selectedOption={selectedTier}
|
||||
onSelect={(value) => {
|
||||
if (value) {
|
||||
setSelectedTier(value);
|
||||
selectedOption={tierOptions.find(option => option.value === selectedTier)}
|
||||
onSelect={(option) => {
|
||||
if (option) {
|
||||
setSelectedTier(option?.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -6,7 +6,7 @@ import PortalPreview from './PortalPreview';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import SignupOptions from './SignupOptions';
|
||||
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 useQueryParams from '../../../../hooks/useQueryParams';
|
||||
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 TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
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 useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
import {JSONError} from '../../../../utils/errors';
|
||||
@ -64,7 +64,7 @@ const Connect: React.FC = () => {
|
||||
|
||||
const saveTier = async () => {
|
||||
const {data} = await fetchActiveTiers();
|
||||
const tier = data?.tiers[0];
|
||||
const tier = data?.pages[0].tiers[0];
|
||||
|
||||
if (tier) {
|
||||
tier.monthly_price = 500;
|
||||
|
@ -13,7 +13,7 @@ import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import TierDetailPreview from './TierDetailPreview';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
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 useRouting from '../../../../hooks/useRouting';
|
||||
import useSettingGroup from '../../../../hooks/useSettingGroup';
|
||||
@ -223,9 +223,9 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
containerClassName='font-medium'
|
||||
controlClasses={{menu: 'w-14'}}
|
||||
options={currencySelectGroups()}
|
||||
selectedOption={formState.currency}
|
||||
selectedOption={currencySelectGroups().flatMap(group => group.options).find(option => option.value === formState.currency)}
|
||||
size='xs'
|
||||
onSelect={currency => updateForm(state => ({...state, currency}))}
|
||||
onSelect={option => updateForm(state => ({...state, currency: option?.value}))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -339,15 +339,21 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
};
|
||||
|
||||
const TierDetailModal: React.FC<RoutingModalProps> = ({params}) => {
|
||||
const {data: {tiers} = {}} = useBrowseTiers();
|
||||
const {data: {tiers, isEnd} = {}, fetchNextPage} = useBrowseTiers();
|
||||
|
||||
let tier: Tier | undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (params?.id && !tier && !isEnd) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [fetchNextPage, isEnd, params?.id, tier]);
|
||||
|
||||
if (params?.id) {
|
||||
tier = tiers?.find(({id}) => id === params?.id);
|
||||
|
||||
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 ThemePreview from './designAndBranding/ThemePreview';
|
||||
import ThemeSettings from './designAndBranding/ThemeSettings';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import handleError from '../../../utils/api/handleError';
|
||||
import useForm from '../../../hooks/useForm';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
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 ThemeInstalledModal from './theme/ThemeInstalledModal';
|
||||
import ThemePreview from './theme/ThemePreview';
|
||||
import handleError from '../../../utils/handleError';
|
||||
import handleError from '../../../utils/api/handleError';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {HostLimitError, useLimiter} from '../../../hooks/useLimiter';
|
||||
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 TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import UnsplashSearchModal from '../../../../utils/unsplash/UnsplashSearchModal';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import handleError from '../../../../utils/api/handleError';
|
||||
import usePinturaEditor from '../../../../hooks/usePinturaEditor';
|
||||
import {SettingValue, getSettingValues} from '../../../../api/settings';
|
||||
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 TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
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 {getImageUrl, useUploadImage} from '../../../../api/images';
|
||||
import {humanizeSettingKey} from '../../../../api/settings';
|
||||
@ -52,9 +52,9 @@ const ThemeSetting: React.FC<{
|
||||
<Select
|
||||
hint={setting.description}
|
||||
options={setting.options.map(option => ({label: option, value: option}))}
|
||||
selectedOption={setting.value}
|
||||
selectedOption={{label: setting.value, value: setting.value}}
|
||||
title={humanizeSettingKey(setting.key)}
|
||||
onSelect={value => setSetting(value || null)}
|
||||
onSelect={option => setSetting(option?.value || null)}
|
||||
/>
|
||||
);
|
||||
case 'color':
|
||||
|
@ -3,7 +3,7 @@ import Modal from '../../../../admin-x-ds/global/modal/Modal';
|
||||
import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
||||
import React from 'react';
|
||||
import RecommendationReasonForm from './RecommendationReasonForm';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import handleError from '../../../../utils/api/handleError';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
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 React from 'react';
|
||||
import RecommendationReasonForm from './RecommendationReasonForm';
|
||||
import handleError from '../../../../utils/handleError';
|
||||
import handleError from '../../../../utils/api/handleError';
|
||||
import useForm from '../../../../hooks/useForm';
|
||||
import useRouting from '../../../../hooks/useRouting';
|
||||
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 NiceModal from '@ebay/nice-modal-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 {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 NiceModal from '@ebay/nice-modal-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 {InstalledTheme, ThemeProblem, useActivateTheme} from '../../../../api/themes';
|
||||
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
|
||||
});
|
||||
const {refetch: fetchNewsletters} = useBrowseNewsletters({
|
||||
searchParams: {filter: 'status:active', limit: 'all'},
|
||||
searchParams: {filter: 'status:active', limit: '1'},
|
||||
enabled: false
|
||||
});
|
||||
|
||||
@ -74,16 +74,17 @@ export const useLimiter = () => {
|
||||
}, [config.hostSettings?.billing]);
|
||||
|
||||
return useMemo(() => {
|
||||
const limits = config.hostSettings?.limits as LimiterLimits;
|
||||
|
||||
if (!LimitService || !limits || isLoading) {
|
||||
if (!LimitService || !config.hostSettings?.limits || isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const limits = {...config.hostSettings.limits} as LimiterLimits;
|
||||
const limiter = new LimitService();
|
||||
|
||||
if (limits.staff) {
|
||||
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 staffInvites = invites.filter(i => i.role !== 'Contributor');
|
||||
|
||||
@ -100,8 +101,8 @@ export const useLimiter = () => {
|
||||
|
||||
if (limits.newsletters) {
|
||||
limits.newsletters.currentCountQuery = async () => {
|
||||
const {data: {newsletters} = {newsletters: []}} = await fetchNewsletters();
|
||||
return newsletters?.length || 0;
|
||||
const {data: {pages} = {pages: []}} = await fetchNewsletters();
|
||||
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';
|
||||
|
||||
export interface PaginationData {
|
||||
|
@ -1,5 +1,5 @@
|
||||
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 useGlobalDirtyState from './useGlobalDirtyState';
|
||||
import {Setting, SettingValue, useEditSettings} from '../api/settings';
|
||||
|
@ -14,6 +14,8 @@ export type UsersHook = {
|
||||
contributorUsers: User[];
|
||||
currentUser: User|null;
|
||||
isLoading: boolean;
|
||||
hasNextPage?: boolean;
|
||||
fetchNextPage: () => void;
|
||||
};
|
||||
|
||||
function getUsersByRole(users: User[], role: string): User[] {
|
||||
@ -30,7 +32,7 @@ function getOwnerUser(users: User[]): User {
|
||||
|
||||
const useStaffUsers = (): UsersHook => {
|
||||
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: {roles} = {}, isLoading: rolesLoading} = useBrowseRoles();
|
||||
|
||||
@ -58,7 +60,9 @@ const useStaffUsers = (): UsersHook => {
|
||||
contributorUsers,
|
||||
currentUser,
|
||||
invites: mappedInvites,
|
||||
isLoading: usersLoading || invitesLoading || rolesLoading
|
||||
isLoading: usersLoading || invitesLoading || rolesLoading,
|
||||
hasNextPage: isEnd,
|
||||
fetchNextPage
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import toast from 'react-hot-toast';
|
||||
import {APIError, ValidationError} from './errors';
|
||||
import {showToast} from '../admin-x-ds/global/Toast';
|
||||
import {APIError, ValidationError} from '../errors';
|
||||
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
|
@ -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) => {
|
||||
if (response.status === 0) {
|
@ -1,12 +1,12 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import handleError from './handleError';
|
||||
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 {getGhostPaths} from './helpers';
|
||||
import {getGhostPaths} from '../helpers';
|
||||
import {useEffect, useMemo} from 'react';
|
||||
import {usePage, usePagination} from '../hooks/usePagination';
|
||||
import {useSentryDSN, useServices} from '../components/providers/ServiceProvider';
|
||||
import {usePage, usePagination} from '../../hooks/usePagination';
|
||||
import {useSentryDSN, useServices} from '../../components/providers/ServiceProvider';
|
||||
|
||||
export interface Meta {
|
||||
pagination: {
|
||||
@ -33,7 +33,8 @@ export const useFetchApi = () => {
|
||||
const {ghostVersion} = useServices();
|
||||
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
|
||||
const defaultHeaders: Record<string, string> = {
|
||||
'app-pragma': 'no-cache',
|
||||
@ -91,7 +92,7 @@ export const useFetchApi = () => {
|
||||
Sentry.captureMessage('Request took multiple attempts', {extra: {attempts, retryingMs, endpoint: endpoint.toString()}});
|
||||
}
|
||||
|
||||
return handleResponse(response);
|
||||
return handleResponse(response) as Response;
|
||||
} catch (error) {
|
||||
retryingMs = Date.now() - startTime;
|
||||
|
||||
@ -160,7 +161,7 @@ export const createQuery = <ResponseData>(options: QueryOptions<ResponseData>) =
|
||||
|
||||
const data = useMemo(() => (
|
||||
(result.data && options.returnData) ? options.returnData(result.data) : result.data)
|
||||
, [result]);
|
||||
, [result.data]);
|
||||
|
||||
useEffect(() => {
|
||||
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'> & {
|
||||
returnData: NonNullable<QueryOptions<ResponseData>['returnData']>
|
||||
defaultNextPageParams?: (data: ResponseData, params: Record<string, string>) => Record<string, string>;
|
||||
}
|
||||
|
||||
type InfiniteQueryHookOptions<ResponseData> = UseInfiniteQueryOptions<ResponseData> & {
|
||||
searchParams?: Record<string, string>;
|
||||
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 nextPageParams = getNextPageParams || options.defaultNextPageParams || (() => ({}));
|
||||
|
||||
const result = useInfiniteQuery<ResponseData>({
|
||||
queryKey: [options.dataType, apiUrl(options.path, 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
|
||||
});
|
||||
|
||||
const data = useMemo(() => result.data && options.returnData(result.data), [result]);
|
||||
const data = useMemo(() => result.data && options.returnData(result.data), [result.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (result.error && query.defaultErrorHandler !== false) {
|
||||
@ -280,7 +284,7 @@ const mutate = <ResponseData, Payload>({fetchApi, path, payload, searchParams, o
|
||||
requestBody = JSON.stringify(generatedBody);
|
||||
}
|
||||
|
||||
return fetchApi(url, {
|
||||
return fetchApi<ResponseData>(url, {
|
||||
body: requestBody,
|
||||
...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) {
|
||||
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}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseTiers: {method: 'GET', path: '/tiers/?limit=all', response: responseFixtures.tiers},
|
||||
browseLabels: {method: 'GET', path: '/labels/?limit=all', response: responseFixtures.labels},
|
||||
browseOffers: {method: 'GET', path: '/offers/?limit=all', response: responseFixtures.offers},
|
||||
browseTiers: {method: 'GET', path: '/tiers/?filter=&limit=20', response: responseFixtures.tiers},
|
||||
browseLabels: {method: 'GET', path: '/labels/?filter=&limit=20', response: responseFixtures.labels},
|
||||
browseOffers: {method: 'GET', path: '/offers/?filter=&limit=20', response: responseFixtures.offers},
|
||||
editSettings: {method: 'PUT', path: '/settings/', response: updatedSettingsResponse([
|
||||
{
|
||||
key: 'editor_default_email_recipients',
|
||||
|
@ -5,7 +5,7 @@ test.describe('Newsletter settings', async () => {
|
||||
test('Supports creating a new newsletter', async ({page}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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: [{
|
||||
id: 'new-newsletter',
|
||||
name: 'New newsletter',
|
||||
@ -48,7 +48,7 @@ test.describe('Newsletter settings', async () => {
|
||||
test('Supports updating a newsletter', async ({page}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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: {
|
||||
newsletters: [{
|
||||
...responseFixtures.newsletters.newsletters[0],
|
||||
@ -93,7 +93,7 @@ test.describe('Newsletter settings', async () => {
|
||||
test('Displays a prompt when email verification is required', async ({page}) => {
|
||||
await mockApi({page, requests: {
|
||||
...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: {
|
||||
newsletters: [responseFixtures.newsletters.newsletters[0]],
|
||||
meta: {
|
||||
@ -120,7 +120,7 @@ test.describe('Newsletter settings', async () => {
|
||||
test('Supports archiving newsletters', async ({page}) => {
|
||||
const activate = await mockApi({page, requests: {
|
||||
...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: {
|
||||
newsletters: [{
|
||||
...responseFixtures.newsletters.newsletters[1],
|
||||
@ -157,7 +157,7 @@ test.describe('Newsletter settings', async () => {
|
||||
|
||||
const archive = await mockApi({page, requests: {
|
||||
...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: {
|
||||
newsletters: [{
|
||||
...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('/');
|
||||
|
@ -7,7 +7,7 @@ test.describe('User actions', async () => {
|
||||
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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: {
|
||||
users: [{
|
||||
...userToEdit,
|
||||
@ -50,7 +50,7 @@ test.describe('User actions', async () => {
|
||||
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: {
|
||||
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: {
|
||||
users: [
|
||||
...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: {
|
||||
...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: {}}
|
||||
}});
|
||||
|
||||
@ -151,7 +151,7 @@ test.describe('User actions', async () => {
|
||||
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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},
|
||||
makeOwner: {method: 'PUT', path: '/users/owner/', response: makeOwnerResponse}
|
||||
}});
|
||||
@ -206,7 +206,7 @@ test.describe('User actions', async () => {
|
||||
await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
...limitRequests,
|
||||
browseUsers: {method: 'GET', path: '/users/?limit=all&include=roles', response: {
|
||||
browseUsers: {method: 'GET', path: '/users/?limit=100&include=roles', response: {
|
||||
users: [
|
||||
...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: {
|
||||
...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},
|
||||
browseRoles: {method: 'GET', path: '/roles/?limit=all', 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}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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},
|
||||
deleteInvite: {method: 'DELETE', path: `/invites/${responseFixtures.invites.invites[0].id}/`, response: {}},
|
||||
addInvite: {method: 'POST', path: '/invites/', response: responseFixtures.invites}
|
||||
@ -97,7 +97,7 @@ test.describe('User invitations', async () => {
|
||||
test('Supports revoking invitations', async ({page}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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},
|
||||
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: {
|
||||
...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: {
|
||||
users: [{
|
||||
...userToEdit,
|
||||
@ -68,7 +68,7 @@ test.describe('User profile', async () => {
|
||||
test('Supports changing password', async ({page}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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: {}}
|
||||
}});
|
||||
|
||||
@ -109,7 +109,7 @@ test.describe('User profile', async () => {
|
||||
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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}]}},
|
||||
editUser: {method: 'PUT', path: `/users/${userToEdit.id}/?include=roles`, response: {
|
||||
users: [{
|
||||
|
@ -5,7 +5,7 @@ test.describe('User roles', async () => {
|
||||
test('Shows users under their role', async ({page}) => {
|
||||
await mockApi({page, requests: {
|
||||
...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('/');
|
||||
@ -42,7 +42,7 @@ test.describe('User roles', async () => {
|
||||
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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},
|
||||
browseAssignableRoles: {method: 'GET', path: '/roles/?limit=all&permissions=assign', response: responseFixtures.roles},
|
||||
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}) => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...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([
|
||||
{key: 'default_content_visibility', value: 'tiers'},
|
||||
{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: {
|
||||
...globalDataRequests,
|
||||
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('/');
|
||||
@ -55,7 +55,7 @@ test.describe('Tier settings', async () => {
|
||||
...globalDataRequests,
|
||||
addTier: {method: 'POST', path: '/tiers/', response: {tiers: [newTier]}},
|
||||
// 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();
|
||||
@ -77,7 +77,7 @@ test.describe('Tier settings', async () => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
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: {
|
||||
tiers: [{
|
||||
...responseFixtures.tiers.tiers[1],
|
||||
@ -140,7 +140,7 @@ test.describe('Tier settings', async () => {
|
||||
const {lastApiRequests} = await mockApi({page, requests: {
|
||||
...globalDataRequests,
|
||||
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: {
|
||||
tiers: [{
|
||||
...responseFixtures.tiers.tiers[0],
|
||||
|
@ -55,10 +55,10 @@ export const globalDataRequests = {
|
||||
};
|
||||
|
||||
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},
|
||||
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}) {
|
||||
|
Loading…
Reference in New Issue
Block a user