Compare commits

..

7 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
d2bf2e9cc9 Fix InputWithContent 2024-11-06 09:19:36 -08:00
Joel Jeremy Marquez
7e1cc49478 Fix import 2024-11-04 15:50:42 -08:00
Joel Jeremy Marquez
e6a49b1d99 Rename className var 2024-11-04 15:48:59 -08:00
Joel Jeremy Marquez
db7d890e79 Fix styling issues 2024-11-04 15:48:32 -08:00
Joel Jeremy Marquez
46977b59ca Delay Input autoSelect a bit 2024-11-04 15:45:28 -08:00
Joel Jeremy Marquez
b3d0348493 Remove FocusScope contain 2024-11-04 15:45:28 -08:00
Joel Jeremy Marquez
b78f1fd575 Use react-aria-component input as base of Actual's Input component 2024-11-04 15:45:28 -08:00
2489 changed files with 39366 additions and 91507 deletions

View File

@@ -1,24 +0,0 @@
---
description:
globs:
alwaysApply: true
---
Before pushing code changes or opening a pull request, follow these steps:
1. Check if your branch already has a changelog file in the "upcoming-release-notes" folder.
2. If there is no changelog file for your branch:
a. Find the number of the most recent (highest-numbered) open issue or pull request on GitHub.
b. Increment that number by 1. Use this as the filename for your new changelog file.
c. Create a new markdown file in the "upcoming-release-notes" folder with the following format:
```
---
category: Features OR Maintenance OR Enhancements OR Bugfix
authors: [$GithubUsername]
---
$Description
```
3. Commit the new changelog file.
4. Proceed with your push or pull request.

View File

@@ -1,32 +0,0 @@
---
description:
globs: *.ts,*.tsx
alwaysApply: false
---
You are an expert in TypeScript and React.
Code Style and Structure
- Write concise, technical TypeScript code.
- Use functional and declarative programming patterns; avoid classes.
- Prefer iteration and modularization over code duplication.
- Use descriptive variable names with auxiliary verbs (e.g., isLoaded, hasError).
- Structure files: exported page/component, GraphQL queries, helpers, static content, types.
Naming Conventions
- Favor named exports for components and utilities.
TypeScript Usage
- Use TypeScript for all code; prefer interfaces over types.
- Avoid enums; use objects or maps instead.
- Avoid using `any` or `unknown` unless absolutely necessary. Look for type definitions in the codebase instead.
- Avoid type assertions with `as` or `!`; prefer using `satisfies`.
Syntax and Formatting
- Use the "function" keyword for pure functions.
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
- Use declarative JSX, keeping JSX minimal and readable.

View File

@@ -1,14 +0,0 @@
---
description:
globs:
alwaysApply: true
---
Vitest test runner is used for unit tests.
When running unit tests, always include the flag `--watch=false` to prevent watch mode.
To run unit tests for a specific package in the monorepo, use the following command:
`yarn workspace <workspaceNameFromPackageJson> run test <pathToTest>`
Recommendation: Minimize the number of dependencies you mock. The fewer dependencies you mock, the better.

View File

@@ -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"
}

28
.eslintignore Normal file
View File

@@ -0,0 +1,28 @@
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-client/playwright-report/
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/*

603
.eslintrc.js Normal file
View File

@@ -0,0 +1,603 @@
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: 'Dont 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 = {
root: true,
env: {
browser: true,
commonjs: true,
es6: true,
jest: true,
node: true,
},
plugins: [
'prettier',
'import',
'rulesdir',
'@typescript-eslint',
'jsx-a11y',
'react-hooks',
],
extends: [
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'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: {
// http://eslint.org/docs/rules/
'array-callback-return': 'warn',
'default-case': ['warn', { commentPattern: '^no default$' }],
'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',
'react-hooks/exhaustive-deps': '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',
// 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',
// 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',
'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: '^(_|React)',
ignoreRestSiblings: true,
caughtErrors: 'none',
},
],
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/react-in-jsx-scope': 'off',
'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 <Link>
selector: 'JSXOpeningElement[name.name="a"]',
message: 'Using <a> is discouraged, please use <Link> 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',
'@typescript-eslint/no-require-imports': 'off',
'import/no-default-export': 'warn',
},
overrides: [
{
files: ['**/*.ts?(x)'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
// typescript-eslint specific options
warnOnUnsupportedTypeScriptVersion: true,
},
plugins: ['@typescript-eslint'],
// 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',
// 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: ['.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/no-restricted-types': [
'warn',
{
types: {
// forbid FC as superflous
FunctionComponent: { message: ruleFCMsg },
FC: { message: ruleFCMsg },
},
},
],
},
},
{
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 Actuals 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',
},
},
{
// 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/App.tsx',
'./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/desktop-client/src/components/common/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',
'./packages/desktop-client/src/components/spreadsheet/useSheetValue.ts',
'./packages/desktop-client/src/components/table.tsx',
'./packages/desktop-client/src/components/Titlebar.tsx',
'./packages/desktop-client/src/components/transactions/MobileTransaction.jsx',
'./packages/desktop-client/src/components/transactions/SelectedTransactions.jsx',
'./packages/desktop-client/src/components/transactions/SimpleTransactionsTable.jsx',
'./packages/desktop-client/src/components/transactions/TransactionList.jsx',
'./packages/desktop-client/src/components/transactions/TransactionsTable.jsx',
'./packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx',
'./packages/desktop-client/src/hooks/useAccounts.ts',
'./packages/desktop-client/src/hooks/useCategories.ts',
'./packages/desktop-client/src/hooks/usePayees.ts',
'./packages/desktop-client/src/hooks/useProperFocus.tsx',
'./packages/desktop-client/src/hooks/useSelected.tsx',
'./packages/loot-core/src/client/query-hooks.tsx',
],
rules: {
'react-hooks/exhaustive-deps': 'off',
},
},
{
files: [
'.eslintrc.js',
'*.test.js',
'*.test.ts',
'*.test.jsx',
'*.test.tsx',
],
rules: {
'rulesdir/typography': 'off',
},
},
],
settings: {
react: {
version: 'detect',
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
},
},
},
};

View File

@@ -1,16 +1,18 @@
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.
**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
@@ -21,6 +23,10 @@ body:
options:
- label: 'I have searched and found no existing issue'
required: true
- label: 'I will be providing steps how to reproduce the bug (in most cases this will also mean uploading a demo budget file)'
required: true
validations:
required: true
- type: textarea
id: what-happened
attributes:
@@ -30,15 +36,8 @@ body:
value: 'A bug happened!'
validations:
required: true
- type: textarea
id: reproduction
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?'
validations:
required: true
- type: markdown
id: env-info
attributes:
value: '## Environment Details'
- type: dropdown

View File

@@ -6,6 +6,3 @@ contact_links:
- 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.

View File

@@ -4,6 +4,7 @@ title: '[Feature] '
labels: ['feature']
body:
- type: markdown
id: intro-md
attributes:
value: |
Thanks for taking the time to fill out this feature request! Please ensure you provide as much information as possible so we can better understand what youre proposing so we can come up with the best solution for everyone.
@@ -15,6 +16,8 @@ body:
options:
- label: 'I have searched and found no existing issue'
required: true
validations:
required: true
- type: checkboxes
attributes:
label: '💻'

View File

@@ -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 -->

View File

