diff --git a/packages/desktop-client/src/components/accounts/Header.jsx b/packages/desktop-client/src/components/accounts/Header.jsx index 80c1030ca219e0148c0762036a18a5a24de02c68..95ae0dddaf940acda6b05c5fde1bca0cc9a2726b 100644 --- a/packages/desktop-client/src/components/accounts/Header.jsx +++ b/packages/desktop-client/src/components/accounts/Header.jsx @@ -22,7 +22,7 @@ import { Search } from '../common/Search'; import { Stack } from '../common/Stack'; import { View } from '../common/View'; import { FilterButton } from '../filters/FiltersMenu'; -import { FiltersStack } from '../filters/SavedFilters'; +import { FiltersStack } from '../filters/FiltersStack'; import { KeyHandlers } from '../KeyHandlers'; import { NotesButton } from '../NotesButton'; import { SelectedTransactionsButton } from '../transactions/SelectedTransactions'; diff --git a/packages/desktop-client/src/components/filters/AppliedFilters.tsx b/packages/desktop-client/src/components/filters/AppliedFilters.tsx index eac0510a1ab7713ac0fb43f7598181819abe3aec..930d319276a7e2787081a1775b5bf10133f32b37 100644 --- a/packages/desktop-client/src/components/filters/AppliedFilters.tsx +++ b/packages/desktop-client/src/components/filters/AppliedFilters.tsx @@ -4,8 +4,8 @@ import { type RuleConditionEntity } from 'loot-core/src/types/models'; import { View } from '../common/View'; +import { CondOpMenu } from './CondOpMenu'; import { FilterExpression } from './FilterExpression'; -import { CondOpMenu } from './SavedFilters'; type AppliedFiltersProps = { filters: RuleConditionEntity[]; @@ -15,7 +15,7 @@ type AppliedFiltersProps = { ) => RuleConditionEntity; onDelete: (filter: RuleConditionEntity) => void; conditionsOp: string; - onCondOpChange: () => void; + onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void; }; export function AppliedFilters({ diff --git a/packages/desktop-client/src/components/filters/CondOpMenu.tsx b/packages/desktop-client/src/components/filters/CondOpMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..af5eec779cb77b54259b100bf526d3a3658ac189 --- /dev/null +++ b/packages/desktop-client/src/components/filters/CondOpMenu.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { type RuleConditionEntity } from 'loot-core/types/models'; + +import { theme } from '../../style'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; +import { FieldSelect } from '../modals/EditRule'; + +export function CondOpMenu({ + conditionsOp, + onCondOpChange, + filters, +}: { + conditionsOp: string; + onCondOpChange: (value: string, filters: RuleConditionEntity[]) => void; + filters: RuleConditionEntity[]; +}) { + return filters.length > 1 ? ( + <Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}> + <FieldSelect + style={{ display: 'inline-flex' }} + fields={[ + ['and', 'all'], + ['or', 'any'], + ]} + value={conditionsOp} + onChange={(name: string, value: string) => + onCondOpChange(value, filters) + } + /> + of: + </Text> + ) : ( + <View /> + ); +} diff --git a/packages/desktop-client/src/components/filters/FilterMenu.tsx b/packages/desktop-client/src/components/filters/FilterMenu.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a392fd1779b3749cfd81f71d957d575e884b9e7e --- /dev/null +++ b/packages/desktop-client/src/components/filters/FilterMenu.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import { Menu } from '../common/Menu'; +import { MenuTooltip } from '../common/MenuTooltip'; + +import { type SavedFilter } from './SavedFilterMenuButton'; + +export function FilterMenu({ + onClose, + filterId, + onFilterMenuSelect, +}: { + onClose: () => void; + filterId: SavedFilter; + onFilterMenuSelect: (item: string) => void; +}) { + return ( + <MenuTooltip width={200} onClose={onClose}> + <Menu + onMenuSelect={item => { + onFilterMenuSelect(item); + }} + items={ + !filterId.id + ? [ + { name: 'save-filter', text: 'Save new filter' }, + { name: 'clear-filter', text: 'Clear all conditions' }, + ] + : filterId.id !== null && filterId.status === 'saved' + ? [ + { name: 'rename-filter', text: 'Rename' }, + { name: 'delete-filter', text: 'Delete' }, + Menu.line, + { + name: 'save-filter', + text: 'Save new filter', + disabled: true, + }, + { name: 'clear-filter', text: 'Clear all conditions' }, + ] + : [ + { name: 'rename-filter', text: 'Rename' }, + { name: 'update-filter', text: 'Update condtions' }, + { name: 'reload-filter', text: 'Revert changes' }, + { name: 'delete-filter', text: 'Delete' }, + Menu.line, + { name: 'save-filter', text: 'Save new filter' }, + { name: 'clear-filter', text: 'Clear all conditions' }, + ] + } + /> + </MenuTooltip> + ); +} diff --git a/packages/desktop-client/src/components/filters/FiltersStack.tsx b/packages/desktop-client/src/components/filters/FiltersStack.tsx new file mode 100644 index 0000000000000000000000000000000000000000..055fd110c68df65c7ad05a388a028368899a3e64 --- /dev/null +++ b/packages/desktop-client/src/components/filters/FiltersStack.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import { type RuleConditionEntity } from 'loot-core/types/models/rule'; + +import { Stack } from '../common/Stack'; +import { View } from '../common/View'; + +import { AppliedFilters } from './AppliedFilters'; +import { + type SavedFilter, + SavedFilterMenuButton, +} from './SavedFilterMenuButton'; + +export function FiltersStack({ + filters, + conditionsOp, + onUpdateFilter, + onDeleteFilter, + onClearFilters, + onReloadSavedFilter, + filterId, + filtersList, + onCondOpChange, +}: { + filters: RuleConditionEntity[]; + conditionsOp: string; + onUpdateFilter: ( + filter: RuleConditionEntity, + newFilter: RuleConditionEntity, + ) => RuleConditionEntity; + onDeleteFilter: (filter: RuleConditionEntity) => void; + onClearFilters: () => void; + onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void; + filterId: SavedFilter; + filtersList: RuleConditionEntity[]; + onCondOpChange: () => void; +}) { + return ( + <View> + <Stack + spacing={2} + direction="row" + justify="flex-start" + align="flex-start" + > + <AppliedFilters + filters={filters} + conditionsOp={conditionsOp} + onCondOpChange={onCondOpChange} + onUpdate={onUpdateFilter} + onDelete={onDeleteFilter} + /> + <View style={{ flex: 1 }} /> + <SavedFilterMenuButton + filters={filters} + conditionsOp={conditionsOp} + filterId={filterId} + onClearFilters={onClearFilters} + onReloadSavedFilter={onReloadSavedFilter} + filtersList={filtersList} + /> + </Stack> + </View> + ); +} diff --git a/packages/desktop-client/src/components/filters/NameFilter.tsx b/packages/desktop-client/src/components/filters/NameFilter.tsx new file mode 100644 index 0000000000000000000000000000000000000000..815860b07691363bd093448150726e171cca886c --- /dev/null +++ b/packages/desktop-client/src/components/filters/NameFilter.tsx @@ -0,0 +1,79 @@ +import React, { useRef, useEffect } from 'react'; + +import { theme } from '../../style'; +import { Button } from '../common/Button'; +import { Input } from '../common/Input'; +import { MenuTooltip } from '../common/MenuTooltip'; +import { Stack } from '../common/Stack'; +import { Text } from '../common/Text'; +import { FormField, FormLabel } from '../forms'; + +export function NameFilter({ + onClose, + menuItem, + name, + setName, + adding, + onAddUpdate, + err, +}: { + onClose: () => void; + menuItem: string; + name: string; + setName: (item: string) => void; + adding: boolean; + onAddUpdate: () => void; + err: string | null; +}) { + const inputRef = useRef<HTMLInputElement>(null); + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, []); + + return ( + <MenuTooltip width={325} onClose={onClose}> + {menuItem !== 'update-filter' && ( + <form> + <Stack + direction="row" + justify="flex-end" + align="center" + style={{ padding: 10 }} + > + <FormField style={{ flex: 1 }}> + <FormLabel + title="Filter Name" + htmlFor="name-field" + style={{ userSelect: 'none' }} + /> + <Input + id="name-field" + inputRef={inputRef} + defaultValue={name || ''} + onUpdate={setName} + /> + </FormField> + <Button + type="primary" + style={{ marginTop: 18 }} + onClick={e => { + e.preventDefault(); + onAddUpdate(); + }} + > + {adding ? 'Add' : 'Update'} + </Button> + </Stack> + </form> + )} + {err && ( + <Stack direction="row" align="center" style={{ padding: 10 }}> + <Text style={{ color: theme.errorText }}>{err}</Text> + </Stack> + )} + </MenuTooltip> + ); +} diff --git a/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dc1aa9dc6e887b99f1693e348b87b6d8c7b793ec --- /dev/null +++ b/packages/desktop-client/src/components/filters/SavedFilterMenuButton.tsx @@ -0,0 +1,203 @@ +import React, { useState } from 'react'; + +import { send, sendCatch } from 'loot-core/src/platform/client/fetch'; +import { type RuleConditionEntity } from 'loot-core/types/models/rule'; + +import { SvgExpandArrow } from '../../icons/v0'; +import { Button } from '../common/Button'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; + +import { FilterMenu } from './FilterMenu'; +import { NameFilter } from './NameFilter'; + +export type SavedFilter = { + conditions?: RuleConditionEntity[]; + conditionsOp?: string; + id?: string; + name: string; + status?: string; +}; + +export function SavedFilterMenuButton({ + filters, + conditionsOp, + filterId, + onClearFilters, + onReloadSavedFilter, + filtersList, +}: { + filters: RuleConditionEntity[]; + conditionsOp: string; + filterId: SavedFilter; + onClearFilters: () => void; + onReloadSavedFilter: (savedFilter: SavedFilter, value?: string) => void; + filtersList: RuleConditionEntity[]; +}) { + const [nameOpen, setNameOpen] = useState(false); + const [adding, setAdding] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); + const [err, setErr] = useState(null); + const [menuItem, setMenuItem] = useState(''); + const [name, setName] = useState(filterId.name); + const id = filterId.id; + let savedFilter: SavedFilter; + + const onFilterMenuSelect = async (item: string) => { + setMenuItem(item); + switch (item) { + case 'rename-filter': + setErr(null); + setAdding(false); + setMenuOpen(false); + setNameOpen(true); + break; + case 'delete-filter': + setMenuOpen(false); + await send('filter-delete', id); + onClearFilters(); + break; + case 'update-filter': + setErr(null); + setAdding(false); + setMenuOpen(false); + savedFilter = { + conditions: filters, + conditionsOp, + id: filterId.id, + name: filterId.name, + status: 'saved', + }; + const response = await sendCatch('filter-update', { + state: savedFilter, + filters: [...filtersList], + }); + + if (response.error) { + setErr(response.error.message); + setNameOpen(true); + return; + } + + onReloadSavedFilter(savedFilter, 'update'); + break; + case 'save-filter': + setErr(null); + setAdding(true); + setMenuOpen(false); + setNameOpen(true); + break; + case 'reload-filter': + setMenuOpen(false); + savedFilter = { + ...savedFilter, + status: 'saved', + }; + onReloadSavedFilter(savedFilter, 'reload'); + break; + case 'clear-filter': + setMenuOpen(false); + onClearFilters(); + break; + default: + } + }; + + async function onAddUpdate() { + if (adding) { + const newSavedFilter = { + conditions: filters, + conditionsOp, + name, + status: 'saved', + }; + + const response = await sendCatch('filter-create', { + state: newSavedFilter, + filters: [...filtersList], + }); + + if (response.error) { + setErr(response.error.message); + setNameOpen(true); + return; + } + + setNameOpen(false); + onReloadSavedFilter({ + ...newSavedFilter, + id: response.data, + }); + return; + } + + const updatedFilter = { + conditions: filterId.conditions, + conditionsOp: filterId.conditionsOp, + id: filterId.id, + name, + }; + + const response = await sendCatch('filter-update', { + state: updatedFilter, + filters: [...filtersList], + }); + + if (response.error) { + setErr(response.error.message); + setNameOpen(true); + return; + } + + setNameOpen(false); + onReloadSavedFilter(updatedFilter); + } + + return ( + <View> + {filters.length > 0 && ( + <Button + type="bare" + style={{ marginTop: 10 }} + onClick={() => { + setMenuOpen(true); + }} + > + <Text + style={{ + maxWidth: 150, + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + flexShrink: 0, + }} + > + {!filterId.id ? 'Unsaved filter' : filterId.name} + </Text> + {filterId.id && filterId.status !== 'saved' && ( + <Text>(modified) </Text> + )} + <SvgExpandArrow width={8} height={8} style={{ marginRight: 5 }} /> + </Button> + )} + {menuOpen && ( + <FilterMenu + onClose={() => setMenuOpen(false)} + filterId={filterId} + onFilterMenuSelect={onFilterMenuSelect} + /> + )} + {nameOpen && ( + <NameFilter + onClose={() => setNameOpen(false)} + menuItem={menuItem} + name={name} + setName={setName} + adding={adding} + onAddUpdate={onAddUpdate} + err={err} + /> + )} + </View> + ); +} diff --git a/packages/desktop-client/src/components/filters/SavedFilters.jsx b/packages/desktop-client/src/components/filters/SavedFilters.jsx deleted file mode 100644 index 1fd0be01155f92e8ec92c04a70c44ae742faee61..0000000000000000000000000000000000000000 --- a/packages/desktop-client/src/components/filters/SavedFilters.jsx +++ /dev/null @@ -1,344 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; - -import { send, sendCatch } from 'loot-core/src/platform/client/fetch'; - -import { SvgExpandArrow } from '../../icons/v0'; -import { theme } from '../../style'; -import { Button } from '../common/Button'; -import { Menu } from '../common/Menu'; -import { MenuTooltip } from '../common/MenuTooltip'; -import { Stack } from '../common/Stack'; -import { Text } from '../common/Text'; -import { View } from '../common/View'; -import { FormField, FormLabel } from '../forms'; -import { FieldSelect } from '../modals/EditRule'; -import { GenericInput } from '../util/GenericInput'; - -import { AppliedFilters } from './AppliedFilters'; - -function FilterMenu({ onClose, filterId, onFilterMenuSelect }) { - return ( - <MenuTooltip width={200} onClose={onClose}> - <Menu - onMenuSelect={item => { - onFilterMenuSelect(item); - }} - items={[ - ...(!filterId.id - ? [ - { name: 'save-filter', text: 'Save new filter' }, - { name: 'clear-filter', text: 'Clear all conditions' }, - ] - : [ - ...(filterId.id !== null && filterId.status === 'saved' - ? [ - { name: 'rename-filter', text: 'Rename' }, - { name: 'delete-filter', text: 'Delete' }, - Menu.line, - { - name: 'save-filter', - text: 'Save new filter', - disabled: true, - }, - { name: 'clear-filter', text: 'Clear all conditions' }, - ] - : [ - { name: 'rename-filter', text: 'Rename' }, - { name: 'update-filter', text: 'Update condtions' }, - { name: 'reload-filter', text: 'Revert changes' }, - { name: 'delete-filter', text: 'Delete' }, - Menu.line, - { name: 'save-filter', text: 'Save new filter' }, - { name: 'clear-filter', text: 'Clear all conditions' }, - ]), - ]), - ]} - /> - </MenuTooltip> - ); -} - -function NameFilter({ - onClose, - menuItem, - name, - setName, - adding, - onAddUpdate, - err, -}) { - const inputRef = useRef(); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <MenuTooltip width={325} onClose={onClose}> - {menuItem !== 'update-filter' && ( - <form> - <Stack - direction="row" - justify="flex-end" - align="center" - style={{ padding: 10 }} - > - <FormField style={{ flex: 1 }}> - <FormLabel - title="Filter Name" - htmlFor="name-field" - style={{ userSelect: 'none' }} - /> - <GenericInput - inputRef={inputRef} - id="name-field" - field="string" - type="string" - value={name} - onChange={setName} - /> - </FormField> - <Button - type="primary" - style={{ marginTop: 18 }} - onClick={e => { - e.preventDefault(); - onAddUpdate(); - }} - > - {adding ? 'Add' : 'Update'} - </Button> - </Stack> - </form> - )} - {err && ( - <Stack direction="row" align="center" style={{ padding: 10 }}> - <Text style={{ color: theme.errorText }}>{err}</Text> - </Stack> - )} - </MenuTooltip> - ); -} - -function SavedFilterMenuButton({ - filters, - conditionsOp, - filterId, - onClearFilters, - onReloadSavedFilter, - filtersList, -}) { - const [nameOpen, setNameOpen] = useState(false); - const [adding, setAdding] = useState(false); - const [menuOpen, setMenuOpen] = useState(false); - const [err, setErr] = useState(null); - const [menuItem, setMenuItem] = useState(null); - const [name, setName] = useState(filterId.name); - const id = filterId.id; - let res; - let savedFilter; - - const onFilterMenuSelect = async item => { - setMenuItem(item); - switch (item) { - case 'rename-filter': - setErr(null); - setAdding(false); - setMenuOpen(false); - setNameOpen(true); - break; - case 'delete-filter': - setMenuOpen(false); - await send('filter-delete', id); - onClearFilters(); - break; - case 'update-filter': - setErr(null); - setAdding(false); - setMenuOpen(false); - savedFilter = { - conditions: filters, - conditionsOp, - id: filterId.id, - name: filterId.name, - status: 'saved', - }; - res = await sendCatch('filter-update', { - state: savedFilter, - filters: [...filtersList], - }); - if (res.error) { - setErr(res.error.message); - setNameOpen(true); - } else { - onReloadSavedFilter(savedFilter, 'update'); - } - break; - case 'save-filter': - setErr(null); - setAdding(true); - setMenuOpen(false); - setNameOpen(true); - break; - case 'reload-filter': - setMenuOpen(false); - savedFilter = { - status: 'saved', - }; - onReloadSavedFilter(savedFilter, 'reload'); - break; - case 'clear-filter': - setMenuOpen(false); - onClearFilters(); - break; - default: - } - }; - - async function onAddUpdate() { - if (adding) { - //create new flow - savedFilter = { - conditions: filters, - conditionsOp, - name, - status: 'saved', - }; - res = await sendCatch('filter-create', { - state: savedFilter, - filters: [...filtersList], - }); - savedFilter = { - ...savedFilter, - id: res.data, - }; - } else { - //rename flow - savedFilter = { - conditions: filterId.conditions, - conditionsOp: filterId.conditionsOp, - id: filterId.id, - name, - }; - res = await sendCatch('filter-update', { - state: savedFilter, - filters: [...filtersList], - }); - } - if (res.error) { - setErr(res.error.message); - } else { - setNameOpen(false); - onReloadSavedFilter(savedFilter); - } - } - - return ( - <View> - {filters.length > 0 && ( - <Button - type="bare" - style={{ marginTop: 10 }} - onClick={() => { - setMenuOpen(true); - }} - > - <Text - style={{ - maxWidth: 150, - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - flexShrink: 0, - }} - > - {!filterId.id ? 'Unsaved filter' : filterId.name} - </Text> - {filterId.id && filterId.status !== 'saved' && ( - <Text>(modified) </Text> - )} - <SvgExpandArrow width={8} height={8} style={{ marginRight: 5 }} /> - </Button> - )} - {menuOpen && ( - <FilterMenu - onClose={() => setMenuOpen(false)} - filterId={filterId} - onFilterMenuSelect={onFilterMenuSelect} - /> - )} - {nameOpen && ( - <NameFilter - onClose={() => setNameOpen(false)} - menuItem={menuItem} - name={name} - setName={setName} - adding={adding} - onAddUpdate={onAddUpdate} - err={err} - /> - )} - </View> - ); -} - -export function CondOpMenu({ conditionsOp, onCondOpChange, filters }) { - return filters.length > 1 ? ( - <Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}> - <FieldSelect - style={{ display: 'inline-flex' }} - fields={[ - ['and', 'all'], - ['or', 'any'], - ]} - value={conditionsOp} - onChange={(name, value) => onCondOpChange(value, filters)} - /> - of: - </Text> - ) : ( - <View /> - ); -} - -export function FiltersStack({ - filters, - conditionsOp, - onUpdateFilter, - onDeleteFilter, - onClearFilters, - onReloadSavedFilter, - filterId, - filtersList, - onCondOpChange, -}) { - return ( - <View> - <Stack - spacing={2} - direction="row" - justify="flex-start" - align="flex-start" - > - <AppliedFilters - filters={filters} - conditionsOp={conditionsOp} - onCondOpChange={onCondOpChange} - onUpdate={onUpdateFilter} - onDelete={onDeleteFilter} - /> - <View style={{ flex: 1 }} /> - <SavedFilterMenuButton - filters={filters} - conditionsOp={conditionsOp} - filterId={filterId} - onClearFilters={onClearFilters} - onReloadSavedFilter={onReloadSavedFilter} - filtersList={filtersList} - /> - </Stack> - </View> - ); -} diff --git a/upcoming-release-notes/2320.md b/upcoming-release-notes/2320.md new file mode 100644 index 0000000000000000000000000000000000000000..18c6e635685ccc7eb633a9ae8cc9423be2c8b863 --- /dev/null +++ b/upcoming-release-notes/2320.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [carkom] +--- + +Split out mega-file SavedFilters.jsx into separate elements and converted them all to Typescript.