diff --git a/packages/desktop-client/src/components/schedules/LinkSchedule.js b/packages/desktop-client/src/components/schedules/LinkSchedule.js index ddced09a641a703d47b7a18bfc34c98795f91c66..d7289298ade2983a7c3a7d2695972f57d3dd5c0d 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 2986ad49b182a18638fb7a865539702acc95dac0..878c32f7132c603e806687ad0e4b2c5fe75a0e2a 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 08822c76eb9e58a1f63b0e1e63bc1304e9266cee..a945113c5083d2c7795fab0e23fabda0b194a053 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 c22fbc8111d83922d0e3d6e5ca6bd50ca8818680..1cf92084e43099ca6debef94c1f230de73370746 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