@@ -1,75 +0,0 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
import fs from 'fs';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const commentId = process.env.GITHUB_EVENT_COMMENT_ID;
if (!token || !repo || !issueNumber || !commentId) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
async function checkFirstComment() {
try {
console.log('Fetching comments with Octokit...');
// Get all comments with automatic pagination
const comments = await octokit.paginate(octokit.rest.issues.listComments, {
owner,
repo: repoName,
issue_number: issueNumber,
});
console.log(`Total comments found: ${comments.length}`);
// Filter for CodeRabbit summary comments (containing the specific marker)
const coderabbitSummaryComments = comments.filter(comment => {
const isCodeRabbit = comment.user.login === 'coderabbitai[bot]';
const hasSummaryMarker = comment.body.includes(
'<!-- This is an auto-generated comment: summarize by coderabbit.ai -->',
);
if (isCodeRabbit) {
console.log(
`CodeRabbit comment found (ID: ${comment.id}), has summary marker: ${hasSummaryMarker}`,
);
}
return isCodeRabbit && hasSummaryMarker;
});
const isFirstSummaryComment =
coderabbitSummaryComments.length === 1 &&
coderabbitSummaryComments[0].id == commentId;
console.log(
`CodeRabbit summary comments found: ${coderabbitSummaryComments.length}`,
);
console.log(`Current comment ID: ${commentId}`);
console.log(`Is first summary comment: ${isFirstSummaryComment}`);
setOutput('result', isFirstSummaryComment);
} catch (error) {
console.log('Error checking CodeRabbit comment:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'false');
process.exit(1);
}
}
checkFirstComment().catch(error => {
console.log('Unhandled error:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'false');
process.exit(1);
});

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
import fs from 'fs';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const prDetailsJson = process.env.PR_DETAILS;
if (!token || !repo || !issueNumber || !prDetailsJson) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
async function checkReleaseNotesExists() {
try {
const prDetails = JSON.parse(prDetailsJson);
if (!prDetails) {
console.log('No PR details available, skipping file check');
setOutput('result', 'false');
return;
}
const fileName = `upcoming-release-notes/${prDetails.number}.md`;
// Get PR info to get head SHA
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: issueNumber,
});
const prHeadSha = pr.head.sha;
console.log(
`Checking for file on PR branch: ${pr.head.ref} (${prHeadSha})`,
);
// Check if file exists
try {
await octokit.rest.repos.getContent({
owner,
repo: repoName,
path: fileName,
ref: prHeadSha,
});
console.log(
`Release notes file already exists on PR branch: ${fileName}`,
);
setOutput('result', 'true');
} catch (error) {
if (error.status === 404) {
console.log(
`No existing release notes file found on PR branch: ${fileName}`,
);
setOutput('result', 'false');
} else {
console.log('Error checking file existence:', error.message);
setOutput('result', 'false');
}
}
} catch (error) {
console.log('Error in file existence check:', error.message);
setOutput('result', 'false');
}
}
checkReleaseNotesExists();

View File

@@ -1,77 +0,0 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const summaryDataJson = process.env.SUMMARY_DATA;
const category = process.env.CATEGORY;
if (!token || !repo || !issueNumber || !summaryDataJson || !category) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
async function commentOnPR() {
try {
const summaryData = JSON.parse(summaryDataJson);
if (!summaryData) {
console.log('No summary data available, skipping comment');
return;
}
if (!category || category === 'null') {
console.log('No valid category available, skipping comment');
return;
}
// Clean category for display
const cleanCategory =
typeof category === 'string'
? category.replace(/^["']|["']$/g, '')
: category;
// Get PR info for the file URL
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: issueNumber,
});
const prBranch = pr.head.ref;
const headOwner = pr.head.repo.owner.login;
const headRepo = pr.head.repo.name;
const fileUrl = `https://github.com/${headOwner}/${headRepo}/blob/${prBranch}/upcoming-release-notes/${summaryData.prNumber}.md`;
const commentBody = [
'🤖 **Auto-generated Release Notes**',
'',
`Hey @${summaryData.author}! I've automatically created a release notes file based on CodeRabbit's analysis:`,
'',
`**Category:** ${cleanCategory}`,
`**Summary:** ${summaryData.summary}`,
`**File:** [upcoming-release-notes/${summaryData.prNumber}.md](${fileUrl})`,
'',
// 'The release notes file has been committed to the repository. You can edit it if needed before merging.',
"If you're happy with this release note, you can add it to your pull request. If not, you'll need to add your own before a maintainer can review your change.",
].join('\n');
await octokit.rest.issues.createComment({
owner,
repo: repoName,
issue_number: issueNumber,
body: commentBody,
});
console.log('✅ Successfully commented on PR');
} catch (error) {
console.log('Error commenting on PR:', error.message);
}
}
commentOnPR();

View File

@@ -1,96 +0,0 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
const summaryDataJson = process.env.SUMMARY_DATA;
const category = process.env.CATEGORY;
if (!token || !repo || !issueNumber || !summaryDataJson || !category) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
async function createReleaseNotesFile() {
try {
const summaryData = JSON.parse(summaryDataJson);
console.log('Debug - Category value:', category);
console.log('Debug - Category type:', typeof category);
console.log('Debug - Category JSON stringified:', JSON.stringify(category));
if (!summaryData) {
console.log('No summary data available, cannot create file');
return;
}
if (!category || category === 'null') {
console.log('No valid category available, cannot create file');
return;
}
// Create file content - ensure category is not quoted
const cleanCategory =
typeof category === 'string'
? category.replace(/^["']|["']$/g, '')
: category;
console.log('Debug - Clean category:', cleanCategory);
const fileContent = `---
category: ${cleanCategory}
authors: [${summaryData.author}]
---
${summaryData.summary}`;
const fileName = `upcoming-release-notes/${summaryData.prNumber}.md`;
console.log(`Creating release notes file: ${fileName}`);
console.log('File content:');
console.log(fileContent);
// Get PR info
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: issueNumber,
});
const prBranch = pr.head.ref;
const headOwner = pr.head.repo.owner.login;
const headRepo = pr.head.repo.name;
console.log(
`Committing to PR branch: ${headOwner}/${headRepo}:${prBranch}`,
);
// Create the file via GitHub API on the PR branch
await octokit.rest.repos.createOrUpdateFileContents({
owner: headOwner,
repo: headRepo,
path: fileName,
message: `Add release notes for PR #${summaryData.prNumber}`,
content: Buffer.from(`${fileContent}\n\n`).toString('base64'),
branch: prBranch,
committer: {
name: 'github-actions[bot]',
email: 'github-actions[bot]@users.noreply.github.com',
},
author: {
name: 'github-actions[bot]',
email: 'github-actions[bot]@users.noreply.github.com',
},
});
console.log(`✅ Successfully created release notes file: ${fileName}`);
} catch (error) {
console.log('Error creating release notes file:', error.message);
}
}
createReleaseNotesFile();

View File

