Autocomplete changes related to mobile modals PR (#2500)
* Autocomplete changes related to mobile modals PR * Release notes * Fix lint error * AccountDetails * Code review updates
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 28 KiB |
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -57,7 +57,7 @@ export function CoverTooltip({
|
||||
}
|
||||
},
|
||||
}}
|
||||
showHiddenItems={false}
|
||||
showHiddenCategories={false}
|
||||
/>
|
||||
)}
|
||||
</InitialFocus>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -151,7 +151,7 @@ export function CloseAccount({
|
||||
setCategoryError(false);
|
||||
}
|
||||
}}
|
||||
showHiddenItems={true}
|
||||
showHiddenCategories={true}
|
||||
/>
|
||||
|
||||
{categoryError && (
|
||||
|
||||
@@ -113,7 +113,7 @@ export function ConfirmCategoryDelete({
|
||||
placeholder: 'Select category...',
|
||||
}}
|
||||
onSelect={category => setTransferCategory(category)}
|
||||
showHiddenItems={true}
|
||||
showHiddenCategories={true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -162,7 +162,6 @@ export function EditField({ modalProps, name, onSubmit, onClose }) {
|
||||
|
||||
onSelect(value);
|
||||
}}
|
||||
isCreatable
|
||||
{...(isNarrowWidth && {
|
||||
renderCreatePayeeButton: props => (
|
||||
<CreatePayeeButton
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -477,7 +477,6 @@ export function ScheduleDetails({ modalProps, actions, id, transaction }) {
|
||||
onSelect={id =>
|
||||
dispatch({ type: 'set-field', field: 'payee', value: id })
|
||||
}
|
||||
isCreatable
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
6
upcoming-release-notes/2500.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Autocomplete changes related to mobile modals.
|
||||