diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-5-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-5-chromium-linux.png index 5e5f6c3feb..7faf369647 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-5-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-6-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-6-chromium-linux.png index 4de0dfb489..a491a62527 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-6-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-creates-a-transaction-via-footer-button-6-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png index b5c82933d0..623c76625f 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-2-chromium-linux.png index 198991d565..5e27158110 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png index 3ddbfa413c..78e8dc758e 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-4-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-4-chromium-linux.png index 24c7723cb4..fbb84aef8f 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-4-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-5-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-5-chromium-linux.png index eec9c1637f..51e86751f5 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-5-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-6-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-6-chromium-linux.png index ce8318f02d..15db811d51 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-6-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-individual-account-page-and-checks-that-filtering-is-working-6-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx index 4610f141eb..1dad100b37 100644 --- a/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/AccountAutocomplete.tsx @@ -1,5 +1,10 @@ // @ts-strict-ignore -import React, { Fragment, type ComponentProps, type ReactNode } from 'react'; +import React, { + Fragment, + type ComponentProps, + type ComponentPropsWithoutRef, + type ReactElement, +} from 'react'; import { css } from 'glamor'; @@ -7,11 +12,29 @@ import { type AccountEntity } from 'loot-core/src/types/models'; import { useAccounts } from '../../hooks/useAccounts'; import { useResponsive } from '../../ResponsiveProvider'; -import { type CSSProperties, theme } from '../../style'; +import { type CSSProperties, theme, styles } from '../../style'; +import { TextOneLine } from '../common/TextOneLine'; import { View } from '../common/View'; import { Autocomplete } from './Autocomplete'; -import { ItemHeader, type ItemHeaderProps } from './ItemHeader'; +import { ItemHeader } from './ItemHeader'; + +type AccountAutocompleteItem = AccountEntity; + +type AccountListProps = { + items: AccountAutocompleteItem[]; + getItemProps: (arg: { + item: AccountAutocompleteItem; + }) => ComponentProps; + highlightedIndex: number; + embedded: boolean; + renderAccountItemGroupHeader?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + renderAccountItem?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; +}; function AccountList({ items, @@ -20,7 +43,7 @@ function AccountList({ embedded, renderAccountItemGroupHeader = defaultRenderAccountItemGroupHeader, renderAccountItem = defaultRenderAccountItem, -}) { +}: AccountListProps) { let lastItem = null; return ( @@ -69,13 +92,19 @@ function AccountList({ ); } -type AccountAutoCompleteProps = { +type AccountAutocompleteProps = ComponentProps< + typeof Autocomplete +> & { embedded?: boolean; - includeClosedAccounts: boolean; - renderAccountItemGroupHeader?: (props: ItemHeaderProps) => ReactNode; - renderAccountItem?: (props: AccountItemProps) => ReactNode; + includeClosedAccounts?: boolean; + renderAccountItemGroupHeader?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + renderAccountItem?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; closeOnBlur?: boolean; -} & ComponentProps; +}; export function AccountAutocomplete({ embedded, @@ -84,12 +113,12 @@ export function AccountAutocomplete({ renderAccountItem, closeOnBlur, ...props -}: AccountAutoCompleteProps) { - let accounts = useAccounts() || []; +}: AccountAutocompleteProps) { + const accounts = useAccounts() || []; //remove closed accounts if needed //then sort by closed, then offbudget - accounts = accounts + const accountSuggestions: AccountAutocompleteItem[] = accounts .filter(item => { return includeClosedAccounts ? item : !item.closed; }) @@ -107,7 +136,7 @@ export function AccountAutocomplete({ highlightFirst={true} embedded={embedded} closeOnBlur={closeOnBlur} - suggestions={accounts} + suggestions={accountSuggestions} renderItems={(items, getItemProps, highlightedIndex) => ( , +): ReactElement { return ; } type AccountItemProps = { - item: AccountEntity; + item: AccountAutocompleteItem; className?: string; style?: CSSProperties; highlighted?: boolean; @@ -145,6 +174,15 @@ export function AccountItem({ ...props }: AccountItemProps) { const { isNarrowWidth } = useResponsive(); + const narrowStyle = isNarrowWidth + ? { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + } + : {}; + return (
- {item.name} + {item.name}
); } -function defaultRenderAccountItem(props: AccountItemProps): ReactNode { +function defaultRenderAccountItem( + props: ComponentPropsWithoutRef, +): ReactElement { return ; } diff --git a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx index ba3b746ffa..2f0b15b340 100644 --- a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx @@ -15,12 +15,45 @@ import Downshift, { type StateChangeTypes } from 'downshift'; import { css } from 'glamor'; import { SvgRemove } from '../../icons/v2'; -import { theme, type CSSProperties } from '../../style'; +import { useResponsive } from '../../ResponsiveProvider'; +import { theme, type CSSProperties, styles } from '../../style'; import { Button } from '../common/Button'; import { Input } from '../common/Input'; import { View } from '../common/View'; import { Tooltip } from '../tooltips'; +type CommonAutocompleteProps = { + focused?: boolean; + embedded?: boolean; + containerProps?: HTMLProps; + labelProps?: { id?: string }; + inputProps?: Omit, 'onChange'> & { + onChange?: (value: string) => void; + }; + suggestions?: T[]; + tooltipStyle?: CSSProperties; + tooltipProps?: ComponentProps; + renderInput?: (props: ComponentProps) => ReactNode; + renderItems?: ( + items: T[], + getItemProps: (arg: { item: T }) => ComponentProps, + idx: number, + value?: string, + ) => ReactNode; + itemToString?: (item: T) => string; + shouldSaveFromKey?: (e: KeyboardEvent) => boolean; + filterSuggestions?: (suggestions: T[], value: string) => T[]; + openOnFocus?: boolean; + getHighlightedIndex?: (suggestions: T[]) => number | null; + highlightFirst?: boolean; + onUpdate?: (id: T['id'], value: string) => void; + strict?: boolean; + clearOnBlur?: boolean; + clearOnSelect?: boolean; + closeOnBlur?: boolean; + onClose?: () => void; +}; + type Item = { id?: string; name: string; @@ -41,7 +74,7 @@ function findItem( return value; } -function getItemName(item: null | string | Item): string { +function getItemName(item: T | T['name'] | null): string { if (item == null) { return ''; } else if (typeof item === 'string') { @@ -50,7 +83,7 @@ function getItemName(item: null | string | Item): string { return item.name || ''; } -function getItemId(item: Item | Item['id']) { +function getItemId(item: T | T['id']) { if (typeof item === 'string') { return item; } @@ -168,38 +201,12 @@ function defaultItemToString(item?: T) { return item ? getItemName(item) : ''; } -type SingleAutocompleteProps = { - focused?: boolean; - embedded?: boolean; - containerProps?: HTMLProps; - labelProps?: { id?: string }; - inputProps?: Omit, 'onChange'> & { - onChange?: (value: string) => void; - }; - suggestions?: T[]; - tooltipStyle?: CSSProperties; - tooltipProps?: ComponentProps; - renderInput?: (props: ComponentProps) => ReactNode; - renderItems?: ( - items: T[], - getItemProps: (arg: { item: T }) => ComponentProps, - idx: number, - value?: string, - ) => ReactNode; - itemToString?: (item: T) => string; - shouldSaveFromKey?: (e: KeyboardEvent) => boolean; - filterSuggestions?: (suggestions: T[], value: string) => T[]; - openOnFocus?: boolean; - getHighlightedIndex?: (suggestions: T[]) => number | null; - highlightFirst?: boolean; - onUpdate?: (id: T['id'], value: string) => void; - strict?: boolean; +type SingleAutocompleteProps = CommonAutocompleteProps & { + type?: 'single' | never; onSelect: (id: T['id'], value: string) => void; - tableBehavior?: boolean; - closeOnBlur?: boolean; value: null | T | T['id']; - isMulti?: boolean; }; + function SingleAutocomplete({ focused, embedded = false, @@ -220,10 +227,11 @@ function SingleAutocomplete({ onUpdate, strict, onSelect, - tableBehavior, + clearOnBlur = true, + clearOnSelect = false, closeOnBlur = true, + onClose, value: initialValue, - isMulti = false, }: SingleAutocompleteProps) { const [selectedItem, setSelectedItem] = useState(() => findItem(strict, suggestions, initialValue), @@ -239,6 +247,26 @@ function SingleAutocomplete({ ); const [highlightedIndex, setHighlightedIndex] = useState(null); const [isOpen, setIsOpen] = useState(embedded); + const open = () => setIsOpen(true); + const close = () => { + setIsOpen(false); + onClose?.(); + }; + + const { isNarrowWidth } = useResponsive(); + const narrowInputStyle = isNarrowWidth + ? { + ...styles.mobileMenuItem, + } + : {}; + + inputProps = { + ...inputProps, + style: { + ...narrowInputStyle, + ...inputProps.style, + }, + }; // Update the selected item if the suggestion list or initial // input value has changed @@ -273,10 +301,10 @@ function SingleAutocomplete({ setSelectedItem(item); setHighlightedIndex(null); - if (isMulti) { + if (clearOnSelect) { setValue(''); } else { - setIsOpen(false); + close(); } if (onSelect) { @@ -359,11 +387,11 @@ function SingleAutocomplete({ setValue(value); setIsChanged(true); - setIsOpen(true); + open(); }} onStateChange={changes => { if ( - tableBehavior && + !clearOnBlur && changes.type === Downshift.stateChangeTypes.mouseUp ) { return; @@ -422,7 +450,7 @@ function SingleAutocomplete({ inputProps.onFocus?.(e); if (openOnFocus) { - setIsOpen(true); + open(); } }, onBlur: e => { @@ -432,11 +460,11 @@ function SingleAutocomplete({ if (!closeOnBlur) return; - if (!tableBehavior) { + if (clearOnBlur) { if (e.target.value === '') { onSelect?.(null, e.target.value); setSelectedItem(null); - setIsOpen(false); + close(); return; } @@ -446,7 +474,7 @@ function SingleAutocomplete({ resetState(value); } else { - setIsOpen(false); + close(); } }, onKeyDown: (e: KeyboardEvent) => { @@ -506,7 +534,11 @@ function SingleAutocomplete({ setValue(getItemName(originalItem)); setSelectedItem(findItem(strict, suggestions, originalItem)); setHighlightedIndex(null); - setIsOpen(embedded ? true : false); + if (embedded) { + open(); + } else { + close(); + } } }, onChange: (e: ChangeEvent) => { @@ -579,36 +611,37 @@ function MultiItem({ name, onRemove }: MultiItemProps) { ); } -type MultiAutocompleteProps< - T extends Item, - Value = SingleAutocompleteProps['value'], -> = Omit, 'value' | 'onSelect'> & { - value: Value[]; - onSelect: (ids: Value[], id?: string) => void; +type MultiAutocompleteProps = CommonAutocompleteProps & { + type: 'multi'; + onSelect: (ids: T['id'][], id?: T['id']) => void; + value: null | T[] | T['id'][]; }; + function MultiAutocomplete({ - value: selectedItems, + value: selectedItems = [], onSelect, suggestions, strict, + clearOnBlur = true, ...props }: MultiAutocompleteProps) { const [focused, setFocused] = useState(false); const lastSelectedItems = useRef(); + const selectedItemIds = selectedItems.map(getItemId); useEffect(() => { lastSelectedItems.current = selectedItems; }); - function onRemoveItem(id: (typeof selectedItems)[0]) { - const items = selectedItems.filter(i => i !== id); + function onRemoveItem(id: T['id']) { + const items = selectedItemIds.filter(i => i !== id); onSelect(items); } - function onAddItem(id: string) { + function onAddItem(id: T['id']) { if (id) { id = id.trim(); - onSelect([...selectedItems, id], id); + onSelect([...selectedItemIds, id], id); } } @@ -617,7 +650,7 @@ function MultiAutocomplete({ prevOnKeyDown?: ComponentProps['onKeyDown'], ) { if (e.key === 'Backspace' && e.currentTarget.value === '') { - onRemoveItem(selectedItems[selectedItems.length - 1]); + onRemoveItem(selectedItemIds[selectedItems.length - 1]); } prevOnKeyDown?.(e); @@ -626,10 +659,12 @@ function MultiAutocomplete({ return ( !selectedItems.includes(getItemId(item)), + item => !selectedItemIds.includes(getItemId(item)), )} onSelect={onAddItem} highlightFirst @@ -721,18 +756,10 @@ type AutocompleteProps = | ComponentProps> | ComponentProps>; -function isMultiAutocomplete( - _props: AutocompleteProps, - multi?: boolean, -): _props is ComponentProps> { - return multi; -} - export function Autocomplete({ - multi, ...props -}: AutocompleteProps & { multi?: boolean }) { - if (isMultiAutocomplete(props, multi)) { +}: AutocompleteProps) { + if (props.type === 'multi') { return ; } diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx index d83f82de5c..91d542c11d 100644 --- a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx @@ -5,6 +5,8 @@ import React, { type ReactNode, type SVGProps, type ComponentType, + type ComponentPropsWithoutRef, + type ReactElement, } from 'react'; import { css } from 'glamor'; @@ -16,26 +18,35 @@ import { import { SvgSplit } from '../../icons/v0'; import { useResponsive } from '../../ResponsiveProvider'; -import { type CSSProperties, theme } from '../../style'; +import { type CSSProperties, theme, styles } from '../../style'; import { Text } from '../common/Text'; +import { TextOneLine } from '../common/TextOneLine'; import { View } from '../common/View'; import { Autocomplete, defaultFilterSuggestion } from './Autocomplete'; -import { ItemHeader, type ItemHeaderProps } from './ItemHeader'; +import { ItemHeader } from './ItemHeader'; + +type CategoryAutocompleteItem = CategoryEntity & { + group?: CategoryGroupEntity; +}; export type CategoryListProps = { - items: Array; + items: CategoryAutocompleteItem[]; getItemProps?: (arg: { - item: CategoryEntity; + item: CategoryAutocompleteItem; }) => Partial>; highlightedIndex: number; embedded?: boolean; footer?: ReactNode; renderSplitTransactionButton?: ( - props: SplitTransactionButtonProps, - ) => ReactNode; - renderCategoryItemGroupHeader?: (props: ItemHeaderProps) => ReactNode; - renderCategoryItem?: (props: CategoryItemProps) => ReactNode; + props: ComponentPropsWithoutRef, + ) => ReactElement; + renderCategoryItemGroupHeader?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + renderCategoryItem?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; showHiddenItems?: boolean; }; function CategoryList({ @@ -84,10 +95,8 @@ function CategoryList({ {renderCategoryItemGroupHeader({ title: groupName, style: { - color: - showHiddenItems && item.group?.hidden - ? theme.pageTextSubdued - : theme.menuAutoCompleteTextHeader, + ...(showHiddenItems && + item.group?.hidden && { color: theme.pageTextSubdued }), }, })} @@ -99,10 +108,8 @@ function CategoryList({ highlighted: highlightedIndex === idx, embedded, style: { - color: - showHiddenItems && item.hidden - ? theme.pageTextSubdued - : 'inherit', + ...(showHiddenItems && + item.hidden && { color: theme.pageTextSubdued }), }, })} @@ -116,16 +123,20 @@ function CategoryList({ } type CategoryAutocompleteProps = ComponentProps< - typeof Autocomplete + typeof Autocomplete > & { categoryGroups: Array; showSplitOption?: boolean; renderSplitTransactionButton?: ( - props: SplitTransactionButtonProps, - ) => ReactNode; - renderCategoryItemGroupHeader?: (props: ItemHeaderProps) => ReactNode; - renderCategoryItem?: (props: CategoryItemProps) => ReactNode; - showHiddenItems?: boolean; + props: ComponentPropsWithoutRef, + ) => ReactElement; + renderCategoryItemGroupHeader?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + renderCategoryItem?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + showHiddenCategories?: boolean; }; export function CategoryAutocomplete({ @@ -136,12 +147,10 @@ export function CategoryAutocomplete({ renderSplitTransactionButton, renderCategoryItemGroupHeader, renderCategoryItem, - showHiddenItems, + showHiddenCategories, ...props }: CategoryAutocompleteProps) { - const categorySuggestions: Array< - CategoryEntity & { group?: CategoryGroupEntity } - > = useMemo( + const categorySuggestions: CategoryAutocompleteItem[] = useMemo( () => categoryGroups.reduce( (list, group) => @@ -190,7 +199,7 @@ export function CategoryAutocomplete({ renderSplitTransactionButton={renderSplitTransactionButton} renderCategoryItemGroupHeader={renderCategoryItemGroupHeader} renderCategoryItem={renderCategoryItem} - showHiddenItems={showHiddenItems} + showHiddenItems={showHiddenCategories} /> )} {...props} @@ -198,7 +207,9 @@ export function CategoryAutocomplete({ ); } -function defaultRenderCategoryItemGroupHeader(props: ItemHeaderProps) { +function defaultRenderCategoryItemGroupHeader( + props: ComponentPropsWithoutRef, +): ReactElement { return ; } @@ -277,12 +288,12 @@ function SplitTransactionButton({ function defaultRenderSplitTransactionButton( props: SplitTransactionButtonProps, -) { +): ReactElement { return ; } type CategoryItemProps = { - item: CategoryEntity & { group?: CategoryGroupEntity }; + item: CategoryAutocompleteItem; className?: string; style?: CSSProperties; highlighted?: boolean; @@ -298,6 +309,15 @@ export function CategoryItem({ ...props }: CategoryItemProps) { const { isNarrowWidth } = useResponsive(); + const narrowStyle = isNarrowWidth + ? { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + } + : {}; + return (
- {item.name} - {item.hidden ? ' (hidden)' : null} + + {item.name} + {item.hidden ? ' (hidden)' : null} +
); } -function defaultRenderCategoryItem(props: CategoryItemProps) { +function defaultRenderCategoryItem( + props: ComponentPropsWithoutRef, +): ReactElement { return ; } diff --git a/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx b/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx index 981c05bec2..0bdc87ba81 100644 --- a/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx +++ b/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx @@ -1,20 +1,32 @@ import React from 'react'; -import { theme } from '../../style/theme'; +import { useResponsive } from '../../ResponsiveProvider'; +import { styles, theme } from '../../style'; import { type CSSProperties } from '../../style/types'; -export type ItemHeaderProps = { +type ItemHeaderProps = { title: string; style?: CSSProperties; type?: string; }; export function ItemHeader({ title, style, type, ...props }: ItemHeaderProps) { + const { isNarrowWidth } = useResponsive(); + const narrowStyle = isNarrowWidth + ? { + ...styles.largeText, + color: theme.menuItemTextHeader, + paddingTop: 10, + paddingBottom: 10, + } + : {}; + return (
ComponentProps; + highlightedIndex: number; + embedded: boolean; + inputValue: string; + renderCreatePayeeButton?: ( + props: ComponentPropsWithoutRef, + ) => ReactNode; + renderPayeeItemGroupHeader?: ( + props: ComponentPropsWithoutRef, + ) => ReactNode; + renderPayeeItem?: ( + props: ComponentPropsWithoutRef, + ) => ReactNode; + footer: ReactNode; +}; + function PayeeList({ items, getItemProps, @@ -70,8 +99,7 @@ function PayeeList({ renderPayeeItemGroupHeader = defaultRenderPayeeItemGroupHeader, renderPayeeItem = defaultRenderPayeeItem, footer, -}) { - const isFiltered = items.filtered; +}: PayeeListProps) { let createNew = null; items = [...items]; @@ -112,7 +140,8 @@ function PayeeList({ } else if (type === 'account' && lastType !== type) { title = 'Transfer To/From'; } - const showMoreMessage = idx === items.length - 1 && isFiltered; + const showMoreMessage = + idx === items.length - 1 && items.length > 100; lastType = type; return ( @@ -152,22 +181,24 @@ function PayeeList({ ); } -type PayeeAutocompleteProps = { - value: ComponentProps['value']; - inputProps: ComponentProps['inputProps']; +type PayeeAutocompleteProps = ComponentProps< + typeof Autocomplete +> & { showMakeTransfer?: boolean; showManagePayees?: boolean; - tableBehavior: ComponentProps['tableBehavior']; embedded?: boolean; - closeOnBlur: ComponentProps['closeOnBlur']; - onUpdate?: (value: string) => void; - onSelect?: (value: string) => void; - onManagePayees: () => void; - renderCreatePayeeButton?: (props: CreatePayeeButtonProps) => ReactNode; - renderPayeeItemGroupHeader?: (props: ItemHeaderProps) => ReactNode; - renderPayeeItem?: (props: PayeeItemProps) => ReactNode; + onManagePayees?: () => void; + renderCreatePayeeButton?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + renderPayeeItemGroupHeader?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + renderPayeeItem?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; accounts?: AccountEntity[]; - payees?: PayeeEntity[]; + payees?: PayeeAutocompleteItem[]; }; export function PayeeAutocomplete({ @@ -175,9 +206,9 @@ export function PayeeAutocomplete({ inputProps, showMakeTransfer = true, showManagePayees = false, - tableBehavior, - embedded, + clearOnBlur = true, closeOnBlur, + embedded, onUpdate, onSelect, onManagePayees, @@ -201,7 +232,7 @@ export function PayeeAutocomplete({ const [focusTransferPayees, setFocusTransferPayees] = useState(false); const [rawPayee, setRawPayee] = useState(''); const hasPayeeInput = !!rawPayee; - const payeeSuggestions = useMemo(() => { + const payeeSuggestions: PayeeAutocompleteItem[] = useMemo(() => { const suggestions = getPayeeSuggestions( payees, focusTransferPayees, @@ -216,20 +247,22 @@ export function PayeeAutocomplete({ const dispatch = useDispatch(); - async function handleSelect(value, rawInputValue) { - if (tableBehavior) { - onSelect?.(makeNew(value, rawInputValue)); + async function handleSelect(idOrIds, rawInputValue) { + if (!clearOnBlur) { + onSelect?.(makeNew(idOrIds, rawInputValue), rawInputValue); } else { - const create = () => dispatch(createPayee(rawInputValue)); + const create = payeeName => dispatch(createPayee(payeeName)); - if (Array.isArray(value)) { - value = await Promise.all(value.map(v => (v === 'new' ? create() : v))); + if (Array.isArray(idOrIds)) { + idOrIds = await Promise.all( + idOrIds.map(v => (v === 'new' ? create(rawInputValue) : v)), + ); } else { - if (value === 'new') { - value = await create(); + if (idOrIds === 'new') { + idOrIds = await create(rawInputValue); } } - onSelect?.(value); + onSelect?.(idOrIds, rawInputValue); } } @@ -242,7 +275,7 @@ export function PayeeAutocomplete({ embedded={embedded} value={stripNew(value)} suggestions={payeeSuggestions} - tableBehavior={tableBehavior} + clearOnBlur={clearOnBlur} closeOnBlur={closeOnBlur} itemToString={item => { if (!item) { @@ -262,9 +295,7 @@ export function PayeeAutocomplete({ onFocus: () => setPayeeFieldFocused(true), onChange: setRawPayee, }} - onUpdate={(value, inputValue) => - onUpdate && onUpdate(makeNew(value, inputValue)) - } + onUpdate={(id, inputValue) => onUpdate?.(id, makeNew(id, inputValue))} onSelect={handleSelect} getHighlightedIndex={suggestions => { if (suggestions.length > 1 && suggestions[0].id === 'new') { @@ -309,10 +340,7 @@ export function PayeeAutocomplete({ } }); - const isf = filtered.length > 100; filtered = filtered.slice(0, 100); - // @ts-expect-error TODO: solve this somehow - filtered.filtered = isf; if (filtered.length >= 2 && filtered[0].id === 'new') { if ( @@ -341,7 +369,7 @@ export function PayeeAutocomplete({ type={focusTransferPayees ? 'menuSelected' : 'menu'} style={showManagePayees && { marginBottom: 5 }} onClick={() => { - onUpdate?.(null); + onUpdate?.(null, null); setFocusTransferPayees(!focusTransferPayees); }} > @@ -379,6 +407,13 @@ export function CreatePayeeButton({ ...props }: CreatePayeeButtonProps) { const { isNarrowWidth } = useResponsive(); + const narrowStyle = isNarrowWidth + ? { + ...styles.mobileMenuItem, + } + : {}; + const iconSize = isNarrowWidth ? 14 : 8; + return ( ) : ( )} @@ -418,17 +454,19 @@ export function CreatePayeeButton({ } function defaultRenderCreatePayeeButton( - props: CreatePayeeButtonProps, -): ReactNode { + props: ComponentPropsWithoutRef, +): ReactElement { return ; } -function defaultRenderPayeeItemGroupHeader(props: ItemHeaderProps): ReactNode { +function defaultRenderPayeeItemGroupHeader( + props: ComponentPropsWithoutRef, +): ReactElement { return ; } type PayeeItemProps = { - item: PayeeEntity; + item: PayeeAutocompleteItem; className?: string; style?: CSSProperties; highlighted?: boolean; @@ -443,6 +481,15 @@ export function PayeeItem({ ...props }: PayeeItemProps) { const { isNarrowWidth } = useResponsive(); + const narrowStyle = isNarrowWidth + ? { + ...styles.mobileMenuItem, + color: theme.menuItemText, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + } + : {}; + return (
- {item.name} + {item.name}
); } -function defaultRenderPayeeItem(props: PayeeItemProps): ReactNode { +function defaultRenderPayeeItem( + props: ComponentPropsWithoutRef, +): ReactElement { return ; } diff --git a/packages/desktop-client/src/components/autocomplete/ReportAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/ReportAutocomplete.tsx index 7a390bbd03..d3183133ba 100644 --- a/packages/desktop-client/src/components/autocomplete/ReportAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/ReportAutocomplete.tsx @@ -6,12 +6,14 @@ import { type CustomReportEntity } from 'loot-core/src/types/models/reports'; import { Autocomplete } from './Autocomplete'; import { ReportList } from './ReportList'; +type ReportAutocompleteProps = { + embedded?: boolean; +} & ComponentProps>; + export function ReportAutocomplete({ embedded, ...props -}: { - embedded?: boolean; -} & ComponentProps>) { +}: ReportAutocompleteProps) { const reports = useReports() || []; return ( diff --git a/packages/desktop-client/src/components/budget/rollover/CoverTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/CoverTooltip.tsx index 3ee4c086d4..590fe23e77 100644 --- a/packages/desktop-client/src/components/budget/rollover/CoverTooltip.tsx +++ b/packages/desktop-client/src/components/budget/rollover/CoverTooltip.tsx @@ -57,7 +57,7 @@ export function CoverTooltip({ } }, }} - showHiddenItems={false} + showHiddenCategories={false} /> )} diff --git a/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx index 936ccfa3cb..d321eea3aa 100644 --- a/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx +++ b/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx @@ -100,7 +100,7 @@ export function TransferTooltip({ onUpdate={() => {}} onSelect={(id: string | undefined) => setCategory(id || null)} inputProps={{ onEnter: () => submit(amount), placeholder: '(none)' }} - showHiddenItems={true} + showHiddenCategories={true} /> & { +type InputProps = InputHTMLAttributes & { style?: CSSProperties; inputRef?: Ref; onEnter?: (event: KeyboardEvent) => void; diff --git a/packages/desktop-client/src/components/common/Menu.tsx b/packages/desktop-client/src/components/common/Menu.tsx index f7b913349a..4d5ed8691d 100644 --- a/packages/desktop-client/src/components/common/Menu.tsx +++ b/packages/desktop-client/src/components/common/Menu.tsx @@ -48,7 +48,7 @@ type MenuItem = { tooltip?: string; }; -export type MenuProps = { +type MenuProps = { header?: ReactNode; footer?: ReactNode; items: Array; diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx b/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx index 0a48c0e01a..2a45e9910b 100644 --- a/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx +++ b/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx @@ -5,7 +5,7 @@ import { syncAndDownload } from 'loot-core/client/actions'; import { SvgAdd } from '../../../icons/v1'; import { SvgSearchAlternate } from '../../../icons/v2'; -import { theme } from '../../../style'; +import { styles, theme } from '../../../style'; import { ButtonLink } from '../../common/ButtonLink'; import { InputWithContent } from '../../common/InputWithContent'; import { Label } from '../../common/Label'; @@ -26,7 +26,6 @@ function TransactionSearchInput({ accountName, onSearch }) { flexDirection: 'row', alignItems: 'center', backgroundColor: theme.mobilePageBackground, - margin: '11px auto 4px', padding: 10, width: '100%', }} @@ -53,11 +52,8 @@ function TransactionSearchInput({ accountName, onSearch }) { style={{ backgroundColor: theme.tableBackground, border: `1px solid ${theme.formInputBorder}`, - fontSize: 15, flex: 1, - height: 32, - marginLeft: 4, - padding: 8, + height: styles.mobileMinHeight, }} /> diff --git a/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx b/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx index d6d2ceced5..055239cc7d 100644 --- a/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx +++ b/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx @@ -63,8 +63,9 @@ export const Transaction = memo(function Transaction({ schedule, } = transaction; + const isPreview = isPreviewId(id); let amount = originalAmount; - if (isPreviewId(id)) { + if (isPreview) { amount = getScheduledAmount(amount); } @@ -89,7 +90,6 @@ export const Transaction = memo(function Transaction({ const prettyCategory = specialCategory || categoryName; - const isPreview = isPreviewId(id); const isReconciled = transaction.reconciled; const textStyle = isPreview && { fontStyle: 'italic', @@ -103,16 +103,15 @@ export const Transaction = memo(function Transaction({ backgroundColor: theme.tableBackground, border: 'none', width: '100%', + height: 60, + ...(isPreview && { + backgroundColor: theme.tableRowHeaderBackground, + }), }} > diff --git a/packages/desktop-client/src/components/modals/CloseAccount.tsx b/packages/desktop-client/src/components/modals/CloseAccount.tsx index b4ca4d38d2..afac51f8a1 100644 --- a/packages/desktop-client/src/components/modals/CloseAccount.tsx +++ b/packages/desktop-client/src/components/modals/CloseAccount.tsx @@ -151,7 +151,7 @@ export function CloseAccount({ setCategoryError(false); } }} - showHiddenItems={true} + showHiddenCategories={true} /> {categoryError && ( diff --git a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx index 6602febd30..d12e4306d1 100644 --- a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx @@ -113,7 +113,7 @@ export function ConfirmCategoryDelete({ placeholder: 'Select category...', }} onSelect={category => setTransferCategory(category)} - showHiddenItems={true} + showHiddenCategories={true} />
diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx index 22e2a9f8fd..03d68eae69 100644 --- a/packages/desktop-client/src/components/modals/EditField.jsx +++ b/packages/desktop-client/src/components/modals/EditField.jsx @@ -162,7 +162,6 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { onSelect(value); }} - isCreatable {...(isNarrowWidth && { renderCreatePayeeButton: props => ( = savedStatus === 'saved' ? { items: [ @@ -27,7 +27,7 @@ export function SaveReportMenu({ items: [], }; - const modifiedMenu: MenuProps = + const modifiedMenu: ComponentPropsWithoutRef = savedStatus === 'modified' ? { items: [ @@ -48,7 +48,7 @@ export function SaveReportMenu({ items: [], }; - const unsavedMenu: MenuProps = { + const unsavedMenu: ComponentPropsWithoutRef = { items: [ { name: 'save-report', diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx index 96cc47d293..442676cb3a 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx +++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx @@ -477,7 +477,6 @@ export function ScheduleDetails({ modalProps, actions, id, transaction }) { onSelect={id => dispatch({ type: 'set-field', field: 'payee', value: id }) } - isCreatable /> diff --git a/packages/desktop-client/src/components/select/DateSelect.tsx b/packages/desktop-client/src/components/select/DateSelect.tsx index 4ccaa1a242..cd3a4b9f39 100644 --- a/packages/desktop-client/src/components/select/DateSelect.tsx +++ b/packages/desktop-client/src/components/select/DateSelect.tsx @@ -9,6 +9,7 @@ import React, { useMemo, type MutableRefObject, type KeyboardEvent, + type ComponentProps, } from 'react'; import { parse, parseISO, format, subDays, addDays, isValid } from 'date-fns'; @@ -27,7 +28,7 @@ import { stringToInteger } from 'loot-core/src/shared/util'; import { useLocalPref } from '../../hooks/useLocalPref'; import { type CSSProperties, theme } from '../../style'; -import { Input, type InputProps } from '../common/Input'; +import { Input } from '../common/Input'; import { View, type ViewProps } from '../common/View'; import { Tooltip } from '../tooltips'; @@ -172,7 +173,7 @@ function defaultShouldSaveFromKey(e) { type DateSelectProps = { containerProps?: ViewProps; - inputProps?: InputProps; + inputProps?: ComponentProps; tooltipStyle?: CSSProperties; value: string; isOpen?: boolean; @@ -182,7 +183,7 @@ type DateSelectProps = { openOnFocus?: boolean; inputRef?: MutableRefObject; shouldSaveFromKey?: (e: KeyboardEvent) => boolean; - tableBehavior?: boolean; + clearOnBlur?: boolean; onUpdate?: (selectedDate: string) => void; onSelect: (selectedDate: string) => void; }; @@ -199,7 +200,7 @@ export function DateSelect({ openOnFocus = true, inputRef: originalInputRef, shouldSaveFromKey = defaultShouldSaveFromKey, - tableBehavior, + clearOnBlur = true, onUpdate, onSelect, }: DateSelectProps) { @@ -362,7 +363,7 @@ export function DateSelect({ } inputProps?.onBlur?.(e); - if (!tableBehavior) { + if (clearOnBlur) { // If value is empty, that drives what gets selected. // Otherwise the input is reset to whatever is already // selected diff --git a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx index 26fe76e128..d05689f40e 100644 --- a/packages/desktop-client/src/components/transactions/TransactionsTable.jsx +++ b/packages/desktop-client/src/components/transactions/TransactionsTable.jsx @@ -521,12 +521,11 @@ function PayeeCell({ style: inputStyle, }} showManagePayees={true} - tableBehavior={true} + clearOnBlur={false} focused={true} - onUpdate={onUpdate} + onUpdate={(id, value) => onUpdate?.(value)} onSelect={onSave} onManagePayees={() => onManagePayees(payeeId)} - isCreatable menuPortalTarget={undefined} /> ); @@ -917,7 +916,7 @@ const Transaction = memo(function Transaction(props) { dateFormat={dateFormat} inputProps={{ onBlur, onKeyDown, style: inputStyle }} shouldSaveFromKey={shouldSaveFromKey} - tableBehavior={true} + clearOnBlur={true} onUpdate={onUpdate} onSelect={onSave} /> @@ -962,7 +961,7 @@ const Transaction = memo(function Transaction(props) { value={accountId} accounts={accounts} shouldSaveFromKey={shouldSaveFromKey} - tableBehavior={true} + clearOnBlur={false} focused={true} inputProps={{ onBlur, onKeyDown, style: inputStyle }} onUpdate={onUpdate} @@ -1176,14 +1175,14 @@ const Transaction = memo(function Transaction(props) { categoryGroups={categoryGroups} value={categoryId} focused={true} - tableBehavior={true} + clearOnBlur={false} showSplitOption={!isChild && !isParent} shouldSaveFromKey={shouldSaveFromKey} inputProps={{ onBlur, onKeyDown, style: inputStyle }} onUpdate={onUpdate} onSelect={onSave} menuPortalTarget={undefined} - showHiddenItems={false} + showHiddenCategories={false} /> )} diff --git a/packages/desktop-client/src/components/util/GenericInput.jsx b/packages/desktop-client/src/components/util/GenericInput.jsx index 84b769cc45..188409efbc 100644 --- a/packages/desktop-client/src/components/util/GenericInput.jsx +++ b/packages/desktop-client/src/components/util/GenericInput.jsx @@ -41,6 +41,7 @@ export function GenericInput({ } const showPlaceholder = multi ? value.length === 0 : true; + const autocompleteType = multi ? 'multi' : 'single'; let content; switch (type) { @@ -49,7 +50,7 @@ export function GenericInput({ case 'payee': content = ( { - return send('payee-create', { name: name.trim() }); + return async (dispatch: Dispatch) => { + const id = await send('payee-create', { name: name.trim() }); + dispatch(getPayees()); + return id; }; } diff --git a/upcoming-release-notes/2500.md b/upcoming-release-notes/2500.md new file mode 100644 index 0000000000..190b359c7f --- /dev/null +++ b/upcoming-release-notes/2500.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Autocomplete changes related to mobile modals.