Added tier and label selection in default recipients

refs https://github.com/TryGhost/Team/issues/3151
This commit is contained in:
Jono Mingard 2023-06-13 15:58:02 +12:00
parent c0e638fecd
commit 731858e9e0
5 changed files with 152 additions and 55 deletions

View File

@ -1,17 +1,18 @@
import Heading from './Heading';
import Hint from './Hint';
import React from 'react';
import {MultiValue, default as ReactSelect, components} from 'react-select';
import {GroupBase, MultiValue, OptionsOrGroups, default as ReactSelect, components} from 'react-select';
export type MultiSelectColor = 'grey' | 'black' | string;
export type MultiSelectColor = 'grey' | 'black' | 'green' | 'pink';
export type MultiSelectOption = {
value: string;
label: string;
color?: MultiSelectColor;
}
interface MultiSelectProps {
options: MultiSelectOption[];
options: OptionsOrGroups<MultiSelectOption, GroupBase<MultiSelectOption>>;
defaultValues?: MultiSelectOption[];
title?: string;
clearBg?: boolean;
@ -22,6 +23,21 @@ interface MultiSelectProps {
onChange: (selected: MultiValue<MultiSelectOption>) => void
}
const multiValueColor = (color?: MultiSelectColor) => {
switch (color) {
case 'black':
return 'bg-black text-white';
case 'grey':
return 'bg-grey-300 text-black';
case 'green':
return 'bg-green-500 text-white';
case 'pink':
return 'bg-pink-500 text-white';
default:
return '';
}
};
const MultiSelect: React.FC<MultiSelectProps> = ({
title = '',
clearBg = false,
@ -34,27 +50,15 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
onChange,
...props
}) => {
let multiValueColor;
switch (color) {
case 'black':
multiValueColor = 'bg-black text-white';
break;
case 'grey':
multiValueColor = 'bg-grey-300 text-black';
break;
default:
break;
}
const customClasses = {
control: `w-full cursor-pointer appearance-none min-h-[40px] border-b ${!clearBg && 'bg-grey-75 px-[10px]'} py-2 outline-none ${error ? 'border-red' : 'border-grey-500 hover:border-grey-700'} ${(title && !clearBg) && 'mt-2'}`,
valueContainer: 'gap-1',
placeHolder: 'text-grey-600',
menu: 'shadow py-2 rounded-b z-50 bg-white',
option: 'hover:cursor-pointer hover:bg-grey-100 px-3 py-[6px]',
multiValue: `rounded-sm items-center text-[14px] py-px pl-2 pr-1 gap-1.5 ${multiValueColor}`,
noOptionsMessage: 'p-3 text-grey-600'
multiValue: (optionColor?: MultiSelectColor) => `rounded-sm items-center text-[14px] py-px pl-2 pr-1 gap-1.5 ${multiValueColor(optionColor || color)}`,
noOptionsMessage: 'p-3 text-grey-600',
groupHeading: 'py-[6px] px-3 text-2xs font-semibold uppercase tracking-wide text-grey-700'
};
const DropdownIndicator: React.FC<any> = ddiProps => (
@ -74,9 +78,11 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
placeholder: () => customClasses.placeHolder,
menu: () => customClasses.menu,
option: () => customClasses.option,
multiValue: () => customClasses.multiValue,
noOptionsMessage: () => customClasses.noOptionsMessage
multiValue: ({data}) => customClasses.multiValue(data.color),
noOptionsMessage: () => customClasses.noOptionsMessage,
groupHeading: () => customClasses.groupHeading
}}
closeMenuOnSelect={false}
components={{DropdownIndicator}}
defaultValue={defaultValues}
isClearable={false}
@ -92,4 +98,4 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
);
};
export default MultiSelect;
export default MultiSelect;

View File

