Fix filtering

This commit is contained in:
Joel Jeremy Marquez
2024-04-26 19:27:07 -07:00
parent fe2166207b
commit 2ece4d35a7
14 changed files with 133 additions and 132 deletions

View File

@@ -1,12 +1,10 @@
import React, { PureComponent, createRef, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { Navigate, useParams, useLocation, useMatch } from 'react-router-dom';
import { debounce } from 'debounce';
import { bindActionCreators } from 'redux';
import { validForTransfer } from 'loot-core/client/transfer';
import * as actions from 'loot-core/src/client/actions';
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
import { SchedulesProvider } from 'loot-core/src/client/data-hooks/schedules';
import * as queries from 'loot-core/src/client/queries';
@@ -26,6 +24,7 @@ import {
import { applyChanges, groupById } from 'loot-core/src/shared/util';
import { useAccounts } from '../../hooks/useAccounts';
import { useActions } from '../../hooks/useActions';
import { useCategories } from '../../hooks/useCategories';
import { useDateFormat } from '../../hooks/useDateFormat';
import { useFailedAccounts } from '../../hooks/useFailedAccounts';
@@ -179,7 +178,7 @@ class AccountInternal extends PureComponent {
this.state = {
search: '',
filters: props.conditions || [],
filterConditions: props.filterConditions || [],
loading: true,
workingHard: false,
reconcileAmount: null,
@@ -256,7 +255,7 @@ class AccountInternal extends PureComponent {
// Important that any async work happens last so that the
// listeners are set up synchronously
await this.props.initiallyLoadPayees();
await this.fetchTransactions(this.state.filters);
await this.fetchTransactions(this.state.filterConditions);
// If there is a pending undo, apply it immediately (this happens
// when an undo changes the location to this page)
@@ -285,7 +284,7 @@ class AccountInternal extends PureComponent {
//Resest sort/filter/search on account change
if (this.props.accountId !== prevProps.accountId) {
this.setState({ sort: [], search: '', filters: [] });
this.setState({ sort: [], search: '', filterConditions: [] });
}
}
@@ -313,10 +312,10 @@ class AccountInternal extends PureComponent {
this.paged?.run();
};
fetchTransactions = filters => {
fetchTransactions = filterConditions => {
const query = this.makeRootQuery();
this.rootQuery = this.currentQuery = query;
if (filters) this.applyFilters(filters);
if (filterConditions) this.applyFilters(filterConditions);
else this.updateQuery(query);
if (this.props.accountId) {
@@ -418,7 +417,10 @@ class AccountInternal extends PureComponent {
onSearchDone = debounce(() => {
if (this.state.search === '') {
this.updateQuery(this.currentQuery, this.state.filters.length > 0);
this.updateQuery(
this.currentQuery,
this.state.filterConditions.length > 0,
);
} else {
this.updateQuery(
queries.makeTransactionSearchQuery(
@@ -511,7 +513,7 @@ class AccountInternal extends PureComponent {
return (
account &&
this.state.search === '' &&
this.state.filters.length === 0 &&
this.state.filterConditions.length === 0 &&
(this.state.sort.length === 0 ||
(this.state.sort.field === 'date' &&
this.state.sort.ascDesc === 'desc'))
@@ -599,7 +601,7 @@ class AccountInternal extends PureComponent {
{
transactions: [],
transactionCount: 0,
filters: [],
filterConditions: [],
search: '',
sort: [],
showBalances: true,
@@ -612,9 +614,9 @@ class AccountInternal extends PureComponent {
break;
case 'remove-sorting': {
this.setState({ sort: [] }, () => {
const filters = this.state.filters;
if (filters.length > 0) {
this.applyFilters([...filters]);
const filterConditions = this.state.filterConditions;
if (filterConditions.length > 0) {
this.applyFilters([...filterConditions]);
} else {
this.fetchTransactions();
}
@@ -637,12 +639,12 @@ class AccountInternal extends PureComponent {
if (this.state.showReconciled) {
this.props.savePrefs({ ['hide-reconciled-' + accountId]: true });
this.setState({ showReconciled: false }, () =>
this.fetchTransactions(this.state.filters),
this.fetchTransactions(this.state.filterConditions),
);
} else {
this.props.savePrefs({ ['hide-reconciled-' + accountId]: false });
this.setState({ showReconciled: true }, () =>
this.fetchTransactions(this.state.filters),
this.fetchTransactions(this.state.filterConditions),
);
}
break;
@@ -797,7 +799,7 @@ class AccountInternal extends PureComponent {
onShowTransactions = async ids => {
this.onApplyFilter({
customName: 'Selected transactions',
filter: { id: { $oneof: ids } },
queryFilter: { id: { $oneof: ids } },
});
};
@@ -1194,11 +1196,11 @@ class AccountInternal extends PureComponent {
onReloadSavedFilter = (savedFilter, item) => {
if (item === 'reload') {
const [getFilter] = this.props.filtersList.filter(
const [savedFilter] = this.props.savedFilters.filter(
f => f.id === this.state.filterId.id,
);
this.setState({ conditionsOp: getFilter.conditionsOp });
this.applyFilters([...getFilter.conditions]);
this.setState({ conditionsOp: savedFilter.conditionsOp });
this.applyFilters([...savedFilter.conditions]);
} else {
if (savedFilter.status) {
this.setState({ conditionsOp: savedFilter.conditionsOp });
@@ -1217,9 +1219,11 @@ class AccountInternal extends PureComponent {
}
};
onUpdateFilter = (oldFilter, updatedFilter) => {
onUpdateFilter = (oldCondition, updatedCondition) => {
this.applyFilters(
this.state.filters.map(f => (f === oldFilter ? updatedFilter : f)),
this.state.filterConditions.map(c =>
c === oldCondition ? updatedCondition : c,
),
);
this.setState({
filterId: {
@@ -1232,9 +1236,9 @@ class AccountInternal extends PureComponent {
}
};
onDeleteFilter = filter => {
this.applyFilters(this.state.filters.filter(f => f !== filter));
if (this.state.filters.length === 1) {
onDeleteFilter = condition => {
this.applyFilters(this.state.filterConditions.filter(c => c !== condition));
if (this.state.filterConditions.length === 1) {
this.setState({ filterId: [] });
this.setState({ conditionsOp: 'and' });
} else {
@@ -1250,23 +1254,31 @@ class AccountInternal extends PureComponent {
}
};
onApplyFilter = async cond => {
let filters = this.state.filters;
if (cond.customName) {
filters = filters.filter(f => f.customName !== cond.customName);
onApplyFilter = async conditionOrSavedFilter => {
let filterConditions = this.state.filterConditions;
if (conditionOrSavedFilter.customName) {
filterConditions = filterConditions.filter(
c => c.customName !== conditionOrSavedFilter.customName,
);
}
if (cond.conditions) {
this.setState({ filterId: { ...cond, status: 'saved' } });
this.setState({ conditionsOp: cond.conditionsOp });
this.applyFilters([...cond.conditions]);
if (conditionOrSavedFilter.conditions) {
// A saved filter was passed in.
const savedFilter = conditionOrSavedFilter;
this.setState({
filterId: { ...savedFilter, status: 'saved' },
});
this.setState({ conditionsOp: savedFilter.conditionsOp });
this.applyFilters([...savedFilter.conditions]);
} else {
// A condition was passed in.
const condition = conditionOrSavedFilter;
this.setState({
filterId: {
...this.state.filterId,
status: this.state.filterId && 'changed',
},
});
this.applyFilters([...filters, cond]);
this.applyFilters([...filterConditions, condition]);
}
if (this.state.search !== '') {
this.onSearch(this.state.search);
@@ -1294,18 +1306,21 @@ class AccountInternal extends PureComponent {
applyFilters = async conditions => {
if (conditions.length > 0) {
const customFilters = conditions
const customQueryFilters = conditions
.filter(cond => !!cond.customName)
.map(f => f.filter);
const { filters } = await send('make-filters-from-conditions', {
conditions: conditions.filter(cond => !cond.customName),
});
.map(f => f.queryFilter);
const { filters: queryFilters } = await send(
'make-filters-from-conditions',
{
conditions: conditions.filter(cond => !cond.customName),
},
);
const conditionsOpKey = this.state.conditionsOp === 'or' ? '$or' : '$and';
this.currentQuery = this.rootQuery.filter({
[conditionsOpKey]: [...filters, ...customFilters],
[conditionsOpKey]: [...queryFilters, ...customQueryFilters],
});
this.setState({ filters: conditions }, () => {
this.setState({ filterConditions: conditions }, () => {
this.updateQuery(this.currentQuery, true);
});
} else {
@@ -1313,7 +1328,7 @@ class AccountInternal extends PureComponent {
{
transactions: [],
transactionCount: 0,
filters: conditions,
filterConditions: conditions,
},
() => {
this.fetchTransactions();
@@ -1327,8 +1342,8 @@ class AccountInternal extends PureComponent {
};
applySort = (field, ascDesc, prevField, prevAscDesc) => {
const filters = this.state.filters;
const isFiltered = filters.length > 0;
const filterConditions = this.state.filterConditions;
const isFiltered = filterConditions.length > 0;
const sortField = getField(!field ? this.state.sort.field : field);
const sortAscDesc = !ascDesc ? this.state.sort.ascDesc : ascDesc;
const sortPrevField = getField(
@@ -1395,7 +1410,7 @@ class AccountInternal extends PureComponent {
// called directly from UI by sorting a column.
// active filters need to be applied before sorting
case isFiltered:
this.applyFilters([...filters]);
this.applyFilters([...filterConditions]);
sortCurrentQuery(this, sortField, sortAscDesc);
break;
@@ -1523,7 +1538,7 @@ class AccountInternal extends PureComponent {
workingHard={workingHard}
account={account}
filterId={filterId}
filtersList={this.props.filtersList}
savedFilters={this.props.savedFilters}
location={this.props.location}
accountName={accountName}
accountsSyncing={accountsSyncing}
@@ -1540,7 +1555,7 @@ class AccountInternal extends PureComponent {
isSorted={this.state.sort.length !== 0}
reconcileAmount={reconcileAmount}
search={this.state.search}
filters={this.state.filters}
filterConditions={this.state.filterConditions}
conditionsOp={this.state.conditionsOp}
savePrefs={this.props.savePrefs}
pushModal={this.props.pushModal}
@@ -1600,7 +1615,8 @@ class AccountInternal extends PureComponent {
isNew={this.isNew}
isMatched={this.isMatched}
isFiltered={
this.state.search !== '' || this.state.filters.length > 0
this.state.search !== '' ||
this.state.filterConditions.length > 0
}
dateFormat={dateFormat}
hideFraction={hideFraction}
@@ -1636,6 +1652,7 @@ class AccountInternal extends PureComponent {
this.setState({ isAdding: false })
}
onCreatePayee={this.onCreatePayee}
onApplyFilters={conditions => this.applyFilters(conditions)}
/>
</View>
</View>
@@ -1683,36 +1700,10 @@ export function Account() {
const modalShowing = useSelector(state => state.modals.modalStack.length > 0);
const accountsSyncing = useSelector(state => state.account.accountsSyncing);
const lastUndoState = useSelector(state => state.app.lastUndoState);
const conditions =
location.state && location.state.conditions
? location.state.conditions
: [];
const filterConditions = location?.state?.filterConditions || [];
const state = {
newTransactions,
matchedTransactions,
accounts,
failedAccounts,
dateFormat,
hideFraction,
expandSplits,
showBalances,
showCleared: !hideCleared,
showReconciled: !hideReconciled,
showExtraBalances,
payees,
modalShowing,
accountsSyncing,
lastUndoState,
conditions,
};
const dispatch = useDispatch();
const filtersList = useFilters();
const actionCreators = useMemo(
() => bindActionCreators(actions, dispatch),
[dispatch],
);
const savedFiters = useFilters();
const actionCreators = useActions();
const transform = useMemo(() => {
const filterByAccount = queries.getAccountFilter(params.id, '_account');
@@ -1741,17 +1732,31 @@ export function Account() {
return (
<SchedulesProvider transform={transform}>
<SplitsExpandedProvider
initialMode={state.expandSplits ? 'collapse' : 'expand'}
initialMode={expandSplits ? 'collapse' : 'expand'}
>
<AccountHack
{...state}
newTransactions={newTransactions}
matchedTransactions={matchedTransactions}
accounts={accounts}
failedAccounts={failedAccounts}
dateFormat={dateFormat}
hideFraction={hideFraction}
expandSplits={expandSplits}
showBalances={showBalances}
showCleared={!hideCleared}
showReconciled={!hideReconciled}
showExtraBalances={showExtraBalances}
payees={payees}
modalShowing={modalShowing}
accountsSyncing={accountsSyncing}
lastUndoState={lastUndoState}
filterConditions={filterConditions}
categoryGroups={categoryGroups}
{...actionCreators}
modalShowing={state.modalShowing}
accountId={params.id}
categoryId={location?.state?.categoryId}
location={location}
filtersList={filtersList}
savedFilters={savedFiters}
/>
</SplitsExpandedProvider>
</SchedulesProvider>

View File

@@ -141,7 +141,7 @@ export function Balances({
showExtraBalances,
onToggleExtraBalances,
account,
filteredItems,
showFilteredBalances,
transactions,
}) {
const selectedItems = useSelectedItems();
@@ -200,9 +200,7 @@ export function Balances({
{selectedItems.size > 0 && (
<SelectedBalance selectedItems={selectedItems} account={account} />
)}
{filteredItems.length > 0 && (
<FilteredBalance selectedItems={transactions} />
)}
{showFilteredBalances && <FilteredBalance selectedItems={transactions} />}
</View>
);
}

View File

@@ -39,7 +39,7 @@ export function AccountHeader({
accountName,
account,
filterId,
filtersList,
savedFilters,
accountsSyncing,
failedAccounts,
accounts,
@@ -54,7 +54,7 @@ export function AccountHeader({
canCalculateBalance,
isSorted,
search,
filters,
filterConditions,
conditionsOp,
pushModal,
onSearch,
@@ -241,7 +241,7 @@ export function AccountHeader({
showExtraBalances={showExtraBalances}
onToggleExtraBalances={onToggleExtraBalances}
account={account}
filteredItems={filters}
showFilteredBalances={filterConditions.length > 0}
transactions={transactions}
/>
@@ -320,7 +320,7 @@ export function AccountHeader({
)}
<Button
type="bare"
disabled={search !== '' || filters.length > 0}
disabled={search !== '' || filterConditions.length > 0}
style={{ padding: 6, marginLeft: 10 }}
onClick={onToggleSplits}
title={
@@ -375,16 +375,16 @@ export function AccountHeader({
)}
</Stack>
{filters && filters.length > 0 && (
{filterConditions?.length > 0 && (
<FiltersStack
filters={filters}
conditions={filterConditions}
conditionsOp={conditionsOp}
onUpdateFilter={onUpdateFilter}
onDeleteFilter={onDeleteFilter}
onClearFilters={onClearFilters}
onReloadSavedFilter={onReloadSavedFilter}
filterId={filterId}
filtersList={filtersList}
savedFilters={savedFilters}
onCondOpChange={onCondOpChange}
/>
)}

View File

@@ -277,7 +277,7 @@ function BudgetInner(props: BudgetInnerProps) {
};
const onShowActivity = (categoryId, month) => {
const conditions = [
const filterConditions = [
{ field: 'category', op: 'is', value: categoryId, type: 'id' },
{
field: 'date',
@@ -290,7 +290,7 @@ function BudgetInner(props: BudgetInnerProps) {
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId,
},
});

View File

@@ -8,7 +8,7 @@ import { CondOpMenu } from './CondOpMenu';
import { FilterExpression } from './FilterExpression';
type AppliedFiltersProps = {
filters: RuleConditionEntity[];
conditions: RuleConditionEntity[];
onUpdate: (
filter: RuleConditionEntity,
newFilter: RuleConditionEntity,
@@ -19,7 +19,7 @@ type AppliedFiltersProps = {
};
export function AppliedFilters({
filters,
conditions,
onUpdate,
onDelete,
conditionsOp,
@@ -36,9 +36,9 @@ export function AppliedFilters({
<CondOpMenu
conditionsOp={conditionsOp}
onCondOpChange={onCondOpChange}
filters={filters}
filters={conditions}
/>
{filters.map((filter: RuleConditionEntity, i: number) => (
{conditions.map((filter: RuleConditionEntity, i: number) => (
<FilterExpression
key={i}
customName={filter.customName}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { type TransactionFilterEntity } from 'loot-core/types/models';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { Stack } from '../common/Stack';
@@ -12,17 +13,17 @@ import {
} from './SavedFilterMenuButton';
export function FiltersStack({
filters,
conditions,
conditionsOp,
onUpdateFilter,
onDeleteFilter,
onClearFilters,
onReloadSavedFilter,
filterId,
filtersList,
savedFilters,
onCondOpChange,
}: {
filters: RuleConditionEntity[];
conditions: RuleConditionEntity[];
conditionsOp: string;
onUpdateFilter: (
filter: RuleConditionEntity,
@@ -32,7 +33,7 @@ export function FiltersStack({
onClearFilters: () => void;
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
filterId: SavedFilter;
filtersList: RuleConditionEntity[];
savedFilters: TransactionFilterEntity[];
onCondOpChange: () => void;
}) {
return (
@@ -44,7 +45,7 @@ export function FiltersStack({
align="flex-start"
>
<AppliedFilters
filters={filters}
conditions={conditions}
conditionsOp={conditionsOp}
onCondOpChange={onCondOpChange}
onUpdate={onUpdateFilter}
@@ -52,12 +53,12 @@ export function FiltersStack({
/>
<View style={{ flex: 1 }} />
<SavedFilterMenuButton
filters={filters}
conditions={conditions}
conditionsOp={conditionsOp}
filterId={filterId}
onClearFilters={onClearFilters}
onReloadSavedFilter={onReloadSavedFilter}
filtersList={filtersList}
savedFilters={savedFilters}
/>
</Stack>
</View>

View File

@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { send, sendCatch } from 'loot-core/src/platform/client/fetch';
import { type TransactionFilterEntity } from 'loot-core/types/models';
import { type RuleConditionEntity } from 'loot-core/types/models/rule';
import { SvgExpandArrow } from '../../icons/v0';
@@ -20,19 +21,19 @@ export type SavedFilter = {
};
export function SavedFilterMenuButton({
filters,
conditions,
conditionsOp,
filterId,
onClearFilters,
onReloadSavedFilter,
filtersList,
savedFilters,
}: {
filters: RuleConditionEntity[];
conditions: RuleConditionEntity[];
conditionsOp: string;
filterId: SavedFilter;
onClearFilters: () => void;
onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void;
filtersList: RuleConditionEntity[];
savedFilters: TransactionFilterEntity[];
}) {
const [nameOpen, setNameOpen] = useState(false);
const [adding, setAdding] = useState(false);
@@ -62,7 +63,7 @@ export function SavedFilterMenuButton({
setAdding(false);
setMenuOpen(false);
savedFilter = {
conditions: filters,
conditions,
conditionsOp,
id: filterId.id,
name: filterId.name,
@@ -70,7 +71,7 @@ export function SavedFilterMenuButton({
};
const response = await sendCatch('filter-update', {
state: savedFilter,
filters: [...filtersList],
filters: [...savedFilters],
});
if (response.error) {
@@ -106,7 +107,7 @@ export function SavedFilterMenuButton({
async function onAddUpdate() {
if (adding) {
const newSavedFilter = {
conditions: filters,
conditions,
conditionsOp,
name,
status: 'saved',
@@ -114,7 +115,7 @@ export function SavedFilterMenuButton({
const response = await sendCatch('filter-create', {
state: newSavedFilter,
filters: [...filtersList],
filters: [...savedFilters],
});
if (response.error) {
@@ -140,7 +141,7 @@ export function SavedFilterMenuButton({
const response = await sendCatch('filter-update', {
state: updatedFilter,
filters: [...filtersList],
filters: [...savedFilters],
});
if (response.error) {
@@ -155,7 +156,7 @@ export function SavedFilterMenuButton({
return (
<View>
{filters.length > 0 && (
{conditions.length > 0 && (
<Button
type="bare"
style={{ marginTop: 10 }}

View File

@@ -164,7 +164,7 @@ export function Header({
align="flex-start"
>
<AppliedFilters
filters={filters}
conditions={filters}
onUpdate={onUpdateFilter}
onDelete={onDeleteFilter}
conditionsOp={conditionsOp}

View File

@@ -191,7 +191,7 @@ export function BarGraph({
.map(e => e.id);
const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
const conditions = [
const filterConditions = [
...filters,
{ field, op: 'is', value: item.id, type: 'id' },
{
@@ -232,7 +232,7 @@ export function BarGraph({
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId: item.id,
},
});

View File

@@ -215,7 +215,7 @@ export function DonutGraph({
.map(e => e.id);
const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
const conditions = [
const filterConditions = [
...filters,
{ field, op: 'is', value: item.id, type: 'id' },
{
@@ -256,7 +256,7 @@ export function DonutGraph({
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId: item.id,
},
});

View File

@@ -150,7 +150,7 @@ export function LineGraph({
.map(e => e.id);
const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
const conditions = [
const filterConditions = [
...filters,
{ field, op: 'is', value: id, type: 'id' },
{
@@ -183,7 +183,7 @@ export function LineGraph({
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId: item.id,
},
});

View File

@@ -181,7 +181,7 @@ export function StackedBarGraph({
.map(e => e.id);
const offBudgetAccounts = accounts.filter(f => f.offbudget).map(e => e.id);
const conditions = [
const filterConditions = [
...filters,
{ field, op: 'is', value: id, type: 'id' },
{
@@ -214,7 +214,7 @@ export function StackedBarGraph({
navigate('/accounts', {
state: {
goBack: true,
conditions,
filterConditions,
categoryId: item.id,
},
});

View File

@@ -539,7 +539,7 @@ export function CustomReport() {
align="flex-start"
>
<AppliedFilters
filters={filters}
conditions={filters}
onUpdate={(oldFilter, newFilter) => {
setSessionReport(
'conditions',

View File

@@ -86,6 +86,7 @@ export function TransactionList({
onRefetch,
onCloseAddTransaction,
onCreatePayee,
onApplyFilters,
}) {
const transactionsLatest = useRef();
const navigate = useNavigate();
@@ -167,15 +168,10 @@ export function TransactionList({
});
const onNavigateToFilteredTagView = useCallback(noteTag => {
const conditions = [
const filterConditions = [
{ field: 'notes', op: 'contains', value: noteTag, type: 'string' },
];
navigate('/accounts', {
state: {
goBack: true,
conditions,
},
});
onApplyFilters(filterConditions);
});
return (