Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ (TypeScript) fix strictFunctionTypes violations (pt 3) #2070

Merged
merged 8 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 88 additions & 58 deletions packages/desktop-client/src/components/autocomplete/Autocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
};

const inst: { lastChangeType?: StateChangeTypes } = {};

function findItem(strict, suggestions, value) {
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') {
Expand All @@ -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
Expand All @@ -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) => {
Expand Down Expand Up @@ -134,47 +163,47 @@ 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>;
labelProps?: { id?: string };
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,
Expand All @@ -198,7 +227,7 @@ function SingleAutocomplete({
closeOnBlur = true,
value: initialValue,
isMulti = false,
}: SingleAutocompleteProps) {
}: SingleAutocompleteProps<T>) {
const [selectedItem, setSelectedItem] = useState(() =>
findItem(strict, suggestions, initialValue),
);
Expand All @@ -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) : '');
Expand Down Expand Up @@ -527,7 +556,12 @@ function SingleAutocomplete({
);
}

function MultiItem({ name, onRemove }) {
type MultiItemProps = {
name: string;
onRemove: () => void;
};

function MultiItem({ name, onRemove }: MultiItemProps) {
return (
<View
style={{
Expand All @@ -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]);
}

Expand Down Expand Up @@ -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} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -57,7 +58,7 @@ function FilterList({

type SavedFilterAutocompleteProps = {
embedded?: boolean;
} & ComponentProps<typeof Autocomplete>;
} & ComponentProps<typeof Autocomplete<TransactionFilterEntity>>;

export default function SavedFilterAutocomplete({
embedded,
Expand All @@ -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}
Expand Down
3 changes: 2 additions & 1 deletion packages/loot-core/src/client/data-hooks/filters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useMemo } from 'react';

import { type TransactionFilterEntity } from '../../types/models';
import q from '../query-helpers';
import { useLiveQuery } from '../query-hooks';

Expand All @@ -17,7 +18,7 @@ function toJS(rows) {
return filters;
}

export function useFilters() {
export function useFilters(): TransactionFilterEntity[] {
const filters = toJS(
useLiveQuery(() => q('transaction_filters').select('*'), []) || [],
);
Expand Down
1 change: 1 addition & 0 deletions packages/loot-core/src/types/models/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 7 additions & 0 deletions packages/loot-core/src/types/models/transaction-filter.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface TransactionFilterEntity {
id: string;
name: string;
conditions_op: string;
conditions: unknown;
tombstone: boolean;
}
6 changes: 6 additions & 0 deletions upcoming-release-notes/2070.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---

Fixing TypeScript issues when enabling `strictFunctionTypes` (pt.3).
Loading