Skip to content
Snippets Groups Projects
PayeeAutocomplete.js 9.97 KiB
Newer Older
  • Learn to ignore specific revisions
  • James Long's avatar
    James Long committed
    import React, { useState, useMemo, useRef } from 'react';
    
    import { useDispatch } from 'react-redux';
    
    James Long's avatar
    James Long committed
    import { getActivePayees } from 'loot-core/src/client/reducers/queries';
    import { createPayee } from 'loot-core/src/client/actions/queries';
    import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees';
    import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts';
    import { View } from './common';
    import Add from '../svg/v1/Add';
    import Autocomplete, {
      defaultFilterSuggestion,
      AutocompleteFooter,
      AutocompleteFooterButton
    } from './Autocomplete';
    import { colors } from '../style';
    
    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
      );
    
    James Long's avatar
    James Long committed
      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}
        />
      );
    }