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 5e5f6c3febb8974b7194e1ed71b725f8bf7b96f8..7faf369647c50435999b5bed682446bbb3755b2d 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 4de0dfb4895dc027a5b16661634a606b52344b0f..a491a625274924ab6b068fab1896e4d37ff519e3 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 b5c82933d0bd9a424571dde31f97ac79091b8282..623c76625f149b4c4ca542951ac28682af13bac5 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 198991d5651c8b89542d281570ed41204ad8c401..5e271581102ce676239f79c24225e182529d7af4 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 3ddbfa413cda1ffb0cbfa5e567cd6603eccde2e1..78e8dc758e83ccc5e757410b5be2103d94c18371 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 24c7723cb49adf882df3fb3514702a6e32bb79d1..fbb84aef8f2e750410e7c0851dd8b6b19300479c 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 eec9c1637fbac613360b60a0ebfa672664327965..51e86751f51469bdf9ce19a2b809ec4249959e09 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 ce8318f02dc0be2cbcbbf0b75fc5c878056ee14a..15db811d51141185fdd17e848048a2799ed11efc 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 4610f141eb44a6f315cd91101712f9d068bc11fe..1dad100b3715c82648aee99814ccb2db137aac41 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<typeof View>; + highlightedIndex: number; + embedded: boolean; + renderAccountItemGroupHeader?: ( + props: ComponentPropsWithoutRef<typeof ItemHeader>, + ) => ReactElement<typeof ItemHeader>; + renderAccountItem?: ( + props: ComponentPropsWithoutRef<typeof AccountItem>, + ) => ReactElement<typeof AccountItem>; +}; 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<AccountAutocompleteItem> +> & { embedded?: boolean; - includeClosedAccounts: boolean; - renderAccountItemGroupHeader?: (props: ItemHeaderProps) => ReactNode; - renderAccountItem?: (props: AccountItemProps) => ReactNode; + includeClosedAccounts?: boolean; + renderAccountItemGroupHeader?: ( + props: ComponentPropsWithoutRef<typeof ItemHeader>, + ) => ReactElement<typeof ItemHeader>; + renderAccountItem?: ( + props: ComponentPropsWithoutRef<typeof AccountItem>, + ) => ReactElement<typeof AccountItem>; closeOnBlur?: boolean; -} & ComponentProps<typeof Autocomplete>; +}; 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) => ( <AccountList items={items} @@ -124,13 +153,13 @@ export function AccountAutocomplete({ } function defaultRenderAccountItemGroupHeader( - props: ItemHeaderProps, -): ReactNode { + props: ComponentPropsWithoutRef<typeof ItemHeader>, +): ReactElement<typeof ItemHeader> { return <ItemHeader {...props} type="account" />; } 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 ( <div // List each account up to a max @@ -180,17 +218,20 @@ export function AccountItem({ padding: 4, paddingLeft: 20, borderRadius: embedded ? 4 : 0, + ...narrowStyle, }, ])}`} data-testid={`${item.name}-account-item`} data-highlighted={highlighted || undefined} {...props} > - {item.name} + <TextOneLine>{item.name}</TextOneLine> </div> ); } -function defaultRenderAccountItem(props: AccountItemProps): ReactNode { +function defaultRenderAccountItem( + props: ComponentPropsWithoutRef<typeof AccountItem>, +): ReactElement<typeof AccountItem> { return <AccountItem {...props} />; } diff --git a/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx b/packages/desktop-client/src/components/autocomplete/Autocomplete.tsx index ba3b746ffa778da12d315eb5e4c5bd5663131b23..2f0b15b340fdde4781f1ad6219b0cb5f244dba39 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<T extends Item> = { + focused?: boolean; + embedded?: boolean; + containerProps?: HTMLProps<HTMLDivElement>; + labelProps?: { id?: string }; + inputProps?: Omit<ComponentProps<typeof Input>, 'onChange'> & { + onChange?: (value: string) => void; + }; + suggestions?: T[]; + tooltipStyle?: CSSProperties; + tooltipProps?: ComponentProps<typeof Tooltip>; + renderInput?: (props: ComponentProps<typeof Input>) => ReactNode; + renderItems?: ( + items: T[], + getItemProps: (arg: { item: T }) => ComponentProps<typeof View>, + 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<T extends Item>( return value; } -function getItemName(item: null | string | Item): string { +function getItemName<T extends Item>(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<T extends Item>(item: T | T['id']) { if (typeof item === 'string') { return item; } @@ -168,38 +201,12 @@ function defaultItemToString<T extends Item>(item?: T) { return item ? getItemName(item) : ''; } -type SingleAutocompleteProps<T extends Item> = { - focused?: boolean; - embedded?: boolean; - containerProps?: HTMLProps<HTMLDivElement>; - labelProps?: { id?: string }; - inputProps?: Omit<ComponentProps<typeof Input>, 'onChange'> & { - onChange?: (value: string) => void; - }; - suggestions?: T[]; - tooltipStyle?: CSSProperties; - tooltipProps?: ComponentProps<typeof Tooltip>; - renderInput?: (props: ComponentProps<typeof Input>) => ReactNode; - renderItems?: ( - items: T[], - getItemProps: (arg: { item: T }) => ComponentProps<typeof View>, - 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<T extends Item> = CommonAutocompleteProps<T> & { + type?: 'single' | never; onSelect: (id: T['id'], value: string) => void; - tableBehavior?: boolean; - closeOnBlur?: boolean; value: null | T | T['id']; - isMulti?: boolean; }; + function SingleAutocomplete<T extends Item>({ focused, embedded = false, @@ -220,10 +227,11 @@ function SingleAutocomplete<T extends Item>({ onUpdate, strict, onSelect, - tableBehavior, + clearOnBlur = true, + clearOnSelect = false, closeOnBlur = true, + onClose, value: initialValue, - isMulti = false, }: SingleAutocompleteProps<T>) { const [selectedItem, setSelectedItem] = useState(() => findItem(strict, suggestions, initialValue), @@ -239,6 +247,26 @@ function SingleAutocomplete<T extends Item>({ ); 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<T extends Item>({ setSelectedItem(item); setHighlightedIndex(null); - if (isMulti) { + if (clearOnSelect) { setValue(''); } else { - setIsOpen(false); + close(); } if (onSelect) { @@ -359,11 +387,11 @@ function SingleAutocomplete<T extends Item>({ setValue(value); setIsChanged(true); - setIsOpen(true); + open(); }} onStateChange={changes => { if ( - tableBehavior && + !clearOnBlur && changes.type === Downshift.stateChangeTypes.mouseUp ) { return; @@ -422,7 +450,7 @@ function SingleAutocomplete<T extends Item>({ inputProps.onFocus?.(e); if (openOnFocus) { - setIsOpen(true); + open(); } }, onBlur: e => { @@ -432,11 +460,11 @@ function SingleAutocomplete<T extends Item>({ 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<T extends Item>({ resetState(value); } else { - setIsOpen(false); + close(); } }, onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => { @@ -506,7 +534,11 @@ function SingleAutocomplete<T extends Item>({ setValue(getItemName(originalItem)); setSelectedItem(findItem(strict, suggestions, originalItem)); setHighlightedIndex(null); - setIsOpen(embedded ? true : false); + if (embedded) { + open(); + } else { + close(); + } } }, onChange: (e: ChangeEvent<HTMLInputElement>) => { @@ -579,36 +611,37 @@ function MultiItem({ name, onRemove }: MultiItemProps) { ); } -type MultiAutocompleteProps< - T extends Item, - Value = SingleAutocompleteProps<T>['value'], -> = Omit<SingleAutocompleteProps<T>, 'value' | 'onSelect'> & { - value: Value[]; - onSelect: (ids: Value[], id?: string) => void; +type MultiAutocompleteProps<T extends Item> = CommonAutocompleteProps<T> & { + type: 'multi'; + onSelect: (ids: T['id'][], id?: T['id']) => void; + value: null | T[] | T['id'][]; }; + function MultiAutocomplete<T extends Item>({ - value: selectedItems, + value: selectedItems = [], onSelect, suggestions, strict, + clearOnBlur = true, ...props }: MultiAutocompleteProps<T>) { const [focused, setFocused] = useState(false); const lastSelectedItems = useRef<typeof selectedItems>(); + 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<T extends Item>({ prevOnKeyDown?: ComponentProps<typeof Input>['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<T extends Item>({ return ( <Autocomplete {...props} - isMulti + type="single" value={null} + clearOnBlur={clearOnBlur} + clearOnSelect={true} suggestions={suggestions.filter( - item => !selectedItems.includes(getItemId(item)), + item => !selectedItemIds.includes(getItemId(item)), )} onSelect={onAddItem} highlightFirst @@ -721,18 +756,10 @@ type AutocompleteProps<T extends Item> = | ComponentProps<typeof SingleAutocomplete<T>> | ComponentProps<typeof MultiAutocomplete<T>>; -function isMultiAutocomplete<T extends Item>( - _props: AutocompleteProps<T>, - multi?: boolean, -): _props is ComponentProps<typeof MultiAutocomplete<T>> { - return multi; -} - export function Autocomplete<T extends Item>({ - multi, ...props -}: AutocompleteProps<T> & { multi?: boolean }) { - if (isMultiAutocomplete(props, multi)) { +}: AutocompleteProps<T>) { + if (props.type === 'multi') { return <MultiAutocomplete {...props} />; } diff --git a/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryAutocomplete.tsx index d83f82de5c948866579191a230ec6158de13a6b1..91d542c11d3732198b6b5cf8029140f4be9eb3f2 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<CategoryEntity & { group?: CategoryGroupEntity }>; + items: CategoryAutocompleteItem[]; getItemProps?: (arg: { - item: CategoryEntity; + item: CategoryAutocompleteItem; }) => Partial<ComponentProps<typeof View>>; highlightedIndex: number; embedded?: boolean; footer?: ReactNode; renderSplitTransactionButton?: ( - props: SplitTransactionButtonProps, - ) => ReactNode; - renderCategoryItemGroupHeader?: (props: ItemHeaderProps) => ReactNode; - renderCategoryItem?: (props: CategoryItemProps) => ReactNode; + props: ComponentPropsWithoutRef<typeof SplitTransactionButton>, + ) => ReactElement<typeof SplitTransactionButton>; + renderCategoryItemGroupHeader?: ( + props: ComponentPropsWithoutRef<typeof ItemHeader>, + ) => ReactElement<typeof ItemHeader>; + renderCategoryItem?: ( + props: ComponentPropsWithoutRef<typeof CategoryItem>, + ) => ReactElement<typeof CategoryItem>; 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 }), }, })} </Fragment> @@ -99,10 +108,8 @@ function CategoryList({ highlighted: highlightedIndex === idx, embedded, style: { - color: - showHiddenItems && item.hidden - ? theme.pageTextSubdued - : 'inherit', + ...(showHiddenItems && + item.hidden && { color: theme.pageTextSubdued }), }, })} </Fragment> @@ -116,16 +123,20 @@ function CategoryList({ } type CategoryAutocompleteProps = ComponentProps< - typeof Autocomplete<CategoryGroupEntity> + typeof Autocomplete<CategoryAutocompleteItem> > & { categoryGroups: Array<CategoryGroupEntity>; showSplitOption?: boolean; renderSplitTransactionButton?: ( - props: SplitTransactionButtonProps, - ) => ReactNode; - renderCategoryItemGroupHeader?: (props: ItemHeaderProps) => ReactNode; - renderCategoryItem?: (props: CategoryItemProps) => ReactNode; - showHiddenItems?: boolean; + props: ComponentPropsWithoutRef<typeof SplitTransactionButton>, + ) => ReactElement<typeof SplitTransactionButton>; + renderCategoryItemGroupHeader?: ( + props: ComponentPropsWithoutRef<typeof ItemHeader>, + ) => ReactElement<typeof ItemHeader>; + renderCategoryItem?: ( + props: ComponentPropsWithoutRef<typeof CategoryItem>, + ) => ReactElement<typeof CategoryItem>; + 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<typeof ItemHeader>, +): ReactElement<typeof ItemHeader> { return <ItemHeader {...props} type="category" />; } @@ -277,12 +288,12 @@ function SplitTransactionButton({ function defaultRenderSplitTransactionButton( props: SplitTransactionButtonProps, -) { +): ReactElement<typeof SplitTransactionButton> { return <SplitTransactionButton {...props} />; } 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 ( <div style={style} @@ -313,18 +333,23 @@ export function CategoryItem({ padding: 4, paddingLeft: 20, borderRadius: embedded ? 4 : 0, + ...narrowStyle, }, ])}`} data-testid={`${item.name}-category-item`} data-highlighted={highlighted || undefined} {...props} > - {item.name} - {item.hidden ? ' (hidden)' : null} + <TextOneLine> + {item.name} + {item.hidden ? ' (hidden)' : null} + </TextOneLine> </div> ); } -function defaultRenderCategoryItem(props: CategoryItemProps) { +function defaultRenderCategoryItem( + props: ComponentPropsWithoutRef<typeof CategoryItem>, +): ReactElement<typeof CategoryItem> { return <CategoryItem {...props} />; } diff --git a/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx b/packages/desktop-client/src/components/autocomplete/ItemHeader.tsx index 981c05bec2695691a85067d0ba515d26ca775326..0bdc87ba811744effbf7aad3044cc27cd3390720 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 ( <div style={{ color: theme.menuAutoCompleteTextHeader, padding: '4px 9px', + ...narrowStyle, ...style, }} data-testid={`${title}-${type}-item-group`} diff --git a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx index 5ab2b41ef051202604be38ba10aa76ae54452d7e..540125eb7d1d185f21b807aad827fd7061532b61 100644 --- a/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx +++ b/packages/desktop-client/src/components/autocomplete/PayeeAutocomplete.tsx @@ -7,6 +7,8 @@ import React, { type ReactNode, type ComponentType, type SVGProps, + type ComponentPropsWithoutRef, + type ReactElement, } from 'react'; import { useDispatch } from 'react-redux'; @@ -23,8 +25,9 @@ import { useAccounts } from '../../hooks/useAccounts'; import { usePayees } from '../../hooks/usePayees'; import { SvgAdd } from '../../icons/v1'; import { useResponsive } from '../../ResponsiveProvider'; -import { type CSSProperties, theme } from '../../style'; +import { type CSSProperties, theme, styles } from '../../style'; import { Button } from '../common/Button'; +import { TextOneLine } from '../common/TextOneLine'; import { View } from '../common/View'; import { @@ -32,9 +35,15 @@ import { defaultFilterSuggestion, AutocompleteFooter, } from './Autocomplete'; -import { ItemHeader, type ItemHeaderProps } from './ItemHeader'; +import { ItemHeader } from './ItemHeader'; -function getPayeeSuggestions(payees, focusTransferPayees, accounts) { +type PayeeAutocompleteItem = PayeeEntity; + +function getPayeeSuggestions( + payees: PayeeAutocompleteItem[], + focusTransferPayees: boolean, + accounts: AccountEntity[], +): PayeeAutocompleteItem[] { let activePayees = accounts ? getActivePayees(payees, accounts) : payees; if (focusTransferPayees && activePayees) { @@ -44,11 +53,11 @@ function getPayeeSuggestions(payees, focusTransferPayees, accounts) { return activePayees || []; } -function makeNew(value, rawPayee) { - if (value === 'new' && !rawPayee.startsWith('new:')) { +function makeNew(id, rawPayee) { + if (id === 'new' && !rawPayee.startsWith('new:')) { return 'new:' + rawPayee; } - return value; + return id; } // Convert the fully resolved new value into the 'new' id that can be @@ -60,6 +69,26 @@ function stripNew(value) { return value; } +type PayeeListProps = { + items: PayeeAutocompleteItem[]; + getItemProps: (arg: { + item: PayeeAutocompleteItem; + }) => ComponentProps<typeof View>; + highlightedIndex: number; + embedded: boolean; + inputValue: string; + renderCreatePayeeButton?: ( + props: ComponentPropsWithoutRef<typeof CreatePayeeButton>, + ) => ReactNode; + renderPayeeItemGroupHeader?: ( + props: ComponentPropsWithoutRef<typeof ItemHeader>, + ) => ReactNode; + renderPayeeItem?: ( + props: ComponentPropsWithoutRef<typeof PayeeItem>, + ) => 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<typeof Autocomplete>['value']; - inputProps: ComponentProps<typeof Autocomplete>['inputProps']; +type PayeeAutocompleteProps = ComponentProps< + typeof Autocomplete<PayeeAutocompleteItem> +> & { showMakeTransfer?: boolean; showManagePayees?: boolean; - tableBehavior: ComponentProps<typeof Autocomplete>['tableBehavior']; embedded?: boolean; - closeOnBlur: ComponentProps<typeof Autocomplete>['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<typeof CreatePayeeButton>, + ) => ReactElement<typeof CreatePayeeButton>; + renderPayeeItemGroupHeader?: ( + props: ComponentPropsWithoutRef<typeof ItemHeader>, + ) => ReactElement<typeof ItemHeader>; + renderPayeeItem?: ( + props: ComponentPropsWithoutRef<typeof PayeeItem>, + ) => ReactElement<typeof PayeeItem>; 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 ( <View data-testid="create-payee-button" @@ -399,6 +434,7 @@ export function CreatePayeeButton({ ':active': { backgroundColor: 'rgba(100, 100, 100, .25)', }, + ...narrowStyle, ...style, }} {...props} @@ -407,8 +443,8 @@ export function CreatePayeeButton({ <Icon style={{ marginRight: 5, display: 'inline-block' }} /> ) : ( <SvgAdd - width={8} - height={8} + width={iconSize} + height={iconSize} style={{ marginRight: 5, display: 'inline-block' }} /> )} @@ -418,17 +454,19 @@ export function CreatePayeeButton({ } function defaultRenderCreatePayeeButton( - props: CreatePayeeButtonProps, -): ReactNode { + props: ComponentPropsWithoutRef<typeof CreatePayeeButton>, +): ReactElement<typeof CreatePayeeButton> { return <CreatePayeeButton {...props} />; } -function defaultRenderPayeeItemGroupHeader(props: ItemHeaderProps): ReactNode { +function defaultRenderPayeeItemGroupHeader( + props: ComponentPropsWithoutRef<typeof ItemHeader>, +): ReactElement<typeof ItemHeader> { return <ItemHeader {...props} type="payee" />; } 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 ( <div // Downshift calls `setTimeout(..., 250)` in the `onMouseMove` @@ -477,17 +524,20 @@ export function PayeeItem({ borderRadius: embedded ? 4 : 0, padding: 4, paddingLeft: 20, + ...narrowStyle, }, ])}`} data-testid={`${item.name}-payee-item`} data-highlighted={highlighted || undefined} {...props} > - {item.name} + <TextOneLine>{item.name}</TextOneLine> </div> ); } -function defaultRenderPayeeItem(props: PayeeItemProps): ReactNode { +function defaultRenderPayeeItem( + props: ComponentPropsWithoutRef<typeof PayeeItem>, +): ReactElement<typeof PayeeItem> { return <PayeeItem {...props} />; } diff --git a/packages/desktop-client/src/components/autocomplete/ReportAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/ReportAutocomplete.tsx index 7a390bbd03d49cc70d67963957e124852ebedbb5..d3183133ba99e164f2ff9055bcad861095bf5654 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<typeof Autocomplete<CustomReportEntity>>; + export function ReportAutocomplete({ embedded, ...props -}: { - embedded?: boolean; -} & ComponentProps<typeof Autocomplete<CustomReportEntity>>) { +}: 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 3ee4c086d45c33d75d0f4f5d165b973020251d2c..590fe23e773d2f22bd07522a8985a7f19d36aa96 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} /> )} </InitialFocus> diff --git a/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx b/packages/desktop-client/src/components/budget/rollover/TransferTooltip.tsx index 936ccfa3cb9fc878d6b8dd6e0ee467a1c131f073..d321eea3aa3fbb1bc9f24c9295225b4ea101cb0d 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} /> <View diff --git a/packages/desktop-client/src/components/common/Input.tsx b/packages/desktop-client/src/components/common/Input.tsx index 2be2f7d6b1c7ae3a3bdd0a06e3fca43bf7417484..5891b3d2e499fb941a71c8d28d51cc9c11bf82c2 100644 --- a/packages/desktop-client/src/components/common/Input.tsx +++ b/packages/desktop-client/src/components/common/Input.tsx @@ -21,7 +21,7 @@ export const defaultInputStyle = { border: '1px solid ' + theme.formInputBorder, }; -export type InputProps = InputHTMLAttributes<HTMLInputElement> & { +type InputProps = InputHTMLAttributes<HTMLInputElement> & { style?: CSSProperties; inputRef?: Ref<HTMLInputElement>; onEnter?: (event: KeyboardEvent<HTMLInputElement>) => void; diff --git a/packages/desktop-client/src/components/common/Menu.tsx b/packages/desktop-client/src/components/common/Menu.tsx index f7b913349a4199cb65f78fc7d173cda95752a09c..4d5ed8691dc9cc9bbdfc9ed473b903bd7c8edc31 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<T extends MenuItem = MenuItem> = { +type MenuProps<T extends MenuItem = MenuItem> = { header?: ReactNode; footer?: ReactNode; items: Array<T | typeof Menu.line>; diff --git a/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx b/packages/desktop-client/src/components/mobile/accounts/AccountDetails.jsx index 0a48c0e01a8f33d0cb47171df94a2f41b44b61b5..2a45e9910bad4288c798440b4a6e9097a9ecaa7f 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, }} /> </View> diff --git a/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx b/packages/desktop-client/src/components/mobile/transactions/Transaction.jsx index d6d2ceced5d9936ae861c9eb80fa0f278109b39a..055239cc7d6ddcd4687e2d0904f3b04927db4dfd 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, + }), }} > <ListItem style={{ flex: 1, - height: 60, - padding: '5px 10px', // remove padding when Button is back - ...(isPreview && { - backgroundColor: theme.tableRowHeaderBackground, - }), ...style, }} > diff --git a/packages/desktop-client/src/components/modals/CloseAccount.tsx b/packages/desktop-client/src/components/modals/CloseAccount.tsx index b4ca4d38d266284d50b85c22850c758f8d23f145..afac51f8a1e839e69cda778be05d0b9339487233 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 6602febd308c58c0906491769f1f7c03c070e13e..d12e4306d1c36bb0e96e9c3e021eae5668719f3a 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} /> </View> diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx index 22e2a9f8fdbc5e93310ab65f095ba1bd75c4448b..03d68eae6953ad2b826b01445b8e1e2e36d2453a 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 => ( <CreatePayeeButton diff --git a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx index 9b5ec358ec9e54bb0a51def40d7beac8f35368e2..d2ba778aefaa909c5aabfb066f3c12683a25c32b 100644 --- a/packages/desktop-client/src/components/reports/SaveReportMenu.tsx +++ b/packages/desktop-client/src/components/reports/SaveReportMenu.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { type ComponentPropsWithoutRef } from 'react'; -import { Menu, type MenuProps } from '../common/Menu'; +import { Menu } from '../common/Menu'; import { MenuTooltip } from '../common/MenuTooltip'; export function SaveReportMenu({ @@ -14,7 +14,7 @@ export function SaveReportMenu({ savedStatus: string; listReports: number; }) { - const savedMenu: MenuProps = + const savedMenu: ComponentPropsWithoutRef<typeof Menu> = savedStatus === 'saved' ? { items: [ @@ -27,7 +27,7 @@ export function SaveReportMenu({ items: [], }; - const modifiedMenu: MenuProps = + const modifiedMenu: ComponentPropsWithoutRef<typeof Menu> = savedStatus === 'modified' ? { items: [ @@ -48,7 +48,7 @@ export function SaveReportMenu({ items: [], }; - const unsavedMenu: MenuProps = { + const unsavedMenu: ComponentPropsWithoutRef<typeof Menu> = { 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 96cc47d2939a0c652d19911974539b376e4d9f4c..442676cb3a5fa914a20e0377253a1a9247e1ea7f 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 /> </FormField> diff --git a/packages/desktop-client/src/components/select/DateSelect.tsx b/packages/desktop-client/src/components/select/DateSelect.tsx index 4ccaa1a24251cb4cf3ce9900ecfe3520326c7e5c..cd3a4b9f3921a5b0789823fc3b9d583c001e5846 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<typeof Input>; tooltipStyle?: CSSProperties; value: string; isOpen?: boolean; @@ -182,7 +183,7 @@ type DateSelectProps = { openOnFocus?: boolean; inputRef?: MutableRefObject<HTMLInputElement>; shouldSaveFromKey?: (e: KeyboardEvent<HTMLInputElement>) => 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 26fe76e12834727d7b0b240908e448dd69f20bce..d05689f40e4ba040dd661e8c3b1a670987b2ed1c 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} /> )} </CustomCell> diff --git a/packages/desktop-client/src/components/util/GenericInput.jsx b/packages/desktop-client/src/components/util/GenericInput.jsx index 84b769cc45c6eef236899059a1e0686074dd97a3..188409efbce3179a7cc58c16fde91b999b61fe20 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 = ( <PayeeAutocomplete - multi={multi} + type={autocompleteType} showMakeTransfer={false} openOnFocus={true} value={value} @@ -65,8 +66,8 @@ export function GenericInput({ case 'account': content = ( <AccountAutocomplete + type={autocompleteType} value={value} - multi={multi} openOnFocus={true} onSelect={onChange} inputProps={{ @@ -80,12 +81,12 @@ export function GenericInput({ case 'category': content = ( <CategoryAutocomplete + type={autocompleteType} categoryGroups={categoryGroups} value={value} - multi={multi} openOnFocus={true} onSelect={onChange} - showHiddenItems={false} + showHiddenCategories={false} inputProps={{ inputRef, ...(showPlaceholder ? { placeholder: 'nothing' } : null), @@ -103,9 +104,9 @@ export function GenericInput({ case 'saved': content = ( <FilterAutocomplete + type={autocompleteType} saved={saved} value={value} - multi={multi} openOnFocus={true} onSelect={onChange} inputProps={{ @@ -118,9 +119,9 @@ export function GenericInput({ case 'report': content = ( <ReportAutocomplete + type={autocompleteType} saved={savedReports} value={value} - multi={multi} openOnFocus={true} onSelect={onChange} inputProps={{ @@ -200,7 +201,7 @@ export function GenericInput({ if (multi) { content = ( <Autocomplete - multi={true} + type={autocompleteType} suggestions={[]} value={value} inputProps={{ inputRef }} diff --git a/packages/desktop-client/src/style/styles.ts b/packages/desktop-client/src/style/styles.ts index 862ac7ebfffe1eadced06345e210bbc2e7b5065b..7f051af222f2e07a34727fbb93c3530fb058e804 100644 --- a/packages/desktop-client/src/style/styles.ts +++ b/packages/desktop-client/src/style/styles.ts @@ -8,11 +8,21 @@ import { tokens } from '../tokens'; import { theme } from './theme'; import { type CSSProperties } from './types'; +const MOBILE_MIN_HEIGHT = 40; + export const styles = { incomeHeaderHeight: 70, cardShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)', monthRightPadding: 5, menuBorderRadius: 4, + mobileMinHeight: MOBILE_MIN_HEIGHT, + mobileMenuItem: { + fontSize: 17, + fontWeight: 400, + paddingTop: 8, + paddingBottom: 8, + height: MOBILE_MIN_HEIGHT, + }, mobileEditingPadding: 12, altMenuMaxHeight: 250, altMenuText: { diff --git a/packages/loot-core/src/client/actions/queries.ts b/packages/loot-core/src/client/actions/queries.ts index 1e2082d1249c944ebdff5b8d45b06bfc5876975d..692d5968224ff76dd256d911f328e3f18b90b2cd 100644 --- a/packages/loot-core/src/client/actions/queries.ts +++ b/packages/loot-core/src/client/actions/queries.ts @@ -249,8 +249,10 @@ export function initiallyLoadPayees() { } export function createPayee(name: string) { - return async () => { - 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 0000000000000000000000000000000000000000..190b359c7f084a2b23bd621b60dff950fdcc15d8 --- /dev/null +++ b/upcoming-release-notes/2500.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Autocomplete changes related to mobile modals.