From b4bcc193a4b1f44976d4d9a1785e31d4a103c60c Mon Sep 17 00:00:00 2001 From: Peter Zimon Date: Wed, 20 Sep 2023 13:28:29 +0300 Subject: [PATCH] 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 --- .../admin-x-ds/global/form/MultiSelect.tsx | 46 +++-- .../src/admin-x-ds/global/form/Select.tsx | 21 ++- .../admin-x-ds/global/modal/Modal.stories.tsx | 14 ++ .../src/admin-x-ds/global/modal/Modal.tsx | 158 ++++++++++++++---- .../settings/advanced/HistoryModal.tsx | 84 ++++++---- .../components/settings/membership/Portal.tsx | 7 +- .../membership/tiers/TierDetailModal.tsx | 2 +- .../settings/site/NavigationModal.tsx | 2 +- 8 files changed, 248 insertions(+), 86 deletions(-) diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/MultiSelect.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/MultiSelect.tsx index efb854e66a..6c16d31932 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/MultiSelect.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/MultiSelect.tsx @@ -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) => void; canCreate?: boolean; @@ -40,11 +44,16 @@ const multiValueColor = (color?: MultiSelectColor) => { } }; -const DropdownIndicator: React.FC & {clearBg: boolean}> = ({clearBg, ...props}) => ( - -
-
-); +const DropdownIndicator: React.FC & {clearBg: boolean, fieldStyle: FieldStyles}> = ({clearBg, fieldStyle, ...props}) => { + if (fieldStyle === 'text') { + return <>; + } + return ( + +
+
+ ); +}; const Option: React.FC> = ({children, ...optionProps}) => ( @@ -58,6 +67,8 @@ const MultiSelect: React.FC = ({ error = false, placeholder, color = 'grey', + size = 'md', + fieldStyle = 'dropdown', hint = '', options, values, @@ -67,12 +78,27 @@ const MultiSelect: React.FC = ({ }) => { 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 = ({ const dropdownIndicatorComponent = useMemo(() => { // TODO: fix "Component definition is missing display name" // eslint-disable-next-line react/display-name - return (ddiProps: DropdownIndicatorProps) => ; - }, [clearBg]); + return (ddiProps: DropdownIndicatorProps) => ; + }, [clearBg, fieldStyle]); return (
diff --git a/apps/admin-x-settings/src/admin-x-ds/global/form/Select.tsx b/apps/admin-x-settings/src/admin-x-ds/global/form/Select.tsx index dfa5081bb8..b5bb01dc92 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/form/Select.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/form/Select.tsx @@ -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 { @@ -54,6 +56,13 @@ const DropdownIndicator: React.FC & ); +const ClearIndicator: React.FC> = props => ( + + + {/*
×
*/} +
+); + const Option: React.FC> = ({children, ...optionProps}) => ( {children} @@ -98,7 +107,7 @@ const Select: React.FC = ({ 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 = ({ ), 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 = ({ 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} diff --git a/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.stories.tsx b/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.stories.tsx index 735ed12d56..2b2b33d432 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.stories.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.stories.tsx @@ -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', diff --git a/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.tsx b/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.tsx index 9dbc91a5a6..e66052384f 100644 --- a/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.tsx +++ b/apps/admin-x-settings/src/admin-x-ds/global/modal/Modal.tsx @@ -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 = ({ backDrop = true, backDropClick = true, stickyFooter = false, + stickyHeader = false, scrolling = true, dirty = false, animate = true, @@ -114,6 +116,8 @@ const Modal: React.FC = ({ let buttons: ButtonProps[] = []; + let footerClasses, contentClasses; + const removeModal = () => { confirmIfDirty(dirty, () => { modal.remove(); @@ -161,46 +165,120 @@ const Modal: React.FC = ({ ); 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 = ({ 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 = ({ formSheet && 'bg-[rgba(98,109,121,0.08)]' )}>
+ {!topRightContent || topRightContent === 'close' ? + (
+ {title && {title}} +
+
+
) + : + (
+ {title && {title}} + {topRightContent} +
)}
-
- {!topRightContent || topRightContent === 'close' ? - (<> - {title && {title}} -
-
- ) - : - (
- {title && {title}} - {topRightContent} -
)} - {children} -
+ {children}
{footerContent}
diff --git a/apps/admin-x-settings/src/components/settings/advanced/HistoryModal.tsx b/apps/admin-x-settings/src/components/settings/advanced/HistoryModal.tsx index 5aa4c2417c..eaf116b288 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/HistoryModal.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/HistoryModal.tsx @@ -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(); + + const resetStaff = () => { + setSearchStaff(null); + }; + + const userOptions = users.map(user => ({label: user.name, value: user.id})); return (
- }> + }>
@@ -92,17 +100,23 @@ const HistoryFilter: React.FC<{
- {userId ? -