mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 11:42:54 -05:00
Compare commits
7 Commits
react-quer
...
PayeeAutoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25c5a59ca1 | ||
|
|
de6f7cb59b | ||
|
|
db8e994bd0 | ||
|
|
4567235f3e | ||
|
|
4406c2515f | ||
|
|
e8508310e2 | ||
|
|
28db29ac5e |
@@ -14,6 +14,7 @@
|
|||||||
"./block": "./src/Block.tsx",
|
"./block": "./src/Block.tsx",
|
||||||
"./button": "./src/Button.tsx",
|
"./button": "./src/Button.tsx",
|
||||||
"./card": "./src/Card.tsx",
|
"./card": "./src/Card.tsx",
|
||||||
|
"./combo-box": "./src/ComboBox.tsx",
|
||||||
"./form-error": "./src/FormError.tsx",
|
"./form-error": "./src/FormError.tsx",
|
||||||
"./initial-focus": "./src/InitialFocus.ts",
|
"./initial-focus": "./src/InitialFocus.ts",
|
||||||
"./inline-field": "./src/InlineField.tsx",
|
"./inline-field": "./src/InlineField.tsx",
|
||||||
|
|||||||
198
packages/component-library/src/ComboBox.tsx
Normal file
198
packages/component-library/src/ComboBox.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import {
|
||||||
|
type ComponentProps,
|
||||||
|
useRef,
|
||||||
|
useContext,
|
||||||
|
type KeyboardEvent,
|
||||||
|
useEffect,
|
||||||
|
createContext,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
import {
|
||||||
|
ComboBox as AriaComboBox,
|
||||||
|
ListBox,
|
||||||
|
ListBoxItem,
|
||||||
|
ListBoxSection,
|
||||||
|
type ListBoxSectionProps,
|
||||||
|
type ComboBoxProps as AriaComboBoxProps,
|
||||||
|
type ListBoxItemProps,
|
||||||
|
ComboBoxStateContext as AriaComboBoxStateContext,
|
||||||
|
type Key,
|
||||||
|
} from 'react-aria-components';
|
||||||
|
import { type ComboBoxState as AriaComboBoxState } from 'react-stately';
|
||||||
|
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
|
||||||
|
import { Input } from './Input';
|
||||||
|
import { Popover } from './Popover';
|
||||||
|
import { styles } from './styles';
|
||||||
|
import { theme } from './theme';
|
||||||
|
import { View } from './View';
|
||||||
|
|
||||||
|
const popoverClassName = () =>
|
||||||
|
css({
|
||||||
|
...styles.darkScrollbar,
|
||||||
|
...styles.popover,
|
||||||
|
backgroundColor: theme.menuAutoCompleteBackground,
|
||||||
|
color: theme.menuAutoCompleteText,
|
||||||
|
padding: '5px 0',
|
||||||
|
borderRadius: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
const listBoxClassName = ({ width }: { width?: number }) =>
|
||||||
|
css({
|
||||||
|
width,
|
||||||
|
minWidth: 200,
|
||||||
|
maxHeight: 200,
|
||||||
|
overflow: 'auto',
|
||||||
|
'& [data-focused]': {
|
||||||
|
backgroundColor: theme.menuAutoCompleteBackgroundHover,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type ComboBoxProps<T extends object> = Omit<
|
||||||
|
AriaComboBoxProps<T>,
|
||||||
|
'children'
|
||||||
|
> & {
|
||||||
|
inputPlaceholder?: string;
|
||||||
|
children: ComponentProps<typeof ListBox<T>>['children'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ComboBox<T extends object>({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ComboBoxProps<T>) {
|
||||||
|
const viewRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
return (
|
||||||
|
<AriaComboBox<T>
|
||||||
|
allowsEmptyCollection
|
||||||
|
allowsCustomValue
|
||||||
|
menuTrigger="focus"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<View ref={viewRef}>
|
||||||
|
<ComboBoxInput placeholder={props.inputPlaceholder} />
|
||||||
|
</View>
|
||||||
|
<Popover isNonModal className={popoverClassName()}>
|
||||||
|
<ListBox<T>
|
||||||
|
className={listBoxClassName({ width: viewRef.current?.clientWidth })}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ListBox>
|
||||||
|
</Popover>
|
||||||
|
</AriaComboBox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComboBoxInputContextValue = {
|
||||||
|
getFocusedKey?: (state: AriaComboBoxState<unknown>) => Key | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ComboBoxInputContext = createContext<ComboBoxInputContextValue | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
type ComboBoxInputProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
getFocusedKey?: (state: AriaComboBoxState<unknown>) => Key | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ComboBoxInputProvider({
|
||||||
|
children,
|
||||||
|
getFocusedKey,
|
||||||
|
}: ComboBoxInputProviderProps) {
|
||||||
|
return (
|
||||||
|
<ComboBoxInputContext.Provider value={{ getFocusedKey }}>
|
||||||
|
{children}
|
||||||
|
</ComboBoxInputContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComboBoxInputProps = ComponentProps<typeof Input>;
|
||||||
|
|
||||||
|
function ComboBoxInput({ onKeyUp, ...props }: ComboBoxInputProps) {
|
||||||
|
const state = useContext(AriaComboBoxStateContext);
|
||||||
|
const _onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
state?.revert();
|
||||||
|
}
|
||||||
|
onKeyUp?.(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const comboBoxInputContext = useContext(ComboBoxInputContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state && state.inputValue && !state.selectionManager.focusedKey) {
|
||||||
|
const focusedKey: Key | null =
|
||||||
|
(comboBoxInputContext?.getFocusedKey
|
||||||
|
? comboBoxInputContext.getFocusedKey(state)
|
||||||
|
: defaultGetFocusedKey(state)) ?? null;
|
||||||
|
|
||||||
|
state.selectionManager.setFocusedKey(focusedKey);
|
||||||
|
}
|
||||||
|
}, [comboBoxInputContext, state, state?.inputValue]);
|
||||||
|
|
||||||
|
return <Input onKeyUp={_onKeyUp} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultGetFocusedKey<T>(state: AriaComboBoxState<T>) {
|
||||||
|
// Focus on the first suggestion item when typing.
|
||||||
|
const keys = Array.from(state.collection.getKeys());
|
||||||
|
return (
|
||||||
|
keys
|
||||||
|
.map(key => state.collection.getItem(key))
|
||||||
|
.find(i => i && i.type === 'item')?.key ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultComboBoxSectionClassName = () =>
|
||||||
|
css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
'& header': {
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingBottom: 5,
|
||||||
|
paddingLeft: 10,
|
||||||
|
color: theme.menuAutoCompleteTextHeader,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
type ComboBoxSectionProps<T extends object> = ListBoxSectionProps<T>;
|
||||||
|
|
||||||
|
export function ComboBoxSection<T extends object>({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComboBoxSectionProps<T>) {
|
||||||
|
return (
|
||||||
|
<ListBoxSection
|
||||||
|
className={cx(defaultComboBoxSectionClassName(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultComboBoxItemClassName = () =>
|
||||||
|
css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingBottom: 5,
|
||||||
|
paddingLeft: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
type ComboBoxItemProps = ListBoxItemProps;
|
||||||
|
|
||||||
|
export function ComboBoxItem({ className, ...props }: ComboBoxItemProps) {
|
||||||
|
return (
|
||||||
|
<ListBoxItem
|
||||||
|
className={
|
||||||
|
typeof className === 'function'
|
||||||
|
? renderProps =>
|
||||||
|
cx(defaultComboBoxItemClassName(), className(renderProps))
|
||||||
|
: cx(defaultComboBoxItemClassName(), className)
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
import { type ComponentProps, useMemo, useState } from 'react';
|
||||||
|
import { Header, type Key } from 'react-aria-components';
|
||||||
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ComboBox,
|
||||||
|
ComboBoxInputProvider,
|
||||||
|
ComboBoxItem,
|
||||||
|
ComboBoxSection,
|
||||||
|
} from '@actual-app/components/combo-box';
|
||||||
|
import { SvgAdd } from '@actual-app/components/icons/v1';
|
||||||
|
import { theme } from '@actual-app/components/theme';
|
||||||
|
|
||||||
|
import {
|
||||||
|
normalisedEquals,
|
||||||
|
normalisedIncludes,
|
||||||
|
} from 'loot-core/shared/normalisation';
|
||||||
|
import { type AccountEntity, type PayeeEntity } from 'loot-core/types/models';
|
||||||
|
|
||||||
|
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||||
|
import { useCommonPayees, usePayees } from '@desktop-client/hooks/usePayees';
|
||||||
|
import { usePrevious } from '@desktop-client/hooks/usePrevious';
|
||||||
|
import {
|
||||||
|
createPayee,
|
||||||
|
getActivePayees,
|
||||||
|
} from '@desktop-client/payees/payeesSlice';
|
||||||
|
import { useDispatch } from '@desktop-client/redux';
|
||||||
|
|
||||||
|
type PayeeComboBoxItem = PayeeEntity & {
|
||||||
|
type: 'account' | 'payee' | 'suggested';
|
||||||
|
};
|
||||||
|
|
||||||
|
type PayeeComboBoxProps = Omit<ComponentProps<typeof ComboBox>, 'children'> & {
|
||||||
|
showInactive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PayeeComboBox({
|
||||||
|
showInactive,
|
||||||
|
selectedKey,
|
||||||
|
onOpenChange,
|
||||||
|
onSelectionChange,
|
||||||
|
...props
|
||||||
|
}: PayeeComboBoxProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const payees = usePayees();
|
||||||
|
const commonPayees = useCommonPayees();
|
||||||
|
const accounts = useAccounts();
|
||||||
|
const [focusTransferPayees] = useState(false);
|
||||||
|
|
||||||
|
const allPayeeSuggestions: PayeeComboBoxItem[] = useMemo(() => {
|
||||||
|
const suggestions = getPayeeSuggestions(commonPayees, payees);
|
||||||
|
|
||||||
|
let filteredSuggestions: PayeeComboBoxItem[] = [...suggestions];
|
||||||
|
|
||||||
|
if (!showInactive) {
|
||||||
|
filteredSuggestions = filterActivePayees(filteredSuggestions, accounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focusTransferPayees) {
|
||||||
|
filteredSuggestions = filterTransferPayees(filteredSuggestions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredSuggestions;
|
||||||
|
}, [commonPayees, payees, showInactive, focusTransferPayees, accounts]);
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState(
|
||||||
|
getPayeeName(allPayeeSuggestions, selectedKey || null),
|
||||||
|
);
|
||||||
|
const [_selectedKey, setSelectedKey] = useState<Key | null>(
|
||||||
|
selectedKey || null,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
|
||||||
|
const previousIsAutocompeleteOpen = usePrevious(isAutocompleteOpen);
|
||||||
|
const isInitialAutocompleteOpen =
|
||||||
|
!previousIsAutocompeleteOpen && isAutocompleteOpen;
|
||||||
|
|
||||||
|
const filter = (textValue: string, inputValue: string) => {
|
||||||
|
return (
|
||||||
|
isInitialAutocompleteOpen || normalisedIncludes(textValue, inputValue)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredPayeeSuggestions = allPayeeSuggestions.filter(p =>
|
||||||
|
filter(p.name, inputValue),
|
||||||
|
);
|
||||||
|
|
||||||
|
const suggestedPayees = filteredPayeeSuggestions.filter(
|
||||||
|
p => p.type === 'suggested',
|
||||||
|
);
|
||||||
|
const regularPayees = filteredPayeeSuggestions.filter(
|
||||||
|
p => !p.favorite && p.type === 'payee',
|
||||||
|
);
|
||||||
|
const accountPayees = filteredPayeeSuggestions.filter(
|
||||||
|
p => p.type === 'account',
|
||||||
|
);
|
||||||
|
|
||||||
|
const findExactMatchPayee = () =>
|
||||||
|
filteredPayeeSuggestions.find(
|
||||||
|
p => p.id !== 'new' && normalisedEquals(p.name, inputValue),
|
||||||
|
);
|
||||||
|
|
||||||
|
const exactMatchPayee = findExactMatchPayee();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const onCreatePayee = () => {
|
||||||
|
return dispatch(createPayee({ name: inputValue })).unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
const _onSelectionChange = async (id: Key | null) => {
|
||||||
|
if (id === 'new') {
|
||||||
|
const newPayeeId = await onCreatePayee?.();
|
||||||
|
setSelectedKey(newPayeeId);
|
||||||
|
onSelectionChange?.(newPayeeId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedKey(id);
|
||||||
|
setInputValue(getPayeeName(filteredPayeeSuggestions, id));
|
||||||
|
onSelectionChange?.(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const _onOpenChange = (isOpen: boolean) => {
|
||||||
|
setIsAutocompleteOpen(isOpen);
|
||||||
|
onOpenChange?.(isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFocusedKey: ComponentProps<
|
||||||
|
typeof ComboBoxInputProvider
|
||||||
|
>['getFocusedKey'] = state => {
|
||||||
|
const keys = Array.from(state.collection.getKeys());
|
||||||
|
const found = keys
|
||||||
|
.map(key => state.collection.getItem(key))
|
||||||
|
.find(i => i && i.type === 'item' && i.key !== 'new');
|
||||||
|
|
||||||
|
// Focus on the first suggestion item when typing.
|
||||||
|
// Otherwise, if there are no results, focus on the "new" item to allow creating a new entry.
|
||||||
|
return found?.key || 'new';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComboBoxInputProvider getFocusedKey={getFocusedKey}>
|
||||||
|
<ComboBox
|
||||||
|
aria-label={t('Payee autocomplete')}
|
||||||
|
inputPlaceholder="nothing"
|
||||||
|
inputValue={inputValue}
|
||||||
|
onInputChange={setInputValue}
|
||||||
|
selectedKey={_selectedKey || selectedKey}
|
||||||
|
onSelectionChange={_onSelectionChange}
|
||||||
|
onOpenChange={_onOpenChange}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<PayeeList
|
||||||
|
showCreatePayee={!!inputValue && !exactMatchPayee}
|
||||||
|
inputValue={inputValue}
|
||||||
|
suggestedPayees={suggestedPayees}
|
||||||
|
regularPayees={regularPayees}
|
||||||
|
accountPayees={accountPayees}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <ComboBoxSection className={css({ position: 'sticky', bottom: 0, })}>
|
||||||
|
<Button variant="menu" slot={null}>
|
||||||
|
<Trans>Make transfer</Trans>
|
||||||
|
</Button>
|
||||||
|
<Button variant="menu" slot={null}>
|
||||||
|
<Trans>Manage payees</Trans>
|
||||||
|
</Button>
|
||||||
|
</ComboBoxSection> */}
|
||||||
|
</ComboBox>
|
||||||
|
</ComboBoxInputProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayeeListProps = {
|
||||||
|
showCreatePayee: boolean;
|
||||||
|
inputValue: string;
|
||||||
|
suggestedPayees: PayeeComboBoxItem[];
|
||||||
|
regularPayees: PayeeComboBoxItem[];
|
||||||
|
accountPayees: PayeeComboBoxItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function PayeeList({
|
||||||
|
showCreatePayee,
|
||||||
|
inputValue,
|
||||||
|
suggestedPayees,
|
||||||
|
regularPayees,
|
||||||
|
accountPayees,
|
||||||
|
}: PayeeListProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showCreatePayee && (
|
||||||
|
<ComboBoxItem
|
||||||
|
key="new"
|
||||||
|
id="new"
|
||||||
|
textValue={inputValue}
|
||||||
|
style={{ paddingLeft: 10, color: theme.noticeText }}
|
||||||
|
>
|
||||||
|
<SvgAdd width={8} height={8} style={{ marginRight: 5 }} />
|
||||||
|
Create payee: {inputValue}
|
||||||
|
</ComboBoxItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{suggestedPayees.length > 0 && (
|
||||||
|
<ComboBoxSection>
|
||||||
|
<Header>
|
||||||
|
<Trans>Suggested Payees</Trans>
|
||||||
|
</Header>
|
||||||
|
{suggestedPayees.map(payee => (
|
||||||
|
<ComboBoxItem
|
||||||
|
key={payee.id}
|
||||||
|
id={payee.id}
|
||||||
|
textValue={payee.name}
|
||||||
|
value={payee}
|
||||||
|
>
|
||||||
|
{payee.name}
|
||||||
|
</ComboBoxItem>
|
||||||
|
))}
|
||||||
|
</ComboBoxSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{regularPayees.length > 0 && (
|
||||||
|
<ComboBoxSection>
|
||||||
|
<Header>
|
||||||
|
<Trans>Payees</Trans>
|
||||||
|
</Header>
|
||||||
|
{regularPayees.map(payee => (
|
||||||
|
<ComboBoxItem
|
||||||
|
key={payee.id}
|
||||||
|
id={payee.id}
|
||||||
|
textValue={payee.name}
|
||||||
|
value={payee}
|
||||||
|
>
|
||||||
|
{payee.name}
|
||||||
|
</ComboBoxItem>
|
||||||
|
))}
|
||||||
|
</ComboBoxSection>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{accountPayees.length > 0 && (
|
||||||
|
<ComboBoxSection>
|
||||||
|
<Header>
|
||||||
|
<Trans>Transfer To/From</Trans>
|
||||||
|
</Header>
|
||||||
|
{accountPayees.map(payee => (
|
||||||
|
<ComboBoxItem
|
||||||
|
key={payee.id}
|
||||||
|
id={payee.id}
|
||||||
|
textValue={payee.name}
|
||||||
|
value={payee}
|
||||||
|
>
|
||||||
|
{payee.name}
|
||||||
|
</ComboBoxItem>
|
||||||
|
))}
|
||||||
|
</ComboBoxSection>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_AUTO_SUGGESTIONS = 5;
|
||||||
|
|
||||||
|
function getPayeeSuggestions(
|
||||||
|
commonPayees: PayeeEntity[],
|
||||||
|
payees: PayeeEntity[],
|
||||||
|
): PayeeComboBoxItem[] {
|
||||||
|
const favoritePayees = payees
|
||||||
|
.filter(p => p.favorite)
|
||||||
|
.map(p => {
|
||||||
|
return { ...p, type: determineType(p, true) };
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
let additionalCommonPayees: PayeeComboBoxItem[] = [];
|
||||||
|
if (commonPayees?.length > 0) {
|
||||||
|
if (favoritePayees.length < MAX_AUTO_SUGGESTIONS) {
|
||||||
|
additionalCommonPayees = commonPayees
|
||||||
|
.filter(
|
||||||
|
p => !(p.favorite || favoritePayees.map(fp => fp.id).includes(p.id)),
|
||||||
|
)
|
||||||
|
.slice(0, MAX_AUTO_SUGGESTIONS - favoritePayees.length)
|
||||||
|
.map(p => {
|
||||||
|
return { ...p, type: determineType(p, true) };
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (favoritePayees.length + additionalCommonPayees.length) {
|
||||||
|
const filteredPayees: PayeeComboBoxItem[] = payees
|
||||||
|
.filter(p => !favoritePayees.find(fp => fp.id === p.id))
|
||||||
|
.filter(p => !additionalCommonPayees.find(fp => fp.id === p.id))
|
||||||
|
.map<PayeeComboBoxItem>(p => {
|
||||||
|
return { ...p, type: determineType(p, false) };
|
||||||
|
});
|
||||||
|
|
||||||
|
return favoritePayees.concat(additionalCommonPayees).concat(filteredPayees);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payees.map(p => {
|
||||||
|
return { ...p, type: determineType(p, false) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterActivePayees<T extends PayeeEntity>(
|
||||||
|
payees: T[],
|
||||||
|
accounts: AccountEntity[],
|
||||||
|
): T[] {
|
||||||
|
return accounts ? (getActivePayees(payees, accounts) as T[]) : payees;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterTransferPayees(payees: PayeeComboBoxItem[]) {
|
||||||
|
return payees.filter(payee => !!payee.transfer_acct);
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineType(
|
||||||
|
payee: PayeeEntity,
|
||||||
|
isCommon: boolean,
|
||||||
|
): PayeeComboBoxItem['type'] {
|
||||||
|
if (payee.transfer_acct) {
|
||||||
|
return 'account';
|
||||||
|
}
|
||||||
|
if (isCommon) {
|
||||||
|
return 'suggested';
|
||||||
|
} else {
|
||||||
|
return 'payee';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPayeeName<T extends PayeeEntity>(items: T[], id: Key | null) {
|
||||||
|
return items.find(p => p.id === id)?.name || '';
|
||||||
|
}
|
||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
|
|
||||||
import { FormulaActionEditor } from './FormulaActionEditor';
|
import { FormulaActionEditor } from './FormulaActionEditor';
|
||||||
|
|
||||||
|
import { PayeeComboBox } from '@desktop-client/components/autocomplete/PayeeComboBox';
|
||||||
import { StatusBadge } from '@desktop-client/components/schedules/StatusBadge';
|
import { StatusBadge } from '@desktop-client/components/schedules/StatusBadge';
|
||||||
import { SimpleTransactionsTable } from '@desktop-client/components/transactions/SimpleTransactionsTable';
|
import { SimpleTransactionsTable } from '@desktop-client/components/transactions/SimpleTransactionsTable';
|
||||||
import { BetweenAmountInput } from '@desktop-client/components/util/AmountInput';
|
import { BetweenAmountInput } from '@desktop-client/components/util/AmountInput';
|
||||||
@@ -319,6 +320,13 @@ function ConditionEditor({
|
|||||||
onChange={v => onChange('value', v)}
|
onChange={v => onChange('value', v)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (field === 'payee') {
|
||||||
|
valueEditor = (
|
||||||
|
<PayeeComboBox
|
||||||
|
selectedKey={value}
|
||||||
|
onSelectionChange={id => onChange('value', id)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
valueEditor = (
|
valueEditor = (
|
||||||
<GenericInput
|
<GenericInput
|
||||||
|
|||||||
@@ -4,3 +4,11 @@ export function getNormalisedString(value: string) {
|
|||||||
.normalize('NFD')
|
.normalize('NFD')
|
||||||
.replace(/\p{Diacritic}/gu, '');
|
.replace(/\p{Diacritic}/gu, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalisedEquals(a: string, b: string) {
|
||||||
|
return getNormalisedString(a) === getNormalisedString(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalisedIncludes(a: string, b: string) {
|
||||||
|
return getNormalisedString(a).includes(getNormalisedString(b));
|
||||||
|
}
|
||||||
|
|||||||
6
upcoming-release-notes/5875.md
Normal file
6
upcoming-release-notes/5875.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
category: Enhancements
|
||||||
|
authors: [joel-jeremy]
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement PayeeAutocomplete2 based on react-aria-component's ComboBox
|
||||||
Reference in New Issue
Block a user