Compare commits
53 commits
Author | SHA1 | Date | |
---|---|---|---|
3edf947145 | |||
b34dfb15b2 | |||
8b1c5777ad | |||
94c195abb9 | |||
7c1c9bf03a | |||
8f7625831f | |||
15e2f2dce7 | |||
29fb2cc641 | |||
2566b950c2 | |||
ba71c1ba05 | |||
fcde52a9c7 | |||
94dbbbc68b | |||
16e01a8f58 | |||
a9218e1625 | |||
0a61acdf8f | |||
157b58a2dd | |||
7b6909eaa6 | |||
3133ddcda3 | |||
4904da5006 | |||
a72ee51e1a | |||
bf03dfc1cc | |||
a157679906 | |||
a4a7803407 | |||
2d9b319e45 | |||
4b83552ddf | |||
5f0da9deb8 | |||
e903f5c20d | |||
04aa1731b5 | |||
bb9c9927db | |||
696a094303 | |||
4421f2a173 | |||
f1b61cf6f1 | |||
6075e846d3 | |||
2857e65ccd | |||
29124f624b | |||
aa97994ad2 | |||
be3dc26166 | |||
9ce6f9564c | |||
12289792da | |||
c8e759fd49 | |||
2256653c16 | |||
182c77a8e3 | |||
de232b3ff0 | |||
a582975d71 | |||
93f0093c1d | |||
0e441eeed2 | |||
f190443272 | |||
603179dda1 | |||
fd3c0f9b18 | |||
d98b8c1d73 | |||
c9a543310d | |||
5a00bf6b43 | |||
55b9a0e7ef |
2
.github/actions/setup/action.yml
vendored
2
.github/actions/setup/action.yml
vendored
|
@ -6,7 +6,7 @@ runs:
|
|||
- name: Install node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 16.15.0
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
id: cache
|
||||
|
|
22
.github/workflows/stale.yml
vendored
Normal file
22
.github/workflows/stale.yml
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
name: Close inactive issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v5
|
||||
with:
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: -1
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "🚧🚨 This issue is being marked as stale due to 90 days of inactivity. 🚧🚨"
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
only-labels: 'needs triage'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
67
README.md
67
README.md
|
@ -1,72 +1,39 @@
|
|||
This is the source code for [Actual](https://actualbudget.com), a local-first personal finance tool. It is 100% free and open-source.
|
||||
## Getting Started
|
||||
|
||||
If you are only interested in running the latest version, you don't need this repo. You can get the latest version through npm.
|
||||
Actual is a local-first personal finance tool. It is 100% free and open-source, written in NodeJS, it has a synchronization element so that all your changes can move between devices without any heavy lifting.
|
||||
|
||||
More docs are available in the [docs](https://github.com/actualbudget/actual/tree/master/docs) folder.
|
||||
If you are interested in contributing, or want to know how development works, see [CONTRIBUTING.md](https://github.com/actualbudget/actual/blob/master/CONTRIBUTING.md) we would love to have you.
|
||||
|
||||
If you are interested in contributing, or want to know how development works, see [CONTRIBUTING.md](https://github.com/actualbudget/actual/blob/master/CONTRIBUTING.md)
|
||||
Want to say thanks? Click the ⭐ at the top of the page.
|
||||
|
||||
Join the [discord](https://discord.gg/pRYNYr4W5A)!
|
||||
## Key Links
|
||||
|
||||
* Actual [discord](https://discord.gg/pRYNYr4W5A) community.
|
||||
* Actual [Community Documentation](https://actualbudget.github.io/docs)
|
||||
|
||||
## Installation
|
||||
|
||||
If you are only interested in running the latest version and not contributing to the source code, you don't need to clone this repo. You can get the latest version through npm.
|
||||
|
||||
**Please Note:** While the Actual repository holds source code for the mobile applications that were supported when Actual was closed source, these are no longer supported on the Open Source version of Actual.
|
||||
|
||||
### The easy way: using a server (recommended)
|
||||
|
||||
The easiest way to get Actual running is to use the [actual-server](https://github.com/actualbudget/actual-server) project. That is the server for syncing changes across devices, and it comes with the latest version of Actual. The server will provide both the web project and a server for syncing.
|
||||
|
||||
```
|
||||
git clone https://github.com/actualbudget/actual-server.git
|
||||
cd actual-server
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.github.io/docs/Installing/Local/your-own-machine)
|
||||
|
||||
Navigate to https://localhost:5006 in your browser and you will see Actual.
|
||||
## Documentation
|
||||
|
||||
You should deploy the server somewhere so you can access your data from anywhere. See instructions on the [actual-server](https://github.com/actualbudget/actual-server) repo.
|
||||
|
||||
### Without a server
|
||||
|
||||
This will give you a fully local web app without a server. This npm package is the `packages/desktop-client` package in this repo built for production:
|
||||
|
||||
```
|
||||
yarn add @actual-app/web
|
||||
```
|
||||
|
||||
Now you need to serve the files in `node_modules/@actual-app/web/build`. One way to do it:
|
||||
|
||||
```
|
||||
cd node_modules/@actual-app/web/build
|
||||
npx http-server .
|
||||
```
|
||||
|
||||
Navigate to http://localhost:8080 and you should see Actual.
|
||||
|
||||
## Building
|
||||
|
||||
If you want to build the latest version, see [releasing.md](https://github.com/actualbudget/actual/blob/master/docs/releasing.md). It provides instructions for building this code into the same artifacts that come from npm.
|
||||
|
||||
## Run locally
|
||||
|
||||
Both the electron and web app can started with a single command. When running in development, it will store data in a `data` directory in the root of the `actual` directory.
|
||||
|
||||
First, make sure to run `yarn install` to install all dependencies.
|
||||
|
||||
In the root of the project:
|
||||
|
||||
```
|
||||
yarn start # Run the electron app
|
||||
yarn start:browser # Run the web app
|
||||
```
|
||||
We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.github.io/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
|
||||
|
||||
## Code structure
|
||||
|
||||
The app is split up into a few packages:
|
||||
The Actual app is split up into a few packages:
|
||||
|
||||
* loot-core - The core application that runs on any platform
|
||||
* loot-design - The generic design components that make up the UI
|
||||
* desktop-client - The desktop UI
|
||||
* desktop-electron - The desktop app
|
||||
* mobile - The mobile app
|
||||
|
||||
More docs are available in the [docs](https://github.com/actualbudget/actual/tree/master/docs) folder.
|
||||
More information on the project structure is available in our [community documentation](https://actualbudget.github.io/docs/Developers/project-layout).
|
||||
|
|
20
docs/API.md
20
docs/API.md
|
@ -1,20 +0,0 @@
|
|||
|
||||
Previous docs for the API are [here](https://actualbudget.com/docs/developers/using-the-API/). The API is currently being improved. Previously, the API connected to an existing running instance of Actual. Now the API is bundled and fully isolated, capable of running all of Actual itself. Setting up the API is different because of this.
|
||||
|
||||
You need to call `init` and pass it the directory where your files live. Call `load-budget` to load the file you want to work on. After that, you can use the same API as before.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
let actual = require('@actual-app/api');
|
||||
|
||||
await actual.init({
|
||||
config: {
|
||||
dataDir: join(__dirname, 'user-files')
|
||||
}
|
||||
});
|
||||
|
||||
await actual.internal.send('load-budget', { id: 'My-Finances' });
|
||||
|
||||
await actual.getAccounts();
|
||||
```
|
|
@ -1,18 +0,0 @@
|
|||
# How to build browser for Windows
|
||||
Many of the build scripts are bash scripts and not natively invokable in Windows. To solve this, you can build the project using Git Bash.
|
||||
1. Install [Git & Git Bash for Windows](https://git-scm.com/downloads)
|
||||
2. Install Node v16.x (latest version 17.x does not work due to issue with crypto package)
|
||||
3. Clone this repo
|
||||
4. From the root of this repo, run `sh` to launch a bash shell
|
||||
5. From inside the bash shell, run `yarn install`
|
||||
6. From still inside the shell, run `yarn start:browser`
|
||||
7. Open your browser to `localhost:3001`
|
||||
|
||||
# How to build electron for Windows
|
||||
1. Follow steps 1 - 5 above.
|
||||
2. Run `yarn start`
|
||||
3. If you get an error from electron, run `yarn rebuild-electron` and rerun `yarn start`;
|
||||
|
||||
## rsync: command not found
|
||||
If you run into this error, you will need to install the rsync binary to Git Bash. Follow the [directions here](https://prasaz.medium.com/add-rsync-to-windows-git-bash-f42736bae1b3). When you get to the final step - installing the libxxhash dll - rename the dll from `msys-xxhash-0.8.0.dll` to `msys-xxhash-0.dll`
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# How to cut a release
|
||||
|
||||
In the open-source version of Actual, all updates go through npm. There are two libraries:
|
||||
|
||||
* `@actual-app/api`: The API for the underlying functionality. This includes the entire backend of Actual, meant to be used with node.
|
||||
* `@actual-app/web`: A web build that will serve the app with a web frontend. This includes both the frontend and backend of Actual. It includes the backend as well because it's built to be used as a Web Worker.
|
||||
|
||||
Both the API and web libraries are versioned together. This may change in the future, but because the web library also brings along its own backend it's easier to maintain a single version for now. That makes it clear which version the backend is regardless of library.
|
||||
|
||||
## Releasing `@actual-app/api`
|
||||
|
||||
This generates a bundle for the API:
|
||||
|
||||
```
|
||||
cd packages/loot-core
|
||||
yarn build:api
|
||||
```
|
||||
|
||||
The built files live in `lib-dist`, so we need to copy them to the API package:
|
||||
|
||||
```
|
||||
cp lib-dist/bundle.api* ../api/app
|
||||
```
|
||||
|
||||
Next, bump the version on package.json. Finally, publish it:
|
||||
|
||||
```
|
||||
npm publish
|
||||
```
|
||||
|
||||
## Releasing `@actual-app/web`
|
||||
|
||||
In the root of `actual` (not just `desktop-client`), run this:
|
||||
|
||||
```
|
||||
./bin/package-browser
|
||||
```
|
||||
|
||||
This will compile both the backend and the frontend into a single directory in `packages/desktop-client/build`. This directory is all the files that need to be published. After bumping the version, publish `desktop-client`:
|
||||
|
||||
```
|
||||
cd packages/desktop-client
|
||||
npm publish
|
||||
```
|
1
packages/api/.gitignore
vendored
Normal file
1
packages/api/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
app/bundle.api.js*
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,8 +1,7 @@
|
|||
let bundle = require('./app/bundle.api.js');
|
||||
let injected = require('./injected');
|
||||
let methods = require('./methods');
|
||||
let utils = require('./utils');
|
||||
|
||||
let injected = require('./injected');
|
||||
let actualApp;
|
||||
|
||||
async function init({ budgetId, config } = {}) {
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "4.0.2",
|
||||
"version": "4.1.5",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"app",
|
||||
"default-db.sqlite",
|
||||
"index.js",
|
||||
"injected.js",
|
||||
"methods.js",
|
||||
"migrations",
|
||||
"utils.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^7.5.0",
|
||||
"node-fetch": "^1.6.3",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
function amountToInteger(n) {
|
||||
return Math.round(n * 100) | 0;
|
||||
return Math.round(n * 100);
|
||||
}
|
||||
|
||||
function integerToAmount(n) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "4.0.2",
|
||||
"version": "22.12.03",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
default-db.sqlite
|
||||
migrations/.force-copy-windows
|
||||
migrations/1548957970627_remove-db-version.sql
|
||||
migrations/1550601598648_payees.sql
|
||||
migrations/1555786194328_remove_category_group_unique.sql
|
||||
|
@ -14,4 +15,3 @@ migrations/1615745967948_meta.sql
|
|||
migrations/1616167010796_accounts_order.sql
|
||||
migrations/1618975177358_schedules.sql
|
||||
migrations/1632571489012_remove_cache.js
|
||||
migrations/.force-copy-windows
|
||||
|
|
|
@ -35,6 +35,7 @@ import {
|
|||
Stack
|
||||
} from 'loot-design/src/components/common';
|
||||
import { KeyHandlers } from 'loot-design/src/components/KeyHandlers';
|
||||
import NotesButton from 'loot-design/src/components/NotesButton';
|
||||
import CellValue from 'loot-design/src/components/spreadsheet/CellValue';
|
||||
import format from 'loot-design/src/components/spreadsheet/format';
|
||||
import useSheetValue from 'loot-design/src/components/spreadsheet/useSheetValue';
|
||||
|
@ -106,6 +107,7 @@ function EmptyMessage({ onAdd }) {
|
|||
function ReconcilingMessage({ balanceQuery, targetBalance, onDone }) {
|
||||
let cleared = useSheetValue({
|
||||
name: balanceQuery.name + '-cleared',
|
||||
value: 0,
|
||||
query: balanceQuery.query.filter({ cleared: true })
|
||||
});
|
||||
let targetDiff = targetBalance - cleared;
|
||||
|
@ -669,30 +671,46 @@ const AccountHeader = React.memo(
|
|||
/>
|
||||
</InitialFocus>
|
||||
) : isNameEditable ? (
|
||||
<Button
|
||||
bare
|
||||
<View
|
||||
style={{
|
||||
fontSize: 25,
|
||||
fontWeight: 500,
|
||||
marginLeft: -5,
|
||||
marginTop: -5,
|
||||
backgroundColor: 'transparent',
|
||||
'& svg': { display: 'none' },
|
||||
'&:hover svg': { display: 'unset' }
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
'& .hover-visible': {
|
||||
opacity: 0,
|
||||
transition: 'opacity .25s'
|
||||
},
|
||||
'&:hover .hover-visible': {
|
||||
opacity: 1
|
||||
}
|
||||
}}
|
||||
onClick={() => onExposeName(true)}
|
||||
>
|
||||
{accountName}
|
||||
|
||||
<Pencil1
|
||||
<View
|
||||
style={{
|
||||
width: 11,
|
||||
height: 11,
|
||||
marginLeft: 5,
|
||||
color: colors.n4
|
||||
fontSize: 25,
|
||||
fontWeight: 500,
|
||||
marginRight: 5,
|
||||
marginBottom: 5
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
>
|
||||
{accountName}
|
||||
</View>
|
||||
|
||||
<NotesButton id={`account-${account.id}`} />
|
||||
<Button
|
||||
bare
|
||||
className="hover-visible"
|
||||
onClick={() => onExposeName(true)}
|
||||
>
|
||||
<Pencil1
|
||||
style={{
|
||||
width: 11,
|
||||
height: 11,
|
||||
color: colors.n8
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{ fontSize: 25, fontWeight: 500, marginBottom: 5 }}
|
||||
|
|
|
@ -336,7 +336,7 @@ function StatusCell({
|
|||
? colors.y5
|
||||
: selected
|
||||
? colors.b7
|
||||
: colors.n6
|
||||
: colors.n7
|
||||
};
|
||||
|
||||
function onSelect() {
|
||||
|
@ -362,7 +362,8 @@ function StatusCell({
|
|||
':focus': {
|
||||
border: '1px solid ' + props.color,
|
||||
boxShadow: `0 1px 2px ${props.color}`
|
||||
}
|
||||
},
|
||||
cursor: isClearedField ? 'pointer' : 'default'
|
||||
},
|
||||
|
||||
isChild && { visibility: 'hidden' }
|
||||
|
|
|
@ -238,9 +238,9 @@ function ScheduleValue({ value }) {
|
|||
field="rule"
|
||||
data={schedules}
|
||||
describe={s => {
|
||||
let { payee } = extractScheduleConds(s._conditions);
|
||||
return payee
|
||||
? `${byId[payee.value].name} (${s.next_date})`
|
||||
let payeeId = s._payee;
|
||||
return payeeId
|
||||
? `${byId[payeeId].name} (${s.next_date})`
|
||||
: `Next: ${s.next_date}`;
|
||||
}}
|
||||
/>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { colors } from 'loot-design/src/style';
|
|||
import AlertTriangle from 'loot-design/src/svg/v2/AlertTriangle';
|
||||
import CalendarIcon from 'loot-design/src/svg/v2/Calendar';
|
||||
import CheckCircle1 from 'loot-design/src/svg/v2/CheckCircle1';
|
||||
import CheckCircleHollow from 'loot-design/src/svg/v2/CheckCircleHollow';
|
||||
import EditSkull1 from 'loot-design/src/svg/v2/EditSkull1';
|
||||
import FavoriteStar from 'loot-design/src/svg/v2/FavoriteStar';
|
||||
import ValidationCheck from 'loot-design/src/svg/v2/ValidationCheck';
|
||||
|
@ -49,10 +50,15 @@ export function getStatusProps(status) {
|
|||
backgroundColor = colors.n11;
|
||||
Icon = CalendarIcon;
|
||||
break;
|
||||
case 'cleared':
|
||||
color = colors.g5;
|
||||
backgroundColor = colors.n11;
|
||||
Icon = CheckCircle1;
|
||||
break;
|
||||
default:
|
||||
color = colors.n1;
|
||||
backgroundColor = colors.n11;
|
||||
Icon = CheckCircle1;
|
||||
Icon = CheckCircleHollow;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -362,8 +362,8 @@ ipcMain.on('screenshot', () => {
|
|||
let width = 1100;
|
||||
|
||||
// This is for the main screenshot inside the frame
|
||||
clientWin.setSize(width, (width * (427 / 623)) | 0);
|
||||
// clientWin.setSize(width, (width * (495 / 700)) | 0);
|
||||
clientWin.setSize(width, Math.floor(width * (427 / 623)));
|
||||
// clientWin.setSize(width, Math.floor(width * (495 / 700)));
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"productName": "Actual",
|
||||
"author": "Shift Reset LLC",
|
||||
"description": "A simple and powerful personal finance system",
|
||||
"version": "4.0.2",
|
||||
"version": "22.12.03",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"build": "electron-builder",
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
// This is a special usage of the API because this package is embedded
|
||||
// into Actual itself. We only want to pull in the methods in that
|
||||
// case and ignore everything else; otherwise we'd be pulling in the
|
||||
// entire backend bundle from the API
|
||||
const actual = require('@actual-app/api/methods');
|
||||
const { amountToInteger } = require('@actual-app/api/utils');
|
||||
const AdmZip = require('adm-zip');
|
||||
const d = require('date-fns');
|
||||
const normalizePathSep = require('slash');
|
||||
const uuid = require('uuid');
|
||||
const AdmZip = require('adm-zip');
|
||||
const actual = require('@actual-app/api');
|
||||
const amountToInteger = actual.utils.amountToInteger;
|
||||
|
||||
// Utils
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"bin": "./index.js",
|
||||
"homepage": "https://github.com/actualbudget/actual/tree/master/packages/import-ynab4#readme",
|
||||
"dependencies": {
|
||||
"@actual-app/api": "^1.0.0",
|
||||
"@actual-app/api": "*",
|
||||
"adm-zip": "^0.5.9",
|
||||
"date-fns": "2.0.0-alpha.27",
|
||||
"slash": "3.0.0",
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
// This is a special usage of the API because this package is embedded
|
||||
// into Actual itself. We only want to pull in the methods in that
|
||||
// case and ignore everything else; otherwise we'd be pulling in the
|
||||
// entire backend bundle from the API
|
||||
const actual = require('@actual-app/api/methods');
|
||||
const d = require('date-fns');
|
||||
const uuid = require('uuid');
|
||||
const actual = require('@actual-app/api');
|
||||
|
||||
function amountFromYnab(amount) {
|
||||
// ynabs multiplies amount by 1000 and actual by 100
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"bin": "./index.js",
|
||||
"homepage": "https://github.com/actualbudget/actual/tree/master/packages/import-ynab5#readme",
|
||||
"dependencies": {
|
||||
"@actual-app/api": "^1.0.0",
|
||||
"@actual-app/api": "*",
|
||||
"date-fns": "2.0.0-alpha.27",
|
||||
"uuid": "3.3.2"
|
||||
}
|
||||
|
|
|
@ -35,31 +35,31 @@ async function init() {
|
|||
for (let i = 0; i < 100; i++) {
|
||||
if (Math.random() < 0.02) {
|
||||
let parent = {
|
||||
date: '2020-01-' + pad((Math.random() * 30) | 0),
|
||||
amount: (Math.random() * 10000) | 0,
|
||||
date: '2020-01-' + pad(Math.floor(Math.random() * 30)),
|
||||
amount: Math.floor(Math.random() * 10000),
|
||||
account: accounts[0].id,
|
||||
notes: 'foo'
|
||||
};
|
||||
db.insertTransaction(parent);
|
||||
db.insertTransaction(
|
||||
makeChild(parent, {
|
||||
amount: (Math.random() * 1000) | 0
|
||||
amount: Math.floor(Math.random() * 1000)
|
||||
})
|
||||
);
|
||||
db.insertTransaction(
|
||||
makeChild(parent, {
|
||||
amount: (Math.random() * 1000) | 0
|
||||
amount: Math.floor(Math.random() * 1000)
|
||||
})
|
||||
);
|
||||
db.insertTransaction(
|
||||
makeChild(parent, {
|
||||
amount: (Math.random() * 1000) | 0
|
||||
amount: Math.floor(Math.random() * 1000)
|
||||
})
|
||||
);
|
||||
} else {
|
||||
db.insertTransaction({
|
||||
date: '2020-01-' + pad((Math.random() * 30) | 0),
|
||||
amount: (Math.random() * 10000) | 0,
|
||||
date: '2020-01-' + pad(Math.floor(Math.random() * 30)),
|
||||
amount: Math.floor(Math.random() * 10000),
|
||||
account: accounts[0].id
|
||||
});
|
||||
}
|
||||
|
|
|
@ -24,21 +24,12 @@ export function applyBudgetAction(month, type, args) {
|
|||
case 'set-3-avg':
|
||||
await send('budget/set-3month-avg', { month });
|
||||
break;
|
||||
case 'set-all-future':
|
||||
await send('budget/set-all-future', { startMonth: month });
|
||||
break;
|
||||
case 'hold':
|
||||
await send('budget/hold-for-next-month', {
|
||||
month,
|
||||
amount: args.amount
|
||||
});
|
||||
break;
|
||||
case 'hold-all-future':
|
||||
await send('budget/hold-for-future-months', {
|
||||
startMonth: month,
|
||||
amount: args.amount
|
||||
});
|
||||
break;
|
||||
case 'reset-hold':
|
||||
await send('budget/reset-hold', { month });
|
||||
break;
|
||||
|
|
|
@ -10,10 +10,6 @@ import {
|
|||
import q from '../shared/query';
|
||||
import { currencyToAmount, amountToInteger } from '../shared/util';
|
||||
|
||||
function isInteger(num) {
|
||||
return (num | 0) === num;
|
||||
}
|
||||
|
||||
export function getAccountFilter(accountId, field = 'account') {
|
||||
if (accountId) {
|
||||
if (accountId === 'budgeted') {
|
||||
|
@ -82,7 +78,7 @@ export function makeTransactionSearchQuery(currentQuery, search, dateFormat) {
|
|||
amount: { $transform: '$abs', $eq: amountToInteger(amount) }
|
||||
},
|
||||
amount != null &&
|
||||
isInteger(amount) && {
|
||||
Number.isInteger(amount) && {
|
||||
amount: {
|
||||
$transform: { $abs: { $idiv: ['$', 100] } },
|
||||
$eq: amount
|
||||
|
|
|
@ -93,7 +93,7 @@ function initBasicServer(delay) {
|
|||
function initPagingServer(dataLength, { delay, eventType = 'select' } = {}) {
|
||||
let data = [];
|
||||
for (let i = 0; i < dataLength; i++) {
|
||||
data.push({ id: i, date: subDays('2020-05-01', (i / 5) | 0) });
|
||||
data.push({ id: i, date: subDays('2020-05-01', Math.floor(i / 5)) });
|
||||
}
|
||||
|
||||
initServer({
|
||||
|
|
|
@ -11,7 +11,7 @@ import * as monthUtils from '../shared/months';
|
|||
import q from '../shared/query';
|
||||
|
||||
function pickRandom(list) {
|
||||
return list[((Math.random() * list.length) | 0) % list.length];
|
||||
return list[Math.floor(Math.random() * list.length) % list.length];
|
||||
}
|
||||
|
||||
function number(start, end) {
|
||||
|
@ -19,7 +19,7 @@ function number(start, end) {
|
|||
}
|
||||
|
||||
function integer(start, end) {
|
||||
return number(start, end) | 0;
|
||||
return Math.round(number(start, end));
|
||||
}
|
||||
|
||||
function findMin(items, field) {
|
||||
|
@ -104,13 +104,13 @@ async function fillPrimaryChecking(handlers, account, payees, groups) {
|
|||
amount,
|
||||
payee: payee.id,
|
||||
account: account.id,
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), (i / 3) | 0),
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), Math.floor(i / 3)),
|
||||
category: category.id
|
||||
};
|
||||
transactions.push(transaction);
|
||||
|
||||
if (Math.random() < 0.2) {
|
||||
let a = (transaction.amount / 3) | 0;
|
||||
let a = Math.round(transaction.amount / 3);
|
||||
let pick = () =>
|
||||
payee === incomePayee
|
||||
? incomeGroup.categories.find(c => c.name === 'Income').id
|
||||
|
@ -244,7 +244,7 @@ async function fillChecking(handlers, account, payees, groups) {
|
|||
amount,
|
||||
payee: payee.id,
|
||||
account: account.id,
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), (i * 2) | 0),
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), i * 2),
|
||||
category: category.id
|
||||
});
|
||||
}
|
||||
|
@ -334,7 +334,7 @@ async function fillSavings(handlers, account, payees, groups) {
|
|||
amount,
|
||||
payee: payee.id,
|
||||
account: account.id,
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), (i * 5) | 0),
|
||||
date: monthUtils.subDays(monthUtils.currentDay(), i * 5),
|
||||
category: category.id
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ export function generateAccount(name, isConnected, type, offbudget) {
|
|||
return {
|
||||
id: uuid.v4Sync(),
|
||||
name,
|
||||
balance_current: isConnected ? (Math.random() * 100000) | 0 : null,
|
||||
bank: isConnected ? (Math.random() * 10000) | 0 : null,
|
||||
bankId: isConnected ? (Math.random() * 10000) | 0 : null,
|
||||
balance_current: isConnected ? Math.floor(Math.random() * 100000) : null,
|
||||
bank: isConnected ? Math.floor(Math.random() * 10000) : null,
|
||||
bankId: isConnected ? Math.floor(Math.random() * 10000) : null,
|
||||
bankName: isConnected ? 'boa' : null,
|
||||
type: type || 'checking',
|
||||
offbudget: offbudget ? 1 : 0,
|
||||
|
@ -54,7 +54,7 @@ function _generateTransaction(data) {
|
|||
const id = data.id || uuid.v4Sync();
|
||||
return {
|
||||
id: id,
|
||||
amount: data.amount || (Math.random() * 10000 - 7000) | 0,
|
||||
amount: data.amount || Math.floor(Math.random() * 10000 - 7000),
|
||||
payee: data.payee || (Math.random() < 0.9 ? 'payed-to' : 'guy'),
|
||||
notes:
|
||||
Math.random() < 0.1 ? 'A really long note that should overflow' : 'Notes',
|
||||
|
|
|
@ -456,7 +456,7 @@ function compileLiteral(value) {
|
|||
} else if (typeof value === 'boolean') {
|
||||
return typed(value ? 1 : 0, 'boolean', { literal: true });
|
||||
} else if (typeof value === 'number') {
|
||||
return typed(value, (value | 0) === value ? 'integer' : 'float', {
|
||||
return typed(value, Number.isInteger(value) ? 'integer' : 'float', {
|
||||
literal: true
|
||||
});
|
||||
} else if (Array.isArray(value)) {
|
||||
|
|
|
@ -42,7 +42,7 @@ export function convertInputType(value, type) {
|
|||
}
|
||||
return value;
|
||||
case 'integer':
|
||||
if (typeof value === 'number' && (value | 0) === value) {
|
||||
if (typeof value === 'number' && Number.isInteger(value)) {
|
||||
return value;
|
||||
} else {
|
||||
throw new Error("Can't convert to integer: " + JSON.stringify(value));
|
||||
|
|
|
@ -91,7 +91,7 @@ function expectTransactionOrder(data, fields) {
|
|||
}
|
||||
|
||||
async function expectPagedData(query, numTransactions, allData) {
|
||||
let pageCount = Math.max((numTransactions / 3) | 0, 3);
|
||||
let pageCount = Math.max(Math.floor(numTransactions / 3), 3);
|
||||
let pagedData = [];
|
||||
let done = false;
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as monthUtils from '../../shared/months';
|
||||
import { safeNumber } from '../../shared/util';
|
||||
import * as db from '../db';
|
||||
import * as prefs from '../prefs';
|
||||
import * as sheet from '../sheet';
|
||||
|
@ -6,7 +7,7 @@ import { batchMessages } from '../sync';
|
|||
|
||||
async function getSheetValue(sheetName, cell) {
|
||||
const node = await sheet.getCell(sheetName, cell);
|
||||
return typeof node.value === 'number' ? node.value : 0;
|
||||
return safeNumber(typeof node.value === 'number' ? node.value : 0);
|
||||
}
|
||||
|
||||
// We want to only allow the positive movement of money back and
|
||||
|
@ -71,9 +72,7 @@ export function getBudget({ category, month }) {
|
|||
}
|
||||
|
||||
export function setBudget({ category, month, amount }) {
|
||||
if (typeof amount !== 'number') {
|
||||
amount = 0;
|
||||
}
|
||||
amount = safeNumber(typeof amount === 'number' ? amount : 0);
|
||||
const table = getBudgetTable();
|
||||
|
||||
let existing = db.firstSync(
|
||||
|
@ -185,32 +184,12 @@ export async function set3MonthAvg({ month }) {
|
|||
'sum-amount-' + cat.id
|
||||
);
|
||||
|
||||
const avg = ((spent1 + spent2 + spent3) / 3) | 0;
|
||||
const avg = Math.round((spent1 + spent2 + spent3) / 3);
|
||||
setBudget({ category: cat.id, month, amount: -avg });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function setAllFuture({ startMonth }) {
|
||||
if (!isReflectBudget()) {
|
||||
throw new Error('setAllFuture only applies to report budget type');
|
||||
}
|
||||
let table = getBudgetTable();
|
||||
let budgetData = await getBudgetData(table, dbMonth(startMonth));
|
||||
let months = getAllMonths(monthUtils.addMonths(startMonth, 1));
|
||||
|
||||
batchMessages(() => {
|
||||
for (let month of months) {
|
||||
budgetData.forEach(budget => {
|
||||
if (budget.is_income === 1 && !isReflectBudget()) {
|
||||
return;
|
||||
}
|
||||
setBudget({ category: budget.category, month, amount: budget.amount });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function holdForNextMonth({ month, amount }) {
|
||||
let row = await db.first(
|
||||
'SELECT buffered FROM zero_budget_months WHERE id = ?',
|
||||
|
@ -233,18 +212,6 @@ export async function holdForNextMonth({ month, amount }) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export async function holdForFutureMonths({ startMonth, amount }) {
|
||||
let months = getAllMonths(startMonth);
|
||||
|
||||
await batchMessages(async () => {
|
||||
for (let month of months) {
|
||||
if (!(await holdForNextMonth({ month, amount }))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function resetHold({ month }) {
|
||||
await setBuffer(month, 0);
|
||||
}
|
||||
|
|
|
@ -12,15 +12,10 @@ app.method(
|
|||
);
|
||||
app.method('budget/set-zero', mutator(undoable(actions.setZero)));
|
||||
app.method('budget/set-3month-avg', mutator(undoable(actions.set3MonthAvg)));
|
||||
app.method('budget/set-all-future', mutator(undoable(actions.setAllFuture)));
|
||||
app.method(
|
||||
'budget/hold-for-next-month',
|
||||
mutator(undoable(actions.holdForNextMonth))
|
||||
);
|
||||
app.method(
|
||||
'budget/hold-for-future-months',
|
||||
mutator(undoable(actions.holdForFutureMonths))
|
||||
);
|
||||
app.method('budget/reset-hold', mutator(undoable(actions.resetHold)));
|
||||
app.method(
|
||||
'budget/cover-overspending',
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { safeNumber } from '../../shared/util';
|
||||
import * as sheet from '../sheet';
|
||||
import { number, sumAmounts } from './util';
|
||||
|
||||
|
@ -25,17 +26,17 @@ export async function createCategory(cat, sheetName, prevSheetName) {
|
|||
],
|
||||
run: (budgeted, sumAmount, prevCarryover, prevLeftover) => {
|
||||
if (cat.is_income) {
|
||||
return (
|
||||
return safeNumber(
|
||||
number(budgeted) -
|
||||
number(sumAmount) +
|
||||
(prevCarryover ? number(prevLeftover) : 0)
|
||||
number(sumAmount) +
|
||||
(prevCarryover ? number(prevLeftover) : 0)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
return safeNumber(
|
||||
number(budgeted) +
|
||||
number(sumAmount) +
|
||||
(prevCarryover ? number(prevLeftover) : 0)
|
||||
number(sumAmount) +
|
||||
(prevCarryover ? number(prevLeftover) : 0)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -50,7 +51,7 @@ export async function createCategory(cat, sheetName, prevSheetName) {
|
|||
refresh: true,
|
||||
run: (budgeted, sumAmount, carryover) => {
|
||||
return carryover
|
||||
? Math.max(0, number(budgeted) + number(sumAmount))
|
||||
? Math.max(0, safeNumber(number(budgeted) + number(sumAmount)))
|
||||
: sumAmount;
|
||||
}
|
||||
});
|
||||
|
@ -109,7 +110,7 @@ export function createSummary(groups, categories, sheetName) {
|
|||
initialValue: 0,
|
||||
dependencies: ['total-income', 'total-spent'],
|
||||
run: (income, spent) => {
|
||||
return income - -spent;
|
||||
return safeNumber(income - -spent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import * as monthUtils from '../../shared/months';
|
||||
import { safeNumber } from '../../shared/util';
|
||||
import * as sheet from '../sheet';
|
||||
import { number, sumAmounts, flatten2, unflatten2 } from './util';
|
||||
|
||||
|
@ -51,10 +52,10 @@ export function createCategory(cat, sheetName, prevSheetName) {
|
|||
`${prevSheetName}!leftover-pos-${cat.id}`
|
||||
],
|
||||
run: (budgeted, spent, prevCarryover, prevLeftover, prevLeftoverPos) => {
|
||||
return (
|
||||
return safeNumber(
|
||||
number(budgeted) +
|
||||
number(spent) +
|
||||
(prevCarryover ? number(prevLeftover) : number(prevLeftoverPos))
|
||||
number(spent) +
|
||||
(prevCarryover ? number(prevLeftover) : number(prevLeftoverPos))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -78,7 +79,7 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
|
|||
sheet.get().createDynamic(sheetName, 'from-last-month', {
|
||||
initialValue: 0,
|
||||
dependencies: [`${prevSheetName}!to-budget`, `${prevSheetName}!buffered`],
|
||||
run: (toBudget, buffered) => number(toBudget) + number(buffered)
|
||||
run: (toBudget, buffered) => safeNumber(number(toBudget) + number(buffered))
|
||||
});
|
||||
|
||||
// Alias the group income total to `total-income`
|
||||
|
@ -91,7 +92,8 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
|
|||
sheet.get().createDynamic(sheetName, 'available-funds', {
|
||||
initialValue: 0,
|
||||
dependencies: ['total-income', 'from-last-month'],
|
||||
run: (income, fromLastMonth) => number(income) + number(fromLastMonth)
|
||||
run: (income, fromLastMonth) =>
|
||||
safeNumber(number(income) + number(fromLastMonth))
|
||||
});
|
||||
|
||||
sheet.get().createDynamic(sheetName, 'last-month-overspent', {
|
||||
|
@ -104,12 +106,14 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
|
|||
),
|
||||
run: (...data) => {
|
||||
data = unflatten2(data);
|
||||
return data.reduce((total, [leftover, carryover]) => {
|
||||
if (carryover) {
|
||||
return total;
|
||||
}
|
||||
return total + Math.min(0, number(leftover));
|
||||
}, 0);
|
||||
return safeNumber(
|
||||
data.reduce((total, [leftover, carryover]) => {
|
||||
if (carryover) {
|
||||
return total;
|
||||
}
|
||||
return total + Math.min(0, number(leftover));
|
||||
}, 0)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -135,11 +139,11 @@ export function createSummary(groups, categories, prevSheetName, sheetName) {
|
|||
'buffered'
|
||||
],
|
||||
run: (available, lastOverspent, totalBudgeted, buffered) => {
|
||||
return (
|
||||
return safeNumber(
|
||||
number(available) +
|
||||
number(lastOverspent) +
|
||||
number(totalBudgeted) -
|
||||
number(buffered)
|
||||
number(lastOverspent) +
|
||||
number(totalBudgeted) -
|
||||
number(buffered)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { safeNumber } from '../../shared/util';
|
||||
import { number } from '../spreadsheet/globals';
|
||||
|
||||
export { number } from '../spreadsheet/globals';
|
||||
|
||||
export function sumAmounts(...amounts) {
|
||||
return amounts.reduce((total, amount) => {
|
||||
return total + number(amount);
|
||||
}, 0);
|
||||
return safeNumber(
|
||||
amounts.reduce((total, amount) => {
|
||||
return total + number(amount);
|
||||
}, 0)
|
||||
);
|
||||
}
|
||||
|
||||
export function flatten2(arr) {
|
||||
|
|
|
@ -22,7 +22,7 @@ export function keyToTimestamp(key) {
|
|||
|
||||
export function insert(trie, timestamp) {
|
||||
let hash = timestamp.hash();
|
||||
let key = Number((timestamp.millis() / 1000 / 60) | 0).toString(3);
|
||||
let key = Number(Math.floor(timestamp.millis() / 1000 / 60)).toString(3);
|
||||
|
||||
trie = Object.assign({}, trie, { hash: trie.hash ^ hash });
|
||||
return insertKey(trie, key, hash);
|
||||
|
|
|
@ -34,7 +34,7 @@ export function shoveSortOrders(items, targetId) {
|
|||
} else {
|
||||
if (target.sort_order - (before ? before.sort_order : 0) <= 2) {
|
||||
let next = to;
|
||||
let order = (items[next].sort_order | 0) + SORT_INCREMENT;
|
||||
let order = Math.floor(items[next].sort_order) + SORT_INCREMENT;
|
||||
while (next < items.length) {
|
||||
// No need to update it if it's already greater than the current
|
||||
// order. This can happen because there may already be large
|
||||
|
|
|
@ -171,7 +171,7 @@ function shuffle(arr) {
|
|||
let shuffled = new Array(src.length);
|
||||
let item;
|
||||
while ((item = src.pop())) {
|
||||
let idx = (Math.random() * shuffled.length) | 0;
|
||||
let idx = Math.floor(Math.random() * shuffled.length);
|
||||
if (shuffled[idx]) {
|
||||
src.push(item);
|
||||
} else {
|
||||
|
|
|
@ -199,5 +199,5 @@ export function makeValue(value, cond) {
|
|||
}
|
||||
|
||||
export function getApproxNumberThreshold(number) {
|
||||
return (Math.abs(number) * 0.075) | 0;
|
||||
return Math.round(Math.abs(number) * 0.075);
|
||||
}
|
||||
|
|
|
@ -221,7 +221,7 @@ export function extractScheduleConds(conditions) {
|
|||
|
||||
export function getScheduledAmount(amount) {
|
||||
if (amount && typeof amount !== 'number') {
|
||||
return ((amount.num1 + amount.num2) / 2) | 0;
|
||||
return Math.round((amount.num1 + amount.num2) / 2);
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
|
|
@ -298,6 +298,30 @@ export function getNumberFormat() {
|
|||
|
||||
setNumberFormat('comma-dot');
|
||||
|
||||
// Number utilities
|
||||
|
||||
// We dont use `Number.MAX_SAFE_NUMBER` and such here because those
|
||||
// numbers are so large that it's not safe to convert them to floats
|
||||
// (i.e. N / 100). For example, `9007199254740987 / 100 ===
|
||||
// 90071992547409.88`. While the internal arithemetic would be correct
|
||||
// because we always do that on numbers, the app would potentially
|
||||
// display wrong numbers. Instead of `2**53` we use `2**51` which
|
||||
// gives division more room to be correct
|
||||
const MAX_SAFE_NUMBER = 2 ** 51 - 1;
|
||||
const MIN_SAFE_NUMBER = -MAX_SAFE_NUMBER;
|
||||
|
||||
export function safeNumber(value) {
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new Error('safeNumber: number is not an integer: ' + value);
|
||||
}
|
||||
if (value > MAX_SAFE_NUMBER || value < MIN_SAFE_NUMBER) {
|
||||
throw new Error(
|
||||
"safeNumber: can't safely perform arithmetic with number: " + value
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function toRelaxedNumber(value) {
|
||||
return integerToAmount(currencyToInteger(value) || 0);
|
||||
}
|
||||
|
@ -307,8 +331,7 @@ export function toRelaxedInteger(value) {
|
|||
}
|
||||
|
||||
export function integerToCurrency(n) {
|
||||
// Awesome
|
||||
return numberFormat.formatter.format(n / 100);
|
||||
return numberFormat.formatter.format(safeNumber(n) / 100);
|
||||
}
|
||||
|
||||
export function amountToCurrency(n) {
|
||||
|
@ -340,7 +363,7 @@ export function amountToInteger(n) {
|
|||
}
|
||||
|
||||
export function integerToAmount(n) {
|
||||
return parseFloat((n / 100).toFixed(2));
|
||||
return parseFloat((safeNumber(n) / 100).toFixed(2));
|
||||
}
|
||||
|
||||
// This is used when the input format could be anything (from
|
||||
|
|
|
@ -368,10 +368,6 @@ export default React.memo(function BudgetSummary({ month }) {
|
|||
{
|
||||
name: 'set-3-avg',
|
||||
text: 'Set budgets to 3 month avg'
|
||||
},
|
||||
{
|
||||
name: 'set-all-future',
|
||||
text: 'Apply to all future budgets'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -204,10 +204,6 @@ function ToBudget({ month, prevMonthName, collapsed, onBudgetAction }) {
|
|||
name: 'buffer',
|
||||
text: 'Hold for next month'
|
||||
},
|
||||
{
|
||||
name: 'buffer-future',
|
||||
text: 'Hold for all future months'
|
||||
},
|
||||
{
|
||||
name: 'reset-buffer',
|
||||
text: "Reset next month's buffer"
|
||||
|
@ -224,14 +220,6 @@ function ToBudget({ month, prevMonthName, collapsed, onBudgetAction }) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{state.menuOpen === 'buffer-future' && (
|
||||
<HoldTooltip
|
||||
onClose={() => setState({ menuOpen: null })}
|
||||
onSubmit={amount => {
|
||||
onBudgetAction(month, 'hold-all-future', { amount });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{state.menuOpen === 'transfer' && (
|
||||
<TransferTooltip
|
||||
initialAmountName="leftover"
|
||||
|
|
|
@ -938,10 +938,6 @@ export function ModalButtons({
|
|||
style
|
||||
]}
|
||||
>
|
||||
{/* Add a dummy button first so that when a user
|
||||
presses "enter" they do a normal submit, instead of
|
||||
activating the back button */}
|
||||
<Button data-hidden={true} style={{ display: 'none' }} />
|
||||
{leftContent}
|
||||
<View style={{ flex: 1 }} />
|
||||
{children}
|
||||
|
|
|
@ -116,7 +116,7 @@ function CloseAccount({
|
|||
<View>
|
||||
<P>
|
||||
This account has a balance of{' '}
|
||||
<strong>${integerToCurrency(balance)}</strong>. To close
|
||||
<strong>{integerToCurrency(balance)}</strong>. To close
|
||||
this account, select a different account to transfer this
|
||||
balance to:
|
||||
</P>
|
||||
|
|
|
@ -146,7 +146,9 @@ function CreateLocalAccount({ modalProps, actions, history }) {
|
|||
)}
|
||||
|
||||
<ModalButtons>
|
||||
<Button onClick={() => modalProps.onBack()}>Back</Button>
|
||||
<Button onClick={() => modalProps.onBack()} type="button">
|
||||
Back
|
||||
</Button>
|
||||
<Button primary style={{ marginLeft: 10 }}>
|
||||
Create
|
||||
</Button>
|
||||
|
|
|
@ -397,6 +397,9 @@ const MenuButton = withRouter(function MenuButton({ history }) {
|
|||
case 'settings':
|
||||
history.push('/settings');
|
||||
break;
|
||||
case 'help':
|
||||
window.open('https://actualbudget.github.io/docs', '_blank');
|
||||
break;
|
||||
case 'close':
|
||||
dispatch(closeBudget());
|
||||
break;
|
||||
|
@ -411,6 +414,7 @@ const MenuButton = withRouter(function MenuButton({ history }) {
|
|||
{ name: 'repair-splits', text: 'Repair split transactions' },
|
||||
Menu.line,
|
||||
{ name: 'settings', text: 'Settings' },
|
||||
{ name: 'help', text: 'Help' },
|
||||
{ name: 'close', text: 'Close File' }
|
||||
];
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function useSheetValue(binding, onChange) {
|
|||
let spreadsheet = useContext(SpreadsheetContext);
|
||||
let [result, setResult] = useState({
|
||||
name: sheetName + '!' + binding.name,
|
||||
value: binding.value,
|
||||
value: binding.value === undefined ? null : binding.value,
|
||||
query: binding.query
|
||||
});
|
||||
let latestOnChange = useRef(onChange);
|
||||
|
|
|
@ -7,7 +7,7 @@ let groups = ['y', 'r', 'b', 'n', 'g', 'p'];
|
|||
let colors = {};
|
||||
|
||||
list.forEach((color, idx) => {
|
||||
const group = (idx / 11) | 0;
|
||||
const group = Math.floor(idx / 11);
|
||||
const n = idx % 11;
|
||||
|
||||
colors[groups[group] + (n + 1)] = color;
|
||||
|
|
|
@ -24,11 +24,11 @@ global.Date.now = () => 123456789;
|
|||
|
||||
let seqId = 1;
|
||||
uuid.v4 = function() {
|
||||
return Promise.resolve('testing-uuid-' + ((Math.random() * 1000000) | 0));
|
||||
return Promise.resolve('testing-uuid-' + Math.floor(Math.random() * 1000000));
|
||||
};
|
||||
|
||||
uuid.v4Sync = function() {
|
||||
return 'testing-uuid-' + ((Math.random() * 1000000) | 0);
|
||||
return 'testing-uuid-' + Math.floor(Math.random() * 1000000);
|
||||
};
|
||||
|
||||
global.__resetWorld = () => {
|
||||
|
|
19
packages/loot-design/src/svg/v2/CheckCircleHollow.js
Normal file
19
packages/loot-design/src/svg/v2/CheckCircleHollow.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
|
||||
const SvgCheckCircleHollow = props => (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
color: '#242134',
|
||||
...props.style
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M 12 0 C 1.3084197 0 -4.0435475 12.925204 3.515625 20.484375 C 11.074797 28.043547 24 22.69158 24 12 C 23.992285 5.3757944 18.624205 0.0077147446 12 0 z M 12.009766 1.9882812 C 17.531104 1.9947115 22.005288 6.4688953 22.011719 11.990234 C 22.011719 20.90177 11.238144 25.363144 4.9375 19.0625 C -1.3631434 12.761856 3.0982293 1.9882812 12.009766 1.9882812 z M 18.244141 6.5761719 A 1 1 0 0 0 17.316406 7.0175781 L 11.089844 15.46875 L 7.0136719 12.207031 A 1.0004882 1.0004882 0 1 0 5.7636719 13.769531 L 10.652344 17.677734 A 1.011 1.011 0 0 0 12.082031 17.488281 L 18.927734 8.1992188 A 1 1 0 0 0 18.244141 6.5761719 z "
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default SvgCheckCircleHollow;
|
|
@ -330,11 +330,6 @@ class Budget extends React.Component {
|
|||
case 3:
|
||||
this.onBudgetAction('set-3-avg');
|
||||
break;
|
||||
case 4:
|
||||
if (budgetType === 'report') {
|
||||
this.onBudgetAction('set-all-future');
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
|
43
yarn.lock
43
yarn.lock
|
@ -22,21 +22,11 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@actual-app/api@npm:^1.0.0":
|
||||
version: 1.1.3
|
||||
resolution: "@actual-app/api@npm:1.1.3"
|
||||
dependencies:
|
||||
node-ipc: 9.1.1
|
||||
uuid: 3.3.2
|
||||
checksum: e7fccff7583d64ac908eb7a7c93226200fd75af92b9fe9718b6e3fe0d004d92d79d87485e212b0d3d86cb685827e6733c939ece799156eea64db886bf1457a94
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@actual-app/import-ynab4@*, @actual-app/import-ynab4@workspace:packages/import-ynab4":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/import-ynab4@workspace:packages/import-ynab4"
|
||||
dependencies:
|
||||
"@actual-app/api": ^1.0.0
|
||||
"@actual-app/api": "*"
|
||||
adm-zip: ^0.5.9
|
||||
date-fns: 2.0.0-alpha.27
|
||||
slash: 3.0.0
|
||||
|
@ -50,7 +40,7 @@ __metadata:
|
|||
version: 0.0.0-use.local
|
||||
resolution: "@actual-app/import-ynab5@workspace:packages/import-ynab5"
|
||||
dependencies:
|
||||
"@actual-app/api": ^1.0.0
|
||||
"@actual-app/api": "*"
|
||||
date-fns: 2.0.0-alpha.27
|
||||
uuid: 3.3.2
|
||||
bin:
|
||||
|
@ -8844,7 +8834,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"easy-stack@npm:^1.0.0, easy-stack@npm:^1.0.1":
|
||||
"easy-stack@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "easy-stack@npm:1.0.1"
|
||||
checksum: 161a99e497b3857b0be4ec9e1ebbe90b241ea9d84702f9881b8e5b3f6822065b8c4e33436996935103e191bffba3607de70712a792f4d406a050def48c6bc381
|
||||
|
@ -13813,13 +13803,6 @@ jest-snapshot@test:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-message@npm:1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "js-message@npm:1.0.5"
|
||||
checksum: fd2fc8837a88a115aa2fa859bf5c13d9b335fd7eeba8426c44da6eb006b04c52cfe6675b3c27d6b112ffc51dadb8bc51d58340c3a3aa5c555d7da6bdc72ce9c0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-message@npm:1.0.7":
|
||||
version: 1.0.7
|
||||
resolution: "js-message@npm:1.0.7"
|
||||
|
@ -13827,15 +13810,6 @@ jest-snapshot@test:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-queue@npm:2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "js-queue@npm:2.0.0"
|
||||
dependencies:
|
||||
easy-stack: ^1.0.0
|
||||
checksum: 8f8e589cc20fd3bc3067db73ecaac77b55411c3ac58fdd6882868924ee19ab4203d19e68d3ec680c5c8f5e8282e30dafa377014dbec05c3f2d33be4596f4fb65
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-queue@npm:2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "js-queue@npm:2.0.2"
|
||||
|
@ -16222,17 +16196,6 @@ jest-snapshot@test:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-ipc@npm:9.1.1":
|
||||
version: 9.1.1
|
||||
resolution: "node-ipc@npm:9.1.1"
|
||||
dependencies:
|
||||
event-pubsub: 4.3.0
|
||||
js-message: 1.0.5
|
||||
js-queue: 2.0.0
|
||||
checksum: 2b66099d1976e4328d34ae7fec853d3969ca337b52b5aefb48ae1d19387c37d6716c2b98d4a4934ec24aa79f0441721961d6c1beb858c294ad6a7a97ddf5460d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-ipc@npm:9.1.4":
|
||||
version: 9.1.4
|
||||
resolution: "node-ipc@npm:9.1.4"
|
||||
|
|
Loading…
Reference in a new issue