From 26f1d444f756d225653519bc7432060265e15574 Mon Sep 17 00:00:00 2001 From: Jed Fox <git@jedfox.com> Date: Fri, 10 Feb 2023 14:21:10 -0500 Subject: [PATCH] Allow editing filters (#646) * Make the filter text clickable * Fix display of notes field in rules/filters * Allow editing filters * debugger --- .../src/components/ManageRules.js | 2 + .../src/components/accounts/Account.js | 14 +- .../src/components/accounts/Filters.js | 170 +++++++++++++----- 3 files changed, 142 insertions(+), 44 deletions(-) diff --git a/packages/desktop-client/src/components/ManageRules.js b/packages/desktop-client/src/components/ManageRules.js index 62ea2a204..836b7da87 100644 --- a/packages/desktop-client/src/components/ManageRules.js +++ b/packages/desktop-client/src/components/ManageRules.js @@ -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); diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js index f8fb0762d..e09d40917 100644 --- a/packages/desktop-client/src/components/accounts/Account.js +++ b/packages/desktop-client/src/components/accounts/Account.js @@ -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} diff --git a/packages/desktop-client/src/components/accounts/Filters.js b/packages/desktop-client/src/components/accounts/Filters.js index bc5f91832..81cd69d4b 100644 --- a/packages/desktop-client/src/components/accounts/Filters.js +++ b/packages/desktop-client/src/components/accounts/Filters.js @@ -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)} /> ))} -- GitLab