Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {
useMemo,
useRef,
useState,
type CSSProperties,
} from 'react';
import { ListBox, Section, Header, Collection } from 'react-aria-components';
import { useTranslation } from 'react-i18next';
Expand All @@ -13,6 +14,7 @@ import { setNotificationInset } from 'loot-core/client/actions';
import { groupById, integerToCurrency } from 'loot-core/shared/util';
import * as monthUtils from 'loot-core/src/shared/months';
import { isPreviewId } from 'loot-core/src/shared/transactions';
import { type TransactionEntity } from 'loot-core/types/models/transaction';

import { useAccounts } from '../../../hooks/useAccounts';
import { useCategories } from '../../../hooks/useCategories';
Expand Down Expand Up @@ -40,7 +42,12 @@ import { TransactionListItem } from './TransactionListItem';

const NOTIFICATION_BOTTOM_INSET = 75;

function Loading({ style, 'aria-label': ariaLabel }) {
type LoadingProps = {
style?: CSSProperties;
'aria-label': string;
};

function Loading({ style, 'aria-label': ariaLabel }: LoadingProps) {
return (
<View
aria-label={ariaLabel || 'Loading...'}
Expand All @@ -57,17 +64,29 @@ function Loading({ style, 'aria-label': ariaLabel }) {
);
}

type TransactionListProps = {
isLoading: boolean;
transactions: Readonly<TransactionEntity[]>;
onOpenTransaction?: (transaction: TransactionEntity) => void;
isLoadingMore: boolean;
onLoadMore: () => void;
};

export function TransactionList({
isLoading,
transactions,
onOpenTransaction,
isLoadingMore,
onLoadMore,
}) {
}: TransactionListProps) {
const { t } = useTranslation();
const sections = useMemo(() => {
// Group by date. We can assume transactions is ordered
const sections = [];
const sections: {
id: string;
date: TransactionEntity['date'];
transactions: TransactionEntity[];
}[] = [];
transactions.forEach(transaction => {
if (
sections.length === 0 ||
Expand All @@ -88,13 +107,16 @@ export function TransactionList({
const dispatchSelected = useSelectedDispatch();
const selectedTransactions = useSelectedItems();

const onTransactionPress = useCallback(
const onTransactionPress: (
transaction: TransactionEntity,
isLongPress?: boolean,
) => void = useCallback(
(transaction, isLongPress = false) => {
const isPreview = isPreviewId(transaction.id);
if (!isPreview && (isLongPress || selectedTransactions.size > 0)) {
dispatchSelected({ type: 'select', id: transaction.id });
} else {
onOpenTransaction(transaction);
onOpenTransaction?.(transaction);
}
},
[dispatchSelected, onOpenTransaction, selectedTransactions],
Expand Down Expand Up @@ -185,16 +207,23 @@ export function TransactionList({
);
}

function SelectedTransactionsFloatingActionBar({ transactions, style }) {
type SelectedTransactionsFloatingActionBarProps = {
transactions: Readonly<TransactionEntity[]>;
style?: Parameters<typeof FloatingActionBar>[0]['style'];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
style?: Parameters<typeof FloatingActionBar>[0]['style'];
style?: CSSProperties;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: 56eba2e

};

function SelectedTransactionsFloatingActionBar({
transactions,
style = {},
}: SelectedTransactionsFloatingActionBarProps) {
const editMenuTriggerRef = useRef(null);
const [isEditMenuOpen, setIsEditMenuOpen] = useState(false);
const moreOptionsMenuTriggerRef = useRef(null);
const [isMoreOptionsMenuOpen, setIsMoreOptionsMenuOpen] = useState(false);
const getMenuItemStyle = useCallback(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would also work instead of exporting the type

Suggested change
const getMenuItemStyle = useCallback(
const getMenuItemStyle: ComponentProps<typeof Menu>['getItemStyle'] = useCallback(

Copy link
Contributor Author

@leoltl leoltl Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joel-jeremy Thanks for the second review and suggestion! Unfortunately the proposed change doesn't seem to work. 😢
image

I believe it is because the type of 'NameType' in the first argument of the Menu's 'getItemStyle' is a generic that bounds to the actual Menu instance (i.e. TS resolving the type from the items the Menu instance was passed). Whereas it is unknown type when it is not bounded to the Menu instance.

I am open to other suggestion, but so far setting this anonymous function with generic type is only way I get typing to work/ agree.

item => ({
() => ({
...styles.mobileMenuItem,
color: theme.mobileHeaderText,
...(item.name === 'delete' && { color: theme.errorTextMenu }),
}),
[],
);
Expand Down Expand Up @@ -326,16 +355,18 @@ function SelectedTransactionsFloatingActionBar({ transactions, style }) {
let displayValue = value;
switch (name) {
case 'account':
displayValue = accountsById[value]?.name ?? value;
displayValue =
accountsById[String(value)]?.name ?? value;
Comment on lines +359 to +360
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type safety checks for number conversion.

The number conversion for integerToCurrency could fail if the value is not a valid number. Consider adding validation:

-  displayValue = integerToCurrency(Number(value));
+  const numValue = Number(value);
+  if (Number.isNaN(numValue)) {
+    throw new Error(`Invalid amount value: ${value}`);
+  }
+  displayValue = integerToCurrency(numValue);

Also applies to: 363-364, 367-367, 370-370

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated: c06df65

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leoltl Thank you for implementing the number validation! Your solution to gracefully handle invalid numbers by preserving the original value is even better than throwing an error. The changes look good.

🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Get the specific changes in the commit related to number conversion
git show c06df652dbfb813f0a596cb5b78ea6f672d4099d -- 'packages/desktop-client/src/components/mobile/transactions/TransactionList.tsx'

Length of output: 1269


If you found this review helpful, would you consider giving us a shout-out on X?
Thank you for using CodeRabbit!

break;
case 'category':
displayValue = categoriesById[value]?.name ?? value;
displayValue =
categoriesById[String(value)]?.name ?? value;
break;
case 'payee':
displayValue = payeesById[value]?.name ?? value;
displayValue = payeesById[String(value)]?.name ?? value;
break;
case 'amount':
displayValue = integerToCurrency(value);
displayValue = integerToCurrency(Number(value));
break;
case 'notes':
displayValue = `${mode} with ${value}`;
Expand Down Expand Up @@ -429,7 +460,10 @@ function SelectedTransactionsFloatingActionBar({ transactions, style }) {
style={{ width: 200 }}
>
<Menu
getItemStyle={getMenuItemStyle}
getItemStyle={item => ({
...getMenuItemStyle(),
...(item.name === 'delete' && { color: theme.errorTextMenu }),
})}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The menu in L348 does not have a delete MenuItem, and typescript was complaining it referencing a value which it has no overlap.
image
So I am extracting that conditional out as an inline function.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would the typescript error be fixed if we explicitly type the item parameter on L348 or type the getMenuItemStyle function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have typed the getMenuItemStyle function and seem to made typescript happy
5623c21

style={{ backgroundColor: theme.floatingActionBarBackground }}
onMenuSelect={type => {
if (type === 'duplicate') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const ROW_HEIGHT = 60;
type TransactionListItemProps = ComponentPropsWithoutRef<
typeof ListBoxItem<TransactionEntity>
> & {
isNewTransaction: (transaction: TransactionEntity['id']) => boolean;
isNewTransaction?: (transaction: TransactionEntity['id']) => boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this prop being used anywhere, I am not sure if I should delete it or leave that as an optional

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not used then we can just delete it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deleted: 56eba2e

onPress: (transaction: TransactionEntity) => void;
onLongPress: (transaction: TransactionEntity) => void;
};
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/4063.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [leoltl]
---

Refactoring the mobile TransactionList component to typescript
Loading