@@ -1,118 +0,0 @@
#!/usr/bin/env node
const https = require('https');
const fs = require('fs');
const commentBody = process.env.GITHUB_EVENT_COMMENT_BODY;
const prDetailsJson = process.env.PR_DETAILS;
const summaryDataJson = process.env.SUMMARY_DATA;
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!commentBody || !prDetailsJson || !summaryDataJson || !openaiApiKey) {
console.log('Missing required environment variables');
process.exit(1);
}
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
try {
const prDetails = JSON.parse(prDetailsJson);
const summaryData = JSON.parse(summaryDataJson);
if (!summaryData || !prDetails) {
console.log('Missing data for categorization');
setOutput('result', 'null');
process.exit(0);
}
const data = JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content:
'You are categorizing pull requests for release notes. You must respond with exactly one of these categories: "Features", "Enhancements", "Bugfix", or "Maintenance". No other text or explanation.',
},
{
role: 'user',
content: `PR Title: ${prDetails.title}\n\nGenerated Summary: ${summaryData.summary}\n\nCodeRabbit Analysis:\n${commentBody}\n\nCategories:\n- Features: New functionality or capabilities\n- Bugfix: Fixes for broken or incorrect behavior\n- Enhancements: Improvements to existing functionality\n- Maintenance: Code cleanup, refactoring, dependencies, etc.\n\nWhat category does this PR belong to?`,
},
],
max_tokens: 10,
temperature: 0.1,
});
const options = {
hostname: 'api.openai.com',
path: '/v1/chat/completions',
method: 'POST',
headers: {
Authorization: `Bearer ${openaiApiKey}`,
'Content-Type': 'application/json',
},
};
const req = https.request(options, res => {
let responseData = '';
res.on('data', chunk => (responseData += chunk));
res.on('end', () => {
if (res.statusCode !== 200) {
console.log('OpenAI API error for categorization');
setOutput('result', 'null');
return;
}
try {
const response = JSON.parse(responseData);
console.log('OpenAI raw response:', JSON.stringify(response, null, 2));
const rawContent = response.choices[0].message.content.trim();
console.log('Raw content from OpenAI:', rawContent);
let category;
try {
category = JSON.parse(rawContent);
console.log('Parsed category:', category);
} catch (parseError) {
console.log(
'JSON parse error, using raw content:',
parseError.message,
);
category = rawContent;
}
// Validate the category response
const validCategories = [
'Features',
'Bugfix',
'Enhancements',
'Maintenance',
];
if (validCategories.includes(category)) {
console.log('OpenAI categorized as:', category);
setOutput('result', category);
} else {
console.log('Invalid category from OpenAI:', category);
console.log('Valid categories are:', validCategories);
setOutput('result', 'null');
}
} catch (error) {
console.log('Error parsing OpenAI response:', error.message);
setOutput('result', 'null');
}
});
});
req.on('error', error => {
console.log('Error in categorization:', error.message);
setOutput('result', 'null');
});
req.write(data);
req.end();
} catch (error) {
console.log('Error in categorization:', error.message);
setOutput('result', 'null');
}

View File

