diff --git a/packages/desktop-client/src/components/filters/AppliedFilters.tsx b/packages/desktop-client/src/components/filters/AppliedFilters.tsx new file mode 100644 index 00000000000..1f5ead431ea --- /dev/null +++ b/packages/desktop-client/src/components/filters/AppliedFilters.tsx @@ -0,0 +1,55 @@ +import React from 'react'; + +import { type RuleConditionEntity } from 'loot-core/types/models'; + +import { View } from '../common/View'; + +import { FilterExpression } from './FilterExpression'; +import { CondOpMenu } from './SavedFilters'; + +type AppliedFiltersProps = { + filters: RuleConditionEntity[]; + onUpdate: ( + filter: RuleConditionEntity, + newFilter: RuleConditionEntity, + ) => RuleConditionEntity; + onDelete: (filter: RuleConditionEntity) => void; + conditionsOp: string; + onCondOpChange: () => void; +}; + +export function AppliedFilters({ + filters, + onUpdate, + onDelete, + conditionsOp, + onCondOpChange, +}: AppliedFiltersProps) { + return ( + + + {filters.map((filter: RuleConditionEntity, i: number) => ( + onUpdate(filter, newFilter)} + onDelete={() => onDelete(filter)} + /> + ))} + + ); +} diff --git a/packages/desktop-client/src/components/filters/CompactFiltersButton.tsx b/packages/desktop-client/src/components/filters/CompactFiltersButton.tsx index 8848ce8bd04..7da1dc8fa7d 100644 --- a/packages/desktop-client/src/components/filters/CompactFiltersButton.tsx +++ b/packages/desktop-client/src/components/filters/CompactFiltersButton.tsx @@ -1,14 +1,9 @@ -// @ts-strict-ignore import React from 'react'; import { SvgFilter } from '../../icons/v1'; import { Button } from '../common/Button'; -type CompactFiltersButtonProps = { - onClick: (newValue) => void; -}; - -export function CompactFiltersButton({ onClick }: CompactFiltersButtonProps) { +export function CompactFiltersButton({ onClick }: { onClick: () => void }) { return ( diff --git a/packages/desktop-client/src/components/filters/FilterExpression.tsx b/packages/desktop-client/src/components/filters/FilterExpression.tsx new file mode 100644 index 00000000000..08bb1564560 --- /dev/null +++ b/packages/desktop-client/src/components/filters/FilterExpression.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; + +import { mapField, friendlyOp } from 'loot-core/src/shared/rules'; +import { integerToCurrency } from 'loot-core/src/shared/util'; +import { + type RuleConditionOp, + type RuleConditionEntity, +} from 'loot-core/src/types/models'; + +import { SvgDelete } from '../../icons/v0'; +import { type CSSProperties, theme } from '../../style'; +import { Button } from '../common/Button'; +import { Text } from '../common/Text'; +import { View } from '../common/View'; +import { Value } from '../rules/Value'; + +import { FilterEditor } from './FiltersMenu'; +import { subfieldFromFilter } from './subfieldFromFilter'; + +type FilterExpressionProps = { + field: string | undefined; + customName: string | undefined; + op: RuleConditionOp | undefined; + value: string | string[] | number | boolean | undefined; + options: RuleConditionEntity['options']; + style?: CSSProperties; + onChange: (cond: RuleConditionEntity) => RuleConditionEntity; + onDelete: () => void; +}; + +export function FilterExpression({ + field: originalField, + customName, + op, + value, + options, + style, + onChange, + onDelete, +}: FilterExpressionProps) { + const [editing, setEditing] = useState(false); + + const field = subfieldFromFilter({ field: originalField, value }); + + return ( + + setEditing(true)} + style={{ marginRight: -7 }} + > + + {customName ? ( + {customName} + ) : ( + <> + + {mapField(field, options)} + {' '} + {friendlyOp(op, null)}{' '} + + > + )} + + + + + + {editing && ( + setEditing(false)} + /> + )} + + ); +} diff --git a/packages/desktop-client/src/components/filters/FiltersButton.tsx b/packages/desktop-client/src/components/filters/FiltersButton.tsx index f6f420f4889..c2491b95132 100644 --- a/packages/desktop-client/src/components/filters/FiltersButton.tsx +++ b/packages/desktop-client/src/components/filters/FiltersButton.tsx @@ -1,14 +1,9 @@ -// @ts-strict-ignore import React from 'react'; import { SvgSettingsSliderAlternate } from '../../icons/v2'; import { Button } from '../common/Button'; -type FiltersButtonProps = { - onClick: (newValue) => void; -}; - -export function FiltersButton({ onClick }: FiltersButtonProps) { +export function FiltersButton({ onClick }: { onClick: () => void }) { return ( [field, mapField(field)]); -function subfieldFromFilter({ field, options, value }) { - if (field === 'date') { - if (value.length === 7) { - return 'month'; - } else if (value.length === 4) { - return 'year'; - } - } else if (field === 'amount') { - if (options && options.inflow) { - return 'amount-inflow'; - } else if (options && options.outflow) { - return 'amount-outflow'; - } - } - return field; -} - -function subfieldToOptions(field, subfield) { - switch (field) { - case 'amount': - switch (subfield) { - case 'amount-inflow': - return { inflow: true }; - case 'amount-outflow': - return { outflow: true }; - default: - return null; - } - case 'date': - switch (subfield) { - case 'month': - return { month: true }; - case 'year': - return { year: true }; - default: - return null; - } - default: - return null; - } -} - -function OpButton({ op, selected, style, onClick }) { - return ( - - {friendlyOp(op)} - - ); -} - -function updateFilterReducer(state, action) { - switch (action.type) { - case 'set-op': { - const type = FIELD_TYPES.get(state.field); - let value = state.value; - if ( - (type === 'id' || type === 'string') && - (action.op === 'contains' || - action.op === 'is' || - action.op === 'doesNotContain' || - action.op === 'isNot') - ) { - // Clear out the value if switching between contains or - // is/oneof for the id or string type - value = null; - } - return { ...state, op: action.op, value }; - } - case 'set-value': { - const { value } = makeValue(action.value, { - type: FIELD_TYPES.get(state.field), - }); - return { ...state, value }; - } - default: - throw new Error(`Unhandled action type: ${action.type}`); - } -} - function ConfigureField({ field, initialSubfield = field, @@ -478,7 +383,7 @@ export function FilterButton({ onApply, compact, hover }) { ); } -function FilterEditor({ field, op, value, options, onSave, onClose }) { +export function FilterEditor({ field, op, value, options, onSave, onClose }) { const [state, dispatch] = useReducer( (state, action) => { switch (action.type) { @@ -508,119 +413,3 @@ function FilterEditor({ field, op, value, options, onSave, onClose }) { /> ); } - -function FilterExpression({ - field: originalField, - customName, - op, - value, - options, - stage, - style, - onChange, - onDelete, -}) { - const [editing, setEditing] = useState(false); - - const field = subfieldFromFilter({ field: originalField, value }); - - return ( - - setEditing(true)} - style={{ marginRight: -7 }} - > - - {customName ? ( - {customName} - ) : ( - <> - - {mapField(field, options)} - {' '} - {friendlyOp(op, null)}{' '} - - > - )} - - - - - - {editing && ( - setEditing(false)} - /> - )} - - ); -} - -export function AppliedFilters({ - filters, - editingFilter, - onUpdate, - onDelete, - conditionsOp, - onCondOpChange, -}) { - return ( - - - {filters.map((filter, i) => ( - onUpdate(filter, newFilter)} - onDelete={() => onDelete(filter)} - /> - ))} - - ); -} diff --git a/packages/desktop-client/src/components/filters/OpButton.tsx b/packages/desktop-client/src/components/filters/OpButton.tsx new file mode 100644 index 00000000000..f061cb0c81a --- /dev/null +++ b/packages/desktop-client/src/components/filters/OpButton.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import { friendlyOp } from 'loot-core/src/shared/rules'; + +import { type CSSProperties, theme } from '../../style'; +import { Button } from '../common/Button'; + +type OpButtonProps = { + op: string; + selected: boolean; + onClick: () => void; + style?: CSSProperties; +}; + +export function OpButton({ op, selected, style, onClick }: OpButtonProps) { + return ( + + {friendlyOp(op)} + + ); +} diff --git a/packages/desktop-client/src/components/filters/SavedFilters.jsx b/packages/desktop-client/src/components/filters/SavedFilters.jsx index 497ccd14ef6..1fd0be01155 100644 --- a/packages/desktop-client/src/components/filters/SavedFilters.jsx +++ b/packages/desktop-client/src/components/filters/SavedFilters.jsx @@ -14,7 +14,7 @@ import { FormField, FormLabel } from '../forms'; import { FieldSelect } from '../modals/EditRule'; import { GenericInput } from '../util/GenericInput'; -import { AppliedFilters } from './FiltersMenu'; +import { AppliedFilters } from './AppliedFilters'; function FilterMenu({ onClose, filterId, onFilterMenuSelect }) { return ( @@ -285,21 +285,21 @@ function SavedFilterMenuButton({ } export function CondOpMenu({ conditionsOp, onCondOpChange, filters }) { - return ( - filters.length > 1 && ( - - onCondOpChange(value, filters)} - /> - of: - - ) + return filters.length > 1 ? ( + + onCondOpChange(value, filters)} + /> + of: + + ) : ( + ); } diff --git a/packages/desktop-client/src/components/filters/subfieldFromFilter.ts b/packages/desktop-client/src/components/filters/subfieldFromFilter.ts new file mode 100644 index 00000000000..d4f60dfe202 --- /dev/null +++ b/packages/desktop-client/src/components/filters/subfieldFromFilter.ts @@ -0,0 +1,27 @@ +import { type RuleConditionEntity } from 'loot-core/src/types/models'; + +export function subfieldFromFilter({ + field, + options, + value, +}: RuleConditionEntity) { + if (field === 'date') { + if (typeof value === 'string') { + if (value.length === 7) { + return 'month'; + } else if (value.length === 4) { + return 'year'; + } + } + } + + if (field === 'amount') { + if (options && options.inflow) { + return 'amount-inflow'; + } else if (options && options.outflow) { + return 'amount-outflow'; + } + } + + return field; +} diff --git a/packages/desktop-client/src/components/filters/subfieldToOptions.ts b/packages/desktop-client/src/components/filters/subfieldToOptions.ts new file mode 100644 index 00000000000..60ac78746e8 --- /dev/null +++ b/packages/desktop-client/src/components/filters/subfieldToOptions.ts @@ -0,0 +1,34 @@ +import { type RuleConditionEntity } from 'loot-core/src/types/models'; + +export function subfieldToOptions(field: string, subfield: string) { + let setOptions: RuleConditionEntity['options']; + switch (field) { + case 'amount': + switch (subfield) { + case 'amount-inflow': + setOptions = { inflow: true }; + break; + case 'amount-outflow': + setOptions = { outflow: true }; + break; + default: + break; + } + break; + case 'date': + switch (subfield) { + case 'month': + setOptions = { month: true }; + break; + case 'year': + setOptions = { year: true }; + break; + default: + break; + } + break; + default: + break; + } + return setOptions; +} diff --git a/packages/desktop-client/src/components/filters/updateFilterReducer.ts b/packages/desktop-client/src/components/filters/updateFilterReducer.ts new file mode 100644 index 00000000000..8bbca3ce850 --- /dev/null +++ b/packages/desktop-client/src/components/filters/updateFilterReducer.ts @@ -0,0 +1,34 @@ +import { makeValue, FIELD_TYPES } from 'loot-core/src/shared/rules'; +import { type RuleConditionEntity } from 'loot-core/src/types/models'; + +export function updateFilterReducer( + state: { field: string; value: string | string[] | number | boolean | null }, + action: RuleConditionEntity, +) { + switch (action.type) { + case 'set-op': { + const type = FIELD_TYPES.get(state.field); + let value = state.value; + if ( + (type === 'id' || type === 'string') && + (action.op === 'contains' || + action.op === 'is' || + action.op === 'doesNotContain' || + action.op === 'isNot') + ) { + // Clear out the value if switching between contains or + // is/oneof for the id or string type + value = null; + } + return { ...state, op: action.op, value }; + } + case 'set-value': { + const { value } = makeValue(action.value, { + type: FIELD_TYPES.get(state.field), + }); + return { ...state, value }; + } + default: + throw new Error(`Unhandled action type: ${action.type}`); + } +} diff --git a/packages/desktop-client/src/components/reports/Header.jsx b/packages/desktop-client/src/components/reports/Header.jsx index e3231f8d696..aaad46b7d05 100644 --- a/packages/desktop-client/src/components/reports/Header.jsx +++ b/packages/desktop-client/src/components/reports/Header.jsx @@ -8,7 +8,8 @@ import { Button } from '../common/Button'; import { ButtonLink } from '../common/ButtonLink'; import { Select } from '../common/Select'; import { View } from '../common/View'; -import { FilterButton, AppliedFilters } from '../filters/FiltersMenu'; +import { AppliedFilters } from '../filters/AppliedFilters'; +import { FilterButton } from '../filters/FiltersMenu'; export function validateStart(allMonths, start, end) { const earliest = allMonths[allMonths.length - 1].name; diff --git a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx index c182ec38479..f4ff885c412 100644 --- a/packages/desktop-client/src/components/reports/reports/CustomReport.jsx +++ b/packages/desktop-client/src/components/reports/reports/CustomReport.jsx @@ -18,7 +18,7 @@ import { AlignedText } from '../../common/AlignedText'; import { Block } from '../../common/Block'; import { Text } from '../../common/Text'; import { View } from '../../common/View'; -import { AppliedFilters } from '../../filters/FiltersMenu'; +import { AppliedFilters } from '../../filters/AppliedFilters'; import { PrivacyFilter } from '../../PrivacyFilter'; import { ChooseGraph } from '../ChooseGraph'; import { Header } from '../Header'; diff --git a/packages/loot-core/src/types/models/rule.d.ts b/packages/loot-core/src/types/models/rule.d.ts index 6b9eb147f55..ce2b384f665 100644 --- a/packages/loot-core/src/types/models/rule.d.ts +++ b/packages/loot-core/src/types/models/rule.d.ts @@ -9,24 +9,31 @@ export interface RuleEntity { tombstone?: boolean; } +export type RuleConditionOp = + | 'is' + | 'isNot' + | 'oneOf' + | 'notOneOf' + | 'isapprox' + | 'isbetween' + | 'gt' + | 'gte' + | 'lt' + | 'lte' + | 'contains' + | 'doesNotContain'; + export interface RuleConditionEntity { - field: unknown; - op: - | 'is' - | 'isNot' - | 'oneOf' - | 'notOneOf' - | 'isapprox' - | 'isbetween' - | 'gt' - | 'gte' - | 'lt' - | 'lte' - | 'contains' - | 'doesNotContain'; - value: unknown; - options?: unknown; - conditionsOp?: unknown; + field?: string; + op?: RuleConditionOp; + value?: string | string[] | number | boolean; + options?: { + inflow?: boolean; + outflow?: boolean; + month?: boolean; + year?: boolean; + }; + conditionsOp?: string; type?: string; customName?: string; } diff --git a/upcoming-release-notes/2231.md b/upcoming-release-notes/2231.md new file mode 100644 index 00000000000..fd198f7f20a --- /dev/null +++ b/upcoming-release-notes/2231.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [carkom] +--- + +Split out mega-file FiltersMenu.jsx into separate elements and converted them all to Typescript.