Compare commits

..

2 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
38368224d3 Release notes 2024-01-21 03:27:31 -08:00
Joel Jeremy Marquez
f964552b5b Update package versions 2024-01-21 03:26:22 -08:00
166 changed files with 1869 additions and 1018 deletions

View File

@@ -31,7 +31,7 @@ jobs:
needs: netlify
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.41.1-jammy
image: mcr.microsoft.com/playwright:v1.37.0-jammy
steps:
- uses: actions/checkout@v3
- name: Set up environment
@@ -51,7 +51,7 @@ jobs:
needs: netlify
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.41.1-jammy
image: mcr.microsoft.com/playwright:v1.37.0-jammy
steps:
- uses: actions/checkout@v3
- name: Set up environment

View File

@@ -43,20 +43,20 @@
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^8.37.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-react-app": "7.0.1",
"eslint-import-resolver-typescript": "3.5.5",
"eslint-plugin-import": "2.27.5",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-react": "7.32.2",
"eslint-plugin-react": "7.33.2",
"eslint-plugin-rulesdir": "^0.2.2",
"node-jq": "^4.0.1",
"npm-run-all": "^4.1.3",
"node-jq": "^4.2.2",
"npm-run-all": "^4.1.5",
"prettier": "3.2.4",
"react-refresh": "^0.14.0",
"source-map-support": "^0.5.21",
"typescript": "^5.0.2",
"typescript": "^5.3.3",
"typescript-strict-plugin": "^2.2.2-beta.2"
},
"engines": {

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "6.5.0",
"version": "6.4.0",
"license": "MIT",
"description": "An API for Actual",
"engines": {
@@ -21,18 +21,18 @@
"clean": "rm -rf dist @types"
},
"dependencies": {
"better-sqlite3": "^9.2.2",
"better-sqlite3": "^9.3.0",
"compare-versions": "^6.1.0",
"node-fetch": "^3.3.2",
"uuid": "^9.0.0"
"uuid": "^9.0.1"
},
"devDependencies": {
"@swc/core": "^1.3.105",
"@swc/jest": "^0.2.31",
"@types/jest": "^27.5.0",
"@types/uuid": "^9.0.2",
"jest": "^27.0.0",
"@types/jest": "^27.5.2",
"@types/uuid": "^9.0.7",
"jest": "^27.5.1",
"tsc-alias": "^1.8.8",
"typescript": "^5.0.2"
"typescript": "^5.3.3"
}
}

View File

@@ -4,7 +4,8 @@
// Using ES2021 because thats the newest version where
// the latest Node 16.x release supports all of the features
"target": "ES2021",
"module": "CommonJS",
"module": "Node16",
"moduleResolution": "Node16",
"noEmit": false,
"declaration": true,
"outDir": "dist",

View File

@@ -15,17 +15,17 @@
"test": "jest -c jest.config.js"
},
"dependencies": {
"google-protobuf": "^3.12.0-rc.1",
"google-protobuf": "^3.21.2",
"murmurhash": "^2.0.1",
"uuid": "^9.0.0"
"uuid": "^9.0.1"
},
"devDependencies": {
"@swc/core": "^1.3.105",
"@swc/jest": "^0.2.31",
"@types/jest": "^27.5.0",
"@types/uuid": "^9.0.2",
"jest": "^27.0.0",
"@types/jest": "^27.5.2",
"@types/uuid": "^9.0.7",
"jest": "^27.5.1",
"ts-protoc-gen": "^0.15.0",
"typescript": "^5.0.2"
"typescript": "^5.3.3"
}
}

View File

@@ -4,7 +4,8 @@
// Using ES2021 because thats the newest version where
// the latest Node 16.x release supports all of the features
"target": "ES2021",
"module": "CommonJS",
"module": "Node16",
"moduleResolution": "Node16",
"noEmit": false,
"declaration": true,
"strict": true,

View File

@@ -32,27 +32,26 @@ Prerequisites:
#### Running against the local server
First start a dev instance:
First start the dev server:
```sh
HTTPS=true yarn start
```
Note the network IP address and port the dev instance is listening on.
Next, navigate to the root of your project folder, run the standartised docker container, and launch the visual regression tests from within it.
```sh
# Run docker container
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.37.1-jammy /bin/bash
# If you receive an error such as "docker: invalid reference format", please instead use the following command:
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.37.1-jammy /bin/bash
# Run the VRT tests: important - they MUST be ran against a HTTPS server. Use the ip and port noted earlier
E2E_START_URL=https://ip:port yarn vrt
# Run the VRT tests: important - they MUST be ran against a HTTPS server
E2E_START_URL=https://192.168.0.178:3001 yarn vrt
# To update snapshots, use the following command:
E2E_START_URL=https://ip:port yarn vrt --update-snapshots
E2E_START_URL=https://192.168.0.178:3001 yarn vrt --update-snapshots
```
#### Running against a remote server

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