@@ -1,97 +0,0 @@
#!/usr/bin/env node
const https = require('https');
const fs = require('fs');
const commentBody = process.env.GITHUB_EVENT_COMMENT_BODY;
const prDetailsJson = process.env.PR_DETAILS;
const openaiApiKey = process.env.OPENAI_API_KEY;
if (!commentBody || !prDetailsJson || !openaiApiKey) {
console.log('Missing required environment variables');
process.exit(1);
}
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
try {
const prDetails = JSON.parse(prDetailsJson);
if (!prDetails) {
console.log('No PR details available, cannot generate summary');
setOutput('result', 'null');
process.exit(0);
}
console.log('CodeRabbit comment body:', commentBody);
const data = JSON.stringify({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content:
'You are a technical writer helping to create concise release notes. Generate a maximum 15-word summary that describes what this PR does. Focus on the user-facing changes or bug fixes. Do not include "This PR" or similar phrases - just describe the change directly. Start with a base form verb (e.g., "Add" not "Adds", "Fix" not "Fixes", "Introduce" not "Introduces").',
},
{
role: 'user',
content: `PR Title: ${prDetails.title}\n\nCodeRabbit Analysis:\n${commentBody}\n\nPlease provide a concise summary (max 15 words) of what this PR accomplishes.`,
},
],
max_tokens: 50,
temperature: 0.3,
});
const options = {
hostname: 'api.openai.com',
path: '/v1/chat/completions',
method: 'POST',
headers: {
Authorization: `Bearer ${openaiApiKey}`,
'Content-Type': 'application/json',
},
};
const req = https.request(options, res => {
let responseData = '';
res.on('data', chunk => (responseData += chunk));
res.on('end', () => {
if (res.statusCode !== 200) {
console.log(`OpenAI API error: ${res.statusCode} ${res.statusMessage}`);
setOutput('result', 'null');
return;
}
try {
const response = JSON.parse(responseData);
const summary = response.choices[0].message.content.trim();
console.log('Generated summary:', summary);
const result = {
summary: summary,
prNumber: prDetails.number,
author: prDetails.author,
};
setOutput('result', JSON.stringify(result));
} catch (error) {
console.log('Error parsing OpenAI response:', error.message);
setOutput('result', 'null');
}
});
});
req.on('error', error => {
console.log('Error generating summary:', error.message);
setOutput('result', 'null');
});
req.write(data);
req.end();
} catch (error) {
console.log('Error generating summary:', error.message);
setOutput('result', 'null');
}

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env node
import { Octokit } from '@octokit/rest';
import fs from 'fs';
const token = process.env.GITHUB_TOKEN;
const repo = process.env.GITHUB_REPOSITORY;
const issueNumber = process.env.GITHUB_EVENT_ISSUE_NUMBER;
if (!token || !repo || !issueNumber) {
console.log('Missing required environment variables');
process.exit(1);
}
const [owner, repoName] = repo.split('/');
const octokit = new Octokit({ auth: token });
function setOutput(name, value) {
fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}
async function getPRDetails() {
try {
console.log(
`Fetching PR details for ${owner}/${repoName}#${issueNumber}...`,
);
const { data: pr } = await octokit.rest.pulls.get({
owner,
repo: repoName,
pull_number: issueNumber,
});
console.log('PR details fetched successfully');
console.log('- PR Number:', pr.number);
console.log('- PR Author:', pr.user.login);
console.log('- PR Title:', pr.title);
const result = {
number: pr.number,
author: pr.user.login,
title: pr.title,
};
setOutput('result', JSON.stringify(result));
} catch (error) {
console.log('Error getting PR details:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'null');
process.exit(1);
}
}
getPRDetails().catch(error => {
console.log('Unhandled error:', error.message);
console.log('Stack:', error.stack);
setOutput('result', 'null');
process.exit(1);
});

View File

@@ -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

View File

@@ -1,94 +0,0 @@
#!/usr/bin/env node
// This script is used in GitHub Actions to get the next version based on the current package.json version.
// It supports three types of versioning: nightly, hotfix, and monthly.
const { parseArgs } = require('node:util');
const fs = require('node:fs');
const args = process.argv;
const options = {
'package-json': {
type: 'string',
short: 'p',
},
type: {
type: 'string', // nightly, hotfix, monthly
short: 't',
},
};
const { values } = parseArgs({
args,
options,
allowPositionals: true,
});
if (!values['package-json']) {
console.error(
'Please specify the path to package.json using --package-json or -p option.',
);
process.exit(1);
}
try {
const packageJsonPath = values['package-json'];
// Read and parse package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
// Parse year and month from version (e.g. 25.5.1 -> year=2025, month=5)
const versionParts = currentVersion.split('.');
const versionYear = parseInt(versionParts[0]);
const versionMonth = parseInt(versionParts[1]);
const versionHotfix = parseInt(versionParts[2]);
// Create date and add 1 month
const versionDate = new Date(2000 + versionYear, versionMonth - 1, 1); // month is 0-indexed
const nextVersionMonthDate = new Date(
versionDate.getFullYear(),
versionDate.getMonth() + 1,
1,
);
// Format back to YY.M format
const nextVersionYear = nextVersionMonthDate
.getFullYear()
.toString()
.slice(-2);
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
// Get current date string
const currentDate = new Date()
.toISOString()
.split('T')[0]
.replaceAll('-', '');
switch (values.type) {
case 'nightly': {
const newVersion = `${nextVersionYear}.${nextVersionMonth}.0-nightly.${currentDate}`;
process.stdout.write(newVersion); // return the new version to stdout
process.exit();
}
case 'hotfix': {
const bugfixVersion = `${versionYear}.${versionMonth}.${versionHotfix + 1}`;
process.stdout.write(bugfixVersion); // return the bugfix version to stdout
process.exit();
}
case 'monthly': {
const stableVersion = `${nextVersionYear}.${nextVersionMonth}.0`;
process.stdout.write(stableVersion); // return the stable version to stdout
process.exit();
}
default:
console.error(
'Invalid type specified. Use "nightly", "hotfix", or "monthly".',
);
process.exit(1);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}

View File

@@ -1,15 +1,4 @@
name: Setup
description: Setup the environment for the project
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
@@ -17,33 +6,14 @@ runs:
- name: Install node
uses: actions/setup-node@v4
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
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' }}

View File

@@ -1,359 +0,0 @@
import { Octokit } from '@octokit/rest';
import { minimatch } from 'minimatch';
/** Repository-specific configuration for points calculation */
const REPOSITORY_CONFIG = new Map([
[
'actual',
{
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 0,
PR_REVIEW_POINT_TIERS: [
{ minChanges: 500, points: 8 },
{ minChanges: 100, points: 6 },
{ minChanges: 10, points: 2 },
{ minChanges: 0, points: 1 },
],
EXCLUDED_FILES: [
'yarn.lock',
'.yarn/**/*',
'packages/component-library/src/icons/**/*',
'release-notes/**/*',
],
},
],
[
'docs',
{
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 4,
PR_REVIEW_POINT_TIERS: [
{ minChanges: 2000, points: 6 },
{ minChanges: 200, points: 4 },
{ minChanges: 0, points: 2 },
],
EXCLUDED_FILES: ['yarn.lock', '.yarn/**/*'],
},
],
]);
/**
* Get the start and end dates for the last month.
* @returns {Object} An object containing the start and end dates.
*/
function getLastMonthDates() {
// Get data relating to the last month
const now = new Date();
const firstDayOfLastMonth = new Date(
now.getFullYear(),
now.getMonth() - 1,
1,
);
const since = process.env.START_DATE
? new Date(process.env.START_DATE)
: firstDayOfLastMonth;
// Calculate the end of the month for the since date
const until = new Date(
since.getFullYear(),
since.getMonth() + 1,
0,
23,
59,
59,
999,
);
return { since, until };
}
/**
* Used for calculating the monthly points each core contributor has earned.
* These are used for payouts depending.
* @param {string} repo - The repository to analyze ('actual' or 'docs')
* @returns {number} The total points earned for the repository
*/
async function countContributorPoints(repo) {
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const owner = 'actualbudget';
const config = REPOSITORY_CONFIG.get(repo);
const { since, until } = getLastMonthDates();
// Get organization members
const { data: orgMembers } = await octokit.orgs.listMembers({
org: owner,
});
const orgMemberLogins = new Set(orgMembers.map(member => member.login));
// Initialize stats map with all org members
const stats = new Map(
Array.from(orgMemberLogins).map(login => [
login,
{
reviews: [], // Will store objects with PR number and points
labelRemovals: [],
issueClosings: [],
points: 0,
},
]),
);
// Helper function to print statistics
const printStats = (title, getValue, formatLine) => {
console.log(`\n${title}:`);
console.log('='.repeat(title.length + 1));
const entries = Array.from(stats.entries())
.map(([user, userStats]) => [user, getValue(userStats)])
.filter(([, count]) => count > 0)
.sort((a, b) => b[1] - a[1]);
if (entries.length === 0) {
console.log(`No ${title.toLowerCase()} found in the last month.`);
} else {
entries.forEach(([user, count]) => {
console.log(formatLine(user, count));
});
}
};
// Get all PRs using search
const searchQuery = `repo:${owner}/${repo} is:pr is:merged merged:${since.toISOString()}..${until.toISOString()}`;
const recentPRs = await octokit.paginate(
octokit.search.issuesAndPullRequests,
{
q: searchQuery,
per_page: 100,
advanced_search: true,
},
response => response.data,
);
// Get reviews and PR details for each PR
for (const pr of recentPRs) {
const { data: reviews } = await octokit.pulls.listReviews({
owner,
repo,
pull_number: pr.number,
});
// Get list of modified files
const { data: modifiedFiles } = await octokit.pulls.listFiles({
owner,
repo,
pull_number: pr.number,
});
// Calculate points based on PR size, excluding specified files
const totalChanges = modifiedFiles
.filter(
file =>
!config.EXCLUDED_FILES.some(pattern =>
minimatch(file.filename, pattern),
),
)
.reduce((sum, file) => sum + file.additions + file.deletions, 0);
// Check if this is a release PR
const isReleasePR = pr.title.match(/^🔖 \(\d+\.\d+\.\d+\)/);
// Calculate points for reviewers based on PR size
const prPoints =
config.PR_REVIEW_POINT_TIERS.find(tier => totalChanges > tier.minChanges)
?.points ?? 0;
// Award points to the PR creator if it's a release PR
if (isReleasePR && stats.has(pr.user.login)) {
const creatorStats = stats.get(pr.user.login);
creatorStats.reviews.push({
pr: pr.number.toString(),
points: config.POINTS_PER_RELEASE_PR,
isReleaseCreator: true,
});
creatorStats.points += config.POINTS_PER_RELEASE_PR;
} else {
// Add points to the reviewers
const uniqueReviewers = new Set();
reviews
.filter(
review =>
stats.has(review.user?.login) &&
review.state === 'APPROVED' &&
!uniqueReviewers.has(review.user?.login),
)
.forEach(({ user: { login: reviewer } }) => {
uniqueReviewers.add(reviewer);
const userStats = stats.get(reviewer);
userStats.reviews.push({
pr: pr.number.toString(),
points: prPoints,
});
userStats.points += prPoints;
});
}
}
// Get all issues with label events in the last month
const issues = await octokit.paginate(
octokit.issues.listForRepo,
{
owner,
repo,
state: 'all',
sort: 'updated',
direction: 'desc',
per_page: 100,
since: since.toISOString(),
},
(response, done) =>
response.data.filter(issue => new Date(issue.updated_at) <= until),
);
// Get label events for each issue
for (const issue of issues) {
const { data: events } = await octokit.issues.listEventsForTimeline({
owner,
repo,
issue_number: issue.number,
});
// Process events
events
.filter(
event =>
new Date(event.created_at) > since &&
new Date(event.created_at) <= until &&
stats.has(event.actor?.login),
)
.forEach(event => {
if (
event.event === 'unlabeled' &&
event.label &&
event.label.name.toLowerCase() === 'needs triage'
) {
const remover = event.actor.login;
const userStats = stats.get(remover);
if (userStats) {
userStats.labelRemovals.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
}
}
// Check if the issue was closed with "no planned" status
if (event.event === 'closed' && event.state_reason === 'not_planned') {
const closer = event.actor.login;
const userStats = stats.get(closer);
if (userStats) {
userStats.issueClosings.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_CLOSING_ACTION;
}
}
});
}
// Print all statistics
printStats(
`PR Review Statistics (${repo})`,
stats => stats.reviews.length,
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
.reviews.map(r => {
if (r.isReleaseCreator) {
return `#${r.pr} (${r.points}pts - Release Creator)`;
}
return `#${r.pr} (${r.points}pts)`;
})
.join(', ')})`,
);
printStats(
`"Needs Triage" Label Removal Statistics (${repo})`,
stats => stats.labelRemovals.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
);
printStats(
`Issue Closing Statistics (${repo})`,
stats => stats.issueClosings.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
);
// Print points summary
printStats(
`Points Summary (${repo})`,
stats => stats.points,
(user, userPoints) => `${user}: ${userPoints}`,
);
// Calculate and print total points
const totalPoints = Array.from(stats.values()).reduce(
(sum, userStats) => sum + userStats.points,
0,
);
console.log(`\nTotal points earned for ${repo}: ${totalPoints}`);
// Return the points
return new Map(
Array.from(stats.entries()).map(([login, userStats]) => [
login,
userStats.points,
]),
);
}
/**
* Calculate the points for both repositories and print cumulative results
*/
async function calculateCumulativePoints() {
// Get stats for each repository
const repoPointsResults = await Promise.all(
Array.from(REPOSITORY_CONFIG.keys()).map(countContributorPoints),
);
// Calculate cumulative stats
const cumulativeStats = new Map(repoPointsResults[0]);
// Combine stats from all repositories
for (let i = 1; i < repoPointsResults.length; i++) {
for (const [login, points] of repoPointsResults[i].entries()) {
if (!cumulativeStats.has(login)) {
cumulativeStats.set(login, 0);
}
cumulativeStats.set(login, cumulativeStats.get(login) + points);
}
}
// Print cumulative statistics
console.log('\n\nCUMULATIVE STATISTICS ACROSS ALL REPOSITORIES');
console.log('='.repeat(50));
console.log('\nCumulative Points Summary:');
console.log('='.repeat('Cumulative Points Summary'.length + 1));
const entries = Array.from(cumulativeStats.entries())
.filter(([, count]) => count > 0)
.sort((a, b) => b[1] - a[1]);
if (entries.length === 0) {
console.log('No cumulative points summary found.');
} else {
entries.forEach(([user, points]) => {
console.log(`${user}: ${points}`);
});
}
// Calculate and print total cumulative points
const totalCumulativePoints = Array.from(cumulativeStats.values()).reduce(
(sum, points) => sum + points,
0,
);
console.log('\nTotal cumulative points earned: ' + totalCumulativePoints);
}
// Run the calculations
calculateCumulativePoints().catch(console.error);

View File

@@ -1,89 +0,0 @@
name: Generate Release Notes from CodeRabbit summary
on:
issue_comment:
types: [created]
jobs:
generate-release-notes:
# Only run on PR comments from CodeRabbit bot
if: github.event.issue.pull_request && github.event.comment.user.login == 'coderabbitai[bot]'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Check if this is CodeRabbit's first comment
id: check-first-comment
run: node .github/actions/ai-generated-release-notes/check-first-comment.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
GITHUB_EVENT_COMMENT_ID: ${{ github.event.comment.id }}
- name: Get PR details
if: steps.check-first-comment.outputs.result == 'true'
id: pr-details
run: node .github/actions/ai-generated-release-notes/pr-details.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
- name: Check if release notes file already exists
if: steps.check-first-comment.outputs.result == 'true' && steps.pr-details.outputs.result != 'null'
id: check-release-notes-exists
run: node .github/actions/ai-generated-release-notes/check-release-notes-exists.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
- name: Generate summary with OpenAI
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false'
id: generate-summary
run: node .github/actions/ai-generated-release-notes/generate-summary.js
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
- name: Determine category with OpenAI
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null'
id: determine-category
run: node .github/actions/ai-generated-release-notes/determine-category.js
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_EVENT_COMMENT_BODY: ${{ github.event.comment.body }}
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
- name: Create and commit release notes file via GitHub API
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
run: node .github/actions/ai-generated-release-notes/create-release-notes-file.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
CATEGORY: ${{ steps.determine-category.outputs.result }}
- name: Comment on PR
if: steps.check-first-comment.outputs.result == 'true' && steps.check-release-notes-exists.outputs.result == 'false' && steps.generate-summary.outputs.result != 'null' && steps.determine-category.outputs.result != 'null' && steps.determine-category.outputs.result != ''
run: node .github/actions/ai-generated-release-notes/comment-on-pr.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
CATEGORY: ${{ steps.determine-category.outputs.result }}

View File

@@ -1,23 +0,0 @@
name: autofix.ci
defaults:
run:
shell: bash
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
permissions:
contents: read
jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Format code
run: yarn lint:fix
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27

View File

@@ -57,7 +57,7 @@ jobs:
- 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
with:
@@ -68,17 +68,3 @@ jobs:
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

View File

@@ -27,16 +27,6 @@ jobs:
uses: ./.github/actions/setup
- name: Typecheck
run: yarn typecheck
validate-cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Build Web
run: yarn build:server
- name: Check that the built CLI works
run: node packages/sync-server/build/bin/actual-server.js --version
test:
runs-on: ubuntu-latest
steps:
@@ -53,6 +43,6 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: '19'
- name: Check migrations
run: node ./.github/actions/check-migrations.js

View File

@@ -1,26 +0,0 @@
name: Count points
on:
schedule:
# Run at 00:00 on the first day of every month
- cron: '0 0 1 * *'
workflow_dispatch:
inputs:
startDate:
description: 'Start date for point counter (YYYY-MM-DD)'
required: true
type: string
jobs:
count-points:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Count points
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
START_DATE: ${{ inputs.startDate }}
run: node .github/scripts/count-points.mjs

View File

@@ -1,109 +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 image for testing
uses: docker/build-push-action@v5
with:
context: .
push: false
load: true
file: packages/sync-server/docker/${{ matrix.os }}.Dockerfile
tags: actualbudget/actual-server-testing
- name: Test that the docker image boots
run: |
docker run --detach --network=host actualbudget/actual-server-testing
sleep 5
curl --fail -sS -LI -w '%{http_code}\n' --retry 10 --retry-delay 1 --retry-connrefused localhost:5006
# This will use the cache from the earlier build step and not rebuild the image
# https://docs.docker.com/build/ci/github-actions/test-before-push/
- name: Build and push images
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 }}

View File

@@ -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 }}

View File

@@ -1,7 +1,6 @@
name: E2E Tests
on:
pull_request:
on: [pull_request]
env:
GITHUB_PR_NUMBER: ${{github.event.pull_request.number}}
@@ -32,7 +31,7 @@ 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
- name: Set up environment
@@ -48,33 +47,12 @@ jobs:
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
- name: Set up environment

View File

@@ -77,68 +77,12 @@ jobs:
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
- name: Add to 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

View File

@@ -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 }}'

View File

@@ -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

View File

@@ -24,7 +24,7 @@ jobs:
body: |
:sparkles: Thanks for sharing your idea! :sparkles:
This repository uses a voting-based system for feature requests. While enhancement issues are automatically closed, we still welcome feature requests! The voting system helps us gauge community interest in potential features. We also encourage community contributions for any feature requests marked as needing votes (just post a comment first so we can help guide you toward a successful contribution).
This repository uses lodash style issue management for enhancements. That means enhancement issues are automatically closed. This doesnt mean we dont accept feature requests, though! We will consider implementing ones that receive many upvotes, and we welcome contributions for any feature requests marked as needing votes (just post a comment first so we can help you make a successful contribution).
The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+

View File

@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: '19'
- name: Handle feature requests
run: node .github/actions/handle-feature-requests.js
env:

View File

@@ -22,15 +22,15 @@ jobs:
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
run: ./bin/package-browser
- name: Deploy to Netlify
id: netlify_deploy
@@ -40,4 +40,4 @@ jobs:
--site ${{ secrets.NETLIFY_SITE_ID }} \
--auth ${{ secrets.NETLIFY_API_TOKEN }} \
--filter @actual-app/web \
--prod
--prod

View File

@@ -1,95 +0,0 @@
name: Publish nightly npm packages
# Nightly npm packages are built daily
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
build-and-pack:
runs-on: ubuntu-latest
name: Build and pack npm packages
if: github.event.repository.fork == false
steps:
- uses: actions/checkout@v4
- name: Set up environment
uses: ./.github/actions/setup
- name: Update package versions
run: |
# Get new nightly versions
NEW_WEB_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(node ./.github/actions/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
# Set package versions
npm version $NEW_WEB_VERSION --no-git-tag-version --workspace=@actual-app/web --no-workspaces-update
npm version $NEW_SYNC_VERSION --no-git-tag-version --workspace=@actual-app/sync-server --no-workspaces-update
npm version $NEW_API_VERSION --no-git-tag-version --workspace=@actual-app/api --no-workspaces-update
- name: Yarn install
run: |
yarn install
- name: Build Server & 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 Nightly 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 --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Sync-Server
run: |
npm publish sync-server/@actual-app/sync-server.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish API
run: |
npm publish api/@actual-app/api.tgz --access public --tag nightly
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -2,7 +2,6 @@ name: 'Close stale PRs'
on:
schedule:
- cron: '30 1 * * *'
workflow_dispatch: # Allow manual triggering
jobs:
stale:
@@ -25,18 +24,3 @@ jobs:
any-of-labels: ':construction: WIP'
days-before-close: -1
days-before-issue-stale: -1
stale-needs-info:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-label: 'needs info'
days-before-stale: -1
days-before-close: 7
close-issue-message: 'This issue has been automatically closed because there have been no comments for 7 days after the "needs info" label was added. If you still need help, please feel free to reopen the issue with the requested information.'
remove-stale-when-updated: false
stale-pr-message: '' # Disable PR processing
close-pr-message: '' # Disable PR processing
days-before-pr-stale: -1 # Disable PR processing
days-before-pr-close: -1 # Disable PR processing

View File

@@ -1,14 +1,14 @@
name: /update-vrt
on:
issue_comment:
types: [created]
types: [ created ]
permissions:
pull-requests: read
contents: read
pull-requests: write
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.issue.number }}-${{ contains(github.event.comment.body, '/update-vrt') }}
group: ${{ github.workflow }}-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
@@ -19,10 +19,10 @@ jobs:
github.event.issue.pull_request &&
contains(github.event.comment.body, '/update-vrt')
container:
image: mcr.microsoft.com/playwright:v1.52.0-jammy
image: mcr.microsoft.com/playwright:v1.41.1-jammy
steps:
- name: Get PR branch
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
# 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
@@ -31,10 +31,6 @@ jobs:
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:
@@ -45,75 +41,15 @@ jobs:
run: yarn vrt --update-snapshots
env:
E2E_START_URL: ${{ steps.netlify.outputs.url }}
- name: Create patch
- name: Commit and push changes
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'
git push origin HEAD:${{ steps.comment-branch.outputs.head_ref }}

10
.gitignore vendored
View File

@@ -3,7 +3,6 @@
!data/.gitkeep
/data2
Actual-*
!actual-server.js
**/xcuserdata/*
export-2020-01-10.csv
@@ -54,12 +53,3 @@ bundle.mobile.js.map
# build output
package.tgz
# Fly.io configuration
fly.toml
# TypeScript cache
build/
# .d.ts files aren't type-checked with skipLibCheck set to true
*.d.ts

2
.nvmrc
View File

@@ -1 +1 @@
v20/*
v18.16.0

View File

@@ -1,27 +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/loot-core/**/node_modules/*
packages/loot-core/**/lib-dist/*
packages/loot-core/**/proto/*
.yarn/*
upcoming-release-notes/*

View File

@@ -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");

894
.yarn/releases/yarn-4.3.1.cjs vendored Executable file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -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.3.1.cjs

View File

@@ -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"]

View File

@@ -66,11 +66,7 @@ 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>
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).
## Repo Activity

View File

@@ -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

View File

@@ -6,8 +6,8 @@ RELEASE=""
CI=${CI:-false}
cd "$ROOT/.."
POSITIONAL=()
SKIP_EXE_BUILD=false
while [[ $# -gt 0 ]]; do
key="$1"
@@ -16,36 +16,27 @@ 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 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 --mode=desktop
yarn workspace desktop-electron update-client
@@ -53,20 +44,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
echo "Created release"
else
SKIP_NOTARIZATION=true yarn build
fi
echo "\nCreated release"
else
SKIP_NOTARIZATION=true yarn build
fi
)

View File

@@ -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 actual/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 initialPrNumber = 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: initialPrNumber,
},
{
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 ||
!result.pullRequestNumber
) {
console.log('All questions must be answered. Exiting');
exit(1);
}
const fileContents = getFileContents(
result.releaseNoteType,
result.githubUsername,
result.oneLineSummary,
);
const prNumber = result.pullRequestNumber;
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: ${filepath}`);
}
});
}
// 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();

