From e6459f306299aacc57538535636565f96eb6dd18 Mon Sep 17 00:00:00 2001 From: Jed Fox <git@jedfox.com> Date: Fri, 3 Feb 2023 14:36:17 -0500 Subject: [PATCH] Add support for filtering the schedules table (#590) * Add support for searching the schedules table at /schedules * Move filtering logic into SchedulesTable * Extract out a <Search> component * Improve margins in Link Schedule dialog * Add support for filtering in Link Schedule * Add support for filtering by date * rename param for clarity * Remove unused imports * Fix schedules with empty values always showing up in search results * Fix matching behavior --- .../src/components/schedules/LinkSchedule.js | 24 ++++++-- .../components/schedules/SchedulesTable.js | 59 ++++++++++++++++--- .../src/components/schedules/index.js | 15 ++++- packages/loot-design/src/components/common.js | 29 +++++++++ 4 files changed, 111 insertions(+), 16 deletions(-) diff --git a/packages/desktop-client/src/components/schedules/LinkSchedule.js b/packages/desktop-client/src/components/schedules/LinkSchedule.js index ddced09a6..d7289298a 100644 --- a/packages/desktop-client/src/components/schedules/LinkSchedule.js +++ b/packages/desktop-client/src/components/schedules/LinkSchedule.js @@ -1,9 +1,9 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import { useLocation, useHistory } from 'react-router-dom'; import { useSchedules } from 'loot-core/src/client/data-hooks/schedules'; import { send } from 'loot-core/src/platform/client/fetch'; -import { Text } from 'loot-design/src/components/common'; +import { Search, Text, View } from 'loot-design/src/components/common'; import { Page } from '../Page'; @@ -16,6 +16,8 @@ export default function ScheduleLink() { useCallback(query => query.filter({ completed: false }), []) ); + let [filter, setFilter] = useState(''); + if (scheduleData == null) { return null; } @@ -35,15 +37,27 @@ export default function ScheduleLink() { return ( <Page title="Link Schedule" modalSize="medium"> - <Text style={{ marginBottom: 20 }}> - Choose a schedule to link these transactions to: - </Text> + <View + style={{ flexDirection: 'row', marginBottom: 20, alignItems: 'center' }} + > + <Text>Choose a schedule to link these transactions to:</Text> + <View style={{ flex: 1 }} /> + <Search + isInModal + width={300} + placeholder="Filter schedules…" + value={filter} + onChange={setFilter} + /> + </View> <SchedulesTable schedules={schedules} + filter={filter} statuses={statuses} minimal={true} onSelect={onSelect} + tableStyle={{ marginInline: -20 }} /> </Page> ); diff --git a/packages/desktop-client/src/components/schedules/SchedulesTable.js b/packages/desktop-client/src/components/schedules/SchedulesTable.js index 2986ad49b..878c32f71 100644 --- a/packages/desktop-client/src/components/schedules/SchedulesTable.js +++ b/packages/desktop-client/src/components/schedules/SchedulesTable.js @@ -1,6 +1,8 @@ import React, { useState, useMemo } from 'react'; import { useSelector } from 'react-redux'; +import { useCachedAccounts } from 'loot-core/src/client/data-hooks/accounts'; +import { useCachedPayees } from 'loot-core/src/client/data-hooks/payees'; import * as monthUtils from 'loot-core/src/shared/months'; import { getScheduledAmount } from 'loot-core/src/shared/schedules'; import { integerToCurrency } from 'loot-core/src/shared/util'; @@ -126,11 +128,13 @@ export function ScheduleAmountCell({ amount, op }) { export function SchedulesTable({ schedules, statuses, + filter, minimal, allowCompleted, style, onSelect, - onAction + onAction, + tableStyle }) { let dateFormat = useSelector(state => { return state.prefs.local.dateFormat || 'MM/dd/yyyy'; @@ -138,19 +142,56 @@ export function SchedulesTable({ let [showCompleted, setShowCompleted] = useState(false); + let payees = useCachedPayees(); + let accounts = useCachedAccounts(); + + let filteredSchedules = useMemo(() => { + if (!filter) { + return schedules; + } + const filterIncludes = str => + str + ? str.toLowerCase().includes(filter.toLowerCase()) || + filter.toLowerCase().includes(str.toLowerCase()) + : false; + + return schedules.filter(schedule => { + let payee = payees.find(p => schedule._payee === p.id); + let account = accounts.find(a => schedule._account === a.id); + let amount = getScheduledAmount(schedule._amount); + let amountStr = + (schedule._amountOp === 'isapprox' || schedule._amountOp === 'isbetween' + ? '~' + : '') + + (amount > 0 ? '+' : '') + + integerToCurrency(Math.abs(amount || 0)); + let dateStr = schedule.next_date + ? monthUtils.format(schedule.next_date, dateFormat) + : null; + + return ( + filterIncludes(payee && payee.name) || + filterIncludes(account && account.name) || + filterIncludes(amountStr) || + filterIncludes(statuses.get(schedule.id)) || + filterIncludes(dateStr) + ); + }); + }, [schedules, filter, statuses]); + let items = useMemo(() => { if (!allowCompleted) { - return schedules.filter(s => !s.completed); + return filteredSchedules.filter(s => !s.completed); } if (showCompleted) { - return schedules; + return filteredSchedules; } - let arr = schedules.filter(s => !s.completed); - if (schedules.find(s => s.completed)) { + let arr = filteredSchedules.filter(s => !s.completed); + if (filteredSchedules.find(s => s.completed)) { arr.push({ type: 'show-completed' }); } return arr; - }, [schedules, showCompleted, allowCompleted]); + }, [filteredSchedules, showCompleted, allowCompleted]); function renderSchedule({ item }) { return ( @@ -231,7 +272,7 @@ export function SchedulesTable({ } return ( - <> + <View style={[{ flex: 1 }, tableStyle]}> <TableHeader height={ROW_HEIGHT} inset={15} version="v2"> <Field width="flex">Payee</Field> <Field width="flex">Account</Field> @@ -254,9 +295,9 @@ export function SchedulesTable({ style={[{ flex: 1, backgroundColor: 'transparent' }, style]} items={items} renderItem={renderItem} - renderEmpty="No schedules" + renderEmpty={filter ? 'No matching schedules' : 'No schedules'} allowPopupsEscape={items.length < 6} /> - </> + </View> ); } diff --git a/packages/desktop-client/src/components/schedules/index.js b/packages/desktop-client/src/components/schedules/index.js index 08822c76e..a945113c5 100644 --- a/packages/desktop-client/src/components/schedules/index.js +++ b/packages/desktop-client/src/components/schedules/index.js @@ -1,9 +1,9 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useHistory } from 'react-router-dom'; import { useSchedules } from 'loot-core/src/client/data-hooks/schedules'; import { send } from 'loot-core/src/platform/client/fetch'; -import { View, Button } from 'loot-design/src/components/common'; +import { View, Button, Search } from 'loot-design/src/components/common'; import { Page } from '../Page'; @@ -12,6 +12,8 @@ import { SchedulesTable, ROW_HEIGHT } from './SchedulesTable'; export default function Schedules() { let history = useHistory(); + let [filter, setFilter] = useState(''); + let scheduleData = useSchedules(); if (scheduleData == null) { @@ -58,6 +60,14 @@ export default function Schedules() { return ( <Page title="Schedules"> + <View style={{ alignItems: 'flex-end' }}> + <Search + placeholder="Filter schedules…" + value={filter} + onChange={setFilter} + /> + </View> + <View style={{ marginTop: 20, @@ -67,6 +77,7 @@ export default function Schedules() { > <SchedulesTable schedules={schedules} + filter={filter} statuses={statuses} allowCompleted={true} onSelect={onEdit} diff --git a/packages/loot-design/src/components/common.js b/packages/loot-design/src/components/common.js index c22fbc811..1cf92084e 100644 --- a/packages/loot-design/src/components/common.js +++ b/packages/loot-design/src/components/common.js @@ -422,6 +422,35 @@ export function InputWithContent({ ); } +export function Search({ + inputRef, + value, + onChange, + placeholder, + isInModal, + width = 350 +}) { + return ( + <Input + inputRef={inputRef} + placeholder={placeholder} + value={value} + onChange={e => onChange(e.target.value)} + style={{ + width, + borderColor: isInModal ? null : 'transparent', + backgroundColor: isInModal ? null : colors.n11, + ':focus': isInModal + ? null + : { + backgroundColor: 'white', + '::placeholder': { color: colors.n8 } + } + }} + /> + ); +} + export function KeyboardButton({ highlighted, children, ...props }) { return ( <Button -- GitLab