Compare commits

..

5 Commits

Author SHA1 Message Date
lelemm
d0c00252e3 code rabbit suggestion 2026-02-09 10:46:37 -03:00
lelemm
f53d3c662d Update .github/workflows/ai-generated-release-notes.yml
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-09 10:44:09 -03:00
lelemm
a8a3355354 lint 2026-02-09 10:41:56 -03:00
github-actions[bot]
9d51ef7ce8 Add release notes for PR #6917 2026-02-09 13:40:37 +00:00
lelemm
e6d63f620e Auto note 2026-02-09 10:34:35 -03:00
672 changed files with 2636 additions and 2742 deletions

View File

@@ -28,7 +28,7 @@ try {
}
const data = JSON.stringify({
model: 'gpt-4o-mini',
model: 'gpt-4.1-mini',
messages: [
{
role: 'system',
@@ -71,30 +71,26 @@ try {
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;
}
//CHANGED HOW IT READS THE CATEGORY TO AVOID ERRORS WHEN LLM DOESNT ANSWER A JSON
// Validate the category response
const validCategories = [
'Features',
'Bugfixes',
'Enhancements',
'Maintenance',
];
if (validCategories.includes(category)) {
const lowerContent = rawContent.toLowerCase();
const category = validCategories.find(cat =>
lowerContent.includes(cat.toLowerCase()),
);
if (category) {
console.log('OpenAI categorized as:', category);
setOutput('result', category);
} else {
console.log('Invalid category from OpenAI:', category);
console.log(
'No valid category found in OpenAI response:',
rawContent,
);
console.log('Valid categories are:', validCategories);
setOutput('result', 'null');
}

View File

@@ -28,7 +28,7 @@ try {
console.log('CodeRabbit comment body:', commentBody);
const data = JSON.stringify({
model: 'gpt-4o-mini',
model: 'gpt-4.1-mini',
messages: [
{
role: 'system',

View File

@@ -8,13 +8,6 @@ const CONFIG = {
POINTS_PER_ISSUE_TRIAGE_ACTION: 1,
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 4, // Awarded to whoever merges the release PR
PR_CONTRIBUTION_POINTS: {
Features: 2,
Enhancements: 2,
Bugfix: 3,
Maintenance: 2,
Unknown: 2,
},
// Point tiers for code changes (non-docs)
CODE_PR_REVIEW_POINT_TIERS: [
{ minChanges: 500, points: 8 },
@@ -38,116 +31,6 @@ const CONFIG = {
DOCS_FILES_PATTERN: 'packages/docs/**/*',
};
/**
* Parse category from release notes file content.
* @param {string} content - The content of the release notes file.
* @returns {string|null} The category or null if not found.
*/
function parseReleaseNotesCategory(content) {
if (!content) return null;
// Extract YAML front matter
const frontMatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!frontMatterMatch) return null;
// Extract category from front matter
const categoryMatch = frontMatterMatch[1].match(/^category:\s*(.+)$/m);
if (!categoryMatch) return null;
return categoryMatch[1].trim();
}
/**
* Get the last commit SHA on or before a given date.
* @param {Octokit} octokit - The Octokit instance.
* @param {string} owner - Repository owner.
* @param {string} repo - Repository name.
* @param {Date} beforeDate - The date to find the last commit before.
* @returns {Promise<string|null>} The commit SHA or null if not found.
*/
async function getLastCommitBeforeDate(octokit, owner, repo, beforeDate) {
try {
// Get the default branch from the repository
const { data: repoData } = await octokit.repos.get({ owner, repo });
const defaultBranch = repoData.default_branch;
const { data: commits } = await octokit.repos.listCommits({
owner,
repo,
sha: defaultBranch,
until: beforeDate.toISOString(),
per_page: 1,
});
if (commits.length > 0) {
return commits[0].sha;
}
} catch {
// If error occurs, return null to fall back to default branch
}
return null;
}
/**
* Get the category and points for a PR by reading its release notes file.
* @param {Octokit} octokit - The Octokit instance.
* @param {string} owner - Repository owner.
* @param {string} repo - Repository name.
* @param {number} prNumber - PR number.
* @param {Date} monthEnd - The end date of the month to use as base revision.
* @returns {Object} Object with category and points, or null if error.
*/
async function getPRCategoryAndPoints(
octokit,
owner,
repo,
prNumber,
monthEnd,
) {
const releaseNotesPath = `upcoming-release-notes/${prNumber}.md`;
try {
// Get the last commit of the month to use as base revision
const commitSha = await getLastCommitBeforeDate(
octokit,
owner,
repo,
monthEnd,
);
// Try to read the release notes file from the last commit of the month
const { data: fileContent } = await octokit.repos.getContent({
owner,
repo,
path: releaseNotesPath,
ref: commitSha || undefined, // Use commit SHA if available, otherwise default branch
});
if (fileContent.content) {
// Decode base64 content
const content = Buffer.from(fileContent.content, 'base64').toString(
'utf-8',
);
const category = parseReleaseNotesCategory(content);
if (category && CONFIG.PR_CONTRIBUTION_POINTS[category]) {
return {
category,
points: CONFIG.PR_CONTRIBUTION_POINTS[category],
};
}
}
} catch {
// Do nothing
}
return {
category: 'Unknown',
points: CONFIG.PR_CONTRIBUTION_POINTS.Unknown,
};
}
/**
* Get the start and end dates for the last month.
* @returns {Object} An object containing the start and end dates.
@@ -206,7 +89,6 @@ async function countContributorPoints() {
{
codeReviews: [], // Will store objects with PR number and points for main repo changes
docsReviews: [], // Will store objects with PR number and points for docs changes
prContributions: [], // Will store objects with PR number, category, and points for PR author contributions
labelRemovals: [],
issueClosings: [],
points: 0,
@@ -320,28 +202,6 @@ async function countContributorPoints() {
mergerStats.points += CONFIG.POINTS_PER_RELEASE_PR;
}
} else {
// Award points to PR author if they are a core maintainer
const prAuthor = pr.user?.login;
if (prAuthor && orgMemberLogins.has(prAuthor)) {
const categoryAndPoints = await getPRCategoryAndPoints(
octokit,
owner,
repo,
pr.number,
until,
);
if (categoryAndPoints) {
const authorStats = stats.get(prAuthor);
authorStats.prContributions.push({
pr: pr.number.toString(),
category: categoryAndPoints.category,
points: categoryAndPoints.points,
});
authorStats.points += categoryAndPoints.points;
}
}
const uniqueReviewers = new Set();
reviews.data.forEach(review => {
if (
@@ -433,7 +293,7 @@ async function countContributorPoints() {
// Print all statistics
printStats(
'Code Review Statistics',
stats => stats.codeReviews.reduce((sum, r) => sum + r.points, 0),
stats => stats.codeReviews.length,
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
@@ -448,7 +308,7 @@ async function countContributorPoints() {
printStats(
'Docs Review Statistics',
stats => stats.docsReviews.reduce((sum, r) => sum + r.points, 0),
stats => stats.docsReviews.length,
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
@@ -456,27 +316,16 @@ async function countContributorPoints() {
.join(', ')})`,
);
printStats(
'PR Contribution Statistics',
stats => stats.prContributions.reduce((sum, r) => sum + r.points, 0),
(user, count) =>
`${user}: ${count} (PRs: ${stats
.get(user)
.prContributions.map(r => `#${r.pr} (${r.points}pts - ${r.category})`)
.join(', ')})`,
);
printStats(
'"Needs Triage" Label Removal Statistics',
stats => stats.labelRemovals.length * CONFIG.POINTS_PER_ISSUE_TRIAGE_ACTION,
stats => stats.labelRemovals.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).labelRemovals.join(', ')})`,
);
printStats(
'Issue Closing Statistics',
stats =>
stats.issueClosings.length * CONFIG.POINTS_PER_ISSUE_CLOSING_ACTION,
stats => stats.issueClosings.length,
(user, count) =>
`${user}: ${count} (Issues: ${stats.get(user).issueClosings.join(', ')})`,
);

View File

@@ -83,11 +83,22 @@ jobs:
PR_DETAILS: ${{ steps.pr-details.outputs.result }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
# Internal bot doesn't trigger workflows on commit; switching to Actual Bot App
- name: Generate GitHub App token
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 != ''
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.ACTUAL_BOT_APP_ID }}
private-key: ${{ secrets.ACTUAL_BOT_PRIVATE_KEY }}
owner: actualbudget
repositories: actual
- 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.ACTIONS_UPDATE_TOKEN }}
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}
@@ -97,7 +108,7 @@ jobs:
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_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_EVENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
SUMMARY_DATA: ${{ steps.generate-summary.outputs.result }}

View File

@@ -199,7 +199,6 @@ jobs:
token: ${{ secrets.FLATHUB_GITHUB_TOKEN }}
commit-message: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
branch: 'release/${{ needs.build.outputs.version }}'
draft: true
title: 'Update Actual flatpak to version ${{ needs.build.outputs.version }}'
body: |
This PR updates the Actual desktop flatpak to version ${{ needs.build.outputs.version }}.

View File

@@ -101,7 +101,6 @@
"typescript/no-var-requires": "warn",
// Import rules
"import/consistent-type-specifier-style": "warn",
"import/first": "error",
"import/no-amd": "error",
"import/no-default-export": "warn",
@@ -112,7 +111,7 @@
"import/no-duplicates": [
"warn",
{
"prefer-inline": false
"prefer-inline": true
}
],
@@ -390,12 +389,6 @@
"typescript-paths/absolute-import": ["error", { "enableAlias": false }]
}
},
{
"files": ["packages/desktop-client/src/style/themes/*"],
"rules": {
"eslint/no-restricted-imports": "off"
}
},
// TODO: enable these
{
"files": [

View File

@@ -1,7 +1,7 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import type { RuleEntity } from 'loot-core/types/models';
import { type RuleEntity } from 'loot-core/types/models';
import * as api from './index';
@@ -356,143 +356,6 @@ describe('API CRUD operations', () => {
);
});
// apis: createTag, getTags, updateTag, deleteTag
test('Tags: successfully complete tag operations', async () => {
// Create tags
const tagId1 = await api.createTag({ tag: 'test-tag1', color: '#ff0000' });
const tagId2 = await api.createTag({
tag: 'test-tag2',
description: 'A test tag',
});
let tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId1,
tag: 'test-tag1',
color: '#ff0000',
}),
expect.objectContaining({
id: tagId2,
tag: 'test-tag2',
description: 'A test tag',
}),
]),
);
// Update tag
await api.updateTag(tagId1, { tag: 'updated-tag', color: '#00ff00' });
tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId1,
tag: 'updated-tag',
color: '#00ff00',
}),
]),
);
// Delete tag
await api.deleteTag(tagId2);
tags = await api.getTags();
expect(tags).not.toEqual(
expect.arrayContaining([expect.objectContaining({ id: tagId2 })]),
);
});
test('Tags: create tag with minimal fields', async () => {
const tagId = await api.createTag({ tag: 'minimal-tag' });
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'minimal-tag',
color: null,
description: null,
}),
]),
);
});
test('Tags: update single field only', async () => {
const tagId = await api.createTag({ tag: 'original', color: '#ff0000' });
// Update only color, tag and description should remain unchanged
await api.updateTag(tagId, { color: '#00ff00' });
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'original',
color: '#00ff00',
description: null,
}),
]),
);
});
test('Tags: handle null values correctly', async () => {
const tagId = await api.createTag({
tag: 'with-nulls',
color: null,
description: null,
});
const tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
color: null,
description: null,
}),
]),
);
});
test('Tags: clear optional field', async () => {
const tagId = await api.createTag({
tag: 'clearable',
color: '#ff0000',
description: 'will be cleared',
});
// Clear color by setting to null
await api.updateTag(tagId, { color: null });
let tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'clearable',
color: null,
description: 'will be cleared',
}),
]),
);
// Clear description by setting to null
await api.updateTag(tagId, { description: null });
tags = await api.getTags();
expect(tags).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: tagId,
tag: 'clearable',
color: null,
description: null,
}),
]),
);
});
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
test('Rules: successfully update rules', async () => {
await api.createPayee({ name: 'test-payee' });

View File

@@ -5,7 +5,6 @@ import type {
APIFileEntity,
APIPayeeEntity,
APIScheduleEntity,
APITagEntity,
} from 'loot-core/server/api-models';
import type { Query } from 'loot-core/shared/query';
import type { Handlers } from 'loot-core/types/handlers';
@@ -275,25 +274,6 @@ export function deletePayee(id: APIPayeeEntity['id']) {
return send('api/payee-delete', { id });
}
export function getTags() {
return send('api/tags-get');
}
export function createTag(tag: Omit<APITagEntity, 'id'>) {
return send('api/tag-create', { tag });
}
export function updateTag(
id: APITagEntity['id'],
fields: Partial<Omit<APITagEntity, 'id'>>,
) {
return send('api/tag-update', { id, fields });
}
export function deleteTag(id: APITagEntity['id']) {
return send('api/tag-delete', { id });
}
export function mergePayees(
targetId: APIPayeeEntity['id'],
mergeIds: APIPayeeEntity['id'][],

View File

@@ -1,4 +1,4 @@
import type { ComponentProps, CSSProperties, ReactNode } from 'react';
import { type ComponentProps, type CSSProperties, type ReactNode } from 'react';
import { Block } from './Block';
import { View } from './View';

View File

@@ -1,8 +1,8 @@
import type { HTMLProps, Ref } from 'react';
import { type HTMLProps, type Ref } from 'react';
import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type BlockProps = HTMLProps<HTMLDivElement> & {
innerRef?: Ref<HTMLDivElement>;

View File

@@ -1,5 +1,10 @@
import React, { forwardRef, useMemo } from 'react';
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react';
import React, {
forwardRef,
useMemo,
type ComponentPropsWithoutRef,
type CSSProperties,
type ReactNode,
} from 'react';
import { Button as ReactAriaButton } from 'react-aria-components';
import { css, cx } from '@emotion/css';

View File

@@ -1,5 +1,4 @@
import { forwardRef } from 'react';
import type { ComponentProps } from 'react';
import { forwardRef, type ComponentProps } from 'react';
import { theme } from './theme';
import { View } from './View';

View File

@@ -1,4 +1,4 @@
import type { ChangeEvent, ReactNode } from 'react';
import { type ChangeEvent, type ReactNode } from 'react';
import {
ColorPicker as AriaColorPicker,
ColorSwatch as AriaColorSwatch,
@@ -8,10 +8,8 @@ import {
Dialog,
DialogTrigger,
parseColor,
} from 'react-aria-components';
import type {
ColorPickerProps as AriaColorPickerProps,
ColorSwatchProps,
type ColorPickerProps as AriaColorPickerProps,
type ColorSwatchProps,
} from 'react-aria-components';
import { css } from '@emotion/css';

View File

@@ -1,4 +1,4 @@
import type { CSSProperties, ReactNode } from 'react';
import { type CSSProperties, type ReactNode } from 'react';
import { View } from './View';

View File

@@ -4,8 +4,10 @@ import {
isValidElement,
useEffect,
useRef,
type ReactElement,
type Ref,
type RefObject,
} from 'react';
import type { ReactElement, Ref, RefObject } from 'react';
type InitialFocusProps<T extends HTMLElement> = {
/**

View File

@@ -1,6 +1,5 @@
import * as React from 'react';
import { forwardRef } from 'react';
import type { Ref } from 'react';
import { forwardRef, type Ref } from 'react';
import { render } from '@testing-library/react';

View File

@@ -1,8 +1,8 @@
import type { ReactNode } from 'react';
import { type ReactNode } from 'react';
import { css } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type InlineFieldProps = {
label: ReactNode;

View File

@@ -1,9 +1,8 @@
import React from 'react';
import type {
ChangeEvent,
ComponentPropsWithRef,
FocusEvent,
KeyboardEvent,
import React, {
type ChangeEvent,
type ComponentPropsWithRef,
type FocusEvent,
type KeyboardEvent,
} from 'react';
import { Input as ReactAriaInput } from 'react-aria-components';

View File

@@ -1,5 +1,4 @@
import { forwardRef } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import { forwardRef, type CSSProperties, type ReactNode } from 'react';
import { styles } from './styles';
import { Text } from './Text';

View File

@@ -1,11 +1,13 @@
import { useEffect, useRef, useState } from 'react';
import type {
ComponentProps,
ComponentType,
CSSProperties,
KeyboardEvent,
ReactNode,
SVGProps,
import {
useEffect,
useRef,
useState,
type ComponentProps,
type ComponentType,
type CSSProperties,
type KeyboardEvent,
type ReactNode,
type SVGProps,
} from 'react';
import { Button } from './Button';

View File

@@ -1,8 +1,8 @@
import type { HTMLProps } from 'react';
import { type HTMLProps } from 'react';
import { css } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type ParagraphProps = HTMLProps<HTMLDivElement> & {
style?: CSSProperties;

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useRef } from 'react';
import type { ComponentProps } from 'react';
import { useCallback, useEffect, useRef, type ComponentProps } from 'react';
import { Popover as ReactAriaPopover } from 'react-aria-components';
import { css } from '@emotion/css';

View File

@@ -1,5 +1,4 @@
import { useRef, useState } from 'react';
import type { CSSProperties } from 'react';
import { useRef, useState, type CSSProperties } from 'react';
import { Button } from './Button';
import { SvgExpandArrow } from './icons/v0';

View File

@@ -1,7 +1,6 @@
import React from 'react';
import type { ReactNode } from 'react';
import React, { type ReactNode } from 'react';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
import { View } from './View';
type SpaceBetweenProps = {

View File

@@ -1,9 +1,13 @@
import React, { forwardRef } from 'react';
import type { HTMLProps, ReactNode, Ref } from 'react';
import React, {
forwardRef,
type HTMLProps,
type ReactNode,
type Ref,
} from 'react';
import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type TextProps = HTMLProps<HTMLSpanElement> & {
innerRef?: Ref<HTMLSpanElement>;

View File

@@ -1,4 +1,4 @@
import type { ComponentProps } from 'react';
import { type ComponentProps } from 'react';
import { Text } from './Text';

View File

@@ -1,5 +1,4 @@
import React from 'react';
import type { CSSProperties } from 'react';
import React, { type CSSProperties } from 'react';
import { css } from '@emotion/css';

View File

@@ -1,5 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { ComponentProps, ReactNode } from 'react';
import React, {
useCallback,
useEffect,
useRef,
useState,
type ComponentProps,
type ReactNode,
} from 'react';
import { Tooltip as AriaTooltip, TooltipTrigger } from 'react-aria-components';
import { styles } from './styles';

View File

@@ -1,9 +1,8 @@
import React, { forwardRef } from 'react';
import type { HTMLProps, Ref } from 'react';
import React, { forwardRef, type HTMLProps, type Ref } from 'react';
import { css, cx } from '@emotion/css';
import type { CSSProperties } from './styles';
import { type CSSProperties } from './styles';
type ViewProps = HTMLProps<HTMLDivElement> & {
className?: string;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import type { SVGProps } from 'react';
import React, { type SVGProps } from 'react';
import { css, keyframes } from '@emotion/css';

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import type { SVGProps } from 'react';
import React, { useState, type SVGProps } from 'react';
export const SvgLoading = (props: SVGProps<SVGSVGElement>) => {
const { color = 'currentColor' } = props;

View File

@@ -1,4 +1,4 @@
import type { Config } from '@svgr/core';
import { type Config } from '@svgr/core';
const tmpl: Config['template'] = (
{ imports, interfaces, componentName, props, jsx },

View File

@@ -12,7 +12,8 @@ const shadowLarge = {
boxShadow: '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
};
export const styles: CSSProperties = {
// oxlint-disable-next-line typescript/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,

View File

@@ -7,7 +7,7 @@
// * Need to check to make sure if account exists when handling
// * transaction changes in syncing
import type { Timestamp } from './timestamp';
import { type Timestamp } from './timestamp';
/**
* Represents a node within a trinary radix trie.

View File

@@ -1,7 +1,7 @@
import murmurhash from 'murmurhash';
import { v4 as uuidv4 } from 'uuid';
import type { TrieNode } from './merkle';
import { type TrieNode } from './merkle';
/**
* Hybrid Unique Logical Clock (HULC) timestamp generator

View File

@@ -1,4 +1,4 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';

View File

@@ -1,9 +1,9 @@
import { join } from 'path';
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import type { AccountPage } from './page-models/account-page';
import { type AccountPage } from './page-models/account-page';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';

View File

@@ -1,8 +1,8 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import type { MobileBankSyncPage } from './page-models/mobile-bank-sync-page';
import { type MobileBankSyncPage } from './page-models/mobile-bank-sync-page';
import { MobileNavigation } from './page-models/mobile-navigation';
test.describe('Mobile Bank Sync', () => {

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import type { BankSyncPage } from './page-models/bank-sync-page';
import { type BankSyncPage } from './page-models/bank-sync-page';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';

View File

@@ -1,11 +1,11 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import * as monthUtils from 'loot-core/shared/months';
import { amountToCurrency, currencyToAmount } from 'loot-core/shared/util';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import type { MobileBudgetPage } from './page-models/mobile-budget-page';
import { type MobileBudgetPage } from './page-models/mobile-budget-page';
import { MobileNavigation } from './page-models/mobile-navigation';
const copyLastMonthBudget = async (

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import type { BudgetPage } from './page-models/budget-page';
import { type BudgetPage } from './page-models/budget-page';
import { ConfigurationPage } from './page-models/configuration-page';
test.describe('Budget', () => {

View File

@@ -1,4 +1,4 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';

View File

@@ -1,5 +1,4 @@
import { expect as baseExpect } from '@playwright/test';
import type { Locator } from '@playwright/test';
import { expect as baseExpect, type Locator } from '@playwright/test';
export { test } from '@playwright/test';

View File

@@ -1,4 +1,4 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';

View File

@@ -1,6 +1,6 @@
import path from 'path';
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { AccountPage } from './page-models/account-page';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { CloseAccountModal } from './close-account-modal';

View File

@@ -1,4 +1,4 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
export class BankSyncPage {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { AccountPage } from './account-page';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class CloseAccountModal {
readonly locator: Locator;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { AccountPage } from './account-page';
import { BudgetPage } from './budget-page';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class CustomReportPage {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
type ConditionsEntry = {
field: string;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { MobileTransactionEntryPage } from './mobile-transaction-entry-page';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { MobileAccountPage } from './mobile-account-page';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class BalanceMenuModal {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class MobileBankSyncPage {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class BudgetMenuModal {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { MobileAccountPage } from './mobile-account-page';
import { BalanceMenuModal } from './mobile-balance-menu-modal';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { EditNotesModal } from './mobile-edit-notes-modal';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class EditNotesModal {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class EnvelopeBudgetSummaryModal {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { MobileAccountPage } from './mobile-account-page';
import { MobileAccountsPage } from './mobile-accounts-page';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class MobilePayeesPage {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class MobileReportsPage {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class MobileRulesPage {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class MobileSchedulesPage {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class TrackingBudgetSummaryModal {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { MobileAccountPage } from './mobile-account-page';

View File

@@ -1,4 +1,4 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { AccountPage } from './account-page';
import { BankSyncPage } from './bank-sync-page';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class PayeesPage {
readonly page: Page;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { CustomReportPage } from './custom-report-page';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { EditRuleModal } from './edit-rule-modal';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
type ScheduleEntry = {
scheduleName?: string;

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
import { ScheduleEditModal } from './schedule-edit-modal';

View File

@@ -1,4 +1,4 @@
import type { Locator, Page } from '@playwright/test';
import { type Locator, type Page } from '@playwright/test';
export class SettingsPage {
readonly page: Page;

View File

@@ -1,9 +1,9 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import { MobileNavigation } from './page-models/mobile-navigation';
import type { MobilePayeesPage } from './page-models/mobile-payees-page';
import { type MobilePayeesPage } from './page-models/mobile-payees-page';
test.describe('Mobile Payees', () => {
let page: Page;

View File

@@ -1,9 +1,9 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';
import type { PayeesPage } from './page-models/payees-page';
import { type PayeesPage } from './page-models/payees-page';
test.describe('Payees', () => {
let page: Page;

View File

@@ -1,10 +1,10 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import type { CustomReportPage } from './page-models/custom-report-page';
import { type CustomReportPage } from './page-models/custom-report-page';
import { Navigation } from './page-models/navigation';
import type { ReportsPage } from './page-models/reports-page';
import { type ReportsPage } from './page-models/reports-page';
test.describe.parallel('Reports', () => {
let page: Page;

View File

@@ -1,9 +1,9 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import { MobileNavigation } from './page-models/mobile-navigation';
import type { MobileRulesPage } from './page-models/mobile-rules-page';
import { type MobileRulesPage } from './page-models/mobile-rules-page';
test.describe('Mobile Rules', () => {
let page: Page;

View File

@@ -1,9 +1,9 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';
import type { RulesPage } from './page-models/rules-page';
import { type RulesPage } from './page-models/rules-page';
test.describe('Rules', () => {
let page: Page;

View File

@@ -1,9 +1,9 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import { MobileNavigation } from './page-models/mobile-navigation';
import type { MobileSchedulesPage } from './page-models/mobile-schedules-page';
import { type MobileSchedulesPage } from './page-models/mobile-schedules-page';
test.describe('Mobile Schedules', () => {
let page: Page;

View File

@@ -1,9 +1,9 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';
import type { SchedulesPage } from './page-models/schedules-page';
import { type SchedulesPage } from './page-models/schedules-page';
test.describe('Schedules', () => {
let page: Page;

View File

@@ -1,4 +1,4 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';

View File

@@ -1,9 +1,9 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';
import type { SettingsPage } from './page-models/settings-page';
import { type SettingsPage } from './page-models/settings-page';
test.describe('Settings', () => {
let page: Page;

View File

@@ -1,4 +1,4 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import { ConfigurationPage } from './page-models/configuration-page';

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test';
import { type Page } from '@playwright/test';
import { expect, test } from './fixtures';
import type { AccountPage } from './page-models/account-page';
import { type AccountPage } from './page-models/account-page';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';

View File

@@ -1,24 +1,23 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import memoizeOne from 'memoize-one';
import { send } from 'loot-core/platform/client/fetch';
import type { SyncResponseWithErrors } from 'loot-core/server/accounts/app';
import { type SyncResponseWithErrors } from 'loot-core/server/accounts/app';
import { groupById } from 'loot-core/shared/util';
import type {
AccountEntity,
CategoryEntity,
SyncServerGoCardlessAccount,
SyncServerPluggyAiAccount,
SyncServerSimpleFinAccount,
TransactionEntity,
import {
type AccountEntity,
type CategoryEntity,
type SyncServerGoCardlessAccount,
type SyncServerPluggyAiAccount,
type SyncServerSimpleFinAccount,
type TransactionEntity,
} from 'loot-core/types/models';
import { resetApp } from '@desktop-client/app/appSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { markPayeesDirty } from '@desktop-client/payees/payeesSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
import type { AppDispatch } from '@desktop-client/redux/store';
import { type AppDispatch } from '@desktop-client/redux/store';
import { setNewTransactions } from '@desktop-client/transactions/transactionsSlice';
const sliceName = 'account';

View File

@@ -1,10 +1,13 @@
import { createAction, createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import {
createAction,
createSlice,
type PayloadAction,
} from '@reduxjs/toolkit';
import { send } from 'loot-core/platform/client/fetch';
import { getUploadError } from 'loot-core/shared/errors';
import type { AccountEntity } from 'loot-core/types/models';
import type { AtLeastOne } from 'loot-core/types/util';
import { type AccountEntity } from 'loot-core/types/models';
import { type AtLeastOne } from 'loot-core/types/util';
import { syncAccounts } from '@desktop-client/accounts/accountsSlice';
import { pushModal } from '@desktop-client/modals/modalsSlice';

View File

@@ -1,7 +1,6 @@
import React, { createContext, useContext } from 'react';
import type { ReactNode } from 'react';
import React, { createContext, useContext, type ReactNode } from 'react';
import type { Permissions } from './types';
import { type Permissions } from './types';
import { useServerURL } from '@desktop-client/components/ServerContext';
import { useSelector } from '@desktop-client/redux';

View File

@@ -1,13 +1,12 @@
import { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import { useEffect, useState, type ReactElement } from 'react';
import { Trans } from 'react-i18next';
import { View } from '@actual-app/components/view';
import type { RemoteFile, SyncedLocalFile } from 'loot-core/types/file';
import { type RemoteFile, type SyncedLocalFile } from 'loot-core/types/file';
import { useAuth } from './AuthProvider';
import type { Permissions } from './types';
import { type Permissions } from './types';
import { useMetadataPref } from '@desktop-client/hooks/useMetadataPref';
import { useSelector } from '@desktop-client/redux';

View File

@@ -1,17 +1,20 @@
import { useTranslation } from 'react-i18next';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { QueryClient, QueryKey } from '@tanstack/react-query';
import type { TFunction } from 'i18next';
import {
useMutation,
useQueryClient,
type QueryClient,
type QueryKey,
} from '@tanstack/react-query';
import { type TFunction } from 'i18next';
import { v4 as uuidv4 } from 'uuid';
import { sendCatch } from 'loot-core/platform/client/fetch';
import type { send } from 'loot-core/platform/client/fetch';
import { sendCatch, type send } from 'loot-core/platform/client/fetch';
import { logger } from 'loot-core/platform/server/log';
import type { IntegerAmount } from 'loot-core/shared/util';
import type {
CategoryEntity,
CategoryGroupEntity,
import { type IntegerAmount } from 'loot-core/shared/util';
import {
type CategoryEntity,
type CategoryGroupEntity,
} from 'loot-core/types/models';
import { categoryQueries } from '.';
@@ -19,7 +22,7 @@ import { categoryQueries } from '.';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
import type { AppDispatch } from '@desktop-client/redux/store';
import { type AppDispatch } from '@desktop-client/redux/store';
const sendThrow: typeof send = async (name, args) => {
const { error, data } = await sendCatch(name, args);

View File

@@ -2,9 +2,9 @@ import { queryOptions } from '@tanstack/react-query';
import i18n from 'i18next';
import { send } from 'loot-core/platform/client/fetch';
import type {
CategoryEntity,
CategoryGroupEntity,
import {
type CategoryEntity,
type CategoryGroupEntity,
} from 'loot-core/types/models';
type CategoryViews = {

View File

@@ -1,13 +1,12 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import { t } from 'i18next';
import { send } from 'loot-core/platform/client/fetch';
import type { RemoteFile } from 'loot-core/server/cloud-storage';
import { type RemoteFile } from 'loot-core/server/cloud-storage';
import { getDownloadError, getSyncError } from 'loot-core/shared/errors';
import type { Budget } from 'loot-core/types/budget';
import type { File } from 'loot-core/types/file';
import type { Handlers } from 'loot-core/types/handlers';
import { type Budget } from 'loot-core/types/budget';
import { type File } from 'loot-core/types/file';
import { type Handlers } from 'loot-core/types/handlers';
import { resetApp, setAppState } from '@desktop-client/app/appSlice';
import { closeModal, pushModal } from '@desktop-client/modals/modalsSlice';
@@ -100,11 +99,10 @@ export const loadBudget = createAppAsyncThunk(
export const closeBudget = createAppAsyncThunk(
`${sliceName}/closeBudget`,
async (_, { dispatch, getState, extra: { queryClient } }) => {
async (_, { dispatch, getState }) => {
const prefs = getState().prefs.local;
if (prefs && prefs.id) {
await dispatch(resetApp());
queryClient.clear();
await dispatch(setAppState({ loadingText: t('Closing...') }));
await send('close-budget');
await dispatch(setAppState({ loadingText: null }));
@@ -117,11 +115,10 @@ export const closeBudget = createAppAsyncThunk(
export const closeBudgetUI = createAppAsyncThunk(
`${sliceName}/closeBudgetUI`,
async (_, { dispatch, getState, extra: { queryClient } }) => {
async (_, { dispatch, getState }) => {
const prefs = getState().prefs.local;
if (prefs && prefs.id) {
await dispatch(resetApp());
queryClient.clear();
}
},
);

View File

@@ -1,6 +1,5 @@
// @ts-strict-ignore
import React from 'react';
import type { CSSProperties } from 'react';
import React, { type CSSProperties } from 'react';
import { SvgRefresh } from '@actual-app/components/icons/v1';
import { View } from '@actual-app/components/view';

View File

@@ -2,8 +2,11 @@
import React, { useEffect, useState } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { ErrorBoundary, useErrorBoundary } from 'react-error-boundary';
import type { FallbackProps } from 'react-error-boundary';
import {
ErrorBoundary,
useErrorBoundary,
type FallbackProps,
} from 'react-error-boundary';
import { HotkeysProvider } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next';
import { BrowserRouter } from 'react-router';

View File

@@ -1,5 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ComponentType, ReactNode, SVGProps } from 'react';
import {
useCallback,
useEffect,
useMemo,
useState,
type ComponentType,
type ReactNode,
type SVGProps,
} from 'react';
import { Trans, useTranslation } from 'react-i18next';
import {

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react';
import type { ReactNode } from 'react';
import React, { useState, type ReactNode } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { Block } from '@actual-app/components/block';

View File

@@ -1,6 +1,10 @@
// @ts-strict-ignore
import React, { useEffect, useEffectEvent, useRef } from 'react';
import type { ReactElement } from 'react';
import React, {
useEffect,
useEffectEvent,
useRef,
type ReactElement,
} from 'react';
import { useTranslation } from 'react-i18next';
import { Navigate, Route, Routes, useHref, useLocation } from 'react-router';

View File

@@ -1,8 +1,6 @@
import React from 'react';
import type { ComponentPropsWithoutRef, ElementType } from 'react';
import React, { type ComponentPropsWithoutRef, type ElementType } from 'react';
import { styles } from '@actual-app/components/styles';
import type { CSSProperties } from '@actual-app/components/styles';
import { styles, type CSSProperties } from '@actual-app/components/styles';
import { Text } from '@actual-app/components/text';
type FinancialTextProps<T extends ElementType = typeof Text> = {

View File

@@ -1,6 +1,13 @@
// @ts-strict-ignore
import { createRef, PureComponent } from 'react';
import type { CSSProperties, ReactNode, Ref, RefObject, UIEvent } from 'react';
import {
createRef,
PureComponent,
type CSSProperties,
type ReactNode,
type Ref,
type RefObject,
type UIEvent,
} from 'react';
import { View } from '@actual-app/components/view';
import memoizeOne from 'memoize-one';

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