View File

@@ -28,5 +28,5 @@ 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 \
MSYS_NO_PATHCONV=1 docker run --rm --network host -v "$(pwd)":/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.41.1-jammy /bin/bash \
-c "E2E_START_URL=$E2E_START_URL yarn vrt $VRT_ARGS"

View File

@@ -15,3 +15,4 @@ services:
volumes:
- '.:/app'
restart: 'no'

View File

@@ -1,801 +0,0 @@
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 pluginTypescript from 'typescript-eslint';
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
import pluginActual from './packages/eslint-plugin-actual/lib/index.js';
import tsParser from '@typescript-eslint/parser';
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/loot-core/**/node_modules/*',
'packages/loot-core/**/lib-dist/*',
'packages/loot-core/**/proto/*',
'packages/sync-server/build/',
'.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: {
actual: pluginActual,
'react-hooks': pluginReactHooks,
'jsx-a11y': pluginJSXA11y,
'typescript-paths': pluginTypescriptPaths,
},
rules: {
'actual/no-untranslated-strings': 'error',
'actual/prefer-trans-over-t': 'error',
},
},
{
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)',
},
],
'actual/typography': 'warn',
'actual/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',
{
paths: [
{
name: 'react-router',
importNames: ['useNavigate'],
message:
"Please import Actual's useNavigate() hook from `src/hooks` instead.",
},
{
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.",
},
],
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',
},
{
group: ['@actual-app/web/*'],
message: 'Please do not import `@actual-app/web` in `loot-core`',
},
],
},
],
'@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/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/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: {
'actual/typography': 'off',
'actual/no-untranslated-strings': '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',
'actual/typography': 'off',
},
},
{
files: ['packages/sync-server/src/app-gocardless/banks/*.js'],
rules: {
'import/no-anonymous-default-export': 'off',
'import/no-default-export': 'off',
},
},
);

View File

@@ -19,83 +19,64 @@
},
"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",
"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",
"prepare": "husky"
},
"devDependencies": {
"@octokit/rest": "^22.0.0",
"@types/node": "^22.15.18",
"@types/prompts": "^2.4.9",
"@typescript-eslint/parser": "^8.32.1",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"confusing-browser-globals": "^1.0.11",
"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-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",
"minimatch": "^10.0.3",
"node-jq": "^6.0.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-prettier": "5.2.1",
"eslint-plugin-react": "7.35.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-rulesdir": "^0.2.2",
"husky": "^9.0.11",
"lint-staged": "^15.2.9",
"node-jq": "^4.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"prompts": "^2.4.2",
"prettier": "3.3.3",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"typescript": "^5.5.4",
"typescript-strict-plugin": "^2.4.4"
},
"resolutions": {
"rollup": "4.40.1",
"socks": ">=2.8.3"
"rollup": "4.9.4"
},
"engines": {
"node": ">=20",
"yarn": "^4.9.1"
"node": ">=18.0.0"
},
"lint-staged": {
"*.{js,mjs,jsx,ts,tsx,md,json,yml}": [
"eslint --fix",
"prettier --write"
]
"*.{js,jsx,ts,tsx,md,json}": "prettier --write"
},
"packageManager": "yarn@4.9.1",
"packageManager": "yarn@4.3.1",
"browserslist": [
"electron 24.0",
"defaults"

View File

@@ -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",

View File

@@ -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';

View 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',
},
};

View File

@@ -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 });
@@ -568,20 +569,8 @@ describe('API CRUD operations', () => {
const accountId = await api.createAccount({ name: 'test-account' }, 0);
let newTransaction = [
{
account: accountId,
date: '2023-11-03',
imported_id: '11',
amount: 100,
notes: 'notes',
},
{
account: accountId,
date: '2023-11-03',
imported_id: '12',
amount: 100,
notes: '',
},
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: 'notes' },
{ date: '2023-11-03', imported_id: '12', amount: 100, notes: '' },
];
const addResult = await api.addTransactions(accountId, newTransaction, {
@@ -609,27 +598,9 @@ describe('API CRUD operations', () => {
expect(transactions).toHaveLength(2);
newTransaction = [
{
account: accountId,
date: '2023-12-03',
imported_id: '11',
amount: 100,
notes: 'notes',
},
{
account: accountId,
date: '2023-12-03',
imported_id: '12',
amount: 100,
notes: 'notes',
},
{
account: accountId,
date: '2023-12-03',
imported_id: '22',
amount: 200,
notes: '',
},
{ 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: '' },
];
const reconciled = await api.importTransactions(accountId, newTransaction);
@@ -683,60 +654,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();
});
});

View File

@@ -1,6 +1,5 @@
// @ts-strict-ignore
import type { Handlers } from 'loot-core/types/handlers';
import type { ImportTransactionEntity } from 'loot-core/types/models/import-transaction';
import type { Handlers } from 'loot-core/src/types/handlers';
import * as injected from './injected';
@@ -53,18 +52,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');
}
@@ -94,22 +85,8 @@ export function addTransactions(
});
}
export interface ImportTransactionsOpts {
defaultCleared?: boolean;
}
export function importTransactions(
accountId: string,
transactions: ImportTransactionEntity[],
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) {

View File

@@ -1,10 +1,10 @@
{
"name": "@actual-app/api",
"version": "25.8.0",
"version": "24.11.0",
"license": "MIT",
"description": "An API for Actual",
"engines": {
"node": ">=20"
"node": ">=18.12.0"
},
"main": "dist/index.js",
"types": "@types/index.d.ts",
@@ -14,24 +14,27 @@
],
"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.6.0",
"compare-versions": "^6.1.0",
"node-fetch": "^3.3.2",
"uuid": "^11.1.0"
"uuid": "^9.0.1"
},
"devDependencies": {
"tsc-alias": "^1.8.16",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
"@swc/core": "^1.5.3",
"@swc/jest": "^0.2.36",
"@types/jest": "^27.5.2",
"@types/uuid": "^9.0.2",
"jest": "^27.5.1",
"tsc-alias": "^1.8.8",
"typescript": "^5.5.4"
}
}

View File

@@ -5,15 +5,15 @@
// 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/src/*"]
"loot-core/src/*": ["./loot-core/*"],
"loot-core/*": ["./@types/loot-core/*"],
}
},
"include": ["."],
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
"exclude": ["**/node_modules/*", "dist", "@types"]
}

View File

@@ -1,9 +0,0 @@
export default {
test: {
globals: true,
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
// print only console.error
return type === 'stderr';
},
},
};

View File

@@ -1,59 +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.2.4"
},
"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",
"./color-picker": "./src/ColorPicker.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"
}
}

View File

@@ -1,136 +0,0 @@
import { ChangeEvent, ReactNode } from 'react';
import {
ColorPicker as AriaColorPicker,
ColorPickerProps as AriaColorPickerProps,
Dialog,
DialogTrigger,
ColorSwatch as AriaColorSwatch,
ColorSwatchProps,
ColorSwatchPicker as AriaColorSwatchPicker,
ColorSwatchPickerItem,
ColorField,
parseColor,
} from 'react-aria-components';
import { css } from '@emotion/css';
import { Input } from './Input';
import { Popover } from './Popover';
function ColorSwatch(props: ColorSwatchProps) {
return (
<AriaColorSwatch
{...props}
style={({ color }) => ({
background: color.toString('hex'),
width: '32px',
height: '32px',
borderRadius: '4px',
boxShadow: 'inset 0 0 0 1px rgba(0, 0, 0, 0.1)',
})}
/>
);
}
// colors from https://materialui.co/colors
const colorsets = [
['#D32F2F', '#C2185B', '#7B1FA2', '#512DA8', '#303F9F'],
['#1976D2', '#0288D1', '#0097A7', '#00796B', '#388E3C'],
['#689F38', '#AFB42B', '#FBC02D', '#FFA000', '#F57C00'],
['#E64A19', '#5D4037', '#616161', '#455A64', '#690CB0'],
];
function ColorSwatchPicker() {
return (
<>
{colorsets.map((colors, idx) => (
<AriaColorSwatchPicker
key={`colorset-${idx}`}
style={{
display: 'flex',
gap: '8px',
flexWrap: 'wrap',
}}
>
{colors.map(color => (
<ColorSwatchPickerItem
key={color}
color={color}
className={css({
position: 'relative',
outline: 'none',
borderRadius: '4px',
width: 'fit-content',
forcedColorAdjust: 'none',
cursor: 'pointer',
'&[data-selected]::after': {
// eslint-disable-next-line actual/typography
content: '""',
position: 'absolute',
inset: 0,
border: '2px solid black',
outline: '2px solid white',
outlineOffset: '-4px',
borderRadius: 'inherit',
},
})}
>
<ColorSwatch />
</ColorSwatchPickerItem>
))}
</AriaColorSwatchPicker>
))}
</>
);
}
const isColor = (value: string) => /^#[0-9a-fA-F]{6}$/.test(value);
interface ColorPickerProps extends AriaColorPickerProps {
children?: ReactNode;
}
export function ColorPicker({ children, ...props }: ColorPickerProps) {
const onInput = (value: string) => {
if (!isColor(value)) {
return;
}
const color = parseColor(value);
if (color) {
props.onChange?.(color);
}
};
return (
<AriaColorPicker {...props}>
<DialogTrigger>
{children}
<Popover>
<Dialog
style={{
outline: 'none',
padding: '15px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
minWidth: '192px',
maxHeight: 'inherit',
boxSizing: 'border-box',
overflow: 'auto',
}}
>
<ColorSwatchPicker />
<ColorField
onInput={({ target: { value } }: ChangeEvent<HTMLInputElement>) =>
onInput(value)
}
>
<Input placeholder="#RRGGBB" style={{ width: '100px' }} />
</ColorField>
</Dialog>
</Popover>
</DialogTrigger>
</AriaColorPicker>
);
}

View File

@@ -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.',
);
}

View File

@@ -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);
});
});

View File

@@ -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} />;
}

View File

@@ -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,
};
}

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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 actual/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',
},
};

View File

@@ -1,204 +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)',
noteTagDefault: 'var(--color-noteTagDefault)',
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)',
};

View File

@@ -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()],
});

View File

@@ -18,9 +18,4 @@ protoc --plugin="protoc-gen-ts=../../node_modules/.bin/protoc-gen-ts" \
../../node_modules/.bin/prettier --write src/proto/*.d.ts
for file in src/proto/*.d.ts; do
{ echo "/* eslint-disable @typescript-eslint/no-namespace */"; sed 's/export class/export declare class/g' "$file"; } > "${file%.d.ts}.ts"
rm "$file"
done
echo 'One more step! Find the `var global = ...` declaration in src/proto/sync_pb.js and change it to `var global = globalThis;`'

