Compare commits

..

10 Commits

Author SHA1 Message Date
Joel Jeremy Marquez
d1cf1042af Fix payee 2025-06-16 16:03:55 -07:00
Joel Jeremy Marquez
f0f42044eb Fix getPrettyPayee 2025-06-16 15:51:04 -07:00
Dmitrijs Minajevs
fb9924aced apply suggestions 2025-06-16 15:32:26 -07:00
Dmitrijs Minajevs
da068173e4 merge 2025-06-16 15:32:08 -07:00
Dmitrijs Minajevs
eb84ddaff2 fix optional prop 2025-06-16 15:28:59 -07:00
Dmitrijs Minajevs
e970a2d11f remove controlled focus from FocusableAmountInput 2025-06-16 15:28:58 -07:00
Dmitrijs Minajevs
8438845e92 Remove single active form hook 2025-06-16 15:22:13 -07:00
Dmitrijs Minajevs
027aa00d0b Release note 2025-06-16 14:52:32 -07:00
Dmitrijs Minajevs
a978b686dc remove console.logs 2025-06-16 14:52:32 -07:00
Dmitrijs Minajevs
58d752e94c Fix mobile transaction form requires additional click to unfocus amount input 2025-06-16 14:52:32 -07:00
901 changed files with 9593 additions and 22670 deletions

View File

@@ -1,12 +0,0 @@
---
alwaysApply: true
---
When running yarn commands - always run them in the root directory. Do not run them in child workspaces.
The following commands can be useful:
- `yarn typecheck` to run typechecker
- `yarn lint` to run the code linter and formatter
- `yarn lint:fix` to fix some of the code lint issues (running this is preferred over `yarn lint`)
- `yarn test` to run all the tests

View File

@@ -0,0 +1,24 @@
---
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

@@ -13,7 +13,6 @@ Code Style and Structure
- 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.
- When creating a new component, place it in its own file rather than grouping multiple components in a single file.
Naming Conventions
@@ -31,7 +30,3 @@ 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.
Change validation
- Run `yarn typecheck` in the root directory to validate that the generated TypeScript code is correct

View File

@@ -2,7 +2,6 @@ name: Bug Report
description: File a bug report also known as an issue or problem.
title: '[Bug]: '
labels: ['needs triage', 'bug']
type: Bug
body:
- type: markdown
attributes:

View File

@@ -2,7 +2,6 @@ name: Feature request
description: Request a missing feature
title: '[Feature] '
labels: ['feature']
type: Feature
body:
- type: markdown
attributes:

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

53
.github/actions/bump-package-versions vendored Executable file
View File

@@ -0,0 +1,53 @@
#!/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

@@ -0,0 +1,94 @@
#!/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,8 +1,5 @@
import { Octokit } from '@octokit/rest';
import { minimatch } from 'minimatch';
import pLimit from 'p-limit';
const limit = pLimit(50);
/** Repository-specific configuration for points calculation */
const REPOSITORY_CONFIG = new Map([
@@ -13,10 +10,9 @@ const REPOSITORY_CONFIG = new Map([
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 },
{ minChanges: 1000, points: 6 },
{ minChanges: 100, points: 4 },
{ minChanges: 0, points: 2 },
],
EXCLUDED_FILES: [
'yarn.lock',
@@ -33,8 +29,8 @@ const REPOSITORY_CONFIG = new Map([
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
POINTS_PER_RELEASE_PR: 4,
PR_REVIEW_POINT_TIERS: [
{ minChanges: 2000, points: 6 },
{ minChanges: 200, points: 4 },
{ minChanges: 1000, points: 6 },
{ minChanges: 100, points: 4 },
{ minChanges: 0, points: 2 },
],
EXCLUDED_FILES: ['yarn.lock', '.yarn/**/*'],
@@ -49,25 +45,24 @@ const REPOSITORY_CONFIG = new Map([
function getLastMonthDates() {
// Get data relating to the last month
const now = new Date();
// Always use UTC for calculations
const firstDayOfLastMonth = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1, 0, 0, 0, 0),
now.getFullYear(),
now.getMonth() - 1,
1,
);
const since = process.env.START_DATE
? new Date(Date.parse(process.env.START_DATE))
? new Date(process.env.START_DATE)
: firstDayOfLastMonth;
// Calculate the end of the month for the since date in UTC
// Calculate the end of the month for the since date
const until = new Date(
Date.UTC(
since.getUTCFullYear(),
since.getUTCMonth() + 1,
0,
23,
59,
59,
999,
),
since.getFullYear(),
since.getMonth() + 1,
0,
23,
59,
59,
999,
);
return { since, until };
@@ -80,9 +75,7 @@ function getLastMonthDates() {
* @returns {number} The total points earned for the repository
*/
async function countContributorPoints(repo) {
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const owner = 'actualbudget';
const config = REPOSITORY_CONFIG.get(repo);
@@ -129,134 +122,128 @@ async function countContributorPoints(repo) {
// Get all PRs using search
const searchQuery = `repo:${owner}/${repo} is:pr is:merged merged:${since.toISOString()}..${until.toISOString()}`;
const recentPRs = await octokit.paginate(
'GET /search/issues',
octokit.search.issuesAndPullRequests,
{
q: searchQuery,
per_page: 100,
advanced_search: true,
},
response => response.data.filter(pr => pr.number),
response => response.data,
);
// Get reviews and PR details for each PR
await Promise.all(
recentPRs.map(pr =>
limit(async () => {
const [reviews, modifiedFiles] = await Promise.all([
octokit.pulls.listReviews({ owner, repo, pull_number: pr.number }),
octokit.paginate(
octokit.pulls.listFiles,
{
owner,
repo,
pull_number: pr.number,
per_page: 100,
},
res => res.data,
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);
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+\)/);
const isReleasePR = pr.title.match(/🔖.*\d+\.\d+\.\d+/);
const prPoints =
config.PR_REVIEW_POINT_TIERS.find(t => totalChanges >= t.minChanges)
?.points ?? 0;
// Calculate points for reviewers based on PR size
const prPoints = config.PR_REVIEW_POINT_TIERS.find(
tier => totalChanges > tier.minChanges,
).points;
if (isReleasePR) {
if (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 {
const uniqueReviewers = new Set();
reviews.data
.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;
});
}
}),
),
);
// 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;
});
// 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;
}
}
// 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(),
});
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
await Promise.all(
issues.map(issue =>
limit(async () => {
const { data: events } = await octokit.issues.listEventsForTimeline({
owner,
repo,
issue_number: issue.number,
});
for (const issue of issues) {
const { data: events } = await octokit.issues.listEventsForTimeline({
owner,
repo,
issue_number: issue.number,
});
events
.filter(event => {
const createdAt = new Date(event.created_at);
return (
createdAt.getTime() > since.getTime() &&
createdAt.getTime() <= until.getTime() &&
stats.has(event.actor?.login)
);
})
.forEach(event => {
if (
event.event === 'unlabeled' &&
event.label?.name.toLowerCase() === 'needs triage'
) {
const remover = event.actor.login;
const userStats = stats.get(remover);
userStats.labelRemovals.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
}
// 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);
userStats.labelRemovals.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
}
if (
event.event === 'closed' &&
event.state_reason === 'not_planned'
) {
const closer = event.actor.login;
const userStats = stats.get(closer);
userStats.issueClosings.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_CLOSING_ACTION;
}
});
}),
),
);
if (event.event === 'closed') {
const closer = event.actor.login;
const userStats = stats.get(closer);
userStats.issueClosings.push(issue.number.toString());
userStats.points += config.POINTS_PER_ISSUE_CLOSING_ACTION;
}
});
}
// Print all statistics
printStats(

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

@@ -22,9 +22,9 @@ permissions:
env:
IMAGES: |
${{ !github.event.repository.fork && 'actualbudget/actual-server' || '' }}
ghcr.io/${{ github.repository_owner }}/actual-server
ghcr.io/${{ github.repository_owner }}/actual
actualbudget/actual-server
ghcr.io/actualbudget/actual-server
ghcr.io/actualbudget/actual
# Creates the following tags:
# - actual-server:edge
@@ -34,7 +34,7 @@ env:
jobs:
build:
if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork
if: ${{ github.event.repository.fork == false }}
name: Build Docker image
runs-on: ubuntu-latest
strategy:
@@ -60,7 +60,7 @@ jobs:
- name: Login to Docker Hub
uses: docker/login-action@v3
if: github.event_name != 'pull_request' && !github.event.repository.fork
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -24,7 +24,7 @@ jobs:
strategy:
matrix:
os:
- ubuntu-22.04
- ubuntu-latest
- windows-latest
- macos-latest
runs-on: ${{ matrix.os }}

View File

@@ -19,7 +19,7 @@ jobs:
strategy:
matrix:
os:
- ubuntu-22.04
- ubuntu-latest
- windows-latest
- macos-latest
runs-on: ${{ matrix.os }}

View File

@@ -24,29 +24,8 @@ jobs:
id: bump_package_versions
shell: bash
run: |
declare -A packages=(
[web]="desktop-client"
[electron]="desktop-electron"
[sync]="sync-server"
[api]="api"
)
for key in "${!packages[@]}"; do
pkg="${packages[$key]}"
if [[ -n "${{ github.event.inputs.version }}" ]]; then
version="${{ github.event.inputs.version }}"
else
version=$(node ./packages/ci-actions/bin/get-next-package-version.js \
--package-json "./packages/$pkg/package.json" \
--type auto \
--update)
fi
eval "NEW_${key^^}_VERSION=\"$version\""
done
echo "version=$NEW_WEB_VERSION" >> "$GITHUB_OUTPUT"
.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:

View File

@@ -10,7 +10,6 @@ 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
@@ -20,9 +19,9 @@ jobs:
- name: Update package versions
run: |
# Get new nightly versions
NEW_WEB_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/desktop-client/package.json --type nightly)
NEW_SYNC_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/sync-server/package.json --type nightly)
NEW_API_VERSION=$(node ./packages/ci-actions/bin/get-next-package-version.js --package-json ./packages/api/package.json --type nightly)
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

View File

@@ -81,7 +81,7 @@ jobs:
base-stats-json-path: ./base/web-stats.json
title: desktop-client
- uses: twk3/rollup-size-compare-action@v1.1.1
- uses: github/webpack-bundlesize-compare-action@v2.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
current-stats-json-path: ./head/loot-core-stats.json

3
.gitignore vendored
View File

@@ -60,6 +60,3 @@ fly.toml
# TypeScript cache
build/
# .d.ts files aren't type-checked with skipLibCheck set to true
*.d.ts

View File

@@ -7,8 +7,6 @@ packages/api/migrations
packages/crdt/dist
packages/component-library/src/icons/**/*
packages/desktop-client/bundle.browser.js
packages/desktop-client/stats.json
packages/desktop-client/.swc/
packages/desktop-client/build/
packages/desktop-client/locale/
packages/desktop-client/build-electron/
@@ -25,6 +23,5 @@ packages/desktop-electron/dist/
packages/loot-core/**/node_modules/*
packages/loot-core/**/lib-dist/*
packages/loot-core/**/proto/*
packages/sync-server/coverage/
.yarn/*
upcoming-release-notes/*

View File

@@ -1,10 +0,0 @@
# CODEOWNERS file for Actual Budget
# Please add your name to code-paths that you feel especially
# passionate about. You will be notified for any PRs there.
/packages/api/ @MatissJanis
/packages/component-library/ @MatissJanis
/packages/desktop-client/src/components/mobile @joel-jeremy
/packages/desktop-electron/ @MikesGlitch
/packages/loot-core/src/server/budget @youngcw
/packages/sync-server/ @matt-fidd

View File

@@ -14,8 +14,6 @@ git pull
popd > /dev/null
packages/desktop-client/bin/remove-untranslated-languages
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace loot-core build:browser
yarn workspace @actual-app/web build:browser

View File

@@ -39,8 +39,6 @@ git pull
popd > /dev/null
packages/desktop-client/bin/remove-untranslated-languages
export NODE_OPTIONS="--max-old-space-size=4096"
yarn workspace loot-core build:node
yarn workspace @actual-app/web build --mode=desktop # electron specific build

View File

@@ -6,7 +6,7 @@ import prompts from 'prompts';
async function run() {
const username = await execAsync(
// eslint-disable-next-line actual/typography
// eslint-disable-next-line rulesdir/typography
"gh api user --jq '.login'",
'To avoid having to enter your username, consider installing the official GitHub CLI (https://github.com/cli/cli) and logging in with `gh auth login`.',
);

View File

@@ -1,15 +1,29 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import globals from 'globals';
import pluginImport from 'eslint-plugin-import';
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
import pluginReact from 'eslint-plugin-react';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import pluginRulesDir from 'eslint-plugin-rulesdir';
import pluginTypescript from 'typescript-eslint';
import pluginTypescriptPaths from 'eslint-plugin-typescript-paths';
import pluginActual from './packages/eslint-plugin-actual/lib/index.js';
import tsParser from '@typescript-eslint/parser';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
pluginRulesDir.RULES_DIR = path.join(
__dirname,
'packages',
'eslint-plugin-actual',
'lib',
'rules',
);
const confusingBrowserGlobals = [
// https://github.com/facebook/create-react-app/tree/main/packages/confusing-browser-globals
'addEventListener',
@@ -153,19 +167,14 @@ export default pluginTypescript.config(
pluginImport.flatConfigs.recommended,
{
plugins: {
actual: pluginActual,
},
rules: {
'actual/no-untranslated-strings': 'error',
'actual/prefer-trans-over-t': 'error',
'react-hooks': pluginReactHooks,
'jsx-a11y': pluginJSXA11y,
rulesdir: pluginRulesDir,
'typescript-paths': pluginTypescriptPaths,
},
},
{
files: ['**/*.{js,ts,jsx,tsx}'],
plugins: {
'jsx-a11y': pluginJSXA11y,
'react-hooks': pluginReactHooks,
},
rules: {
// http://eslint.org/docs/rules/
'array-callback-return': 'warn',
@@ -449,9 +458,8 @@ export default pluginTypescript.config(
},
],
'actual/typography': 'warn',
'actual/prefer-if-statement': 'warn',
'actual/prefer-logger-over-console': 'error',
'rulesdir/typography': 'warn',
'rulesdir/prefer-if-statement': 'warn',
// Note: base rule explicitly disabled in favor of the TS one
'no-unused-vars': 'off',
@@ -459,7 +467,6 @@ export default pluginTypescript.config(
'warn',
{
varsIgnorePattern: '^(_|React)',
argsIgnorePattern: '^(_|React)',
ignoreRestSiblings: true,
caughtErrors: 'none',
},
@@ -632,9 +639,6 @@ export default pluginTypescript.config(
},
{
files: ['packages/desktop-client/**/*.{js,ts,jsx,tsx}'],
plugins: {
'typescript-paths': pluginTypescriptPaths,
},
rules: {
'typescript-paths/absolute-parent-import': [
'error',
@@ -774,9 +778,7 @@ export default pluginTypescript.config(
],
rules: {
'actual/typography': 'off',
'actual/no-untranslated-strings': 'off',
'actual/prefer-logger-over-console': 'off',
'rulesdir/typography': 'off',
},
},
{
@@ -795,7 +797,7 @@ export default pluginTypescript.config(
// TODO: fix the issues in these files
rules: {
'import/extensions': 'off',
'actual/typography': 'off',
'rulesdir/typography': 'off',
},
},
{
@@ -803,6 +805,8 @@ export default pluginTypescript.config(
rules: {
'import/no-anonymous-default-export': 'off',
'import/no-default-export': 'off',
// can be re-enabled after https://github.com/actualbudget/actual/pull/4253
'@typescript-eslint/no-unused-vars': 'off',
},
},
);

View File

@@ -41,7 +41,6 @@
"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",
"playwright": "yarn workspace @actual-app/web run playwright",
"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",
@@ -55,17 +54,18 @@
},
"devDependencies": {
"@octokit/rest": "^22.0.0",
"@types/node": "^22.17.0",
"@types/node": "^22.15.18",
"@types/prompts": "^2.4.9",
"@typescript-eslint/parser": "^8.42.0",
"@typescript-eslint/parser": "^8.32.1",
"cross-env": "^7.0.3",
"eslint": "^9.34.0",
"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": "^6.0.0-rc.2",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-rulesdir": "^0.2.2",
"eslint-plugin-typescript-paths": "^0.0.33",
"globals": "^15.15.0",
"html-to-image": "^1.11.13",
@@ -74,13 +74,12 @@
"minimatch": "^10.0.3",
"node-jq": "^6.0.1",
"npm-run-all": "^4.1.5",
"p-limit": "^6.2.0",
"prettier": "^3.5.3",
"prompts": "^2.4.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.2",
"typescript": "^5.9.2",
"typescript-eslint": "^8.42.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.32.1",
"typescript-strict-plugin": "^2.4.4"
},
"resolutions": {
@@ -99,7 +98,7 @@
},
"packageManager": "yarn@4.9.1",
"browserslist": [
"electron >= 35.0",
"electron 24.0",
"defaults"
]
}

View File

@@ -42,11 +42,7 @@ export async function init(config: InitConfig = {}) {
export async function shutdown() {
if (actualApp) {
try {
await actualApp.send('sync');
} catch (e) {
// most likely that no budget is loaded, so the sync failed
}
await actualApp.send('sync');
await actualApp.send('close-budget');
actualApp = null;
}

View File

@@ -740,122 +740,3 @@ describe('API CRUD operations', () => {
expect(transactions[0].notes).toBeNull();
});
});
//apis: createSchedule, getSchedules, updateSchedule, deleteSchedule
test('Schedules: successfully complete schedules operations', async () => {
await api.loadBudget(budgetName);
//test a schedule with a recuring configuration
const ScheduleId1 = await api.createSchedule({
name: 'test-schedule 1',
posts_transaction: true,
// amount: -5000,
amountOp: 'is',
date: {
frequency: 'monthly',
interval: 1,
start: '2025-06-13',
patterns: [],
skipWeekend: false,
weekendSolveMode: 'after',
endMode: 'never',
},
});
//test the creation of non recurring schedule
const ScheduleId2 = await api.createSchedule({
name: 'test-schedule 2',
posts_transaction: false,
amount: 4000,
amountOp: 'is',
date: '2025-06-13',
});
let schedules = await api.getSchedules();
// Schedules successfully created
expect(schedules).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'test-schedule 1',
posts_transaction: true,
// amount: -5000,
amountOp: 'is',
date: {
frequency: 'monthly',
interval: 1,
start: '2025-06-13',
patterns: [],
skipWeekend: false,
weekendSolveMode: 'after',
endMode: 'never',
},
}),
expect.objectContaining({
name: 'test-schedule 2',
posts_transaction: false,
amount: 4000,
amountOp: 'is',
date: '2025-06-13',
}),
]),
);
//check getIDByName works on schedules
expect(await api.getIDByName('schedules', 'test-schedule 1')).toEqual(
ScheduleId1,
);
expect(await api.getIDByName('schedules', 'test-schedule 2')).toEqual(
ScheduleId2,
);
//check getIDByName works on accounts
const schedAccountId1 = await api.createAccount(
{ name: 'sched-test-account1', offbudget: true },
1000,
);
expect(await api.getIDByName('accounts', 'sched-test-account1')).toEqual(
schedAccountId1,
);
//check getIDByName works on payees
const schedPayeeId1 = await api.createPayee({ name: 'sched-test-payee1' });
expect(await api.getIDByName('payees', 'sched-test-payee1')).toEqual(
schedPayeeId1,
);
await api.updateSchedule(ScheduleId1, {
amount: -10000,
account: schedAccountId1,
});
await api.deleteSchedule(ScheduleId2);
// schedules successfully updated, and one of them deleted
await api.updateSchedule(ScheduleId1, {
amount: -10000,
account: schedAccountId1,
payee: schedPayeeId1,
});
await api.deleteSchedule(ScheduleId2);
schedules = await api.getSchedules();
expect(schedules).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: ScheduleId1,
posts_transaction: true,
amount: -10000,
account: schedAccountId1,
payee: schedPayeeId1,
amountOp: 'is',
date: {
frequency: 'monthly',
interval: 1,
start: '2025-06-13',
patterns: [],
skipWeekend: false,
weekendSolveMode: 'after',
endMode: 'never',
},
}),
expect.not.objectContaining({ id: ScheduleId2 }),
]),
);
});

View File

@@ -96,7 +96,6 @@ export function addTransactions(
export interface ImportTransactionsOpts {
defaultCleared?: boolean;
dryRun?: boolean;
}
export function importTransactions(
@@ -104,13 +103,11 @@ export function importTransactions(
transactions: ImportTransactionEntity[],
opts: ImportTransactionsOpts = {
defaultCleared: true,
dryRun: false,
},
) {
return send('api/transactions-import', {
accountId,
transactions,
isPreview: opts.dryRun,
opts,
});
}
@@ -242,31 +239,3 @@ export function holdBudgetForNextMonth(month, amount) {
export function resetBudgetHold(month) {
return send('api/budget-reset-hold', { month });
}
export function createSchedule(schedule) {
return send('api/schedule-create', schedule);
}
export function updateSchedule(id, fields, resetNextDate?: boolean) {
return send('api/schedule-update', {
id,
fields,
resetNextDate,
});
}
export function deleteSchedule(scheduleId) {
return send('api/schedule-delete', scheduleId);
}
export function getSchedules() {
return send('api/schedules-get');
}
export function getIDByName(type, name) {
return send('api/get-id-by-name', { type, name });
}
export function getServerVersion() {
return send('api/get-server-version');
}

View File

@@ -1,6 +1,6 @@
{
"name": "@actual-app/api",
"version": "25.9.0",
"version": "25.6.1",
"license": "MIT",
"description": "An API for Actual",
"engines": {
@@ -24,14 +24,14 @@
},
"dependencies": {
"@actual-app/crdt": "workspace:^",
"better-sqlite3": "^12.2.0",
"better-sqlite3": "^11.10.0",
"compare-versions": "^6.1.1",
"node-fetch": "^3.3.2",
"uuid": "^11.1.0"
},
"devDependencies": {
"tsc-alias": "^1.8.16",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"typescript": "^5.8.3",
"vitest": "^3.1.3"
}
}

View File

@@ -11,7 +11,7 @@
"outDir": "dist",
"declarationDir": "@types",
"paths": {
"loot-core/*": ["./@types/loot-core/src/*"]
"loot-core/*": ["./@types/loot-core/*"]
}
},
"include": ["."],

View File

@@ -1,73 +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.
import fs from 'node:fs';
import { parseArgs } from 'node:util';
// eslint-disable-next-line import/extensions
import { getNextVersion } from '../src/versions/get-next-package-version.js';
const args = process.argv;
const options = {
'package-json': {
type: 'string',
short: 'p',
},
type: {
type: 'string', // nightly, hotfix, monthly, auto
short: 't',
},
update: {
type: 'boolean',
short: 'u',
default: false,
},
};
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'];
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const currentVersion = packageJson.version;
let newVersion;
try {
newVersion = getNextVersion({
currentVersion,
type: values.type,
currentDate: new Date(),
});
} catch (e) {
console.error(e.message);
process.exit(1);
}
process.stdout.write(newVersion);
if (values.update) {
packageJson.version = newVersion;
fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, null, 2) + '\n',
'utf8',
);
}
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
}

View File

@@ -1,11 +0,0 @@
{
"name": "@actual-app/ci-actions",
"private": true,
"type": "module",
"devDependencies": {
"vitest": "^3.2.4"
},
"scripts": {
"test": "vitest"
}
}

View File

@@ -1,72 +0,0 @@
function parseVersion(version) {
const [y, m, p] = version.split('.');
return {
versionYear: parseInt(y, 10),
versionMonth: parseInt(m, 10),
versionHotfix: parseInt(p, 10),
};
}
function computeNextMonth(versionYear, versionMonth) {
// 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 fullYear = nextVersionMonthDate.getFullYear();
const nextVersionYear = fullYear.toString().slice(fullYear < 2100 ? -2 : -3);
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
return { nextVersionYear, nextVersionMonth };
}
// Determine logical type from 'auto' based on the current date and version
function resolveType(type, currentDate, versionYear, versionMonth) {
if (type !== 'auto') return type;
const inPatchMonth =
currentDate.getFullYear() === 2000 + versionYear &&
currentDate.getMonth() + 1 === versionMonth;
if (inPatchMonth && currentDate.getDate() <= 25) return 'hotfix';
return 'monthly';
}
export function getNextVersion({
currentVersion,
type,
currentDate = new Date(),
}) {
const { versionYear, versionMonth, versionHotfix } =
parseVersion(currentVersion);
const { nextVersionYear, nextVersionMonth } = computeNextMonth(
versionYear,
versionMonth,
);
const resolvedType = resolveType(
type,
currentDate,
versionYear,
versionMonth,
);
// Format date stamp once for nightly
const currentDateString = currentDate
.toISOString()
.split('T')[0]
.replaceAll('-', '');
switch (resolvedType) {
case 'nightly':
return `${nextVersionYear}.${nextVersionMonth}.0-nightly.${currentDateString}`;
case 'hotfix':
return `${versionYear}.${versionMonth}.${versionHotfix + 1}`;
case 'monthly':
return `${nextVersionYear}.${nextVersionMonth}.0`;
default:
throw new Error(
'Invalid type specified. Use “auto”, “nightly”, “hotfix”, or “monthly”.',
);
}
}

