Compare commits
2 Commits
v24.2.0
...
package-up
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38368224d3 | ||
|
|
f964552b5b |
4
.github/workflows/e2e-test.yml
vendored
@@ -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
|
||||
|
||||
14
package.json
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// Using ES2021 because that’s 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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
// Using ES2021 because that’s 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 83 KiB |
@@ -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);
|
||||
|
||||
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
@@ -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": {
|
||||
|
||||
@@ -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 : (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 won‘t 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
|
||||
|
||||
@@ -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>
|
||||
.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -142,7 +142,8 @@ async function fetchAll(table, ids) {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
query: { sql, params: partIds },
|
||||
sql,
|
||||
params: partIds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/2053.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [twk3]
|
||||
---
|
||||
|
||||
Bundle loot-core types into the API
|
||||
6
upcoming-release-notes/2056.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [HansiWursti]
|
||||
---
|
||||
|
||||
Added cleared and uncleared Balances to Account Mobile View
|
||||
6
upcoming-release-notes/2072.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Fixing TypeScript issues when enabling `strictFunctionTypes` (pt.5).
|
||||
6
upcoming-release-notes/2081.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Refactored MobileBudget component to TypeScript
|
||||
6
upcoming-release-notes/2084.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [twk3]
|
||||
---
|
||||
|
||||
Switch desktop-client to the Vite JS framework.
|
||||