View File

@@ -1 +1 @@
export * from './src';
export * from './src/main';

View File

@@ -0,0 +1,6 @@
module.exports = {
testEnvironment: 'node',
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest',
},
};

View File

@@ -11,19 +11,21 @@
"scripts": {
"build:node": "tsc --p tsconfig.dist.json",
"proto:generate": "./bin/generate-proto",
"build": "rm -rf dist && yarn run build:node",
"test": "vitest --globals"
"build": "rm -rf dist && yarn run build:node && cp src/proto/sync_pb.d.ts dist/src/proto/",
"test": "jest -c jest.config.js"
},
"dependencies": {
"google-protobuf": "^3.21.4",
"google-protobuf": "^3.12.0-rc.1",
"murmurhash": "^2.0.1",
"uuid": "^11.1.0"
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/google-protobuf": "^3.15.12",
"protoc-gen-js": "^3.21.4-4",
"@swc/core": "^1.5.3",
"@swc/jest": "^0.2.36",
"@types/jest": "^27.5.2",
"@types/uuid": "^9.0.2",
"jest": "^27.5.1",
"ts-protoc-gen": "^0.15.0",
"typescript": "^5.8.3",
"vitest": "^3.2.4"
"typescript": "^5.5.4"
}
}

View File

@@ -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,

