Skip to content

Commit

Permalink
Merge branch 'master' into server-offline
Browse files Browse the repository at this point in the history
  • Loading branch information
psybers authored Sep 20, 2024
2 parents f72c132 + e6bf6da commit 372dc3d
Show file tree
Hide file tree
Showing 42 changed files with 510 additions and 366 deletions.
42 changes: 42 additions & 0 deletions packages/desktop-client/e2e/accounts.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { join } from 'path';

import { test, expect } from '@playwright/test';

import { ConfigurationPage } from './page-models/configuration-page';
Expand Down Expand Up @@ -99,4 +101,44 @@ test.describe('Accounts', () => {
await expect(transaction.account).toHaveText('Ally Savings');
});
});

test.describe('Import Transactions', () => {
test.beforeEach(async () => {
accountPage = await navigation.createAccount({
name: 'CSV import',
offBudget: false,
balance: 0,
});
});

test.afterEach(async () => {
const close = await accountPage.clickCloseAccount();
await close.selectTransferAccount('Vanguard 401k');
await close.forceCloseAccount();
});

async function importCsv(screenshot = false) {
const fileChooserPromise = page.waitForEvent('filechooser');
await accountPage.page.getByRole('button', { name: 'Import' }).click();

const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));

if (screenshot) await expect(page).toMatchThemeScreenshots();

await accountPage.page
.getByRole('button', { name: /Import \d+ transactions/ })
.click();
}

test('imports transactions from a CSV file', async () => {
await importCsv(true);
});

test('import csv file twice', async () => {
await importCsv(false);
await page.waitForTimeout(1000);
await importCsv(true);
});
});
});
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions packages/desktop-client/e2e/data/test.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Date,Payee,Notes,Category,Amount
2024-08-02,Deposit,test 1,Income,1787.76
2024-07-02,Deposit,test 2,Income,1787.76
2024-06-02,Deposit,test 3,Income,1787.76
2024-05-02,Deposit,test 4,Income,1787.76
2024-04-02,Deposit,test 5,Income,1787.76
2024-03-02,Deposit,test 6,Income,1787.76
2024-02-02,Deposit,test 7,Income,1787.76
2024-01-02,Starting Balance,test 8,Starting Balances,-330000
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,8 @@ export class CloseAccountModal {
async closeAccount() {
await this.page.getByRole('button', { name: 'Close account' }).click();
}

async forceCloseAccount() {
await this.page.getByLabel('Force close').click();
}
}
3 changes: 2 additions & 1 deletion packages/desktop-client/src/components/Modals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ export function Modals() {
<TransferModal
key={name}
title={options.title}
categoryId={options.categoryId}
month={options.month}
amount={options.amount}
onSubmit={options.onSubmit}
Expand All @@ -509,9 +510,9 @@ export function Modals() {
<CoverModal
key={name}
title={options.title}
categoryId={options.categoryId}
month={options.month}
showToBeBudgeted={options.showToBeBudgeted}
category={options.category}
onSubmit={options.onSubmit}
/>
);
Expand Down
48 changes: 25 additions & 23 deletions packages/desktop-client/src/components/Notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import React, {
} from 'react';
import { useDispatch, useSelector } from 'react-redux';

import { css } from 'glamor';

import { removeNotification } from 'loot-core/client/actions';
import { type State } from 'loot-core/src/client/state-types';
import type { NotificationWithId } from 'loot-core/src/client/state-types/notifications';
Expand Down Expand Up @@ -200,29 +202,29 @@ function Notification({
onRemove();
setLoading(false);
}}
style={({ isHovered, isPressed }) => ({
backgroundColor: 'transparent',
border: `1px solid ${
positive
? theme.noticeBorder
: error
? theme.errorBorder
: theme.warningBorder
}`,
color: 'currentColor',
...styles.mediumText,
flexShrink: 0,
...(isHovered || isPressed
? {
backgroundColor: positive
? theme.noticeBackground
: error
? theme.errorBackground
: theme.warningBackground,
}
: {}),
...narrowStyle,
})}
className={String(
css({
backgroundColor: 'transparent',
border: `1px solid ${
positive
? theme.noticeBorder
: error
? theme.errorBorder
: theme.warningBorder
}`,
color: 'currentColor',
...styles.mediumText,
flexShrink: 0,
'&[data-hovered], &[data-pressed]': {
backgroundColor: positive
? theme.noticeBackground
: error
? theme.errorBackground
: theme.warningBackground,
},
...narrowStyle,
}),
)}
>
{button.title}
</ButtonWithLoading>
Expand Down
34 changes: 19 additions & 15 deletions packages/desktop-client/src/components/Titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { useState, useEffect, type SetStateAction } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Routes, Route, useLocation } from 'react-router-dom';

