Compare commits
33 Commits
ai/sync-se
...
feature/en
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0a4d21b2 | ||
|
|
8289865b66 | ||
|
|
7762a07a57 | ||
|
|
2ec3779b33 | ||
|
|
3958933a26 | ||
|
|
89bea19dfd | ||
|
|
fd259a5b4c | ||
|
|
4ac722e0c0 | ||
|
|
f3272c74a6 | ||
|
|
cbe15de31d | ||
|
|
298c3b9773 | ||
|
|
4042d32663 | ||
|
|
b9f5121d18 | ||
|
|
a90889b4de | ||
|
|
14174e6c2f | ||
|
|
beee8ee518 | ||
|
|
744cba7a0a | ||
|
|
1f379b6e4c | ||
|
|
f36a8880bf | ||
|
|
327469411a | ||
|
|
31893074a6 | ||
|
|
0cafb4acbc | ||
|
|
e8b6366816 | ||
|
|
e4b9d9c94e | ||
|
|
9403f57e6f | ||
|
|
5661ab7a6f | ||
|
|
8658b889ec | ||
|
|
cc544222da | ||
|
|
b13d04cc4a | ||
|
|
dc62a6aff7 | ||
|
|
1cbe1efbf4 | ||
|
|
33619dfc1d | ||
|
|
d8863a8d16 |
@@ -1,7 +1,7 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||
{
|
||||
"name": "Actual Devcontainer",
|
||||
"name": "Actual development",
|
||||
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
|
||||
1
.github/actions/docs-spelling/expect.txt
vendored
@@ -44,7 +44,6 @@ CLP
|
||||
CMCIFRPAXXX
|
||||
COBADEFF
|
||||
CODEOWNERS
|
||||
Codespaces
|
||||
COEP
|
||||
commerzbank
|
||||
Copiar
|
||||
|
||||
11
.github/actions/setup/action.yml
vendored
@@ -10,10 +10,6 @@ inputs:
|
||||
description: 'Whether to download translations as part of setup, default true'
|
||||
required: false
|
||||
default: 'true'
|
||||
cache:
|
||||
description: 'Whether to restore and save dependency and Lage caches, default true'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -22,7 +18,6 @@ runs:
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
package-manager-cache: ${{ inputs.cache }}
|
||||
- name: Install yarn
|
||||
run: npm install -g yarn
|
||||
shell: bash
|
||||
@@ -33,19 +28,15 @@ runs:
|
||||
shell: bash
|
||||
- name: Cache
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
if: ${{ inputs.cache == 'true' }}
|
||||
id: cache
|
||||
with:
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
key: yarn-v1-${{ runner.os }}-${{ steps.get-node.outputs.version }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
- name: Ensure Lage cache directory exists
|
||||
run: mkdir -p "$WORKING_DIRECTORY/.lage"
|
||||
run: mkdir -p ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
shell: bash
|
||||
env:
|
||||
WORKING_DIRECTORY: ${{ inputs.working-directory }}
|
||||
- name: Cache Lage
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
if: ${{ inputs.cache == 'true' }}
|
||||
with:
|
||||
path: ${{ format('{0}/.lage', inputs.working-directory) }}
|
||||
key: lage-${{ runner.os }}-${{ github.sha }}
|
||||
|
||||
27
.github/workflows/ai-generated-label.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: Add 'AI generated' label to '[AI]' PRs
|
||||
|
||||
##########################################################################################
|
||||
# This workflow uses the 'pull_request_target' event so it has a token that can add a #
|
||||
# label to PRs from forks. It does NOT check out or execute any code from the PR, so it #
|
||||
# is not vulnerable to the usual 'pull_request_target' code-injection concerns. Keep it #
|
||||
# that way - do not add a checkout step or run any PR-provided scripts here. #
|
||||
##########################################################################################
|
||||
|
||||
on:
|
||||
# This workflow never checks out or runs PR code; it only reads the PR title and adds a label.
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers]
|
||||
types: [opened, reopened, edited]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
add-ai-generated-label:
|
||||
name: Add 'AI generated' label
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.event.pull_request.title, '[AI]')
|
||||
steps:
|
||||
- uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 # v1.1.0
|
||||
with:
|
||||
labels: AI generated
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -9,7 +9,6 @@ jobs:
|
||||
# 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
|
||||
environment: ai-release-notes
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
3
.github/workflows/build.yml
vendored
@@ -14,9 +14,6 @@ on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
||||
|
||||
3
.github/workflows/check.yml
vendored
@@ -7,9 +7,6 @@ on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
|
||||
|
||||
5
.github/workflows/count-points.yml
vendored
@@ -11,11 +11,6 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
count-points:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
48
.github/workflows/crdt-version-check.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: CRDT version bump check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'packages/crdt/**'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-version-bump:
|
||||
runs-on: ubuntu-latest
|
||||
name: Ensure @actual-app/crdt version is bumped
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Verify version bump
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if ! git cat-file -e "origin/${BASE_REF}:packages/crdt/package.json" 2>/dev/null; then
|
||||
echo "packages/crdt/package.json does not exist on the base branch; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
BASE_VERSION=$(git show "origin/${BASE_REF}:packages/crdt/package.json" | jq -r .version)
|
||||
HEAD_VERSION=$(jq -r .version packages/crdt/package.json)
|
||||
echo "Base version: $BASE_VERSION"
|
||||
echo "Head version: $HEAD_VERSION"
|
||||
|
||||
if [ "$BASE_VERSION" = "$HEAD_VERSION" ]; then
|
||||
echo "::error file=packages/crdt/package.json::Files in packages/crdt/ were modified but the @actual-app/crdt version was not bumped. Please update the \"version\" field in packages/crdt/package.json."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HIGHEST=$(printf '%s\n%s\n' "$BASE_VERSION" "$HEAD_VERSION" | sort -V | tail -n1)
|
||||
if [ "$HIGHEST" != "$HEAD_VERSION" ]; then
|
||||
echo "::error file=packages/crdt/package.json::The @actual-app/crdt version ($HEAD_VERSION) must be greater than the base version ($BASE_VERSION)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Version bumped from $BASE_VERSION to $HEAD_VERSION."
|
||||
2
.github/workflows/cut-release-branch.yml
vendored
@@ -37,8 +37,6 @@ jobs:
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
download-translations: 'false'
|
||||
|
||||
- name: Bump package versions
|
||||
|
||||
14
.github/workflows/docker-edge.yml
vendored
@@ -75,9 +75,6 @@ jobs:
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
@@ -91,15 +88,10 @@ jobs:
|
||||
tags: actualbudget/actual-server-testing
|
||||
|
||||
- name: Test that the docker image boots
|
||||
timeout-minutes: 1
|
||||
run: |
|
||||
docker run --detach --network=host --name actual-server actualbudget/actual-server-testing
|
||||
HEALTHCMD=$(yq -r '.services.actual_server.healthcheck.test[1]' packages/sync-server/docker-compose.yml)
|
||||
until docker exec actual-server sh -c "$HEALTHCMD"; do sleep 1; done
|
||||
|
||||
- name: Dump container logs on failure
|
||||
if: failure()
|
||||
run: docker logs actual-server || true
|
||||
docker run --detach --network=host actualbudget/actual-server-testing
|
||||
sleep 10
|
||||
curl --fail -sS -LI -w '%{http_code}\n' --retry 20 --retry-delay 1 --retry-connrefused localhost:5006
|
||||
|
||||
# This will use the cache from the earlier build step and not rebuild the image
|
||||
# https://docs.docker.com/build/ci/github-actions/test-before-push/
|
||||
|
||||
45
.github/workflows/docker-release.yml
vendored
@@ -23,10 +23,6 @@ env:
|
||||
TAGS: |
|
||||
type=semver,pattern={{version}}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Docker image
|
||||
@@ -81,29 +77,9 @@ jobs:
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
- name: Build Web
|
||||
run: yarn build:server
|
||||
|
||||
- name: Build ubuntu image for testing
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
load: true
|
||||
file: packages/sync-server/docker/ubuntu.Dockerfile
|
||||
tags: actualbudget/actual-server-testing
|
||||
|
||||
- name: Test that the ubuntu image boots
|
||||
timeout-minutes: 1
|
||||
run: |
|
||||
docker rm -f actual-server 2>/dev/null || true
|
||||
docker run --detach --network=host --name actual-server actualbudget/actual-server-testing
|
||||
HEALTHCMD=$(yq -r '.services.actual_server.healthcheck.test[1]' packages/sync-server/docker-compose.yml)
|
||||
until docker exec actual-server sh -c "$HEALTHCMD"; do sleep 1; done
|
||||
|
||||
- name: Build and push ubuntu image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
@@ -113,23 +89,6 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
- name: Build alpine image for testing
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
load: true
|
||||
file: packages/sync-server/docker/alpine.Dockerfile
|
||||
tags: actualbudget/actual-server-testing
|
||||
|
||||
- name: Test that the alpine image boots
|
||||
timeout-minutes: 1
|
||||
run: |
|
||||
docker rm -f actual-server 2>/dev/null || true
|
||||
docker run --detach --network=host --name actual-server actualbudget/actual-server-testing
|
||||
HEALTHCMD=$(yq -r '.services.actual_server.healthcheck.test[1]' packages/sync-server/docker-compose.yml)
|
||||
until docker exec actual-server sh -c "$HEALTHCMD"; do sleep 1; done
|
||||
|
||||
- name: Build and push alpine image
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
@@ -138,7 +97,3 @@ jobs:
|
||||
file: packages/sync-server/docker/alpine.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
tags: ${{ steps.alpine-meta.outputs.tags }}
|
||||
|
||||
- name: Dump container logs on failure
|
||||
if: failure()
|
||||
run: docker logs actual-server || true
|
||||
|
||||
1
.github/workflows/docs-spelling.yml
vendored
@@ -146,7 +146,6 @@ jobs:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
runs-on: ubuntu-latest
|
||||
environment: docs-spelling
|
||||
if: ${{
|
||||
github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
|
||||
6
.github/workflows/e2e-test.yml
vendored
@@ -199,13 +199,11 @@ jobs:
|
||||
if: github.event_name == 'pull_request'
|
||||
run: |
|
||||
mkdir -p vrt-metadata
|
||||
echo "${PR_NUMBER}" > vrt-metadata/pr-number.txt
|
||||
echo "${VRT_RESULT}" > vrt-metadata/vrt-result.txt
|
||||
echo "${{ github.event.pull_request.number }}" > vrt-metadata/pr-number.txt
|
||||
echo "${{ needs.vrt.result }}" > vrt-metadata/vrt-result.txt
|
||||
echo "${STEPS_PLAYWRIGHT_REPORT_VRT_OUTPUTS_ARTIFACT_URL}" > vrt-metadata/artifact-url.txt
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
STEPS_PLAYWRIGHT_REPORT_VRT_OUTPUTS_ARTIFACT_URL: ${{ steps.playwright-report-vrt.outputs.artifact-url }}
|
||||
VRT_RESULT: ${{ needs.vrt.result }}
|
||||
- name: Upload VRT metadata
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
|
||||
76
.github/workflows/electron-master.yml
vendored
@@ -67,9 +67,6 @@ jobs:
|
||||
STEPS_PROCESS_VERSION_OUTPUTS_VERSION: ${{ steps.process_version.outputs.version }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
- name: Build Electron for Mac
|
||||
if: ${{ startsWith(matrix.os, 'macos') }}
|
||||
run: ./bin/package-electron
|
||||
@@ -100,11 +97,10 @@ jobs:
|
||||
path: |
|
||||
packages/desktop-electron/dist/*.appx
|
||||
- name: Add to new release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
RELEASE_NOTES: |
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
draft: true
|
||||
body: |
|
||||
:link: [View release notes](https://actualbudget.org/blog/release-${{ steps.process_version.outputs.version }})
|
||||
|
||||
## Desktop releases
|
||||
@@ -115,27 +111,55 @@ jobs:
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAAAAACw=" width="12" height="1" alt="" />
|
||||
<a href="https://flathub.org/apps/com.actualbudget.actual"><img width="165" style="margin-left:12px;" alt="Get it on Flathub" src="https://flathub.org/api/badge?locale=en" /></a>
|
||||
</p>
|
||||
run: |
|
||||
# The matrix runs three OS jobs in parallel against one release;
|
||||
# only ignore the "already exists" error that the race losers hit.
|
||||
if ! create_output=$(gh release create "$TAG" --draft --title "$TAG" --notes "$RELEASE_NOTES" 2>&1); then
|
||||
if [[ "$create_output" != *already_exists* ]]; then
|
||||
echo "$create_output" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
shopt -s extglob nullglob
|
||||
files=(
|
||||
files: |
|
||||
packages/desktop-electron/dist/*.dmg
|
||||
packages/desktop-electron/dist/!(Actual-windows).exe
|
||||
packages/desktop-electron/dist/*.exe
|
||||
!packages/desktop-electron/dist/Actual-windows.exe
|
||||
packages/desktop-electron/dist/*.AppImage
|
||||
packages/desktop-electron/dist/*.flatpak
|
||||
packages/desktop-electron/dist/*.appx
|
||||
)
|
||||
if [ ${#files[@]} -gt 0 ]; then
|
||||
gh release upload "$TAG" --clobber "${files[@]}"
|
||||
fi
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.process_version.outputs.version }}
|
||||
|
||||
publish-microsoft-store:
|
||||
needs: build
|
||||
runs-on: windows-latest
|
||||
environment: release
|
||||
if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
steps:
|
||||
- name: Install StoreBroker
|
||||
shell: powershell
|
||||
run: |
|
||||
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
|
||||
|
||||
- name: Download Microsoft Store artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: actual-electron-windows-latest-appx
|
||||
|
||||
- name: Submit to Microsoft Store
|
||||
shell: powershell
|
||||
run: |
|
||||
# Disable telemetry
|
||||
$global:SBDisableTelemetry = $true
|
||||
|
||||
# Authenticate against the store
|
||||
$pass = ConvertTo-SecureString -String '${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}' -AsPlainText -Force
|
||||
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ${{ secrets.MICROSOFT_STORE_CLIENT_ID }},$pass
|
||||
Set-StoreBrokerAuthentication -TenantId '${{ secrets.MICROSOFT_STORE_TENANT_ID }}' -Credential $cred
|
||||
|
||||
# Zip and create metadata files
|
||||
$artifacts = Get-ChildItem -Path . -Filter *.appx | Select-Object -ExpandProperty FullName
|
||||
New-StoreBrokerConfigFile -Path "$PWD/config.json" -AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }}
|
||||
New-SubmissionPackage -ConfigPath "$PWD/config.json" -DisableAutoPackageNameFormatting -AppxPath $artifacts -OutPath "$PWD" -OutName submission
|
||||
|
||||
# Submit the app
|
||||
# See https://github.com/microsoft/StoreBroker/blob/master/Documentation/USAGE.md#the-easy-way
|
||||
Update-ApplicationSubmission `
|
||||
-AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }} `
|
||||
-SubmissionDataPath "submission.json" `
|
||||
-PackagePath "submission.zip" `
|
||||
-ReplacePackages `
|
||||
-NoStatus `
|
||||
-AutoCommit `
|
||||
-Force
|
||||
|
||||
3
.github/workflows/electron-pr.yml
vendored
@@ -19,9 +19,6 @@ on:
|
||||
- '!packages/docs/**' # Docs changes don't affect Electron
|
||||
- '!packages/eslint-plugin-actual/**' # Eslint plugin changes don't affect Electron
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
@@ -6,13 +6,9 @@ on:
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
extract-and-upload-i18n-strings:
|
||||
runs-on: ubuntu-latest
|
||||
environment: i18n
|
||||
if: github.repository == 'actualbudget/actual'
|
||||
steps:
|
||||
- name: Check out main repository
|
||||
|
||||
@@ -4,28 +4,25 @@ on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
needs-votes:
|
||||
if: ${{ github.event.label.name == 'feature' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
steps:
|
||||
- name: Add needs votes label
|
||||
run: gh issue edit "$ISSUE_NUMBER" --add-label "needs votes"
|
||||
- uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 # v1.1.0
|
||||
with:
|
||||
labels: needs votes
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Add reactions
|
||||
uses: aidan-mundy/react-to-issue@109392cac5159c2df6c47c8ab3b5d6b708852fe5 # v1.1.2
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
reactions: '+1'
|
||||
- name: Create comment
|
||||
env:
|
||||
COMMENT_BODY: |
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
:sparkles: Thanks for sharing your idea! :sparkles:
|
||||
|
||||
This repository uses a voting-based system for feature requests. While enhancement issues are automatically closed, we still welcome feature requests! The voting system helps us gauge community interest in potential features. We also encourage community contributions for any feature requests marked as needing votes (just post a comment first so we can help guide you toward a successful contribution).
|
||||
@@ -35,6 +32,7 @@ jobs:
|
||||
Don't forget to upvote the top comment with 👍!
|
||||
|
||||
<!-- feature-auto-close-comment -->
|
||||
run: gh issue comment "$ISSUE_NUMBER" --body "$COMMENT_BODY"
|
||||
- name: Close Issue
|
||||
run: gh issue close "$ISSUE_NUMBER"
|
||||
run: gh issue close "https://github.com/actualbudget/actual/issues/${{ github.event.issue.number }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -4,9 +4,6 @@ on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
tech-support:
|
||||
if: ${{ github.event.label.name == 'tech-support' }}
|
||||
|
||||
@@ -4,9 +4,6 @@ on:
|
||||
issues:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
remove-help-wanted:
|
||||
if: ${{ !contains(github.event.issue.labels.*.name, 'feature') && contains(github.event.issue.labels.*.name, 'help wanted') }}
|
||||
|
||||
37
.github/workflows/merge-freeze-unfreeze.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# When the "unfreeze" label is added to a PR, add that PR to Merge Freeze's unblocked list
|
||||
# so it can be merged during a freeze. Uses pull_request_target so the workflow runs in
|
||||
# the base repo and has access to MERGEFREEZE_ACCESS_TOKEN for fork PRs; it does not
|
||||
# checkout or run any PR code. Requires MERGEFREEZE_ACCESS_TOKEN repo secret
|
||||
# (project-specific token from Merge Freeze Web API panel for actualbudget/actual / master).
|
||||
# See: https://docs.mergefreeze.com/web-api#post-freeze-status
|
||||
|
||||
name: Merge Freeze – add PR to unblocked list
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
unfreeze:
|
||||
if: ${{ github.event.label.name == 'unfreeze' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
concurrency:
|
||||
group: merge-freeze-unfreeze-${{ github.ref }}-labels
|
||||
cancel-in-progress: false
|
||||
steps:
|
||||
- name: POST to Merge Freeze – add PR to unblocked list
|
||||
env:
|
||||
MERGEFREEZE_ACCESS_TOKEN: ${{ secrets.MERGEFREEZE_ACCESS_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
USER_NAME: ${{ github.actor }}
|
||||
run: |
|
||||
set -e
|
||||
if [ -z "$MERGEFREEZE_ACCESS_TOKEN" ]; then
|
||||
echo "::error::MERGEFREEZE_ACCESS_TOKEN secret is not set"
|
||||
exit 1
|
||||
fi
|
||||
url="https://www.mergefreeze.com/api/branches/actualbudget/actual/master/?access_token=${MERGEFREEZE_ACCESS_TOKEN}"
|
||||
payload=$(jq -n --arg user_name "$USER_NAME" --argjson pr "$PR_NUMBER" '{frozen: true, user_name: $user_name, unblocked_prs: [$pr]}')
|
||||
curl -sf -X POST "$url" -H "Content-Type: application/json" -d "$payload"
|
||||
echo "Merge Freeze updated: PR #$PR_NUMBER added to unblocked list."
|
||||
6
.github/workflows/netlify-release.yml
vendored
@@ -12,9 +12,6 @@ on:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
@@ -31,9 +28,6 @@ jobs:
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
|
||||
- name: Install Netlify
|
||||
run: npm install netlify-cli@17.10.1 -g
|
||||
|
||||
86
.github/workflows/publish-crdt.yml
vendored
@@ -1,86 +0,0 @@
|
||||
name: Publish @actual-app/crdt
|
||||
|
||||
# Automatically publishes @actual-app/crdt when its package.json version
|
||||
# changes on master (typically via a merged PR that bumped the version).
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'packages/crdt/package.json'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-crdt
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check if publish is needed
|
||||
outputs:
|
||||
should-publish: ${{ steps.check.outputs.should-publish }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Compare local version against npm registry
|
||||
id: check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
LOCAL_VERSION=$(jq -r .version packages/crdt/package.json)
|
||||
echo "Local version: $LOCAL_VERSION"
|
||||
|
||||
PUBLISHED_VERSION=$(npm view @actual-app/crdt version 2>/dev/null || echo "")
|
||||
echo "Published version: ${PUBLISHED_VERSION:-<none>}"
|
||||
|
||||
if [ "$LOCAL_VERSION" = "$PUBLISHED_VERSION" ]; then
|
||||
echo "Versions match - nothing to publish."
|
||||
echo "should-publish=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "Version changed - publish required."
|
||||
echo "should-publish=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
publish:
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should-publish == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish @actual-app/crdt to npm
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for npm OIDC provenance
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
download-translations: 'false'
|
||||
|
||||
- name: Build @actual-app/crdt
|
||||
run: yarn workspace @actual-app/crdt build
|
||||
|
||||
- name: Pack @actual-app/crdt
|
||||
run: yarn workspace @actual-app/crdt pack --filename @actual-app/crdt.tgz
|
||||
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
package-manager-cache: false
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish to npm
|
||||
run: npm publish packages/crdt/@actual-app/crdt.tgz --access public --provenance
|
||||
3
.github/workflows/publish-flathub.yml
vendored
@@ -18,9 +18,6 @@ concurrency:
|
||||
group: publish-flathub
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish-flathub:
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
116
.github/workflows/publish-microsoft-store.yml
vendored
@@ -1,116 +0,0 @@
|
||||
name: Publish Microsoft Store
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag (e.g. v25.3.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: publish-microsoft-store
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
publish-microsoft-store:
|
||||
runs-on: windows-latest
|
||||
environment: release
|
||||
steps:
|
||||
- name: Resolve version
|
||||
id: resolve_version
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
if [[ "$EVENT_NAME" == "release" ]]; then
|
||||
TAG="$RELEASE_TAG"
|
||||
else
|
||||
TAG="$INPUT_TAG"
|
||||
fi
|
||||
|
||||
if [[ -z "$TAG" ]]; then
|
||||
echo "::error::No tag provided"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate tag format (v-prefixed semver, e.g. v25.3.0 or v1.2.3-beta.1)
|
||||
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then
|
||||
echo "::error::Invalid tag format: $TAG (expected v-prefixed semver, e.g. v25.3.0)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION="${TAG#v}"
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved tag=$TAG version=$VERSION"
|
||||
|
||||
- name: Verify release assets exist
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
STEPS_RESOLVE_VERSION_OUTPUTS_TAG: ${{ steps.resolve_version.outputs.tag }}
|
||||
run: |
|
||||
TAG="${STEPS_RESOLVE_VERSION_OUTPUTS_TAG}"
|
||||
|
||||
echo "Checking release assets for tag $TAG..."
|
||||
ASSETS=$(gh api "repos/${{ github.repository }}/releases/tags/$TAG" --jq '.assets[].name')
|
||||
|
||||
echo "Found assets:"
|
||||
echo "$ASSETS"
|
||||
|
||||
if ! echo "$ASSETS" | grep -q "\.appx$"; then
|
||||
echo "::error::No .appx assets found in release $TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Required .appx assets found."
|
||||
|
||||
- name: Download Microsoft Store artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
STEPS_RESOLVE_VERSION_OUTPUTS_TAG: ${{ steps.resolve_version.outputs.tag }}
|
||||
run: |
|
||||
TAG="${STEPS_RESOLVE_VERSION_OUTPUTS_TAG}"
|
||||
gh release download "$TAG" --repo "${{ github.repository }}" --pattern "*.appx"
|
||||
|
||||
- name: Install StoreBroker
|
||||
shell: powershell
|
||||
run: |
|
||||
Install-Module -Name StoreBroker -AcceptLicense -Force -Scope CurrentUser -Verbose
|
||||
|
||||
- name: Submit to Microsoft Store
|
||||
shell: powershell
|
||||
run: |
|
||||
# Disable telemetry
|
||||
$global:SBDisableTelemetry = $true
|
||||
|
||||
# Authenticate against the store
|
||||
$pass = ConvertTo-SecureString -String '${{ secrets.MICROSOFT_STORE_CLIENT_SECRET }}' -AsPlainText -Force
|
||||
$cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ${{ secrets.MICROSOFT_STORE_CLIENT_ID }},$pass
|
||||
Set-StoreBrokerAuthentication -TenantId '${{ secrets.MICROSOFT_STORE_TENANT_ID }}' -Credential $cred
|
||||
|
||||
# Zip and create metadata files
|
||||
$artifacts = Get-ChildItem -Path . -Filter *.appx | Select-Object -ExpandProperty FullName
|
||||
New-StoreBrokerConfigFile -Path "$PWD/config.json" -AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }}
|
||||
New-SubmissionPackage -ConfigPath "$PWD/config.json" -DisableAutoPackageNameFormatting -AppxPath $artifacts -OutPath "$PWD" -OutName submission
|
||||
|
||||
# Submit the app
|
||||
# See https://github.com/microsoft/StoreBroker/blob/master/Documentation/USAGE.md#the-easy-way
|
||||
Update-ApplicationSubmission `
|
||||
-AppId ${{ secrets.MICROSOFT_STORE_PRODUCT_ID }} `
|
||||
-SubmissionDataPath "submission.json" `
|
||||
-PackagePath "submission.zip" `
|
||||
-ReplacePackages `
|
||||
-NoStatus `
|
||||
-AutoCommit `
|
||||
-Force
|
||||
@@ -13,9 +13,6 @@ defaults:
|
||||
env:
|
||||
CI: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
@@ -48,9 +45,6 @@ jobs:
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
|
||||
- if: ${{ startsWith(matrix.os, 'ubuntu') }}
|
||||
name: Setup Flatpak dependencies
|
||||
|
||||
8
.github/workflows/publish-npm-packages.yml
vendored
@@ -9,9 +9,6 @@ on:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-and-pack:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -24,9 +21,6 @@ jobs:
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
with:
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
cache: 'false'
|
||||
|
||||
- name: Update package versions
|
||||
if: github.event_name != 'push'
|
||||
@@ -111,8 +105,6 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
check-latest: true
|
||||
# Avoid restoring potentially poisoned caches in release jobs.
|
||||
package-manager-cache: false
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Core
|
||||
|
||||
1
.github/workflows/release-notes.yml
vendored
@@ -14,7 +14,6 @@ concurrency:
|
||||
jobs:
|
||||
release-notes:
|
||||
runs-on: ubuntu-latest
|
||||
environment: pr-automation
|
||||
steps:
|
||||
- name: Check if triggered by bot
|
||||
id: bot-check
|
||||
|
||||
145
.github/workflows/size-compare.yml
vendored
@@ -33,7 +33,6 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
actions: read
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -45,120 +44,140 @@ jobs:
|
||||
with:
|
||||
download-translations: 'false'
|
||||
|
||||
# Resolve one successful `build.yml` run for each side (master and PR
|
||||
# head) up front, then pin every download below to its `run_id`. This
|
||||
# ensures artifact downloads are consistent and prevents race conditions.
|
||||
- name: Resolve build runs
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
id: build-runs
|
||||
env:
|
||||
BASE_REF: ${{ github.base_ref }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
- name: Wait for ${{github.base_ref}} web build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: master-web-build
|
||||
with:
|
||||
script: |
|
||||
const TIMEOUT_MS = 30 * 60 * 1000;
|
||||
const SLEEP_MS = 15000;
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: web
|
||||
ref: ${{github.base_ref}}
|
||||
- name: Wait for ${{github.base_ref}} API build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: master-api-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: api
|
||||
ref: ${{github.base_ref}}
|
||||
- name: Wait for ${{github.base_ref}} CLI build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: master-cli-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: cli
|
||||
ref: ${{github.base_ref}}
|
||||
- name: Wait for ${{github.base_ref}} CRDT build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: master-crdt-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: crdt
|
||||
ref: ${{github.base_ref}}
|
||||
|
||||
async function resolveRun({ label, filter, notFoundHint }) {
|
||||
const deadline = Date.now() + TIMEOUT_MS;
|
||||
while (true) {
|
||||
const { data } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'build.yml',
|
||||
...filter,
|
||||
status: 'success',
|
||||
per_page: 1,
|
||||
});
|
||||
if (data.workflow_runs.length > 0) {
|
||||
const run = data.workflow_runs[0];
|
||||
core.info(`Found ${label} build run ${run.id} (${run.html_url})`);
|
||||
return run.id;
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(
|
||||
`No successful build.yml run found for ${label} within 30 min — ${notFoundHint}.`,
|
||||
);
|
||||
}
|
||||
core.info(`No successful ${label} build run yet — sleeping 15s.`);
|
||||
await new Promise(r => setTimeout(r, SLEEP_MS));
|
||||
}
|
||||
}
|
||||
- name: Wait for PR build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: wait-for-web-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: web
|
||||
ref: ${{github.event.pull_request.head.sha}}
|
||||
- name: Wait for API PR build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: wait-for-api-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: api
|
||||
ref: ${{github.event.pull_request.head.sha}}
|
||||
- name: Wait for CLI PR build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: wait-for-cli-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: cli
|
||||
ref: ${{github.event.pull_request.head.sha}}
|
||||
- name: Wait for CRDT PR build to succeed
|
||||
uses: fountainhead/action-wait-for-check@5a908a24814494009c4bb27c242ea38c93c593be # v1.2.0
|
||||
id: wait-for-crdt-build
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
checkName: crdt
|
||||
ref: ${{github.event.pull_request.head.sha}}
|
||||
|
||||
const baseRef = process.env.BASE_REF;
|
||||
const headSha = process.env.HEAD_SHA;
|
||||
const [masterRunId, headRunId] = await Promise.all([
|
||||
resolveRun({
|
||||
label: baseRef,
|
||||
filter: { branch: baseRef },
|
||||
notFoundHint: `${baseRef} may be broken`,
|
||||
}),
|
||||
resolveRun({
|
||||
label: `PR head ${headSha}`,
|
||||
filter: { head_sha: headSha },
|
||||
notFoundHint:
|
||||
'build may still be running, have failed, or the branch may have been force-pushed',
|
||||
}),
|
||||
]);
|
||||
core.setOutput('master_run_id', masterRunId);
|
||||
core.setOutput('head_run_id', headRunId);
|
||||
- name: Report build failure
|
||||
if: steps.wait-for-web-build.outputs.conclusion == 'failure' || steps.wait-for-api-build.outputs.conclusion == 'failure' || steps.wait-for-cli-build.outputs.conclusion == 'failure' || steps.wait-for-crdt-build.outputs.conclusion == 'failure'
|
||||
run: |
|
||||
echo "Build failed on PR branch or ${GITHUB_BASE_REF}"
|
||||
exit 1
|
||||
|
||||
- name: Download web build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
id: pr-web-build
|
||||
with:
|
||||
run_id: ${{ steps.build-runs.outputs.master_run_id }}
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: base
|
||||
- name: Download API build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
id: pr-api-build
|
||||
with:
|
||||
run_id: ${{ steps.build-runs.outputs.master_run_id }}
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: api-build-stats
|
||||
path: base
|
||||
- name: Download build stats from PR
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
run_id: ${{ steps.build-runs.outputs.head_run_id }}
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
- name: Download API stats from PR
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
run_id: ${{ steps.build-runs.outputs.head_run_id }}
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: api-build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
- name: Download CLI build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
run_id: ${{ steps.build-runs.outputs.master_run_id }}
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: cli-build-stats
|
||||
path: base
|
||||
- name: Download CLI stats from PR
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
run_id: ${{ steps.build-runs.outputs.head_run_id }}
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: cli-build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
- name: Download CRDT build artifact from ${{github.base_ref}}
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
run_id: ${{ steps.build-runs.outputs.master_run_id }}
|
||||
branch: ${{github.base_ref}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: crdt-build-stats
|
||||
path: base
|
||||
- name: Download CRDT stats from PR
|
||||
uses: dawidd6/action-download-artifact@8305c0f1062bb0d184d09ef4493ecb9288447732 # v20
|
||||
with:
|
||||
run_id: ${{ steps.build-runs.outputs.head_run_id }}
|
||||
pr: ${{github.event.pull_request.number}}
|
||||
workflow: build.yml
|
||||
workflow_conclusion: '' # ignore the conclusion of the workflow, since we already checked it
|
||||
name: crdt-build-stats
|
||||
path: head
|
||||
allow_forks: true
|
||||
- name: Strip content hashes from stats files
|
||||
run: |
|
||||
if [ -f ./head/web-stats.json ]; then
|
||||
|
||||
1
.github/workflows/vrt-update-apply.yml
vendored
@@ -16,7 +16,6 @@ jobs:
|
||||
apply-vrt-updates:
|
||||
name: Apply VRT Updates
|
||||
runs-on: ubuntu-latest
|
||||
environment: pr-automation
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download patch artifact
|
||||
|
||||
25
.github/workflows/vrt-update-generate.yml
vendored
@@ -82,17 +82,16 @@ jobs:
|
||||
with:
|
||||
download-translations: 'false'
|
||||
- name: Build browser bundle
|
||||
# REACT_APP_NETLIFY=true flips isNonProductionEnvironment() on in the
|
||||
# bundle so the "Create test file" button (used by every e2e beforeEach
|
||||
# via ConfigurationPage.createTestFile()) is still rendered in a
|
||||
# production build. Without it, e2e tests would time out waiting for
|
||||
# a button that was tree-shaken out.
|
||||
# --skip-translations keeps VRT screenshots deterministic by rendering
|
||||
# source-code English instead of upstream Weblate en.json (which can
|
||||
# drift between snapshot capture and test runs).
|
||||
# REACT_APP_NETLIFY=true keeps the "Create test file" button in the
|
||||
# production bundle — every VRT test's beforeEach relies on it via
|
||||
# ConfigurationPage.createTestFile().
|
||||
env:
|
||||
REACT_APP_NETLIFY: 'true'
|
||||
run: yarn build:browser --skip-translations
|
||||
run: |
|
||||
yarn workspace plugins-service build
|
||||
yarn workspace @actual-app/crdt build
|
||||
yarn workspace @actual-app/core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
- name: Upload build artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
@@ -258,17 +257,13 @@ jobs:
|
||||
|
||||
- name: Merge shard patches
|
||||
id: create-patch
|
||||
shell: bash
|
||||
run: |
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# actions/download-artifact puts a lone matched artifact directly in
|
||||
# `path` but gives each of several its own `path/<name>/` subdir, so
|
||||
# recurse instead of globbing `*/vrt-shard.patch` (which would miss
|
||||
# the common single-shard case).
|
||||
mapfile -t patches < <(find /tmp/shard-patches -type f -name 'vrt-shard.patch' | sort)
|
||||
shopt -s nullglob
|
||||
patches=(/tmp/shard-patches/*/vrt-shard.patch)
|
||||
|
||||
if [ ${#patches[@]} -eq 0 ]; then
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
"vi": "readonly",
|
||||
"backend": "readonly",
|
||||
"importScripts": "readonly",
|
||||
"FS": "readonly",
|
||||
"__APP_VERSION__": "readonly"
|
||||
"FS": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
// Import sorting
|
||||
|
||||
@@ -7,7 +7,3 @@ enableTransparentWorkspaces: false
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.13.0.cjs
|
||||
|
||||
# Secure default: don't run postinstall scripts.
|
||||
# If a new package requires them, add it to dependenciesMeta in package.json.
|
||||
enableScripts: false
|
||||
|
||||
19
package.json
@@ -52,7 +52,7 @@
|
||||
"playwright": "yarn workspace @actual-app/web run playwright",
|
||||
"vrt": "yarn workspace @actual-app/web run vrt",
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -m ./packages/desktop-electron -o better-sqlite3,bcrypt --build-from-source -f",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/desktop-electron -o better-sqlite3,bcrypt",
|
||||
"rebuild-node": "yarn workspace @actual-app/core rebuild",
|
||||
"lint": "oxfmt --check . && oxlint --type-aware --quiet",
|
||||
"lint:fix": "oxfmt . && oxlint --fix --type-aware --quiet",
|
||||
@@ -87,23 +87,6 @@
|
||||
"typescript": "^6.0.2",
|
||||
"vitest": "^4.1.2"
|
||||
},
|
||||
"dependenciesMeta": {
|
||||
"bcrypt": {
|
||||
"built": true
|
||||
},
|
||||
"better-sqlite3": {
|
||||
"built": true
|
||||
},
|
||||
"electron": {
|
||||
"built": true
|
||||
},
|
||||
"esbuild": {
|
||||
"built": true
|
||||
},
|
||||
"sharp": {
|
||||
"built": true
|
||||
}
|
||||
},
|
||||
"resolutions": {
|
||||
"adm-zip": "patch:adm-zip@npm%3A0.5.16#~/.yarn/patches/adm-zip-npm-0.5.16-4556fea098.patch",
|
||||
"minimatch@10.2.1": "10.2.5",
|
||||
|
||||
@@ -516,29 +516,6 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// apis: getNote, updateNote
|
||||
test('Notes: successfully get and update note', async () => {
|
||||
const categories = await api.getCategories();
|
||||
const categoryId = categories[0].id;
|
||||
|
||||
// No note exists initially
|
||||
const initial = await api.getNote(categoryId);
|
||||
expect(initial).toBeNull();
|
||||
|
||||
// Set a note
|
||||
await api.updateNote(categoryId, 'Test note content');
|
||||
const afterSet = await api.getNote(categoryId);
|
||||
expect(afterSet).toEqual({ id: categoryId, note: 'Test note content' });
|
||||
|
||||
// Update the note
|
||||
await api.updateNote(categoryId, 'Updated note content');
|
||||
const afterUpdate = await api.getNote(categoryId);
|
||||
expect(afterUpdate).toEqual({
|
||||
id: categoryId,
|
||||
note: 'Updated note content',
|
||||
});
|
||||
});
|
||||
|
||||
// apis: getRules, getPayeeRules, createRule, updateRule, deleteRule
|
||||
test('Rules: successfully update rules', async () => {
|
||||
await api.createPayee({ name: 'test-payee' });
|
||||
|
||||
@@ -13,7 +13,6 @@ import type { ImportTransactionsOpts } from '@actual-app/core/types/api-handlers
|
||||
import type { Handlers } from '@actual-app/core/types/handlers';
|
||||
import type {
|
||||
ImportTransactionEntity,
|
||||
NoteEntity,
|
||||
RuleEntity,
|
||||
TransactionEntity,
|
||||
} from '@actual-app/core/types/models';
|
||||
@@ -204,8 +203,8 @@ export function getAccountBalance(id: APIAccountEntity['id'], cutoff?: Date) {
|
||||
return send('api/account-balance', { id, cutoff });
|
||||
}
|
||||
|
||||
export function getCategoryGroups(options: { hidden?: boolean } = {}) {
|
||||
return send('api/category-groups-get', options);
|
||||
export function getCategoryGroups() {
|
||||
return send('api/category-groups-get');
|
||||
}
|
||||
|
||||
export function createCategoryGroup(group: Omit<APICategoryGroupEntity, 'id'>) {
|
||||
@@ -226,8 +225,8 @@ export function deleteCategoryGroup(
|
||||
return send('api/category-group-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getCategories(options: { hidden?: boolean } = {}) {
|
||||
return send('api/categories-get', { grouped: false, ...options });
|
||||
export function getCategories() {
|
||||
return send('api/categories-get', { grouped: false });
|
||||
}
|
||||
|
||||
export function createCategory(category: Omit<APICategoryEntity, 'id'>) {
|
||||
@@ -248,14 +247,6 @@ export function deleteCategory(
|
||||
return send('api/category-delete', { id, transferCategoryId });
|
||||
}
|
||||
|
||||
export function getNote(id: NoteEntity['id']) {
|
||||
return send('api/note-get', { id });
|
||||
}
|
||||
|
||||
export function updateNote(id: NoteEntity['id'], note: NoteEntity['note']) {
|
||||
return send('api/note-update', { id, note });
|
||||
}
|
||||
|
||||
export function getCommonPayees() {
|
||||
return send('api/common-payees-get');
|
||||
}
|
||||
|
||||
@@ -85,12 +85,6 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
// Each test loads a budget file and runs all DB migrations, which can be
|
||||
// slow on busy CI runners; the default 5s timeout is too tight and causes
|
||||
// flaky timeouts (and a cascade of unhandled rejections from in-flight work
|
||||
// continuing after teardown).
|
||||
testTimeout: 20_000,
|
||||
hookTimeout: 20_000,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import * as api from '@actual-app/api';
|
||||
import { Command } from 'commander';
|
||||
|
||||
import { printOutput } from '#output';
|
||||
|
||||
import { registerCategoriesCommand } from './categories';
|
||||
import { registerCategoryGroupsCommand } from './category-groups';
|
||||
|
||||
vi.mock('@actual-app/api', () => ({
|
||||
getCategories: vi.fn().mockResolvedValue([]),
|
||||
createCategory: vi.fn().mockResolvedValue('new-id'),
|
||||
updateCategory: vi.fn().mockResolvedValue(undefined),
|
||||
deleteCategory: vi.fn().mockResolvedValue(undefined),
|
||||
getCategoryGroups: vi.fn().mockResolvedValue([]),
|
||||
createCategoryGroup: vi.fn().mockResolvedValue('new-group-id'),
|
||||
updateCategoryGroup: vi.fn().mockResolvedValue(undefined),
|
||||
deleteCategoryGroup: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
vi.mock('#connection', () => ({
|
||||
withConnection: vi.fn((_opts, fn) => fn()),
|
||||
}));
|
||||
|
||||
vi.mock('#output', () => ({
|
||||
printOutput: vi.fn(),
|
||||
}));
|
||||
|
||||
function createProgram(): Command {
|
||||
const program = new Command();
|
||||
program.option('--format <format>');
|
||||
program.option('--server-url <url>');
|
||||
program.option('--password <pw>');
|
||||
program.option('--session-token <token>');
|
||||
program.option('--sync-id <id>');
|
||||
program.option('--data-dir <dir>');
|
||||
program.option('--verbose');
|
||||
program.exitOverride();
|
||||
registerCategoriesCommand(program);
|
||||
registerCategoryGroupsCommand(program);
|
||||
return program;
|
||||
}
|
||||
|
||||
async function run(args: string[]) {
|
||||
const program = createProgram();
|
||||
await program.parseAsync(['node', 'test', ...args]);
|
||||
}
|
||||
|
||||
describe('categories commands', () => {
|
||||
let stderrSpy: ReturnType<typeof vi.spyOn>;
|
||||
let stdoutSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
stderrSpy = vi
|
||||
.spyOn(process.stderr, 'write')
|
||||
.mockImplementation(() => true);
|
||||
stdoutSpy = vi
|
||||
.spyOn(process.stdout, 'write')
|
||||
.mockImplementation(() => true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
stderrSpy.mockRestore();
|
||||
stdoutSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('categories list', () => {
|
||||
it('asks the API to exclude hidden categories by default', async () => {
|
||||
await run(['categories', 'list']);
|
||||
|
||||
expect(api.getCategories).toHaveBeenCalledWith({ hidden: false });
|
||||
});
|
||||
|
||||
it('asks the API for all categories when --include-hidden is passed', async () => {
|
||||
await run(['categories', 'list', '--include-hidden']);
|
||||
|
||||
expect(api.getCategories).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('prints whatever the API returns', async () => {
|
||||
const visible = {
|
||||
id: '1',
|
||||
name: 'Visible',
|
||||
group_id: 'g1',
|
||||
hidden: false,
|
||||
};
|
||||
vi.mocked(api.getCategories).mockResolvedValue([visible]);
|
||||
|
||||
await run(['categories', 'list']);
|
||||
|
||||
expect(printOutput).toHaveBeenCalledWith([visible], undefined);
|
||||
});
|
||||
|
||||
it('passes format option to printOutput', async () => {
|
||||
vi.mocked(api.getCategories).mockResolvedValue([]);
|
||||
|
||||
await run(['--format', 'csv', 'categories', 'list']);
|
||||
|
||||
expect(printOutput).toHaveBeenCalledWith([], 'csv');
|
||||
});
|
||||
});
|
||||
|
||||
describe('category-groups list', () => {
|
||||
it('asks the API to exclude hidden groups by default', async () => {
|
||||
await run(['category-groups', 'list']);
|
||||
|
||||
expect(api.getCategoryGroups).toHaveBeenCalledWith({ hidden: false });
|
||||
});
|
||||
|
||||
it('asks the API for all groups when --include-hidden is passed', async () => {
|
||||
await run(['category-groups', 'list', '--include-hidden']);
|
||||
|
||||
expect(api.getCategoryGroups).toHaveBeenCalledWith({});
|
||||
});
|
||||
|
||||
it('prints whatever the API returns', async () => {
|
||||
const group = {
|
||||
id: 'g1',
|
||||
name: 'Group',
|
||||
is_income: false,
|
||||
hidden: false,
|
||||
categories: [{ id: 'c1', name: 'Cat', group_id: 'g1', hidden: false }],
|
||||
};
|
||||
vi.mocked(api.getCategoryGroups).mockResolvedValue([group]);
|
||||
|
||||
await run(['category-groups', 'list']);
|
||||
|
||||
expect(printOutput).toHaveBeenCalledWith([group], undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -12,16 +12,13 @@ export function registerCategoriesCommand(program: Command) {
|
||||
|
||||
categories
|
||||
.command('list')
|
||||
.description('List categories (excludes hidden by default)')
|
||||
.option('--include-hidden', 'Include hidden categories', false)
|
||||
.action(async cmdOpts => {
|
||||
.description('List all categories')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(
|
||||
opts,
|
||||
async () => {
|
||||
const result = await api.getCategories(
|
||||
cmdOpts.includeHidden ? {} : { hidden: false },
|
||||
);
|
||||
const result = await api.getCategories();
|
||||
printOutput(result, opts.format);
|
||||
},
|
||||
{ mutates: false },
|
||||
|
||||
@@ -12,16 +12,13 @@ export function registerCategoryGroupsCommand(program: Command) {
|
||||
|
||||
groups
|
||||
.command('list')
|
||||
.description('List category groups (excludes hidden by default)')
|
||||
.option('--include-hidden', 'Include hidden groups and categories', false)
|
||||
.action(async cmdOpts => {
|
||||
.description('List all category groups')
|
||||
.action(async () => {
|
||||
const opts = program.opts();
|
||||
await withConnection(
|
||||
opts,
|
||||
async () => {
|
||||
const result = await api.getCategoryGroups(
|
||||
cmdOpts.includeHidden ? {} : { hidden: false },
|
||||
);
|
||||
const result = await api.getCategoryGroups();
|
||||
printOutput(result, opts.format);
|
||||
},
|
||||
{ mutates: false },
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$(dirname "$0")")"
|
||||
|
||||
@@ -8,10 +7,20 @@ if ! [ -x "$(command -v protoc)" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
protoc --plugin="protoc-gen-es=../../node_modules/.bin/protoc-gen-es" \
|
||||
--es_opt=target=ts \
|
||||
--es_out="src/proto" \
|
||||
export PATH="$PWD/bin:$PATH"
|
||||
|
||||
protoc --plugin="protoc-gen-ts=../../node_modules/.bin/protoc-gen-ts" \
|
||||
--ts_opt=esModuleInterop=true \
|
||||
--ts_out="src/proto" \
|
||||
--js_out=import_style=commonjs,binary:src/proto \
|
||||
--proto_path=src/proto \
|
||||
sync.proto
|
||||
|
||||
../../node_modules/.bin/oxfmt src/proto/*.ts
|
||||
../../node_modules/.bin/oxfmt src/proto/*.d.ts
|
||||
|
||||
for file in src/proto/*.d.ts; do
|
||||
{ echo "/* oxlint-disable typescript/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;`'
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
{
|
||||
"name": "@actual-app/crdt",
|
||||
"version": "3.0.0",
|
||||
"version": "2.1.0",
|
||||
"description": "CRDT layer of Actual",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/actualbudget/actual.git",
|
||||
"directory": "packages/crdt"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"!dist/**/*.test.d.ts",
|
||||
@@ -15,11 +10,14 @@
|
||||
"!dist/**/*.spec.d.ts",
|
||||
"!dist/**/*.spec.d.ts.map"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"exports": {
|
||||
@@ -27,9 +25,7 @@
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build:node": "vite build",
|
||||
@@ -39,14 +35,16 @@
|
||||
"typecheck": "tsgo -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.11.0",
|
||||
"google-protobuf": "^3.21.4",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^14.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@bufbuild/protoc-gen-es": "^2.11.0",
|
||||
"@types/google-protobuf": "3.15.12",
|
||||
"@typescript/native-preview": "beta",
|
||||
"protoc-gen-js": "3.21.4-4",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"ts-protoc-gen": "0.15.0",
|
||||
"vite": "^8.0.5",
|
||||
"vitest": "^4.1.2"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import './proto/sync_pb.js'; // Import for side effects
|
||||
import type * as SyncPb from './proto/sync_pb';
|
||||
|
||||
export {
|
||||
merkle,
|
||||
getClock,
|
||||
@@ -10,17 +13,16 @@ export {
|
||||
Timestamp,
|
||||
} from './crdt';
|
||||
|
||||
export {
|
||||
type EncryptedData,
|
||||
type Message,
|
||||
type MessageEnvelope,
|
||||
type SyncRequest,
|
||||
type SyncResponse,
|
||||
EncryptedDataSchema,
|
||||
MessageSchema,
|
||||
MessageEnvelopeSchema,
|
||||
SyncRequestSchema,
|
||||
SyncResponseSchema,
|
||||
} from './proto/sync_pb';
|
||||
declare global {
|
||||
var proto: typeof SyncPb;
|
||||
}
|
||||
|
||||
export { create, fromBinary, toBinary } from '@bufbuild/protobuf';
|
||||
const { proto } = globalThis;
|
||||
|
||||
export const SyncRequest = proto.SyncRequest;
|
||||
export const SyncResponse = proto.SyncResponse;
|
||||
export const Message = proto.Message;
|
||||
export const MessageEnvelope = proto.MessageEnvelope;
|
||||
export const EncryptedData = proto.EncryptedData;
|
||||
|
||||
export const SyncProtoBuf = proto;
|
||||
|
||||
@@ -21,7 +21,6 @@ message MessageEnvelope {
|
||||
}
|
||||
|
||||
message SyncRequest {
|
||||
reserved 4;
|
||||
repeated MessageEnvelope messages = 1;
|
||||
string fileId = 2;
|
||||
string groupId = 3;
|
||||
|
||||
1206
packages/crdt/src/proto/sync_pb.js
Normal file
@@ -1,161 +1,217 @@
|
||||
// @generated by protoc-gen-es v2.11.0 with parameter "target=ts"
|
||||
// @generated from file sync.proto (syntax proto3)
|
||||
/* eslint-disable */
|
||||
/* oxlint-disable typescript/no-namespace */
|
||||
// package:
|
||||
// file: sync.proto
|
||||
|
||||
import type { Message as Message$1 } from '@bufbuild/protobuf';
|
||||
import type { GenFile, GenMessage } from '@bufbuild/protobuf/codegenv2';
|
||||
import { fileDesc, messageDesc } from '@bufbuild/protobuf/codegenv2';
|
||||
import * as jspb from 'google-protobuf';
|
||||
|
||||
/**
|
||||
* Describes the file sync.proto.
|
||||
*/
|
||||
export const file_sync: GenFile /*@__PURE__*/ = fileDesc(
|
||||
'CgpzeW5jLnByb3RvIjoKDUVuY3J5cHRlZERhdGESCgoCaXYYASABKAwSDwoHYXV0aFRhZxgCIAEoDBIMCgRkYXRhGAMgASgMIkYKB01lc3NhZ2USDwoHZGF0YXNldBgBIAEoCRILCgNyb3cYAiABKAkSDgoGY29sdW1uGAMgASgJEg0KBXZhbHVlGAQgASgJIkoKD01lc3NhZ2VFbnZlbG9wZRIRCgl0aW1lc3RhbXAYASABKAkSEwoLaXNFbmNyeXB0ZWQYAiABKAgSDwoHY29udGVudBgDIAEoDCJ2CgtTeW5jUmVxdWVzdBIiCghtZXNzYWdlcxgBIAMoCzIQLk1lc3NhZ2VFbnZlbG9wZRIOCgZmaWxlSWQYAiABKAkSDwoHZ3JvdXBJZBgDIAEoCRINCgVrZXlJZBgFIAEoCRINCgVzaW5jZRgGIAEoCUoECAQQBSJCCgxTeW5jUmVzcG9uc2USIgoIbWVzc2FnZXMYASADKAsyEC5NZXNzYWdlRW52ZWxvcGUSDgoGbWVya2xlGAIgASgJYgZwcm90bzM',
|
||||
);
|
||||
export declare class EncryptedData extends jspb.Message {
|
||||
getIv(): Uint8Array | string;
|
||||
getIv_asU8(): Uint8Array;
|
||||
getIv_asB64(): string;
|
||||
setIv(value: Uint8Array | string): void;
|
||||
|
||||
/**
|
||||
* @generated from message EncryptedData
|
||||
*/
|
||||
export type EncryptedData = Message$1<'EncryptedData'> & {
|
||||
/**
|
||||
* @generated from field: bytes iv = 1;
|
||||
*/
|
||||
iv: Uint8Array;
|
||||
getAuthtag(): Uint8Array | string;
|
||||
getAuthtag_asU8(): Uint8Array;
|
||||
getAuthtag_asB64(): string;
|
||||
setAuthtag(value: Uint8Array | string): void;
|
||||
|
||||
/**
|
||||
* @generated from field: bytes authTag = 2;
|
||||
*/
|
||||
authTag: Uint8Array;
|
||||
getData(): Uint8Array | string;
|
||||
getData_asU8(): Uint8Array;
|
||||
getData_asB64(): string;
|
||||
setData(value: Uint8Array | string): void;
|
||||
|
||||
/**
|
||||
* @generated from field: bytes data = 3;
|
||||
*/
|
||||
data: Uint8Array;
|
||||
};
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): EncryptedData.AsObject;
|
||||
static toObject(
|
||||
includeInstance: boolean,
|
||||
msg: EncryptedData,
|
||||
): EncryptedData.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: EncryptedData,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): EncryptedData;
|
||||
static deserializeBinaryFromReader(
|
||||
message: EncryptedData,
|
||||
reader: jspb.BinaryReader,
|
||||
): EncryptedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the message EncryptedData.
|
||||
* Use `create(EncryptedDataSchema)` to create a new message.
|
||||
*/
|
||||
export const EncryptedDataSchema: GenMessage<EncryptedData> /*@__PURE__*/ =
|
||||
messageDesc(file_sync, 0);
|
||||
export namespace EncryptedData {
|
||||
export type AsObject = {
|
||||
iv: Uint8Array | string;
|
||||
authtag: Uint8Array | string;
|
||||
data: Uint8Array | string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message Message
|
||||
*/
|
||||
export type Message = Message$1<'Message'> & {
|
||||
/**
|
||||
* @generated from field: string dataset = 1;
|
||||
*/
|
||||
dataset: string;
|
||||
export declare class Message extends jspb.Message {
|
||||
getDataset(): string;
|
||||
setDataset(value: string): void;
|
||||
|
||||
/**
|
||||
* @generated from field: string row = 2;
|
||||
*/
|
||||
row: string;
|
||||
getRow(): string;
|
||||
setRow(value: string): void;
|
||||
|
||||
/**
|
||||
* @generated from field: string column = 3;
|
||||
*/
|
||||
column: string;
|
||||
getColumn(): string;
|
||||
setColumn(value: string): void;
|
||||
|
||||
/**
|
||||
* @generated from field: string value = 4;
|
||||
*/
|
||||
value: string;
|
||||
};
|
||||
getValue(): string;
|
||||
setValue(value: string): void;
|
||||
|
||||
/**
|
||||
* Describes the message Message.
|
||||
* Use `create(MessageSchema)` to create a new message.
|
||||
*/
|
||||
export const MessageSchema: GenMessage<Message> /*@__PURE__*/ = messageDesc(
|
||||
file_sync,
|
||||
1,
|
||||
);
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): Message.AsObject;
|
||||
static toObject(includeInstance: boolean, msg: Message): Message.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: Message,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): Message;
|
||||
static deserializeBinaryFromReader(
|
||||
message: Message,
|
||||
reader: jspb.BinaryReader,
|
||||
): Message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from message MessageEnvelope
|
||||
*/
|
||||
export type MessageEnvelope = Message$1<'MessageEnvelope'> & {
|
||||
/**
|
||||
* @generated from field: string timestamp = 1;
|
||||
*/
|
||||
timestamp: string;
|
||||
export namespace Message {
|
||||
export type AsObject = {
|
||||
dataset: string;
|
||||
row: string;
|
||||
column: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from field: bool isEncrypted = 2;
|
||||
*/
|
||||
isEncrypted: boolean;
|
||||
export declare class MessageEnvelope extends jspb.Message {
|
||||
getTimestamp(): string;
|
||||
setTimestamp(value: string): void;
|
||||
|
||||
/**
|
||||
* @generated from field: bytes content = 3;
|
||||
*/
|
||||
content: Uint8Array;
|
||||
};
|
||||
getIsencrypted(): boolean;
|
||||
setIsencrypted(value: boolean): void;
|
||||
|
||||
/**
|
||||
* Describes the message MessageEnvelope.
|
||||
* Use `create(MessageEnvelopeSchema)` to create a new message.
|
||||
*/
|
||||
export const MessageEnvelopeSchema: GenMessage<MessageEnvelope> /*@__PURE__*/ =
|
||||
messageDesc(file_sync, 2);
|
||||
getContent(): Uint8Array | string;
|
||||
getContent_asU8(): Uint8Array;
|
||||
getContent_asB64(): string;
|
||||
setContent(value: Uint8Array | string): void;
|
||||
|
||||
/**
|
||||
* @generated from message SyncRequest
|
||||
*/
|
||||
export type SyncRequest = Message$1<'SyncRequest'> & {
|
||||
/**
|
||||
* @generated from field: repeated MessageEnvelope messages = 1;
|
||||
*/
|
||||
messages: MessageEnvelope[];
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): MessageEnvelope.AsObject;
|
||||
static toObject(
|
||||
includeInstance: boolean,
|
||||
msg: MessageEnvelope,
|
||||
): MessageEnvelope.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: MessageEnvelope,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): MessageEnvelope;
|
||||
static deserializeBinaryFromReader(
|
||||
message: MessageEnvelope,
|
||||
reader: jspb.BinaryReader,
|
||||
): MessageEnvelope;
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from field: string fileId = 2;
|
||||
*/
|
||||
fileId: string;
|
||||
export namespace MessageEnvelope {
|
||||
export type AsObject = {
|
||||
timestamp: string;
|
||||
isencrypted: boolean;
|
||||
content: Uint8Array | string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @generated from field: string groupId = 3;
|
||||
*/
|
||||
groupId: string;
|
||||
export declare class SyncRequest extends jspb.Message {
|
||||
clearMessagesList(): void;
|
||||
getMessagesList(): Array<MessageEnvelope>;
|
||||
setMessagesList(value: Array<MessageEnvelope>): void;
|
||||
addMessages(value?: MessageEnvelope, index?: number): MessageEnvelope;
|
||||
|
||||
/**
|
||||
* @generated from field: string keyId = 5;
|
||||
*/
|
||||
keyId: string;
|
||||
getFileid(): string;
|
||||
setFileid(value: string): void;
|
||||
|
||||
/**
|
||||
* @generated from field: string since = 6;
|
||||
*/
|
||||
since: string;
|
||||
};
|
||||
getGroupid(): string;
|
||||
setGroupid(value: string): void;
|
||||
|
||||
/**
|
||||
* Describes the message SyncRequest.
|
||||
* Use `create(SyncRequestSchema)` to create a new message.
|
||||
*/
|
||||
export const SyncRequestSchema: GenMessage<SyncRequest> /*@__PURE__*/ =
|
||||
messageDesc(file_sync, 3);
|
||||
getKeyid(): string;
|
||||
setKeyid(value: string): void;
|
||||
|
||||
/**
|
||||
* @generated from message SyncResponse
|
||||
*/
|
||||
export type SyncResponse = Message$1<'SyncResponse'> & {
|
||||
/**
|
||||
* @generated from field: repeated MessageEnvelope messages = 1;
|
||||
*/
|
||||
messages: MessageEnvelope[];
|
||||
getSince(): string;
|
||||
setSince(value: string): void;
|
||||
|
||||
/**
|
||||
* @generated from field: string merkle = 2;
|
||||
*/
|
||||
merkle: string;
|
||||
};
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): SyncRequest.AsObject;
|
||||
static toObject(
|
||||
includeInstance: boolean,
|
||||
msg: SyncRequest,
|
||||
): SyncRequest.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: SyncRequest,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): SyncRequest;
|
||||
static deserializeBinaryFromReader(
|
||||
message: SyncRequest,
|
||||
reader: jspb.BinaryReader,
|
||||
): SyncRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes the message SyncResponse.
|
||||
* Use `create(SyncResponseSchema)` to create a new message.
|
||||
*/
|
||||
export const SyncResponseSchema: GenMessage<SyncResponse> /*@__PURE__*/ =
|
||||
messageDesc(file_sync, 4);
|
||||
export namespace SyncRequest {
|
||||
export type AsObject = {
|
||||
messagesList: Array<MessageEnvelope.AsObject>;
|
||||
fileid: string;
|
||||
groupid: string;
|
||||
keyid: string;
|
||||
since: string;
|
||||
};
|
||||
}
|
||||
|
||||
export declare class SyncResponse extends jspb.Message {
|
||||
clearMessagesList(): void;
|
||||
getMessagesList(): Array<MessageEnvelope>;
|
||||
setMessagesList(value: Array<MessageEnvelope>): void;
|
||||
addMessages(value?: MessageEnvelope, index?: number): MessageEnvelope;
|
||||
|
||||
getMerkle(): string;
|
||||
setMerkle(value: string): void;
|
||||
|
||||
serializeBinary(): Uint8Array;
|
||||
toObject(includeInstance?: boolean): SyncResponse.AsObject;
|
||||
static toObject(
|
||||
includeInstance: boolean,
|
||||
msg: SyncResponse,
|
||||
): SyncResponse.AsObject;
|
||||
static extensions: { [key: number]: jspb.ExtensionFieldInfo<jspb.Message> };
|
||||
static extensionsBinary: {
|
||||
[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>;
|
||||
};
|
||||
static serializeBinaryToWriter(
|
||||
message: SyncResponse,
|
||||
writer: jspb.BinaryWriter,
|
||||
): void;
|
||||
static deserializeBinary(bytes: Uint8Array): SyncResponse;
|
||||
static deserializeBinaryFromReader(
|
||||
message: SyncResponse,
|
||||
reader: jspb.BinaryReader,
|
||||
): SyncResponse;
|
||||
}
|
||||
|
||||
export namespace SyncResponse {
|
||||
export type AsObject = {
|
||||
messagesList: Array<MessageEnvelope.AsObject>;
|
||||
merkle: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"rootDir": "./src",
|
||||
"composite": true,
|
||||
"target": "ES2021",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "bundler",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noEmit": false,
|
||||
"emitDeclarationOnly": true,
|
||||
"declaration": true,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { defineConfig } from 'vite';
|
||||
export default defineConfig({
|
||||
ssr: {
|
||||
noExternal: true,
|
||||
external: ['@bufbuild/protobuf', 'murmurhash'],
|
||||
external: ['google-protobuf', 'murmurhash'],
|
||||
},
|
||||
build: {
|
||||
ssr: true,
|
||||
@@ -16,7 +16,7 @@ export default defineConfig({
|
||||
sourcemap: true,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'src/index.ts'),
|
||||
formats: ['es'],
|
||||
formats: ['cjs'],
|
||||
fileName: () => 'index.js',
|
||||
},
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 133 KiB |
@@ -25,75 +25,6 @@ export class ReportsPage {
|
||||
return new ReportsPage(this.page);
|
||||
}
|
||||
|
||||
async goToBalanceForecastPage() {
|
||||
const gridItems = this.pageContent.locator('.react-grid-item');
|
||||
const count = await gridItems.count();
|
||||
|
||||
let targetItem: Locator | null = null;
|
||||
for (let i = count - 1; i >= 0; i--) {
|
||||
const item = gridItems.nth(i);
|
||||
await item.scrollIntoViewIfNeeded();
|
||||
const heading = item.getByRole('heading', { name: /^Balance Forecast/i });
|
||||
if (await heading.isVisible()) {
|
||||
targetItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetItem) {
|
||||
await this.page.evaluate(() => {
|
||||
window.scrollTo(0, document.documentElement.scrollHeight);
|
||||
});
|
||||
const refreshedCount = await gridItems.count();
|
||||
for (let i = refreshedCount - 1; i >= 0; i--) {
|
||||
const item = gridItems.nth(i);
|
||||
await item.scrollIntoViewIfNeeded();
|
||||
const heading = item.getByRole('heading', {
|
||||
name: /^Balance Forecast/i,
|
||||
});
|
||||
if (await heading.isVisible()) {
|
||||
targetItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetItem) {
|
||||
throw new Error('No Balance Forecast dashboard card found in the grid');
|
||||
}
|
||||
|
||||
const cardNavigateButton = targetItem.getByRole('button', {
|
||||
name: /^Balance Forecast/i,
|
||||
});
|
||||
await Promise.all([
|
||||
this.page.waitForURL(/\/reports\/forecast\//),
|
||||
cardNavigateButton.click(),
|
||||
]);
|
||||
|
||||
await this.pageContent
|
||||
.getByRole('button', { name: 'Monthly' })
|
||||
.waitFor({ state: 'visible' });
|
||||
|
||||
return new ReportsPage(this.page);
|
||||
}
|
||||
|
||||
async selectForecastGranularity(granularity: string) {
|
||||
await this.pageContent.getByRole('button', { name: 'Monthly' }).click();
|
||||
const option = this.page.getByRole('button', { name: granularity });
|
||||
await option.waitFor({ state: 'visible' });
|
||||
await option.click();
|
||||
await this.pageContent
|
||||
.getByRole('button', { name: granularity })
|
||||
.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async addWidget(widgetName: string) {
|
||||
await this.pageContent
|
||||
.getByRole('button', { name: 'Add new widget' })
|
||||
.click();
|
||||
await this.page.getByRole('button', { name: widgetName }).click();
|
||||
}
|
||||
|
||||
async goToCustomReportPage() {
|
||||
await this.pageContent
|
||||
.getByRole('button', { name: 'Add new widget' })
|
||||
|
||||
@@ -42,22 +42,17 @@ export class SettingsPage {
|
||||
}
|
||||
|
||||
async enableExperimentalFeature(featureName: string) {
|
||||
await this.advancedSettingsButton.waitFor({
|
||||
state: 'visible',
|
||||
timeout: 2000,
|
||||
});
|
||||
await this.advancedSettingsButton.click();
|
||||
if (await this.advancedSettingsButton.isVisible()) {
|
||||
await this.advancedSettingsButton.click();
|
||||
}
|
||||
|
||||
await this.experimentalSettingsButton.waitFor({
|
||||
state: 'visible',
|
||||
timeout: 2000,
|
||||
});
|
||||
await this.experimentalSettingsButton.click();
|
||||
if (await this.experimentalSettingsButton.isVisible()) {
|
||||
await this.experimentalSettingsButton.click();
|
||||
}
|
||||
|
||||
const featureCheckbox = this.page.getByRole('checkbox', {
|
||||
name: featureName,
|
||||
});
|
||||
await featureCheckbox.waitFor({ state: 'visible' });
|
||||
if (!(await featureCheckbox.isChecked())) {
|
||||
await featureCheckbox.click();
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
@@ -55,28 +55,6 @@ test.describe.parallel('Reports', () => {
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test.describe('balance forecast', () => {
|
||||
test.beforeEach(async () => {
|
||||
const settingsPage = await navigation.goToSettingsPage();
|
||||
await settingsPage.enableExperimentalFeature('Balance Forecast Report');
|
||||
|
||||
reportsPage = await navigation.goToReportsPage();
|
||||
await reportsPage.waitToLoad();
|
||||
await reportsPage.addWidget('Balance forecast');
|
||||
await reportsPage.goToBalanceForecastPage();
|
||||
});
|
||||
|
||||
test('loads balance forecast report with monthly granularity', async () => {
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
|
||||
test('switches to daily granularity', async () => {
|
||||
await reportsPage.selectForecastGranularity('Daily');
|
||||
|
||||
await expect(page).toMatchThemeScreenshots();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe.parallel('custom reports', () => {
|
||||
let customReportPage: CustomReportPage;
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 6.9 KiB |
@@ -42,7 +42,6 @@
|
||||
"#components/budget": "./src/components/budget/index.tsx",
|
||||
"#components/budget/goals/actions": "./src/components/budget/goals/actions.ts",
|
||||
"#components/budget/goals/automationExamples": "./src/components/budget/goals/automationExamples.ts",
|
||||
"#components/budget/goals/cleanupModel": "./src/components/budget/goals/cleanupModel.ts",
|
||||
"#components/budget/goals/constants": "./src/components/budget/goals/constants.ts",
|
||||
"#components/budget/goals/displayTemplateMeta": "./src/components/budget/goals/displayTemplateMeta.ts",
|
||||
"#components/budget/goals/formatMonthLabel": "./src/components/budget/goals/formatMonthLabel.ts",
|
||||
@@ -162,7 +161,6 @@
|
||||
"cross-env": "^10.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"downshift": "9.3.2",
|
||||
"fzf": "^0.5.2",
|
||||
"html-to-image": "^1.11.13",
|
||||
"hyperformula": "^3.2.0",
|
||||
"i18next": "^25.10.10",
|
||||
|
||||
@@ -633,8 +633,6 @@ export function useSyncAccountsMutation() {
|
||||
accountIdsToSync = accountIdsToSync.filter(
|
||||
id => !simpleFinAccounts.find(sfa => sfa.id === id),
|
||||
);
|
||||
|
||||
dispatch(setAccountsSyncing({ ids: accountIdsToSync }));
|
||||
}
|
||||
|
||||
// Loop through the accounts and perform sync operation.. one by one
|
||||
|
||||
@@ -335,17 +335,10 @@ const isUpdateReadyForDownloadPromise = new Promise(resolve => {
|
||||
resolve(true);
|
||||
};
|
||||
});
|
||||
// Skip SW registration in dev so stale cached assets don't override edits
|
||||
// between page loads. Plugin code that needs a SW can register one itself.
|
||||
// In dev there is no SW to install, so applyAppUpdate() can't rely on the
|
||||
// SW lifecycle to swap the page — fall back to a plain reload so callers
|
||||
// don't hang on the never-resolving promise inside applyAppUpdate.
|
||||
const updateSW = IS_DEV
|
||||
? () => window.location.reload()
|
||||
: registerSW({
|
||||
immediate: true,
|
||||
onNeedRefresh: markUpdateReadyForDownload,
|
||||
});
|
||||
const updateSW = registerSW({
|
||||
immediate: true,
|
||||
onNeedRefresh: markUpdateReadyForDownload,
|
||||
});
|
||||
|
||||
global.Actual = {
|
||||
IS_DEV,
|
||||
|
||||