View File

@@ -1,10 +1,9 @@
/* eslint-disable @typescript-eslint/no-namespace */
// package:
// file: sync.proto
import * as jspb from 'google-protobuf';
export declare class EncryptedData extends jspb.Message {
export class EncryptedData extends jspb.Message {
getIv(): Uint8Array | string;
getIv_asU8(): Uint8Array;
getIv_asB64(): string;
@@ -49,7 +48,7 @@ export namespace EncryptedData {
};
}
export declare class Message extends jspb.Message {
export class Message extends jspb.Message {
getDataset(): string;
setDataset(value: string): void;
@@ -89,7 +88,7 @@ export namespace Message {
};
}
export declare class MessageEnvelope extends jspb.Message {
export class MessageEnvelope extends jspb.Message {
getTimestamp(): string;
setTimestamp(value: string): void;
@@ -130,7 +129,7 @@ export namespace MessageEnvelope {
};
}
export declare class SyncRequest extends jspb.Message {
export class SyncRequest extends jspb.Message {
clearMessagesList(): void;
getMessagesList(): Array<MessageEnvelope>;
setMessagesList(value: Array<MessageEnvelope>): void;
@@ -179,7 +178,7 @@ export namespace SyncRequest {
};
}
export declare class SyncResponse extends jspb.Message {
export class SyncResponse extends jspb.Message {
clearMessagesList(): void;
getMessagesList(): Array<MessageEnvelope>;
setMessagesList(value: Array<MessageEnvelope>): void;

View File

@@ -157,9 +157,9 @@ proto.EncryptedData.prototype.toObject = function(opt_includeInstance) {
*/
proto.EncryptedData.toObject = function(includeInstance, msg) {
var f, obj = {
iv: msg.getIv_asB64(),
authtag: msg.getAuthtag_asB64(),
data: msg.getData_asB64()
iv: msg.getIv_asB64(),
authtag: msg.getAuthtag_asB64(),
data: msg.getData_asB64()
};
if (includeInstance) {
@@ -419,10 +419,10 @@ proto.Message.prototype.toObject = function(opt_includeInstance) {
*/
proto.Message.toObject = function(includeInstance, msg) {
var f, obj = {
dataset: jspb.Message.getFieldWithDefault(msg, 1, ""),
row: jspb.Message.getFieldWithDefault(msg, 2, ""),
column: jspb.Message.getFieldWithDefault(msg, 3, ""),
value: jspb.Message.getFieldWithDefault(msg, 4, "")
dataset: jspb.Message.getFieldWithDefault(msg, 1, ""),
row: jspb.Message.getFieldWithDefault(msg, 2, ""),
column: jspb.Message.getFieldWithDefault(msg, 3, ""),
value: jspb.Message.getFieldWithDefault(msg, 4, "")
};
if (includeInstance) {
@@ -639,9 +639,9 @@ proto.MessageEnvelope.prototype.toObject = function(opt_includeInstance) {
*/
proto.MessageEnvelope.toObject = function(includeInstance, msg) {
var f, obj = {
timestamp: jspb.Message.getFieldWithDefault(msg, 1, ""),
isencrypted: jspb.Message.getBooleanFieldWithDefault(msg, 2, false),
content: msg.getContent_asB64()
timestamp: jspb.Message.getFieldWithDefault(msg, 1, ""),
isencrypted: jspb.Message.getBooleanFieldWithDefault(msg, 2, false),
content: msg.getContent_asB64()
};
if (includeInstance) {
@@ -860,12 +860,12 @@ proto.SyncRequest.prototype.toObject = function(opt_includeInstance) {
*/
proto.SyncRequest.toObject = function(includeInstance, msg) {
var f, obj = {
messagesList: jspb.Message.toObjectList(msg.getMessagesList(),
messagesList: jspb.Message.toObjectList(msg.getMessagesList(),
proto.MessageEnvelope.toObject, includeInstance),
fileid: jspb.Message.getFieldWithDefault(msg, 2, ""),
groupid: jspb.Message.getFieldWithDefault(msg, 3, ""),
keyid: jspb.Message.getFieldWithDefault(msg, 5, ""),
since: jspb.Message.getFieldWithDefault(msg, 6, "")
fileid: jspb.Message.getFieldWithDefault(msg, 2, ""),
groupid: jspb.Message.getFieldWithDefault(msg, 3, ""),
keyid: jspb.Message.getFieldWithDefault(msg, 5, ""),
since: jspb.Message.getFieldWithDefault(msg, 6, "")
};
if (includeInstance) {
@@ -1140,9 +1140,9 @@ proto.SyncResponse.prototype.toObject = function(opt_includeInstance) {
*/
proto.SyncResponse.toObject = function(includeInstance, msg) {
var f, obj = {
messagesList: jspb.Message.toObjectList(msg.getMessagesList(),
messagesList: jspb.Message.toObjectList(msg.getMessagesList(),
proto.MessageEnvelope.toObject, includeInstance),
merkle: jspb.Message.getFieldWithDefault(msg, 2, "")
merkle: jspb.Message.getFieldWithDefault(msg, 2, "")
};
if (includeInstance) {

View File

@@ -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,

View File

@@ -10,7 +10,6 @@ playwright-report
# production
build
build-electron
build-stats
stats.json
@@ -25,6 +24,3 @@ public/kcab
public/data
public/data-file-index.txt
public/*.wasm
# translations
locale/

View File

@@ -39,7 +39,6 @@ HTTPS=true yarn start
```
or using the dev container:
```
HTTPS=true docker compose up --build
```
@@ -65,10 +64,10 @@ Run manually:
```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
@@ -83,7 +82,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
@@ -92,7 +90,6 @@ E2E_START_URL=https://my-remote-server.com yarn vrt:docker
```
Run locally:
```sh
E2E_START_URL=https://my-remote-server.com yarn vrt
```

View File

@@ -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();

View File

@@ -1,13 +1,12 @@
import { type Page } from '@playwright/test';
import { test, expect } 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;
let page;
let navigation;
let configurationPage;
test.beforeEach(async ({ browser }) => {
page = await browser.newPage();
@@ -28,7 +27,6 @@ test.describe('Mobile Accounts', () => {
test('opens the accounts page and asserts on balances', async () => {
const accountsPage = await navigation.goToAccountsPage();
await accountsPage.waitFor();
const account = await accountsPage.getNthAccount(1);
@@ -39,10 +37,7 @@ test.describe('Mobile Accounts', () => {
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();
@@ -55,9 +50,6 @@ test.describe('Mobile Accounts', () => {
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();

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