mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 10:14:53 -05:00
Convert FiltersMenu to Typescript (part 1) (#2231)
* migration work * notes * typecheck * typecheck fixes * fixes * merge fixes * typecheck updates * review fixes
This commit is contained in:
@@ -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 (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CondOpMenu
|
||||||
|
conditionsOp={conditionsOp}
|
||||||
|
onCondOpChange={onCondOpChange}
|
||||||
|
filters={filters}
|
||||||
|
/>
|
||||||
|
{filters.map((filter: RuleConditionEntity, i: number) => (
|
||||||
|
<FilterExpression
|
||||||
|
key={i}
|
||||||
|
customName={filter.customName}
|
||||||
|
field={filter.field}
|
||||||
|
op={filter.op}
|
||||||
|
value={filter.value}
|
||||||
|
options={filter.options}
|
||||||
|
onChange={newFilter => onUpdate(filter, newFilter)}
|
||||||
|
onDelete={() => onDelete(filter)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
// @ts-strict-ignore
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { SvgFilter } from '../../icons/v1';
|
import { SvgFilter } from '../../icons/v1';
|
||||||
import { Button } from '../common/Button';
|
import { Button } from '../common/Button';
|
||||||
|
|
||||||
type CompactFiltersButtonProps = {
|
export function CompactFiltersButton({ onClick }: { onClick: () => void }) {
|
||||||
onClick: (newValue) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function CompactFiltersButton({ onClick }: CompactFiltersButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<Button type="bare" onClick={onClick}>
|
<Button type="bare" onClick={onClick}>
|
||||||
<SvgFilter width={15} height={15} />
|
<SvgFilter width={15} height={15} />
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.pillBackground,
|
||||||
|
borderRadius: 4,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: 10,
|
||||||
|
marginTop: 10,
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="bare"
|
||||||
|
disabled={customName != null}
|
||||||
|
onClick={() => setEditing(true)}
|
||||||
|
style={{ marginRight: -7 }}
|
||||||
|
>
|
||||||
|
<div style={{ paddingBlock: 1, paddingLeft: 5, paddingRight: 2 }}>
|
||||||
|
{customName ? (
|
||||||
|
<Text style={{ color: theme.pageTextPositive }}>{customName}</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text style={{ color: theme.pageTextPositive }}>
|
||||||
|
{mapField(field, options)}
|
||||||
|
</Text>{' '}
|
||||||
|
<Text>{friendlyOp(op, null)}</Text>{' '}
|
||||||
|
<Value
|
||||||
|
value={value}
|
||||||
|
field={field}
|
||||||
|
inline={true}
|
||||||
|
valueIsRaw={op === 'contains' || op === 'doesNotContain'}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
<Button type="bare" onClick={onDelete} aria-label="Delete filter">
|
||||||
|
<SvgDelete
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
margin: 5,
|
||||||
|
marginLeft: 3,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
{editing && (
|
||||||
|
<FilterEditor
|
||||||
|
field={originalField}
|
||||||
|
op={op}
|
||||||
|
value={
|
||||||
|
field === 'amount' && typeof value === 'number'
|
||||||
|
? integerToCurrency(value)
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
options={options}
|
||||||
|
onSave={onChange}
|
||||||
|
onClose={() => setEditing(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
// @ts-strict-ignore
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { SvgSettingsSliderAlternate } from '../../icons/v2';
|
import { SvgSettingsSliderAlternate } from '../../icons/v2';
|
||||||
import { Button } from '../common/Button';
|
import { Button } from '../common/Button';
|
||||||
|
|
||||||
type FiltersButtonProps = {
|
export function FiltersButton({ onClick }: { onClick: () => void }) {
|
||||||
onClick: (newValue) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function FiltersButton({ onClick }: FiltersButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<Button type="bare" onClick={onClick} title="Filters">
|
<Button type="bare" onClick={onClick} title="Filters">
|
||||||
<SvgSettingsSliderAlternate
|
<SvgSettingsSliderAlternate
|
||||||
|
|||||||
@@ -13,17 +13,14 @@ import { send } from 'loot-core/src/platform/client/fetch';
|
|||||||
import { getMonthYearFormat } from 'loot-core/src/shared/months';
|
import { getMonthYearFormat } from 'loot-core/src/shared/months';
|
||||||
import {
|
import {
|
||||||
mapField,
|
mapField,
|
||||||
friendlyOp,
|
|
||||||
deserializeField,
|
deserializeField,
|
||||||
getFieldError,
|
getFieldError,
|
||||||
unparse,
|
unparse,
|
||||||
makeValue,
|
|
||||||
FIELD_TYPES,
|
FIELD_TYPES,
|
||||||
TYPE_INFO,
|
TYPE_INFO,
|
||||||
} from 'loot-core/src/shared/rules';
|
} from 'loot-core/src/shared/rules';
|
||||||
import { titleFirst, integerToCurrency } from 'loot-core/src/shared/util';
|
import { titleFirst } from 'loot-core/src/shared/util';
|
||||||
|
|
||||||
import { SvgDelete } from '../../icons/v0';
|
|
||||||
import { theme } from '../../style';
|
import { theme } from '../../style';
|
||||||
import { Button } from '../common/Button';
|
import { Button } from '../common/Button';
|
||||||
import { HoverTarget } from '../common/HoverTarget';
|
import { HoverTarget } from '../common/HoverTarget';
|
||||||
@@ -32,13 +29,15 @@ import { Select } from '../common/Select';
|
|||||||
import { Stack } from '../common/Stack';
|
import { Stack } from '../common/Stack';
|
||||||
import { Text } from '../common/Text';
|
import { Text } from '../common/Text';
|
||||||
import { View } from '../common/View';
|
import { View } from '../common/View';
|
||||||
import { Value } from '../rules/Value';
|
|
||||||
import { Tooltip } from '../tooltips';
|
import { Tooltip } from '../tooltips';
|
||||||
import { GenericInput } from '../util/GenericInput';
|
import { GenericInput } from '../util/GenericInput';
|
||||||
|
|
||||||
import { CompactFiltersButton } from './CompactFiltersButton';
|
import { CompactFiltersButton } from './CompactFiltersButton';
|
||||||
import { FiltersButton } from './FiltersButton';
|
import { FiltersButton } from './FiltersButton';
|
||||||
import { CondOpMenu } from './SavedFilters';
|
import { OpButton } from './OpButton';
|
||||||
|
import { subfieldFromFilter } from './subfieldFromFilter';
|
||||||
|
import { subfieldToOptions } from './subfieldToOptions';
|
||||||
|
import { updateFilterReducer } from './updateFilterReducer';
|
||||||
|
|
||||||
const filterFields = [
|
const filterFields = [
|
||||||
'date',
|
'date',
|
||||||
@@ -52,100 +51,6 @@ const filterFields = [
|
|||||||
'saved',
|
'saved',
|
||||||
].map(field => [field, mapField(field)]);
|
].map(field => [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 (
|
|
||||||
<Button
|
|
||||||
type="bare"
|
|
||||||
style={{
|
|
||||||
backgroundColor: theme.pillBackground,
|
|
||||||
marginBottom: 5,
|
|
||||||
...style,
|
|
||||||
...(selected && {
|
|
||||||
color: theme.buttonNormalSelectedText,
|
|
||||||
'&,:hover,:active': {
|
|
||||||
backgroundColor: theme.buttonNormalSelectedBackground,
|
|
||||||
color: theme.buttonNormalSelectedText,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
{friendlyOp(op)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
function ConfigureField({
|
||||||
field,
|
field,
|
||||||
initialSubfield = 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(
|
const [state, dispatch] = useReducer(
|
||||||
(state, action) => {
|
(state, action) => {
|
||||||
switch (action.type) {
|
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 (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
backgroundColor: theme.pillBackground,
|
|
||||||
borderRadius: 4,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginRight: 10,
|
|
||||||
marginTop: 10,
|
|
||||||
...style,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
type="bare"
|
|
||||||
disabled={customName != null}
|
|
||||||
onClick={() => setEditing(true)}
|
|
||||||
style={{ marginRight: -7 }}
|
|
||||||
>
|
|
||||||
<div style={{ paddingBlock: 1, paddingLeft: 5, paddingRight: 2 }}>
|
|
||||||
{customName ? (
|
|
||||||
<Text style={{ color: theme.pageTextPositive }}>{customName}</Text>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Text style={{ color: theme.pageTextPositive }}>
|
|
||||||
{mapField(field, options)}
|
|
||||||
</Text>{' '}
|
|
||||||
<Text>{friendlyOp(op, null)}</Text>{' '}
|
|
||||||
<Value
|
|
||||||
value={value}
|
|
||||||
field={field}
|
|
||||||
inline={true}
|
|
||||||
valueIsRaw={op === 'contains' || op === 'doesNotContain'}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<Button type="bare" onClick={onDelete} aria-label="Delete filter">
|
|
||||||
<SvgDelete
|
|
||||||
style={{
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
margin: 5,
|
|
||||||
marginLeft: 3,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
{editing && (
|
|
||||||
<FilterEditor
|
|
||||||
field={originalField}
|
|
||||||
customName={customName}
|
|
||||||
op={op}
|
|
||||||
value={field === 'amount' ? integerToCurrency(value) : value}
|
|
||||||
options={options}
|
|
||||||
stage={stage}
|
|
||||||
onSave={onChange}
|
|
||||||
onClose={() => setEditing(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AppliedFilters({
|
|
||||||
filters,
|
|
||||||
editingFilter,
|
|
||||||
onUpdate,
|
|
||||||
onDelete,
|
|
||||||
conditionsOp,
|
|
||||||
onCondOpChange,
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CondOpMenu
|
|
||||||
conditionsOp={conditionsOp}
|
|
||||||
onCondOpChange={onCondOpChange}
|
|
||||||
filters={filters}
|
|
||||||
/>
|
|
||||||
{filters.map((filter, i) => (
|
|
||||||
<FilterExpression
|
|
||||||
key={i}
|
|
||||||
customName={filter.customName}
|
|
||||||
field={filter.field}
|
|
||||||
op={filter.op}
|
|
||||||
value={filter.value}
|
|
||||||
options={filter.options}
|
|
||||||
editing={editingFilter === filter}
|
|
||||||
onChange={newFilter => onUpdate(filter, newFilter)}
|
|
||||||
onDelete={() => onDelete(filter)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
36
packages/desktop-client/src/components/filters/OpButton.tsx
Normal file
36
packages/desktop-client/src/components/filters/OpButton.tsx
Normal file
@@ -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 (
|
||||||
|
<Button
|
||||||
|
type="bare"
|
||||||
|
style={{
|
||||||
|
backgroundColor: theme.pillBackground,
|
||||||
|
marginBottom: 5,
|
||||||
|
...style,
|
||||||
|
...(selected && {
|
||||||
|
color: theme.buttonNormalSelectedText,
|
||||||
|
'&,:hover,:active': {
|
||||||
|
backgroundColor: theme.buttonNormalSelectedBackground,
|
||||||
|
color: theme.buttonNormalSelectedText,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{friendlyOp(op)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import { FormField, FormLabel } from '../forms';
|
|||||||
import { FieldSelect } from '../modals/EditRule';
|
import { FieldSelect } from '../modals/EditRule';
|
||||||
import { GenericInput } from '../util/GenericInput';
|
import { GenericInput } from '../util/GenericInput';
|
||||||
|
|
||||||
import { AppliedFilters } from './FiltersMenu';
|
import { AppliedFilters } from './AppliedFilters';
|
||||||
|
|
||||||
function FilterMenu({ onClose, filterId, onFilterMenuSelect }) {
|
function FilterMenu({ onClose, filterId, onFilterMenuSelect }) {
|
||||||
return (
|
return (
|
||||||
@@ -285,21 +285,21 @@ function SavedFilterMenuButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CondOpMenu({ conditionsOp, onCondOpChange, filters }) {
|
export function CondOpMenu({ conditionsOp, onCondOpChange, filters }) {
|
||||||
return (
|
return filters.length > 1 ? (
|
||||||
filters.length > 1 && (
|
<Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
|
||||||
<Text style={{ color: theme.pageText, marginTop: 11, marginRight: 5 }}>
|
<FieldSelect
|
||||||
<FieldSelect
|
style={{ display: 'inline-flex' }}
|
||||||
style={{ display: 'inline-flex' }}
|
fields={[
|
||||||
fields={[
|
['and', 'all'],
|
||||||
['and', 'all'],
|
['or', 'any'],
|
||||||
['or', 'any'],
|
]}
|
||||||
]}
|
value={conditionsOp}
|
||||||
value={conditionsOp}
|
onChange={(name, value) => onCondOpChange(value, filters)}
|
||||||
onChange={(name, value) => onCondOpChange(value, filters)}
|
/>
|
||||||
/>
|
of:
|
||||||
of:
|
</Text>
|
||||||
</Text>
|
) : (
|
||||||
)
|
<View />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ import { Button } from '../common/Button';
|
|||||||
import { ButtonLink } from '../common/ButtonLink';
|
import { ButtonLink } from '../common/ButtonLink';
|
||||||
import { Select } from '../common/Select';
|
import { Select } from '../common/Select';
|
||||||
import { View } from '../common/View';
|
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) {
|
export function validateStart(allMonths, start, end) {
|
||||||
const earliest = allMonths[allMonths.length - 1].name;
|
const earliest = allMonths[allMonths.length - 1].name;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { AlignedText } from '../../common/AlignedText';
|
|||||||
import { Block } from '../../common/Block';
|
import { Block } from '../../common/Block';
|
||||||
import { Text } from '../../common/Text';
|
import { Text } from '../../common/Text';
|
||||||
import { View } from '../../common/View';
|
import { View } from '../../common/View';
|
||||||
import { AppliedFilters } from '../../filters/FiltersMenu';
|
import { AppliedFilters } from '../../filters/AppliedFilters';
|
||||||
import { PrivacyFilter } from '../../PrivacyFilter';
|
import { PrivacyFilter } from '../../PrivacyFilter';
|
||||||
import { ChooseGraph } from '../ChooseGraph';
|
import { ChooseGraph } from '../ChooseGraph';
|
||||||
import { Header } from '../Header';
|
import { Header } from '../Header';
|
||||||
|
|||||||
41
packages/loot-core/src/types/models/rule.d.ts
vendored
41
packages/loot-core/src/types/models/rule.d.ts
vendored
@@ -9,24 +9,31 @@ export interface RuleEntity {
|
|||||||
tombstone?: boolean;
|
tombstone?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RuleConditionOp =
|
||||||
|
| 'is'
|
||||||
|
| 'isNot'
|
||||||
|
| 'oneOf'
|
||||||
|
| 'notOneOf'
|
||||||
|
| 'isapprox'
|
||||||
|
| 'isbetween'
|
||||||
|
| 'gt'
|
||||||
|
| 'gte'
|
||||||
|
| 'lt'
|
||||||
|
| 'lte'
|
||||||
|
| 'contains'
|
||||||
|
| 'doesNotContain';
|
||||||
|
|
||||||
export interface RuleConditionEntity {
|
export interface RuleConditionEntity {
|
||||||
field: unknown;
|
field?: string;
|
||||||
op:
|
op?: RuleConditionOp;
|
||||||
| 'is'
|
value?: string | string[] | number | boolean;
|
||||||
| 'isNot'
|
options?: {
|
||||||
| 'oneOf'
|
inflow?: boolean;
|
||||||
| 'notOneOf'
|
outflow?: boolean;
|
||||||
| 'isapprox'
|
month?: boolean;
|
||||||
| 'isbetween'
|
year?: boolean;
|
||||||
| 'gt'
|
};
|
||||||
| 'gte'
|
conditionsOp?: string;
|
||||||
| 'lt'
|
|
||||||
| 'lte'
|
|
||||||
| 'contains'
|
|
||||||
| 'doesNotContain';
|
|
||||||
value: unknown;
|
|
||||||
options?: unknown;
|
|
||||||
conditionsOp?: unknown;
|
|
||||||
type?: string;
|
type?: string;
|
||||||
customName?: string;
|
customName?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
6
upcoming-release-notes/2231.md
Normal file
6
upcoming-release-notes/2231.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
category: Maintenance
|
||||||
|
authors: [carkom]
|
||||||
|
---
|
||||||
|
|
||||||
|
Split out mega-file FiltersMenu.jsx into separate elements and converted them all to Typescript.
|
||||||
Reference in New Issue
Block a user