import { css } from 'glamor';

import * as Platform from 'loot-core/src/client/platform';
import * as queries from 'loot-core/src/client/queries';
import { listen } from 'loot-core/src/platform/client/fetch';
Expand Down Expand Up @@ -206,21 +208,23 @@ function SyncButton({
<Button
variant="bare"
aria-label="Sync"
style={({ isHovered, isPressed }) => ({
...(isMobile
? {
...style,
WebkitAppRegion: 'none',
...mobileIconStyle,
}
: {
...style,
WebkitAppRegion: 'none',
color: desktopColor,
}),
...(isHovered ? hoveredStyle : {}),
...(isPressed ? activeStyle : {}),
})}
className={String(
css({
...(isMobile
? {
...style,
WebkitAppRegion: 'none',
...mobileIconStyle,
}
: {
...style,
WebkitAppRegion: 'none',
color: desktopColor,
}),
'&[data-hovered]': hoveredStyle,
'&[data-pressed]': activeStyle,
}),
)}
onPress={sync}
>
{isMobile ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import React, { useState } from 'react';

import { runQuery } from 'loot-core/client/query-helpers';
import { send } from 'loot-core/platform/client/fetch';
import { q } from 'loot-core/shared/query';
import { rolloverBudget } from 'loot-core/src/client/queries';
import * as monthUtils from 'loot-core/src/shared/months';
import { groupById, integerToCurrency } from 'loot-core/src/shared/util';
import { type CategoryEntity } from 'loot-core/types/models';
import { type WithRequired } from 'loot-core/types/util';

import { useCategories } from '../../../hooks/useCategories';

import { BalanceMenu } from './BalanceMenu';
import { CoverMenu } from './CoverMenu';
Expand All @@ -34,8 +25,6 @@ export function BalanceMovementMenu({
);
const [menu, setMenu] = useState('menu');

const { addBudgetTransferNotes } = useBudgetTransferNotes({ month });

return (
<>
{menu === 'menu' && (
Expand All @@ -55,6 +44,7 @@ export function BalanceMovementMenu({

{menu === 'transfer' && (
<TransferMenu
categoryId={categoryId}
initialAmount={catBalance}
showToBeBudgeted={true}
onClose={onClose}
Expand All @@ -64,18 +54,13 @@ export function BalanceMovementMenu({
from: categoryId,
to: toCategoryId,
});
addBudgetTransferNotes({
fromCategoryId: categoryId,
toCategoryId,
amount,
});
}}
/>
)}

{menu === 'cover' && (
<CoverMenu
category={categoryId}
categoryId={categoryId}
onClose={onClose}
onSubmit={fromCategoryId => {
onBudgetAction(month, 'cover-overspending', {
Expand All @@ -88,50 +73,3 @@ export function BalanceMovementMenu({
</>
);
}

const useBudgetTransferNotes = ({ month }: { month: string }) => {
const { list: categories } = useCategories();
const categoriesById = useMemo(() => {
return groupById(categories as WithRequired<CategoryEntity, 'id'>[]);
}, [categories]);

const getNotes = async (id: string) => {
const { data: notes } = await runQuery(
q('notes').filter({ id }).select('note'),
);
return (notes && notes[0]?.note) ?? '';
};

const addNewLine = (notes?: string) => `${notes}${notes && '\n'}`;

const addBudgetTransferNotes = useCallback(
async ({
fromCategoryId,
toCategoryId,
amount,
}: {
fromCategoryId: Required<CategoryEntity['id']>;
toCategoryId: Required<CategoryEntity['id']>;
amount: number;
}) => {
const displayAmount = integerToCurrency(amount);

const monthBudgetNotesId = `budget-${month}`;
const existingMonthBudgetNotes = addNewLine(
await getNotes(monthBudgetNotesId),
);

const displayDay = monthUtils.format(monthUtils.currentDate(), 'MMMM dd');
const fromCategoryName = categoriesById[fromCategoryId || ''].name;
const toCategoryName = categoriesById[toCategoryId || ''].name;

await send('notes-save', {
id: monthBudgetNotesId,
note: `${existingMonthBudgetNotes}- Reassigned ${displayAmount} from ${fromCategoryName} to ${toCategoryName} on ${displayDay}`,
});
},
[categoriesById, month],
);

return { addBudgetTransferNotes };
};
Original file line number Diff line number Diff line change
@@ -1,61 +1,44 @@
import React, { useMemo, useState } from 'react';

import {
type CategoryGroupEntity,
type CategoryEntity,
} from 'loot-core/src/types/models';
import { type CategoryEntity } from 'loot-core/src/types/models';

import { useCategories } from '../../../hooks/useCategories';
import { CategoryAutocomplete } from '../../autocomplete/CategoryAutocomplete';
import { Button } from '../../common/Button2';
import { InitialFocus } from '../../common/InitialFocus';
import { View } from '../../common/View';
import { addToBeBudgetedGroup } from '../util';

function removeSelectedCategory(
categoryGroups: CategoryGroupEntity[],
category?: CategoryEntity['id'],
) {
if (!category) return categoryGroups;

return categoryGroups
.map(group => ({
...group,
categories: group.categories?.filter(cat => cat.id !== category),
}))
.filter(group => group.categories?.length);
}
import { addToBeBudgetedGroup, removeCategoriesFromGroups } from '../util';

type CoverMenuProps = {
showToBeBudgeted?: boolean;
category?: CategoryEntity['id'];
onSubmit: (categoryId: string) => void;
categoryId?: CategoryEntity['id'];
onSubmit: (categoryId: CategoryEntity['id']) => void;
onClose: () => void;
};

export function CoverMenu({
showToBeBudgeted = true,
category,
categoryId,
onSubmit,
onClose,
}: CoverMenuProps) {
const { grouped: originalCategoryGroups } = useCategories();
const expenseGroups = originalCategoryGroups.filter(g => !g.is_income);

const categoryGroups = showToBeBudgeted
? addToBeBudgetedGroup(expenseGroups)
: expenseGroups;
const [fromCategoryId, setFromCategoryId] = useState<string | null>(null);

const [categoryId, setCategoryId] = useState<string | null>(null);

const filteredCategoryGroups = useMemo(
() => removeSelectedCategory(categoryGroups, category),
[categoryGroups, category],
);
const filteredCategoryGroups = useMemo(() => {
const expenseGroups = originalCategoryGroups.filter(g => !g.is_income);
const categoryGroups = showToBeBudgeted
? addToBeBudgetedGroup(expenseGroups)
: expenseGroups;
return categoryId
? removeCategoriesFromGroups(categoryGroups, categoryId)
: categoryGroups;
}, [categoryId, showToBeBudgeted, originalCategoryGroups]);

function submit() {
if (categoryId) {
onSubmit(categoryId);
if (fromCategoryId) {
onSubmit(fromCategoryId);
}
onClose();
}
Expand All @@ -67,9 +50,9 @@ export function CoverMenu({
{node => (
<CategoryAutocomplete
categoryGroups={filteredCategoryGroups}
value={categoryGroups.find(g => g.id === categoryId) ?? null}
value={null}
openOnFocus={true}
onSelect={(id: string | undefined) => setCategoryId(id || null)}
onSelect={(id: string | undefined) => setFromCategoryId(id || null)}
inputProps={{
inputRef: node,
onEnter: event => !event.defaultPrevented && submit(),
Expand Down
Loading

0 comments on commit 372dc3d

Please sign in to comment.