mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
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:
parent
474923ba8a
commit
b4bcc193a4
@ -2,9 +2,11 @@ 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';
|
||||
|
||||
export type MultiSelectColor = 'grey' | 'black' | 'green' | 'pink';
|
||||
type FieldStyles = 'text' | 'dropdown';
|
||||
|
||||
export type MultiSelectOption = {
|
||||
value: string;
|
||||
@ -20,6 +22,8 @@ interface MultiSelectProps {
|
||||
error?: boolean;
|
||||
placeholder?: string;
|
||||
color?: MultiSelectColor
|
||||
size?: 'sm' | 'md';
|
||||
fieldStyle?: FieldStyles;
|
||||
hint?: string;
|
||||
onChange: (selected: MultiValue<MultiSelectOption>) => void;
|
||||
canCreate?: boolean;
|
||||
@ -40,11 +44,16 @@ const multiValueColor = (color?: MultiSelectColor) => {
|
||||
}
|
||||
};
|
||||
|
||||
const DropdownIndicator: React.FC<DropdownIndicatorProps<MultiSelectOption, true> & {clearBg: boolean}> = ({clearBg, ...props}) => (
|
||||
<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 DropdownIndicator: React.FC<DropdownIndicatorProps<MultiSelectOption, true> & {clearBg: boolean, fieldStyle: FieldStyles}> = ({clearBg, fieldStyle, ...props}) => {
|
||||
if (fieldStyle === 'text') {
|
||||
return <></>;
|
||||
}
|
||||
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}) => (
|
||||
<components.Option {...optionProps}>
|
||||
@ -58,6 +67,8 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
error = false,
|
||||
placeholder,
|
||||
color = 'grey',
|
||||
size = 'md',
|
||||
fieldStyle = 'dropdown',
|
||||
hint = '',
|
||||
options,
|
||||
values,
|
||||
@ -67,12 +78,27 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
}) => {
|
||||
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 = {
|
||||
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',
|
||||
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',
|
||||
option: 'hover:cursor-pointer hover:bg-grey-100 px-3 py-[6px] dark:text-white dark:hover:bg-grey-900',
|
||||
menu: 'shadow py-2 rounded-b z-[10000] bg-white dark:bg-black dark:border dark:border-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)}`,
|
||||
noOptionsMessage: 'p-3 text-grey-600',
|
||||
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(() => {
|
||||
// TODO: fix "Component definition is missing display name"
|
||||
// eslint-disable-next-line react/display-name
|
||||
return (ddiProps: DropdownIndicatorProps<MultiSelectOption, true>) => <DropdownIndicator {...ddiProps} clearBg={clearBg} />;
|
||||
}, [clearBg]);
|
||||
return (ddiProps: DropdownIndicatorProps<MultiSelectOption, true>) => <DropdownIndicator {...ddiProps} clearBg={clearBg} fieldStyle={fieldStyle} />;
|
||||
}, [clearBg, fieldStyle]);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
|
@ -1,8 +1,9 @@
|
||||
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 Hint from '../Hint';
|
||||
import Icon from '../Icon';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface SelectOption {
|
||||
@ -27,6 +28,7 @@ export interface SelectControlClasses {
|
||||
option?: string;
|
||||
noOptionsMessage?: string;
|
||||
groupHeading?: string;
|
||||
clearIndicator?: string;
|
||||
}
|
||||
|
||||
export interface SelectProps extends Props<SelectOption, false> {
|
||||
@ -54,6 +56,13 @@ const DropdownIndicator: React.FC<DropdownIndicatorProps<SelectOption, false> &
|
||||
</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`}>×</div> */}
|
||||
</components.ClearIndicator>
|
||||
);
|
||||
|
||||
const Option: React.FC<OptionProps<SelectOption, false>> = ({children, ...optionProps}) => (
|
||||
<components.Option {...optionProps}>
|
||||
<span data-testid="select-option">{children}</span>
|
||||
@ -98,7 +107,7 @@ const Select: React.FC<SelectProps> = ({
|
||||
const customClasses = {
|
||||
control: clsx(
|
||||
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',
|
||||
border && 'border-b',
|
||||
!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),
|
||||
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(() => {
|
||||
@ -143,9 +153,10 @@ const Select: React.FC<SelectProps> = ({
|
||||
menu: () => customClasses.menu,
|
||||
option: () => customClasses.option,
|
||||
noOptionsMessage: () => customClasses.noOptionsMessage,
|
||||
groupHeading: () => customClasses.groupHeading
|
||||
groupHeading: () => customClasses.groupHeading,
|
||||
clearIndicator: () => customClasses.clearIndicator
|
||||
}}
|
||||
components={{DropdownIndicator: dropdownIndicatorComponent, Option}}
|
||||
components={{DropdownIndicator: dropdownIndicatorComponent, Option, ClearIndicator}}
|
||||
inputId={id}
|
||||
isClearable={false}
|
||||
options={options}
|
||||
|
@ -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 = {
|
||||
args: {
|
||||
size: 'md',
|
||||
|
@ -37,6 +37,7 @@ export interface ModalProps {
|
||||
backDrop?: boolean;
|
||||
backDropClick?: boolean;
|
||||
stickyFooter?: boolean;
|
||||
stickyHeader?:boolean;
|
||||
scrolling?: boolean;
|
||||
dirty?: boolean;
|
||||
animate?: boolean;
|
||||
@ -67,6 +68,7 @@ const Modal: React.FC<ModalProps> = ({
|
||||
backDrop = true,
|
||||
backDropClick = true,
|
||||
stickyFooter = false,
|
||||
stickyHeader = false,
|
||||
scrolling = true,
|
||||
dirty = false,
|
||||
animate = true,
|
||||
@ -114,6 +116,8 @@ const Modal: React.FC<ModalProps> = ({
|
||||
|
||||
let buttons: ButtonProps[] = [];
|
||||
|
||||
let footerClasses, contentClasses;
|
||||
|
||||
const removeModal = () => {
|
||||
confirmIfDirty(dirty, () => {
|
||||
modal.remove();
|
||||
@ -161,46 +165,120 @@ const Modal: React.FC<ModalProps> = ({
|
||||
);
|
||||
|
||||
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) {
|
||||
case 'sm':
|
||||
modalClasses += ' max-w-[480px] ';
|
||||
backdropClasses += ' p-4 md:p-[8vmin]';
|
||||
modalClasses = clsx(
|
||||
modalClasses,
|
||||
'max-w-[480px]'
|
||||
);
|
||||
backdropClasses = clsx(
|
||||
backdropClasses,
|
||||
'p-4 md:p-[8vmin]'
|
||||
);
|
||||
paddingClasses = 'p-8';
|
||||
headerClasses = clsx(
|
||||
headerClasses,
|
||||
'-inset-x-8'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'md':
|
||||
modalClasses += ' max-w-[720px] ';
|
||||
backdropClasses += ' p-4 md:p-[8vmin]';
|
||||
modalClasses = clsx(
|
||||
modalClasses,
|
||||
'max-w-[720px]'
|
||||
);
|
||||
backdropClasses = clsx(
|
||||
backdropClasses,
|
||||
'p-4 md:p-[8vmin]'
|
||||
);
|
||||
paddingClasses = 'p-8';
|
||||
headerClasses = clsx(
|
||||
headerClasses,
|
||||
'-inset-x-8'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'lg':
|
||||
modalClasses += ' max-w-[1020px] ';
|
||||
backdropClasses += ' p-4 md:p-[4vmin]';
|
||||
modalClasses = clsx(
|
||||
modalClasses,
|
||||
'max-w-[1020px]'
|
||||
);
|
||||
backdropClasses = clsx(
|
||||
backdropClasses,
|
||||
'p-4 md:p-[4vmin]'
|
||||
);
|
||||
paddingClasses = 'p-8';
|
||||
headerClasses = clsx(
|
||||
headerClasses,
|
||||
'-inset-x-8'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'xl':
|
||||
modalClasses += ' max-w-[1240px] ';
|
||||
backdropClasses += ' p-4 md:p-[3vmin]';
|
||||
modalClasses = clsx(
|
||||
modalClasses,
|
||||
'max-w-[1240px]0'
|
||||
);
|
||||
backdropClasses = clsx(
|
||||
backdropClasses,
|
||||
'p-4 md:p-[3vmin]'
|
||||
);
|
||||
paddingClasses = 'p-10';
|
||||
headerClasses = clsx(
|
||||
headerClasses,
|
||||
'-inset-x-10 -top-10'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'full':
|
||||
modalClasses += ' h-full ';
|
||||
backdropClasses += ' p-4 md:p-[3vmin]';
|
||||
modalClasses = clsx(
|
||||
modalClasses,
|
||||
'h-full'
|
||||
);
|
||||
backdropClasses = clsx(
|
||||
backdropClasses,
|
||||
'p-4 md:p-[3vmin]'
|
||||
);
|
||||
paddingClasses = 'p-10';
|
||||
headerClasses = clsx(
|
||||
headerClasses,
|
||||
'-inset-x-10'
|
||||
);
|
||||
break;
|
||||
|
||||
case 'bleed':
|
||||
modalClasses += ' h-full ';
|
||||
modalClasses = clsx(
|
||||
modalClasses,
|
||||
'h-full'
|
||||
);
|
||||
paddingClasses = 'p-10';
|
||||
headerClasses = clsx(
|
||||
headerClasses,
|
||||
'-inset-x-10'
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
backdropClasses += ' p-4 md:p-[8vmin]';
|
||||
backdropClasses = clsx(
|
||||
backdropClasses,
|
||||
'p-4 md:p-[8vmin]'
|
||||
);
|
||||
paddingClasses = 'p-8';
|
||||
headerClasses = clsx(
|
||||
headerClasses,
|
||||
'-inset-x-8'
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -208,16 +286,34 @@ const Modal: React.FC<ModalProps> = ({
|
||||
paddingClasses = 'p-0';
|
||||
}
|
||||
|
||||
// Set bottom padding for backdrop when the menu is on
|
||||
backdropClasses += ' max-[800px]:!pb-20';
|
||||
modalClasses = clsx(
|
||||
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'}`,
|
||||
'flex w-full items-center justify-between'
|
||||
);
|
||||
|
||||
let contentClasses = clsx(
|
||||
paddingClasses,
|
||||
contentClasses = clsx(
|
||||
contentClasses,
|
||||
((size === 'full' || size === 'bleed') && 'grow')
|
||||
);
|
||||
|
||||
@ -273,22 +369,20 @@ const Modal: React.FC<ModalProps> = ({
|
||||
formSheet && 'bg-[rgba(98,109,121,0.08)]'
|
||||
)}></div>
|
||||
<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='h-full'>
|
||||
{!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>
|
||||
{children}
|
||||
</div>
|
||||
{footerContent}
|
||||
</section>
|
||||
|
@ -5,9 +5,10 @@ import InfiniteScrollListener from '../../../admin-x-ds/global/InfiniteScrollLis
|
||||
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 MultiSelect from '../../../admin-x-ds/global/form/MultiSelect';
|
||||
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 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 useRouting from '../../../hooks/useRouting';
|
||||
@ -70,13 +71,20 @@ const HistoryFilter: React.FC<{
|
||||
excludedResources: string[];
|
||||
toggleEventType: (event: string, included: boolean) => void;
|
||||
toggleResourceType: (resource: string, included: boolean) => void;
|
||||
}> = ({userId, excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => {
|
||||
}> = ({excludedEvents, excludedResources, toggleEventType, toggleResourceType}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
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 (
|
||||
<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'>
|
||||
<ToggleGroup>
|
||||
<HistoryFilterToggle excludedItems={excludedEvents} item='added' label='Added' toggleItem={toggleEventType} />
|
||||
@ -92,17 +100,23 @@ const HistoryFilter: React.FC<{
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
</Popover>
|
||||
{userId ?
|
||||
<Button label='Clear search' link onClick={() => updateRoute('history/view')} /> :
|
||||
<div className='w-[200px]'>
|
||||
<MultiSelect
|
||||
options={users.map(user => ({label: user.name, value: user.id}))}
|
||||
placeholder='Search staff'
|
||||
values={[]}
|
||||
onChange={([option]) => updateRoute(`history/view/${option.value}`)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className='w-[200px]'>
|
||||
<Select
|
||||
options={userOptions}
|
||||
placeholder='Search staff'
|
||||
value={searchedStaff}
|
||||
isClearable
|
||||
onSelect={(value) => {
|
||||
if (value) {
|
||||
setSearchStaff(userOptions.find(option => option.value === value)!);
|
||||
updateRoute(`history/view/${value}`);
|
||||
} else {
|
||||
resetStaff();
|
||||
updateRoute('history/view');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -195,6 +209,7 @@ const HistoryModal = NiceModal.create<RoutingModalProps>(({params}) => {
|
||||
scrolling={true}
|
||||
size='md'
|
||||
stickyFooter={true}
|
||||
stickyHeader={true}
|
||||
testId='history-modal'
|
||||
title='History'
|
||||
topRightContent={<HistoryFilter
|
||||
@ -211,23 +226,30 @@ const HistoryModal = NiceModal.create<RoutingModalProps>(({params}) => {
|
||||
>
|
||||
<div className='relative -mb-8 mt-6'>
|
||||
<List hint={data?.isEnd ? 'End of history log' : undefined}>
|
||||
<InfiniteScrollListener offset={250} onTrigger={fetchNext} />
|
||||
{data?.actions.map(action => !action.skip && <ListItem
|
||||
avatar={<HistoryAvatar action={action} />}
|
||||
detail={[
|
||||
new Date(action.created_at).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}),
|
||||
new Date(action.created_at).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit', second: '2-digit'})
|
||||
].join(' | ')}
|
||||
title={
|
||||
<div className='text-sm'>
|
||||
{getActionTitle(action)}{isBulkAction(action) ? '' : ': '}
|
||||
{!isBulkAction(action) && <HistoryActionDescription action={action} />}
|
||||
{action.count ? <> {action.count} times</> : null}
|
||||
<span> — by {action.actor?.name || action.actor?.slug}</span>
|
||||
</div>
|
||||
}
|
||||
separator
|
||||
/>)}
|
||||
{data?.actions ? <>
|
||||
<InfiniteScrollListener offset={250} onTrigger={fetchNext} />
|
||||
{data?.actions.map(action => !action.skip && <ListItem
|
||||
avatar={<HistoryAvatar action={action} />}
|
||||
detail={[
|
||||
new Date(action.created_at).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}),
|
||||
new Date(action.created_at).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit', second: '2-digit'})
|
||||
].join(' | ')}
|
||||
title={
|
||||
<div className='text-sm'>
|
||||
{getActionTitle(action)}{isBulkAction(action) ? '' : ': '}
|
||||
{!isBulkAction(action) && <HistoryActionDescription action={action} />}
|
||||
{action.count ? <> {action.count} times</> : null}
|
||||
<span> — by {action.actor?.name || action.actor?.slug}</span>
|
||||
</div>
|
||||
}
|
||||
separator
|
||||
/>)}
|
||||
</>
|
||||
:
|
||||
<NoValueLabel>
|
||||
No entries found.
|
||||
</NoValueLabel>
|
||||
}
|
||||
</List>
|
||||
</div>
|
||||
</Modal>
|
||||
|
@ -2,22 +2,17 @@ import Button from '../../../admin-x-ds/global/Button';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import useRouting from '../../../hooks/useRouting';
|
||||
import {getSettingValues} from '../../../api/settings';
|
||||
import {useGlobalData} from '../../providers/GlobalDataProvider';
|
||||
|
||||
const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
||||
const {updateRoute} = useRouting();
|
||||
const {settings} = useGlobalData();
|
||||
|
||||
const openPreviewModal = () => {
|
||||
updateRoute('portal/edit');
|
||||
};
|
||||
|
||||
const [membersSignupAccess] = getSettingValues<string>(settings, ['members_signup_access']);
|
||||
|
||||
return (
|
||||
<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"
|
||||
keywords={keywords}
|
||||
navid='portal'
|
||||
|
@ -316,7 +316,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
|
||||
</div>
|
||||
</Form>
|
||||
</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} />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,7 +58,7 @@ const NavigationModal = NiceModal.create(() => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className='-mb-8 mt-6'>
|
||||
<div className='mt-6'>
|
||||
<TabView
|
||||
selectedTab={selectedTab}
|
||||
tabs={[
|
||||
|
Loading…
Reference in New Issue
Block a user