actual/packages/loot-design/src/components/PayeeAutocomplete.js
Tom French 9c0df36e16
Sort import in alphabetical order (#238)
* style: enforce sorting of imports

* style: alphabetize imports

* style: merge duplicated imports
2022-09-02 15:07:24 +01:00

361 lines
10 KiB
JavaScript

import React, { useState, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { createPayee } from 'loot-core/src/client/actions/queries';
import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
import { getActivePayees } from 'loot-core/src/client/reducers/queries';
import { colors } from '../style';
import Add from '../svg/v1/Add';
import Autocomplete, {
defaultFilterSuggestion,
AutocompleteFooter,
AutocompleteFooterButton
} from './Autocomplete';
import { View } from './common';
function getPayeeSuggestions(payees, focusTransferPayees, accounts) {
let activePayees = accounts ? getActivePayees(payees, accounts) : payees;
if (focusTransferPayees && activePayees) {
activePayees = activePayees.filter(p => !!p.transfer_acct);
}
return activePayees || [];
}
function makeNew(value, rawPayee) {
if (value === 'new' && !rawPayee.current.startsWith('new:')) {
return 'new:' + rawPayee.current;
}
return value;
}
// Convert the fully resolved new value into the 'new' id that can be
// looked up in the suggestions
function stripNew(value) {
if (typeof value === 'string' && value.startsWith('new:')) {
return 'new';
}
return value;
}
export function PayeeList({
items,
getItemProps,
highlightedIndex,
embedded,
inputValue,
footer
}) {
let isFiltered = items.filtered;
let createNew = null;
items = [...items];
// If the "new payee" item exists, create it as a special-cased item
// with the value of the input so it always shows whatever the user
// entered
if (items[0].id === 'new') {
let [first, ...rest] = items;
createNew = first;
items = rest;
}
let offset = createNew ? 1 : 0;
let lastType = null;
return (
<View>
<View
style={[
{ overflow: 'auto', padding: '5px 0' },
!embedded && { maxHeight: 175 }
]}
>
{createNew && (
<View
{...(getItemProps ? getItemProps({ item: createNew }) : null)}
style={{
flexShrink: 0,
padding: '6px 9px',
backgroundColor:
highlightedIndex === 0 ? colors.n4 : 'transparent',
borderRadius: embedded ? 4 : 0
}}
>
<View
style={{
display: 'block',
color: colors.g8,
borderRadius: 4,
fontSize: 11,
fontWeight: 500
}}
>
<Add
width={8}
height={8}
style={{
color: colors.g8,
marginRight: 5,
display: 'inline-block'
}}
/>
Create Payee "{inputValue}"
</View>
</View>
)}
{items.map((item, idx) => {
let type = item.transfer_acct ? 'account' : 'payee';
let title;
if (type === 'payee' && lastType !== type) {
title = 'Payees';
} else if (type === 'account' && lastType !== type) {
title = 'Transfer To/From';
}
let showMoreMessage = idx === items.length - 1 && isFiltered;
lastType = type;
return (
<React.Fragment key={item.id}>
{title && (
<div
key={'title-' + idx}
style={{
color: colors.y9,
padding: '4px 9px'
}}
>
{title}
</div>
)}
<div
{...(getItemProps ? getItemProps({ item }) : null)}
key={item.id}
style={{
backgroundColor:
highlightedIndex === idx + offset
? colors.n4
: 'transparent',
borderRadius: embedded ? 4 : 0,
padding: 4,
paddingLeft: 20
}}
>
{item.name}
</div>
{showMoreMessage && (
<div
style={{
fontSize: 11,
padding: 5,
color: colors.n5,
textAlign: 'center'
}}
>
More payees are available, search to find them
</div>
)}
</React.Fragment>
);
})}
</View>
{footer}
</View>
);
}
export default function PayeeAutocomplete({
value,
inputProps,
showMakeTransfer = true,
showManagePayees = false,
defaultFocusTransferPayees = false,
tableBehavior,
embedded,
onUpdate,
onSelect,
onManagePayees,
...props
}) {
let payees = useCachedPayees();
let accounts = useCachedAccounts();
let [focusTransferPayees, setFocusTransferPayees] = useState(
defaultFocusTransferPayees
);
let payeeSuggestions = useMemo(
() => [
{ id: 'new', name: '' },
...getPayeeSuggestions(payees, focusTransferPayees, accounts)
],
[payees, focusTransferPayees, accounts]
);
let rawPayee = useRef('');
let dispatch = useDispatch();
async function handleSelect(value) {
if (tableBehavior) {
onSelect && onSelect(makeNew(value, rawPayee));
} else {
let create = () => dispatch(createPayee(rawPayee.current));
if (Array.isArray(value)) {
value = await Promise.all(value.map(v => (v === 'new' ? create() : v)));
} else {
if (value === 'new') {
value = await create();
}
}
onSelect && onSelect(value);
}
}
return (
<Autocomplete
key={focusTransferPayees ? 'transfers' : 'all'}
strict={true}
embedded={embedded}
value={stripNew(value)}
suggestions={payeeSuggestions}
tableBehavior={tableBehavior}
itemToString={item => {
if (!item) {
return '';
} else if (item.id === 'new') {
return rawPayee.current;
}
return item.name;
}}
inputProps={{
...inputProps,
onChange: text => (rawPayee.current = text)
}}
onUpdate={value => onUpdate && onUpdate(makeNew(value, rawPayee))}
onSelect={handleSelect}
getHighlightedIndex={suggestions => {
if (suggestions.length > 1 && suggestions[0].id === 'new') {
return 1;
}
return 0;
}}
filterSuggestions={(suggestions, value) => {
let filtered = suggestions.filter((suggestion, idx) => {
if (suggestion.id === 'new') {
return !value || value === '' || focusTransferPayees ? false : true;
}
return defaultFilterSuggestion(suggestion, value);
});
filtered.sort((p1, p2) => {
let r1 = p1.name.toLowerCase().startsWith(value.toLowerCase());
let r2 = p2.name.toLowerCase().startsWith(value.toLowerCase());
let r1exact = p1.name.toLowerCase() === value.toLowerCase();
let r2exact = p2.name.toLowerCase() === value.toLowerCase();
// (maniacal laughter) mwahaHAHAHAHAH
if (p1.id === 'new') {
return -1;
} else if (p2.id === 'new') {
return 1;
} else {
if (r1exact && !r2exact) {
return -1;
} else if (!r1exact && r2exact) {
return 1;
} else {
if (r1 === r2) {
return 0;
} else if (r1 && !r2) {
return -1;
} else {
return 1;
}
}
}
});
let isf = filtered.length > 100;
filtered = filtered.slice(0, 100);
filtered.filtered = isf;
if (filtered.length >= 2 && filtered[0].id === 'new') {
if (filtered[1].name.toLowerCase() === value.toLowerCase()) {
return filtered.slice(1);
}
}
return filtered;
}}
initialFilterSuggestions={suggestions => {
let filtered = false;
let res = suggestions.filter((suggestion, idx) => {
if (suggestion.id === 'new') {
// Never show the "create new" initially
return false;
}
if (idx >= 100 && !suggestion.transfer_acct) {
filtered = true;
return false;
}
return true;
});
if (filtered) {
res.filtered = true;
}
return res;
}}
renderItems={(items, getItemProps, highlightedIndex, inputValue) => (
<PayeeList
items={items}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
inputValue={inputValue}
embedded={embedded}
footer={
<AutocompleteFooter embedded={embedded}>
{showMakeTransfer && (
<AutocompleteFooterButton
title="Make Transfer"
style={[
showManagePayees && { marginBottom: 5 },
focusTransferPayees && {
backgroundColor: colors.y8,
color: colors.g2,
borderColor: colors.y8
}
]}
hoveredStyle={
focusTransferPayees && {
backgroundColor: colors.y8,
colors: colors.y2
}
}
onClick={() => {
onUpdate && onUpdate(null);
setFocusTransferPayees(!focusTransferPayees);
}}
/>
)}
{showManagePayees && (
<AutocompleteFooterButton
title="Manage Payees"
onClick={() => onManagePayees()}
/>
)}
</AutocompleteFooter>
}
/>
)}
{...props}
/>
);
}