@ -1,10 +1,12 @@
import MultiSelect, {MultiSelectOption} from '../../../admin-x-ds/global/MultiSelect';
import React from 'react';
import React, {useContext, useEffect, useState} from 'react';
import Select from '../../../admin-x-ds/global/Select';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {MultiValue} from 'react-select';
import {GroupBase, MultiValue} from 'react-select';
import {Label, Tier} from '../../../types/api';
import {ServicesContext} from '../../providers/ServiceProvider';
import {getOptionLabel, getSettingValues} from '../../../utils/helpers';
type RefipientValueArgs = {
@ -29,6 +31,16 @@ 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
@ -63,6 +75,25 @@ const DefaultRecipients: React.FC = () => {
'editor_default_email_recipients', 'editor_default_email_recipients_filter'
]) as [string, string|null];
const [selectedOption, setSelectedOption] = useState(getDefaultRecipientValue({
defaultEmailRecipients,
defaultEmailRecipientsFilter
}));
const {api} = useContext(ServicesContext);
const [tiers, setTiers] = useState<Tier[]>([]);
const [labels, setLabels] = useState<Label[]>([]);
useEffect(() => {
api.tiers.browse().then((response) => {
setTiers(response.tiers);
});
api.labels.browse().then((response) => {
setLabels(response.labels);
});
}, [api]);
const setDefaultRecipientValue = (value: string) => {
if (['visibility', 'disabled'].includes(value)) {
updateSetting('editor_default_email_recipients', value);
@ -82,12 +113,34 @@ const DefaultRecipients: React.FC = () => {
if (value === 'none') {
updateSetting('editor_default_email_recipients_filter', null);
}
setSelectedOption(value);
};
const emailRecipientValue = getDefaultRecipientValue({
defaultEmailRecipients,
defaultEmailRecipientsFilter
});
const segmentOptionGroups: GroupBase<MultiSelectOption>[] = [
{
options: SIMPLE_SEGMENT_OPTIONS
},
{
label: 'Active Tiers',
options: tiers.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'}))
}
];
const segmentOptions = segmentOptionGroups.flatMap(({options}) => options);
const defaultSelectedSegments = (defaultEmailRecipientsFilter?.split(',') || [])
.map(value => segmentOptions.find(option => option.value === value))
.filter((option): option is MultiSelectOption => Boolean(option));
const setSelectedSegments = (selected: MultiValue<MultiSelectOption>) => {
const selectedGroups = selected?.map(({value}) => value).join(',');
updateSetting('editor_default_email_recipients_filter', selectedGroups);
};
const values = (
<SettingGroupContent
@ -95,7 +148,7 @@ const DefaultRecipients: React.FC = () => {
{
heading: 'Default Newsletter recipients',
key: 'default-recipients',
value: getOptionLabel(RECIPIENT_FILTER_OPTIONS, emailRecipientValue)
value: getOptionLabel(RECIPIENT_FILTER_OPTIONS, selectedOption)
}
]}
/>
@ -104,7 +157,7 @@ const DefaultRecipients: React.FC = () => {
const form = (
<SettingGroupContent columns={1}>
<Select
defaultSelectedOption={emailRecipientValue}
defaultSelectedOption={selectedOption}
hint='Who should be able to subscribe to your site?'
options={RECIPIENT_FILTER_OPTIONS}
title="Default Newsletter recipients"
@ -112,23 +165,13 @@ const DefaultRecipients: React.FC = () => {
setDefaultRecipientValue(value);
}}
/>
{(emailRecipientValue === 'segment') && (
{(selectedOption === 'segment') && (
<MultiSelect
defaultValues={[
{value: 'option2', label: 'Fake tier 2'}
]}
options={[
{value: 'option1', label: 'Fake tier 1'},
{value: 'option2', label: 'Fake tier 2'},
{value: 'option3', label: 'Fake tier 3'}
]}
defaultValues={defaultSelectedSegments}
options={segmentOptionGroups}
title='Select tiers'
clearBg
onChange={(selected: MultiValue<MultiSelectOption>) => {
selected?.map(o => (
alert(`${o.label} (${o.value})`)
));
}}
onChange={setSelectedSegments}
/>
)}
</SettingGroupContent>

View File

@ -61,6 +61,32 @@ export type Post = {
url: string;
};
export type Tier = {
id: string;
name: string;
description: string | null;
slug: string;
active: true,
type: string;
welcome_page_url: string | null;
created_at: string;
updated_at: string;
visibility: string;
benefits: string[];
currency?: string;
monthly_price?: number;
yearly_price?: number;
trial_days: number;
}
export type Label = {
id: string;
name: string;
slug: string;
created_at: string;
updated_at: string;
}
type CustomThemeSettingData =
{ type: 'text', value: string | null, default: string | null } |
{ type: 'color', value: string, default: string } |

View File

@ -1,4 +1,4 @@
import {CustomThemeSetting, Post, Setting, SiteData, User, UserRole} from '../types/api';
import {CustomThemeSetting, Label, Post, Setting, SiteData, Tier, User, UserRole} from '../types/api';
import {getGhostPaths} from './helpers';
interface Meta {
@ -54,19 +54,20 @@ export interface CustomThemeSettingsResponseType {
}
export interface PostsResponseType {
meta: {
pagination: {
page: number
limit: number
pages: number
total: number
next: number | null
prev: number | null
}
}
meta?: Meta
posts: Post[];
}
export interface TiersResponseType {
meta?: Meta
tiers: Tier[]
}
export interface LabelsResponseType {
meta?: Meta
labels: Label[]
}
export interface SiteResponseType {
site: SiteData;
}
@ -144,6 +145,12 @@ interface API {
};
latestPost: {
browse: () => Promise<PostsResponseType>
};
tiers: {
browse: () => Promise<TiersResponseType>
};
labels: {
browse: () => Promise<LabelsResponseType>
}
}
@ -349,6 +356,21 @@ function setupGhostApi({ghostVersion}: GhostApiOptions): API {
const data: PostsResponseType = await response.json();
return data;
}
},
tiers: {
browse: async () => {
const filter = encodeURIComponent('type:paid+active:true');
const response = await fetcher(`/tiers/?filter=${filter}&limit=all`);
const data: TiersResponseType = await response.json();
return data;
}
},
labels: {
browse: async () => {
const response = await fetcher(`/labels/?limit=all`);
const data: LabelsResponseType = await response.json();
return data;
}
}
};

@ -1 +1 @@
Subproject commit ba0b3d08cca796d29dc3c96f25f6420557059591
Subproject commit 23f7c303657eb413c0cbe296fffb88647f91a258