Skip to content
Snippets Groups Projects
Unverified Commit e6459f30 authored by Jed Fox's avatar Jed Fox Committed by GitHub
Browse files

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
parent 212b854e
No related branches found
No related tags found
No related merge requests found
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>
);
......
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>
);
}
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}
......
......@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment