Compare commits
194 Commits
dm-fix-sec
...
v25.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5459b8baca | ||
|
|
073d91a7b7 | ||
|
|
58a638cee2 | ||
|
|
23f1bae7db | ||
|
|
57240284a3 | ||
|
|
6c6d8931bb | ||
|
|
cae8fa4e6f | ||
|
|
48ae371ecc | ||
|
|
e8d93fb797 | ||
|
|
6790f99de2 | ||
|
|
68f0b05aed | ||
|
|
c954d3924e | ||
|
|
adf4bd2d0f | ||
|
|
102c6eaff6 | ||
|
|
21105fc25b | ||
|
|
c69142f58e | ||
|
|
fe32bf14c6 | ||
|
|
92e43bc3b5 | ||
|
|
165be3d0df | ||
|
|
3dd22994b7 | ||
|
|
96bfc69332 | ||
|
|
284fc13161 | ||
|
|
30102b1474 | ||
|
|
3a8eb96d76 | ||
|
|
91a8bc3ef1 | ||
|
|
dc2ab4843f | ||
|
|
89e5676cfb | ||
|
|
645342d47d | ||
|
|
116c695964 | ||
|
|
a5d18929c8 | ||
|
|
989d332e1b | ||
|
|
169d08e721 | ||
|
|
a74da11904 | ||
|
|
cccd66713d | ||
|
|
1ce53b2762 | ||
|
|
d75f984186 | ||
|
|
692ade7254 | ||
|
|
da0ac0b144 | ||
|
|
be20f65b6e | ||
|
|
1067e32028 | ||
|
|
dcb1c69e67 | ||
|
|
f084e28086 | ||
|
|
f54e459e03 | ||
|
|
ccdde60bfe | ||
|
|
712d315229 | ||
|
|
31c6362307 | ||
|
|
d1519993d6 | ||
|
|
ebde78434a | ||
|
|
8fcaff8e3a | ||
|
|
13bc99738f | ||
|
|
959824d317 | ||
|
|
2abc144b03 | ||
|
|
71250f5fb7 | ||
|
|
c5f050f6f8 | ||
|
|
0d46e221f9 | ||
|
|
6bf2f581a3 | ||
|
|
3c34603111 | ||
|
|
3e488ae8f7 | ||
|
|
bacf3091b6 | ||
|
|
ac77c0f360 | ||
|
|
e21256e7a2 | ||
|
|
22237d11ca | ||
|
|
63604c1161 | ||
|
|
74b95ca83e | ||
|
|
6a9028464b | ||
|
|
186d417c6e | ||
|
|
c898116412 | ||
|
|
85bd6bfb81 | ||
|
|
72616376e2 | ||
|
|
136ad055f7 | ||
|
|
c621f68e0a | ||
|
|
4f611ca458 | ||
|
|
d98e8375a8 | ||
|
|
4f50c1a889 | ||
|
|
0fa582b3d3 | ||
|
|
80cd2cf347 | ||
|
|
f328332ab2 | ||
|
|
7465bdb54d | ||
|
|
1ad3406e84 | ||
|
|
48166952ce | ||
|
|
994b959050 | ||
|
|
7f4f5005a5 | ||
|
|
1e05b169c8 | ||
|
|
c54a5b3405 | ||
|
|
5c11a0a51a | ||
|
|
8f69669cc6 | ||
|
|
9ebdba27fd | ||
|
|
8df3d23e03 | ||
|
|
5d238c238d | ||
|
|
e69235a35b | ||
|
|
611f7b046a | ||
|
|
ee8f7453ba | ||
|
|
380fae1ccd | ||
|
|
8d84f16604 | ||
|
|
239a087542 | ||
|
|
895d69f875 | ||
|
|
1cb5e97fab | ||
|
|
13bd08d243 | ||
|
|
d946852ee9 | ||
|
|
4cdff76547 | ||
|
|
1a1975b5ab | ||
|
|
6e2154d401 | ||
|
|
45bfd23daa | ||
|
|
c44b32805f | ||
|
|
45530638fe | ||
|
|
a15ff85c20 | ||
|
|
1861060bda | ||
|
|
9281acb819 | ||
|
|
dc86441809 | ||
|
|
78bcac95ed | ||
|
|
45610bae81 | ||
|
|
f5a6700b21 | ||
|
|
8cea059834 | ||
|
|
31b31f2edb | ||
|
|
4b56e0cb54 | ||
|
|
f8dd4897e7 | ||
|
|
526d7a9baa | ||
|
|
af8f26f200 | ||
|
|
548f36e8d6 | ||
|
|
9aed1f400c | ||
|
|
59bcee9369 | ||
|
|
ff7529f6e5 | ||
|
|
77e99af297 | ||
|
|
82a3c97222 | ||
|
|
cafcc823cb | ||
|
|
582e27dbdb | ||
|
|
d747563915 | ||
|
|
84dec137bb | ||
|
|
76de8bf67f | ||
|
|
5db7026435 | ||
|
|
e4a993ad67 | ||
|
|
38ab63638a | ||
|
|
4eea349966 | ||
|
|
f66baeabd3 | ||
|
|
19180138bb | ||
|
|
7705a2df08 | ||
|
|
202af094af | ||
|
|
333e7ff7bc | ||
|
|
eb11e14e94 | ||
|
|
dbcfb63857 | ||
|
|
64f4d200dd | ||
|
|
4be7e03570 | ||
|
|
ca969cc61b | ||
|
|
682d439c34 | ||
|
|
37d91a90f7 | ||
|
|
3a09d91399 | ||
|
|
405c8b986f | ||
|
|
bd9f0aec89 | ||
|
|
a4f0d5bca8 | ||
|
|
0085c3b58a | ||
|
|
cfb0f51e36 | ||
|
|
2d95fe6d03 | ||
|
|
80bb4ab299 | ||
|
|
2f71e007d6 | ||
|
|
7b71374e79 | ||
|
|
c1d70722b8 | ||
|
|
2cd79960a9 | ||
|
|
9a6afda6de | ||
|
|
979fa43c4a | ||
|
|
a20805bfae | ||
|
|
485830c859 | ||
|
|
4aea6f4898 | ||
|
|
1f828b6562 | ||
|
|
ad9a84ddf1 | ||
|
|
d9a171b249 | ||
|
|
e5c84d4ae0 | ||
|
|
94a76a008d | ||
|
|
432c2b6165 | ||
|
|
46eb2a7c38 | ||
|
|
3214d5dd53 | ||
|
|
66d8f1a631 | ||
|
|
e3aa63d1fa | ||
|
|
9478707ebb | ||
|
|
9952412e1d | ||
|
|
8231bbbf5a | ||
|
|
eadd88ce31 | ||
|
|
6c61cf6a8d | ||
|
|
0fb9c252ca | ||
|
|
d73ead135e | ||
|
|
2c87c44168 | ||
|
|
15beba2ca3 | ||
|
|
667cc24fac | ||
|
|
4cc542a658 | ||
|
|
093d799ba0 | ||
|
|
68d10f6b29 | ||
|
|
252f04e02c | ||
|
|
13cb85835b | ||
|
|
39cf04c74d | ||
|
|
562b5e2afd | ||
|
|
4b4e32d0e2 | ||
|
|
d821f1cebc | ||
|
|
53e3694a38 | ||
|
|
8647452ccc | ||
|
|
5a40b017f0 |
12
.cursor/rules/commands.mdc
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
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
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
Before pushing code changes or opening a pull request, follow these steps:
|
||||
|
||||
1. Check if your branch already has a changelog file in the "upcoming-release-notes" folder.
|
||||
2. If there is no changelog file for your branch:
|
||||
a. Find the number of the most recent (highest-numbered) open issue or pull request on GitHub.
|
||||
b. Increment that number by 1. Use this as the filename for your new changelog file.
|
||||
c. Create a new markdown file in the "upcoming-release-notes" folder with the following format:
|
||||
|
||||
```
|
||||
---
|
||||
category: Features OR Maintenance OR Enhancements OR Bugfix
|
||||
authors: [$GithubUsername]
|
||||
---
|
||||
|
||||
$Description
|
||||
```
|
||||
|
||||
3. Commit the new changelog file.
|
||||
4. Proceed with your push or pull request.
|
||||
@@ -30,3 +30,7 @@ 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
|
||||
|
||||
75
.github/actions/ai-generated-release-notes/check-first-comment.js
vendored
Executable file
@@ -0,0 +1,75 @@
|
||||
#!/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);
|
||||
});
|
||||
76
.github/actions/ai-generated-release-notes/check-release-notes-exists.js
vendored
Executable file
@@ -0,0 +1,76 @@
|
||||
#!/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();
|
||||
77
.github/actions/ai-generated-release-notes/comment-on-pr.js
vendored
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/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();
|
||||
96
.github/actions/ai-generated-release-notes/create-release-notes-file.js
vendored
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/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();
|
||||
118
.github/actions/ai-generated-release-notes/determine-category.js
vendored
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/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');
|
||||
}
|
||||
97
.github/actions/ai-generated-release-notes/generate-summary.js
vendored
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/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');
|
||||
}
|
||||
59
.github/actions/ai-generated-release-notes/pr-details.js
vendored
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/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
@@ -1,53 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
version="${1#v}"
|
||||
else
|
||||
version=""
|
||||
fi
|
||||
|
||||
files_to_bump=(
|
||||
packages/api/package.json
|
||||
packages/desktop-client/package.json
|
||||
packages/desktop-electron/package.json
|
||||
packages/sync-server/package.json
|
||||
)
|
||||
|
||||
for file in "${files_to_bump[@]}"; do
|
||||
if [ -z "$version" ]; then
|
||||
# version format: YY.MM.patch
|
||||
version="$(jq -r .version "$file" | perl -e '
|
||||
($y,$m,$p)=split(/\./,<>);
|
||||
($sec,$min,$hour,$day,$mon,$year)=localtime();
|
||||
$year -= 100; # Perl year starts at 1900
|
||||
$mon++; # Adjust 0-indexed month to 1-indexed
|
||||
if ($y == $year && $m == $mon) {
|
||||
if ($day <= 25) {
|
||||
# Patch release for the current month
|
||||
$p++;
|
||||
} else {
|
||||
# Use next month for a new release period
|
||||
$p = 0;
|
||||
$m++;
|
||||
$m > 12 && ($m=1, $y++);
|
||||
}
|
||||
} else {
|
||||
# Use the current date for a new release period
|
||||
$y = $year;
|
||||
$m = $mon;
|
||||
$p = 0;
|
||||
}
|
||||
print "$y.$m.$p\n";
|
||||
')"
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
echo "Error: Failed to calculate new version" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Bumping $file to version $version"
|
||||
jq '.version = "'"$version"'"' "$file" > "$file.tmp"
|
||||
mv "$file.tmp" "$file"
|
||||
done
|
||||
49
.github/actions/get-next-package-version.js
vendored
@@ -14,9 +14,14 @@ const options = {
|
||||
short: 'p',
|
||||
},
|
||||
type: {
|
||||
type: 'string', // nightly, hotfix, monthly
|
||||
type: 'string', // nightly, hotfix, monthly, auto
|
||||
short: 't',
|
||||
},
|
||||
update: {
|
||||
type: 'boolean',
|
||||
short: 'u',
|
||||
default: false,
|
||||
},
|
||||
};
|
||||
|
||||
const { values } = parseArgs({
|
||||
@@ -57,37 +62,55 @@ try {
|
||||
const nextVersionYear = nextVersionMonthDate
|
||||
.getFullYear()
|
||||
.toString()
|
||||
.slice(-2);
|
||||
.slice(nextVersionMonthDate.getFullYear() < 2100 ? -2 : -3);
|
||||
const nextVersionMonth = nextVersionMonthDate.getMonth() + 1; // Convert back to 1-indexed
|
||||
|
||||
// Get current date string
|
||||
const currentDate = new Date()
|
||||
const currentDate = new Date();
|
||||
const currentDateString = currentDate
|
||||
.toISOString()
|
||||
.split('T')[0]
|
||||
.replaceAll('-', '');
|
||||
|
||||
if (values.type === 'auto') {
|
||||
if (currentDate.getDate() <= 25) {
|
||||
values.type = 'hotfix';
|
||||
} else {
|
||||
values.type = 'monthly';
|
||||
}
|
||||
}
|
||||
|
||||
let newVersion;
|
||||
switch (values.type) {
|
||||
case 'nightly': {
|
||||
const newVersion = `${nextVersionYear}.${nextVersionMonth}.0-nightly.${currentDate}`;
|
||||
process.stdout.write(newVersion); // return the new version to stdout
|
||||
process.exit();
|
||||
newVersion = `${nextVersionYear}.${nextVersionMonth}.0-nightly.${currentDateString}`;
|
||||
break;
|
||||
}
|
||||
case 'hotfix': {
|
||||
const bugfixVersion = `${versionYear}.${versionMonth}.${versionHotfix + 1}`;
|
||||
process.stdout.write(bugfixVersion); // return the bugfix version to stdout
|
||||
process.exit();
|
||||
newVersion = `${versionYear}.${versionMonth}.${versionHotfix + 1}`;
|
||||
break;
|
||||
}
|
||||
case 'monthly': {
|
||||
const stableVersion = `${nextVersionYear}.${nextVersionMonth}.0`;
|
||||
process.stdout.write(stableVersion); // return the stable version to stdout
|
||||
process.exit();
|
||||
newVersion = `${nextVersionYear}.${nextVersionMonth}.0`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.error(
|
||||
'Invalid type specified. Use "nightly", "hotfix", or "monthly".',
|
||||
'Invalid type specified. Use "auto", "nightly", "hotfix", or "monthly".',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
process.stdout.write(newVersion); // return the new version to stdout
|
||||
|
||||
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);
|
||||
|
||||
255
.github/scripts/count-points.mjs
vendored
@@ -1,5 +1,8 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { minimatch } from 'minimatch';
|
||||
import pLimit from 'p-limit';
|
||||
|
||||
const limit = pLimit(30);
|
||||
|
||||
/** Repository-specific configuration for points calculation */
|
||||
const REPOSITORY_CONFIG = new Map([
|
||||
@@ -10,9 +13,10 @@ const REPOSITORY_CONFIG = new Map([
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 0,
|
||||
PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 1000, points: 6 },
|
||||
{ minChanges: 100, points: 4 },
|
||||
{ minChanges: 0, points: 2 },
|
||||
{ minChanges: 500, points: 8 },
|
||||
{ minChanges: 100, points: 6 },
|
||||
{ minChanges: 10, points: 2 },
|
||||
{ minChanges: 0, points: 1 },
|
||||
],
|
||||
EXCLUDED_FILES: [
|
||||
'yarn.lock',
|
||||
@@ -29,8 +33,8 @@ const REPOSITORY_CONFIG = new Map([
|
||||
POINTS_PER_ISSUE_CLOSING_ACTION: 1,
|
||||
POINTS_PER_RELEASE_PR: 4,
|
||||
PR_REVIEW_POINT_TIERS: [
|
||||
{ minChanges: 1000, points: 6 },
|
||||
{ minChanges: 100, points: 4 },
|
||||
{ minChanges: 2000, points: 6 },
|
||||
{ minChanges: 200, points: 4 },
|
||||
{ minChanges: 0, points: 2 },
|
||||
],
|
||||
EXCLUDED_FILES: ['yarn.lock', '.yarn/**/*'],
|
||||
@@ -45,24 +49,25 @@ 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(
|
||||
now.getFullYear(),
|
||||
now.getMonth() - 1,
|
||||
1,
|
||||
Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1, 0, 0, 0, 0),
|
||||
);
|
||||
const since = process.env.START_DATE
|
||||
? new Date(process.env.START_DATE)
|
||||
? new Date(Date.parse(process.env.START_DATE))
|
||||
: firstDayOfLastMonth;
|
||||
|
||||
// Calculate the end of the month for the since date
|
||||
// Calculate the end of the month for the since date in UTC
|
||||
const until = new Date(
|
||||
since.getFullYear(),
|
||||
since.getMonth() + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
999,
|
||||
Date.UTC(
|
||||
since.getUTCFullYear(),
|
||||
since.getUTCMonth() + 1,
|
||||
0,
|
||||
23,
|
||||
59,
|
||||
59,
|
||||
999,
|
||||
),
|
||||
);
|
||||
|
||||
return { since, until };
|
||||
@@ -75,7 +80,9 @@ 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);
|
||||
|
||||
@@ -132,118 +139,124 @@ async function countContributorPoints(repo) {
|
||||
);
|
||||
|
||||
// Get reviews and PR details for each PR
|
||||
for (const pr of recentPRs) {
|
||||
const { data: reviews } = await octokit.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
});
|
||||
|
||||
// Get list of modified files
|
||||
const { data: modifiedFiles } = await octokit.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr.number,
|
||||
});
|
||||
|
||||
// Calculate points based on PR size, excluding specified files
|
||||
const totalChanges = modifiedFiles
|
||||
.filter(
|
||||
file =>
|
||||
!config.EXCLUDED_FILES.some(pattern =>
|
||||
minimatch(file.filename, pattern),
|
||||
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,
|
||||
),
|
||||
)
|
||||
.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 totalChanges = modifiedFiles
|
||||
.filter(
|
||||
file =>
|
||||
!config.EXCLUDED_FILES.some(pattern =>
|
||||
minimatch(file.filename, pattern),
|
||||
),
|
||||
)
|
||||
.reduce((sum, file) => sum + file.additions + file.deletions, 0);
|
||||
|
||||
// Calculate points for reviewers based on PR size
|
||||
const prPoints = config.PR_REVIEW_POINT_TIERS.find(
|
||||
tier => totalChanges > tier.minChanges,
|
||||
).points;
|
||||
const isReleasePR = pr.title.match(/🔖.*\d+\.\d+\.\d+/);
|
||||
const prPoints =
|
||||
config.PR_REVIEW_POINT_TIERS.find(t => totalChanges >= t.minChanges)
|
||||
?.points ?? 0;
|
||||
|
||||
// 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(),
|
||||
},
|
||||
(response, done) =>
|
||||
response.data.filter(issue => new Date(issue.updated_at) <= until),
|
||||
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;
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// 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(),
|
||||
});
|
||||
|
||||
// Get label events for each issue
|
||||
for (const issue of issues) {
|
||||
const { data: events } = await octokit.issues.listEventsForTimeline({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
});
|
||||
await Promise.all(
|
||||
issues.map(issue =>
|
||||
limit(async () => {
|
||||
const { data: events } = await octokit.issues.listEventsForTimeline({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: issue.number,
|
||||
});
|
||||
|
||||
// Process events
|
||||
events
|
||||
.filter(
|
||||
event =>
|
||||
new Date(event.created_at) > since &&
|
||||
new Date(event.created_at) <= until &&
|
||||
stats.has(event.actor?.login),
|
||||
)
|
||||
.forEach(event => {
|
||||
if (
|
||||
event.event === 'unlabeled' &&
|
||||
event.label &&
|
||||
event.label.name.toLowerCase() === 'needs triage'
|
||||
) {
|
||||
const remover = event.actor.login;
|
||||
const userStats = stats.get(remover);
|
||||
userStats.labelRemovals.push(issue.number.toString());
|
||||
userStats.points += config.POINTS_PER_ISSUE_TRIAGE_ACTION;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Print all statistics
|
||||
printStats(
|
||||
|
||||
89
.github/workflows/ai-generated-release-notes.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
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 }}
|
||||
10
.github/workflows/docker-edge.yml
vendored
@@ -22,9 +22,9 @@ permissions:
|
||||
|
||||
env:
|
||||
IMAGES: |
|
||||
actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual-server
|
||||
ghcr.io/actualbudget/actual
|
||||
${{ !github.event.repository.fork && 'actualbudget/actual-server' || '' }}
|
||||
ghcr.io/${{ github.repository_owner }}/actual-server
|
||||
ghcr.io/${{ github.repository_owner }}/actual
|
||||
|
||||
# Creates the following tags:
|
||||
# - actual-server:edge
|
||||
@@ -34,7 +34,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ github.event.repository.fork == false }}
|
||||
if: github.event_name == 'workflow_dispatch' || !github.event.repository.fork
|
||||
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'
|
||||
if: github.event_name != 'pull_request' && !github.event.repository.fork
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
25
.github/workflows/generate-release-pr.yml
vendored
@@ -24,8 +24,29 @@ jobs:
|
||||
id: bump_package_versions
|
||||
shell: bash
|
||||
run: |
|
||||
.github/actions/bump-package-versions ${{ github.event.inputs.version }}
|
||||
echo "version=$(jq -r .version packages/desktop-client/package.json)" > $GITHUB_OUTPUT
|
||||
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 ./.github/actions/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"
|
||||
- name: Create PR
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
|
||||
@@ -10,6 +10,7 @@ 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
|
||||
|
||||
|
||||
2
.github/workflows/size-compare.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
base-stats-json-path: ./base/web-stats.json
|
||||
title: desktop-client
|
||||
|
||||
- uses: github/webpack-bundlesize-compare-action@v2.1.0
|
||||
- uses: twk3/rollup-size-compare-action@v1.1.1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
current-stats-json-path: ./head/loot-core-stats.json
|
||||
|
||||
3
.gitignore
vendored
@@ -60,3 +60,6 @@ fly.toml
|
||||
|
||||
# TypeScript cache
|
||||
build/
|
||||
|
||||
# .d.ts files aren't type-checked with skipLibCheck set to true
|
||||
*.d.ts
|
||||
|
||||
@@ -7,6 +7,8 @@ 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/
|
||||
@@ -23,5 +25,6 @@ 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/*
|
||||
|
||||
10
CODEOWNERS
Normal file
@@ -0,0 +1,10 @@
|
||||
# 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
|
||||
@@ -6,7 +6,7 @@ import prompts from 'prompts';
|
||||
|
||||
async function run() {
|
||||
const username = await execAsync(
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
// eslint-disable-next-line actual/typography
|
||||
"gh api user --jq '.login'",
|
||||
'To avoid having to enter your username, consider installing the official GitHub CLI (https://github.com/cli/cli) and logging in with `gh auth login`.',
|
||||
);
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
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',
|
||||
@@ -167,11 +153,15 @@ export default pluginTypescript.config(
|
||||
pluginImport.flatConfigs.recommended,
|
||||
{
|
||||
plugins: {
|
||||
actual: pluginActual,
|
||||
'react-hooks': pluginReactHooks,
|
||||
'jsx-a11y': pluginJSXA11y,
|
||||
rulesdir: pluginRulesDir,
|
||||
'typescript-paths': pluginTypescriptPaths,
|
||||
},
|
||||
rules: {
|
||||
'actual/no-untranslated-strings': 'error',
|
||||
'actual/prefer-trans-over-t': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,ts,jsx,tsx}'],
|
||||
@@ -458,8 +448,8 @@ export default pluginTypescript.config(
|
||||
},
|
||||
],
|
||||
|
||||
'rulesdir/typography': 'warn',
|
||||
'rulesdir/prefer-if-statement': 'warn',
|
||||
'actual/typography': 'warn',
|
||||
'actual/prefer-if-statement': 'warn',
|
||||
|
||||
// Note: base rule explicitly disabled in favor of the TS one
|
||||
'no-unused-vars': 'off',
|
||||
@@ -467,6 +457,7 @@ export default pluginTypescript.config(
|
||||
'warn',
|
||||
{
|
||||
varsIgnorePattern: '^(_|React)',
|
||||
argsIgnorePattern: '^(_|React)',
|
||||
ignoreRestSiblings: true,
|
||||
caughtErrors: 'none',
|
||||
},
|
||||
@@ -778,7 +769,8 @@ export default pluginTypescript.config(
|
||||
],
|
||||
|
||||
rules: {
|
||||
'rulesdir/typography': 'off',
|
||||
'actual/typography': 'off',
|
||||
'actual/no-untranslated-strings': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -797,7 +789,7 @@ export default pluginTypescript.config(
|
||||
// TODO: fix the issues in these files
|
||||
rules: {
|
||||
'import/extensions': 'off',
|
||||
'rulesdir/typography': 'off',
|
||||
'actual/typography': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -805,8 +797,6 @@ 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',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"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",
|
||||
@@ -54,7 +55,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/node": "^22.17.0",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.32.1",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -65,7 +66,6 @@
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"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,11 +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.8.3",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.32.1",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.6.1",
|
||||
"version": "25.9.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
@@ -24,14 +24,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^11.10.0",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"outDir": "dist",
|
||||
"declarationDir": "@types",
|
||||
"paths": {
|
||||
"loot-core/*": ["./@types/loot-core/*"]
|
||||
"loot-core/*": ["./@types/loot-core/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["."],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"@types/react": "^19.1.4",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"vitest": "^3.1.3"
|
||||
"vitest": "^3.2.4"
|
||||
},
|
||||
"exports": {
|
||||
"./hooks/*": "./src/hooks/*.ts",
|
||||
@@ -48,7 +48,8 @@
|
||||
"./tokens": "./src/tokens.ts",
|
||||
"./toggle": "./src/Toggle.tsx",
|
||||
"./tooltip": "./src/Tooltip.tsx",
|
||||
"./view": "./src/View.tsx"
|
||||
"./view": "./src/View.tsx",
|
||||
"./color-picker": "./src/ColorPicker.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"generate:icons": "rm src/icons/*/*.tsx; cd src/icons && svgr --template template.ts --index-template index-template.ts --typescript --expand-props start -d . .",
|
||||
|
||||
180
packages/component-library/src/ColorPicker.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { type CSSProperties, type ReactNode } from 'react';
|
||||
import React, { type ReactNode } from 'react';
|
||||
|
||||
import { type CSSProperties } from './styles';
|
||||
import { View } from './View';
|
||||
|
||||
type SpaceBetweenProps = {
|
||||
|
||||
@@ -62,7 +62,7 @@ export const Toggle = ({
|
||||
data-on={isOn}
|
||||
className={css(
|
||||
{
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
// eslint-disable-next-line actual/typography
|
||||
content: '" "',
|
||||
position: 'absolute',
|
||||
top: '2px',
|
||||
|
||||
@@ -27,8 +27,13 @@ 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);
|
||||
@@ -41,8 +46,10 @@ export const Tooltip = ({
|
||||
clearTimeout(hoverTimeoutRef.current);
|
||||
}
|
||||
|
||||
setIsHover(false);
|
||||
}, []);
|
||||
closeTimeoutRef.current = setTimeout(() => {
|
||||
setIsHover(false);
|
||||
}, triggerProps.closeDelay ?? 0);
|
||||
}, [triggerProps.closeDelay]);
|
||||
|
||||
// Force closing the tooltip whenever the disablement state changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -23,7 +23,11 @@ export const View = forwardRef<HTMLDivElement, ViewProps>((props, ref) => {
|
||||
{...restProps}
|
||||
ref={innerRef ?? ref}
|
||||
style={nativeStyle}
|
||||
className={cx('view', className, css(style))}
|
||||
className={cx(
|
||||
'view',
|
||||
className,
|
||||
style && Object.keys(style).length > 0 ? css(style) : undefined,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -91,7 +91,7 @@ export const styles: Record<string, any> = {
|
||||
},
|
||||
shadowLarge,
|
||||
tnum: {
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
// eslint-disable-next-line actual/typography
|
||||
fontFeatureSettings: '"tnum"',
|
||||
},
|
||||
notFixed: { fontFeatureSettings: '' },
|
||||
|
||||
@@ -188,6 +188,7 @@ 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)',
|
||||
|
||||
@@ -18,4 +18,9 @@ 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;`'
|
||||
|
||||
@@ -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 && cp src/proto/sync_pb.d.ts dist/src/proto/",
|
||||
"build": "rm -rf dist && yarn run build:node",
|
||||
"test": "vitest --globals"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20,8 +20,10 @@
|
||||
"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.8.3",
|
||||
"vitest": "^3.1.3"
|
||||
"typescript": "^5.9.2",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as SyncPb from './proto/sync_pb';
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import './proto/sync_pb.js'; // Import for side effects
|
||||
|
||||
export {
|
||||
merkle,
|
||||
getClock,
|
||||
@@ -11,4 +13,11 @@ export {
|
||||
Timestamp,
|
||||
} from './crdt';
|
||||
|
||||
export const SyncProtoBuf = SyncPb;
|
||||
// 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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
// package:
|
||||
// file: sync.proto
|
||||
|
||||
import * as jspb from 'google-protobuf';
|
||||
|
||||
export class EncryptedData extends jspb.Message {
|
||||
export declare class EncryptedData extends jspb.Message {
|
||||
getIv(): Uint8Array | string;
|
||||
getIv_asU8(): Uint8Array;
|
||||
getIv_asB64(): string;
|
||||
@@ -48,7 +49,7 @@ export namespace EncryptedData {
|
||||
};
|
||||
}
|
||||
|
||||
export class Message extends jspb.Message {
|
||||
export declare class Message extends jspb.Message {
|
||||
getDataset(): string;
|
||||
setDataset(value: string): void;
|
||||
|
||||
@@ -88,7 +89,7 @@ export namespace Message {
|
||||
};
|
||||
}
|
||||
|
||||
export class MessageEnvelope extends jspb.Message {
|
||||
export declare class MessageEnvelope extends jspb.Message {
|
||||
getTimestamp(): string;
|
||||
setTimestamp(value: string): void;
|
||||
|
||||
@@ -129,7 +130,7 @@ export namespace MessageEnvelope {
|
||||
};
|
||||
}
|
||||
|
||||
export class SyncRequest extends jspb.Message {
|
||||
export declare class SyncRequest extends jspb.Message {
|
||||
clearMessagesList(): void;
|
||||
getMessagesList(): Array<MessageEnvelope>;
|
||||
setMessagesList(value: Array<MessageEnvelope>): void;
|
||||
@@ -178,7 +179,7 @@ export namespace SyncRequest {
|
||||
};
|
||||
}
|
||||
|
||||
export class SyncResponse extends jspb.Message {
|
||||
export declare class SyncResponse extends jspb.Message {
|
||||
clearMessagesList(): void;
|
||||
getMessagesList(): Array<MessageEnvelope>;
|
||||
setMessagesList(value: Array<MessageEnvelope>): void;
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 26 KiB |