Compare commits
2 Commits
v25.6.1
...
ts-LoadBac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e71dcccd8 | ||
|
|
de9a1880a7 |
@@ -1,11 +1,14 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||
{
|
||||
"name": "Actual development",
|
||||
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
"service": "actual-development",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"postCreateCommand": "yarn install"
|
||||
"name": "Actual development",
|
||||
"dockerComposeFile": [
|
||||
"../docker-compose.yml",
|
||||
"docker-compose.yml"
|
||||
],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
"service": "actual-development",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"postCreateCommand": "yarn install"
|
||||
}
|
||||
|
||||
27
.eslintignore
Normal file
@@ -0,0 +1,27 @@
|
||||
packages/api/app/bundle.api.js
|
||||
packages/api/dist
|
||||
packages/api/@types
|
||||
packages/api/migrations
|
||||
|
||||
packages/crdt/dist
|
||||
|
||||
packages/desktop-client/bundle.browser.js
|
||||
packages/desktop-client/build/
|
||||
packages/desktop-client/build-stats/
|
||||
packages/desktop-client/public/kcab/
|
||||
packages/desktop-client/public/data/
|
||||
packages/desktop-client/**/node_modules/*
|
||||
packages/desktop-client/node_modules/
|
||||
packages/desktop-client/src/icons/**/*
|
||||
packages/desktop-client/test-results/
|
||||
|
||||
packages/desktop-electron/client-build/
|
||||
packages/desktop-electron/dist/
|
||||
|
||||
packages/import-ynab4/**/node_modules/*
|
||||
|
||||
packages/import-ynab5/**/node_modules/*
|
||||
|
||||
packages/loot-core/**/node_modules/*
|
||||
packages/loot-core/**/lib-dist/*
|
||||
packages/loot-core/**/proto/*
|
||||
288
.eslintrc.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/* eslint-disable rulesdir/typography */
|
||||
const path = require('path');
|
||||
|
||||
const rulesDirPlugin = require('eslint-plugin-rulesdir');
|
||||
rulesDirPlugin.RULES_DIR = path.join(
|
||||
__dirname,
|
||||
'packages',
|
||||
'eslint-plugin-actual',
|
||||
'lib',
|
||||
'rules',
|
||||
);
|
||||
|
||||
const ruleFCMsg =
|
||||
'Type the props argument and let TS infer or use ComponentType for a component prop';
|
||||
|
||||
const restrictedImportPatterns = [
|
||||
{
|
||||
group: ['*.api', '*.web', '*.electron'],
|
||||
message: 'Don’t directly reference imports from other platforms',
|
||||
},
|
||||
{
|
||||
group: ['uuid'],
|
||||
importNames: ['*'],
|
||||
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
|
||||
},
|
||||
];
|
||||
|
||||
const restrictedImportColors = [
|
||||
{
|
||||
group: ['**/style', '**/colors'],
|
||||
importNames: ['colors'],
|
||||
message: 'Please use themes instead of colors',
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
plugins: ['prettier', 'import', 'rulesdir', '@typescript-eslint'],
|
||||
extends: [
|
||||
'react-app',
|
||||
'plugin:react/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:import/typescript',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: { project: [path.join(__dirname, './tsconfig.json')] },
|
||||
reportUnusedDisableDirectives: true,
|
||||
globals: {
|
||||
globalThis: false,
|
||||
vi: true,
|
||||
},
|
||||
rules: {
|
||||
'prettier/prettier': 'warn',
|
||||
|
||||
// Note: base rule explicitly disabled in favor of the TS one
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
varsIgnorePattern: '^_',
|
||||
ignoreRestSiblings: true,
|
||||
},
|
||||
],
|
||||
|
||||
curly: ['warn', 'multi-line', 'consistent'],
|
||||
|
||||
'no-restricted-globals': ['warn'].concat(
|
||||
require('confusing-browser-globals').filter(g => g !== 'self'),
|
||||
),
|
||||
|
||||
'react/jsx-filename-extension': [
|
||||
'warn',
|
||||
{ extensions: ['.jsx', '.tsx'], allow: 'as-needed' },
|
||||
],
|
||||
'react/jsx-no-useless-fragment': 'warn',
|
||||
'react/self-closing-comp': 'warn',
|
||||
'react/no-unstable-nested-components': [
|
||||
'warn',
|
||||
{ allowAsProps: true, customValidators: ['formatter'] },
|
||||
],
|
||||
|
||||
'rulesdir/typography': 'warn',
|
||||
'rulesdir/prefer-if-statement': 'warn',
|
||||
|
||||
// https://github.com/eslint/eslint/issues/16954
|
||||
// https://github.com/eslint/eslint/issues/16953
|
||||
'no-loop-func': 'off',
|
||||
|
||||
// Do don't need this as we're using TypeScript
|
||||
'react/prop-types': 'off',
|
||||
|
||||
// TODO: re-enable these rules
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/display-name': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
// 'react-hooks/exhaustive-deps': [
|
||||
// 'warn',
|
||||
// {
|
||||
// additionalHooks: 'useLiveQuery',
|
||||
// },
|
||||
// ],
|
||||
|
||||
'no-var': 'warn',
|
||||
'react/jsx-curly-brace-presence': 'warn',
|
||||
'object-shorthand': ['warn', 'properties'],
|
||||
|
||||
'import/extensions': [
|
||||
'warn',
|
||||
'never',
|
||||
{
|
||||
json: 'always',
|
||||
},
|
||||
],
|
||||
'import/no-useless-path-segments': 'warn',
|
||||
'import/no-duplicates': ['warn', { 'prefer-inline': true }],
|
||||
'import/no-unused-modules': ['warn', { unusedExports: true }],
|
||||
'import/order': [
|
||||
'warn',
|
||||
{
|
||||
alphabetize: {
|
||||
caseInsensitive: true,
|
||||
order: 'asc',
|
||||
},
|
||||
groups: [
|
||||
'builtin', // Built-in types are first
|
||||
'external',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index', // Then the index file
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
pathGroups: [
|
||||
// Enforce that React (and react-related packages) is the first import
|
||||
{ group: 'builtin', pattern: 'react?(-*)', position: 'before' },
|
||||
// Separate imports from Actual from "real" external imports
|
||||
{
|
||||
group: 'external',
|
||||
pattern: 'loot-{core,design}/**/*',
|
||||
position: 'after',
|
||||
},
|
||||
],
|
||||
pathGroupsExcludedImportTypes: ['react'],
|
||||
},
|
||||
],
|
||||
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
{
|
||||
// forbid React.* as they are legacy https://twitter.com/dan_abramov/status/1308739731551858689
|
||||
selector:
|
||||
":matches(MemberExpression[object.name='React'], TSQualifiedName[left.name='React'])",
|
||||
message:
|
||||
'Using default React import is discouraged, please use named exports directly instead.',
|
||||
},
|
||||
{
|
||||
// forbid <a> in favor of <LinkButton> or <ExternalLink>
|
||||
selector: 'JSXOpeningElement[name.name="a"]',
|
||||
message:
|
||||
'Using <a> is discouraged, please use <LinkButton> or <ExternalLink> instead.',
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{ patterns: [...restrictedImportPatterns, ...restrictedImportColors] },
|
||||
],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{ 'ts-ignore': 'allow-with-description' },
|
||||
],
|
||||
|
||||
// Rules disable during TS migration
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'prefer-const': 'warn',
|
||||
'prefer-spread': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'import/no-default-export': 'warn',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['.eslintrc.js', './**/.eslintrc.js'],
|
||||
parserOptions: { project: null },
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-exports': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'./packages/desktop-client/**/*.{ts,tsx}',
|
||||
'./packages/loot-core/src/client/**/*.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
// enforce type over interface
|
||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
|
||||
// enforce import type
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
||||
],
|
||||
'@typescript-eslint/ban-types': [
|
||||
'warn',
|
||||
{
|
||||
types: {
|
||||
// forbid FC as superflous
|
||||
FunctionComponent: { message: ruleFCMsg },
|
||||
FC: { message: ruleFCMsg },
|
||||
},
|
||||
extendDefaults: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./packages/desktop-client/**/*'],
|
||||
excludedFiles: [
|
||||
'./packages/desktop-client/src/hooks/useNavigate.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['react-router-dom'],
|
||||
importNames: ['useNavigate'],
|
||||
message: 'Please use Actual’s useNavigate() hook instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./packages/loot-core/src/**/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
patterns: [
|
||||
...restrictedImportPatterns,
|
||||
{
|
||||
group: ['loot-core/**'],
|
||||
message:
|
||||
'Please use relative imports in loot-core instead of importing from `loot-core/*`',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/loot-core/src/types/**/*',
|
||||
'packages/loot-core/src/client/state-types/**/*',
|
||||
'**/icons/**/*',
|
||||
'**/{mocks,__mocks__}/**/*',
|
||||
// can't correctly resolve usages
|
||||
'**/*.{testing,electron,browser,web,api}.ts',
|
||||
],
|
||||
rules: { 'import/no-unused-modules': 'off' },
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'./packages/desktop-client/src/style/index.*',
|
||||
'./packages/desktop-client/src/style/palette.*',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-imports': ['off', { patterns: restrictedImportColors }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'./packages/api/migrations/*',
|
||||
'./packages/loot-core/migrations/*',
|
||||
],
|
||||
rules: {
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
31
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -1,20 +1,13 @@
|
||||
name: Bug Report
|
||||
description: File a bug report also known as an issue or problem.
|
||||
title: '[Bug]: '
|
||||
labels: ['needs triage', 'bug']
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to fill out this bug report! Please ensure you provide as much information as possible to better assist in confirming and identifying a fix for the bug.
|
||||
- type: markdown
|
||||
id: intro-md
|
||||
attributes:
|
||||
value: |
|
||||
**IMPORTANT:** we use GitHub Issues only for BUG REPORTS and FEATURE REQUESTS. If you are looking for help/support - please reach out to the [community on Discord](https://discord.gg/pRYNYr4W5A). All non-bug and non-feature-request issues will be closed.
|
||||
|
||||
**Bank-sync problems (SimpleFin / GoCardless)?** Reach out via the [community Discord](https://discord.gg/pRYNYr4W5A) first and open an issue only if the community deems the issue to be a legitimate bug in Actual.
|
||||
- type: checkboxes
|
||||
id: existing-issue
|
||||
attributes:
|
||||
@@ -25,6 +18,18 @@ body:
|
||||
required: true
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
id: bank-sync-issue
|
||||
attributes:
|
||||
label: 'Is this related to GoCardless, Simplefin or another bank-sync provider?'
|
||||
description: 'Most issues with bank-sync providers are due to a lack of a custom bank-mapper (i.e. payee or other fields not coming through). In such cases you can create a custom bank mapper in [actual-server](https://github.com/actualbudget/actual-server/blob/master/src/app-gocardless/README.md) repository. Other likely issue is misconfigured server - in which case please reach out via the [community Discord](https://discord.gg/pRYNYr4W5A) to get support.'
|
||||
options:
|
||||
- label: 'I have checked my server logs and could not see any errors there'
|
||||
- label: 'I will be attaching my server logs to this issue'
|
||||
- label: 'I will be attaching my client-side (browser) logs to this issue'
|
||||
- label: 'I understand that this issue will be automatically closed if insufficient information is provided'
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
@@ -35,13 +40,12 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
id: errors-received
|
||||
attributes:
|
||||
label: How can we reproduce the issue?
|
||||
description: Please give step-by-step instructions on how to reproduce the issue. In most cases this might also require uploading a sample budget/import file.
|
||||
value: 'How can we reproduce the issue?'
|
||||
label: 'What error did you receive?'
|
||||
description: 'If you received an error or a message on the screen, please provide that here.'
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
- type: markdown
|
||||
id: env-info
|
||||
attributes:
|
||||
@@ -55,7 +59,6 @@ body:
|
||||
- Locally via Yarn
|
||||
- Docker
|
||||
- Fly.io
|
||||
- Pikapods
|
||||
- NAS
|
||||
- Desktop App (Electron)
|
||||
- Other
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,11 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Bank-sync issues
|
||||
url: https://discord.gg/pRYNYr4W5A
|
||||
about: Is bank-sync not working? Returning too much or too few information? Reach out to the community on Discord.
|
||||
- name: Support
|
||||
url: https://discord.gg/pRYNYr4W5A
|
||||
about: Need help with something? Having troubles setting up? Or perhaps issues using the API? Reach out to the community on Discord.
|
||||
- name: Translations
|
||||
url: https://hosted.weblate.org/projects/actualbudget/actual/
|
||||
about: Found a string that needs a better translation? Add your suggestion or upvote an existing one in Weblate.
|
||||
about: Need help with something? Perhaps having issues setting up bank-sync with GoCardless or SimpleFin? Reach out to the community on Discord.
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +1 @@
|
||||
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes. Try running yarn generate:release-notes *before* pushing your PR for an interactive experience. -->
|
||||
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes -->
|
||||
|
||||
53
.github/actions/bump-package-versions
vendored
@@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
version="${1#v}"
|
||||
else
|
||||
version=""
|
||||
fi
|
||||
|
||||
files_to_bump=(
|
||||
packages/api/package.json
|
||||
packages/desktop-client/package.json
|
||||
packages/desktop-electron/package.json
|
||||
packages/sync-server/package.json
|
||||
)
|
||||
|
||||
for file in "${files_to_bump[@]}"; do
|
||||
if [ -z "$version" ]; then
|
||||
# version format: YY.MM.patch
|
||||
version="$(jq -r .version "$file" | perl -e '
|
||||
($y,$m,$p)=split(/\./,<>);
|
||||
($sec,$min,$hour,$day,$mon,$year)=localtime();
|
||||
$year -= 100; # Perl year starts at 1900
|
||||
$mon++; # Adjust 0-indexed month to 1-indexed
|
||||
if ($y == $year && $m == $mon) {
|
||||
if ($day <= 25) {
|
||||
# Patch release for the current month
|
||||
$p++;
|
||||
} else {
|
||||
# Use next month for a new release period
|
||||
$p = 0;
|
||||
$m++;
|
||||
$m > 12 && ($m=1, $y++);
|
||||
}
|
||||
} else {
|
||||
# Use the current date for a new release period
|
||||
$y = $year;
|
||||
$m = $mon;
|
||||
$p = 0;
|
||||
}
|
||||
print "$y.$m.$p\n";
|
||||
')"
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "Error: Failed to calculate new version" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Bumping $file to version $version"
|
||||
jq '.version = "'"$version"'"' "$file" > "$file.tmp"
|
||||
mv "$file.tmp" "$file"
|
||||
done
|
||||
39
.github/actions/setup/action.yml
vendored
@@ -1,48 +1,19 @@
|
||||
name: Setup
|
||||
|
||||
inputs:
|
||||
working-directory:
|
||||
description: 'Working directory to run in, default .'
|
||||
required: false
|
||||
default: '.'
|
||||
download-translations:
|
||||
description: 'Whether to download translations as part of setup, default true'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
shell: bash
|
||||
if: ${{ env.ACT }}
|
||||
- name: Get Node version
|
||||
id: get-node
|
||||
run: echo "version=$(node -v)" >> "$GITHUB_OUTPUT"
|
||||
shell: bash
|
||||
node-version: 18.16.0
|
||||
- name: Cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
key: yarn-v1-${{ runner.os }}-${{ steps.get-node.outputs.version }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
path: '**/node_modules'
|
||||
key: yarn-v1-${{ runner.os }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Install
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: yarn --immutable
|
||||
shell: bash
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- name: Download translations
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: actualbudget/translations
|
||||
path: ${{ inputs.working-directory }}/packages/desktop-client/locale
|
||||
if: ${{ inputs.download-translations == 'true' }}
|
||||
- name: Remove untranslated languages
|
||||
run: packages/desktop-client/bin/remove-untranslated-languages
|
||||
shell: bash
|
||||
if: ${{ inputs.download-translations == 'true' }}
|
||||
|
||||
30
.github/workflows/build.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
api:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build API
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Create package tgz
|
||||
run: cd packages/api && yarn pack && mv package.tgz actual-api.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: actual-api
|
||||
path: packages/api/actual-api.tgz
|
||||
@@ -37,7 +37,7 @@ jobs:
|
||||
crdt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build CRDT
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Create package tgz
|
||||
run: cd packages/crdt && yarn pack && mv package.tgz actual-crdt.tgz
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: actual-crdt
|
||||
path: packages/crdt/actual-crdt.tgz
|
||||
@@ -53,32 +53,18 @@ jobs:
|
||||
web:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:browser
|
||||
run: ./bin/package-browser
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: actual-web
|
||||
path: packages/desktop-client/build
|
||||
- name: Upload Build Stats
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-stats
|
||||
path: packages/desktop-client/build-stats
|
||||
|
||||
server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Server
|
||||
run: yarn workspace @actual-app/sync-server build
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sync-server
|
||||
path: packages/sync-server/build
|
||||
|
||||
12
.github/workflows/check.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Lint
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Typecheck
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Test
|
||||
@@ -40,9 +40,9 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: '19'
|
||||
- name: Check migrations
|
||||
run: node ./.github/actions/check-migrations.js
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
@@ -22,14 +22,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: '/language:javascript'
|
||||
|
||||
92
.github/workflows/docker-edge.yml
vendored
@@ -1,92 +0,0 @@
|
||||
name: Build Edge Docker Image
|
||||
|
||||
# Edge Docker images are built for every commit, and daily
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/sync-server/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/sync-server/**'
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
IMAGES: |
|
||||
actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual
|
||||
|
||||
# Creates the following tags:
|
||||
# - actual-server:edge
|
||||
TAGS: |
|
||||
type=edge,value=edge
|
||||
type=sha
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.event.repository.fork == false }}
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu, alpine]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# Push to both Docker Hub and Github Container Registry
|
||||
images: ${{ env.IMAGES }}
|
||||
flavor: ${{ matrix.os != 'ubuntu' && format('suffix=-{0}', matrix.os) || '' }}
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Building outside of the docker image allows us to build once and push to multiple platforms
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: packages/sync-server/docker/${{ matrix.os }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7${{ matrix.os == 'alpine' && ',linux/arm/v6' || '' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
|
||||
96
.github/workflows/docker-release.yml
vendored
@@ -1,96 +0,0 @@
|
||||
name: Build Stable Docker Image
|
||||
|
||||
# Stable Docker images are built for every new tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths-ignore:
|
||||
- README.md
|
||||
- LICENSE.txt
|
||||
|
||||
env:
|
||||
IMAGES: |
|
||||
actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual
|
||||
|
||||
# Creates the following tags:
|
||||
# - actual-server:latest (see docker/metadata-action flavor inputs, below)
|
||||
# - actual-server:1.3
|
||||
# - actual-server:1.3.7
|
||||
# - actual-server:sha-90dd603
|
||||
TAGS: |
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
# Push to both Docker Hub and Github Container Registry
|
||||
images: ${{ env.IMAGES }}
|
||||
# Automatically update :latest
|
||||
flavor: latest=true
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Docker meta for Alpine image
|
||||
id: alpine-meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.IMAGES }}
|
||||
# Automatically update :latest
|
||||
flavor: |
|
||||
latest=true
|
||||
suffix=-alpine,onlatest=true
|
||||
tags: ${{ env.TAGS }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Building outside of the docker image allows us to build once and push to multiple platforms
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
- name: Build and push ubuntu image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: packages/sync-server/docker/ubuntu.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
- name: Build and push alpine image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: packages/sync-server/docker/alpine.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
tags: ${{ steps.alpine-meta.outputs.tags }}
|
||||
40
.github/workflows/e2e-test.yml
vendored
@@ -1,7 +1,6 @@
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
on: [pull_request]
|
||||
|
||||
env:
|
||||
GITHUB_PR_NUMBER: ${{github.event.pull_request.number}}
|
||||
@@ -17,7 +16,7 @@ jobs:
|
||||
outputs:
|
||||
netlify_url: ${{ steps.netlify.outputs.url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Wait for Netlify build to finish
|
||||
@@ -32,61 +31,38 @@ jobs:
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run E2E Tests on Netlify URL
|
||||
run: yarn e2e
|
||||
env:
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
functional-desktop-app:
|
||||
name: Functional Desktop App
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run Desktop app E2E Tests
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-app-test-results
|
||||
path: packages/desktop-electron/e2e/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
vrt:
|
||||
name: Visual regression
|
||||
needs: netlify
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
run: yarn vrt
|
||||
env:
|
||||
E2E_START_URL: ${{ needs.netlify.outputs.netlify_url }}
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-client-test-results
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
108
.github/workflows/electron-master.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Electron Master
|
||||
name: Electron
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -9,8 +9,8 @@ env:
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -18,9 +18,6 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
# this is so the assets can be added to the release
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
@@ -29,116 +26,25 @@ jobs:
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
run: |
|
||||
mkdir .venv
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python3 -m pip install setuptools
|
||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
run: python3 -m pip install setuptools
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron for Mac
|
||||
if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
- name: Build Electron
|
||||
run: ./bin/package-electron
|
||||
env:
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
|
||||
- name: Build Electron
|
||||
if: ${{ ! startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
- name: Process release version
|
||||
id: process_version
|
||||
run: |
|
||||
echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||
- name: Add to new release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
body: |
|
||||
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.process_version.outputs.version }})
|
||||
|
||||
## Desktop releases
|
||||
Please note: Microsoft store updates can sometimes lag behind the main release by a couple of days while they verify the new version.
|
||||
|
||||
<a href="https://apps.microsoft.com/detail/9p2hmlhsdbrm?cid=Github+Releases&mode=direct">
|
||||
<img src="https://get.microsoft.com/images/en-us%20dark.svg" width="200"/>
|
||||
</a>
|
||||
files: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
|
||||
publish-microsoft-store:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Install StoreBroker
|
||||
shell: powershell
|
||||
run: |
|
||||
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
|
||||
|
||||
- name: Download Microsoft Store artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: actual-electron-windows-latest-appx
|
||||
|
||||
- name: Submit to Microsoft Store
|
||||
shell: powershell
|
||||
run: |
|
||||
# Disable telemetry
|
||||
$global:SBDisableTelemetry = $true
|
||||
|
||||
# Authenticate against the store
|
||||
$pass = ConvertTo-SecureString -String '${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}' -AsPlainText -Force
|
||||
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ${{ secrets.MICROSOFT_STORE_CLIENT_ID }},$pass
|
||||
Set-StoreBrokerAuthentication -TenantId '${{ secrets.MICROSOFT_STORE_TENANT_ID }}' -Credential $cred
|
||||
|
||||
# Zip and create metadata files
|
||||
$artifacts = Get-ChildItem -Path . -Filter *.appx | Select-Object -ExpandProperty FullName
|
||||
New-StoreBrokerConfigFile -Path "$PWD/config.json" -AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }}
|
||||
New-SubmissionPackage -ConfigPath "$PWD/config.json" -DisableAutoPackageNameFormatting -AppxPath $artifacts -OutPath "$PWD" -OutName submission
|
||||
|
||||
# Submit the app
|
||||
# See https://github.com/microsoft/StoreBroker/blob/master/Documentation/USAGE.md#the-easy-way
|
||||
Update-ApplicationSubmission `
|
||||
-AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }} `
|
||||
-SubmissionDataPath "submission.json" `
|
||||
-PackagePath "submission.zip" `
|
||||
-ReplacePackages `
|
||||
-NoStatus `
|
||||
-AutoCommit `
|
||||
-Force
|
||||
|
||||
28
.github/workflows/electron-pr.yml
vendored
@@ -24,42 +24,20 @@ jobs:
|
||||
- macos-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
run: pip.exe install setuptools
|
||||
- if: ${{ ! startsWith(matrix.os, 'windows') }}
|
||||
run: |
|
||||
mkdir .venv
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python3 -m pip install setuptools
|
||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install flatpak -y
|
||||
sudo apt-get install flatpak-builder -y
|
||||
sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
sudo flatpak install org.freedesktop.Sdk/x86_64/23.08 -y
|
||||
sudo flatpak install org.freedesktop.Platform/x86_64/23.08 -y
|
||||
sudo flatpak install org.electronjs.Electron2.BaseApp/x86_64/23.08 -y
|
||||
run: python3 -m pip install setuptools
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Electron
|
||||
run: ./bin/package-electron
|
||||
- name: Upload Build
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
- name: Upload Windows Store Build
|
||||
if: ${{ startsWith(matrix.os, 'windows') }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: actual-electron-${{ matrix.os }}-appx
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
|
||||
35
.github/workflows/generate-release-pr.yml
vendored
@@ -1,35 +0,0 @@
|
||||
name: Generate release PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: 'Commit or branch to release'
|
||||
required: true
|
||||
default: 'master'
|
||||
version:
|
||||
description: 'Version number for the release (optional)'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
generate-release-pr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.ref }}
|
||||
- name: Bump package versions
|
||||
id: bump_package_versions
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/bump-package-versions ${{ github.event.inputs.version }}
|
||||
echo "version=$(jq -r .version packages/desktop-client/package.json)" > $GITHUB_OUTPUT
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
commit-message: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
title: '🔖 (${{ steps.bump_package_versions.outputs.version }})'
|
||||
body: 'Generated by [generate-release-pr.yml](../tree/master/.github/workflows/generate-release-pr.yml)'
|
||||
branch: 'release/v${{ steps.bump_package_versions.outputs.version }}'
|
||||
87
.github/workflows/i18n-string-extract-master.yml
vendored
@@ -1,87 +0,0 @@
|
||||
name: Extract and upload i18n strings
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 4am UTC
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
extract-and-upload-i18n-strings:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'actualbudget/actual'
|
||||
steps:
|
||||
- name: Check out main repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: actual
|
||||
- name: Set up environment
|
||||
uses: ./actual/.github/actions/setup
|
||||
with:
|
||||
working-directory: actual
|
||||
download-translations: false # As we'll manually clone instead
|
||||
- name: Configure Git config
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: Configure i18n client
|
||||
run: |
|
||||
pip install wlc
|
||||
|
||||
- name: Lock translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
lock \
|
||||
actualbudget/actual
|
||||
|
||||
- name: Update VCS with latest translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
push \
|
||||
actualbudget/actual
|
||||
- name: Check out updated translations
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
|
||||
repository: actualbudget/translations
|
||||
path: translations
|
||||
- name: Generate i18n strings
|
||||
working-directory: actual
|
||||
run: |
|
||||
mkdir -p packages/desktop-client/locale/
|
||||
cp ../translations/en.json packages/desktop-client/locale/
|
||||
yarn generate:i18n
|
||||
if [[ ! -f packages/desktop-client/locale/en.json ]]; then
|
||||
echo "File packages/desktop-client/locale/en.json not found. Ensure the file was generated correctly."
|
||||
exit 1
|
||||
fi
|
||||
- name: Check in new i18n strings
|
||||
working-directory: translations
|
||||
run: |
|
||||
cp ../actual/packages/desktop-client/locale/en.json .
|
||||
git add .
|
||||
if git commit -m "Update source strings"; then
|
||||
git push
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
- name: Update Weblate with latest translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
pull \
|
||||
actualbudget/actual
|
||||
|
||||
- name: Unlock translations
|
||||
if: always() # Clean up even on failure
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
unlock \
|
||||
actualbudget/actual
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# This is not a security concern because we have approved & merged the PR
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: '19'
|
||||
- name: Handle feature requests
|
||||
run: node .github/actions/handle-feature-requests.js
|
||||
env:
|
||||
|
||||
43
.github/workflows/netlify-release.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Deploy Netlify Release
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Repository Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Install Netlify
|
||||
run: npm install netlify-cli@17.10.1 -g
|
||||
|
||||
- name: Build Actual
|
||||
run: yarn build:browser
|
||||
|
||||
- name: Deploy to Netlify
|
||||
id: netlify_deploy
|
||||
run: |
|
||||
netlify deploy \
|
||||
--dir packages/desktop-client/build \
|
||||
--site ${{ secrets.NETLIFY_SITE_ID }} \
|
||||
--auth ${{ secrets.NETLIFY_API_TOKEN }} \
|
||||
--filter @actual-app/web \
|
||||
--prod
|
||||
78
.github/workflows/publish-npm-packages.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: Publish npm packages
|
||||
|
||||
# # Npm packages are published for every new tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build-and-pack:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and pack npm packages
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
- name: Pack the web and server packages
|
||||
run: |
|
||||
yarn workspace @actual-app/web pack --filename @actual-app/web.tgz
|
||||
yarn workspace @actual-app/sync-server pack --filename @actual-app/sync-server.tgz
|
||||
|
||||
- name: Build API
|
||||
run: yarn build:api
|
||||
|
||||
- name: Pack the api package
|
||||
run: |
|
||||
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-packages
|
||||
path: |
|
||||
packages/desktop-client/@actual-app/web.tgz
|
||||
packages/sync-server/@actual-app/sync-server.tgz
|
||||
packages/api/@actual-app/api.tgz
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish npm packages
|
||||
needs: build-and-pack
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Download the artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: npm-packages
|
||||
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Web
|
||||
run: |
|
||||
npm publish desktop-client/@actual-app/web.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Sync-Server
|
||||
run: |
|
||||
npm publish sync-server/@actual-app/sync-server.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish API
|
||||
run: |
|
||||
npm publish api/@actual-app/api.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
2
.github/workflows/release-notes.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: Check release notes
|
||||
if: startsWith(github.head_ref, 'release/') == false
|
||||
uses: actualbudget/actions/release-notes/check@main
|
||||
|
||||
16
.github/workflows/size-compare.yml
vendored
@@ -13,9 +13,6 @@ name: Compare Sizes
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
paths:
|
||||
- 'packages/**'
|
||||
- '!packages/sync-server/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
@@ -28,7 +25,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Wait for ${{github.base_ref}} build to succeed
|
||||
uses: fountainhead/action-wait-for-check@v1.2.0
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: master-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -36,7 +33,7 @@ jobs:
|
||||
ref: ${{github.base_ref}}
|
||||
|
||||
- name: Wait for PR build to succeed
|
||||
uses: fountainhead/action-wait-for-check@v1.2.0
|
||||
uses: fountainhead/action-wait-for-check@v1.1.0
|
||||
id: wait-for-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -49,7 +46,7 @@ jobs:
|
||||
echo "Build failed on PR branch or ${{github.base_ref}}"
|
||||
exit 1
|
||||
- name: Download build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
id: pr-build
|
||||
with:
|
||||
branch: ${{github.base_ref}}
|
||||
@@ -58,13 +55,12 @@ jobs:
|
||||
path: base
|
||||
|
||||
- name: Download build artifact from PR
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
name: build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
|
||||
- name: Strip content hashes from stats files
|
||||
run: |
|
||||
@@ -74,14 +70,14 @@ jobs:
|
||||
sed -i -E 's/index\.[0-9a-zA-Z_-]{8,}\./index./g' ./base/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-zA-Z_-]{8,}\.chunk\././g' ./base/web-stats.json
|
||||
sed -i -E 's/\.[0-9a-f]{8,}\././g' ./base/*.json
|
||||
- uses: twk3/rollup-size-compare-action@v1.1.1
|
||||
- uses: twk3/rollup-size-compare-action@v1.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
current-stats-json-path: ./head/web-stats.json
|
||||
base-stats-json-path: ./base/web-stats.json
|
||||
title: desktop-client
|
||||
|
||||
- uses: github/webpack-bundlesize-compare-action@v2.1.0
|
||||
- uses: github/webpack-bundlesize-compare-action@v1.8.2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
current-stats-json-path: ./head/loot-core-stats.json
|
||||
|
||||
12
.github/workflows/stale.yml
vendored
@@ -7,20 +7,10 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 5 days with no activity.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
days-before-issue-stale: -1
|
||||
stale-wip:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-pr-message: ':wave: Hi! It looks like this PR has not had any changes for a week now. Would you like someone to review this PR? If so - please remove the "[WIP]" prefix from the PR title. That will let the community know that this PR is open for a review.'
|
||||
days-before-stale: 7
|
||||
any-of-labels: ':construction: WIP'
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: -1
|
||||
|
||||
119
.github/workflows/update-vrt.yml
vendored
@@ -1,119 +0,0 @@
|
||||
name: /update-vrt
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ contains(github.event.comment.body, '/update-vrt') }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
update-vrt:
|
||||
name: Update VRT
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-jammy
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run VRT Tests on Desktop app
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./.github/actions/netlify-wait-for-build
|
||||
- name: Run VRT Tests on Netlify URL
|
||||
run: yarn vrt --update-snapshots
|
||||
env:
|
||||
E2E_START_URL: ${{ steps.netlify.outputs.url }}
|
||||
- name: Create patch
|
||||
run: |
|
||||
git config --system --add safe.directory "*"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git reset
|
||||
git add "**/*.png"
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update VRT"
|
||||
git format-patch -1 HEAD --stdout > Update-VRT.patch
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: patch
|
||||
path: Update-VRT.patch
|
||||
|
||||
push-patch:
|
||||
runs-on: ubuntu-latest
|
||||
needs: update-vrt
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }}
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- uses: actions/download-artifact@v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: patch
|
||||
- name: Apply patch and push
|
||||
env:
|
||||
BRANCH_NAME: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git apply Update-VRT.patch
|
||||
git add "**/*.png"
|
||||
if git diff --staged --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "Update VRT"
|
||||
git push origin HEAD:${BRANCH_NAME}
|
||||
- name: Add finished reaction
|
||||
uses: dkershner6/reaction-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: 'rocket'
|
||||
|
||||
add-starting-reaction:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '/update-vrt')
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: React to comment
|
||||
uses: dkershner6/reaction-action@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: '+1'
|
||||
48
.gitignore
vendored
@@ -1,35 +1,29 @@
|
||||
# Sample Data
|
||||
/data/*
|
||||
!data/.gitkeep
|
||||
/data2
|
||||
Actual-*
|
||||
!actual-server.js
|
||||
**/xcuserdata/*
|
||||
export-2020-01-10.csv
|
||||
|
||||
# Secrets
|
||||
.secret-tokens
|
||||
|
||||
# MacOS
|
||||
.DS_Store
|
||||
|
||||
# Logs
|
||||
**/*.log
|
||||
|
||||
# JavaScript
|
||||
node_modules
|
||||
packages/api/dist
|
||||
packages/api/@types
|
||||
packages/crdt/dist
|
||||
packages/desktop-electron/client-build
|
||||
packages/desktop-electron/build
|
||||
packages/desktop-electron/.electron-symbols
|
||||
packages/desktop-electron/dist
|
||||
packages/desktop-electron/loot-core
|
||||
node_modules
|
||||
.DS_Store
|
||||
lerna-debug.log
|
||||
Actual-*
|
||||
.#*
|
||||
**/xcuserdata/*
|
||||
.secret-tokens
|
||||
bundle.desktop.js
|
||||
bundle.desktop.js.map
|
||||
bundle.mobile.js
|
||||
bundle.mobile.js.map
|
||||
export-2020-01-10.csv
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
**/*.log
|
||||
|
||||
# Yarn
|
||||
.pnp.*
|
||||
@@ -42,21 +36,3 @@ bundle.mobile.js.map
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea
|
||||
|
||||
# Misc
|
||||
.#*
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
|
||||
# build output
|
||||
package.tgz
|
||||
|
||||
# Fly.io configuration
|
||||
fly.toml
|
||||
|
||||
# TypeScript cache
|
||||
build/
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
yarn lint-staged
|
||||
@@ -1,30 +1 @@
|
||||
sync_pb.*
|
||||
packages/api/app/bundle.api.js
|
||||
packages/api/app/stats.json
|
||||
packages/api/dist
|
||||
packages/api/@types
|
||||
packages/api/migrations
|
||||
packages/crdt/dist
|
||||
packages/component-library/src/icons/**/*
|
||||
packages/desktop-client/bundle.browser.js
|
||||
packages/desktop-client/build/
|
||||
packages/desktop-client/locale/
|
||||
packages/desktop-client/build-electron/
|
||||
packages/desktop-client/build-stats/
|
||||
packages/desktop-client/public/kcab/
|
||||
packages/desktop-client/public/data/
|
||||
packages/desktop-client/**/node_modules/*
|
||||
packages/desktop-client/node_modules/
|
||||
packages/desktop-client/test-results/
|
||||
packages/desktop-client/playwright-report/
|
||||
packages/desktop-electron/client-build/
|
||||
packages/desktop-electron/build/
|
||||
packages/desktop-electron/dist/
|
||||
packages/import-ynab4/**/node_modules/*
|
||||
packages/import-ynab5/**/node_modules/*
|
||||
packages/loot-core/**/node_modules/*
|
||||
packages/loot-core/**/lib-dist/*
|
||||
packages/loot-core/**/proto/*
|
||||
.yarn/*
|
||||
.github/*
|
||||
upcoming-release-notes/*
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
diff --git a/methods/inflater.js b/methods/inflater.js
|
||||
index 8769e66e82b25541aba80b1ac6429199c9a8179f..1d4402402f0e1aaf64062c1f004c3d6e6fe93e76 100644
|
||||
--- a/methods/inflater.js
|
||||
+++ b/methods/inflater.js
|
||||
@@ -1,4 +1,4 @@
|
||||
-const version = +(process.versions ? process.versions.node : "").split(".")[0] || 0;
|
||||
+const version = +(process?.versions?.node ?? "").split(".")[0] || 0;
|
||||
|
||||
module.exports = function (/*Buffer*/ inbuf, /*number*/ expectedLength) {
|
||||
var zlib = require("zlib");
|
||||
893
.yarn/releases/yarn-4.0.1.cjs
vendored
Executable file
948
.yarn/releases/yarn-4.9.1.cjs
vendored
@@ -2,8 +2,6 @@ compressionLevel: mixed
|
||||
|
||||
enableGlobalCache: false
|
||||
|
||||
enableTransparentWorkspaces: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.9.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.0.1.cjs
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# you are doing.
|
||||
###################################################
|
||||
|
||||
FROM node:20-bullseye as dev
|
||||
FROM node:18-bullseye as dev
|
||||
RUN apt-get update -y && apt-get upgrade -y && apt-get install -y openssl
|
||||
WORKDIR /app
|
||||
CMD ["sh", "./bin/docker-start"]
|
||||
|
||||
44
README.md
@@ -14,40 +14,22 @@ Want to say thanks? Click the ⭐ at the top of the page.
|
||||
|
||||
- Actual [discord](https://discord.gg/pRYNYr4W5A) community.
|
||||
- Actual [Community Documentation](https://actualbudget.org/docs)
|
||||
- [Frequently asked questions](https://actualbudget.org/docs/faq)
|
||||
|
||||
## Installation
|
||||
|
||||
There are four ways to deploy Actual:
|
||||
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.
|
||||
|
||||
1. One-click deployment [via PikaPods](https://www.pikapods.com/pods?run=actual) (~1.40 $/month) - recommended for non-technical users
|
||||
1. Managed hosting [via Fly.io](https://actualbudget.org/docs/install/fly) (~1.50 $/month)
|
||||
1. Self-hosted by using [a Docker image](https://actualbudget.org/docs/install/docker)
|
||||
1. Local-only apps - [downloadable Windows, Mac and Linux apps](https://actualbudget.org/download/) you can run on your device
|
||||
### The easy way: using a server (recommended)
|
||||
|
||||
Learn more in the [installation instructions docs](https://actualbudget.org/docs/install/).
|
||||
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.
|
||||
|
||||
## Ready to Start Budgeting?
|
||||
|
||||
Read about [Envelope budgeting](https://actualbudget.org/docs/getting-started/envelope-budgeting) to know more about the idea behind Actual Budget.
|
||||
|
||||
### Are you new to budgeting or want to start fresh?
|
||||
|
||||
Check out the community's [Starting Fresh](https://actualbudget.org/docs/getting-started/starting-fresh) guide so you can quickly get up and running!
|
||||
|
||||
### Are you migrating from other budgeting apps?
|
||||
|
||||
Check out the community's [Migration](https://actualbudget.org/docs/migration/) guide to start jumping on the Actual Budget train!
|
||||
You can get up and running quickly and easily by following our [Running Actual Locally Guide](https://actualbudget.org/docs/install/local)
|
||||
|
||||
## Documentation
|
||||
|
||||
We have a wide range of documentation on how to use Actual, this is all available in our [Community Documentation](https://actualbudget.org/docs), this includes topics on Budgeting, Account Management, Tips & Tricks and some documentation for developers.
|
||||
|
||||
## Contributing
|
||||
|
||||
Actual is a community driven product. Learn more about [contributing to Actual](https://actualbudget.org/docs/contributing/).
|
||||
|
||||
### Code structure
|
||||
## Code structure
|
||||
|
||||
The Actual app is split up into a few packages:
|
||||
|
||||
@@ -57,27 +39,15 @@ The Actual app is split up into a few packages:
|
||||
|
||||
More information on the project structure is available in our [community documentation](https://actualbudget.org/docs/contributing/project-details).
|
||||
|
||||
### Feature Requests
|
||||
## Feature Requests
|
||||
|
||||
Current feature requests can be seen [here](https://github.com/actualbudget/actual/issues?q=is%3Aissue+label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc).
|
||||
Vote for your favorite requests by reacting :+1: to the top comment of the request.
|
||||
|
||||
To add new feature requests, open a new Issue of the "Feature Request" type.
|
||||
|
||||
### Translation
|
||||
|
||||
Make Actual Budget accessible to more people by helping with the [Internationalization](https://actualbudget.org/docs/contributing/i18n/) of Actual. We are using a crowd sourcing tool to manage the translations, see our [Weblate Project](https://hosted.weblate.org/projects/actualbudget/). Weblate proudly supports open-source software projects through their [Libre plan](https://weblate.org/en/hosting/#libre).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/actualbudget/">
|
||||
<img src="https://hosted.weblate.org/widget/actualbudget/actual/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Repo Activity
|
||||
|
||||

|
||||
|
||||
## Sponsors
|
||||
|
||||
Thanks to our wonderful sponsors who make Actual Budget possible!
|
||||
Thanks to our wonderful sponsors who make Actual budget possible!
|
||||
|
||||
<a href="https://www.netlify.com"> <img src="https://www.netlify.com/v3/img/components/netlify-color-accent.svg" alt="Deploys by Netlify" /> </a>
|
||||
|
||||
@@ -10,4 +10,4 @@ if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
|
||||
yarn
|
||||
fi
|
||||
|
||||
BROWSER=0 yarn start:browser
|
||||
yarn start:browser
|
||||
|
||||
@@ -4,16 +4,6 @@ ROOT=`dirname $0`
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
echo "Updating translations..."
|
||||
if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git checkout .
|
||||
git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ RELEASE=""
|
||||
CI=${CI:-false}
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
POSITIONAL=()
|
||||
SKIP_EXE_BUILD=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
|
||||
@@ -16,36 +16,29 @@ while [[ $# -gt 0 ]]; do
|
||||
RELEASE="production"
|
||||
shift
|
||||
;;
|
||||
--skip-exe-build)
|
||||
SKIP_EXE_BUILD=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL[@]}"
|
||||
|
||||
# Get translations
|
||||
echo "Updating translations..."
|
||||
if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
if [ "$OSTYPE" == "msys" ]; then
|
||||
if [ $CI != true ]; then
|
||||
read -s -p "Windows certificate password: " -r CSC_KEY_PASSWORD
|
||||
export CSC_KEY_PASSWORD
|
||||
elif [ -n "$CIRCLE_TAG" ]; then
|
||||
# We only want to run this on CircleCI as Github doesn't have the CSC_KEY_PASSWORD secret set.
|
||||
certutil -f -p ${CSC_KEY_PASSWORD} -importPfx ~/windows-shift-reset-llc.p12
|
||||
fi
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
yarn rebuild-electron
|
||||
|
||||
yarn workspace loot-core build:node
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
# required for running the sync-server server
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
yarn workspace @actual-app/sync-server build
|
||||
yarn workspace @actual-app/web build
|
||||
|
||||
yarn workspace desktop-electron update-client
|
||||
|
||||
@@ -53,20 +46,14 @@ yarn workspace desktop-electron update-client
|
||||
cd packages/desktop-electron;
|
||||
yarn clean;
|
||||
|
||||
if [ $SKIP_EXE_BUILD == true ]; then
|
||||
echo "Building the dist"
|
||||
yarn build:dist
|
||||
echo "Skipping exe build"
|
||||
else
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build --publish never --arm64 --x64
|
||||
|
||||
echo "Created release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build
|
||||
fi
|
||||
echo "\nCreated release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build --publish never --x64
|
||||
fi
|
||||
)
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
import { exec } from 'node:child_process';
|
||||
import { existsSync, writeFile } from 'node:fs';
|
||||
import { exit } from 'node:process';
|
||||
|
||||
import prompts from 'prompts';
|
||||
|
||||
async function run() {
|
||||
const username = await execAsync(
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
"gh api user --jq '.login'",
|
||||
'To avoid having to enter your username, consider installing the official GitHub CLI (https://github.com/cli/cli) and logging in with `gh auth login`.',
|
||||
);
|
||||
const activePr = await getActivePr(username);
|
||||
if (activePr) {
|
||||
console.log(
|
||||
`Found potentially matching PR ${activePr.number}: ${activePr.title}`,
|
||||
);
|
||||
}
|
||||
const prNumber = activePr?.number ?? (await getNextPrNumber());
|
||||
|
||||
const result = await prompts([
|
||||
{
|
||||
name: 'githubUsername',
|
||||
message: 'Comma-separated GitHub username(s)',
|
||||
type: 'text',
|
||||
initial: username,
|
||||
},
|
||||
{
|
||||
name: 'pullRequestNumber',
|
||||
message: 'PR Number',
|
||||
type: 'number',
|
||||
initial: prNumber,
|
||||
},
|
||||
{
|
||||
name: 'releaseNoteType',
|
||||
message: 'Release Note Type',
|
||||
type: 'select',
|
||||
choices: [
|
||||
{ title: '✨ Features', value: 'Features' },
|
||||
{ title: '👍 Enhancements', value: 'Enhancements' },
|
||||
{ title: '🐛 Bugfix', value: 'Bugfix' },
|
||||
{ title: '⚙️ Maintenance', value: 'Maintenance' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'oneLineSummary',
|
||||
message: 'Brief Summary',
|
||||
type: 'text',
|
||||
initial: activePr?.title,
|
||||
},
|
||||
]);
|
||||
|
||||
if (
|
||||
!result.githubUsername ||
|
||||
!result.oneLineSummary ||
|
||||
!result.releaseNoteType
|
||||
) {
|
||||
console.log('All questions must be answered. Exiting');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const fileContents = getFileContents(
|
||||
result.releaseNoteType,
|
||||
result.githubUsername,
|
||||
result.oneLineSummary,
|
||||
);
|
||||
|
||||
const filepath = `./upcoming-release-notes/${prNumber}.md`;
|
||||
if (existsSync(filepath)) {
|
||||
const { confirm } = await prompts({
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
message: `This will overwrite the existing release note ${filepath} Are you sure?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
console.log('Exiting');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(filepath, fileContents, err => {
|
||||
if (err) {
|
||||
console.error('Failed to write release note file:', err);
|
||||
exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
`Release note generated successfully: ./upcoming-release-notes/${prNumber}.md`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// makes an attempt to find an existing open PR from <username>:<branch>
|
||||
async function getActivePr(
|
||||
username: string,
|
||||
): Promise<{ number: number; title: string } | undefined> {
|
||||
if (!username) {
|
||||
return undefined;
|
||||
}
|
||||
const branchName = await execAsync('git rev-parse --abbrev-ref HEAD');
|
||||
if (!branchName) {
|
||||
return undefined;
|
||||
}
|
||||
const forkHead = `${username}:${branchName}`;
|
||||
return getPrNumberFromHead(forkHead);
|
||||
}
|
||||
|
||||
async function getPrNumberFromHead(
|
||||
head: string,
|
||||
): Promise<{ number: number; title: string } | undefined> {
|
||||
try {
|
||||
// head is a weird query parameter in this API call. If nothing matches, it
|
||||
// will return as if the head query parameter doesn't exist. To get around
|
||||
// this, we make the page size 2 and only return the number if the length.
|
||||
const resp = await fetch(
|
||||
'https://api.github.com/repos/actualbudget/actual/pulls?state=open&per_page=2&head=' +
|
||||
head,
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn('error fetching from github pulls api:', resp.status);
|
||||
return undefined;
|
||||
}
|
||||
const ghResponse = await resp.json();
|
||||
if (ghResponse?.length === 1) {
|
||||
return ghResponse[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('error fetching from github pulls api:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNextPrNumber(): Promise<number> {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
'https://api.github.com/repos/actualbudget/actual/issues?state=all&per_page=1',
|
||||
);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`API responded with status: ${resp.status}`);
|
||||
}
|
||||
const ghResponse = await resp.json();
|
||||
const latestPrNumber = ghResponse?.[0]?.number;
|
||||
if (!latestPrNumber) {
|
||||
console.error(
|
||||
'Could not find latest issue number in GitHub API response',
|
||||
ghResponse,
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
return latestPrNumber + 1;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch next PR number:', error);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileContents(type: string, username: string, summary: string) {
|
||||
return `---
|
||||
category: ${type}
|
||||
authors: [${username}]
|
||||
---
|
||||
|
||||
${summary}
|
||||
`;
|
||||
}
|
||||
|
||||
// simple exec that fails silently and returns an empty string on failure
|
||||
async function execAsync(cmd: string, errorLog?: string): Promise<string> {
|
||||
return new Promise<string>(res => {
|
||||
exec(cmd, (error, stdout) => {
|
||||
if (error) {
|
||||
console.log(errorLog);
|
||||
res('');
|
||||
} else {
|
||||
res(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
32
bin/run-vrt
@@ -1,32 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# See here for more information: https://github.com/actualbudget/actual/tree/master/packages/desktop-client#visual-regression
|
||||
|
||||
if [ ! -d "node_modules" ] || [ "$(ls -A node_modules)" = "" ]; then
|
||||
yarn
|
||||
fi
|
||||
|
||||
E2E_START_URL="${E2E_START_URL:-https://localhost:3001}"
|
||||
VRT_ARGS=""
|
||||
|
||||
# Loop through all arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
key="$1"
|
||||
case $key in
|
||||
--e2e-start-url)
|
||||
E2E_START_URL="$2"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
VRT_ARGS="$VRT_ARGS $1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
echo "Running VRT tests with the following parameters:"
|
||||
echo "E2E_START_URL: $E2E_START_URL"
|
||||
echo "VRT_ARGS: $VRT_ARGS"
|
||||
|
||||
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.52.0-jammy /bin/bash \
|
||||
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"
|
||||
@@ -8,10 +8,9 @@ services:
|
||||
actual-development:
|
||||
build: .
|
||||
image: actual-development
|
||||
environment:
|
||||
- HTTPS
|
||||
ports:
|
||||
- '3001:3001'
|
||||
volumes:
|
||||
- '.:/app'
|
||||
restart: 'no'
|
||||
|
||||
|
||||
@@ -1,886 +0,0 @@
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import globals from 'globals';
|
||||
|
||||
import pluginImport from 'eslint-plugin-import';
|
||||
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
|
||||
import pluginReact from 'eslint-plugin-react';
|
||||
import pluginReactHooks from 'eslint-plugin-react-hooks';
|
||||
import pluginRulesDir from 'eslint-plugin-rulesdir';
|
||||
import pluginTypescript from 'typescript-eslint';
|
||||
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
|
||||
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
pluginRulesDir.RULES_DIR = path.join(
|
||||
__dirname,
|
||||
'packages',
|
||||
'eslint-plugin-actual',
|
||||
'lib',
|
||||
'rules',
|
||||
);
|
||||
|
||||
const confusingBrowserGlobals = [
|
||||
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
|
||||
'addEventListener',
|
||||
'blur',
|
||||
'close',
|
||||
'closed',
|
||||
'confirm',
|
||||
'defaultStatus',
|
||||
'defaultstatus',
|
||||
'event',
|
||||
'external',
|
||||
'find',
|
||||
'focus',
|
||||
'frameElement',
|
||||
'frames',
|
||||
'history',
|
||||
'innerHeight',
|
||||
'innerWidth',
|
||||
'length',
|
||||
'location',
|
||||
'locationbar',
|
||||
'menubar',
|
||||
'moveBy',
|
||||
'moveTo',
|
||||
'name',
|
||||
'onblur',
|
||||
'onerror',
|
||||
'onfocus',
|
||||
'onload',
|
||||
'onresize',
|
||||
'onunload',
|
||||
'open',
|
||||
'opener',
|
||||
'opera',
|
||||
'outerHeight',
|
||||
'outerWidth',
|
||||
'pageXOffset',
|
||||
'pageYOffset',
|
||||
'parent',
|
||||
'print',
|
||||
'removeEventListener',
|
||||
'resizeBy',
|
||||
'resizeTo',
|
||||
'screen',
|
||||
'screenLeft',
|
||||
'screenTop',
|
||||
'screenX',
|
||||
'screenY',
|
||||
'scroll',
|
||||
'scrollbars',
|
||||
'scrollBy',
|
||||
'scrollTo',
|
||||
'scrollX',
|
||||
'scrollY',
|
||||
'status',
|
||||
'statusbar',
|
||||
'stop',
|
||||
'toolbar',
|
||||
'top',
|
||||
];
|
||||
|
||||
export default pluginTypescript.config(
|
||||
{
|
||||
ignores: [
|
||||
'packages/api/app/bundle.api.js',
|
||||
'packages/api/app/stats.json',
|
||||
'packages/api/dist',
|
||||
'packages/api/@types',
|
||||
'packages/api/migrations',
|
||||
'packages/crdt/dist',
|
||||
'packages/component-library/src/icons/**/*',
|
||||
'packages/desktop-client/bundle.browser.js',
|
||||
'packages/desktop-client/build/',
|
||||
'packages/desktop-client/build-electron/',
|
||||
'packages/desktop-client/build-stats/',
|
||||
'packages/desktop-client/public/kcab/',
|
||||
'packages/desktop-client/public/data/',
|
||||
'packages/desktop-client/**/node_modules/*',
|
||||
'packages/desktop-client/node_modules/',
|
||||
'packages/desktop-client/test-results/',
|
||||
'packages/desktop-client/playwright-report/',
|
||||
'packages/desktop-electron/client-build/',
|
||||
'packages/desktop-electron/build/',
|
||||
'packages/desktop-electron/dist/',
|
||||
'packages/import-ynab4/**/node_modules/*',
|
||||
'packages/import-ynab5/**/node_modules/*',
|
||||
'packages/loot-core/**/node_modules/*',
|
||||
'packages/loot-core/**/lib-dist/*',
|
||||
'packages/loot-core/**/proto/*',
|
||||
'.yarn/*',
|
||||
'.github/*',
|
||||
],
|
||||
},
|
||||
{
|
||||
// Temporary until the sync-server is migrated to TypeScript
|
||||
files: [
|
||||
'packages/sync-server/**/*.spec.{js,jsx}',
|
||||
'packages/sync-server/**/*.test.{js,jsx}',
|
||||
],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
vi: true,
|
||||
describe: true,
|
||||
expect: true,
|
||||
it: true,
|
||||
beforeAll: true,
|
||||
beforeEach: true,
|
||||
afterAll: true,
|
||||
afterEach: true,
|
||||
test: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true,
|
||||
},
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.commonjs,
|
||||
...globals.node,
|
||||
globalThis: false,
|
||||
vi: true,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
alwaysTryTypes: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginReact.configs.flat.recommended,
|
||||
pluginReact.configs.flat['jsx-runtime'],
|
||||
pluginTypescript.configs.recommended,
|
||||
pluginImport.flatConfigs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': pluginReactHooks,
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
rulesdir: pluginRulesDir,
|
||||
'typescript-paths': pluginTypescriptPaths,
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,ts,jsx,tsx}'],
|
||||
rules: {
|
||||
// http://eslint.org/docs/rules/
|
||||
'array-callback-return': 'warn',
|
||||
|
||||
'default-case': [
|
||||
'warn',
|
||||
{
|
||||
commentPattern: '^no default$',
|
||||
},
|
||||
],
|
||||
|
||||
curly: ['warn', 'multi-line', 'consistent'],
|
||||
'dot-location': ['warn', 'property'],
|
||||
eqeqeq: ['warn', 'smart'],
|
||||
'new-parens': 'warn',
|
||||
'no-array-constructor': 'warn',
|
||||
'no-caller': 'warn',
|
||||
'no-cond-assign': ['warn', 'except-parens'],
|
||||
'no-const-assign': 'warn',
|
||||
'no-control-regex': 'warn',
|
||||
'no-delete-var': 'warn',
|
||||
'no-dupe-args': 'warn',
|
||||
'no-dupe-class-members': 'warn',
|
||||
'no-dupe-keys': 'warn',
|
||||
'no-duplicate-case': 'warn',
|
||||
'no-empty-character-class': 'warn',
|
||||
'no-empty-pattern': 'warn',
|
||||
'no-eval': 'warn',
|
||||
'no-ex-assign': 'warn',
|
||||
'no-extend-native': 'warn',
|
||||
'no-extra-bind': 'warn',
|
||||
'no-extra-label': 'warn',
|
||||
'no-fallthrough': 'warn',
|
||||
'no-func-assign': 'warn',
|
||||
'no-implied-eval': 'warn',
|
||||
'no-invalid-regexp': 'warn',
|
||||
'no-iterator': 'warn',
|
||||
'no-label-var': 'warn',
|
||||
|
||||
'no-labels': [
|
||||
'warn',
|
||||
{
|
||||
allowLoop: true,
|
||||
allowSwitch: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-lone-blocks': 'warn',
|
||||
|
||||
'no-mixed-operators': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
['&', '|', '^', '~', '<<', '>>', '>>>'],
|
||||
['==', '!=', '===', '!==', '>', '>=', '<', '<='],
|
||||
['&&', '||'],
|
||||
['in', 'instanceof'],
|
||||
],
|
||||
|
||||
allowSamePrecedence: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-multi-str': 'warn',
|
||||
'no-global-assign': 'warn',
|
||||
'no-unsafe-negation': 'warn',
|
||||
'no-new-func': 'warn',
|
||||
'no-new-object': 'warn',
|
||||
'no-new-symbol': 'warn',
|
||||
'no-new-wrappers': 'warn',
|
||||
'no-obj-calls': 'warn',
|
||||
'no-octal': 'warn',
|
||||
'no-octal-escape': 'warn',
|
||||
'no-redeclare': 'warn',
|
||||
'no-regex-spaces': 'warn',
|
||||
'no-script-url': 'warn',
|
||||
'no-self-assign': 'warn',
|
||||
'no-self-compare': 'warn',
|
||||
'no-sequences': 'warn',
|
||||
'no-shadow-restricted-names': 'warn',
|
||||
'no-sparse-arrays': 'warn',
|
||||
'no-template-curly-in-string': 'warn',
|
||||
'no-this-before-super': 'warn',
|
||||
'no-throw-literal': 'warn',
|
||||
'no-undef': 'error',
|
||||
'no-unreachable': 'warn',
|
||||
|
||||
'no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
},
|
||||
],
|
||||
|
||||
'no-unused-labels': 'warn',
|
||||
|
||||
'no-use-before-define': [
|
||||
'warn',
|
||||
{
|
||||
functions: false,
|
||||
classes: false,
|
||||
variables: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-useless-computed-key': 'warn',
|
||||
'no-useless-concat': 'warn',
|
||||
'no-useless-constructor': 'warn',
|
||||
'no-useless-escape': 'warn',
|
||||
|
||||
'no-useless-rename': [
|
||||
'warn',
|
||||
{
|
||||
ignoreDestructuring: false,
|
||||
ignoreImport: false,
|
||||
ignoreExport: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-with': 'warn',
|
||||
'no-whitespace-before-property': 'warn',
|
||||
|
||||
'require-yield': 'warn',
|
||||
'rest-spread-spacing': ['warn', 'never'],
|
||||
strict: ['warn', 'never'],
|
||||
'unicode-bom': ['warn', 'never'],
|
||||
'use-isnan': 'warn',
|
||||
'valid-typeof': 'warn',
|
||||
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: 'require',
|
||||
property: 'ensure',
|
||||
message:
|
||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
||||
},
|
||||
{
|
||||
object: 'System',
|
||||
property: 'import',
|
||||
message:
|
||||
'Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting',
|
||||
},
|
||||
],
|
||||
|
||||
'getter-return': 'warn',
|
||||
|
||||
// https://github.com/benmosher/eslint-plugin-import/tree/master/docs/rules
|
||||
'import/first': 'error',
|
||||
'import/no-amd': 'error',
|
||||
'import/no-anonymous-default-export': 'warn',
|
||||
'import/no-webpack-loader-syntax': 'error',
|
||||
'import/extensions': [
|
||||
'warn',
|
||||
'never',
|
||||
{
|
||||
json: 'always',
|
||||
},
|
||||
],
|
||||
'import/no-useless-path-segments': 'warn',
|
||||
'import/no-duplicates': [
|
||||
'warn',
|
||||
{
|
||||
'prefer-inline': true,
|
||||
},
|
||||
],
|
||||
'import/order': [
|
||||
'warn',
|
||||
{
|
||||
alphabetize: {
|
||||
caseInsensitive: true,
|
||||
order: 'asc',
|
||||
},
|
||||
|
||||
groups: ['builtin', 'external', 'parent', 'sibling', 'index'],
|
||||
'newlines-between': 'always',
|
||||
|
||||
pathGroups: [
|
||||
{
|
||||
// Enforce that React (and react-related packages) is the first import
|
||||
group: 'builtin',
|
||||
pattern: 'react?(-*)',
|
||||
position: 'before',
|
||||
},
|
||||
{
|
||||
// Separate imports from Actual from "real" external imports
|
||||
group: 'external',
|
||||
pattern: 'loot-{core,design}/**/*',
|
||||
position: 'after',
|
||||
},
|
||||
],
|
||||
|
||||
pathGroupsExcludedImportTypes: ['react'],
|
||||
},
|
||||
],
|
||||
|
||||
// https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules
|
||||
'react/forbid-foreign-prop-types': [
|
||||
'warn',
|
||||
{
|
||||
allowInPropTypes: true,
|
||||
},
|
||||
],
|
||||
'react/jsx-no-comment-textnodes': 'warn',
|
||||
'react/jsx-no-duplicate-props': 'warn',
|
||||
'react/jsx-no-target-blank': 'warn',
|
||||
'react/jsx-no-undef': 'error',
|
||||
'react/jsx-pascal-case': [
|
||||
'warn',
|
||||
{
|
||||
allowAllCaps: true,
|
||||
ignore: [],
|
||||
},
|
||||
],
|
||||
'react/no-danger-with-children': 'warn',
|
||||
// Disabled because of undesirable warnings
|
||||
// See https://github.com/facebook/create-react-app/issues/5204 for
|
||||
// blockers until its re-enabled
|
||||
// 'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'react/jsx-no-useless-fragment': 'warn',
|
||||
'react/self-closing-comp': 'warn',
|
||||
'react/jsx-filename-extension': [
|
||||
'warn',
|
||||
{
|
||||
extensions: ['.jsx', '.tsx'],
|
||||
allow: 'as-needed',
|
||||
},
|
||||
],
|
||||
'react/no-unstable-nested-components': [
|
||||
'warn',
|
||||
{
|
||||
allowAsProps: true,
|
||||
customValidators: ['formatter'],
|
||||
},
|
||||
],
|
||||
// Don't need this as we're using TypeScript
|
||||
'react/prop-types': 'off',
|
||||
|
||||
// https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules
|
||||
'jsx-a11y/alt-text': 'warn',
|
||||
'jsx-a11y/anchor-has-content': 'warn',
|
||||
'jsx-a11y/anchor-is-valid': [
|
||||
'warn',
|
||||
{
|
||||
aspects: ['noHref', 'invalidHref'],
|
||||
},
|
||||
],
|
||||
'jsx-a11y/aria-activedescendant-has-tabindex': 'warn',
|
||||
'jsx-a11y/aria-props': 'warn',
|
||||
'jsx-a11y/aria-proptypes': 'warn',
|
||||
'jsx-a11y/aria-role': [
|
||||
'warn',
|
||||
{
|
||||
ignoreNonDOM: true,
|
||||
},
|
||||
],
|
||||
'jsx-a11y/aria-unsupported-elements': 'warn',
|
||||
'jsx-a11y/heading-has-content': 'warn',
|
||||
'jsx-a11y/iframe-has-title': 'warn',
|
||||
'jsx-a11y/img-redundant-alt': 'warn',
|
||||
'jsx-a11y/no-access-key': 'warn',
|
||||
'jsx-a11y/no-distracting-elements': 'warn',
|
||||
'jsx-a11y/no-redundant-roles': 'warn',
|
||||
'jsx-a11y/role-has-required-aria-props': 'warn',
|
||||
'jsx-a11y/role-supports-aria-props': 'warn',
|
||||
'jsx-a11y/scope': 'warn',
|
||||
|
||||
// https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': [
|
||||
'warn',
|
||||
{
|
||||
additionalHooks: '(useQuery)',
|
||||
},
|
||||
],
|
||||
|
||||
'rulesdir/typography': 'warn',
|
||||
'rulesdir/prefer-if-statement': 'warn',
|
||||
|
||||
// Note: base rule explicitly disabled in favor of the TS one
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
varsIgnorePattern: '^(_|React)',
|
||||
ignoreRestSiblings: true,
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
],
|
||||
|
||||
'no-restricted-globals': ['warn', ...confusingBrowserGlobals],
|
||||
|
||||
// https://github.com/eslint/eslint/issues/16954
|
||||
// https://github.com/eslint/eslint/issues/16953
|
||||
'no-loop-func': 'off',
|
||||
|
||||
// TODO: re-enable these rules
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'no-var': 'warn',
|
||||
'react/jsx-curly-brace-presence': 'warn',
|
||||
'object-shorthand': ['warn', 'properties'],
|
||||
|
||||
'no-restricted-syntax': [
|
||||
'warn',
|
||||
{
|
||||
// forbid React.* as they are legacy https://twitter.com/dan_abramov/status/1308739731551858689
|
||||
selector:
|
||||
":matches(MemberExpression[object.name='React'], TSQualifiedName[left.name='React'])",
|
||||
message:
|
||||
'Using default React import is discouraged, please use named exports directly instead.',
|
||||
},
|
||||
{
|
||||
// forbid <a> in favor of <Link>
|
||||
selector: 'JSXOpeningElement[name.name="a"]',
|
||||
message: 'Using <a> is discouraged, please use <Link> instead.',
|
||||
},
|
||||
],
|
||||
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['*.api', '*.web', '*.electron'],
|
||||
message: "Don't directly reference imports from other platforms",
|
||||
},
|
||||
{
|
||||
group: ['uuid'],
|
||||
importNames: ['*'],
|
||||
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
|
||||
},
|
||||
{
|
||||
group: ['**/style', '**/colors'],
|
||||
importNames: ['colors'],
|
||||
message: 'Please use themes instead of colors',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': [
|
||||
'error',
|
||||
{
|
||||
'ts-ignore': 'allow-with-description',
|
||||
},
|
||||
],
|
||||
|
||||
// Rules disabled during TS migration
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'prefer-const': 'warn',
|
||||
'prefer-spread': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'import/no-default-export': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
|
||||
// typescript-eslint specific options
|
||||
warnOnUnsupportedTypeScriptVersion: true,
|
||||
},
|
||||
},
|
||||
|
||||
// If adding a typescript-eslint version of an existing ESLint rule,
|
||||
// make sure to disable the ESLint rule here.
|
||||
rules: {
|
||||
// TypeScript's `noFallthroughCasesInSwitch` option is more robust (#6906)
|
||||
'default-case': 'off',
|
||||
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/291)
|
||||
'no-dupe-class-members': 'off',
|
||||
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
|
||||
'no-undef': 'off',
|
||||
|
||||
// TypeScript already handles these (https://typescript-eslint.io/troubleshooting/typed-linting/performance/#eslint-plugin-import)
|
||||
'import/named': 'off',
|
||||
'import/namespace': 'off',
|
||||
'import/default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
|
||||
// Add TypeScript specific rules (and turn off ESLint equivalents)
|
||||
'@typescript-eslint/consistent-type-assertions': 'warn',
|
||||
'no-array-constructor': 'off',
|
||||
'@typescript-eslint/no-array-constructor': 'warn',
|
||||
'no-redeclare': 'off',
|
||||
'@typescript-eslint/no-redeclare': 'warn',
|
||||
'no-use-before-define': 'off',
|
||||
|
||||
'@typescript-eslint/no-use-before-define': [
|
||||
'warn',
|
||||
{
|
||||
functions: false,
|
||||
classes: false,
|
||||
variables: false,
|
||||
typedefs: false,
|
||||
},
|
||||
],
|
||||
|
||||
'no-unused-expressions': 'off',
|
||||
|
||||
'@typescript-eslint/no-unused-expressions': [
|
||||
'error',
|
||||
{
|
||||
allowShortCircuit: true,
|
||||
allowTernary: true,
|
||||
allowTaggedTemplates: true,
|
||||
},
|
||||
],
|
||||
|
||||
'no-useless-constructor': 'off',
|
||||
'@typescript-eslint/no-useless-constructor': 'warn',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*.{js,ts,jsx,tsx}'],
|
||||
rules: {
|
||||
'typescript-paths/absolute-parent-import': [
|
||||
'error',
|
||||
{ preferPathOverBaseUrl: true },
|
||||
],
|
||||
'typescript-paths/absolute-import': ['error', { enableAlias: false }],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/**/*.{ts,tsx}',
|
||||
'packages/loot-core/src/client/**/*.{ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
// enforce import type
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
{
|
||||
prefer: 'type-imports',
|
||||
fixStyle: 'inline-type-imports',
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/no-restricted-types': [
|
||||
'warn',
|
||||
{
|
||||
types: {
|
||||
// forbid FC as superflous
|
||||
FunctionComponent: {
|
||||
message:
|
||||
'Type the props argument and let TS infer or use ComponentType for a component prop',
|
||||
},
|
||||
|
||||
FC: {
|
||||
message:
|
||||
'Type the props argument and let TS infer or use ComponentType for a component prop',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*'],
|
||||
ignores: ['packages/desktop-client/src/hooks/useNavigate.{ts,tsx}'],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'react-router-dom',
|
||||
importNames: ['useNavigate'],
|
||||
message:
|
||||
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/desktop-client/**/*', 'packages/loot-core/**/*'],
|
||||
ignores: ['packages/desktop-client/src/redux/index.{ts,tsx}'],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useDispatch'],
|
||||
message:
|
||||
"Please import Actual's useDispatch() hook from `src/redux` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useSelector'],
|
||||
message:
|
||||
"Please import Actual's useSelector() hook from `src/redux` instead.",
|
||||
},
|
||||
{
|
||||
name: 'react-redux',
|
||||
importNames: ['useStore'],
|
||||
message:
|
||||
"Please import Actual's useStore() hook from `src/redux` instead.",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/loot-core/src/**/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'warn',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['*.api', '*.web', '*.electron'],
|
||||
message: "Don't directly reference imports from other platforms",
|
||||
},
|
||||
{
|
||||
group: ['uuid'],
|
||||
importNames: ['*'],
|
||||
message: "Use `import { v4 as uuidv4 } from 'uuid'` instead",
|
||||
},
|
||||
{
|
||||
group: ['loot-core/**'],
|
||||
message:
|
||||
'Please use relative imports in loot-core instead of importing from `loot-core/*`',
|
||||
},
|
||||
{
|
||||
group: ['@actual-app/web/*'],
|
||||
message: 'Please do not import `@actual-app/web` in `loot-core`',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/loot-core/src/types/**/*',
|
||||
'packages/loot-core/src/client/state-types/**/*',
|
||||
'**/icons/**/*',
|
||||
'**/{mocks,__mocks__}/**/*',
|
||||
// can't correctly resolve usages
|
||||
'**/*.{testing,electron,browser,web,api}.ts',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'import/no-unused-modules': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/src/style/index.*',
|
||||
'packages/desktop-client/src/style/palette.*',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'off',
|
||||
{
|
||||
patterns: [
|
||||
{
|
||||
group: ['**/style', '**/colors'],
|
||||
importNames: ['colors'],
|
||||
message: 'Please use themes instead of colors',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/api/migrations/*', 'packages/loot-core/migrations/*'],
|
||||
|
||||
rules: {
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/api/index.ts'],
|
||||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Allow configuring vitest with default exports (recommended as per vitest docs)
|
||||
{
|
||||
files: ['**/vitest.config.ts', '**/vitest.web.config.ts'],
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{},
|
||||
{
|
||||
// TODO: fix the issues in these files
|
||||
files: [
|
||||
'packages/desktop-client/src/components/accounts/Account.jsx',
|
||||
'packages/desktop-client/src/components/accounts/MobileAccount.jsx',
|
||||
'packages/desktop-client/src/components/accounts/MobileAccounts.jsx',
|
||||
'packages/desktop-client/src/components/budget/BudgetCategories.jsx',
|
||||
'packages/desktop-client/src/components/budget/BudgetSummaries.tsx',
|
||||
'packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx',
|
||||
'packages/desktop-client/src/components/budget/index.tsx',
|
||||
'packages/desktop-client/src/components/budget/MobileBudget.tsx',
|
||||
'packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx',
|
||||
'packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx',
|
||||
'packages/component-library/src/Menu.tsx',
|
||||
'packages/desktop-client/src/components/FinancesApp.tsx',
|
||||
'packages/desktop-client/src/components/GlobalKeys.ts',
|
||||
'packages/desktop-client/src/components/LoggedInUser.tsx',
|
||||
'packages/desktop-client/src/components/manager/ManagementApp.jsx',
|
||||
'packages/desktop-client/src/components/manager/subscribe/common.tsx',
|
||||
'packages/desktop-client/src/components/ManageRules.tsx',
|
||||
'packages/desktop-client/src/components/mobile/MobileAmountInput.jsx',
|
||||
'packages/desktop-client/src/components/mobile/MobileNavTabs.tsx',
|
||||
'packages/desktop-client/src/components/Modals.tsx',
|
||||
'packages/desktop-client/src/components/modals/EditRule.jsx',
|
||||
'packages/desktop-client/src/components/modals/ImportTransactions.jsx',
|
||||
'packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx',
|
||||
'packages/desktop-client/src/components/Notifications.tsx',
|
||||
'packages/desktop-client/src/components/payees/ManagePayees.jsx',
|
||||
'packages/desktop-client/src/components/payees/ManagePayeesWithData.jsx',
|
||||
'packages/desktop-client/src/components/payees/PayeeTable.tsx',
|
||||
'packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx',
|
||||
'packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx',
|
||||
'packages/desktop-client/src/components/reports/reports/CashFlowCard.jsx',
|
||||
'packages/desktop-client/src/components/reports/reports/CustomReport.jsx',
|
||||
'packages/desktop-client/src/components/reports/reports/NetWorthCard.jsx',
|
||||
'packages/desktop-client/src/components/reports/SaveReportName.tsx',
|
||||
'packages/desktop-client/src/components/reports/useReport.ts',
|
||||
'packages/desktop-client/src/components/schedules/ScheduleDetails.jsx',
|
||||
'packages/desktop-client/src/components/schedules/SchedulesTable.tsx',
|
||||
'packages/desktop-client/src/components/select/DateSelect.tsx',
|
||||
'packages/desktop-client/src/components/sidebar/Tools.tsx',
|
||||
'packages/desktop-client/src/components/sort.tsx',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'eslint.config.mjs',
|
||||
'**/*.test.js',
|
||||
'**/*.test.ts',
|
||||
'**/*.test.jsx',
|
||||
'**/*.test.tsx',
|
||||
'**/*.spec.js',
|
||||
],
|
||||
|
||||
rules: {
|
||||
'rulesdir/typography': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'packages/desktop-client/**/*.{ts,tsx}',
|
||||
'packages/loot-core/src/client/**/*.{ts,tsx}',
|
||||
],
|
||||
ignores: ['**/**/globals.d.ts'],
|
||||
rules: {
|
||||
// enforce type over interface
|
||||
'@typescript-eslint/consistent-type-definitions': ['warn', 'type'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/sync-server/**/*'],
|
||||
// TODO: fix the issues in these files
|
||||
rules: {
|
||||
'import/extensions': 'off',
|
||||
'rulesdir/typography': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['packages/sync-server/src/app-gocardless/banks/*.js'],
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
// can be re-enabled after https://github.com/actualbudget/actual/pull/4253
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
},
|
||||
},
|
||||
);
|
||||
76
package.json
@@ -19,82 +19,50 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn start:browser",
|
||||
"start:server": "yarn workspace @actual-app/sync-server start",
|
||||
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
|
||||
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
||||
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
||||
"desktop-dependencies": "yarn rebuild-electron && yarn workspace loot-core build:browser",
|
||||
"start:desktop": "yarn rebuild-electron && npm-run-all --parallel 'start:desktop-*'",
|
||||
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
|
||||
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
||||
"start:electron": "yarn start:desktop",
|
||||
"start:browser": "npm-run-all --parallel 'start:browser-*'",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"build:server": "yarn build:browser && yarn workspace @actual-app/sync-server build",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
||||
"test": "yarn workspaces foreach --all --parallel --verbose run test",
|
||||
"test:debug": "yarn workspaces foreach --all --verbose run test",
|
||||
"e2e": "yarn workspaces foreach --all --exclude desktop-electron --parallel --verbose run e2e",
|
||||
"e2e:desktop": "yarn build:desktop --skip-exe-build && yarn workspace desktop-electron e2e",
|
||||
"e2e": "yarn workspaces foreach --all --parallel --verbose run e2e",
|
||||
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
"lint": "prettier --check . && eslint . --max-warnings 0",
|
||||
"lint:fix": "prettier --check --write . && eslint . --max-warnings 0 --fix",
|
||||
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
||||
"typecheck": "yarn tsc --incremental && tsc-strict",
|
||||
"jq": "./node_modules/node-jq/bin/jq",
|
||||
"prepare": "husky"
|
||||
"lint": "eslint . --max-warnings 0 --ext .js,.jsx,.ts,.tsx",
|
||||
"lint:verbose": "DEBUG=eslint:cli-engine eslint . --max-warnings 0",
|
||||
"typecheck": "yarn tsc && tsc-strict",
|
||||
"jq": "./node_modules/node-jq/bin/jq"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
"eslint-import-resolver-typescript": "^4.3.5",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint": "^8.37.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-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"eslint-plugin-typescript-paths": "^0.0.33",
|
||||
"globals": "^15.15.0",
|
||||
"html-to-image": "^1.11.13",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.2",
|
||||
"node-jq": "^6.0.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.5.3",
|
||||
"prompts": "^2.4.2",
|
||||
"node-jq": "^4.0.1",
|
||||
"npm-run-all": "^4.1.3",
|
||||
"prettier": "3.2.4",
|
||||
"react-refresh": "^0.14.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"rollup": "4.40.1",
|
||||
"socks": ">=2.8.3"
|
||||
"typescript": "^5.0.2",
|
||||
"typescript-strict-plugin": "^2.2.2-beta.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20",
|
||||
"yarn": "^4.9.1"
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,json,yml}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.9.1",
|
||||
"packageManager": "yarn@4.0.1",
|
||||
"browserslist": [
|
||||
"electron 24.0",
|
||||
"defaults"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`API setup and teardown > successfully loads budget 1`] = `
|
||||
[
|
||||
exports[`API setup and teardown successfully loads budget 1`] = `
|
||||
Array [
|
||||
"2016-10",
|
||||
"2016-11",
|
||||
"2016-12",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type {
|
||||
RequestInfo as FetchInfo,
|
||||
RequestInit as FetchInit,
|
||||
} from 'node-fetch';
|
||||
// @ts-ignore: false-positive commonjs module error on build until typescript 5.3
|
||||
} from 'node-fetch'; // with { 'resolution-mode': 'import' };
|
||||
|
||||
// loot-core types
|
||||
import type { InitConfig } from 'loot-core/server/main';
|
||||
@@ -15,6 +16,9 @@ import { validateNodeVersion } from './validateNodeVersion';
|
||||
let actualApp: null | typeof bundle.lib;
|
||||
export const internal = bundle.lib;
|
||||
|
||||
// DEPRECATED: remove the next line in @actual-app/api v7
|
||||
export * as methods from './methods';
|
||||
|
||||
export * from './methods';
|
||||
export * as utils from './utils';
|
||||
|
||||
|
||||
24
packages/api/jest.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
moduleFileExtensions: [
|
||||
'testing.js',
|
||||
'testing.ts',
|
||||
'api.js',
|
||||
'api.ts',
|
||||
'api.tsx',
|
||||
'electron.js',
|
||||
'electron.ts',
|
||||
'mjs',
|
||||
'js',
|
||||
'ts',
|
||||
'tsx',
|
||||
'json',
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
watchPathIgnorePatterns: ['<rootDir>/mocks/budgets/'],
|
||||
setupFilesAfterEnv: ['<rootDir>/../loot-core/src/mocks/setup.ts'],
|
||||
transformIgnorePatterns: ['/node_modules/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
};
|
||||
@@ -6,9 +6,10 @@ import * as api from './index';
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
|
||||
global.IS_TESTING = true;
|
||||
|
||||
beforeEach(async () => {
|
||||
// we need real datetime if we are going to mix new timestamps with our mock data
|
||||
global.restoreDateNow();
|
||||
|
||||
const budgetPath = path.join(__dirname, '/mocks/budgets/', budgetName);
|
||||
await fs.rm(budgetPath, { force: true, recursive: true });
|
||||
|
||||
@@ -57,49 +58,11 @@ describe('API CRUD operations', () => {
|
||||
await api.loadBudget(budgetName);
|
||||
});
|
||||
|
||||
// api: getBudgets
|
||||
test('getBudgets', async () => {
|
||||
const budgets = await api.getBudgets();
|
||||
expect(budgets).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'test-budget',
|
||||
name: 'Default Test Db',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
// apis: getCategoryGroups, createCategoryGroup, updateCategoryGroup, deleteCategoryGroup
|
||||
// apis: createCategoryGroup, updateCategoryGroup, deleteCategoryGroup
|
||||
test('CategoryGroups: successfully update category groups', async () => {
|
||||
const month = '2023-10';
|
||||
global.currentMonth = month;
|
||||
|
||||
// get existing category groups
|
||||
const groups = await api.getCategoryGroups();
|
||||
expect(groups).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
hidden: false,
|
||||
id: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
is_income: false,
|
||||
name: 'Usual Expenses',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
hidden: false,
|
||||
id: 'a137772f-cf2f-4089-9432-822d2ddc1466',
|
||||
is_income: false,
|
||||
name: 'Investments and Savings',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
hidden: false,
|
||||
id: '2E1F5BDB-209B-43F9-AF2C-3CE28E380C00',
|
||||
is_income: true,
|
||||
name: 'Income',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// create our test category group
|
||||
const mainGroupId = await api.createCategoryGroup({
|
||||
name: 'test-group',
|
||||
@@ -257,7 +220,7 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
});
|
||||
|
||||
//apis: createAccount, getAccounts, updateAccount, closeAccount, deleteAccount, reopenAccount, getAccountBalance
|
||||
//apis: createAccount, getAccounts, updateAccount, closeAccount, deleteAccount, reopenAccount
|
||||
test('Accounts: successfully complete account operators', async () => {
|
||||
const accountId1 = await api.createAccount(
|
||||
{ name: 'test-account1', offbudget: true },
|
||||
@@ -278,9 +241,6 @@ describe('API CRUD operations', () => {
|
||||
]),
|
||||
);
|
||||
|
||||
expect(await api.getAccountBalance(accountId1)).toEqual(1000);
|
||||
expect(await api.getAccountBalance(accountId2)).toEqual(0);
|
||||
|
||||
await api.updateAccount(accountId1, { offbudget: false });
|
||||
await api.closeAccount(accountId1, accountId2, null);
|
||||
await api.deleteAccount(accountId2);
|
||||
@@ -355,221 +315,13 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
|
||||
test('Rules: successfully update rules', async () => {
|
||||
await api.createPayee({ name: 'test-payee' });
|
||||
await api.createPayee({ name: 'test-payee2' });
|
||||
|
||||
// create our test rules
|
||||
const rule = await api.createRule({
|
||||
stage: 'pre',
|
||||
conditionsOp: 'and',
|
||||
conditions: [
|
||||
{
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
value: 'test-payee',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
op: 'set',
|
||||
field: 'category',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
},
|
||||
],
|
||||
});
|
||||
const rule2 = await api.createRule({
|
||||
stage: 'pre',
|
||||
conditionsOp: 'and',
|
||||
conditions: [
|
||||
{
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
value: 'test-payee2',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
op: 'set',
|
||||
field: 'category',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// get existing rules
|
||||
const rules = await api.getRules();
|
||||
expect(rules).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee2',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule2.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// get by payee
|
||||
expect(await api.getPayeeRules('test-payee')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(await api.getPayeeRules('test-payee2')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee2',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule2.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// update one rule
|
||||
const updatedRule = {
|
||||
...rule,
|
||||
stage: 'post',
|
||||
conditionsOp: 'or',
|
||||
};
|
||||
expect(await api.updateRule(updatedRule)).toEqual(updatedRule);
|
||||
|
||||
expect(await api.getRules()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'or',
|
||||
id: rule.id,
|
||||
stage: 'post',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
actions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'category',
|
||||
op: 'set',
|
||||
type: 'id',
|
||||
value: 'fc3825fd-b982-4b72-b768-5b30844cf832',
|
||||
}),
|
||||
]),
|
||||
conditions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
type: 'id',
|
||||
value: 'test-payee2',
|
||||
}),
|
||||
]),
|
||||
conditionsOp: 'and',
|
||||
id: rule2.id,
|
||||
stage: 'pre',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// delete rules
|
||||
await api.deleteRule(rules[1].id);
|
||||
expect(await api.getRules()).toHaveLength(1);
|
||||
|
||||
await api.deleteRule(rules[0].id);
|
||||
expect(await api.getRules()).toHaveLength(0);
|
||||
});
|
||||
|
||||
// apis: addTransactions, getTransactions, importTransactions, updateTransaction, deleteTransaction
|
||||
test('Transactions: successfully update transactions', async () => {
|
||||
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
||||
|
||||
let newTransaction = [
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-11-03', imported_id: '12', amount: 100, notes: '' },
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100 },
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100 },
|
||||
];
|
||||
|
||||
const addResult = await api.addTransactions(accountId, newTransaction, {
|
||||
@@ -578,11 +330,6 @@ describe('API CRUD operations', () => {
|
||||
});
|
||||
expect(addResult).toBe('ok');
|
||||
|
||||
expect(await api.getAccountBalance(accountId)).toEqual(200);
|
||||
expect(
|
||||
await api.getAccountBalance(accountId, new Date(2023, 10, 2)),
|
||||
).toEqual(0);
|
||||
|
||||
// confirm added transactions exist
|
||||
let transactions = await api.getTransactions(
|
||||
accountId,
|
||||
@@ -597,9 +344,8 @@ describe('API CRUD operations', () => {
|
||||
expect(transactions).toHaveLength(2);
|
||||
|
||||
newTransaction = [
|
||||
{ date: '2023-12-03', imported_id: '11', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-12-03', imported_id: '12', amount: 100, notes: 'notes' },
|
||||
{ date: '2023-12-03', imported_id: '22', amount: 200, notes: '' },
|
||||
{ date: '2023-12-03', imported_id: '11', amount: 100 },
|
||||
{ date: '2023-12-03', imported_id: '22', amount: 200 },
|
||||
];
|
||||
|
||||
const reconciled = await api.importTransactions(accountId, newTransaction);
|
||||
@@ -615,22 +361,9 @@ describe('API CRUD operations', () => {
|
||||
'2023-12-31',
|
||||
);
|
||||
expect(transactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ imported_id: '22', amount: 200 }),
|
||||
]),
|
||||
);
|
||||
expect(transactions).toHaveLength(1);
|
||||
|
||||
// confirm imported transactions update perfomed
|
||||
transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ notes: 'notes', amount: 100 }),
|
||||
]),
|
||||
expect.arrayContaining(
|
||||
newTransaction.map(trans => expect.objectContaining(trans)),
|
||||
),
|
||||
);
|
||||
expect(transactions).toHaveLength(2);
|
||||
|
||||
@@ -653,60 +386,4 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
expect(transactions).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Transactions: import notes are preserved when importing', async () => {
|
||||
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
||||
|
||||
// Test with notes
|
||||
const transactionsWithNotes = [
|
||||
{
|
||||
date: '2023-11-03',
|
||||
imported_id: '11',
|
||||
amount: 100,
|
||||
notes: 'test note',
|
||||
},
|
||||
];
|
||||
|
||||
const addResultWithNotes = await api.addTransactions(
|
||||
accountId,
|
||||
transactionsWithNotes,
|
||||
{
|
||||
learnCategories: true,
|
||||
runTransfers: true,
|
||||
},
|
||||
);
|
||||
expect(addResultWithNotes).toBe('ok');
|
||||
|
||||
let transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions[0].notes).toBe('test note');
|
||||
|
||||
// Clear transactions
|
||||
await api.deleteTransaction(transactions[0].id);
|
||||
|
||||
// Test without notes
|
||||
const transactionsWithoutNotes = [
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: null },
|
||||
];
|
||||
|
||||
const addResultWithoutNotes = await api.addTransactions(
|
||||
accountId,
|
||||
transactionsWithoutNotes,
|
||||
{
|
||||
learnCategories: true,
|
||||
runTransfers: true,
|
||||
},
|
||||
);
|
||||
expect(addResultWithoutNotes).toBe('ok');
|
||||
|
||||
transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions[0].notes).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import type { Handlers } from 'loot-core/types/handlers';
|
||||
import type { Handlers } from 'loot-core/src/types/handlers';
|
||||
|
||||
import * as injected from './injected';
|
||||
|
||||
@@ -31,18 +31,10 @@ export async function downloadBudget(syncId, { password }: { password? } = {}) {
|
||||
return send('api/download-budget', { syncId, password });
|
||||
}
|
||||
|
||||
export async function getBudgets() {
|
||||
return send('api/get-budgets');
|
||||
}
|
||||
|
||||
export async function sync() {
|
||||
return send('api/sync');
|
||||
}
|
||||
|
||||
export async function runBankSync(args?: { accountId: string }) {
|
||||
return send('api/bank-sync', args);
|
||||
}
|
||||
|
||||
export async function batchBudgetUpdates(func) {
|
||||
await send('api/batch-budget-start');
|
||||
try {
|
||||
@@ -52,18 +44,10 @@ export async function batchBudgetUpdates(func) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Please use `aqlQuery` instead.
|
||||
* This function will be removed in a future release.
|
||||
*/
|
||||
export function runQuery(query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function aqlQuery(query) {
|
||||
return send('api/query', { query: query.serialize() });
|
||||
}
|
||||
|
||||
export function getBudgetMonths() {
|
||||
return send('api/budget-months');
|
||||
}
|
||||
@@ -93,22 +77,8 @@ export function addTransactions(
|
||||
});
|
||||
}
|
||||
|
||||
export interface ImportTransactionsOpts {
|
||||
defaultCleared?: boolean;
|
||||
}
|
||||
|
||||
export function importTransactions(
|
||||
accountId,
|
||||
transactions,
|
||||
opts: ImportTransactionsOpts = {
|
||||
defaultCleared: true,
|
||||
},
|
||||
) {
|
||||
return send('api/transactions-import', {
|
||||
accountId,
|
||||
transactions,
|
||||
opts,
|
||||
});
|
||||
export function importTransactions(accountId, transactions) {
|
||||
return send('api/transactions-import', { accountId, transactions });
|
||||
}
|
||||
|
||||
export function getTransactions(accountId, startDate, endDate) {
|
||||
@@ -151,14 +121,6 @@ export function deleteAccount(id) {
|
||||
return send('api/account-delete', { id });
|
||||
}
|
||||
|
||||
export function getAccountBalance(id, cutoff?) {
|
||||
return send('api/account-balance', { id, cutoff });
|
||||
}
|
||||
|
||||
export function getCategoryGroups() {
|
||||
return send('api/category-groups-get');
|
||||
}
|
||||
|
||||
export function createCategoryGroup(group) {
|
||||
return send('api/category-group-create', { group });
|
||||
}
|
||||
@@ -187,10 +149,6 @@ export function deleteCategory(id, transferCategoryId?) {
|
||||
return send('api/category-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getCommonPayees() {
|
||||
return send('api/common-payees-get');
|
||||
}
|
||||
|
||||
export function getPayees() {
|
||||
return send('api/payees-get');
|
||||
}
|
||||
@@ -206,35 +164,3 @@ export function updatePayee(id, fields) {
|
||||
export function deletePayee(id) {
|
||||
return send('api/payee-delete', { id });
|
||||
}
|
||||
|
||||
export function mergePayees(targetId, mergeIds) {
|
||||
return send('api/payees-merge', { targetId, mergeIds });
|
||||
}
|
||||
|
||||
export function getRules() {
|
||||
return send('api/rules-get');
|
||||
}
|
||||
|
||||
export function getPayeeRules(id) {
|
||||
return send('api/payee-rules-get', { id });
|
||||
}
|
||||
|
||||
export function createRule(rule) {
|
||||
return send('api/rule-create', { rule });
|
||||
}
|
||||
|
||||
export function updateRule(rule) {
|
||||
return send('api/rule-update', { rule });
|
||||
}
|
||||
|
||||
export function deleteRule(id: string) {
|
||||
return send('api/rule-delete', id);
|
||||
}
|
||||
|
||||
export function holdBudgetForNextMonth(month, amount) {
|
||||
return send('api/budget-hold-for-next-month', { month, amount });
|
||||
}
|
||||
|
||||
export function resetBudgetHold(month) {
|
||||
return send('api/budget-reset-hold', { month });
|
||||
}
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.6.1",
|
||||
"version": "6.4.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
"node": ">=18.12.0"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "@types/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"@types"
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:crdt": "yarn workspace @actual-app/crdt build",
|
||||
"build:node": "tsc --p tsconfig.dist.json && tsc-alias -p tsconfig.dist.json",
|
||||
"build:migrations": "cp migrations/*.sql dist/migrations",
|
||||
"build:default-db": "cp default-db.sqlite dist/",
|
||||
"build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
|
||||
"test": "yarn run build:app && yarn run build:crdt && vitest",
|
||||
"test": "yarn run build:app && jest -c jest.config.js",
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"compare-versions": "^6.1.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.8",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
"@swc/core": "^1.3.105",
|
||||
"@swc/jest": "^0.2.31",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.0.0",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,14 @@
|
||||
// the latest Node 16.x release supports all of the features
|
||||
"target": "ES2021",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node10",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"declarationDir": "@types",
|
||||
"paths": {
|
||||
"loot-core/*": ["./@types/loot-core/*"]
|
||||
"loot-core/*": ["./@types/loot-core/*"],
|
||||
}
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
|
||||
"exclude": ["**/node_modules/*", "dist", "@types"]
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export default {
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "@actual-app/components",
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2",
|
||||
"react-dom": ">=18.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.5",
|
||||
"react-aria-components": "^1.8.0",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^19.1.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"vitest": "^3.1.3"
|
||||
},
|
||||
"exports": {
|
||||
"./hooks/*": "./src/hooks/*.ts",
|
||||
"./icons/logo": "./src/icons/logo/index.ts",
|
||||
"./icons/v0": "./src/icons/v0/index.ts",
|
||||
"./icons/v1": "./src/icons/v1/index.ts",
|
||||
"./icons/v2": "./src/icons/v2/index.ts",
|
||||
"./icons/AnimatedLoading": "./src/icons/AnimatedLoading.tsx",
|
||||
"./icons/Loading": "./src/icons/Loading.tsx",
|
||||
"./aligned-text": "./src/AlignedText.tsx",
|
||||
"./block": "./src/Block.tsx",
|
||||
"./button": "./src/Button.tsx",
|
||||
"./card": "./src/Card.tsx",
|
||||
"./form-error": "./src/FormError.tsx",
|
||||
"./initial-focus": "./src/InitialFocus.ts",
|
||||
"./inline-field": "./src/InlineField.tsx",
|
||||
"./input": "./src/Input.tsx",
|
||||
"./label": "./src/Label.tsx",
|
||||
"./menu": "./src/Menu.tsx",
|
||||
"./paragraph": "./src/Paragraph.tsx",
|
||||
"./popover": "./src/Popover.tsx",
|
||||
"./select": "./src/Select.tsx",
|
||||
"./space-between": "./src/SpaceBetween.tsx",
|
||||
"./stack": "./src/Stack.tsx",
|
||||
"./styles": "./src/styles.ts",
|
||||
"./text": "./src/Text.tsx",
|
||||
"./text-one-line": "./src/TextOneLine.tsx",
|
||||
"./theme": "./src/theme.ts",
|
||||
"./tokens": "./src/tokens.ts",
|
||||
"./toggle": "./src/Toggle.tsx",
|
||||
"./tooltip": "./src/Tooltip.tsx",
|
||||
"./view": "./src/View.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
||||
"test": "npm-run-all -cp 'test:*'",
|
||||
"test:web": "ENV=web vitest -c vitest.web.config.ts"
|
||||
}
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
import React, {
|
||||
forwardRef,
|
||||
useMemo,
|
||||
type ComponentPropsWithoutRef,
|
||||
type ReactNode,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
import { Button as ReactAriaButton } from 'react-aria-components';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { AnimatedLoading } from './icons/AnimatedLoading';
|
||||
import { styles } from './styles';
|
||||
import { theme } from './theme';
|
||||
import { View } from './View';
|
||||
|
||||
const backgroundColor: {
|
||||
[key in ButtonVariant | `${ButtonVariant}Disabled`]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalBackground,
|
||||
normalDisabled: theme.buttonNormalDisabledBackground,
|
||||
primary: theme.buttonPrimaryBackground,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledBackground,
|
||||
bare: theme.buttonBareBackground,
|
||||
bareDisabled: theme.buttonBareDisabledBackground,
|
||||
menu: theme.buttonMenuBackground,
|
||||
menuSelected: theme.buttonMenuSelectedBackground,
|
||||
};
|
||||
|
||||
const backgroundColorHover: Record<
|
||||
ButtonVariant | `${ButtonVariant}Disabled`,
|
||||
CSSProperties['backgroundColor']
|
||||
> = {
|
||||
normal: theme.buttonNormalBackgroundHover,
|
||||
primary: theme.buttonPrimaryBackgroundHover,
|
||||
bare: theme.buttonBareBackgroundHover,
|
||||
menu: theme.buttonMenuBackgroundHover,
|
||||
menuSelected: theme.buttonMenuSelectedBackgroundHover,
|
||||
normalDisabled: 'transparent',
|
||||
primaryDisabled: 'transparent',
|
||||
bareDisabled: 'transparent',
|
||||
menuDisabled: 'transparent',
|
||||
menuSelectedDisabled: 'transparent',
|
||||
};
|
||||
|
||||
const borderColor: {
|
||||
[key in
|
||||
| ButtonVariant
|
||||
| `${ButtonVariant}Disabled`]?: CSSProperties['borderColor'];
|
||||
} = {
|
||||
normal: theme.buttonNormalBorder,
|
||||
normalDisabled: theme.buttonNormalDisabledBorder,
|
||||
primary: theme.buttonPrimaryBorder,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledBorder,
|
||||
menu: theme.buttonMenuBorder,
|
||||
menuSelected: theme.buttonMenuSelectedBorder,
|
||||
};
|
||||
|
||||
const textColor: {
|
||||
[key in ButtonVariant | `${ButtonVariant}Disabled`]?: CSSProperties['color'];
|
||||
} = {
|
||||
normal: theme.buttonNormalText,
|
||||
normalDisabled: theme.buttonNormalDisabledText,
|
||||
primary: theme.buttonPrimaryText,
|
||||
primaryDisabled: theme.buttonPrimaryDisabledText,
|
||||
bare: theme.buttonBareText,
|
||||
bareDisabled: theme.buttonBareDisabledText,
|
||||
menu: theme.buttonMenuText,
|
||||
menuSelected: theme.buttonMenuSelectedText,
|
||||
};
|
||||
|
||||
const textColorHover: {
|
||||
[key in ButtonVariant]?: string;
|
||||
} = {
|
||||
normal: theme.buttonNormalTextHover,
|
||||
primary: theme.buttonPrimaryTextHover,
|
||||
bare: theme.buttonBareTextHover,
|
||||
menu: theme.buttonMenuTextHover,
|
||||
menuSelected: theme.buttonMenuSelectedTextHover,
|
||||
};
|
||||
|
||||
const _getBorder = (
|
||||
variant: ButtonVariant,
|
||||
variantWithDisabled: keyof typeof borderColor,
|
||||
): string => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return 'none';
|
||||
|
||||
default:
|
||||
return '1px solid ' + borderColor[variantWithDisabled];
|
||||
}
|
||||
};
|
||||
|
||||
const _getPadding = (variant: ButtonVariant): string => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return '5px';
|
||||
default:
|
||||
return '5px 10px';
|
||||
}
|
||||
};
|
||||
|
||||
const _getHoveredStyles = (variant: ButtonVariant): CSSProperties => ({
|
||||
...(variant !== 'bare' && styles.shadow),
|
||||
backgroundColor: backgroundColorHover[variant],
|
||||
color: textColorHover[variant],
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
const _getActiveStyles = (
|
||||
variant: ButtonVariant,
|
||||
bounce: boolean,
|
||||
): CSSProperties => {
|
||||
switch (variant) {
|
||||
case 'bare':
|
||||
return { backgroundColor: theme.buttonBareBackgroundActive };
|
||||
default:
|
||||
return {
|
||||
transform: bounce ? 'translateY(1px)' : undefined,
|
||||
boxShadow: `0 1px 4px 0 ${
|
||||
variant === 'primary'
|
||||
? theme.buttonPrimaryShadow
|
||||
: theme.buttonNormalShadow
|
||||
}`,
|
||||
transition: 'none',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
type ButtonProps = ComponentPropsWithoutRef<typeof ReactAriaButton> & {
|
||||
variant?: ButtonVariant;
|
||||
bounce?: boolean;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
type ButtonVariant = 'normal' | 'primary' | 'bare' | 'menu' | 'menuSelected';
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(props, ref) => {
|
||||
const { children, variant = 'normal', bounce = true, ...restProps } = props;
|
||||
|
||||
const variantWithDisabled: ButtonVariant | `${ButtonVariant}Disabled` =
|
||||
props.isDisabled ? `${variant}Disabled` : variant;
|
||||
|
||||
const defaultButtonClassName: string = useMemo(
|
||||
() =>
|
||||
css({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
padding: _getPadding(variant),
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
borderRadius: 4,
|
||||
backgroundColor: backgroundColor[variantWithDisabled],
|
||||
border: _getBorder(variant, variantWithDisabled),
|
||||
color: textColor[variantWithDisabled],
|
||||
transition: 'box-shadow .25s',
|
||||
WebkitAppRegion: 'no-drag',
|
||||
...styles.smallText,
|
||||
'&[data-hovered]': _getHoveredStyles(variant),
|
||||
'&[data-pressed]': _getActiveStyles(variant, bounce),
|
||||
}),
|
||||
[bounce, variant, variantWithDisabled],
|
||||
);
|
||||
|
||||
const className = restProps.className;
|
||||
|
||||
return (
|
||||
<ReactAriaButton
|
||||
ref={ref}
|
||||
{...restProps}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps => cx(defaultButtonClassName, className(renderProps))
|
||||
: cx(defaultButtonClassName, className)
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ReactAriaButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
type ButtonWithLoadingProps = ButtonProps & {
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
export const ButtonWithLoading = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ButtonWithLoadingProps
|
||||
>((props, ref) => {
|
||||
const { isLoading, children, style, ...buttonProps } = props;
|
||||
return (
|
||||
<Button
|
||||
{...buttonProps}
|
||||
ref={ref}
|
||||
style={buttonRenderProps => ({
|
||||
position: 'relative',
|
||||
...(typeof style === 'function' ? style(buttonRenderProps) : style),
|
||||
})}
|
||||
>
|
||||
{isLoading && (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<AnimatedLoading style={{ width: 20, height: 20 }} />
|
||||
</View>
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
opacity: isLoading ? 0 : 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
|
||||
ButtonWithLoading.displayName = 'ButtonWithLoading';
|
||||
@@ -1,59 +0,0 @@
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
Ref,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
|
||||
type InitialFocusProps<T extends HTMLElement> = {
|
||||
/**
|
||||
* The child element to focus when the component mounts. This can be either a single React element or a function that returns a React element.
|
||||
*/
|
||||
children: ReactElement<{ ref: Ref<T> }> | ((ref: Ref<T>) => ReactElement);
|
||||
};
|
||||
|
||||
/**
|
||||
* InitialFocus sets focus on its child element
|
||||
* when it mounts.
|
||||
* @param {Object} props - The component props.
|
||||
* @param {ReactElement | function} props.children - A single React element or a function that returns a React element.
|
||||
*/
|
||||
export function InitialFocus<T extends HTMLElement = HTMLElement>({
|
||||
children,
|
||||
}: InitialFocusProps<T>) {
|
||||
const ref = useRef<T | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
// This is needed to avoid a strange interaction with
|
||||
// `ScopeTab`, which doesn't allow it to be focused at first for
|
||||
// some reason. Need to look into it.
|
||||
setTimeout(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
if (
|
||||
ref.current instanceof HTMLInputElement ||
|
||||
ref.current instanceof HTMLTextAreaElement
|
||||
) {
|
||||
ref.current.setSelectionRange(0, 10000);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return children(ref);
|
||||
}
|
||||
|
||||
const child = Children.only(children);
|
||||
if (isValidElement(child)) {
|
||||
return cloneElement(child, { ref });
|
||||
}
|
||||
throw new Error(
|
||||
'InitialFocus expects a single valid React element as its child.',
|
||||
);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { forwardRef, Ref } from 'react';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { InitialFocus } from './InitialFocus';
|
||||
import { View } from './View';
|
||||
|
||||
describe('InitialFocus', () => {
|
||||
it('should focus a text input', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<input type="text" title="focused" />
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
const unfocusedInput = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(document.activeElement).not.toBe(unfocusedInput);
|
||||
});
|
||||
|
||||
it('should focus a textarea', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<textarea title="focused" />
|
||||
</InitialFocus>
|
||||
<textarea title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const textarea = component.getByTitle('focused') as HTMLTextAreaElement;
|
||||
const unfocusedTextarea = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLTextAreaElement;
|
||||
expect(document.activeElement).toBe(textarea);
|
||||
expect(document.activeElement).not.toBe(unfocusedTextarea);
|
||||
});
|
||||
|
||||
it('should select text in an input', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<input type="text" title="focused" defaultValue="Hello World" />
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(input.selectionStart).toBe(0);
|
||||
expect(input.selectionEnd).toBe(11); // Length of "Hello World"
|
||||
});
|
||||
|
||||
it('should focus a button', async () => {
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
<button title="focused">Click me</button>
|
||||
</InitialFocus>
|
||||
<button title="unfocused">Do not click me</button>
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const button = component.getByTitle('focused') as HTMLButtonElement;
|
||||
const unfocusedButton = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLButtonElement;
|
||||
expect(document.activeElement).toBe(button);
|
||||
expect(document.activeElement).not.toBe(unfocusedButton);
|
||||
});
|
||||
|
||||
it('should focus a custom component with ref forwarding', async () => {
|
||||
const CustomInput = forwardRef<HTMLInputElement>((props, ref) => (
|
||||
<input type="text" ref={ref} {...props} title="focused" />
|
||||
));
|
||||
CustomInput.displayName = 'CustomInput';
|
||||
|
||||
const component = render(
|
||||
<View>
|
||||
<InitialFocus>
|
||||
{node => <CustomInput ref={node as Ref<HTMLInputElement>} />}
|
||||
</InitialFocus>
|
||||
<input type="text" title="unfocused" />
|
||||
</View>,
|
||||
);
|
||||
|
||||
// This is needed bc of the `setTimeout` in the `InitialFocus` component.
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const input = component.getByTitle('focused') as HTMLInputElement;
|
||||
const unfocusedInput = component.getByTitle(
|
||||
'unfocused',
|
||||
) as HTMLInputElement;
|
||||
expect(document.activeElement).toBe(input);
|
||||
expect(document.activeElement).not.toBe(unfocusedInput);
|
||||
});
|
||||
});
|
||||
@@ -1,118 +0,0 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
ComponentPropsWithRef,
|
||||
type KeyboardEvent,
|
||||
type FocusEvent,
|
||||
} from 'react';
|
||||
import { Input as ReactAriaInput } from 'react-aria-components';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { useResponsive } from './hooks/useResponsive';
|
||||
import { styles } from './styles';
|
||||
import { theme } from './theme';
|
||||
|
||||
export const baseInputStyle = {
|
||||
outline: 0,
|
||||
backgroundColor: theme.tableBackground,
|
||||
color: theme.formInputText,
|
||||
margin: 0,
|
||||
padding: 5,
|
||||
borderRadius: 4,
|
||||
border: '1px solid ' + theme.formInputBorder,
|
||||
};
|
||||
|
||||
const defaultInputClassName = css({
|
||||
...baseInputStyle,
|
||||
color: theme.formInputText,
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
flexShrink: 0,
|
||||
'&[data-focused]': {
|
||||
border: '1px solid ' + theme.formInputBorderSelected,
|
||||
boxShadow: '0 1px 1px ' + theme.formInputShadowSelected,
|
||||
},
|
||||
'&[data-disabled]': {
|
||||
color: theme.formInputTextPlaceholder,
|
||||
},
|
||||
'::placeholder': { color: theme.formInputTextPlaceholder },
|
||||
...styles.smallText,
|
||||
});
|
||||
|
||||
export type InputProps = ComponentPropsWithRef<typeof ReactAriaInput> & {
|
||||
onEnter?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onEscape?: (value: string, event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
onChangeValue?: (
|
||||
newValue: string,
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => void;
|
||||
onUpdate?: (newValue: string, event: FocusEvent<HTMLInputElement>) => void;
|
||||
};
|
||||
|
||||
export function Input({
|
||||
ref,
|
||||
onEnter,
|
||||
onEscape,
|
||||
onChangeValue,
|
||||
onUpdate,
|
||||
className,
|
||||
...props
|
||||
}: InputProps) {
|
||||
return (
|
||||
<ReactAriaInput
|
||||
ref={ref}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps => cx(defaultInputClassName, className(renderProps))
|
||||
: cx(defaultInputClassName, className)
|
||||
}
|
||||
{...props}
|
||||
onKeyUp={e => {
|
||||
props.onKeyUp?.(e);
|
||||
|
||||
if (e.key === 'Enter' && onEnter) {
|
||||
onEnter(e.currentTarget.value, e);
|
||||
}
|
||||
|
||||
if (e.key === 'Escape' && onEscape) {
|
||||
onEscape(e.currentTarget.value, e);
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
onUpdate?.(e.currentTarget.value, e);
|
||||
props.onBlur?.(e);
|
||||
}}
|
||||
onChange={e => {
|
||||
onChangeValue?.(e.currentTarget.value, e);
|
||||
props.onChange?.(e);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultBigInputClassName = css({
|
||||
padding: 10,
|
||||
fontSize: 15,
|
||||
border: 'none',
|
||||
...styles.shadow,
|
||||
'&[data-focused]': { border: 'none', ...styles.shadow },
|
||||
});
|
||||
|
||||
export function BigInput({ className, ...props }: InputProps) {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps => cx(defaultBigInputClassName, className(renderProps))
|
||||
: cx(defaultBigInputClassName, className)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResponsiveInput(props: InputProps) {
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
|
||||
return isNarrowWidth ? <BigInput {...props} /> : <Input {...props} />;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { forwardRef, type ReactNode, type CSSProperties } from 'react';
|
||||
|
||||
import { styles } from './styles';
|
||||
import { Text } from './Text';
|
||||
import { theme } from './theme';
|
||||
|
||||
type LabelProps = {
|
||||
title: ReactNode;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export const Label = forwardRef<HTMLSpanElement, LabelProps>(
|
||||
({ title, style }: LabelProps, ref) => {
|
||||
return (
|
||||
<Text
|
||||
ref={ref}
|
||||
style={{
|
||||
...styles.text,
|
||||
color: theme.tableRowHeaderText,
|
||||
textAlign: 'right',
|
||||
fontSize: 14,
|
||||
marginBottom: 2,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Label.displayName = 'Label';
|
||||
@@ -1,57 +0,0 @@
|
||||
import { type ComponentProps, useCallback, useEffect, useRef } from 'react';
|
||||
import { Popover as ReactAriaPopover } from 'react-aria-components';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { styles } from './styles';
|
||||
|
||||
type PopoverProps = ComponentProps<typeof ReactAriaPopover>;
|
||||
|
||||
export const Popover = ({
|
||||
style = {},
|
||||
shouldCloseOnInteractOutside,
|
||||
...props
|
||||
}: PopoverProps) => {
|
||||
const ref = useRef<HTMLElement>(null);
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(e: FocusEvent) => {
|
||||
if (!ref.current?.contains(e.relatedTarget as Node)) {
|
||||
props.onOpenChange?.(false);
|
||||
}
|
||||
},
|
||||
[props],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.isNonModal) return;
|
||||
if (props.isOpen) {
|
||||
ref.current?.addEventListener('focusout', handleFocus);
|
||||
} else {
|
||||
ref.current?.removeEventListener('focusout', handleFocus);
|
||||
}
|
||||
}, [handleFocus, props.isNonModal, props.isOpen]);
|
||||
|
||||
return (
|
||||
<ReactAriaPopover
|
||||
ref={ref}
|
||||
placement="bottom end"
|
||||
offset={1}
|
||||
className={css({
|
||||
...styles.tooltip,
|
||||
...styles.lightScrollbar,
|
||||
padding: 0,
|
||||
userSelect: 'none',
|
||||
...style,
|
||||
})}
|
||||
shouldCloseOnInteractOutside={element => {
|
||||
if (shouldCloseOnInteractOutside) {
|
||||
return shouldCloseOnInteractOutside(element);
|
||||
}
|
||||
|
||||
return true;
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import { useRef, useState, type CSSProperties } from 'react';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { SvgExpandArrow } from './icons/v0';
|
||||
import { Menu } from './Menu';
|
||||
import { Popover } from './Popover';
|
||||
import { View } from './View';
|
||||
|
||||
function isValueOption<Value>(
|
||||
option: readonly [Value, string] | typeof Menu.line,
|
||||
): option is [Value, string] {
|
||||
return option !== Menu.line;
|
||||
}
|
||||
|
||||
export type SelectOption<Value = string> = [Value, string] | typeof Menu.line;
|
||||
|
||||
type SelectProps<Value> = {
|
||||
id?: string;
|
||||
bare?: boolean;
|
||||
options: Array<readonly [Value, string] | typeof Menu.line>;
|
||||
value: Value;
|
||||
defaultLabel?: string;
|
||||
onChange?: (newValue: Value) => void;
|
||||
disabled?: boolean;
|
||||
disabledKeys?: Value[];
|
||||
style?: CSSProperties;
|
||||
popoverStyle?: CSSProperties;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Array<[string, string]>} options - An array of options value-label pairs.
|
||||
* @param {string} value - The currently selected option value.
|
||||
* @param {string} [defaultLabel] - The label to display when the selected value is not in the options.
|
||||
* @param {function} [onChange] - A callback function invoked when the selected value changes.
|
||||
* @param {CSSProperties} [style] - Custom styles to apply to the selected button.
|
||||
* @param {string[]} [disabledKeys] - An array of option values to disable.
|
||||
*
|
||||
* @example
|
||||
* // Usage:
|
||||
* // <Select options={[['1', 'Option 1'], ['2', 'Option 2']]} value="1" onChange={handleOnChange} />
|
||||
* // <Select options={[['1', 'Option 1'], ['2', 'Option 2']]} value="3" defaultLabel="Select an option" onChange={handleOnChange} />
|
||||
*/
|
||||
export function Select<const Value = string>({
|
||||
id,
|
||||
bare,
|
||||
options,
|
||||
value,
|
||||
defaultLabel = '',
|
||||
onChange,
|
||||
disabled = false,
|
||||
disabledKeys = [],
|
||||
style = {},
|
||||
popoverStyle = {},
|
||||
className,
|
||||
}: SelectProps<Value>) {
|
||||
const targetOption = options
|
||||
.filter(isValueOption)
|
||||
.find(option => option[0] === value);
|
||||
|
||||
const triggerRef = useRef(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
ref={triggerRef}
|
||||
id={id}
|
||||
variant={bare ? 'bare' : 'normal'}
|
||||
isDisabled={disabled}
|
||||
onPress={() => {
|
||||
setIsOpen(true);
|
||||
}}
|
||||
style={style}
|
||||
className={className}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 5,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: 'calc(100% - 7px)',
|
||||
}}
|
||||
>
|
||||
{targetOption ? targetOption[1] : defaultLabel}
|
||||
</span>
|
||||
<SvgExpandArrow
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
color: 'inherit',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</Button>
|
||||
|
||||
<Popover
|
||||
triggerRef={triggerRef}
|
||||
placement="bottom start"
|
||||
isOpen={isOpen}
|
||||
onOpenChange={() => setIsOpen(false)}
|
||||
style={popoverStyle}
|
||||
>
|
||||
<Menu
|
||||
onMenuSelect={item => {
|
||||
onChange?.(item);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
items={options.map(item =>
|
||||
item === Menu.line
|
||||
? Menu.line
|
||||
: {
|
||||
name: item[0],
|
||||
text: item[1],
|
||||
disabled: disabledKeys.includes(item[0]),
|
||||
},
|
||||
)}
|
||||
getItemStyle={option => {
|
||||
if (targetOption && targetOption[0] === option.name) {
|
||||
return { fontWeight: 'bold' };
|
||||
}
|
||||
return {};
|
||||
}}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, { type CSSProperties, type ReactNode } from 'react';
|
||||
|
||||
import { View } from './View';
|
||||
|
||||
type SpaceBetweenProps = {
|
||||
direction?: 'horizontal' | 'vertical';
|
||||
gap?: number;
|
||||
style?: CSSProperties;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const SpaceBetween = ({
|
||||
direction = 'horizontal',
|
||||
gap = 15,
|
||||
style,
|
||||
children,
|
||||
}: SpaceBetweenProps) => {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
flexDirection: direction === 'horizontal' ? 'row' : 'column',
|
||||
alignItems: 'center',
|
||||
gap,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import React, {
|
||||
type HTMLProps,
|
||||
type Ref,
|
||||
type ReactNode,
|
||||
forwardRef,
|
||||
} from 'react';
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
|
||||
type TextProps = HTMLProps<HTMLSpanElement> & {
|
||||
innerRef?: Ref<HTMLSpanElement>;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export const Text = forwardRef<HTMLSpanElement, TextProps>((props, ref) => {
|
||||
const { className = '', style, innerRef, ...restProps } = props;
|
||||
return (
|
||||
<span
|
||||
{...restProps}
|
||||
ref={innerRef ?? ref}
|
||||
className={cx(className, css(style))}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Text.displayName = 'Text';
|
||||
@@ -1,88 +0,0 @@
|
||||
import React, { type CSSProperties } from 'react';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { View } from './View';
|
||||
|
||||
type ToggleProps = {
|
||||
id: string;
|
||||
isOn: boolean;
|
||||
isDisabled?: boolean;
|
||||
onToggle?: (isOn: boolean) => void;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export const Toggle = ({
|
||||
id,
|
||||
isOn,
|
||||
isDisabled = false,
|
||||
onToggle,
|
||||
className,
|
||||
style,
|
||||
}: ToggleProps) => {
|
||||
return (
|
||||
<View style={style} className={className}>
|
||||
<input
|
||||
id={id}
|
||||
checked={isOn}
|
||||
disabled={isDisabled}
|
||||
onChange={e => onToggle?.(e.target.checked)}
|
||||
className={css({
|
||||
height: 0,
|
||||
width: 0,
|
||||
visibility: 'hidden',
|
||||
})}
|
||||
type="checkbox"
|
||||
/>
|
||||
<label
|
||||
data-toggle-container
|
||||
data-on={isOn}
|
||||
className={String(
|
||||
css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
width: '32px',
|
||||
height: '16px',
|
||||
borderRadius: '100px',
|
||||
position: 'relative',
|
||||
transition: 'background-color .2s',
|
||||
backgroundColor: isOn
|
||||
? theme.checkboxToggleBackgroundSelected
|
||||
: theme.checkboxToggleBackground,
|
||||
}),
|
||||
)}
|
||||
htmlFor={id}
|
||||
>
|
||||
<span
|
||||
data-toggle
|
||||
data-on={isOn}
|
||||
className={css(
|
||||
{
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
content: '" "',
|
||||
position: 'absolute',
|
||||
top: '2px',
|
||||
left: '2px',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
borderRadius: '100px',
|
||||
transition: '0.2s',
|
||||
boxShadow: '0 0 2px 0 rgba(10, 10, 10, 0.29)',
|
||||
backgroundColor: isDisabled
|
||||
? theme.checkboxToggleDisabled
|
||||
: '#fff',
|
||||
},
|
||||
isOn && {
|
||||
left: 'calc(100% - 2px)',
|
||||
transform: 'translateX(-100%)',
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import { Tooltip as AriaTooltip, TooltipTrigger } from 'react-aria-components';
|
||||
|
||||
import { styles } from './styles';
|
||||
import { View } from './View';
|
||||
|
||||
type TooltipProps = Partial<ComponentProps<typeof AriaTooltip>> & {
|
||||
children: ReactNode;
|
||||
content: ReactNode;
|
||||
triggerProps?: Partial<ComponentProps<typeof TooltipTrigger>>;
|
||||
};
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
content,
|
||||
triggerProps = {},
|
||||
...props
|
||||
}: TooltipProps) => {
|
||||
const triggerRef = useRef(null);
|
||||
const [isHovered, setIsHover] = useState(false);
|
||||
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
|
||||
const handlePointerEnter = useCallback(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setIsHover(true);
|
||||
}, triggerProps.delay ?? 300);
|
||||
|
||||
hoverTimeoutRef.current = timeout;
|
||||
}, [triggerProps.delay]);
|
||||
|
||||
const handlePointerLeave = useCallback(() => {
|
||||
if (hoverTimeoutRef.current) {
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
|
||||
setIsHover(false);
|
||||
}, []);
|
||||
|
||||
// Force closing the tooltip whenever the disablement state changes
|
||||
useEffect(() => {
|
||||
setIsHover(false);
|
||||
}, [triggerProps.isDisabled]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{ minHeight: 'auto', flexShrink: 0, maxWidth: '100%' }}
|
||||
ref={triggerRef}
|
||||
onMouseEnter={handlePointerEnter}
|
||||
onMouseLeave={handlePointerLeave}
|
||||
>
|
||||
<TooltipTrigger
|
||||
isOpen={isHovered && !triggerProps.isDisabled}
|
||||
{...triggerProps}
|
||||
>
|
||||
{children}
|
||||
|
||||
<AriaTooltip triggerRef={triggerRef} style={styles.tooltip} {...props}>
|
||||
{content}
|
||||
</AriaTooltip>
|
||||
</TooltipTrigger>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useWindowSize } from 'usehooks-ts';
|
||||
|
||||
import { breakpoints } from '../tokens';
|
||||
|
||||
export function useResponsive() {
|
||||
const { height, width } = useWindowSize({
|
||||
debounceDelay: 250,
|
||||
});
|
||||
|
||||
// Possible view modes: narrow, small, medium, wide
|
||||
// To check if we're at least small width, check !isNarrowWidth
|
||||
return {
|
||||
// atLeastMediumWidth is provided to avoid checking (isMediumWidth || isWideWidth)
|
||||
atLeastMediumWidth: width >= breakpoints.medium,
|
||||
isNarrowWidth: width < breakpoints.small,
|
||||
isSmallWidth: width >= breakpoints.small && width < breakpoints.medium,
|
||||
isMediumWidth: width >= breakpoints.medium && width < breakpoints.wide,
|
||||
// No atLeastWideWidth because that's identical to isWideWidth
|
||||
isWideWidth: width >= breakpoints.wide,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgChartArea = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M 2.5 13 C 2 13.5 2 13.5 2 14 V 15 C 2 16 2 16 3 16 L 17 16 C 18 16 18 16 18 15 V 8.5 C 18 8 18 8 17.5 7.5 L 16 6 C 15.5 5.6 15.5 5.6 15 6 L 11 10 C 10.5 10.25 10.5 10.25 10 10 L 8 9 C 7.5 8.7 7.5 8.7 7 9 z M 0 5 c 0 -1.1 0.9 -2 2 -2 h 16 a 2 2 0 0 1 2 2 v 12 a 2 2 0 0 1 -2 2 H 2 a 2 2 0 0 1 -2 -2 V 4 z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgArrowButtonSingleLeft1 = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M.25 12a2.643 2.643 0 0 1 .775-1.875L10.566.584a1.768 1.768 0 0 1 2.5 2.5l-8.739 8.739a.25.25 0 0 0 0 .354l8.739 8.739a1.768 1.768 0 0 1-2.5 2.5l-9.541-9.541A2.643 2.643 0 0 1 .25 12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 17 56.7" style="enable-background:new 0 0 17 56.7;" xml:space="preserve">
|
||||
<path d="M1.9,2.2L1.9,2.2c1.3,0.2,2.4,0.7,3.4,1.5c0.8,0.8,1.5,1.8,2,2.9c0.9,2.3,3.1,8.4,3.1,21.8S8.2,47.8,7.3,50.1
|
||||
c-0.5,1.1-1.2,2.1-2,2.9c-1,0.8-2.1,1.3-3.3,1.5H1.9c-0.4,0.1-0.6,0.4-0.6,0.8c0.1,0.3,0.3,0.6,0.6,0.6c1.6,0.2,3.2-0.1,4.7-0.8
|
||||
c1.4-0.8,2.7-1.9,3.6-3.2c1.7-2.6,2.9-5.4,3.6-8.4l0.1-0.3c2.5-9.7,2.5-19.8,0-29.5l-0.1-0.3c-0.7-3-1.9-5.8-3.6-8.4
|
||||
C9.3,3.5,8,2.4,6.6,1.6C5.1,0.9,3.5,0.7,1.9,0.9c-0.4,0-0.7,0.4-0.6,0.7C1.3,1.9,1.5,2.2,1.9,2.2L1.9,2.2z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 817 B |
@@ -1,22 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgCloseParenthesis = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 17 56.7"
|
||||
preserveAspectRatio="none"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M1.9,2.2L1.9,2.2c1.3,0.2,2.4,0.7,3.4,1.5c0.8,0.8,1.5,1.8,2,2.9c0.9,2.3,3.1,8.4,3.1,21.8S8.2,47.8,7.3,50.1
|
||||
c-0.5,1.1-1.2,2.1-2,2.9c-1,0.8-2.1,1.3-3.3,1.5H1.9c-0.4,0.1-0.6,0.4-0.6,0.8c0.1,0.3,0.3,0.6,0.6,0.6c1.6,0.2,3.2-0.1,4.7-0.8
|
||||
c1.4-0.8,2.7-1.9,3.6-3.2c1.7-2.6,2.9-5.4,3.6-8.4l0.1-0.3c2.5-9.7,2.5-19.8,0-29.5l-0.1-0.3c-0.7-3-1.9-5.8-3.6-8.4
|
||||
C9.3,3.5,8,2.4,6.6,1.6C5.1,0.9,3.5,0.7,1.9,0.9c-0.4,0-0.7,0.4-0.6,0.7C1.3,1.9,1.5,2.2,1.9,2.2L1.9,2.2z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgDownAndRightArrow = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M4.092 3.658 1.267 6.484l.708.708.708.708 1.609-1.608L5.9 4.684V7.4c.001 2.838.022 3.466.132 3.96.393 1.766 1.732 2.972 3.985 3.59.238.065.53.133.65.151.119.018 1.229.055 2.466.082 1.238.028 2.458.06 2.712.072l.462.021-1.561 1.562-1.562 1.562.708.708.708.708 2.7-2.699 2.7-2.7v-.5l-2.7-2.7-2.7-2.7-.7.7-.7.699 1.675 1.679 1.675 1.678-.617-.019c-.339-.011-1.584-.043-2.766-.071-1.191-.028-2.232-.067-2.334-.087a6.822 6.822 0 0 1-1.283-.426c-.754-.356-1.201-.777-1.447-1.365-.167-.399-.17-.463-.17-3.649V4.684L9.55 6.3l1.617 1.616.708-.708.708-.708L9.75 3.667 6.917.833 4.092 3.658"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,19 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgHelp = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zM7.92 9.234v.102a.5.5 0 0 0 .5.5h.997a.499.499 0 0 0 .499-.499c0-1.29.998-1.979 2.34-1.979 1.308 0 2.168.689 2.168 1.67 0 .928-.482 1.359-1.686 1.91l-.344.154C11.379 11.54 11 12.21 11 13.381v.119a.5.5 0 0 0 .5.5h.997a.499.499 0 0 0 .499-.499c0-.516.138-.723.55-.912l.345-.155c1.445-.654 2.529-1.514 2.529-3.39v-.103c0-1.978-1.72-3.441-4.164-3.441-2.478 0-4.336 1.428-4.336 3.734zm2.58 7.757c0 .867.659 1.509 1.491 1.509.85 0 1.509-.642 1.509-1.509 0-.867-.659-1.491-1.509-1.491-.832 0-1.491.624-1.491 1.491z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 17 56.7" style="enable-background:new 0 0 17 56.7;" xml:space="preserve">
|
||||
<path d="M15.1,54.5L15.1,54.5c-1.3-0.2-2.4-0.7-3.4-1.5c-0.8-0.8-1.5-1.8-2-2.9c-0.9-2.3-3.1-8.4-3.1-21.8S8.8,8.9,9.8,6.6
|
||||
c0.5-1.1,1.2-2.1,2-2.9c1-0.8,2.1-1.3,3.3-1.5h0.1c0.4-0.1,0.6-0.4,0.6-0.8c-0.1-0.3-0.3-0.6-0.6-0.6c-1.6-0.2-3.2,0.1-4.7,0.8
|
||||
C9,2.4,7.8,3.5,6.8,4.8c-1.7,2.6-2.9,5.4-3.6,8.4l-0.1,0.3c-2.5,9.7-2.5,19.8,0,29.5l0.1,0.3c0.7,3,1.9,5.8,3.6,8.4
|
||||
c0.9,1.3,2.2,2.4,3.6,3.2c1.5,0.7,3.1,0.9,4.7,0.8c0.4,0,0.7-0.4,0.6-0.7C15.7,54.8,15.5,54.5,15.1,54.5L15.1,54.5z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 829 B |
@@ -1,22 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgOpenParenthesis = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 17 56.7"
|
||||
preserveAspectRatio="none"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M15.1,54.5L15.1,54.5c-1.3-0.2-2.4-0.7-3.4-1.5c-0.8-0.8-1.5-1.8-2-2.9c-0.9-2.3-3.1-8.4-3.1-21.8S8.8,8.9,9.8,6.6
|
||||
c0.5-1.1,1.2-2.1,2-2.9c1-0.8,2.1-1.3,3.3-1.5h0.1c0.4-0.1,0.6-0.4,0.6-0.8c-0.1-0.3-0.3-0.6-0.6-0.6c-1.6-0.2-3.2,0.1-4.7,0.8
|
||||
C9,2.4,7.8,3.5,6.8,4.8c-1.7,2.6-2.9,5.4-3.6,8.4l-0.1,0.3c-2.5,9.7-2.5,19.8,0,29.5l0.1,0.3c0.7,3,1.9,5.8,3.6,8.4
|
||||
c0.9,1.3,2.2,2.4,3.6,3.2c1.5,0.7,3.1,0.9,4.7,0.8c0.4,0,0.7-0.4,0.6-0.7C15.7,54.8,15.5,54.5,15.1,54.5L15.1,54.5z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" baseProfile="basic" id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 28.3 28.3"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M23.2,10.1c1.1,0,2-0.9,2-2V2.2c0-1.1-0.9-2-2-2h-18c-1.1,0-2,0.9-2,2c0,0.4,0.1,0.9,0.4,1.2l8.1,10.8L3.6,25
|
||||
c-0.7,0.9-0.5,2.1,0.4,2.8c0.3,0.3,0.8,0.4,1.2,0.4h18c1.1,0,2-0.9,2-2v-5.8c0-1.1-0.9-2-2-2s-2,0.9-2,2v3.8h-12l6.6-8.8
|
||||
c0.5-0.7,0.5-1.7,0-2.4L9.2,4.2h12v3.9C21.2,9.2,22.1,10.1,23.2,10.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 654 B |
@@ -1,20 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgSum = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 28.3 28.3"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M23.2,10.1c1.1,0,2-0.9,2-2V2.2c0-1.1-0.9-2-2-2h-18c-1.1,0-2,0.9-2,2c0,0.4,0.1,0.9,0.4,1.2l8.1,10.8L3.6,25
|
||||
c-0.7,0.9-0.5,2.1,0.4,2.8c0.3,0.3,0.8,0.4,1.2,0.4h18c1.1,0,2-0.9,2-2v-5.8c0-1.1-0.9-2-2-2s-2,0.9-2,2v3.8h-12l6.6-8.8
|
||||
c0.5-0.7,0.5-1.7,0-2.4L9.2,4.2h12v3.9C21.2,9.2,22.1,10.1,23.2,10.1z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
<svg id="Bold" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>arrow-button-single-left-1</title><path d="M.25,12a2.643,2.643,0,0,1,.775-1.875L10.566.584a1.768,1.768,0,0,1,2.5,2.5L4.327,11.823a.25.25,0,0,0,0,.354l8.739,8.739a1.768,1.768,0,0,1-2.5,2.5L1.025,13.875A2.643,2.643,0,0,1,.25,12Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 312 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M4.092 3.658 1.267 6.484l.708.708.708.708 1.609-1.608L5.9 4.684V7.4c.001 2.838.022 3.466.132 3.96.393 1.766 1.732 2.972 3.985 3.59.238.065.53.133.65.151.119.018 1.229.055 2.466.082 1.238.028 2.458.06 2.712.072l.462.021-1.561 1.562-1.562 1.562.708.708.708.708 2.7-2.699 2.7-2.7v-.5l-2.7-2.7-2.7-2.7-.7.7-.7.699 1.675 1.679 1.675 1.678-.617-.019c-.339-.011-1.584-.043-2.766-.071-1.191-.028-2.232-.067-2.334-.087a6.822 6.822 0 0 1-1.283-.426c-.754-.356-1.201-.777-1.447-1.365-.167-.399-.17-.463-.17-3.649V4.684L9.55 6.3l1.617 1.616.708-.708.708-.708L9.75 3.667 6.917.833 4.092 3.658" /></svg>
|
||||
|
Before Width: | Height: | Size: 658 B |
@@ -1,9 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path
|
||||
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zM7.92 9.234v.102a.5.5 0 0 0 .5.5h.997a.499.499 0 0 0 .499-.499c0-1.29.998-1.979 2.34-1.979 1.308 0 2.168.689 2.168 1.67 0 .928-.482 1.359-1.686 1.91l-.344.154C11.379 11.54 11 12.21 11 13.381v.119a.5.5 0 0 0 .5.5h.997a.499.499 0 0 0 .499-.499c0-.516.138-.723.55-.912l.345-.155c1.445-.654 2.529-1.514 2.529-3.39v-.103c0-1.978-1.72-3.441-4.164-3.441-2.478 0-4.336 1.428-4.336 3.734zm2.58 7.757c0 .867.659 1.509 1.491 1.509.85 0 1.509-.642 1.509-1.509 0-.867-.659-1.491-1.509-1.491-.832 0-1.491.624-1.491 1.491z"
|
||||
fill="inherit"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 905 B |
@@ -1,157 +0,0 @@
|
||||
import { keyframes } from '@emotion/css';
|
||||
|
||||
import { theme } from './theme';
|
||||
import { tokens } from './tokens';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type CSSProperties = Record<string, any>;
|
||||
|
||||
const MOBILE_MIN_HEIGHT = 40;
|
||||
|
||||
const shadowLarge = {
|
||||
boxShadow: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const styles: Record<string, any> = {
|
||||
incomeHeaderHeight: 70,
|
||||
cardShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
|
||||
monthRightPadding: 5,
|
||||
menuBorderRadius: 4,
|
||||
mobileMinHeight: MOBILE_MIN_HEIGHT,
|
||||
mobileMenuItem: {
|
||||
fontSize: 17,
|
||||
fontWeight: 400,
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
height: MOBILE_MIN_HEIGHT,
|
||||
minHeight: MOBILE_MIN_HEIGHT,
|
||||
},
|
||||
mobileEditingPadding: 12,
|
||||
altMenuMaxHeight: 250,
|
||||
altMenuText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
altMenuHeaderText: {
|
||||
fontSize: 13,
|
||||
fontWeight: 700,
|
||||
},
|
||||
veryLargeText: {
|
||||
fontSize: 30,
|
||||
fontWeight: 600,
|
||||
},
|
||||
largeText: {
|
||||
fontSize: 20,
|
||||
fontWeight: 700,
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
mediumText: {
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
},
|
||||
smallText: {
|
||||
fontSize: 13,
|
||||
},
|
||||
verySmallText: {
|
||||
fontSize: 12,
|
||||
},
|
||||
tinyText: {
|
||||
fontSize: 10,
|
||||
},
|
||||
page: {
|
||||
flex: 1,
|
||||
'@media (max-height: 550px)': {
|
||||
minHeight: 700, // ensure we can scroll on small screens
|
||||
},
|
||||
paddingTop: 8, // height of the titlebar
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
paddingTop: 36,
|
||||
},
|
||||
},
|
||||
pageContent: {
|
||||
paddingLeft: 2,
|
||||
paddingRight: 2,
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
},
|
||||
},
|
||||
settingsPageContent: {
|
||||
padding: 20,
|
||||
[`@media (min-width: ${tokens.breakpoint_small})`]: {
|
||||
padding: 'inherit',
|
||||
},
|
||||
},
|
||||
staticText: {
|
||||
cursor: 'default',
|
||||
userSelect: 'none',
|
||||
},
|
||||
shadow: {
|
||||
boxShadow: '0 2px 4px 0 rgba(0,0,0,0.1)',
|
||||
},
|
||||
shadowLarge,
|
||||
tnum: {
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
fontFeatureSettings: '"tnum"',
|
||||
},
|
||||
notFixed: { fontFeatureSettings: '' },
|
||||
text: {
|
||||
fontSize: 16,
|
||||
// lineHeight: 22.4 // TODO: This seems like trouble, but what's the right value?
|
||||
},
|
||||
delayedFadeIn: {
|
||||
animationName: keyframes({
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
}),
|
||||
animationDuration: '1s',
|
||||
animationFillMode: 'both',
|
||||
animationDelay: '0.5s',
|
||||
},
|
||||
underlinedText: {
|
||||
borderBottom: `2px solid`,
|
||||
},
|
||||
noTapHighlight: {
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
},
|
||||
lineClamp: (lines: number) => {
|
||||
return {
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: lines,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
wordBreak: 'break-word',
|
||||
};
|
||||
},
|
||||
tooltip: {
|
||||
padding: 5,
|
||||
...shadowLarge,
|
||||
borderWidth: 2,
|
||||
borderRadius: 4,
|
||||
borderStyle: 'solid',
|
||||
borderColor: theme.tooltipBorder,
|
||||
backgroundColor: theme.tooltipBackground,
|
||||
color: theme.tooltipText,
|
||||
overflow: 'auto',
|
||||
},
|
||||
popover: {
|
||||
border: 'none',
|
||||
backgroundColor: theme.menuBackground,
|
||||
color: theme.menuItemText,
|
||||
},
|
||||
// Dynamically set
|
||||
horizontalScrollbar: null as CSSProperties | null,
|
||||
lightScrollbar: null as CSSProperties | null,
|
||||
darkScrollbar: null as CSSProperties | null,
|
||||
scrollbarWidth: null as number | null,
|
||||
editorPill: {
|
||||
color: theme.pillText,
|
||||
backgroundColor: theme.pillBackground,
|
||||
borderRadius: 4,
|
||||
padding: '3px 5px',
|
||||
},
|
||||
};
|
||||
@@ -1,203 +0,0 @@
|
||||
export const theme = {
|
||||
pageBackground: 'var(--color-pageBackground)',
|
||||
pageBackgroundModalActive: 'var(--color-pageBackgroundModalActive)',
|
||||
pageBackgroundTopLeft: 'var(--color-pageBackgroundTopLeft)',
|
||||
pageBackgroundBottomRight: 'var(--color-pageBackgroundBottomRight)',
|
||||
pageBackgroundLineTop: 'var(--color-pageBackgroundLineTop)',
|
||||
pageBackgroundLineMid: 'var(--color-pageBackgroundLineMid)',
|
||||
pageBackgroundLineBottom: 'var(--color-pageBackgroundLineBottom)',
|
||||
pageText: 'var(--color-pageText)',
|
||||
pageTextLight: 'var(--color-pageTextLight)',
|
||||
pageTextSubdued: 'var(--color-pageTextSubdued)',
|
||||
pageTextDark: 'var(--color-pageTextDark)',
|
||||
pageTextPositive: 'var(--color-pageTextPositive)',
|
||||
pageTextLink: 'var(--color-pageTextLink)',
|
||||
pageTextLinkLight: 'var(--color-pageTextLinkLight)',
|
||||
cardBackground: 'var(--color-cardBackground)',
|
||||
cardBorder: 'var(--color-cardBorder)',
|
||||
cardShadow: 'var(--color-cardShadow)',
|
||||
tableBackground: 'var(--color-tableBackground)',
|
||||
tableRowBackgroundHover: 'var(--color-tableRowBackgroundHover)',
|
||||
tableText: 'var(--color-tableText)',
|
||||
tableTextLight: 'var(--color-tableTextLight)',
|
||||
tableTextSubdued: 'var(--color-tableTextSubdued)',
|
||||
tableTextSelected: 'var(--color-tableTextSelected)',
|
||||
tableTextHover: 'var(--color-tableTextHover)',
|
||||
tableTextInactive: 'var(--color-tableTextInactive)',
|
||||
tableHeaderText: 'var(--color-tableHeaderText)',
|
||||
tableHeaderBackground: 'var(--color-tableHeaderBackground)',
|
||||
tableBorder: 'var(--color-tableBorder)',
|
||||
tableBorderSelected: 'var(--color-tableBorderSelected)',
|
||||
tableBorderHover: 'var(--color-tableBorderHover)',
|
||||
tableBorderSeparator: 'var(--color-tableBorderSeparator)',
|
||||
tableRowBackgroundHighlight: 'var(--color-tableRowBackgroundHighlight)',
|
||||
tableRowBackgroundHighlightText:
|
||||
'var(--color-tableRowBackgroundHighlightText)',
|
||||
tableRowHeaderBackground: 'var(--color-tableRowHeaderBackground)',
|
||||
tableRowHeaderText: 'var(--color-tableRowHeaderText)',
|
||||
sidebarBackground: 'var(--color-sidebarBackground)',
|
||||
sidebarItemBackgroundPending: 'var(--color-sidebarItemBackgroundPending)',
|
||||
sidebarItemBackgroundPositive: 'var(--color-sidebarItemBackgroundPositive)',
|
||||
sidebarItemBackgroundFailed: 'var(--color-sidebarItemBackgroundFailed)',
|
||||
sidebarItemAccentSelected: 'var(--color-sidebarItemAccentSelected)',
|
||||
sidebarItemBackgroundHover: 'var(--color-sidebarItemBackgroundHover)',
|
||||
sidebarItemText: 'var(--color-sidebarItemText)',
|
||||
sidebarItemTextSelected: 'var(--color-sidebarItemTextSelected)',
|
||||
menuBackground: 'var(--color-menuBackground)',
|
||||
menuItemBackground: 'var(--color-menuItemBackground)',
|
||||
menuItemBackgroundHover: 'var(--color-menuItemBackgroundHover)',
|
||||
menuItemText: 'var(--color-menuItemText)',
|
||||
menuItemTextHover: 'var(--color-menuItemTextHover)',
|
||||
menuItemTextSelected: 'var(--color-menuItemTextSelected)',
|
||||
menuItemTextHeader: 'var(--color-menuItemTextHeader)',
|
||||
menuBorder: 'var(--color-menuBorder)',
|
||||
menuBorderHover: 'var(--color-menuBorderHover)',
|
||||
menuKeybindingText: 'var(--color-menuKeybindingText)',
|
||||
menuAutoCompleteBackground: 'var(--color-menuAutoCompleteBackground)',
|
||||
menuAutoCompleteBackgroundHover:
|
||||
'var(--color-menuAutoCompleteBackgroundHover)',
|
||||
menuAutoCompleteText: 'var(--color-menuAutoCompleteText)',
|
||||
menuAutoCompleteTextHover: 'var(--color-menuAutoCompleteTextHover)',
|
||||
menuAutoCompleteTextHeader: 'var(--color-menuAutoCompleteTextHeader)',
|
||||
menuAutoCompleteItemTextHover: 'var(--color-menuAutoCompleteItemTextHover)',
|
||||
menuAutoCompleteItemText: 'var(--color-menuAutoCompleteItemText)',
|
||||
modalBackground: 'var(--color-modalBackground)',
|
||||
modalBorder: 'var(--color-modalBorder)',
|
||||
mobileHeaderBackground: 'var(--color-mobileHeaderBackground)',
|
||||
mobileHeaderText: 'var(--color-mobileHeaderText)',
|
||||
mobileHeaderTextSubdued: 'var(--color-mobileHeaderTextSubdued)',
|
||||
mobileHeaderTextHover: 'var(--color-mobileHeaderTextHover)',
|
||||
mobilePageBackground: 'var(--color-mobilePageBackground)',
|
||||
mobileNavBackground: 'var(--color-mobileNavBackground)',
|
||||
mobileNavItem: 'var(--color-mobileNavItem)',
|
||||
mobileNavItemSelected: 'var(--color-mobileNavItemSelected)',
|
||||
mobileAccountShadow: 'var(--color-mobileAccountShadow)',
|
||||
mobileAccountText: 'var(--color-mobileAccountText)',
|
||||
mobileTransactionSelected: 'var(--color-mobileTransactionSelected)',
|
||||
mobileViewTheme: 'var(--color-mobileViewTheme)',
|
||||
mobileConfigServerViewTheme: 'var(--color-mobileConfigServerViewTheme)',
|
||||
markdownNormal: 'var(--color-markdownNormal)',
|
||||
markdownDark: 'var(--color-markdownDark)',
|
||||
markdownLight: 'var(--color-markdownLight)',
|
||||
buttonMenuText: 'var(--color-buttonMenuText)',
|
||||
buttonMenuTextHover: 'var(--color-buttonMenuTextHover)',
|
||||
buttonMenuBackground: 'var(--color-buttonMenuBackground)',
|
||||
buttonMenuBackgroundHover: 'var(--color-buttonMenuBackgroundHover)',
|
||||
buttonMenuBorder: 'var(--color-buttonMenuBorder)',
|
||||
buttonMenuSelectedText: 'var(--color-buttonMenuSelectedText)',
|
||||
buttonMenuSelectedTextHover: 'var(--color-buttonMenuSelectedTextHover)',
|
||||
buttonMenuSelectedBackground: 'var(--color-buttonMenuSelectedBackground)',
|
||||
buttonMenuSelectedBackgroundHover:
|
||||
'var(--color-buttonMenuSelectedBackgroundHover)',
|
||||
buttonMenuSelectedBorder: 'var(--color-buttonMenuSelectedBorder)',
|
||||
buttonPrimaryText: 'var(--color-buttonPrimaryText)',
|
||||
buttonPrimaryTextHover: 'var(--color-buttonPrimaryTextHover)',
|
||||
buttonPrimaryBackground: 'var(--color-buttonPrimaryBackground)',
|
||||
buttonPrimaryBackgroundHover: 'var(--color-buttonPrimaryBackgroundHover)',
|
||||
buttonPrimaryBorder: 'var(--color-buttonPrimaryBorder)',
|
||||
buttonPrimaryShadow: 'var(--color-buttonPrimaryShadow)',
|
||||
buttonPrimaryDisabledText: 'var(--color-buttonPrimaryDisabledText)',
|
||||
buttonPrimaryDisabledBackground:
|
||||
'var(--color-buttonPrimaryDisabledBackground)',
|
||||
buttonPrimaryDisabledBorder: 'var(--color-buttonPrimaryDisabledBorder)',
|
||||
buttonNormalText: 'var(--color-buttonNormalText)',
|
||||
buttonNormalTextHover: 'var(--color-buttonNormalTextHover)',
|
||||
buttonNormalBackground: 'var(--color-buttonNormalBackground)',
|
||||
buttonNormalBackgroundHover: 'var(--color-buttonNormalBackgroundHover)',
|
||||
buttonNormalBorder: 'var(--color-buttonNormalBorder)',
|
||||
buttonNormalShadow: 'var(--color-buttonNormalShadow)',
|
||||
buttonNormalSelectedText: 'var(--color-buttonNormalSelectedText)',
|
||||
buttonNormalSelectedBackground: 'var(--color-buttonNormalSelectedBackground)',
|
||||
buttonNormalDisabledText: 'var(--color-buttonNormalDisabledText)',
|
||||
buttonNormalDisabledBackground: 'var(--color-buttonNormalDisabledBackground)',
|
||||
buttonNormalDisabledBorder: 'var(--color-buttonNormalDisabledBorder)',
|
||||
buttonBareText: 'var(--color-buttonBareText)',
|
||||
buttonBareTextHover: 'var(--color-buttonBareTextHover)',
|
||||
buttonBareBackground: 'var(--color-buttonBareBackground)',
|
||||
buttonBareBackgroundHover: 'var(--color-buttonBareBackgroundHover)',
|
||||
buttonBareBackgroundActive: 'var(--color-buttonBareBackgroundActive)',
|
||||
buttonBareDisabledText: 'var(--color-buttonBareDisabledText)',
|
||||
buttonBareDisabledBackground: 'var(--color-buttonBareDisabledBackground)',
|
||||
calendarText: 'var(--color-calendarText)',
|
||||
calendarBackground: 'var(--color-calendarBackground)',
|
||||
calendarItemText: 'var(--color-calendarItemText)',
|
||||
calendarItemBackground: 'var(--color-calendarItemBackground)',
|
||||
calendarSelectedBackground: 'var(--color-calendarSelectedBackground)',
|
||||
noticeBackground: 'var(--color-noticeBackground)',
|
||||
noticeBackgroundLight: 'var(--color-noticeBackgroundLight)',
|
||||
noticeBackgroundDark: 'var(--color-noticeBackgroundDark)',
|
||||
noticeText: 'var(--color-noticeText)',
|
||||
noticeTextLight: 'var(--color-noticeTextLight)',
|
||||
noticeTextDark: 'var(--color-noticeTextDark)',
|
||||
noticeTextMenu: 'var(--color-noticeTextMenu)',
|
||||
noticeTextMenuHover: 'var(--color-noticeTextMenuHover)',
|
||||
noticeBorder: 'var(--color-noticeBorder)',
|
||||
warningBackground: 'var(--color-warningBackground)',
|
||||
warningText: 'var(--color-warningText)',
|
||||
warningTextLight: 'var(--color-warningTextLight)',
|
||||
warningTextDark: 'var(--color-warningTextDark)',
|
||||
warningBorder: 'var(--color-warningBorder)',
|
||||
errorBackground: 'var(--color-errorBackground)',
|
||||
errorText: 'var(--color-errorText)',
|
||||
errorTextDark: 'var(--color-errorTextDark)',
|
||||
errorTextDarker: 'var(--color-errorTextDarker)',
|
||||
errorTextMenu: 'var(--color-errorTextMenu)',
|
||||
errorBorder: 'var(--color-errorBorder)',
|
||||
upcomingBackground: 'var(--color-upcomingBackground)',
|
||||
upcomingText: 'var(--color-upcomingText)',
|
||||
upcomingBorder: 'var(--color-upcomingBorder)',
|
||||
formLabelText: 'var(--color-formLabelText)',
|
||||
formLabelBackground: 'var(--color-formLabelBackground)',
|
||||
formInputBackground: 'var(--color-formInputBackground)',
|
||||
formInputBackgroundSelected: 'var(--color-formInputBackgroundSelected)',
|
||||
formInputBackgroundSelection: 'var(--color-formInputBackgroundSelection)',
|
||||
formInputBorder: 'var(--color-formInputBorder)',
|
||||
formInputTextReadOnlySelection: 'var(--color-formInputTextReadOnlySelection)',
|
||||
formInputBorderSelected: 'var(--color-formInputBorderSelected)',
|
||||
formInputText: 'var(--color-formInputText)',
|
||||
formInputTextSelected: 'var(--color-formInputTextSelected)',
|
||||
formInputTextPlaceholder: 'var(--color-formInputTextPlaceholder)',
|
||||
formInputTextPlaceholderSelected:
|
||||
'var(--color-formInputTextPlaceholderSelected)',
|
||||
formInputTextSelection: 'var(--color-formInputTextSelection)',
|
||||
formInputShadowSelected: 'var(--color-formInputShadowSelected)',
|
||||
formInputTextHighlight: 'var(--color-formInputTextHighlight)',
|
||||
checkboxText: 'var(--color-checkboxText)',
|
||||
checkboxBackgroundSelected: 'var(--color-checkboxBackgroundSelected)',
|
||||
checkboxBorderSelected: 'var(--color-checkboxBorderSelected)',
|
||||
checkboxShadowSelected: 'var(--color-checkboxShadowSelected)',
|
||||
checkboxToggleBackground: 'var(--color-checkboxToggleBackground)',
|
||||
checkboxToggleBackgroundSelected:
|
||||
'var(--color-checkboxToggleBackgroundSelected)',
|
||||
checkboxToggleDisabled: 'var(--color-checkboxToggleDisabled)',
|
||||
pillBackground: 'var(--color-pillBackground)',
|
||||
pillBackgroundLight: 'var(--color-pillBackgroundLight)',
|
||||
pillText: 'var(--color-pillText)',
|
||||
pillTextHighlighted: 'var(--color-pillTextHighlighted)',
|
||||
pillBorder: 'var(--color-pillBorder)',
|
||||
pillBorderDark: 'var(--color-pillBorderDark)',
|
||||
pillBackgroundSelected: 'var(--color-pillBackgroundSelected)',
|
||||
pillTextSelected: 'var(--color-pillTextSelected)',
|
||||
pillBorderSelected: 'var(--color-pillBorderSelected)',
|
||||
pillTextSubdued: 'var(--color-pillTextSubdued)',
|
||||
reportsRed: 'var(--color-reportsRed)',
|
||||
reportsBlue: 'var(--color-reportsBlue)',
|
||||
reportsGreen: 'var(--color-reportsGreen)',
|
||||
reportsGray: 'var(--color-reportsGray)',
|
||||
reportsLabel: 'var(--color-reportsLabel)',
|
||||
reportsInnerLabel: 'var(--color-reportsInnerLabel)',
|
||||
noteTagBackground: 'var(--color-noteTagBackground)',
|
||||
noteTagBackgroundHover: 'var(--color-noteTagBackgroundHover)',
|
||||
noteTagText: 'var(--color-noteTagText)',
|
||||
budgetOtherMonth: 'var(--color-budgetOtherMonth)',
|
||||
budgetCurrentMonth: 'var(--color-budgetCurrentMonth)',
|
||||
budgetHeaderOtherMonth: 'var(--color-budgetHeaderOtherMonth)',
|
||||
budgetHeaderCurrentMonth: 'var(--color-budgetHeaderCurrentMonth)',
|
||||
floatingActionBarBackground: 'var(--color-floatingActionBarBackground)',
|
||||
floatingActionBarBorder: 'var(--color-floatingActionBarBorder)',
|
||||
floatingActionBarText: 'var(--color-floatingActionBarText)',
|
||||
tooltipText: 'var(--color-tooltipText)',
|
||||
tooltipBackground: 'var(--color-tooltipBackground)',
|
||||
tooltipBorder: 'var(--color-tooltipBorder)',
|
||||
calendarCellBackground: 'var(--color-calendarCellBackground)',
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
import path from 'path';
|
||||
|
||||
import peggyLoader from 'vite-plugin-peggy-loader';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
const resolveExtensions = [
|
||||
'.testing.ts',
|
||||
'.web.ts',
|
||||
'.mjs',
|
||||
'.js',
|
||||
'.mts',
|
||||
'.ts',
|
||||
'.jsx',
|
||||
'.tsx',
|
||||
'.json',
|
||||
'.wasm',
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
include: ['src/**/*.web.test.(js|jsx|ts|tsx)'],
|
||||
},
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: /^@actual-app\/crdt(\/.*)?$/,
|
||||
replacement: path.resolve('../../../crdt/src$1'),
|
||||
},
|
||||
],
|
||||
extensions: resolveExtensions,
|
||||
},
|
||||
plugins: [peggyLoader()],
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
export * from './src';
|
||||
export * from './src/main';
|
||||
|
||||
6
packages/crdt/jest.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
};
|
||||
@@ -12,17 +12,20 @@
|
||||
"build:node": "tsc --p tsconfig.dist.json",
|
||||
"proto:generate": "./bin/generate-proto",
|
||||
"build": "rm -rf dist && yarn run build:node && cp src/proto/sync_pb.d.ts dist/src/proto/",
|
||||
"test": "vitest --globals"
|
||||
"test": "jest -c jest.config.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.21.4",
|
||||
"google-protobuf": "^3.12.0-rc.1",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/uuid": "^9.0.8",
|
||||
"@swc/core": "^1.3.105",
|
||||
"@swc/jest": "^0.2.31",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.0.0",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
"typescript": "^5.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`merkle trie > adding an item works 1`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
exports[`merkle trie adding an item works 1`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"hash": 1983295247,
|
||||
},
|
||||
"hash": 1983295247,
|
||||
@@ -34,14 +34,14 @@ exports[`merkle trie > adding an item works 1`] = `
|
||||
},
|
||||
"hash": 1983295247,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
"0": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"hash": 1469038940,
|
||||
},
|
||||
"hash": 1469038940,
|
||||
@@ -78,33 +78,33 @@ exports[`merkle trie > adding an item works 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
exports[`merkle trie pruning works and keeps correct hashes 1`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1000,
|
||||
},
|
||||
"hash": 1000,
|
||||
},
|
||||
"hash": 1000,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1100,
|
||||
},
|
||||
"hash": 1100,
|
||||
@@ -113,28 +113,28 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
},
|
||||
"hash": 1956,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1200,
|
||||
},
|
||||
"hash": 1200,
|
||||
},
|
||||
"hash": 1200,
|
||||
},
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1400,
|
||||
},
|
||||
"hash": 1400,
|
||||
@@ -143,28 +143,28 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
},
|
||||
"hash": 1244,
|
||||
},
|
||||
"2": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1500,
|
||||
},
|
||||
"hash": 1500,
|
||||
},
|
||||
"hash": 1500,
|
||||
},
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1700,
|
||||
},
|
||||
"hash": 1700,
|
||||
@@ -175,29 +175,29 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 1800,
|
||||
},
|
||||
"hash": 1800,
|
||||
},
|
||||
"hash": 1800,
|
||||
},
|
||||
"1": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"1": {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 2000,
|
||||
},
|
||||
"hash": 2000,
|
||||
@@ -206,10 +206,10 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
},
|
||||
"hash": 1972,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 2100,
|
||||
},
|
||||
"hash": 2100,
|
||||
@@ -246,33 +246,33 @@ exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
exports[`merkle trie pruning works and keeps correct hashes 2`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1400,
|
||||
},
|
||||
"hash": 1400,
|
||||
@@ -281,19 +281,19 @@ exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
},
|
||||
"hash": 1244,
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"hash": 1700,
|
||||
},
|
||||
"hash": 1700,
|
||||
@@ -304,20 +304,20 @@ exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"2": {
|
||||
"1": {
|
||||
"1": {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 2000,
|
||||
},
|
||||
"hash": 2000,
|
||||
@@ -326,10 +326,10 @@ exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
},
|
||||
"hash": 1972,
|
||||
},
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"hash": 2100,
|
||||
},
|
||||
"hash": 2100,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import * as merkle from './merkle';
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
|
||||
@@ -134,7 +134,6 @@ export function diff(trie1: TrieNode, trie2: TrieNode): number | null {
|
||||
node2 = node2[diffkey] || emptyTrie();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unreachable
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import { Timestamp } from './timestamp';
|
||||
|
||||
describe('Timestamp', function () {
|
||||
|
||||
@@ -154,7 +154,7 @@ export class Timestamp {
|
||||
/**
|
||||
* maximum timestamp
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
static max = Timestamp.parse(
|
||||
'9999-12-31T23:59:59.999Z-FFFF-FFFFFFFFFFFFFFFF',
|
||||
)!;
|
||||
@@ -294,7 +294,7 @@ export class Timestamp {
|
||||
/**
|
||||
* zero/minimum timestamp
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
static zero = Timestamp.parse(
|
||||
'1970-01-01T00:00:00.000Z-0000-0000000000000000',
|
||||
)!;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// the latest Node 16.x release supports all of the features
|
||||
"target": "ES2021",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node10",
|
||||
"noEmit": false,
|
||||
"declaration": true,
|
||||
"strict": true,
|
||||
|
||||
5
packages/desktop-client/.gitignore
vendored
@@ -6,11 +6,9 @@ node_modules
|
||||
# testing
|
||||
coverage
|
||||
test-results
|
||||
playwright-report
|
||||
|
||||
# production
|
||||
build
|
||||
build-electron
|
||||
build-stats
|
||||
stats.json
|
||||
|
||||
@@ -25,6 +23,3 @@ public/kcab
|
||||
public/data
|
||||
public/data-file-index.txt
|
||||
public/*.wasm
|
||||
|
||||
# translations
|
||||
locale/
|
||||
|
||||
@@ -37,41 +37,18 @@ First start a dev instance:
|
||||
```sh
|
||||
HTTPS=true yarn start
|
||||
```
|
||||
|
||||
or using the dev container:
|
||||
|
||||
```
|
||||
HTTPS=true docker compose up --build
|
||||
```
|
||||
|
||||
Note the network IP address and port the dev instance is listening on.
|
||||
|
||||
Next, navigate to the root of your project folder, run the standardized docker container, and launch the visual regression tests from within it.
|
||||
|
||||
Run via yarn:
|
||||
|
||||
```sh
|
||||
# By default, this connects to https://localhost:3001
|
||||
yarn vrt:docker
|
||||
|
||||
# To use a different ip and port:
|
||||
yarn vrt:docker --e2e-start-url https://ip:port
|
||||
|
||||
# To update snapshots, use the following command:
|
||||
yarn vrt:docker --e2e-start-url https://ip:port --update-snapshots
|
||||
```
|
||||
|
||||
Run manually:
|
||||
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.52.0-jammy /bin/bash
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.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.52.0-jammy /bin/bash
|
||||
docker run --rm --network host -v ${pwd}:/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash
|
||||
|
||||
# Once inside the docker container, run the VRT tests: important - they MUST be ran against a HTTPS server.
|
||||
# Use the ip and port noted earlier
|
||||
# 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
|
||||
|
||||
# To update snapshots, use the following command:
|
||||
@@ -82,17 +59,6 @@ E2E_START_URL=https://ip:port yarn vrt
|
||||
|
||||
You can also run the tests against a remote server by passing the URL:
|
||||
|
||||
Run in standardized docker container:
|
||||
|
||||
```sh
|
||||
E2E_START_URL=https://my-remote-server.com yarn vrt:docker
|
||||
|
||||
# Or pass in server URL as argument
|
||||
yarn vrt:docker --e2e-start-url https://my-remote-server.com
|
||||
```
|
||||
|
||||
Run locally:
|
||||
|
||||
```sh
|
||||
E2E_START_URL=https://my-remote-server.com yarn vrt
|
||||
```
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Local path to the cloned translations repository
|
||||
const localRepoPath = './packages/desktop-client/locale';
|
||||
|
||||
// Compare JSON files and delete incomplete ones
|
||||
const processTranslations = () => {
|
||||
try {
|
||||
const files = fs.readdirSync(localRepoPath);
|
||||
const enJsonPath = path.join(localRepoPath, 'en.json');
|
||||
|
||||
if (!fs.existsSync(enJsonPath)) {
|
||||
throw new Error('en.json not found in the repository.');
|
||||
}
|
||||
|
||||
const enJson = JSON.parse(fs.readFileSync(enJsonPath, 'utf8'));
|
||||
const enKeysCount = Object.keys(enJson).length;
|
||||
|
||||
console.log(`en.json has ${enKeysCount} keys.`);
|
||||
|
||||
files.forEach(file => {
|
||||
if (file === 'en.json' || path.extname(file) !== '.json') return;
|
||||
|
||||
if (file.startsWith('en-')) {
|
||||
console.log(`Keeping ${file} as it's an English language.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = path.join(localRepoPath, file);
|
||||
const jsonData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
const fileKeysCount = Object.keys(jsonData).length;
|
||||
|
||||
// Calculate the percentage of keys present compared to en.json
|
||||
const percentage = (fileKeysCount / enKeysCount) * 100;
|
||||
console.log(
|
||||
`${file} has ${fileKeysCount} keys (${percentage.toFixed(2)}%).`,
|
||||
);
|
||||
|
||||
if (percentage < 50) {
|
||||
fs.unlinkSync(filePath);
|
||||
console.log(`Deleted ${file} due to insufficient keys.`);
|
||||
} else {
|
||||
console.log(`Keeping ${file}.`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Processing completed.');
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
processTranslations();
|
||||
@@ -1,65 +0,0 @@
|
||||
import { type Page } from '@playwright/test';
|
||||
|
||||
import { expect, test } from './fixtures';
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { MobileNavigation } from './page-models/mobile-navigation';
|
||||
|
||||
test.describe('Mobile Accounts', () => {
|
||||
let page: Page;
|
||||
let navigation: MobileNavigation;
|
||||
let configurationPage: ConfigurationPage;
|
||||
|
||||
test.beforeEach(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
navigation = new MobileNavigation(page);
|
||||
configurationPage = new ConfigurationPage(page);
|
||||
|
||||
await page.setViewportSize({
|
||||
width: 350,
|
||||
height: 600,
|
||||
});
|
||||
await page.goto('/');
|
||||
await configurationPage.createTestFile();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('opens the accounts page and asserts on balances', async () => {
|
||||
const accountsPage = await navigation.goToAccountsPage();
|
||||
await accountsPage.waitFor();
|
||||
|
||||
const account = await accountsPage.getNthAccount(1);
|
||||
|
||||
await expect(account.name).toHaveText('Ally Savings');
|
||||
await expect(account.balance).toHaveText('7,653.00');
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('opens individual account page and checks that filtering is working', async () => {
|
||||
const accountsPage = await navigation.goToAccountsPage();
|
||||
await accountsPage.waitFor();
|
||||
|
||||
const accountPage = await accountsPage.openNthAccount(0);
|
||||
await accountPage.waitFor();
|
||||
|
||||
await expect(accountPage.heading).toHaveText('Bank of America');
|
||||
await expect(accountPage.transactionList).toBeVisible();
|
||||
await expect(await accountPage.getBalance()).toBeGreaterThan(0);
|
||||
await expect(accountPage.noTransactionsMessage).not.toBeVisible();
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await accountPage.searchByText('nothing should be found');
|
||||
await expect(accountPage.noTransactionsMessage).toBeVisible();
|
||||
await expect(accountPage.transactions).toHaveCount(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
|
||||
await accountPage.clearSearch();
|
||||
await expect(accountPage.transactions).not.toHaveCount(0);
|
||||
|
||||
await accountPage.searchByText('Kroger');
|
||||
await expect(accountPage.transactions).not.toHaveCount(0);
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB |