Updated history staff search (#18108)

refs. https://github.com/TryGhost/Product/issues/3349

- newsletter searchfield was showing a "Clear" button once it had content. It needed to be using standard React dropdown instead
- modals needed an option to make the header sticky so it's more versatile. It's now used in the History modal

---------

Co-authored-by: Jono Mingard <reason.koan@gmail.com>
This commit is contained in:
Peter Zimon 2023-09-20 13:28:29 +03:00 committed by GitHub
parent 474923ba8a
commit b4bcc193a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 248 additions and 86 deletions

View File

@ -2,9 +2,11 @@ import CreatableSelect from 'react-select/creatable';
import Heading from '../Heading'; import Heading from '../Heading';
import Hint from '../Hint'; import Hint from '../Hint';
import React, {useId, useMemo} from 'react'; import React, {useId, useMemo} from 'react';
import clsx from 'clsx';
import {DropdownIndicatorProps, GroupBase, MultiValue, OptionProps, OptionsOrGroups, default as ReactSelect, components} from 'react-select'; import {DropdownIndicatorProps, GroupBase, MultiValue, OptionProps, OptionsOrGroups, default as ReactSelect, components} from 'react-select';
export type MultiSelectColor = 'grey' | 'black' | 'green' | 'pink'; export type MultiSelectColor = 'grey' | 'black' | 'green' | 'pink';
type FieldStyles = 'text' | 'dropdown';
export type MultiSelectOption = { export type MultiSelectOption = {
value: string; value: string;
@ -20,6 +22,8 @@ interface MultiSelectProps {
error?: boolean; error?: boolean;
placeholder?: string; placeholder?: string;
color?: MultiSelectColor color?: MultiSelectColor
size?: 'sm' | 'md';
fieldStyle?: FieldStyles;
hint?: string; hint?: string;
onChange: (selected: MultiValue<MultiSelectOption>) => void; onChange: (selected: MultiValue<MultiSelectOption>) => void;
canCreate?: boolean; canCreate?: boolean;
@ -40,11 +44,16 @@ const multiValueColor = (color?: MultiSelectColor) => {
} }
}; };
const DropdownIndicator: React.FC<DropdownIndicatorProps<MultiSelectOption, true> & {clearBg: boolean}> = ({clearBg, ...props}) => ( const DropdownIndicator: React.FC<DropdownIndicatorProps<MultiSelectOption, true> & {clearBg: boolean, fieldStyle: FieldStyles}> = ({clearBg, fieldStyle, ...props}) => {
<components.DropdownIndicator {...props}> if (fieldStyle === 'text') {
<div className={`absolute top-[14px] block h-2 w-2 rotate-45 border-[1px] border-l-0 border-t-0 border-grey-900 content-[''] dark:border-grey-400 ${clearBg ? 'right-0' : 'right-4'} `}></div> return <></>;
</components.DropdownIndicator> }
); return (
<components.DropdownIndicator {...props}>
<div className={`absolute top-[14px] block h-2 w-2 rotate-45 border-[1px] border-l-0 border-t-0 border-grey-900 content-[''] dark:border-grey-400 ${clearBg ? 'right-0' : 'right-4'} `}></div>
</components.DropdownIndicator>
);
};
const Option: React.FC<OptionProps<MultiSelectOption, true>> = ({children, ...optionProps}) => ( const Option: React.FC<OptionProps<MultiSelectOption, true>> = ({children, ...optionProps}) => (
<components.Option {...optionProps}> <components.Option {...optionProps}>
@ -58,6 +67,8 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
error = false, error = false,
placeholder, placeholder,
color = 'grey', color = 'grey',
size = 'md',
fieldStyle = 'dropdown',
hint = '', hint = '',
options, options,
values, values,
@ -67,12 +78,27 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
}) => { }) => {
const id = useId(); const id = useId();
const controlClasses = clsx(
size === 'sm' ? 'min-h-[36px] py-1 text-sm' : 'min-h-[40px] py-2',
'w-full cursor-pointer appearance-none border-b dark:text-white',
fieldStyle === 'dropdown' ? 'cursor-pointer' : 'cursor-text',
!clearBg && 'bg-grey-75 px-[10px] dark:bg-grey-950',
'outline-none',
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 dark:border-grey-800 dark:hover:border-grey-700',
(title && !clearBg) && 'mt-2'
);
const optionClasses = clsx(
size === 'sm' ? 'text-sm' : '',
'px-3 py-[6px] hover:cursor-pointer hover:bg-grey-100 dark:text-white dark:hover:bg-grey-900'
);
const customClasses = { const customClasses = {
control: `w-full cursor-pointer appearance-none min-h-[40px] border-b dark:text-white ${!clearBg && 'bg-grey-75 dark:bg-grey-950 px-[10px]'} py-2 outline-none ${error ? 'border-red' : 'border-grey-500 hover:border-grey-700 dark:border-grey-800 dark:hover:border-grey-700'} ${(title && !clearBg) && 'mt-2'}`, control: controlClasses,
valueContainer: 'gap-1', valueContainer: 'gap-1',
placeHolder: 'text-grey-500 dark:text-grey-800', placeHolder: 'text-grey-500 dark:text-grey-800',
menu: 'shadow py-2 rounded-b z-50 bg-white dark:bg-black dark:border dark:border-grey-900', menu: 'shadow py-2 rounded-b z-[10000] bg-white dark:bg-black dark:border dark:border-grey-900',
option: 'hover:cursor-pointer hover:bg-grey-100 px-3 py-[6px] dark:text-white dark:hover:bg-grey-900', option: optionClasses,
multiValue: (optionColor?: MultiSelectColor) => `rounded-sm items-center text-[14px] py-px pl-2 pr-1 gap-1.5 ${multiValueColor(optionColor || color)}`, 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', noOptionsMessage: 'p-3 text-grey-600',
groupHeading: 'py-[6px] px-3 text-2xs font-semibold uppercase tracking-wide text-grey-700' groupHeading: 'py-[6px] px-3 text-2xs font-semibold uppercase tracking-wide text-grey-700'
@ -81,8 +107,8 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
const dropdownIndicatorComponent = useMemo(() => { const dropdownIndicatorComponent = useMemo(() => {
// TODO: fix "Component definition is missing display name" // TODO: fix "Component definition is missing display name"
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
return (ddiProps: DropdownIndicatorProps<MultiSelectOption, true>) => <DropdownIndicator {...ddiProps} clearBg={clearBg} />; return (ddiProps: DropdownIndicatorProps<MultiSelectOption, true>) => <DropdownIndicator {...ddiProps} clearBg={clearBg} fieldStyle={fieldStyle} />;
}, [clearBg]); }, [clearBg, fieldStyle]);
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>

View File

@ -1,8 +1,9 @@
import React, {useId, useMemo} from 'react'; import React, {useId, useMemo} from 'react';
import ReactSelect, {DropdownIndicatorProps, OptionProps, Props, components} from 'react-select'; import ReactSelect, {ClearIndicatorProps, DropdownIndicatorProps, OptionProps, Props, components} from 'react-select';
import Heading from '../Heading'; import Heading from '../Heading';
import Hint from '../Hint'; import Hint from '../Hint';
import Icon from '../Icon';
import clsx from 'clsx'; import clsx from 'clsx';
export interface SelectOption { export interface SelectOption {
@ -27,6 +28,7 @@ export interface SelectControlClasses {
option?: string; option?: string;
noOptionsMessage?: string; noOptionsMessage?: string;
groupHeading?: string; groupHeading?: string;
clearIndicator?: string;
} }
export interface SelectProps extends Props<SelectOption, false> { export interface SelectProps extends Props<SelectOption, false> {
@ -54,6 +56,13 @@ const DropdownIndicator: React.FC<DropdownIndicatorProps<SelectOption, false> &
</components.DropdownIndicator> </components.DropdownIndicator>
); );
const ClearIndicator: React.FC<ClearIndicatorProps<SelectOption, false>> = props => (
<components.ClearIndicator {...props}>
<Icon className='mr-2' name='close' size='xs' />
{/* <div className={`pr-2 text-xl leading-none text-grey-900 dark:text-grey-400`}>&times;</div> */}
</components.ClearIndicator>
);
const Option: React.FC<OptionProps<SelectOption, false>> = ({children, ...optionProps}) => ( const Option: React.FC<OptionProps<SelectOption, false>> = ({children, ...optionProps}) => (
<components.Option {...optionProps}> <components.Option {...optionProps}>
<span data-testid="select-option">{children}</span> <span data-testid="select-option">{children}</span>
@ -98,7 +107,7 @@ const Select: React.FC<SelectProps> = ({
const customClasses = { const customClasses = {
control: clsx( control: clsx(
controlClasses?.control, controlClasses?.control,
'min-h-[40px] w-full cursor-pointer appearance-none outline-none dark:text-white', 'min-h-[40px] w-full cursor-pointer appearance-none pr-4 outline-none dark:text-white',
size === 'xs' ? 'py-0 text-xs' : 'py-2', size === 'xs' ? 'py-0 text-xs' : 'py-2',
border && 'border-b', border && 'border-b',
!clearBg && 'bg-grey-75 px-[10px] dark:bg-grey-950', !clearBg && 'bg-grey-75 px-[10px] dark:bg-grey-950',
@ -114,7 +123,8 @@ const Select: React.FC<SelectProps> = ({
), ),
option: clsx('px-3 py-[6px] hover:cursor-pointer hover:bg-grey-100 dark:text-white dark:hover:bg-grey-900', controlClasses?.option), option: clsx('px-3 py-[6px] hover:cursor-pointer hover:bg-grey-100 dark:text-white dark:hover:bg-grey-900', controlClasses?.option),
noOptionsMessage: clsx('p-3 text-grey-600', controlClasses?.noOptionsMessage), noOptionsMessage: clsx('p-3 text-grey-600', controlClasses?.noOptionsMessage),
groupHeading: clsx('px-3 py-[6px] text-2xs font-semibold uppercase tracking-wide text-grey-700', controlClasses?.groupHeading) groupHeading: clsx('px-3 py-[6px] text-2xs font-semibold uppercase tracking-wide text-grey-700', controlClasses?.groupHeading),
clearIndicator: clsx('', controlClasses?.clearIndicator)
}; };
const dropdownIndicatorComponent = useMemo(() => { const dropdownIndicatorComponent = useMemo(() => {
@ -143,9 +153,10 @@ const Select: React.FC<SelectProps> = ({
menu: () => customClasses.menu, menu: () => customClasses.menu,
option: () => customClasses.option, option: () => customClasses.option,
noOptionsMessage: () => customClasses.noOptionsMessage, noOptionsMessage: () => customClasses.noOptionsMessage,
groupHeading: () => customClasses.groupHeading groupHeading: () => customClasses.groupHeading,
clearIndicator: () => customClasses.clearIndicator
}} }}
components={{DropdownIndicator: dropdownIndicatorComponent, Option}} components={{DropdownIndicator: dropdownIndicatorComponent, Option, ClearIndicator}}
inputId={id} inputId={id}
isClearable={false} isClearable={false}
options={options} options={options}

View File

@ -161,6 +161,20 @@ const longContent = (
</> </>
); );
export const StickyHeader: Story = {
args: {
size: 'md',
stickyHeader: true,
onOk: () => {
alert('Clicked OK!');
},
onCancel: undefined,
title: 'Sticky header',
stickyFooter: true,
children: longContent
}
};
export const StickyFooter: Story = { export const StickyFooter: Story = {
args: { args: {
size: 'md', size: 'md',

View File

@ -37,6 +37,7 @@ export interface ModalProps {
backDrop?: boolean; backDrop?: boolean;
backDropClick?: boolean; backDropClick?: boolean;
stickyFooter?: boolean; stickyFooter?: boolean;
stickyHeader?:boolean;
scrolling?: boolean; scrolling?: boolean;
dirty?: boolean; dirty?: boolean;
animate?: boolean; animate?: boolean;
@ -67,6 +68,7 @@ const Modal: React.FC<ModalProps> = ({
backDrop = true, backDrop = true,
backDropClick = true, backDropClick = true,
stickyFooter = false, stickyFooter = false,
stickyHeader = false,
scrolling = true, scrolling = true,
dirty = false, dirty = false,
animate = true, animate = true,
@ -114,6 +116,8 @@ const Modal: React.FC<ModalProps> = ({
let buttons: ButtonProps[] = []; let buttons: ButtonProps[] = [];
let footerClasses, contentClasses;
const removeModal = () => { const removeModal = () => {
confirmIfDirty(dirty, () => { confirmIfDirty(dirty, () => {
modal.remove(); modal.remove();
@ -161,46 +165,120 @@ const Modal: React.FC<ModalProps> = ({
); );
let paddingClasses = ''; let paddingClasses = '';
let headerClasses = clsx(
(!topRightContent || topRightContent === 'close') ? '' : 'flex items-center justify-between gap-5'
);
if (stickyHeader) {
headerClasses = clsx(
headerClasses,
'sticky top-0 z-[200] -mb-4 bg-white !pb-4 dark:bg-black'
);
}
switch (size) { switch (size) {
case 'sm': case 'sm':
modalClasses += ' max-w-[480px] '; modalClasses = clsx(
backdropClasses += ' p-4 md:p-[8vmin]'; modalClasses,
'max-w-[480px]'
);
backdropClasses = clsx(
backdropClasses,
'p-4 md:p-[8vmin]'
);
paddingClasses = 'p-8'; paddingClasses = 'p-8';
headerClasses = clsx(
headerClasses,
'-inset-x-8'
);
break; break;
case 'md': case 'md':
modalClasses += ' max-w-[720px] '; modalClasses = clsx(
backdropClasses += ' p-4 md:p-[8vmin]'; modalClasses,
'max-w-[720px]'
);
backdropClasses = clsx(
backdropClasses,
'p-4 md:p-[8vmin]'
);
paddingClasses = 'p-8'; paddingClasses = 'p-8';
headerClasses = clsx(
headerClasses,
'-inset-x-8'
);
break; break;
case 'lg': case 'lg':
modalClasses += ' max-w-[1020px] '; modalClasses = clsx(
backdropClasses += ' p-4 md:p-[4vmin]'; modalClasses,
'max-w-[1020px]'
);
backdropClasses = clsx(
backdropClasses,
'p-4 md:p-[4vmin]'
);
paddingClasses = 'p-8'; paddingClasses = 'p-8';
headerClasses = clsx(
headerClasses,
'-inset-x-8'
);
break; break;
case 'xl': case 'xl':
modalClasses += ' max-w-[1240px] '; modalClasses = clsx(
backdropClasses += ' p-4 md:p-[3vmin]'; modalClasses,
'max-w-[1240px]0'
);
backdropClasses = clsx(
backdropClasses,
'p-4 md:p-[3vmin]'
);
paddingClasses = 'p-10'; paddingClasses = 'p-10';
headerClasses = clsx(
headerClasses,
'-inset-x-10 -top-10'
);
break; break;
case 'full': case 'full':
modalClasses += ' h-full '; modalClasses = clsx(
backdropClasses += ' p-4 md:p-[3vmin]'; modalClasses,
'h-full'
);
backdropClasses = clsx(
backdropClasses,
'p-4 md:p-[3vmin]'
);
paddingClasses = 'p-10'; paddingClasses = 'p-10';
headerClasses = clsx(
headerClasses,
'-inset-x-10'
);
break; break;
case 'bleed': case 'bleed':
modalClasses += ' h-full '; modalClasses = clsx(
modalClasses,
'h-full'
);
paddingClasses = 'p-10'; paddingClasses = 'p-10';
headerClasses = clsx(
headerClasses,
'-inset-x-10'
);
break; break;
default: default:
backdropClasses += ' p-4 md:p-[8vmin]'; backdropClasses = clsx(
backdropClasses,
'p-4 md:p-[8vmin]'
);
paddingClasses = 'p-8'; paddingClasses = 'p-8';
headerClasses = clsx(
headerClasses,
'-inset-x-8'
);
break; break;
} }
@ -208,16 +286,34 @@ const Modal: React.FC<ModalProps> = ({
paddingClasses = 'p-0'; paddingClasses = 'p-0';
} }
// Set bottom padding for backdrop when the menu is on modalClasses = clsx(
backdropClasses += ' max-[800px]:!pb-20'; modalClasses
);
let footerClasses = clsx( headerClasses = clsx(
headerClasses,
paddingClasses,
'pb-0'
);
contentClasses = clsx(
paddingClasses,
'py-0'
);
// Set bottom padding for backdrop when the menu is on
backdropClasses = clsx(
backdropClasses,
'max-[800px]:!pb-20'
);
footerClasses = clsx(
`${paddingClasses} ${stickyFooter ? 'py-6' : 'pt-0'}`, `${paddingClasses} ${stickyFooter ? 'py-6' : 'pt-0'}`,
'flex w-full items-center justify-between' 'flex w-full items-center justify-between'
); );
let contentClasses = clsx( contentClasses = clsx(
paddingClasses, contentClasses,
((size === 'full' || size === 'bleed') && 'grow') ((size === 'full' || size === 'bleed') && 'grow')
); );
@ -273,22 +369,20 @@ const Modal: React.FC<ModalProps> = ({
formSheet && 'bg-[rgba(98,109,121,0.08)]' formSheet && 'bg-[rgba(98,109,121,0.08)]'
)}></div> )}></div>
<section className={modalClasses} data-testid={testId} style={modalStyles}> <section className={modalClasses} data-testid={testId} style={modalStyles}>
{!topRightContent || topRightContent === 'close' ?
(<header className={headerClasses}>
{title && <Heading level={3}>{title}</Heading>}
<div className={`${topRightContent !== 'close' && 'md:!invisible md:!hidden'} ${hideXOnMobile && 'hidden'} absolute right-6 top-6`}>
<Button className='-m-2 cursor-pointer p-2 opacity-50 hover:opacity-100' icon='close' iconColorClass='text-black dark:text-white' size='sm' unstyled onClick={removeModal} />
</div>
</header>)
:
(<header className={headerClasses}>
{title && <Heading level={3}>{title}</Heading>}
{topRightContent}
</header>)}
<div className={contentClasses}> <div className={contentClasses}>
<div className='h-full'> {children}
{!topRightContent || topRightContent === 'close' ?
(<>
{title && <Heading level={3}>{title}</Heading>}
<div className={`${topRightContent !== 'close' && 'md:!invisible md:!hidden'} ${hideXOnMobile && 'hidden'} absolute right-6 top-6`}>
<Button className='-m-2 cursor-pointer p-2 opacity-50 hover:opacity-100' icon='close' iconColorClass='text-black dark:text-white' size='sm' unstyled onClick={removeModal} />
</div>
</>)
:
(<div className='flex items-center justify-between gap-5'>
{title && <Heading level={3}>{title}</Heading>}
{topRightContent}
</div>)}
{children}
</div>
</div> </div>
{footerContent} {footerContent}
</section> </section>

View File

@ -5,9 +5,10 @@ import InfiniteScrollListener from '../../../admin-x-ds/global/InfiniteScrollLis
import List from '../../../admin-x-ds/global/List'; import List from '../../../admin-x-ds/global/List';
import ListItem from '../../../admin-x-ds/global/ListItem'; import ListItem from '../../../admin-x-ds/global/ListItem';
import Modal from '../../../admin-x-ds/global/modal/Modal'; import Modal from '../../../admin-x-ds/global/modal/Modal';
import MultiSelect from '../../../admin-x-ds/global/form/MultiSelect';
import NiceModal, {useModal} from '@ebay/nice-modal-react'; import NiceModal, {useModal} from '@ebay/nice-modal-react';
import NoValueLabel from '../../../admin-x-ds/global/NoValueLabel';
import Popover from '../../../admin-x-ds/global/Popover'; 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 Toggle from '../../../admin-x-ds/global/form/Toggle';
import ToggleGroup from '../../../admin-x-ds/global/form/ToggleGroup'; import ToggleGroup from '../../../admin-x-ds/global/form/ToggleGroup';
import useRouting from '../../../hooks/useRouting'; import useRouting from '../../../hooks/useRouting';
@ -70,13 +71,20 @@ const HistoryFilter: React.FC<{
excludedResources: string[]; excludedResources: string[];
toggleEventType: (event: string, included: boolean) => void; toggleEventType: (event: string, included: boolean) => void;
toggleResourceType: (resource: string, included: boolean) => void; toggleResourceType: (resource: string, included: boolean) => void;
}> = ({userId, excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => { }> = ({excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => {
const {updateRoute} = useRouting(); const {updateRoute} = useRouting();
const {users} = useStaffUsers(); const {users} = useStaffUsers();
const [searchedStaff, setSearchStaff] = useState<SelectOption | null>();
const resetStaff = () => {
setSearchStaff(null);
};
const userOptions = users.map(user => ({label: user.name, value: user.id}));
return ( return (
<div className='flex items-center gap-4'> <div className='flex items-center gap-4'>
<Popover position='right' trigger={<Button label='Filter' link />}> <Popover position='right' trigger={<Button color='outline' label='Filter' size='sm' />}>
<div className='flex w-[220px] flex-col gap-8 p-5'> <div className='flex w-[220px] flex-col gap-8 p-5'>
<ToggleGroup> <ToggleGroup>
<HistoryFilterToggle excludedItems={excludedEvents} item='added' label='Added' toggleItem={toggleEventType} /> <HistoryFilterToggle excludedItems={excludedEvents} item='added' label='Added' toggleItem={toggleEventType} />
@ -92,17 +100,23 @@ const HistoryFilter: React.FC<{
</ToggleGroup> </ToggleGroup>
</div> </div>
</Popover> </Popover>
{userId ? <div className='w-[200px]'>
<Button label='Clear search' link onClick={() => updateRoute('history/view')} /> : <Select
<div className='w-[200px]'> options={userOptions}
<MultiSelect placeholder='Search staff'
options={users.map(user => ({label: user.name, value: user.id}))} value={searchedStaff}
placeholder='Search staff' isClearable
values={[]} onSelect={(value) => {
onChange={([option]) => updateRoute(`history/view/${option.value}`)} if (value) {
/> setSearchStaff(userOptions.find(option => option.value === value)!);
</div> updateRoute(`history/view/${value}`);
} } else {
resetStaff();
updateRoute('history/view');
}
}}
/>
</div>
</div> </div>
); );
}; };
@ -195,6 +209,7 @@ const HistoryModal = NiceModal.create<RoutingModalProps>(({params}) => {
scrolling={true} scrolling={true}
size='md' size='md'
stickyFooter={true} stickyFooter={true}
stickyHeader={true}
testId='history-modal' testId='history-modal'
title='History' title='History'
topRightContent={<HistoryFilter topRightContent={<HistoryFilter
@ -211,23 +226,30 @@ const HistoryModal = NiceModal.create<RoutingModalProps>(({params}) => {
> >
<div className='relative -mb-8 mt-6'> <div className='relative -mb-8 mt-6'>
<List hint={data?.isEnd ? 'End of history log' : undefined}> <List hint={data?.isEnd ? 'End of history log' : undefined}>
<InfiniteScrollListener offset={250} onTrigger={fetchNext} /> {data?.actions ? <>
{data?.actions.map(action => !action.skip && <ListItem <InfiniteScrollListener offset={250} onTrigger={fetchNext} />
avatar={<HistoryAvatar action={action} />} {data?.actions.map(action => !action.skip && <ListItem
detail={[ avatar={<HistoryAvatar action={action} />}
new Date(action.created_at).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}), detail={[
new Date(action.created_at).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit', second: '2-digit'}) new Date(action.created_at).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}),
].join(' | ')} new Date(action.created_at).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit', second: '2-digit'})
title={ ].join(' | ')}
<div className='text-sm'> title={
{getActionTitle(action)}{isBulkAction(action) ? '' : ': '} <div className='text-sm'>
{!isBulkAction(action) && <HistoryActionDescription action={action} />} {getActionTitle(action)}{isBulkAction(action) ? '' : ': '}
{action.count ? <> {action.count} times</> : null} {!isBulkAction(action) && <HistoryActionDescription action={action} />}
<span> &mdash; by {action.actor?.name || action.actor?.slug}</span> {action.count ? <> {action.count} times</> : null}
</div> <span> &mdash; by {action.actor?.name || action.actor?.slug}</span>
} </div>
separator }
/>)} separator
/>)}
</>
:
<NoValueLabel>
No entries found.
</NoValueLabel>
}
</List> </List>
</div> </div>
</Modal> </Modal>

View File

@ -2,22 +2,17 @@ import Button from '../../../admin-x-ds/global/Button';
import React from 'react'; import React from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import useRouting from '../../../hooks/useRouting'; import useRouting from '../../../hooks/useRouting';
import {getSettingValues} from '../../../api/settings';
import {useGlobalData} from '../../providers/GlobalDataProvider';
const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => { const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => {
const {updateRoute} = useRouting(); const {updateRoute} = useRouting();
const {settings} = useGlobalData();
const openPreviewModal = () => { const openPreviewModal = () => {
updateRoute('portal/edit'); updateRoute('portal/edit');
}; };
const [membersSignupAccess] = getSettingValues<string>(settings, ['members_signup_access']);
return ( return (
<SettingGroup <SettingGroup
customButtons={<Button color='green' disabled={membersSignupAccess === 'none'} label='Customize' link linkWithPadding onClick={openPreviewModal}/>} customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
description="Customize members modal signup flow" description="Customize members modal signup flow"
keywords={keywords} keywords={keywords}
navid='portal' navid='portal'

View File

@ -316,7 +316,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
</div> </div>
</Form> </Form>
</div> </div>
<div className='sticky top-[94px] hidden shrink-0 basis-[380px] min-[920px]:!visible min-[920px]:!block'> <div className='sticky top-[96px] hidden shrink-0 basis-[380px] min-[920px]:!visible min-[920px]:!block'>
<TierDetailPreview isFreeTier={isFreeTier} tier={formState} /> <TierDetailPreview isFreeTier={isFreeTier} tier={formState} />
</div> </div>
</div> </div>

View File

@ -58,7 +58,7 @@ const NavigationModal = NiceModal.create(() => {
} }
}} }}
> >
<div className='-mb-8 mt-6'> <div className='mt-6'>
<TabView <TabView
selectedTab={selectedTab} selectedTab={selectedTab}
tabs={[ tabs={[