mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-29 19:14:22 -05:00
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:
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user