import React, { useState, useRef, useEffect, useReducer } from 'react'; import { useSelector } from 'react-redux'; import { parse as parseDate, format as formatDate, isValid as isDateValid } from 'date-fns'; import scopeTab from 'react-modal/lib/helpers/scopeTab'; import { send } from 'loot-core/src/platform/client/fetch'; import { getMonthYearFormat } from 'loot-core/src/shared/months'; import { mapField, friendlyOp, deserializeField, getFieldError, unparse, makeValue, FIELD_TYPES, TYPE_INFO } from 'loot-core/src/shared/rules'; import { titleFirst } from 'loot-core/src/shared/util'; import { View, Text, Tooltip, Stack, Button, Menu, CustomSelect } from 'loot-design/src/components/common'; import { colors } from 'loot-design/src/style'; import DeleteIcon from 'loot-design/src/svg/Delete'; import SettingsSliderAlternate from 'loot-design/src/svg/v2/SettingsSliderAlternate'; import { Value } from '../modals/ManageRules'; import GenericInput from '../util/GenericInput'; let filterFields = [ 'date', 'account', 'payee', 'notes', 'category', 'amount', 'cleared' ].map(field => [field, mapField(field)]); function subfieldToOptions(field, subfield) { switch (field) { case 'amount': switch (subfield) { case 'amount-inflow': return { inflow: true }; case 'amount-outflow': return { outflow: true }; default: return null; } case 'date': switch (subfield) { case 'month': return { month: true }; case 'year': return { year: true }; default: return null; } default: return null; } } function ScopeTab({ children }) { let contentRef = useRef(); function onKeyDown(e) { if (e.keyCode === 9) { scopeTab(contentRef.current, e); } } useEffect(() => { contentRef.current.focus(); }, []); return (
{children}
); } function OpButton({ op, selected, style, onClick }) { return ( ); } function ConfigureField({ field, op, value, dispatch, onApply }) { let [subfield, setSubfield] = useState(field); let inputRef = useRef(); let prevOp = useRef(null); useEffect(() => { if (prevOp.current !== op && inputRef.current) { inputRef.current.focus(); } prevOp.current = op; }, [op]); let type = FIELD_TYPES.get(field); let ops = TYPE_INFO[type].ops; // Month and year fields are quite hacky right now! Figure out how // to clean this up later if (subfield === 'month' || subfield === 'year') { ops = ['is']; } return ( dispatch({ type: 'close' })} > {field === 'amount' || field === 'date' ? ( { setSubfield(sub); if (sub === 'month' || sub === 'year') { dispatch({ type: 'set-op', op: 'is' }); } }} style={{ borderWidth: 1 }} /> ) : ( titleFirst(mapField(field)) )} {type === 'boolean' ? [ { dispatch({ type: 'set-op', op: 'is' }); dispatch({ type: 'set-value', value: true }); }} />, { dispatch({ type: 'set-op', op: 'is' }); dispatch({ type: 'set-value', value: false }); }} /> ] : ops.map(currOp => ( dispatch({ type: 'set-op', op: currOp })} /> ))}
{type !== 'boolean' && ( dispatch({ type: 'set-value', value: v })} /> )}
); } export function FilterButton({ onApply }) { let { dateFormat } = useSelector(state => { return { dateFormat: state.prefs.local.dateFormat || 'MM/dd/yyyy' }; }); let [state, dispatch] = useReducer( (state, action) => { switch (action.type) { case 'select-field': return { ...state, fieldsOpen: true, condOpen: false }; case 'configure': { let { field } = deserializeField(action.field); let type = FIELD_TYPES.get(field); let ops = TYPE_INFO[type].ops; return { ...state, fieldsOpen: false, condOpen: true, field: action.field, op: ops[0], value: type === 'boolean' ? true : null }; } case 'set-op': { let type = FIELD_TYPES.get(state.field); let value = state.value; if (type === 'id' && action.op === 'contains') { // Clear out the value if switching between contains for // the id type value = null; } return { ...state, op: action.op, value }; } case 'set-value': let { value } = makeValue(action.value, { type: FIELD_TYPES.get(state.field) }); return { ...state, value: value }; case 'close': return { fieldsOpen: false, condOpen: false, value: null }; default: throw new Error('Unknown action: ' + action.type); } }, { fieldsOpen: false, condOpen: false, field: null, value: null } ); async function onValidateAndApply(cond) { cond = unparse({ ...cond, type: FIELD_TYPES.get(cond.field) }); if (cond.type === 'date' && cond.options) { if (cond.options.month) { let date = parseDate( cond.value, getMonthYearFormat(dateFormat), new Date() ); if (isDateValid(date)) { cond.value = formatDate(date, 'yyyy-MM'); } else { alert('Invalid date format'); return; } } else if (cond.options.year) { let date = parseDate(cond.value, 'yyyy', new Date()); if (isDateValid(date)) { cond.value = formatDate(date, 'yyyy'); } else { alert('Invalid date format'); return; } } } let { error } = await send('rule-validate', { conditions: [cond], actions: [] }); if (error && error.conditionErrors.length > 0) { let field = titleFirst(mapField(cond.field)); alert(field + ': ' + getFieldError(error.conditionErrors[0])); } else { onApply(cond); dispatch({ type: 'close' }); } } return ( {state.fieldsOpen && ( dispatch({ type: 'close' })} > { dispatch({ type: 'configure', field: name }); }} items={filterFields.map(([name, text]) => ({ name: name, text: titleFirst(text) }))} /> )} {state.condOpen && ( )} ); } function FilterExpression({ field: originalField, customName, op, value, options, stage, style, onDelete }) { let type = FIELD_TYPES.get(originalField); let field = originalField; if (type === 'date') { if (value.length === 7) { field = 'month'; } else if (value.length === 4) { field = 'year'; } } return (
{customName ? ( {customName} ) : ( <> {mapField(field, options)}{' '} {friendlyOp(op)}{' '} )}
); } export function AppliedFilters({ filters, editingFilter, onDelete }) { return ( {filters.map((filter, i) => ( onDelete(filter)} /> ))} ); }