View File

@@ -1,85 +0,0 @@
import { describe, it, expect } from 'vitest';
import { getNextVersion } from './get-next-package-version';
describe('getNextVersion (lib)', () => {
it('hotfix increments patch', () => {
expect(
getNextVersion({
currentVersion: '25.8.1',
type: 'hotfix',
currentDate: new Date('2025-08-10'),
}),
).toBe('25.8.2');
});
it('monthly advances month same year', () => {
expect(
getNextVersion({
currentVersion: '25.8.3',
type: 'monthly',
currentDate: new Date('2025-08-15'),
}),
).toBe('25.9.0');
});
it('monthly wraps year December -> January', () => {
expect(
getNextVersion({
currentVersion: '25.12.3',
type: 'monthly',
currentDate: new Date('2025-12-05'),
}),
).toBe('26.1.0');
});
it('nightly format with date stamp', () => {
expect(
getNextVersion({
currentVersion: '25.8.1',
type: 'nightly',
currentDate: new Date('2025-08-22'),
}),
).toBe('25.9.0-nightly.20250822');
});
it('auto before 25th -> hotfix', () => {
expect(
getNextVersion({
currentVersion: '25.8.4',
type: 'auto',
currentDate: new Date('2025-08-20'),
}),
).toBe('25.8.5');
});
it('auto after 25th (same month) -> monthly', () => {
expect(
getNextVersion({
currentVersion: '25.8.4',
type: 'auto',
currentDate: new Date('2025-08-27'),
}),
).toBe('25.9.0');
});
it('auto after 25th (next month) -> monthly', () => {
expect(
getNextVersion({
currentVersion: '25.8.4',
type: 'auto',
currentDate: new Date('2025-09-02'),
}),
).toBe('25.9.0');
});
it('invalid type throws', () => {
expect(() =>
getNextVersion({
currentVersion: '25.8.4',
type: 'unknown',
currentDate: new Date('2025-08-10'),
}),
).toThrow(/Invalid type/);
});
});