@@ -49,7 +49,7 @@ test.describe('Mobile', () => {
test('opens the accounts page and asserts on balances', async () => {
const accountsPage = await navigation.goToAccountsPage();
const account = await accountsPage.getNthAccount(1);
const account = await accountsPage.getNthAccount(0);
await expect(account.name).toHaveText('Ally Savings');
await expect(account.balance).toHaveText('7,653.00');
@@ -58,7 +58,7 @@ test.describe('Mobile', () => {
test('opens individual account page and checks that filtering is working', async () => {
const accountsPage = await navigation.goToAccountsPage();
const accountPage = await accountsPage.openNthAccount(0);
const accountPage = await accountsPage.openNthAccount(1);
await expect(accountPage.heading).toHaveText('Bank of America');
expect(await accountPage.getBalance()).toBeGreaterThan(0);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -1,13 +1,13 @@
{
"name": "@actual-app/web",
"version": "24.2.0",
"version": "24.1.0",
"license": "MIT",
"files": [
"build"
],
"devDependencies": {
"@juggle/resize-observer": "^3.1.2",
"@playwright/test": "^1.41.1",
"@playwright/test": "^1.37.1",
"@reach/listbox": "^0.18.0",
"@react-aria/focus": "^3.14.0",
"@react-aria/listbox": "^3.10.1",
@@ -16,8 +16,8 @@
"@react-stately/list": "^3.9.1",
"@rollup/plugin-inject": "^5.0.5",
"@svgr/cli": "^8.0.1",
"@swc/core": "^1.3.105",
"@swc/helpers": "^0.5.3",
"@swc/core": "^1.3.82",
"@swc/helpers": "^0.5.1",
"@swc/plugin-react-remove-properties": "^1.5.108",
"@testing-library/react": "14.0.0",
"@testing-library/user-event": "14.4.3",
@@ -26,9 +26,9 @@
"@types/react-modal": "^3.16.0",
"@types/react-redux": "^7.1.25",
"@types/uuid": "^9.0.2",
"@types/webpack-bundle-analyzer": "^4.6.3",
"@types/webpack-bundle-analyzer": "^4.6.0",
"@use-gesture/react": "^10.3.0",
"@vitejs/plugin-basic-ssl": "^1.1.0",
"@vitejs/plugin-basic-ssl": "^1.0.2",
"@vitejs/plugin-react-swc": "^3.5.0",
"chokidar": "^3.5.3",
"cross-env": "^7.0.3",
@@ -65,13 +65,12 @@
"sass": "^1.63.6",
"swc-loader": "^0.2.3",
"terser-webpack-plugin": "^5.3.9",
"typescript": "^5.0.2",
"uuid": "^9.0.0",
"victory": "^36.6.8",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.1",
"vitest": "^1.2.1",
"webpack-bundle-analyzer": "^4.10.1",
"vite": "^5.0.10",
"vite-tsconfig-paths": "^4.2.2",
"vitest": "^1.0.4",
"webpack-bundle-analyzer": "^4.9.1",
"xml2js": "^0.6.2"
},
"scripts": {

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import React, {
createContext,
useState,
@@ -60,14 +61,7 @@ export type TitlebarContextValue = {
subscribe: (listener: Listener) => () => void;
};
export const TitlebarContext = createContext<TitlebarContextValue>({
sendEvent() {
throw new Error('TitlebarContext not initialized');
},
subscribe() {
throw new Error('TitlebarContext not initialized');
},
});
export const TitlebarContext = createContext<TitlebarContextValue>(null);
type TitlebarProviderProps = {
children?: ReactNode;
@@ -94,32 +88,26 @@ export function TitlebarProvider({ children }: TitlebarProviderProps) {
}
function UncategorizedButton() {
const count: number | null = useSheetValue(queries.uncategorizedCount());
if (count === null || count <= 0) {
return null;
}
const count = useSheetValue(queries.uncategorizedCount());
return (
<Link
variant="button"
type="bare"
to="/accounts/uncategorized"
style={{
color: theme.errorText,
}}
>
{count} uncategorized {count === 1 ? 'transaction' : 'transactions'}
</Link>
count !== 0 && (
<Link
variant="button"
type="bare"
to="/accounts/uncategorized"
style={{
color: theme.errorText,
}}
>
{count} uncategorized {count === 1 ? 'transaction' : 'transactions'}
</Link>
)
);
}
type PrivacyButtonProps = {
style?: CSSProperties;
};
function PrivacyButton({ style }: PrivacyButtonProps) {
function PrivacyButton({ style }) {
const isPrivacyEnabled = useSelector(
state => state.prefs.local?.isPrivacyEnabled,
state => state.prefs.local.isPrivacyEnabled,
);
const { savePrefs } = useActions();
@@ -146,13 +134,11 @@ type SyncButtonProps = {
isMobile?: boolean;
};
function SyncButton({ style, isMobile = false }: SyncButtonProps) {
const cloudFileId = useSelector(state => state.prefs.local?.cloudFileId);
const cloudFileId = useSelector(state => state.prefs.local.cloudFileId);
const { sync } = useActions();
const [syncing, setSyncing] = useState(false);
const [syncState, setSyncState] = useState<
null | 'offline' | 'local' | 'disabled' | 'error'
>(null);
const [syncState, setSyncState] = useState(null);
useEffect(() => {
const unlisten = listen('sync-event', ({ type, subtype, syncDisabled }) => {
@@ -286,8 +272,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
}
function BudgetTitlebar() {
const maxMonths = useSelector(state => state.prefs.global?.maxMonths);
const budgetType = useSelector(state => state.prefs.local?.budgetType);
const maxMonths = useSelector(state => state.prefs.global.maxMonths);
const budgetType = useSelector(state => state.prefs.local.budgetType);
const { saveGlobalPrefs } = useActions();
const { sendEvent } = useContext(TitlebarContext);
@@ -380,18 +366,14 @@ function BudgetTitlebar() {
);
}
type TitlebarProps = {
style?: CSSProperties;
};
export function Titlebar({ style }: TitlebarProps) {
export function Titlebar({ style }) {
const navigate = useNavigate();
const location = useLocation();
const sidebar = useSidebar();
const { isNarrowWidth } = useResponsive();
const serverURL = useServerURL();
const floatingSidebar = useSelector(
state => state.prefs.global?.floatingSidebar,
state => state.prefs.global.floatingSidebar,
);
return isNarrowWidth ? null : (

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import React from 'react';
import { useSelector } from 'react-redux';
@@ -10,6 +11,14 @@ import { LinkButton } from './common/LinkButton';
import { Text } from './common/Text';
import { View } from './common/View';
function closeNotification(setAppState) {
// Set a flag to never show an update notification again for this session
setAppState({
updateInfo: null,
showUpdateNotification: false,
});
}
export function UpdateNotification() {
const updateInfo = useSelector(state => state.app.updateInfo);
const showUpdateNotification = useSelector(
@@ -59,7 +68,7 @@ export function UpdateNotification() {
textDecoration: 'underline',
}}
onClick={() =>
window.Actual?.openURLInBrowser(
window.Actual.openURLInBrowser(
'https://actualbudget.org/docs/releases',
)
}
@@ -71,13 +80,7 @@ export function UpdateNotification() {
type="bare"
aria-label="Close"
style={{ display: 'inline', padding: '1px 7px 2px 7px' }}
onClick={() => {
// Set a flag to never show an update notification again for this session
setAppState({
updateInfo: null,
showUpdateNotification: false,
});
}}
onClick={() => closeNotification(setAppState)}
>
<SvgClose
width={9}

View File

@@ -83,14 +83,11 @@ export function AccountHeader({
const [menuOpen, setMenuOpen] = useState(false);
const searchInput = useRef(null);
const splitsExpanded = useSplitsExpanded();
const syncServerStatus = useSyncServerStatus();
const isUsingServer = syncServerStatus !== 'no-server';
const isServerOffline = syncServerStatus === 'offline';
let canSync = account && account.account_id && isUsingServer;
let canSync = account && account.account_id;
if (!account) {
// All accounts - check for any syncable account
canSync = !!accounts.find(account => !!account.account_id) && isUsingServer;
canSync = !!accounts.find(account => !!account.account_id);
}
function onToggleSplits() {
@@ -213,11 +210,7 @@ export function AccountHeader({
style={{ marginTop: 12 }}
>
{((account && !account.closed) || canSync) && (
<Button
type="bare"
onClick={canSync ? onSync : onImport}
disabled={canSync && isServerOffline}
>
<Button type="bare" onClick={canSync ? onSync : onImport}>
{canSync ? (
<>
<AnimatedRefresh
@@ -229,7 +222,7 @@ export function AccountHeader({
}
style={{ marginRight: 4 }}
/>{' '}
{isServerOffline ? 'Sync offline' : 'Sync'}
Sync
</>
) : (
<>

View File

@@ -199,11 +199,11 @@ export function AccountDetails({
</View>
<PullToRefresh onRefresh={onRefresh}>
<TransactionList
account={account}
transactions={allTransactions}
categories={categories}
accounts={accounts}
payees={payees}
showCategory={!account.offbudget}
isNew={isNewTransaction}
onLoadMore={onLoadMore}
onSelect={onSelectTransaction}

View File

@@ -691,7 +691,7 @@ function MultiAutocomplete<T extends Item>({
type AutocompleteFooterProps = {
show?: boolean;
embedded?: boolean;
embedded: boolean;
children: ReactNode;
};
export function AutocompleteFooter({
@@ -699,20 +699,18 @@ export function AutocompleteFooter({
embedded,
children,
}: AutocompleteFooterProps) {
if (!show) {
return null;
}
return (
<View
style={{
flexShrink: 0,
...(embedded ? { paddingTop: 5 } : { padding: 5 }),
}}
onMouseDown={e => e.preventDefault()}
>
{children}
</View>
show && (
<View
style={{
flexShrink: 0,
...(embedded ? { paddingTop: 5 } : { padding: 5 }),
}}
onMouseDown={e => e.preventDefault()}
>
{children}
</View>
)
);
}

View File

@@ -1,3 +1,4 @@
// @ts-strict-ignore
import React, {
type ComponentProps,
Fragment,
@@ -24,11 +25,9 @@ import { Autocomplete, defaultFilterSuggestion } from './Autocomplete';
export type CategoryListProps = {
items: Array<CategoryEntity & { group?: CategoryGroupEntity }>;
getItemProps?: (arg: {
item: CategoryEntity;
}) => Partial<ComponentProps<typeof View>>;
getItemProps?: (arg: { item }) => Partial<ComponentProps<typeof View>>;
highlightedIndex: number;
embedded?: boolean;
embedded: boolean;
footer?: ReactNode;
renderSplitTransactionButton?: (
props: SplitTransactionButtonProps,
@@ -48,7 +47,7 @@ function CategoryList({
renderCategoryItemGroupHeader = defaultRenderCategoryItemGroupHeader,
renderCategoryItem = defaultRenderCategoryItem,
}: CategoryListProps) {
let lastGroup: string | undefined | null = null;
let lastGroup = null;
return (
<View>
@@ -73,10 +72,10 @@ function CategoryList({
lastGroup = item.cat_group;
return (
<Fragment key={item.id}>
{showGroup && item.group?.name && (
<Fragment key={item.group.name}>
{showGroup && (
<Fragment key={item.group?.name}>
{renderCategoryItemGroupHeader({
title: item.group.name,
title: item.group?.name,
})}
</Fragment>
)}
@@ -126,7 +125,7 @@ export function CategoryAutocomplete({
categoryGroups.reduce(
(list, group) =>
list.concat(
(group.categories || [])
group.categories
.filter(category => category.cat_group === group.id)
.map(category => ({
...category,
@@ -215,7 +214,8 @@ type SplitTransactionButtonProps = {
style?: CSSProperties;
};
function SplitTransactionButton({
// eslint-disable-next-line import/no-unused-modules
export function SplitTransactionButton({
Icon,
highlighted,
embedded,

View File

@@ -22,6 +22,9 @@ import { SyncRefresh } from '../SyncRefresh';
import { BudgetTable } from './MobileBudgetTable';
import { prewarmMonth, switchBudgetType } from './util';
const CATEGORY_BUDGET_EDIT_ACTION = 'category-budget';
const BALANCE_MENU_OPEN_ACTION = 'balance-menu';
type BudgetInnerProps = {
categories: CategoryEntity[];
categoryGroups: CategoryGroupEntity[];
@@ -73,6 +76,8 @@ function BudgetInner(props: BudgetInnerProps) {
const [currentMonth, setCurrentMonth] = useState(currMonth);
const [initialized, setInitialized] = useState(false);
const [editMode, setEditMode] = useState(false);
const [editingBudgetCategoryId, setEditingBudgetCategoryId] = useState(null);
const [openBalanceActionMenuId, setOpenBalanceActionMenuId] = useState(null);
useEffect(() => {
async function init() {
@@ -355,6 +360,29 @@ function BudgetInner(props: BudgetInnerProps) {
});
};
const onEditCategoryBudget = id => {
onEdit(CATEGORY_BUDGET_EDIT_ACTION, id);
};
const onOpenBalanceActionMenu = id => {
onEdit(BALANCE_MENU_OPEN_ACTION, id);
};
const onEdit = (action, id) => {
// Do not allow editing if another field is currently being edited.
// Cancel the currently editing field in that case.
const currentlyEditing = editingBudgetCategoryId || openBalanceActionMenuId;
setEditingBudgetCategoryId(
action === CATEGORY_BUDGET_EDIT_ACTION && !currentlyEditing ? id : null,
);
setOpenBalanceActionMenuId(
action === BALANCE_MENU_OPEN_ACTION && !currentlyEditing ? id : null,
);
return { action, editingId: !currentlyEditing ? id : null };
};
const numberFormat = prefs?.numberFormat || 'comma-dot';
const hideFraction = prefs?.hideFraction || false;
@@ -410,6 +438,10 @@ function BudgetInner(props: BudgetInnerProps) {
pushModal={pushModal}
onEditGroup={onEditGroup}
onEditCategory={onEditCategory}
editingBudgetCategoryId={editingBudgetCategoryId}
onEditCategoryBudget={onEditCategoryBudget}
openBalanceActionMenuId={openBalanceActionMenuId}
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
/>
)}
</SyncRefresh>

View File

@@ -1,4 +1,4 @@
import React, { memo, useRef, useState } from 'react';
import React, { memo, useEffect, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import memoizeOne from 'memoize-one';
@@ -7,10 +7,6 @@ import { rolloverBudget, reportBudget } from 'loot-core/src/client/queries';
import * as monthUtils from 'loot-core/src/shared/months';
import { useFeatureFlag } from '../../hooks/useFeatureFlag';
import {
SingleActiveEditFormProvider,
useSingleActiveEditForm,
} from '../../hooks/useSingleActiveEditForm';
import {
SvgArrowThinLeft,
SvgArrowThinRight,
@@ -138,7 +134,6 @@ function BudgetCell({
month,
onBudgetAction,
onEdit,
onBlur,
isEditing,
}) {
const sheetValue = useSheetValue(binding);
@@ -151,7 +146,7 @@ function BudgetCell({
}
function onAmountClick() {
onEdit?.();
onEdit?.(categoryId);
}
return (
@@ -167,7 +162,7 @@ function BudgetCell({
focused={isEditing}
textStyle={{ ...styles.smallText, ...textStyle }}
onUpdate={updateBudgetAmount}
onBlur={onBlur}
onBlur={() => onEdit?.(null)}
/>
<View
role="button"
@@ -249,6 +244,10 @@ const ExpenseCategory = memo(function ExpenseCategory({
style,
month,
onEdit,
isEditingBudget,
onEditBudget,
isBalanceActionMenuOpen,
onOpenBalanceActionMenu,
onBudgetAction,
show3Cols,
showBudgetedCol,
@@ -256,22 +255,11 @@ const ExpenseCategory = memo(function ExpenseCategory({
const opacity = blank ? 0 : 1;
const balanceTooltip = useTooltip();
const [isEditingBudget, setIsEditingBudget] = useState(false);
const { onRequestActiveEdit, onClearActiveEdit } = useSingleActiveEditForm();
const onEditBudget = () => {
onRequestActiveEdit(`${category.id}-budget`, () => {
setIsEditingBudget(true);
return () => setIsEditingBudget(false);
});
};
const onOpenBalanceActionMenu = () => {
onRequestActiveEdit(`${category.id}-balance`, () => {
useEffect(() => {
if (isBalanceActionMenuOpen) {
balanceTooltip.open();
return () => balanceTooltip.close();
});
};
}
}, [isBalanceActionMenuOpen, balanceTooltip]);
const listItemRef = useRef();
@@ -329,7 +317,6 @@ const ExpenseCategory = memo(function ExpenseCategory({
onBudgetAction={onBudgetAction}
isEditing={isEditingBudget}
onEdit={onEditBudget}
onBlur={onClearActiveEdit}
/>
<View
style={{
@@ -362,7 +349,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
>
<span
role="button"
onPointerUp={() => onOpenBalanceActionMenu?.()}
onPointerUp={() => onOpenBalanceActionMenu?.(category.id)}
onPointerDown={e => e.preventDefault()}
>
<BalanceWithCarryover
@@ -384,7 +371,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
monthIndex={monthUtils.getMonthIndex(month)}
onBudgetAction={_onBudgetAction}
onClose={() => {
onClearActiveEdit();
onOpenBalanceActionMenu?.(null);
}}
/>
) : (
@@ -395,7 +382,7 @@ const ExpenseCategory = memo(function ExpenseCategory({
monthIndex={monthUtils.getMonthIndex(month)}
onBudgetAction={_onBudgetAction}
onClose={() => {
onClearActiveEdit();
onOpenBalanceActionMenu?.(null);
}}
/>
))}
@@ -808,6 +795,10 @@ const ExpenseGroup = memo(function ExpenseGroup({
editMode,
onEditGroup,
onEditCategory,
editingBudgetCategoryId,
onEditCategoryBudget,
openBalanceActionMenuId,
onOpenBalanceActionMenu,
// gestures,
month,
// onReorderCategory,
@@ -882,6 +873,10 @@ const ExpenseGroup = memo(function ExpenseGroup({
{group.categories
.filter(category => !category.hidden || showHiddenCategories)
.map((category, index) => {
const isEditingCategoryBudget =
editingBudgetCategoryId === category.id;
const isBalanceActionMenuOpen =
openBalanceActionMenuId === category.id;
return (
<ExpenseCategory
key={category.id}
@@ -920,6 +915,10 @@ const ExpenseGroup = memo(function ExpenseGroup({
showBudgetedCol={showBudgetedCol}
editMode={editMode}
onEdit={onEditCategory}
isEditingBudget={isEditingCategoryBudget}
onEditBudget={onEditCategoryBudget}
isBalanceActionMenuOpen={isBalanceActionMenuOpen}
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
// gestures={gestures}
month={month}
// onReorder={onReorderCategory}
@@ -1020,6 +1019,10 @@ function BudgetGroups({
categoryGroups,
onEditGroup,
onEditCategory,
editingBudgetCategoryId,
onEditCategoryBudget,
openBalanceActionMenuId,
onOpenBalanceActionMenu,
editMode,
gestures,
month,
@@ -1045,67 +1048,71 @@ function BudgetGroups({
const { incomeGroup, expenseGroups } = separateGroups(categoryGroups);
return (
<SingleActiveEditFormProvider formName="mobile-budget-table">
<View
data-testid="budget-groups"
style={{ flex: '1 0 auto', overflowY: 'auto', paddingBottom: 15 }}
>
{expenseGroups
.filter(group => !group.hidden || showHiddenCategories)
.map(group => {
return (
<ExpenseGroup
key={group.id}
type={type}
group={group}
showBudgetedCol={showBudgetedCol}
gestures={gestures}
month={month}
editMode={editMode}
onEditGroup={onEditGroup}
onEditCategory={onEditCategory}
editingBudgetCategoryId={editingBudgetCategoryId}
onEditCategoryBudget={onEditCategoryBudget}
openBalanceActionMenuId={openBalanceActionMenuId}
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
onSaveCategory={onSaveCategory}
onDeleteCategory={onDeleteCategory}
onAddCategory={onAddCategory}
onReorderCategory={onReorderCategory}
onReorderGroup={onReorderGroup}
onBudgetAction={onBudgetAction}
show3Cols={show3Cols}
showHiddenCategories={showHiddenCategories}
pushModal={pushModal}
/>
);
})}
<View
data-testid="budget-groups"
style={{ flex: '1 0 auto', overflowY: 'auto', paddingBottom: 15 }}
style={{
alignItems: 'flex-start',
justifyContent: 'flex-start',
}}
>
{expenseGroups
.filter(group => !group.hidden || showHiddenCategories)
.map(group => {
return (
<ExpenseGroup
key={group.id}
type={type}
group={group}
showBudgetedCol={showBudgetedCol}
gestures={gestures}
month={month}
editMode={editMode}
onEditGroup={onEditGroup}
onEditCategory={onEditCategory}
onSaveCategory={onSaveCategory}
onDeleteCategory={onDeleteCategory}
onAddCategory={onAddCategory}
onReorderCategory={onReorderCategory}
onReorderGroup={onReorderGroup}
onBudgetAction={onBudgetAction}
show3Cols={show3Cols}
showHiddenCategories={showHiddenCategories}
pushModal={pushModal}
/>
);
})}
<View
style={{
alignItems: 'flex-start',
justifyContent: 'flex-start',
}}
>
<Button onClick={onAddGroup} style={{ fontSize: 12, margin: 10 }}>
Add Group
</Button>
</View>
{incomeGroup && (
<IncomeGroup
type={type}
group={incomeGroup}
month={month}
onAddCategory={onAddCategory}
onSaveCategory={onSaveCategory}
onDeleteCategory={onDeleteCategory}
showHiddenCategories={showHiddenCategories}
editMode={editMode}
onEditGroup={onEditGroup}
onEditCategory={onEditCategory}
onBudgetAction={onBudgetAction}
pushModal={pushModal}
/>
)}
<Button onClick={onAddGroup} style={{ fontSize: 12, margin: 10 }}>
Add Group
</Button>
</View>
</SingleActiveEditFormProvider>
{incomeGroup && (
<IncomeGroup
type={type}
group={incomeGroup}
month={month}
onAddCategory={onAddCategory}
onSaveCategory={onSaveCategory}
onDeleteCategory={onDeleteCategory}
showHiddenCategories={showHiddenCategories}
editMode={editMode}
onEditGroup={onEditGroup}
onEditCategory={onEditCategory}
editingBudgetCategoryId={editingBudgetCategoryId}
onEditCategoryBudget={onEditCategoryBudget}
onBudgetAction={onBudgetAction}
pushModal={pushModal}
/>
)}
</View>
);
}
@@ -1136,6 +1143,10 @@ export function BudgetTable({
pushModal,
onEditGroup,
onEditCategory,
editingBudgetCategoryId,
onEditCategoryBudget,
openBalanceActionMenuId,
onOpenBalanceActionMenu,
}) {
const { width } = useResponsive();
const show3Cols = width >= 360;
@@ -1374,6 +1385,10 @@ export function BudgetTable({
editMode={editMode}
onEditGroup={onEditGroup}
onEditCategory={onEditCategory}
editingBudgetCategoryId={editingBudgetCategoryId}
onEditCategoryBudget={onEditCategoryBudget}
openBalanceActionMenuId={openBalanceActionMenuId}
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
onSaveCategory={onSaveCategory}
onDeleteCategory={onDeleteCategory}
onAddCategory={onAddCategory}
@@ -1408,6 +1423,10 @@ export function BudgetTable({
editMode={editMode}
onEditGroup={onEditGroup}
onEditCategory={onEditCategory}
editingBudgetCategoryId={editingBudgetCategoryId}
onEditCategoryBudget={onEditCategoryBudget}
openBalanceActionMenuId={openBalanceActionMenuId}
onOpenBalanceActionMenu={onOpenBalanceActionMenu}
onSaveCategory={onSaveCategory}
onDeleteCategory={onDeleteCategory}
onAddCategory={onAddCategory}

View File

@@ -10,7 +10,6 @@ import {
import { type CSSProperties, theme } from '../../style';
import { Text } from './Text';
import { Toggle } from './Toggle';
import { View } from './View';
type KeybindingProps = {
@@ -34,8 +33,6 @@ type MenuItem = {
text: string;
key?: string;
style?: CSSProperties;
toggle?: boolean;
tooltip?: string;
};
type MenuProps<T extends MenuItem = MenuItem> = {
@@ -167,48 +164,23 @@ export function Menu<T extends MenuItem>({
onMouseEnter={() => setHoveredIndex(idx)}
onMouseLeave={() => setHoveredIndex(null)}
onClick={() =>
!item.disabled &&
onMenuSelect &&
item.toggle === undefined &&
onMenuSelect(item.name)
!item.disabled && onMenuSelect && onMenuSelect(item.name)
}
>
{/* Force it to line up evenly */}
{item.toggle === undefined ? (
<>
<Text style={{ lineHeight: 0 }}>
{item.icon &&
createElement(item.icon, {
width: item.iconSize || 10,
height: item.iconSize || 10,
style: {
marginRight: 7,
width: item.iconSize || 10,
},
})}
</Text>
<Text title={item.tooltip}>{item.text}</Text>
<View style={{ flex: 1 }} />
</>
) : (
<>
<label htmlFor={item.name} title={item.tooltip}>
{item.text}
</label>
<View style={{ flex: 1 }} />
<Toggle
id={item.name}
checked={item.toggle}
onColor={theme.pageTextPositive}
style={{ marginLeft: 5, ...item.style }}
onToggle={() =>
!item.disabled &&
item.toggle !== undefined &&
onMenuSelect(item.name)
}
/>
</>
)}
<Text style={{ lineHeight: 0 }}>
{item.icon &&
createElement(item.icon, {
width: item.iconSize || 10,
height: item.iconSize || 10,
style: {
marginRight: 7,
width: item.iconSize || 10,
},
})}
</Text>
<Text>{item.text}</Text>
<View style={{ flex: 1 }} />
{item.key && <Keybinding keyName={item.key} />}
</View>
);

View File

@@ -1,75 +0,0 @@
import React from 'react';
import { css } from 'glamor';
import { theme, type CSSProperties } from '../../style';
type ToggleProps = {
id: string;
checked: boolean;
onToggle?: () => void;
onColor?: string;
style?: CSSProperties;
};
export const Toggle = ({
id,
checked,
onToggle,
onColor,
style,
}: ToggleProps) => {
return (
<div style={{ marginTop: -20, ...style }}>
<input
id={id}
checked={checked}
onChange={onToggle}
className={`${css({
height: 0,
width: 0,
visibility: 'hidden',
})}`}
type="checkbox"
/>
<label
style={{
background: checked ? onColor : theme.checkboxToggleBackground,
}}
className={`${css({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
cursor: 'pointer',
width: '32px',
height: '16px',
borderRadius: '100px',
position: 'relative',
transition: 'background-color .2s',
})}`}
htmlFor={id}
>
<span
className={`${css(
{
content: '',
position: 'absolute',
top: '2px',
left: '2px',
width: '12px',
height: '12px',
borderRadius: '100px',
transition: '0.2s',
background: '#fff',
boxShadow: '0 0 2px 0 rgba(10, 10, 10, 0.29)',
},
checked && {
left: 'calc(100% - 2px)',
transform: 'translateX(-100%)',
},
)}`}
/>
</label>
</div>
);
};

View File

@@ -42,12 +42,6 @@ export function ConfirmTransactionEdit({
Saving your changes to this reconciled transaction may bring your
reconciliation out of balance.
</Block>
) : confirmReason === 'unlockReconciled' ? (
<Block>
Unlocking this transaction means you wont be warned about changes
that can impact your reconciled balance. (Changes to amount,
account, payee, etc).
</Block>
) : confirmReason === 'deleteReconciled' ? (
<Block>
Deleting this reconciled transaction may bring your reconciliation

View File

@@ -58,10 +58,7 @@ export const GoCardlessInitialise = ({
In order to enable bank-sync via GoCardless (only for EU banks) you
will need to create access credentials. This can be done by creating
an account with{' '}
<ExternalLink
to="https://actualbudget.org/docs/advanced/bank-sync/"
linkColor="purple"
>
<ExternalLink to="https://gocardless.com/" linkColor="purple">
GoCardless
</ExternalLink>
.

View File

@@ -376,9 +376,7 @@ function Transaction({
<Field
width="flex"
title={
categoryList.includes(transaction.category)
? transaction.category
: undefined
categoryList.includes(transaction.category) && transaction.category
}
>
{categoryList.includes(transaction.category) && transaction.category}

View File

@@ -1,14 +1,12 @@
import React, { useState } from 'react';
import React from 'react';
import * as monthUtils from 'loot-core/src/shared/months';
import { theme } from '../../style';
import { Button } from '../common/Button';
import { Menu } from '../common/Menu';
import { Select } from '../common/Select';
import { Text } from '../common/Text';
import { View } from '../common/View';
import { Tooltip } from '../tooltips';
import { Checkbox } from '../forms';
import { CategorySelector } from './CategorySelector';
import {
@@ -41,7 +39,6 @@ export function ReportSidebar({
onChangeDates,
onChangeViews,
}) {
const [menuOpen, setMenuOpen] = useState(false);
const onSelectRange = cond => {
setDateRange(cond);
switch (cond) {
@@ -245,62 +242,70 @@ export function ReportSidebar({
}}
>
<Text style={{ width: 40, textAlign: 'right', marginRight: 5 }} />
<Button
onClick={() => {
setMenuOpen(true);
}}
style={{
color: 'currentColor',
padding: '5px 10px',
}}
<Checkbox
id="show-empty-columns"
checked={customReportItems.showEmpty}
value={customReportItems.showEmpty}
onChange={() => setShowEmpty(!customReportItems.showEmpty)}
/>
<label
htmlFor="show-empty-columns"
title="Show rows that are zero or blank"
style={{ fontSize: 12 }}
>
Options
{menuOpen && (
<Tooltip
position="bottom-left"
style={{ padding: 0 }}
onClose={() => {
setMenuOpen(false);
}}
>
<Menu
onMenuSelect={type => {
if (type === 'show-hidden-categories') {
setShowOffBudgetHidden(
!customReportItems.showOffBudgetHidden,
);
} else if (type === 'show-empty-rows') {
setShowEmpty(!customReportItems.showEmpty);
} else if (type === 'show-uncategorized') {
setShowUncategorized(
!customReportItems.showUncategorized,
);
}
}}
items={[
{
name: 'show-empty-rows',
text: 'Show Empty Rows',
tooltip: 'Show rows that are zero or blank',
toggle: customReportItems.showEmpty,
},
{
name: 'show-hidden-categories',
text: 'Show Off Budget',
tooltip: 'Show off budget accounts and hidden categories',
toggle: customReportItems.showOffBudgetHidden,
},
{
name: 'show-uncategorized',
text: 'Show Uncategorized',
tooltip: 'Show uncategorized transactions',
toggle: customReportItems.showUncategorized,
},
]}
/>
</Tooltip>
)}
</Button>
Show Empty Rows
</label>
</View>
<View
style={{
flexDirection: 'row',
padding: 5,
alignItems: 'center',
}}
>
<Text style={{ width: 40, textAlign: 'right', marginRight: 5 }} />
<Checkbox
id="show-hidden-columns"
checked={customReportItems.showOffBudgetHidden}
value={customReportItems.showOffBudgetHidden}
onChange={() =>
setShowOffBudgetHidden(!customReportItems.showOffBudgetHidden)
}
/>
<label
htmlFor="show-hidden-columns"
title="Show off budget accounts and hidden categories"
style={{ fontSize: 12 }}
>
Off Budget Items
</label>
</View>
<View
style={{
flexDirection: 'row',
padding: 5,
alignItems: 'center',
}}
>
<Text style={{ width: 40, textAlign: 'right', marginRight: 5 }} />
<Checkbox
id="show-uncategorized"
checked={customReportItems.showUncategorized}
value={customReportItems.showUncategorized}
onChange={() =>
setShowUncategorized(!customReportItems.showUncategorized)
}
/>
<label
htmlFor="show-uncategorized"
title="Show uncategorized transactions"
style={{ fontSize: 12 }}
>
Uncategorized
</label>
</View>
<View
style={{

View File

@@ -1,164 +1,66 @@
import React, { useState } from 'react';
// @ts-strict-ignore
import React from 'react';
import * as d from 'date-fns';
import { css } from 'glamor';
import {
Bar,
CartesianGrid,
ComposedChart,
Line,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
type TooltipProps,
} from 'recharts';
import { usePrivacyMode } from 'loot-core/src/client/privacy';
import {
amountToCurrency,
amountToCurrencyNoDecimal,
} from 'loot-core/src/shared/util';
VictoryChart,
VictoryBar,
VictoryLine,
VictoryAxis,
VictoryVoronoiContainer,
VictoryGroup,
} from 'victory';
import { theme } from '../../../style';
import { AlignedText } from '../../common/AlignedText';
import { chartTheme } from '../chart-theme';
const MAX_BAR_SIZE = 50;
const ANIMATION_DURATION = 1000; // in ms
type CustomTooltipProps = TooltipProps<number, 'date'> & {
isConcise: boolean;
};
function CustomTooltip({ active, payload, isConcise }: CustomTooltipProps) {
if (!active || !payload) {
return null;
}
const [{ payload: data }] = payload;
return (
<div
className={`${css({
pointerEvents: 'none',
borderRadius: 2,
boxShadow: '0 1px 6px rgba(0, 0, 0, .20)',
backgroundColor: theme.menuBackground,
color: theme.menuItemText,
padding: 10,
})}`}
>
<div>
<div style={{ marginBottom: 10 }}>
<strong>
{d.format(data.date, isConcise ? 'MMMM yyyy' : 'MMMM dd, yyyy')}
</strong>
</div>
<div style={{ lineHeight: 1.5 }}>
<AlignedText left="Income:" right={amountToCurrency(data.income)} />
<AlignedText
left="Expenses:"
right={amountToCurrency(data.expenses)}
/>
<AlignedText
left="Change:"
right={
<strong>{amountToCurrency(data.income + data.expenses)}</strong>
}
/>
{data.transfers !== 0 && (
<AlignedText
left="Transfers:"
right={amountToCurrency(data.transfers)}
/>
)}
<AlignedText left="Balance:" right={amountToCurrency(data.balance)} />
</div>
</div>
</div>
);
}
import { Container } from '../Container';
import { Tooltip } from '../Tooltip';
type CashFlowGraphProps = {
graphData: {
expenses: { x: Date; y: number }[];
income: { x: Date; y: number }[];
balances: { x: Date; y: number }[];
transfers: { x: Date; y: number }[];
};
graphData: { expenses; income; balances };
isConcise: boolean;
};
export function CashFlowGraph({ graphData, isConcise }: CashFlowGraphProps) {
const privacyMode = usePrivacyMode();
const [yAxisIsHovered, setYAxisIsHovered] = useState(false);
const data = graphData.expenses.map((row, idx) => ({
date: row.x,
expenses: row.y,
income: graphData.income[idx].y,
balance: graphData.balances[idx].y,
transfers: graphData.transfers[idx].y,
}));
return (
<ResponsiveContainer width="100%" height={300}>
<ComposedChart stackOffset="sign" data={data}>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="date"
tick={{ fill: theme.reportsLabel }}
tickFormatter={x => {
// eslint-disable-next-line rulesdir/typography
return d.format(x, isConcise ? "MMM ''yy" : 'MMM d');
}}
minTickGap={50}
/>
<YAxis
tick={{ fill: theme.reportsLabel }}
tickCount={8}
tickFormatter={value =>
privacyMode && !yAxisIsHovered
? '...'
: amountToCurrencyNoDecimal(value)
}
onMouseEnter={() => setYAxisIsHovered(true)}
onMouseLeave={() => setYAxisIsHovered(false)}
/>
<Tooltip
labelFormatter={x => {
// eslint-disable-next-line rulesdir/typography
return d.format(x, isConcise ? "MMM ''yy" : 'MMM d');
}}
content={<CustomTooltip isConcise={isConcise} />}
isAnimationActive={false}
/>
<ReferenceLine y={0} stroke="#000" />
<Bar
dataKey="income"
stackId="a"
fill={chartTheme.colors.blue}
maxBarSize={MAX_BAR_SIZE}
animationDuration={ANIMATION_DURATION}
/>
<Bar
dataKey="expenses"
stackId="a"
fill={chartTheme.colors.red}
maxBarSize={MAX_BAR_SIZE}
animationDuration={ANIMATION_DURATION}
/>
<Line
type="monotone"
dataKey="balance"
dot={false}
stroke={theme.pageTextLight}
strokeWidth={2}
animationDuration={ANIMATION_DURATION}
/>
</ComposedChart>
</ResponsiveContainer>
<Container>
{(width, height, portalHost) =>
graphData && (
<VictoryChart
scale={{ x: 'time', y: 'linear' }}
theme={chartTheme}
domainPadding={10}
width={width}
height={height}
containerComponent={
<VictoryVoronoiContainer voronoiDimension="x" />
}
>
<VictoryGroup>
<VictoryBar
data={graphData.expenses}
style={{ data: { fill: chartTheme.colors.red } }}
/>
<VictoryBar data={graphData.income} />
</VictoryGroup>
<VictoryLine
data={graphData.balances}
labelComponent={<Tooltip portalHost={portalHost} />}
labels={x => x.premadeLabel}
style={{
data: { stroke: theme.pageTextLight },
}}
/>
<VictoryAxis
// eslint-disable-next-line rulesdir/typography
tickFormat={x => d.format(x, isConcise ? "MMM ''yy" : 'MMM d')}
tickValues={graphData.balances.map(item => item.x)}
tickCount={Math.min(5, graphData.balances.length)}
offsetY={50}
/>
<VictoryAxis dependentAxis crossAxis={false} />
</VictoryChart>
)
}
</Container>
);
}

View File

@@ -7,7 +7,7 @@ export const adjustTextSize = (
let source;
switch (type) {
case 'variable':
source = variableLookup.find(({ value }) => values >= value).arr;
source = variableLookup.find(({ value }) => values > value).arr;
break;
case 'donut':
source = donutLookup;

View File

@@ -1,6 +1,6 @@
import React, { useState, useMemo, useCallback } from 'react';
import { Bar, BarChart, ResponsiveContainer } from 'recharts';
import { VictoryBar, VictoryGroup, VictoryVoronoiContainer } from 'victory';
import * as monthUtils from 'loot-core/src/shared/months';
import { integerToCurrency } from 'loot-core/src/shared/util';
@@ -11,37 +11,14 @@ import { View } from '../../common/View';
import { PrivacyFilter } from '../../PrivacyFilter';
import { Change } from '../Change';
import { chartTheme } from '../chart-theme';
import { Container } from '../Container';
import { DateRange } from '../DateRange';
import { LoadingIndicator } from '../LoadingIndicator';
import { ReportCard } from '../ReportCard';
import { simpleCashFlow } from '../spreadsheets/cash-flow-spreadsheet';
import { Tooltip } from '../Tooltip';
import { useReport } from '../useReport';
function CustomLabel({ value, name, position, ...props }) {
return (
<>
<text
{...props}
dy={10}
dx={position === 'right' ? 20 : -5}
textAnchor={position === 'right' ? 'start' : 'end'}
fill={theme.tableText}
>
{name}
</text>
<text
{...props}
dy={26}
dx={position === 'right' ? 20 : -4}
textAnchor={position === 'right' ? 'start' : 'end'}
fill={theme.tableText}
>
<PrivacyFilter>{integerToCurrency(value)}</PrivacyFilter>
</text>
</>
);
}
export function CashFlowCard() {
const end = monthUtils.currentDay();
const start = monthUtils.currentMonth() + '-01';
@@ -54,7 +31,7 @@ export function CashFlowCard() {
const onCardHoverEnd = useCallback(() => setIsCardHovered(false));
const { graphData } = data || {};
const expenses = -(graphData?.expense || 0);
const expense = -(graphData?.expense || 0);
const income = graphData?.income || 0;
return (
@@ -78,7 +55,7 @@ export function CashFlowCard() {
<View style={{ textAlign: 'right' }}>
<PrivacyFilter activationFilters={[!isCardHovered]}>
<Change
amount={income - expenses}
amount={income - expense}
style={{ color: theme.tableText, fontWeight: 300 }}
/>
</PrivacyFilter>
@@ -87,33 +64,84 @@ export function CashFlowCard() {
</View>
{data ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={[
{
income,
expenses,
},
]}
margin={{
top: 10,
bottom: 0,
}}
>
<Bar
dataKey="income"
fill={chartTheme.colors.blue}
barSize={14}
label={<CustomLabel name="Income" position="left" />}
/>
<Bar
dataKey="expenses"
fill={chartTheme.colors.red}
barSize={14}
label={<CustomLabel name="Expenses" position="right" />}
/>
</BarChart>
</ResponsiveContainer>
<Container style={{ height: 'auto', flex: 1 }}>
{(width, height, portalHost) => (
<VictoryGroup
colorScale={[chartTheme.colors.blue, chartTheme.colors.red]}
width={100}
height={height}
theme={chartTheme}
domain={{
x: [0, 100],
y: [0, Math.max(income, expense, 100)],
}}
containerComponent={
<VictoryVoronoiContainer voronoiDimension="x" />
}
labelComponent={
<Tooltip
portalHost={portalHost}
offsetX={(width - 100) / 2}
offsetY={y => (y + 40 > height ? height - 40 : y)}
light={true}
forceActive={true}
style={{
padding: 0,
}}
/>
}
padding={{
top: 0,
bottom: 0,
left: 0,
right: 0,
}}
>
<VictoryBar
barWidth={13}
data={[
{
x: 30,
y: Math.max(income, 5),
premadeLabel: (
<View style={{ textAlign: 'right' }}>
Income
<View>
<PrivacyFilter activationFilters={[!isCardHovered]}>
{integerToCurrency(income)}
</PrivacyFilter>
</View>
</View>
),
labelPosition: 'left',
},
]}
labels={d => d.premadeLabel}
/>
<VictoryBar
barWidth={13}
data={[
{
x: 60,
y: Math.max(expense, 5),
premadeLabel: (
<View>
Expenses
<View>
<PrivacyFilter activationFilters={[!isCardHovered]}>
{integerToCurrency(expense)}
</PrivacyFilter>
</View>
</View>
),
labelPosition: 'right',
},
]}
labels={d => d.premadeLabel}
/>
</VictoryGroup>
)}
</Container>
) : (
<LoadingIndicator />
)}

View File

@@ -180,10 +180,6 @@ function recalculate(data, start, end, isConcise) {
res.income.push({ x, y: integerToAmount(income) });
res.expenses.push({ x, y: integerToAmount(expense) });
res.transfers.push({
x,
y: integerToAmount(creditTransfers + debitTransfers),
});
res.balances.push({
x,
y: integerToAmount(balance),
@@ -192,7 +188,7 @@ function recalculate(data, start, end, isConcise) {
});
return res;
},
{ expenses: [], income: [], transfers: [], balances: [] },
{ expenses: [], income: [], balances: [] },
);
const { balances } = graphData;

View File

@@ -848,14 +848,13 @@ type TableHandleRef<T extends TableItem = TableItem> = {
type TableWithNavigatorProps = TableProps & {
fields;
};
export function TableWithNavigator({
fields,
...props
}: TableWithNavigatorProps) {
export const TableWithNavigator = forwardRef<
TableHandleRef<TableItem>,
TableWithNavigatorProps
>(({ fields, ...props }) => {
const navigator = useTableNavigator(props.items, fields);
return <Table {...props} navigator={navigator} />;
}
});
type TableItem = { id: number | string };

View File

@@ -1131,10 +1131,10 @@ export const TransactionEdit = props => {
const Transaction = memo(function Transaction({
transaction,
account,
accounts,
categories,
payees,
showCategory,
added,
onSelect,
style,
@@ -1169,15 +1169,11 @@ const Transaction = memo(function Transaction({
payee,
transferAcct,
);
const specialCategory = account?.offbudget
? 'Off Budget'
: transferAcct
? 'Transfer'
: isParent
? 'Split'
: null;
const prettyCategory = specialCategory || categoryName;
const prettyCategory = transferAcct
? 'Transfer'
: isParent
? 'Split'
: categoryName;
const isPreview = isPreviewId(id);
const isReconciled = transaction.reconciled;
@@ -1264,21 +1260,22 @@ const Transaction = memo(function Transaction({
}}
/>
)}
<TextOneLine
style={{
fontSize: 11,
marginTop: 1,
fontWeight: '400',
color: prettyCategory
? theme.tableTextSelected
: theme.menuItemTextSelected,
fontStyle:
specialCategory || !prettyCategory ? 'italic' : undefined,
textAlign: 'left',
}}
>
{prettyCategory || 'Uncategorized'}
</TextOneLine>
{showCategory && (
<TextOneLine
style={{
fontSize: 11,
marginTop: 1,
fontWeight: '400',
color: prettyCategory
? theme.tableTextSelected
: theme.menuItemTextSelected,
fontStyle: prettyCategory ? null : 'italic',
textAlign: 'left',
}}
>
{prettyCategory || 'Uncategorized'}
</TextOneLine>
)}
</View>
)}
</View>
@@ -1299,11 +1296,11 @@ const Transaction = memo(function Transaction({
});
export function TransactionList({
account,
accounts,
categories,
payees,
transactions,
showCategory,
isNew,
onSelect,
scrollProps = {},
@@ -1387,10 +1384,10 @@ export function TransactionList({
>
<Transaction
transaction={transaction}
account={account}
categories={categories}
accounts={accounts}
payees={payees}
showCategory={showCategory}
added={isNew(transaction.id)}
onSelect={onSelect} // onSelect(transaction)}
/>

View File

@@ -773,16 +773,6 @@ const Transaction = memo(function Transaction(props) {
onUpdateAfterConfirm(name, value);
}
}
// Allow un-reconciling (unlocking) transactions
if (name === 'cleared' && transaction.reconciled) {
props.pushModal('confirm-transaction-edit', {
onConfirm: () => {
onUpdateAfterConfirm('reconciled', false);
},
confirmReason: 'unlockReconciled',
});
}
}
function onUpdateAfterConfirm(name, value) {

View File

@@ -174,7 +174,6 @@ export const checkboxText = tableText;
export const checkboxBackgroundSelected = colorPalette.purple300;
export const checkboxBorderSelected = colorPalette.purple300;
export const checkboxShadowSelected = colorPalette.purple500;
export const checkboxToggleBackground = colorPalette.gray700;
export const pillBackground = colorPalette.navy800;
export const pillBackgroundLight = colorPalette.navy900;

View File

@@ -174,7 +174,6 @@ export const checkboxText = tableBackground;
export const checkboxBackgroundSelected = colorPalette.blue500;
export const checkboxBorderSelected = colorPalette.blue500;
export const checkboxShadowSelected = colorPalette.blue300;
export const checkboxToggleBackground = colorPalette.gray400;
export const pillBackground = colorPalette.navy150;
export const pillBackgroundLight = colorPalette.navy100;

View File

@@ -3,7 +3,7 @@
"author": "Actual",
"productName": "Actual",
"description": "A simple and powerful personal finance system",
"version": "24.2.0",
"version": "24.1.0",
"scripts": {
"clean": "rm -rf dist",
"update-client": "bin/update-client",

View File

@@ -11,9 +11,9 @@
"requireindex": "^1.2.0"
},
"devDependencies": {
"eslint-plugin-eslint-plugin": "^5.0.0",
"eslint-plugin-eslint-plugin": "^5.2.1",
"eslint-plugin-node": "^11.1.0",
"jest": "^27.0.0",
"jest": "^27.5.1",
"npm-run-all": "^4.1.5"
},
"peerDependencies": {

View File

@@ -2,4 +2,10 @@ BEGIN TRANSACTION;
ALTER TABLE accounts ADD COLUMN account_sync_source TEXT;
UPDATE accounts SET
account_sync_source = CASE
WHEN account_id THEN 'goCardless'
ELSE NULL
END;
COMMIT;

View File

@@ -1,9 +0,0 @@
BEGIN TRANSACTION;
UPDATE accounts
SET
account_sync_source = 'goCardless'
WHERE account_id IS NOT NULL
AND account_sync_source IS NULL;
COMMIT;

View File

@@ -41,17 +41,17 @@
"devDependencies": {
"@actual-app/api": "*",
"@actual-app/crdt": "*",
"@swc/core": "^1.3.105",
"@swc/helpers": "^0.5.3",
"@swc/jest": "^0.2.31",
"@swc/core": "^1.3.82",
"@swc/helpers": "^0.5.1",
"@swc/jest": "^0.2.29",
"@types/better-sqlite3": "^7.6.8",
"@types/jest": "^27.5.0",
"@types/jlongster__sql.js": "npm:@types/sql.js@latest",
"@types/pegjs": "^0.10.3",
"@types/react-redux": "^7.1.25",
"@types/uuid": "^9.0.2",
"@types/webpack": "^5.28.5",
"@types/webpack-bundle-analyzer": "^4.6.3",
"@types/webpack": "^5.28.2",
"@types/webpack-bundle-analyzer": "^4.6.0",
"adm-zip": "^0.5.9",
"buffer": "^6.0.3",
"cross-env": "^7.0.3",
@@ -73,10 +73,10 @@
"terser-webpack-plugin": "^5.3.9",
"throttleit": "^1.0.0",
"ts-node": "^10.7.0",
"typescript": "^5.0.2",
"typescript": "^4.6.4",
"uuid": "^9.0.0",
"webpack": "^5.89.0",
"webpack-bundle-analyzer": "^4.10.1",
"webpack": "^5.88.2",
"webpack-bundle-analyzer": "^4.9.1",
"webpack-cli": "^5.1.4",
"yargs": "^9.0.1"
}

View File

@@ -589,12 +589,13 @@ export async function createTestBudget(handlers: Handlers) {
{ name: 'House Asset', offBudget: true },
{ name: 'Roth IRA', offBudget: true },
];
await runMutator(async () => {
for (const account of accounts) {
account.id = await handlers['account-create'](account);
}
});
await runMutator(() =>
batchMessages(async () => {
for (const account of accounts) {
account.id = await handlers['account-create'](account);
}
}),
);
const payees: Array<MockPayeeEntity> = [
{ name: 'Starting Balance' },

View File

@@ -1,5 +1,4 @@
// @ts-strict-ignore
import { APIError } from '../../../server/errors';
import { runHandler, isMutating } from '../../../server/mutators';
import { captureException } from '../../exceptions';
@@ -71,7 +70,7 @@ export const init: T.Init = function (_socketName, handlers) {
type: 'reply',
id,
result: null,
error: APIError('Unknown method: ' + name),
error: { type: 'APIError', message: 'Unknown method: ' + name },
});
}
});

View File

@@ -1,5 +1,4 @@
// @ts-strict-ignore
import { APIError } from '../../../server/errors';
import { runHandler, isMutating } from '../../../server/mutators';
import { captureException } from '../../exceptions';
@@ -91,7 +90,7 @@ export const init: T.Init = function (serverChn, handlers) {
type: 'reply',
id,
result: null,
error: APIError('Unknown method: ' + name),
error: { type: 'APIError', message: 'Unknown method: ' + name },
});
}
},

View File

@@ -28,7 +28,6 @@ import {
import { runQuery as aqlQuery } from './aql';
import * as cloudStorage from './cloud-storage';
import * as db from './db';
import { APIError } from './errors';
import { runMutator } from './mutators';
import * as prefs from './prefs';
import * as sheet from './sheet';
@@ -36,6 +35,11 @@ import { setSyncingMode, batchMessages } from './sync';
let IMPORT_MODE = false;
// This is duplicate from main.js...
function APIError(msg, meta?) {
return { type: 'APIError', message: msg, meta };
}
// The API is different in two ways: we never want undo enabled, and
// we also need to notify the UI manually if stuff has changed (if
// they are connecting to an already running instance, the UI should

View File

@@ -127,9 +127,7 @@ export function setGoal({ month, category, goal }): Promise<void> {
});
}
return db.insert(table, {
id: `${dbMonth(month)}-${category}`,
month: dbMonth(month),
category,
id: month,
goal,
});
}

View File

@@ -3,7 +3,7 @@ import { Notification } from '../../client/state-types/notifications';
import * as monthUtils from '../../shared/months';
import * as db from '../db';
import { setBudget, getSheetValue, setGoal } from './actions';
import { setBudget, getSheetValue } from './actions';
import { parse } from './cleanup-template.pegjs';
export function cleanupTemplate({ month }: { month: string }) {
@@ -35,20 +35,11 @@ async function processCleanup(month: string): Promise<Notification> {
sheetName,
`budget-${category.id}`,
);
const spent = await getSheetValue(
sheetName,
`sum-amount-${category.id}`,
);
await setBudget({
category: category.id,
month,
amount: budgeted - balance,
});
await setGoal({
category: category.id,
month,
goal: -spent,
});
num_sources += 1;
}
if (template.filter(t => t.type === 'sink').length > 0) {

View File

@@ -1,6 +1,7 @@
// @ts-strict-ignore
export const SORT_INCREMENT = 16384;
function midpoint<T extends { sort_order: number }>(items: T[], to: number) {
function midpoint(items, to) {
const below = items[to - 1];
const above = items[to];
@@ -13,14 +14,11 @@ function midpoint<T extends { sort_order: number }>(items: T[], to: number) {
}
}
export function shoveSortOrders<T extends { id: string; sort_order: number }>(
items: T[],
targetId?: string,
) {
export function shoveSortOrders(items, targetId?: string) {
const to = items.findIndex(item => item.id === targetId);
const target = items[to];
const before = items[to - 1];
const updates: Array<{ id: string; sort_order: number }> = [];
const updates = [];
// If no target is specified, append at the end
if (!targetId || to === -1) {

View File

@@ -1,10 +1,11 @@
// @ts-strict-ignore
// TODO: normalize error types
export class PostError extends Error {
meta?: { meta: string };
reason: string;
type: 'PostError';
meta;
reason;
type;
constructor(reason: string, meta?: { meta: string }) {
constructor(reason, meta?) {
super('PostError: ' + reason);
this.type = 'PostError';
this.reason = reason;
@@ -13,10 +14,10 @@ export class PostError extends Error {
}
export class HTTPError extends Error {
statusCode: number;
responseBody: string;
statusCode;
responseBody;
constructor(code: number, body: string) {
constructor(code, body) {
super(`HTTPError: unsuccessful status code (${code}): ${body}`);
this.statusCode = code;
this.responseBody = body;
@@ -24,27 +25,10 @@ export class HTTPError extends Error {
}
export class SyncError extends Error {
meta?:
| {
isMissingKey: boolean;
}
| {
error: { message: string; stack: string };
query: { sql: string; params: Array<string | number> };
};
reason: string;
meta;
reason;
constructor(
reason: string,
meta?:
| {
isMissingKey: boolean;
}
| {
error: { message: string; stack: string };
query: { sql: string; params: Array<string | number> };
},
) {
constructor(reason, meta?) {
super('SyncError: ' + reason);
this.reason = reason;
this.meta = meta;
@@ -62,20 +46,14 @@ export class RuleError extends Error {
}
}
export function APIError(msg: string) {
return { type: 'APIError', message: msg };
export function APIError(msg, meta?) {
return { type: 'APIError', message: msg, meta };
}
export function FileDownloadError(
reason: string,
meta?: { fileId?: string; isMissingKey?: boolean },
) {
export function FileDownloadError(reason, meta?) {
return { type: 'FileDownloadError', reason, meta };
}
export function FileUploadError(
reason: string,
meta?: { isMissingKey: boolean },
) {
export function FileUploadError(reason, meta?) {
return { type: 'FileUploadError', reason, meta };
}

View File

@@ -1,30 +1,19 @@
import {
AccountEntity,
CategoryEntity,
CategoryGroupEntity,
PayeeEntity,
} from '../types/models';
export function requiredFields<T extends object, K extends keyof T>(
name: string,
row: T,
fields: K[],
update?: boolean,
) {
// @ts-strict-ignore
export function requiredFields(name, row, fields, update) {
fields.forEach(field => {
if (update) {
if (row.hasOwnProperty(field) && row[field] == null) {
throw new Error(`${name} is missing field ${String(field)}`);
throw new Error(`${name} is missing field ${field}`);
}
} else {
if (!row.hasOwnProperty(field) || row[field] == null) {
throw new Error(`${name} is missing field ${String(field)}`);
throw new Error(`${name} is missing field ${field}`);
}
}
});
}
export function toDateRepr(str: string) {
export function toDateRepr(str) {
if (typeof str !== 'string') {
throw new Error('toDateRepr not passed a string: ' + str);
}
@@ -32,7 +21,7 @@ export function toDateRepr(str: string) {
return parseInt(str.replace(/-/g, ''));
}
export function fromDateRepr(number: number) {
export function fromDateRepr(number) {
if (typeof number !== 'number') {
throw new Error('fromDateRepr not passed a number: ' + number);
}
@@ -48,7 +37,7 @@ export function fromDateRepr(number: number) {
}
export const accountModel = {
validate(account: AccountEntity, { update }: { update?: boolean } = {}) {
validate(account, { update }: { update?: boolean } = {}) {
requiredFields(
'account',
account,
@@ -61,7 +50,7 @@ export const accountModel = {
};
export const categoryModel = {
validate(category: CategoryEntity, { update }: { update?: boolean } = {}) {
validate(category, { update }: { update?: boolean } = {}) {
requiredFields(
'category',
category,
@@ -75,10 +64,7 @@ export const categoryModel = {
};
export const categoryGroupModel = {
validate(
categoryGroup: CategoryGroupEntity,
{ update }: { update?: boolean } = {},
) {
validate(categoryGroup, { update }: { update?: boolean } = {}) {
requiredFields(
'categoryGroup',
categoryGroup,
@@ -92,8 +78,78 @@ export const categoryGroupModel = {
};
export const payeeModel = {
validate(payee: PayeeEntity, { update }: { update?: boolean } = {}) {
validate(payee, { update }: { update?: boolean } = {}) {
requiredFields('payee', payee, ['name'], update);
return payee;
},
};
export const transactionModel = {
validate(trans, { update }: { update?: boolean } = {}) {
requiredFields('transaction', trans, ['date', 'acct'], update);
if ('date' in trans) {
// Make sure it's the right format, and also do a sanity check.
// Really old dates can mess up the system and can happen by
// accident
if (
trans.date.match(/^\d{4}-\d{2}-\d{2}$/) == null ||
trans.date < '2000-01-01'
) {
throw new Error('Invalid transaction date: ' + trans.date);
}
}
return trans;
},
toJS(row) {
// Check a non-important field that typically wouldn't be passed in
// manually, and use it as a smoke test to see if this is a
// fully-formed transaction or not.
if (!('location' in row)) {
throw new Error(
'A full transaction is required to be passed to `toJS`. Instead got: ' +
JSON.stringify(row),
);
}
const trans = { ...row };
trans.error = row.error ? JSON.parse(row.error) : null;
trans.isParent = row.isParent === 1 ? true : false;
trans.isChild = row.isChild === 1 ? true : false;
trans.starting_balance_flag =
row.starting_balance_flag === 1 ? true : false;
trans.cleared = row.cleared === 1 ? true : false;
trans.pending = row.pending === 1 ? true : false;
trans.date = trans.date && fromDateRepr(trans.date);
return trans;
},
fromJS(trans) {
const row = { ...trans };
if ('error' in row) {
row.error = trans.error ? JSON.stringify(trans.error) : null;
}
if ('isParent' in row) {
row.isParent = trans.isParent ? 1 : 0;
}
if ('isChild' in row) {
row.isChild = trans.isChild ? 1 : 0;
}
if ('cleared' in row) {
row.cleared = trans.cleared ? 1 : 0;
}
if ('pending' in row) {
row.pending = trans.pending ? 1 : 0;
}
if ('starting_balance_flag' in row) {
row.starting_balance_flag = trans.starting_balance_flag ? 1 : 0;
}
if ('date' in row) {
row.date = toDateRepr(trans.date);
}
return row;
},
};

View File

@@ -15,7 +15,7 @@ import { ReportsHandlers } from './types/handlers';
const reportModel = {
validate(report: CustomReportEntity, { update }: { update?: boolean } = {}) {
requiredFields('reports', report, ['conditionsOp'], update);
requiredFields('reports', report, ['conditions'], update);
if (!update || 'conditionsOp' in report) {
if (!['and', 'or'].includes(report.conditionsOp)) {

View File

@@ -142,7 +142,8 @@ async function fetchAll(table, ids) {
message: error.message,
stack: error.stack,
},
query: { sql, params: partIds },
sql,
params: partIds,
});
}
}

View File

@@ -6,8 +6,8 @@
],
"compilerOptions": {
// "composite": true,
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"target": "ES2021",
"lib": ["ES2021", "DOM", "DOM.Iterable"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
@@ -25,8 +25,8 @@
"checkJs": false,
// Used for temp builds
"outDir": "build",
"moduleResolution": "Node10",
"module": "ES2022",
"moduleResolution": "Node16",
"module": "Node16",
// Until/if we build using tsc
"noEmit": true,
"paths": {
@@ -44,7 +44,7 @@
"exclude": ["**/node_modules/*", "**/build/*", "**/lib-dist/*"],
"ts-node": {
"compilerOptions": {
"module": "CommonJS"
"module": "commonjs"
}
}
}

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [twk3]
---
Bundle loot-core types into the API

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [HansiWursti]
---
Added cleared and uncleared Balances to Account Mobile View

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
Fixing TypeScript issues when enabling `strictFunctionTypes` (pt.5).

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [joel-jeremy]
---
Refactored MobileBudget component to TypeScript

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [twk3]
---
Switch desktop-client to the Vite JS framework.

Some files were not shown because too many files have changed in this diff Show More