mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 20:44:32 -05:00
♻️ (TypeScript) fix strictFunctionTypes violations (pt 3) (#2070)
This commit is contained in:
committed by
GitHub
parent
5854dffd50
commit
03e943f383
@@ -10,7 +10,7 @@ import React, {
|
||||
type ChangeEvent,
|
||||
} from 'react';
|
||||
|
||||
import Downshift from 'downshift';
|
||||
import Downshift, { type StateChangeTypes } from 'downshift';
|
||||
import { css } from 'glamor';
|
||||
|
||||
import Remove from '../../icons/v2/Remove';
|
||||
@@ -20,18 +20,31 @@ import Input from '../common/Input';
|
||||
import View from '../common/View';
|
||||
import { Tooltip } from '../tooltips';
|
||||
|
||||
const inst: { lastChangeType? } = {};
|
||||
type Item = {
|
||||
id?: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function findItem(strict, suggestions, value) {
|
||||
const inst: { lastChangeType?: StateChangeTypes } = {};
|
||||
|
||||
function findItem<T extends Item>(
|
||||
strict: boolean,
|
||||
suggestions: T[],
|
||||
value: T | T['id'],
|
||||
): T | null {
|
||||
if (strict) {
|
||||
const idx = suggestions.findIndex(item => item.id === value);
|
||||
return idx === -1 ? null : suggestions[idx];
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
throw new Error('value can be string only if strict = false');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getItemName(item) {
|
||||
function getItemName(item: null | string | Item): string {
|
||||
if (item == null) {
|
||||
return '';
|
||||
} else if (typeof item === 'string') {
|
||||
@@ -40,24 +53,36 @@ function getItemName(item) {
|
||||
return item.name || '';
|
||||
}
|
||||
|
||||
function getItemId(item) {
|
||||
function getItemId(item: Item | Item['id']) {
|
||||
if (typeof item === 'string') {
|
||||
return item;
|
||||
}
|
||||
return item ? item.id : null;
|
||||
}
|
||||
|
||||
export function defaultFilterSuggestion(suggestion, value) {
|
||||
export function defaultFilterSuggestion<T extends Item>(
|
||||
suggestion: T,
|
||||
value: string,
|
||||
) {
|
||||
return getItemName(suggestion).toLowerCase().includes(value.toLowerCase());
|
||||
}
|
||||
|
||||
function defaultFilterSuggestions(suggestions, value) {
|
||||
function defaultFilterSuggestions<T extends Item>(
|
||||
suggestions: T[],
|
||||
value: string,
|
||||
) {
|
||||
return suggestions.filter(suggestion =>
|
||||
defaultFilterSuggestion(suggestion, value),
|
||||
);
|
||||
}
|
||||
|
||||
function fireUpdate(onUpdate, strict, suggestions, index, value) {
|
||||
function fireUpdate<T extends Item>(
|
||||
onUpdate: ((selected: string | null, value: string) => void) | undefined,
|
||||
strict: boolean,
|
||||
suggestions: T[],
|
||||
index: number,
|
||||
value: string,
|
||||
) {
|
||||
// If the index is null, look up the id in the suggestions. If the
|
||||
// value is empty it will select nothing (as expected). If it's not
|
||||
// empty but nothing is selected, it still resolves to an id. It
|
||||
@@ -82,11 +107,15 @@ function fireUpdate(onUpdate, strict, suggestions, index, value) {
|
||||
onUpdate?.(selected, value);
|
||||
}
|
||||
|
||||
function defaultRenderInput(props) {
|
||||
function defaultRenderInput(props: ComponentProps<typeof Input>) {
|
||||
return <Input {...props} />;
|
||||
}
|
||||
|
||||
function defaultRenderItems(items, getItemProps, highlightedIndex) {
|
||||
function defaultRenderItems<T extends Item>(
|
||||
items: T[],
|
||||
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>,
|
||||
highlightedIndex: number,
|
||||
) {
|
||||
return (
|
||||
<div>
|
||||
{items.map((item, index) => {
|
||||
@@ -134,15 +163,15 @@ function defaultRenderItems(items, getItemProps, highlightedIndex) {
|
||||
);
|
||||
}
|
||||
|
||||
function defaultShouldSaveFromKey(e) {
|
||||
function defaultShouldSaveFromKey(e: KeyboardEvent) {
|
||||
return e.code === 'Enter';
|
||||
}
|
||||
|
||||
function defaultItemToString(item) {
|
||||
function defaultItemToString<T extends Item>(item?: T) {
|
||||
return item ? getItemName(item) : '';
|
||||
}
|
||||
|
||||
type SingleAutocompleteProps = {
|
||||
type SingleAutocompleteProps<T extends Item> = {
|
||||
focused?: boolean;
|
||||
embedded?: boolean;
|
||||
containerProps?: HTMLProps<HTMLDivElement>;
|
||||
@@ -150,31 +179,31 @@ type SingleAutocompleteProps = {
|
||||
inputProps?: Omit<ComponentProps<typeof Input>, 'onChange'> & {
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
suggestions?: unknown[];
|
||||
suggestions?: T[];
|
||||
tooltipStyle?: CSSProperties;
|
||||
tooltipProps?: ComponentProps<typeof Tooltip>;
|
||||
renderInput?: (props: ComponentProps<typeof Input>) => ReactNode;
|
||||
renderItems?: (
|
||||
items,
|
||||
getItemProps: (arg: { item: unknown }) => ComponentProps<typeof View>,
|
||||
items: T[],
|
||||
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>,
|
||||
idx: number,
|
||||
value?: unknown,
|
||||
value?: string,
|
||||
) => ReactNode;
|
||||
itemToString?: (item) => string;
|
||||
itemToString?: (item: T) => string;
|
||||
shouldSaveFromKey?: (e: KeyboardEvent) => boolean;
|
||||
filterSuggestions?: (suggestions, value: string) => unknown[];
|
||||
filterSuggestions?: (suggestions: T[], value: string) => T[];
|
||||
openOnFocus?: boolean;
|
||||
getHighlightedIndex?: (suggestions) => number | null;
|
||||
getHighlightedIndex?: (suggestions: T[]) => number | null;
|
||||
highlightFirst?: boolean;
|
||||
onUpdate?: (id: unknown, value: string) => void;
|
||||
onUpdate?: (id: T['id'], value: string) => void;
|
||||
strict?: boolean;
|
||||
onSelect: (id: unknown, value: string) => void;
|
||||
onSelect: (id: T['id'], value: string) => void;
|
||||
tableBehavior?: boolean;
|
||||
closeOnBlur?: boolean;
|
||||
value: unknown[] | string;
|
||||
value: T | T['id'];
|
||||
isMulti?: boolean;
|
||||
};
|
||||
function SingleAutocomplete({
|
||||
function SingleAutocomplete<T extends Item>({
|
||||
focused,
|
||||
embedded = false,
|
||||
containerProps,
|
||||
@@ -198,7 +227,7 @@ function SingleAutocomplete({
|
||||
closeOnBlur = true,
|
||||
value: initialValue,
|
||||
isMulti = false,
|
||||
}: SingleAutocompleteProps) {
|
||||
}: SingleAutocompleteProps<T>) {
|
||||
const [selectedItem, setSelectedItem] = useState(() =>
|
||||
findItem(strict, suggestions, initialValue),
|
||||
);
|
||||
@@ -220,9 +249,9 @@ function SingleAutocomplete({
|
||||
setSelectedItem(findItem(strict, suggestions, initialValue));
|
||||
}, [initialValue, suggestions, strict]);
|
||||
|
||||
function resetState(newValue) {
|
||||
function resetState(newValue?: string) {
|
||||
const val = newValue === undefined ? initialValue : newValue;
|
||||
const selectedItem = findItem(strict, suggestions, val);
|
||||
const selectedItem = findItem<T>(strict, suggestions, val);
|
||||
|
||||
setSelectedItem(selectedItem);
|
||||
setValue(selectedItem ? getItemName(selectedItem) : '');
|
||||
@@ -527,7 +556,12 @@ function SingleAutocomplete({
|
||||
);
|
||||
}
|
||||
|
||||
function MultiItem({ name, onRemove }) {
|
||||
type MultiItemProps = {
|
||||
name: string;
|
||||
onRemove: () => void;
|
||||
};
|
||||
|
||||
function MultiItem({ name, onRemove }: MultiItemProps) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
@@ -547,41 +581,44 @@ function MultiItem({ name, onRemove }) {
|
||||
);
|
||||
}
|
||||
|
||||
type MultiAutocompleteProps = Omit<
|
||||
SingleAutocompleteProps,
|
||||
'value' | 'onSelect'
|
||||
> & {
|
||||
value: unknown[];
|
||||
onSelect: (ids: unknown[], id?: string) => void;
|
||||
type MultiAutocompleteProps<
|
||||
T extends Item,
|
||||
Value = SingleAutocompleteProps<T>['value'],
|
||||
> = Omit<SingleAutocompleteProps<T>, 'value' | 'onSelect'> & {
|
||||
value: Value[];
|
||||
onSelect: (ids: Value[], id?: string) => void;
|
||||
};
|
||||
function MultiAutocomplete({
|
||||
function MultiAutocomplete<T extends Item>({
|
||||
value: selectedItems,
|
||||
onSelect,
|
||||
suggestions,
|
||||
strict,
|
||||
...props
|
||||
}: MultiAutocompleteProps) {
|
||||
}: MultiAutocompleteProps<T>) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const lastSelectedItems = useRef<unknown[]>();
|
||||
const lastSelectedItems = useRef<typeof selectedItems>();
|
||||
|
||||
useEffect(() => {
|
||||
lastSelectedItems.current = selectedItems;
|
||||
});
|
||||
|
||||
function onRemoveItem(id) {
|
||||
function onRemoveItem(id: (typeof selectedItems)[0]) {
|
||||
const items = selectedItems.filter(i => i !== id);
|
||||
onSelect(items);
|
||||
}
|
||||
|
||||
function onAddItem(id) {
|
||||
function onAddItem(id: string) {
|
||||
if (id) {
|
||||
id = id.trim();
|
||||
onSelect([...selectedItems, id], id);
|
||||
}
|
||||
}
|
||||
|
||||
function onKeyDown(e, prevOnKeyDown) {
|
||||
if (e.key === 'Backspace' && e.target.value === '') {
|
||||
function onKeyDown(
|
||||
e: KeyboardEvent<HTMLInputElement>,
|
||||
prevOnKeyDown?: ComponentProps<typeof Input>['onKeyDown'],
|
||||
) {
|
||||
if (e.key === 'Backspace' && e.currentTarget.value === '') {
|
||||
onRemoveItem(selectedItems[selectedItems.length - 1]);
|
||||
}
|
||||
|
||||
@@ -680,31 +717,24 @@ export function AutocompleteFooter({
|
||||
);
|
||||
}
|
||||
|
||||
type AutocompleteProps =
|
||||
| ComponentProps<typeof SingleAutocomplete>
|
||||
| ComponentProps<typeof MultiAutocomplete>;
|
||||
type AutocompleteProps<T extends Item> =
|
||||
| ComponentProps<typeof SingleAutocomplete<T>>
|
||||
| ComponentProps<typeof MultiAutocomplete<T>>;
|
||||
|
||||
function isMultiAutocomplete(
|
||||
props: AutocompleteProps,
|
||||
function isMultiAutocomplete<T extends Item>(
|
||||
_props: AutocompleteProps<T>,
|
||||
multi?: boolean,
|
||||
): props is ComponentProps<typeof MultiAutocomplete> {
|
||||
): _props is ComponentProps<typeof MultiAutocomplete<T>> {
|
||||
return multi;
|
||||
}
|
||||
|
||||
function isSingleAutocomplete(
|
||||
props: AutocompleteProps,
|
||||
multi?: boolean,
|
||||
): props is ComponentProps<typeof SingleAutocomplete> {
|
||||
return !multi;
|
||||
}
|
||||
|
||||
export default function Autocomplete({
|
||||
export default function Autocomplete<T extends Item>({
|
||||
multi,
|
||||
...props
|
||||
}: AutocompleteProps & { multi?: boolean }) {
|
||||
}: AutocompleteProps<T> & { multi?: boolean }) {
|
||||
if (isMultiAutocomplete(props, multi)) {
|
||||
return <MultiAutocomplete {...props} />;
|
||||
} else if (isSingleAutocomplete(props, multi)) {
|
||||
return <SingleAutocomplete {...props} />;
|
||||
}
|
||||
|
||||
return <SingleAutocomplete {...props} />;
|
||||
}
|
||||
|
||||
@@ -308,6 +308,7 @@ export default function PayeeAutocomplete({
|
||||
|
||||
const isf = filtered.length > 100;
|
||||
filtered = filtered.slice(0, 100);
|
||||
// @ts-expect-error TODO: solve this somehow
|
||||
filtered.filtered = isf;
|
||||
|
||||
if (filtered.length >= 2 && filtered[0].id === 'new') {
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import React, { type ComponentProps } from 'react';
|
||||
|
||||
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
|
||||
import { type TransactionFilterEntity } from 'loot-core/src/types/models';
|
||||
|
||||
import { theme } from '../../style';
|
||||
import View from '../common/View';
|
||||
|
||||
import Autocomplete from './Autocomplete';
|
||||
|
||||
type FilterListProps = {
|
||||
items: { id: string; name: string }[];
|
||||
getItemProps: (arg: { item: unknown }) => ComponentProps<typeof View>;
|
||||
type FilterListProps<T> = {
|
||||
items: T[];
|
||||
getItemProps: (arg: { item: T }) => ComponentProps<typeof View>;
|
||||
highlightedIndex: number;
|
||||
embedded?: boolean;
|
||||
};
|
||||
|
||||
function FilterList({
|
||||
function FilterList<T extends { id: string; name: string }>({
|
||||
items,
|
||||
getItemProps,
|
||||
highlightedIndex,
|
||||
embedded,
|
||||
}: FilterListProps) {
|
||||
}: FilterListProps<T>) {
|
||||
return (
|
||||
<View>
|
||||
<View
|
||||
@@ -57,7 +58,7 @@ function FilterList({
|
||||
|
||||
type SavedFilterAutocompleteProps = {
|
||||
embedded?: boolean;
|
||||
} & ComponentProps<typeof Autocomplete>;
|
||||
} & ComponentProps<typeof Autocomplete<TransactionFilterEntity>>;
|
||||
|
||||
export default function SavedFilterAutocomplete({
|
||||
embedded,
|
||||
@@ -73,6 +74,7 @@ export default function SavedFilterAutocomplete({
|
||||
suggestions={filters}
|
||||
renderItems={(items, getItemProps, highlightedIndex) => (
|
||||
<FilterList
|
||||
// @ts-expect-error This issue will go away when `strictFunctionTypes` is enabled
|
||||
items={items}
|
||||
getItemProps={getItemProps}
|
||||
highlightedIndex={highlightedIndex}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { type TransactionFilterEntity } from '../../types/models';
|
||||
import q from '../query-helpers';
|
||||
import { useLiveQuery } from '../query-hooks';
|
||||
|
||||
@@ -17,7 +18,7 @@ function toJS(rows) {
|
||||
return filters;
|
||||
}
|
||||
|
||||
export function useFilters() {
|
||||
export function useFilters(): TransactionFilterEntity[] {
|
||||
const filters = toJS(
|
||||
useLiveQuery(() => q('transaction_filters').select('*'), []) || [],
|
||||
);
|
||||
|
||||
@@ -6,3 +6,4 @@ export type * from './payee';
|
||||
export type * from './rule';
|
||||
export type * from './schedule';
|
||||
export type * from './transaction';
|
||||
export type * from './transaction-filter';
|
||||
|
||||
7
packages/loot-core/src/types/models/transaction-filter.d.ts
vendored
Normal file
7
packages/loot-core/src/types/models/transaction-filter.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface TransactionFilterEntity {
|
||||
id: string;
|
||||
name: string;
|
||||
conditions_op: string;
|
||||
conditions: unknown;
|
||||
tombstone: boolean;
|
||||
}
|
||||
6
upcoming-release-notes/2070.md
Normal file
6
upcoming-release-notes/2070.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Fixing TypeScript issues when enabling `strictFunctionTypes` (pt.3).
|
||||
Reference in New Issue
Block a user