Allow editing filters (#646)

* Make the filter text clickable

* Fix display of notes field in rules/filters

* Allow editing filters

* debugger
This commit is contained in:
Jed Fox
2023-02-10 14:21:10 -05:00
committed by GitHub
parent f75ff99114
commit 26f1d444f7
3 changed files with 142 additions and 44 deletions

View File

@@ -109,6 +109,8 @@ export function Value({
: null;
} else if (field === 'year') {
return value ? formatDate(parseISO(value), 'yyyy') : null;
} else if (field === 'notes') {
return value;
} else {
if (data && data.length) {
let item = data.find(item => item.id === value);

View File

@@ -636,6 +636,7 @@ const AccountHeader = React.memo(
onBatchEdit,
onBatchUnlink,
onApplyFilter,
onUpdateFilter,
onDeleteFilter,
onScheduleAction
}) => {
@@ -915,7 +916,11 @@ const AccountHeader = React.memo(
</Stack>
{filters && filters.length > 0 && (
<AppliedFilters filters={filters} onDelete={onDeleteFilter} />
<AppliedFilters
filters={filters}
onUpdate={onUpdateFilter}
onDelete={onDeleteFilter}
/>
)}
</View>
{reconcileAmount != null && (
@@ -1608,6 +1613,12 @@ class AccountInternal extends React.PureComponent {
await this.refetchTransactions();
};
onUpdateFilter = (oldFilter, updatedFilter) => {
this.applyFilters(
this.state.filters.map(f => (f === oldFilter ? updatedFilter : f))
);
};
onDeleteFilter = filter => {
this.applyFilters(this.state.filters.filter(f => f !== filter));
};
@@ -1755,6 +1766,7 @@ class AccountInternal extends React.PureComponent {
onBatchDuplicate={this.onBatchDuplicate}
onBatchEdit={this.onBatchEdit}
onBatchUnlink={this.onBatchUnlink}
onUpdateFilter={this.onUpdateFilter}
onDeleteFilter={this.onDeleteFilter}
onApplyFilter={this.onApplyFilter}
onScheduleAction={this.onScheduleAction}

View File

@@ -47,6 +47,23 @@ let filterFields = [
'cleared'
].map(field => [field, mapField(field)]);
function subfieldFromFilter({ field, options, value }) {
if (field === 'date') {
if (value.length === 7) {
return 'month';
} else if (value.length === 4) {
return 'year';
}
} else if (field === 'amount') {
if (options && options.inflow) {
return 'amount-inflow';
} else if (options && options.outflow) {
return 'amount-outflow';
}
}
return field;
}
function subfieldToOptions(field, subfield) {
switch (field) {
case 'amount':
@@ -111,8 +128,38 @@ function OpButton({ op, selected, style, onClick }) {
);
}
function ConfigureField({ field, op, value, dispatch, onApply }) {
let [subfield, setSubfield] = useState(field);
function updateFilterReducer(state, action) {
switch (action.type) {
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 };
}
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
function ConfigureField({
field,
initialSubfield = field,
op,
value,
dispatch,
onApply
}) {
let [subfield, setSubfield] = useState(initialSubfield);
let inputRef = useRef();
let prevOp = useRef(null);
@@ -272,25 +319,10 @@ export function FilterButton({ onApply }) {
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);
return updateFilterReducer(state, action);
}
},
{ fieldsOpen: false, condOpen: false, field: null, value: null }
@@ -380,6 +412,36 @@ export function FilterButton({ onApply }) {
);
}
function FilterEditor({ field, op, value, options, onSave, onClose }) {
let [state, dispatch] = useReducer(
(state, action) => {
switch (action.type) {
case 'close':
onClose();
return state;
default:
return updateFilterReducer(state, action);
}
},
{ field, op, value, options }
);
return (
<ConfigureField
field={state.field}
initialSubfield={subfieldFromFilter({ field, options, value })}
op={state.op}
value={state.value}
options={state.options}
dispatch={dispatch}
onApply={cond => {
onSave(cond);
onClose();
}}
/>
);
}
function FilterExpression({
field: originalField,
customName,
@@ -388,18 +450,12 @@ function FilterExpression({
options,
stage,
style,
onChange,
onDelete
}) {
let type = FIELD_TYPES.get(originalField);
let [editing, setEditing] = useState(false);
let field = originalField;
if (type === 'date') {
if (value.length === 7) {
field = 'month';
} else if (value.length === 4) {
field = 'year';
}
}
let field = subfieldFromFilter({ field: originalField, value });
return (
<View
@@ -409,33 +465,60 @@ function FilterExpression({
borderRadius: 4,
flexDirection: 'row',
alignItems: 'center',
padding: 5,
paddingLeft: 10,
marginBottom: 10,
marginRight: 10
},
style
]}
>
<div>
{customName ? (
<Text style={{ color: colors.p4 }}>{customName}</Text>
) : (
<>
<Text style={{ color: colors.p4 }}>{mapField(field, options)}</Text>{' '}
<Text style={{ color: colors.n3 }}>{friendlyOp(op)}</Text>{' '}
<Value value={value} field={field} inline={true} />
</>
)}
</div>
<Button bare style={{ marginLeft: 3 }} onClick={onDelete}>
<DeleteIcon style={{ width: 8, height: 8, color: colors.n4 }} />
<Button
bare
disabled={customName != null}
onClick={() => setEditing(true)}
style={{ marginRight: -7 }}
>
<div style={{ paddingBlock: 1, paddingLeft: 5, paddingRight: 2 }}>
{customName ? (
<Text style={{ color: colors.p4 }}>{customName}</Text>
) : (
<>
<Text style={{ color: colors.p4 }}>
{mapField(field, options)}
</Text>{' '}
<Text style={{ color: colors.n3 }}>{friendlyOp(op)}</Text>{' '}
<Value value={value} field={field} inline={true} />
</>
)}
</div>
</Button>
<Button bare onClick={onDelete}>
<DeleteIcon
style={{
width: 8,
height: 8,
color: colors.n4,
margin: 5,
marginLeft: 3
}}
/>
</Button>
{editing && (
<FilterEditor
field={originalField}
customName={customName}
op={op}
value={value}
options={options}
stage={stage}
onSave={onChange}
onClose={() => setEditing(false)}
/>
)}
</View>
);
}
export function AppliedFilters({ filters, editingFilter, onDelete }) {
export function AppliedFilters({ filters, editingFilter, onUpdate, onDelete }) {
return (
<View
style={{
@@ -455,6 +538,7 @@ export function AppliedFilters({ filters, editingFilter, onDelete }) {
value={filter.value}
options={filter.options}
editing={editingFilter === filter}
onChange={newFilter => onUpdate(filter, newFilter)}
onDelete={() => onDelete(filter)}
/>
))}