diff --git a/packages/desktop-client/e2e/transactions.test.ts b/packages/desktop-client/e2e/transactions.test.ts index 1517e9a520..120e2f1547 100644 --- a/packages/desktop-client/e2e/transactions.test.ts +++ b/packages/desktop-client/e2e/transactions.test.ts @@ -62,26 +62,56 @@ test.describe('Transactions', () => { const autocomplete = page.getByTestId('autocomplete'); await expect(autocomplete).toMatchThemeScreenshots(); + // Ensure that autocomplete filters properly + await page.keyboard.type('C'); + await expect(autocomplete).toMatchThemeScreenshots(); + // Select the active item await page.getByTestId('Clothing-category-item').click(); await filterTooltip.applyButton.click(); // Assert that there are only clothing transactions - await expect(accountPage.getNthTransaction(0).category).toHaveText( - 'Clothing', - ); - await expect(accountPage.getNthTransaction(1).category).toHaveText( - 'Clothing', - ); - await expect(accountPage.getNthTransaction(2).category).toHaveText( - 'Clothing', - ); - await expect(accountPage.getNthTransaction(3).category).toHaveText( - 'Clothing', - ); - await expect(accountPage.getNthTransaction(4).category).toHaveText( - 'Clothing', - ); + for (let i = 0; i < 5; i++) { + await expect(accountPage.getNthTransaction(i).category).toHaveText( + 'Clothing', + ); + } + await expect(page).toMatchThemeScreenshots(); + }); + + test('by category group', async () => { + // Use Capital One Checking because it has transactions that aren't just Clothing + accountPage = await navigation.goToAccountPage('Capital One Checking'); + + const filterTooltip = await accountPage.filterBy('Category'); + + await filterTooltip.locator + .getByRole('button', { name: 'Category', exact: true }) + .click(); + await page + .getByRole('button', { name: 'Category group', exact: true }) + .click(); + + await expect(filterTooltip.locator).toMatchThemeScreenshots(); + + // Type in the autocomplete box + const autocomplete = page.getByTestId('autocomplete'); + await expect(autocomplete).toMatchThemeScreenshots(); + + // Ensure that autocomplete filters properly + await page.keyboard.type('U'); + await expect(autocomplete).toMatchThemeScreenshots(); + + // Select the active item + await page.getByTestId('Usual Expenses-category-group-item').click(); + await filterTooltip.applyButton.click(); + + // Assert that there are only transactions with categories in the Usual Expenses group + for (let i = 0; i < 5; i++) { + await expect(accountPage.getNthTransaction(i).category).toHaveText( + /^(Savings|Medical|Gift|General|Clothing|Entertainment|Restaurants|Food)$/, + ); + } await expect(page).toMatchThemeScreenshots(); }); diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-1-chromium-linux.png index e64deb902c..4879b75a7c 100644 Binary files a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-1-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-10-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-10-chromium-linux.png new file mode 100644 index 0000000000..0c2feecdf1 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-10-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-11-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-11-chromium-linux.png new file mode 100644 index 0000000000..65047d5662 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-11-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-12-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-12-chromium-linux.png new file mode 100644 index 0000000000..18011cb585 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-12-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-2-chromium-linux.png index c8535a23a8..57a4880049 100644 Binary files a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-2-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-3-chromium-linux.png index e59f942f33..b2ac55d12b 100644 Binary files a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-3-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png index 69f6389f5f..83c424f46b 100644 Binary files a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-7-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png index 490fa1a300..23f6789ebd 100644 Binary files a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-8-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png index c7769ec55b..f16d50a89c 100644 Binary files a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-9-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-1-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-1-chromium-linux.png new file mode 100644 index 0000000000..6cb6ea2338 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-10-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-10-chromium-linux.png new file mode 100644 index 0000000000..c09e24b44a Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-10-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-11-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-11-chromium-linux.png new file mode 100644 index 0000000000..d7c39fc78b Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-11-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-12-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-12-chromium-linux.png new file mode 100644 index 0000000000..46db12c856 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-12-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-2-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-2-chromium-linux.png new file mode 100644 index 0000000000..7b38278bf7 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-3-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-3-chromium-linux.png new file mode 100644 index 0000000000..b8918ab6d0 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-4-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-4-chromium-linux.png new file mode 100644 index 0000000000..20fa7eb018 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-5-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-5-chromium-linux.png new file mode 100644 index 0000000000..76254c2986 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-5-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-6-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-6-chromium-linux.png new file mode 100644 index 0000000000..fb3f9f4b66 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-6-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-7-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-7-chromium-linux.png new file mode 100644 index 0000000000..2c53b5889d Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-7-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-8-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-8-chromium-linux.png new file mode 100644 index 0000000000..5898d6bf27 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-8-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-9-chromium-linux.png b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-9-chromium-linux.png new file mode 100644 index 0000000000..0170862c02 Binary files /dev/null and b/packages/desktop-client/e2e/transactions.test.ts-snapshots/Transactions-filters-transactions-by-category-group-9-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index f73223f057..856279616f 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -11,6 +11,7 @@ import { AccountMenuModal } from './modals/AccountMenuModal'; import { BudgetAutomationsModal } from './modals/BudgetAutomationsModal'; import { BudgetPageMenuModal } from './modals/BudgetPageMenuModal'; import { CategoryAutocompleteModal } from './modals/CategoryAutocompleteModal'; +import { CategoryGroupAutocompleteModal } from './modals/CategoryGroupAutocompleteModal'; import { CategoryGroupMenuModal } from './modals/CategoryGroupMenuModal'; import { CategoryMenuModal } from './modals/CategoryMenuModal'; import { CloseAccountModal } from './modals/CloseAccountModal'; @@ -206,6 +207,11 @@ export function Modals() { case 'category-autocomplete': return ; + case 'category-group-autocomplete': + return ( + + ); + case 'account-autocomplete': return ; diff --git a/packages/desktop-client/src/components/autocomplete/CategoryGroupAutocomplete.tsx b/packages/desktop-client/src/components/autocomplete/CategoryGroupAutocomplete.tsx new file mode 100644 index 0000000000..44354a8190 --- /dev/null +++ b/packages/desktop-client/src/components/autocomplete/CategoryGroupAutocomplete.tsx @@ -0,0 +1,219 @@ +import React, { Fragment, useMemo } from 'react'; +import type { + ComponentProps, + ComponentPropsWithoutRef, + CSSProperties, + ReactElement, + ReactNode, +} from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useResponsive } from '@actual-app/components/hooks/useResponsive'; +import { styles } from '@actual-app/components/styles'; +import { TextOneLine } from '@actual-app/components/text-one-line'; +import { theme } from '@actual-app/components/theme'; +import { View } from '@actual-app/components/view'; +import { css, cx } from '@emotion/css'; + +import type { CategoryGroupEntity } from 'loot-core/types/models'; + +import { Autocomplete } from './Autocomplete'; + +import { useCategories } from '@desktop-client/hooks/useCategories'; + +type CategoryGroupAutocompleteItem = CategoryGroupEntity; + +type CategoryGroupListProps = { + items: CategoryGroupAutocompleteItem[]; + getItemProps?: (arg: { + item: CategoryGroupAutocompleteItem; + }) => Partial>; + highlightedIndex: number; + embedded?: boolean; + footer?: ReactNode; + renderCategoryGroupItem?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + showHiddenItems?: boolean; +}; + +function CategoryGroupList({ + items, + getItemProps, + highlightedIndex, + embedded, + footer, + renderCategoryGroupItem = defaultRenderCategoryItem, + showHiddenItems, +}: CategoryGroupListProps) { + const categoryGroups = useMemo(() => { + return items.reduce< + (CategoryGroupAutocompleteItem & { highlightedIndex: number })[] + >((acc, item, index) => { + const itemWithIndex = { + ...item, + highlightedIndex: index, + }; + + acc.push(itemWithIndex); + + return acc; + }, []); + }, [items]); + + return ( + + + {categoryGroups.map(item => ( + + {renderCategoryGroupItem({ + ...(getItemProps ? getItemProps({ item }) : {}), + item, + highlighted: highlightedIndex === item.highlightedIndex, + embedded, + style: { + ...(showHiddenItems && + item.hidden && { + color: theme.pageTextSubdued, + }), + }, + })} + + ))} + + {footer} + + ); +} + +type CategoryGroupAutocompleteProps = ComponentProps< + typeof Autocomplete +> & { + categoryGroups?: Array; + renderCategoryGroupItem?: ( + props: ComponentPropsWithoutRef, + ) => ReactElement; + showHiddenCategories?: boolean; +}; + +export function CategoryGroupAutocomplete({ + categoryGroups, + embedded, + closeOnBlur, + renderCategoryGroupItem, + showHiddenCategories, + ...props +}: CategoryGroupAutocompleteProps) { + const { + data: { grouped: defaultCategoryGroups } = { + grouped: [], + }, + } = useCategories(); + + const categoryGroupSuggestions: CategoryGroupAutocompleteItem[] = + useMemo(() => { + const allSuggestions = categoryGroups || defaultCategoryGroups; + + if (!showHiddenCategories) { + return allSuggestions.filter(suggestion => !suggestion.hidden); + } + + return allSuggestions; + }, [categoryGroups, defaultCategoryGroups, showHiddenCategories]); + + return ( + ( + + )} + {...props} + /> + ); +} + +type CategoryGroupItemProps = { + item: CategoryGroupAutocompleteItem; + className?: string; + style?: CSSProperties; + highlighted?: boolean; + embedded?: boolean; + showBalances?: boolean; +}; + +function CategoryGroupItem({ + item, + className, + style, + highlighted, + embedded, + ...props +}: CategoryGroupItemProps) { + const { t } = useTranslation(); + const { isNarrowWidth } = useResponsive(); + const narrowStyle = isNarrowWidth + ? { + ...styles.mobileMenuItem, + borderRadius: 0, + borderTop: `1px solid ${theme.pillBorder}`, + } + : {}; + + return ( + + ); +} + +function defaultRenderCategoryItem( + props: ComponentPropsWithoutRef, +): ReactElement { + return ; +} diff --git a/packages/desktop-client/src/components/filters/FiltersMenu.tsx b/packages/desktop-client/src/components/filters/FiltersMenu.tsx index 7aa1316e91..05a8337fba 100644 --- a/packages/desktop-client/src/components/filters/FiltersMenu.tsx +++ b/packages/desktop-client/src/components/filters/FiltersMenu.tsx @@ -82,8 +82,8 @@ type ConfigureFieldProps = }; function ConfigureField({ - field, - initialSubfield = field, + field: initialField, + initialSubfield = initialField, op, value, dispatch, @@ -92,9 +92,11 @@ function ConfigureField({ const { t } = useTranslation(); const format = useFormat(); const dateFormat = useDateFormat() || 'MM/dd/yyyy'; + const field = initialField === 'category_group' ? 'category' : initialField; const [subfield, setSubfield] = useState(initialSubfield); const inputRef = useRef(null); const prevOp = useRef(null); + const prevSubfield = useRef(null); useEffect(() => { if (prevOp.current !== op && inputRef.current) { @@ -103,6 +105,13 @@ function ConfigureField({ prevOp.current = op; }, [op]); + useEffect(() => { + if (prevSubfield.current !== subfield && inputRef.current) { + inputRef.current.focus(); + } + prevSubfield.current = subfield; + }, [subfield]); + const type = FIELD_TYPES.get(field); let ops = getValidOps(field).filter(op => op !== 'isbetween'); @@ -131,25 +140,42 @@ function ConfigureField({ const isPayeeIdOp = (op: T['op']) => ['is', 'is not', 'one of', 'not one of'].includes(op); + const subfieldSelectOptions = ( + field: 'amount' | 'date' | 'category', + ): Array => { + switch (field) { + case 'amount': + return [ + ['amount', t('Amount')], + ['amount-inflow', t('Amount (inflow)')], + ['amount-outflow', t('Amount (outflow)')], + ]; + + case 'date': + return [ + ['date', t('Date')], + ['month', t('Month')], + ['year', t('Year')], + ]; + + case 'category': + return [ + ['category', t('Category')], + ['category_group', t('Category group')], + ]; + + default: + return []; + } + }; + return ( - {field === 'amount' || field === 'date' ? ( + {field === 'amount' || field === 'date' || field === 'category' ? (