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 })}
/>
))}
);
}
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' })}
>
)}
{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)}
/>
))}
);
}