mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-30 01:40:02 -05:00
Convert FiltersMenu to TypeScript (#6091)
This commit is contained in:
committed by
GitHub
parent
717a7db9e7
commit
f41386b753
@@ -30,7 +30,8 @@ import {
|
||||
FIELD_TYPES,
|
||||
getValidOps,
|
||||
} from 'loot-core/shared/rules';
|
||||
import { titleFirst } from 'loot-core/shared/util';
|
||||
import { type IntegerAmount, titleFirst } from 'loot-core/shared/util';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models';
|
||||
|
||||
import { CompactFiltersButton } from './CompactFiltersButton';
|
||||
import { FiltersButton } from './FiltersButton';
|
||||
@@ -45,6 +46,18 @@ import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
||||
import { useFormat } from '@desktop-client/hooks/useFormat';
|
||||
import { useTransactionFilters } from '@desktop-client/hooks/useTransactionFilters';
|
||||
|
||||
type FilterReducerState<T extends RuleConditionEntity> = Pick<
|
||||
T,
|
||||
'value' | 'op' | 'field'
|
||||
>;
|
||||
type FilterReducerAction =
|
||||
| { type: 'close' }
|
||||
| Parameters<typeof updateFilterReducer>[1];
|
||||
|
||||
type AmountInputRef = HTMLInputElement & {
|
||||
getCurrentAmount: () => IntegerAmount;
|
||||
};
|
||||
|
||||
let isDatepickerClick = false;
|
||||
|
||||
const filterFields = [
|
||||
@@ -60,19 +73,27 @@ const filterFields = [
|
||||
'transfer',
|
||||
].map(field => [field, mapField(field)]);
|
||||
|
||||
function ConfigureField({
|
||||
type ConfigureFieldProps<T extends RuleConditionEntity> =
|
||||
FilterReducerState<T> &
|
||||
Pick<T, 'options'> & {
|
||||
initialSubfield?: string;
|
||||
dispatch: (action: FilterReducerAction) => void;
|
||||
onApply: (cond: T) => void;
|
||||
};
|
||||
|
||||
function ConfigureField<T extends RuleConditionEntity>({
|
||||
field,
|
||||
initialSubfield = field,
|
||||
op,
|
||||
value,
|
||||
dispatch,
|
||||
onApply,
|
||||
}) {
|
||||
}: ConfigureFieldProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const format = useFormat();
|
||||
const [subfield, setSubfield] = useState(initialSubfield);
|
||||
const inputRef = useRef();
|
||||
const prevOp = useRef(null);
|
||||
const inputRef = useRef<AmountInputRef>(null);
|
||||
const prevOp = useRef<T['op'] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevOp.current !== op && inputRef.current) {
|
||||
@@ -94,6 +115,7 @@ function ConfigureField({
|
||||
if (
|
||||
field === 'date' &&
|
||||
subfield === 'month' &&
|
||||
typeof value === 'string' &&
|
||||
/^\d{4}-\d{2}$/.test(value)
|
||||
) {
|
||||
const [year, month] = value.split('-');
|
||||
@@ -115,13 +137,11 @@ function ConfigureField({
|
||||
['amount-inflow', t('Amount (inflow)')],
|
||||
['amount-outflow', t('Amount (outflow)')],
|
||||
]
|
||||
: field === 'date'
|
||||
? [
|
||||
['date', t('Date')],
|
||||
['month', t('Month')],
|
||||
['year', t('Year')],
|
||||
]
|
||||
: null
|
||||
: [
|
||||
['date', t('Date')],
|
||||
['month', t('Month')],
|
||||
['year', t('Year')],
|
||||
]
|
||||
}
|
||||
value={subfield}
|
||||
onChange={sub => {
|
||||
@@ -229,6 +249,7 @@ function ConfigureField({
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error - fix me
|
||||
onApply({
|
||||
field,
|
||||
op,
|
||||
@@ -240,7 +261,9 @@ function ConfigureField({
|
||||
{type !== 'boolean' && field !== 'payee' && (
|
||||
<GenericInput
|
||||
ref={inputRef}
|
||||
// @ts-expect-error - fix me
|
||||
field={field === 'date' ? subfield : field}
|
||||
// @ts-expect-error - fix me
|
||||
type={
|
||||
type === 'id' &&
|
||||
(op === 'contains' ||
|
||||
@@ -251,14 +274,17 @@ function ConfigureField({
|
||||
: type
|
||||
}
|
||||
numberFormatType="currency"
|
||||
// @ts-expect-error - fix me
|
||||
value={
|
||||
formattedValue ?? (op === 'oneOf' || op === 'notOneOf' ? [] : '')
|
||||
}
|
||||
// @ts-expect-error - fix me
|
||||
multi={op === 'oneOf' || op === 'notOneOf'}
|
||||
op={op}
|
||||
options={subfieldToOptions(field, subfield)}
|
||||
style={{ marginTop: 10 }}
|
||||
onChange={v => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onChange={(v: any) => {
|
||||
dispatch({ type: 'set-value', value: v });
|
||||
}}
|
||||
/>
|
||||
@@ -266,7 +292,9 @@ function ConfigureField({
|
||||
|
||||
{field === 'payee' && (
|
||||
<PayeeFilter
|
||||
// @ts-expect-error - fix me
|
||||
value={formattedValue}
|
||||
// @ts-expect-error - fix me
|
||||
op={op}
|
||||
onChange={v => dispatch({ type: 'set-value', value: v })}
|
||||
/>
|
||||
@@ -289,11 +317,22 @@ function ConfigureField({
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterButton({ onApply, compact, hover, exclude }) {
|
||||
type FilterButtonProps<T extends RuleConditionEntity> = {
|
||||
onApply: (cond: T) => void;
|
||||
compact: boolean;
|
||||
hover: boolean;
|
||||
exclude?: string[];
|
||||
};
|
||||
|
||||
export function FilterButton<T extends RuleConditionEntity>({
|
||||
onApply,
|
||||
compact,
|
||||
hover,
|
||||
exclude,
|
||||
}: FilterButtonProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const filters = useTransactionFilters();
|
||||
const triggerRef = useRef(null);
|
||||
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
||||
|
||||
const translatedFilterFields = useMemo(() => {
|
||||
@@ -309,7 +348,17 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
|
||||
}, []);
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
(state, action) => {
|
||||
// @ts-expect-error - fix me
|
||||
(
|
||||
state: FilterReducerState<T> & {
|
||||
fieldsOpen: boolean;
|
||||
condOpen: boolean;
|
||||
},
|
||||
action:
|
||||
| FilterReducerAction
|
||||
| { type: 'select-field' }
|
||||
| { type: 'configure'; field: string },
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case 'select-field':
|
||||
return { ...state, fieldsOpen: true, condOpen: false };
|
||||
@@ -335,12 +384,14 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
|
||||
{ fieldsOpen: false, condOpen: false, field: null, value: null },
|
||||
);
|
||||
|
||||
async function onValidateAndApply(cond) {
|
||||
async function onValidateAndApply(cond: T) {
|
||||
// @ts-expect-error - fix me
|
||||
cond = unparse({ ...cond, type: FIELD_TYPES.get(cond.field) });
|
||||
|
||||
if (cond.type === 'date' && cond.options) {
|
||||
if (cond.options.month) {
|
||||
const date = parseDate(
|
||||
// @ts-expect-error - fix me
|
||||
cond.value,
|
||||
getMonthYearFormat(dateFormat),
|
||||
new Date(),
|
||||
@@ -352,6 +403,7 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
|
||||
return;
|
||||
}
|
||||
} else if (cond.options.year) {
|
||||
// @ts-expect-error - fix me
|
||||
const date = parseDate(cond.value, 'yyyy', new Date());
|
||||
if (isDateValid(date)) {
|
||||
cond.value = formatDate(date, 'yyyy');
|
||||
@@ -363,11 +415,12 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
|
||||
}
|
||||
|
||||
const { error } =
|
||||
cond.field !== 'saved' &&
|
||||
(await send('rule-validate', {
|
||||
conditions: [cond],
|
||||
actions: [],
|
||||
}));
|
||||
cond.field === 'saved'
|
||||
? { error: null }
|
||||
: await send('rule-validate', {
|
||||
conditions: [cond],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const saved = filters.find(f => cond.value === f.id);
|
||||
|
||||
@@ -375,6 +428,7 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
|
||||
const field = titleFirst(mapField(cond.field));
|
||||
alert(field + ': ' + getFieldError(error.conditionErrors[0]));
|
||||
} else {
|
||||
// @ts-expect-error - fix me
|
||||
onApply(saved ? saved : cond);
|
||||
dispatch({ type: 'close' });
|
||||
}
|
||||
@@ -444,6 +498,7 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
|
||||
// Datepicker selections for some reason register 2x clicks
|
||||
// We want to keep the popover open after selecting a date.
|
||||
// So we ignore the "close" event on selection + the subsequent event.
|
||||
// @ts-expect-error - fix me
|
||||
if (element.dataset.pikaYear) {
|
||||
isDatepickerClick = true;
|
||||
return false;
|
||||
@@ -472,12 +527,25 @@ export function FilterButton({ onApply, compact, hover, exclude }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function FilterEditor({ field, op, value, options, onSave, onClose }) {
|
||||
type FilterEditorProps<T extends RuleConditionEntity> = FilterReducerState<T> &
|
||||
Pick<T, 'options'> & {
|
||||
onSave: (cond: T) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function FilterEditor<T extends RuleConditionEntity>({
|
||||
field,
|
||||
op,
|
||||
value,
|
||||
options,
|
||||
onSave,
|
||||
onClose,
|
||||
}: FilterEditorProps<T>) {
|
||||
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
(state, action) => {
|
||||
(state: FilterReducerState<T>, action: FilterReducerAction) => {
|
||||
switch (action.type) {
|
||||
case 'close':
|
||||
onClose();
|
||||
@@ -486,7 +554,7 @@ export function FilterEditor({ field, op, value, options, onSave, onClose }) {
|
||||
return updateFilterReducer(state, action);
|
||||
}
|
||||
},
|
||||
{ field, op, value, options },
|
||||
{ field, op, value },
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -495,13 +563,18 @@ export function FilterEditor({ field, op, value, options, onSave, onClose }) {
|
||||
initialSubfield={subfieldFromFilter({ field, options, value })}
|
||||
op={state.op}
|
||||
value={state.value}
|
||||
options={state.options}
|
||||
options={options}
|
||||
dispatch={dispatch}
|
||||
onApply={cond => {
|
||||
// @ts-expect-error - fix me
|
||||
cond = unparse({ ...cond, type: FIELD_TYPES.get(cond.field) });
|
||||
|
||||
if (cond.type === 'date' && cond.options) {
|
||||
if (cond.options.month && !/\d{4}-\d{2}/.test(cond.value)) {
|
||||
if (
|
||||
cond.options.month &&
|
||||
typeof cond.value === 'string' &&
|
||||
!/\d{4}-\d{2}/.test(cond.value)
|
||||
) {
|
||||
const date = parseDate(
|
||||
cond.value,
|
||||
getMonthYearFormat(dateFormat),
|
||||
@@ -514,6 +587,7 @@ export function FilterEditor({ field, op, value, options, onSave, onClose }) {
|
||||
return;
|
||||
}
|
||||
} else if (cond.options.year) {
|
||||
// @ts-expect-error - fix me
|
||||
const date = parseDate(cond.value, 'yyyy', new Date());
|
||||
if (isDateValid(date)) {
|
||||
cond.value = formatDate(date, 'yyyy');
|
||||
@@ -1,17 +1,16 @@
|
||||
import { makeValue, FIELD_TYPES } from 'loot-core/shared/rules';
|
||||
import { type RuleConditionEntity } from 'loot-core/types/models';
|
||||
|
||||
export function updateFilterReducer(
|
||||
state: Pick<RuleConditionEntity, 'op' | 'field' | 'value'>,
|
||||
action: { type: 'set-op' | 'set-value' } & Pick<
|
||||
RuleConditionEntity,
|
||||
'op' | 'value'
|
||||
>,
|
||||
export function updateFilterReducer<T extends RuleConditionEntity>(
|
||||
state: Pick<T, 'op' | 'field'> & { value: T['value'] | null },
|
||||
action:
|
||||
| { type: 'set-op'; op: T['op'] }
|
||||
| { type: 'set-value'; value: T['value'] },
|
||||
) {
|
||||
switch (action.type) {
|
||||
case 'set-op': {
|
||||
const type = FIELD_TYPES.get(state.field);
|
||||
let value: RuleConditionEntity['value'] | null = state.value;
|
||||
let value = state.value;
|
||||
if (
|
||||
(type === 'id' || type === 'string') &&
|
||||
(action.op === 'contains' ||
|
||||
@@ -36,6 +35,7 @@ export function updateFilterReducer(
|
||||
return { ...state, value };
|
||||
}
|
||||
default:
|
||||
// @ts-expect-error - fix me
|
||||
throw new Error(`Unhandled action type: ${action.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,7 +851,6 @@ function QueryItem({
|
||||
compact={false}
|
||||
onApply={filters.onApply}
|
||||
hover={false}
|
||||
exclude={undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -41,9 +41,7 @@ type HeaderProps = {
|
||||
end: TimeFrame['end'],
|
||||
mode: TimeFrame['mode'],
|
||||
) => void;
|
||||
filters?: RuleConditionEntity[];
|
||||
conditionsOp: 'and' | 'or';
|
||||
onApply?: (conditions: RuleConditionEntity) => void;
|
||||
onUpdateFilter: ComponentProps<typeof AppliedFilters>['onUpdate'];
|
||||
onDeleteFilter: ComponentProps<typeof AppliedFilters>['onDelete'];
|
||||
onConditionsOpChange: ComponentProps<
|
||||
@@ -51,7 +49,16 @@ type HeaderProps = {
|
||||
>['onConditionsOpChange'];
|
||||
children?: ReactNode;
|
||||
inlineContent?: ReactNode;
|
||||
};
|
||||
} & (
|
||||
| {
|
||||
filters: RuleConditionEntity[];
|
||||
onApply: (conditions: RuleConditionEntity) => void;
|
||||
}
|
||||
| {
|
||||
filters?: never;
|
||||
onApply?: never;
|
||||
}
|
||||
);
|
||||
|
||||
export function Header({
|
||||
start,
|
||||
@@ -263,7 +270,6 @@ export function Header({
|
||||
compact={isNarrowWidth}
|
||||
onApply={onApply}
|
||||
hover={false}
|
||||
exclude={undefined}
|
||||
/>
|
||||
)}
|
||||
</SpaceBetween>
|
||||
|
||||
@@ -346,7 +346,6 @@ function SummaryInner({ widget }: SummaryInnerProps) {
|
||||
firstDayOfWeekIdx={firstDayOfWeekIdx}
|
||||
mode={mode}
|
||||
onChangeDates={onChangeDates}
|
||||
onApply={dividendFilters.onApply}
|
||||
onUpdateFilter={dividendFilters.onUpdate}
|
||||
onDeleteFilter={dividendFilters.onDelete}
|
||||
conditionsOp={dividendFilters.conditionsOp}
|
||||
@@ -644,7 +643,6 @@ function SumWithRange({
|
||||
compact={false}
|
||||
onApply={filterObject.onApply}
|
||||
hover={false}
|
||||
exclude={undefined}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -56,7 +56,7 @@ type FieldInfoConstraint = Record<
|
||||
{
|
||||
type: keyof typeof TYPE_INFO;
|
||||
disallowedOps?: Set<RuleConditionOp>;
|
||||
internalOps?: Set<RuleConditionOp>;
|
||||
internalOps?: Set<RuleConditionOp | 'and'>;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -103,7 +103,7 @@ export function isValidOp(field: keyof FieldValueTypes, op: RuleConditionOp) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getValidOps(field: keyof FieldValueTypes) {
|
||||
export function getValidOps(field: keyof FieldValueTypes): RuleConditionOp[] {
|
||||
const type = FIELD_TYPES.get(field);
|
||||
if (!type) {
|
||||
return [];
|
||||
|
||||
@@ -12,24 +12,7 @@ export interface RuleEntity extends NewRuleEntity {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type RuleConditionOp =
|
||||
| 'is'
|
||||
| 'isNot'
|
||||
| 'oneOf'
|
||||
| 'notOneOf'
|
||||
| 'isapprox'
|
||||
| 'isbetween'
|
||||
| 'gt'
|
||||
| 'gte'
|
||||
| 'lt'
|
||||
| 'lte'
|
||||
| 'contains'
|
||||
| 'doesNotContain'
|
||||
| 'hasTags'
|
||||
| 'and'
|
||||
| 'matches'
|
||||
| 'onBudget'
|
||||
| 'offBudget';
|
||||
export type RuleConditionOp = RuleConditionEntity['op'];
|
||||
|
||||
export type FieldValueTypes = {
|
||||
account: string;
|
||||
|
||||
6
upcoming-release-notes/6091.md
Normal file
6
upcoming-release-notes/6091.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
TypeScript: refactor FiltersMenu to TS
|
||||
Reference in New Issue
Block a user