View File

@@ -1,9 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
include: ['src/**/*.test.(js|jsx|ts|tsx)'],
environment: 'node',
},
});

View File

@@ -13,10 +13,10 @@
},
"devDependencies": {
"@svgr/cli": "^8.1.0",
"@types/react": "^19.1.12",
"react": "19.1.1",
"react-dom": "19.1.1",
"vitest": "^3.2.4"
"@types/react": "^19.1.4",
"react": "19.1.0",
"react-dom": "19.1.0",
"vitest": "^3.1.3"
},
"exports": {
"./hooks/*": "./src/hooks/*.ts",
@@ -48,8 +48,7 @@
"./tokens": "./src/tokens.ts",
"./toggle": "./src/Toggle.tsx",
"./tooltip": "./src/Tooltip.tsx",
"./view": "./src/View.tsx",
"./color-picker": "./src/ColorPicker.tsx"
"./view": "./src/View.tsx"
},
"scripts": {
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",

View File

@@ -1,180 +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 DEFAULT_COLOR_SET = [
'#690CB0',
'#D32F2F',
'#C2185B',
'#7B1FA2',
'#512DA8',
'#303F9F',
'#1976D2',
'#0288D1',
'#0097A7',
'#00796B',
'#388E3C',
'#689F38',
'#AFB42B',
'#FBC02D',
'#FFA000',
'#F57C00',
'#E64A19',
'#5D4037',
'#616161',
'#455A64',
];
interface ColorSwatchPickerProps {
columns?: number;
colorset?: string[];
}
function ColorSwatchPicker({
columns = 5,
colorset = DEFAULT_COLOR_SET,
}: ColorSwatchPickerProps) {
const pickers = [];
for (let l = 0; l < colorset.length / columns; l++) {
const pickerItems = [];
for (let c = 0; c < columns; c++) {
const color = colorset[columns * l + c];
if (!color) {
break;
}
pickerItems.push(
<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>,
);
}
pickers.push(
<AriaColorSwatchPicker
key={`colorset-${l}`}
style={{
display: 'flex',
gap: '8px',
flexWrap: 'wrap',
}}
>
{pickerItems}
</AriaColorSwatchPicker>,
);
}
return pickers;
}
const isColor = (value: string) => /^#[0-9a-fA-F]{6}$/.test(value);
interface ColorPickerProps extends AriaColorPickerProps {
children?: ReactNode;
columns?: number;
colorset?: string[];
}
export function ColorPicker({
children,
columns,
colorset,
...props
}: ColorPickerProps) {
const onInput = (value: string) => {
if (!isColor(value)) {
return;
}
const color = parseColor(value);
if (color) {
props.onChange?.(color);
}
};
return (
<AriaColorPicker defaultValue={props.defaultValue ?? '#690CB0'} {...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 columns={columns} colorset={colorset} />
<ColorField
onInput={({ target: { value } }: ChangeEvent<HTMLInputElement>) =>
onInput(value)
}
>
<Input placeholder="#RRGGBB" style={{ width: '100px' }} />
</ColorField>
</Dialog>
</Popover>
</DialogTrigger>
</AriaColorPicker>
);
}

View File

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

View File

@@ -62,7 +62,7 @@ export const Toggle = ({
data-on={isOn}
className={css(
{
// eslint-disable-next-line actual/typography
// eslint-disable-next-line rulesdir/typography
content: '" "',
position: 'absolute',
top: '2px',

View File

@@ -27,13 +27,8 @@ export const Tooltip = ({
const [isHovered, setIsHover] = useState(false);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const closeTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
const handlePointerEnter = useCallback(() => {
if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
}
const timeout = setTimeout(() => {
setIsHover(true);
}, triggerProps.delay ?? 300);
@@ -46,10 +41,8 @@ export const Tooltip = ({
clearTimeout(hoverTimeoutRef.current);
}
closeTimeoutRef.current = setTimeout(() => {
setIsHover(false);
}, triggerProps.closeDelay ?? 0);
}, [triggerProps.closeDelay]);
setIsHover(false);
}, []);
// Force closing the tooltip whenever the disablement state changes
useEffect(() => {

View File

@@ -23,11 +23,7 @@ export const View = forwardRef<HTMLDivElement, ViewProps>((props, ref) => {
{...restProps}
ref={innerRef ?? ref}
style={nativeStyle}
className={cx(
'view',
className,
style && Object.keys(style).length > 0 ? css(style) : undefined,
)}
className={cx('view', className, css(style))}
/>
);
});

View File

@@ -91,7 +91,7 @@ export const styles: Record<string, any> = {
},
shadowLarge,
tnum: {
// eslint-disable-next-line actual/typography
// eslint-disable-next-line rulesdir/typography
fontFeatureSettings: '"tnum"',
},
notFixed: { fontFeatureSettings: '' },

View File

@@ -188,7 +188,6 @@ export const theme = {
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)',

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

@@ -11,7 +11,7 @@
"scripts": {
"build:node": "tsc --p tsconfig.dist.json",
"proto:generate": "./bin/generate-proto",
"build": "rm -rf dist && yarn run build:node",
"build": "rm -rf dist && yarn run build:node && cp src/proto/sync_pb.d.ts dist/src/proto/",
"test": "vitest --globals"
},
"dependencies": {
@@ -20,10 +20,8 @@
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/google-protobuf": "^3.15.12",
"protoc-gen-js": "^3.21.4-4",
"ts-protoc-gen": "^0.15.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4"
"typescript": "^5.8.3",
"vitest": "^3.1.3"
}
}

View File

@@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import './proto/sync_pb.js'; // Import for side effects
import * as SyncPb from './proto/sync_pb';
export {
merkle,
getClock,
@@ -13,11 +11,4 @@ export {
Timestamp,
} from './crdt';
// Access global proto namespace
export const SyncRequest = (globalThis as any).proto.SyncRequest;
export const SyncResponse = (globalThis as any).proto.SyncResponse;
export const Message = (globalThis as any).proto.Message;
export const MessageEnvelope = (globalThis as any).proto.MessageEnvelope;
export const EncryptedData = (globalThis as any).proto.EncryptedData;
export const SyncProtoBuf = (globalThis as any).proto;
export const SyncProtoBuf = SyncPb;

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) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

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