mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +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 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'>
|
||||||
|
@ -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`}>×</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}
|
||||||
|
@ -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',
|
||||||
|
@ -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>
|
||||||
|
@ -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> — by {action.actor?.name || action.actor?.slug}</span>
|
{action.count ? <> {action.count} times</> : null}
|
||||||
</div>
|
<span> — by {action.actor?.name || action.actor?.slug}</span>
|
||||||
}
|
</div>
|
||||||
separator
|
}
|
||||||
/>)}
|
separator
|
||||||
|
/>)}
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<NoValueLabel>
|
||||||
|
No entries found.
|
||||||
|
</NoValueLabel>
|
||||||
|
}
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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={[
|
||||||
|
Loading…
Reference in New Issue
Block a user