Compare commits
126 Commits
fix-sql-in
...
v25.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23cbb4b0e | ||
|
|
8da1c59362 | ||
|
|
916a27a89a | ||
|
|
97b99d1f16 | ||
|
|
cf114ef69e | ||
|
|
7064106748 | ||
|
|
b92a1cacf7 | ||
|
|
f5c1879de1 | ||
|
|
92f444f548 | ||
|
|
94600731d9 | ||
|
|
5d59ecd72c | ||
|
|
54d9f0ff41 | ||
|
|
eebdbdf34e | ||
|
|
bddc372650 | ||
|
|
d027adc734 | ||
|
|
e67c064d1f | ||
|
|
2f1ccad25a | ||
|
|
a2b9153d02 | ||
|
|
f1594e5bca | ||
|
|
fed426d704 | ||
|
|
aeb26b75af | ||
|
|
4b58335780 | ||
|
|
153eaeecc5 | ||
|
|
e28db52c70 | ||
|
|
83f7a79c76 | ||
|
|
7e3ebb7e5f | ||
|
|
a8d41e88a5 | ||
|
|
62ab6a12e8 | ||
|
|
d91fd2fb0e | ||
|
|
4e9d51be1f | ||
|
|
1bc87fb35a | ||
|
|
2f5464c735 | ||
|
|
c85769eb82 | ||
|
|
a657021303 | ||
|
|
523a62c8a0 | ||
|
|
925efc4cb6 | ||
|
|
31fe766a2b | ||
|
|
b9e7d7e9c2 | ||
|
|
04aa086c9e | ||
|
|
cc02970a16 | ||
|
|
9489e2f3b4 | ||
|
|
608b4be765 | ||
|
|
01f45d5072 | ||
|
|
1b5be7f9d2 | ||
|
|
e5d301c66b | ||
|
|
fe3fe47b67 | ||
|
|
b4ad639bb5 | ||
|
|
c77168fa18 | ||
|
|
6fb7fe1343 | ||
|
|
490ec22b8a | ||
|
|
0d420ab4d9 | ||
|
|
6b22e7422b | ||
|
|
35a833ecba | ||
|
|
140039ea1f | ||
|
|
afab6ee773 | ||
|
|
cefc8da7bc | ||
|
|
91aa2ae47b | ||
|
|
a2878ff2a2 | ||
|
|
0dfa2910c7 | ||
|
|
4e0ab44e2f | ||
|
|
aba97e8b74 | ||
|
|
1d6b70d160 | ||
|
|
a9047bfcd6 | ||
|
|
af03477d3a | ||
|
|
c4b4108eca | ||
|
|
26ee3179e1 | ||
|
|
82b6589c37 | ||
|
|
56f509dcda | ||
|
|
bbfa0093cd | ||
|
|
aef38f1679 | ||
|
|
e04ca554e2 | ||
|
|
77949ad276 | ||
|
|
45f9cc3c1d | ||
|
|
31ed12832c | ||
|
|
0f8a1aeb2b | ||
|
|
161c0625b1 | ||
|
|
009a3dff4e | ||
|
|
5d01e109e6 | ||
|
|
b2287cded3 | ||
|
|
19dbfd0673 | ||
|
|
36c40d90d2 | ||
|
|
00ff2e2522 | ||
|
|
1d074730f4 | ||
|
|
1e685b993b | ||
|
|
6b2d2420a5 | ||
|
|
af09e5b1d5 | ||
|
|
8f12893ff0 | ||
|
|
a85dc890e5 | ||
|
|
8180476531 | ||
|
|
ad1df689d7 | ||
|
|
d9716caf5d | ||
|
|
b2cca2337c | ||
|
|
e0bddaeb99 | ||
|
|
2c4c5014ea | ||
|
|
c7f3dadc07 | ||
|
|
999010cca6 | ||
|
|
b9603d0e54 | ||
|
|
0c85523037 | ||
|
|
eb5944b353 | ||
|
|
f35a850e3d | ||
|
|
3ca9f6ecbc | ||
|
|
602b84342b | ||
|
|
b5cbaa52b2 | ||
|
|
5fdaa98249 | ||
|
|
c8813e9953 | ||
|
|
074d5b76cf | ||
|
|
9f9f349cbf | ||
|
|
78e763659e | ||
|
|
0cc817f2ef | ||
|
|
f00484b17c | ||
|
|
0cdaac6944 | ||
|
|
864aaacd11 | ||
|
|
bdf76f6c63 | ||
|
|
47c0d394ee | ||
|
|
fdac2839c9 | ||
|
|
379a84d2e2 | ||
|
|
d16d022b50 | ||
|
|
606e39252f | ||
|
|
2523c091c3 | ||
|
|
d4b37baa4d | ||
|
|
ed4ef5b30e | ||
|
|
baadafb15e | ||
|
|
346727a539 | ||
|
|
ddcd771488 | ||
|
|
3ecfb4be7e | ||
|
|
bf154db3d6 |
@@ -1,14 +1,11 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose
|
||||
{
|
||||
"name": "Actual development",
|
||||
"dockerComposeFile": [
|
||||
"../docker-compose.yml",
|
||||
"docker-compose.yml"
|
||||
],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
"service": "actual-development",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"postCreateCommand": "yarn install"
|
||||
"name": "Actual development",
|
||||
"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
|
||||
// Alternatively:
|
||||
// "image": "mcr.microsoft.com/devcontainers/typescript-node:0-16",
|
||||
"service": "actual-development",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
"postCreateCommand": "yarn install"
|
||||
}
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1 +1 @@
|
||||
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes -->
|
||||
<!-- Thank you for submitting a pull request! Make sure to follow the instructions to write release notes for your PR — it should only take a minute or two: https://github.com/actualbudget/docs#writing-good-release-notes. Try running yarn generate:release-notes *before* pushing your PR for an interactive experience. -->
|
||||
|
||||
27
.github/actions/bump-package-versions
vendored
@@ -11,13 +11,36 @@ 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
|
||||
# logic: if before the 25th, bump patch, else set minor/major to next month
|
||||
version="$(jq -r .version "$file" | perl -e '($y,$m,$p)=split/\./,<>;$d=(localtime)[3];$d>25?($p=0,++$m,$m>12&&($m=1,++$y)):$p++;print"$y.$m.$p\n"')"
|
||||
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
|
||||
|
||||
12
.github/workflows/docker-edge.yml
vendored
@@ -73,17 +73,19 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Download artifacts
|
||||
run: ./packages/sync-server/docker/download-artifacts.sh
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Building outside of the docker image allows us to build once and push to multiple platforms
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: ./bin/package-browser
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
file: packages/sync-server/docker/edge-${{ matrix.os }}.Dockerfile
|
||||
file: packages/sync-server/docker/${{ matrix.os }}.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7${{ matrix.os == 'alpine' && ',linux/arm/v6' || '' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
|
||||
11
.github/workflows/docker-release.yml
vendored
@@ -70,12 +70,19 @@ jobs:
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# Building outside of the docker image allows us to build once and push to multiple platforms
|
||||
# This is faster and avoids yarn memory issues
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Build Web
|
||||
run: ./bin/package-browser
|
||||
|
||||
- name: Build and push ubuntu image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: packages/sync-server/docker/stable-ubuntu.Dockerfile
|
||||
file: packages/sync-server/docker/ubuntu.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
||||
@@ -84,6 +91,6 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
file: packages/sync-server/docker/stable-alpine.Dockerfile
|
||||
file: packages/sync-server/docker/alpine.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6
|
||||
tags: ${{ steps.alpine-meta.outputs.tags }}
|
||||
|
||||
21
.github/workflows/e2e-test.yml
vendored
@@ -48,6 +48,27 @@ jobs:
|
||||
path: packages/desktop-client/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
functional-desktop-app:
|
||||
name: Functional Desktop App
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run Desktop app E2E Tests
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: desktop-app-test-results
|
||||
path: packages/desktop-electron/e2e/test-results/
|
||||
retention-days: 30
|
||||
overwrite: true
|
||||
|
||||
vrt:
|
||||
name: Visual regression
|
||||
needs: netlify
|
||||
|
||||
78
.github/workflows/publish-npm-packages.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Publish npm packages
|
||||
|
||||
# # Npm packages are published for every new tag
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build-and-pack:
|
||||
runs-on: ubuntu-latest
|
||||
name: Build and pack npm packages
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
|
||||
- name: Build Web
|
||||
run: yarn build:browser
|
||||
|
||||
- name: Pack the web and server packages
|
||||
run: |
|
||||
yarn workspace @actual-app/web pack --filename @actual-app/web.tgz
|
||||
yarn workspace @actual-app/sync-server pack --filename @actual-app/sync-server.tgz
|
||||
|
||||
- name: Build API
|
||||
run: yarn build:api
|
||||
|
||||
- name: Pack the api package
|
||||
run: |
|
||||
yarn workspace @actual-app/api pack --filename @actual-app/api.tgz
|
||||
|
||||
- name: Upload package artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: npm-packages
|
||||
path: |
|
||||
packages/desktop-client/@actual-app/web.tgz
|
||||
packages/sync-server/@actual-app/sync-server.tgz
|
||||
packages/api/@actual-app/api.tgz
|
||||
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
name: Publish npm packages
|
||||
needs: build-and-pack
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Download the artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: npm-packages
|
||||
|
||||
- name: Setup node and npm registry
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Publish Web
|
||||
run: |
|
||||
npm publish desktop-client/@actual-app/web.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish Sync-Server
|
||||
run: |
|
||||
npm publish sync-server/@actual-app/sync-server.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Publish API
|
||||
run: |
|
||||
npm publish api/@actual-app/api.tgz --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
12
.github/workflows/update-vrt.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: /update-vrt
|
||||
on:
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
image: mcr.microsoft.com/playwright:v1.41.1-jammy
|
||||
steps:
|
||||
- name: Get PR branch
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
# Until https://github.com/xt0rted/pull-request-comment-branch/issues/322 is resolved we use the forked version
|
||||
uses: gotson/pull-request-comment-branch@head-repo-owner-dist
|
||||
id: comment-branch
|
||||
- uses: actions/checkout@v4
|
||||
@@ -31,6 +31,10 @@ jobs:
|
||||
ref: ${{ steps.comment-branch.outputs.head_ref }}
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Run VRT Tests on Desktop app
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xvfb-run --auto-servernum --server-args="-screen 0 1920x1080x24" -- yarn e2e:desktop --update-snapshots
|
||||
- name: Wait for Netlify build to finish
|
||||
id: netlify
|
||||
env:
|
||||
@@ -97,7 +101,7 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: "rocket"
|
||||
reaction: 'rocket'
|
||||
|
||||
add-starting-reaction:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -112,4 +116,4 @@ jobs:
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commentId: ${{ github.event.comment.id }}
|
||||
reaction: "+1"
|
||||
reaction: '+1'
|
||||
|
||||
4
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
!data/.gitkeep
|
||||
/data2
|
||||
Actual-*
|
||||
!actual-server.js
|
||||
**/xcuserdata/*
|
||||
export-2020-01-10.csv
|
||||
|
||||
@@ -56,3 +57,6 @@ package.tgz
|
||||
|
||||
# Fly.io configuration
|
||||
fly.toml
|
||||
|
||||
# TypeScript cache
|
||||
build/
|
||||
|
||||
@@ -1 +1,30 @@
|
||||
sync_pb.*
|
||||
packages/api/app/bundle.api.js
|
||||
packages/api/app/stats.json
|
||||
packages/api/dist
|
||||
packages/api/@types
|
||||
packages/api/migrations
|
||||
packages/crdt/dist
|
||||
packages/component-library/src/icons/**/*
|
||||
packages/desktop-client/bundle.browser.js
|
||||
packages/desktop-client/build/
|
||||
packages/desktop-client/locale/
|
||||
packages/desktop-client/build-electron/
|
||||
packages/desktop-client/build-stats/
|
||||
packages/desktop-client/public/kcab/
|
||||
packages/desktop-client/public/data/
|
||||
packages/desktop-client/**/node_modules/*
|
||||
packages/desktop-client/node_modules/
|
||||
packages/desktop-client/test-results/
|
||||
packages/desktop-client/playwright-report/
|
||||
packages/desktop-electron/client-build/
|
||||
packages/desktop-electron/build/
|
||||
packages/desktop-electron/dist/
|
||||
packages/import-ynab4/**/node_modules/*
|
||||
packages/import-ynab5/**/node_modules/*
|
||||
packages/loot-core/**/node_modules/*
|
||||
packages/loot-core/**/lib-dist/*
|
||||
packages/loot-core/**/proto/*
|
||||
.yarn/*
|
||||
.github/*
|
||||
upcoming-release-notes/*
|
||||
|
||||
894
.yarn/releases/yarn-4.3.1.cjs
vendored
935
.yarn/releases/yarn-4.7.0.cjs
vendored
Executable file
@@ -6,4 +6,4 @@ enableTransparentWorkspaces: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.3.1.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.7.0.cjs
|
||||
|
||||
@@ -66,7 +66,11 @@ To add new feature requests, open a new Issue of the "Feature Request" type.
|
||||
|
||||
### Translation
|
||||
|
||||
Make Actual Budget accessible to more people by helping with the [Internationalization](https://actualbudget.org/docs/contributing/i18n/) of Actual. We are using a crowd sourcing tool to manage the translations, see our [Weblate Project](https://hosted.weblate.org/projects/actualbudget/). Weblate proudly supports open-source software projects through their [Libre plan](https://weblate.org/en/hosting/#libre).
|
||||
Make Actual Budget accessible to more people by helping with the [Internationalization](https://actualbudget.org/docs/contributing/i18n/) of Actual. We are using a crowd sourcing tool to manage the translations, see our [Weblate Project](https://hosted.weblate.org/projects/actualbudget/). Weblate proudly supports open-source software projects through their [Libre plan](https://weblate.org/en/hosting/#libre).
|
||||
|
||||
<a href="https://hosted.weblate.org/engage/actualbudget/">
|
||||
<img src="https://hosted.weblate.org/widget/actualbudget/actual/287x66-grey.png" alt="Translation status" />
|
||||
</a>
|
||||
|
||||
## Repo Activity
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ RELEASE=""
|
||||
CI=${CI:-false}
|
||||
|
||||
cd "$ROOT/.."
|
||||
|
||||
POSITIONAL=()
|
||||
SKIP_EXE_BUILD=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
key="$1"
|
||||
|
||||
@@ -16,53 +16,56 @@ while [[ $# -gt 0 ]]; do
|
||||
RELEASE="production"
|
||||
shift
|
||||
;;
|
||||
--skip-exe-build)
|
||||
SKIP_EXE_BUILD=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
POSITIONAL+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
set -- "${POSITIONAL[@]}"
|
||||
|
||||
if [ "$OSTYPE" == "msys" ]; then
|
||||
if [ $CI != true ]; then
|
||||
read -s -p "Windows certificate password: " -r CSC_KEY_PASSWORD
|
||||
export CSC_KEY_PASSWORD
|
||||
elif [ -n "$CIRCLE_TAG" ]; then
|
||||
# We only want to run this on CircleCI as Github doesn't have the CSC_KEY_PASSWORD secret set.
|
||||
certutil -f -p ${CSC_KEY_PASSWORD} -importPfx ~/windows-shift-reset-llc.p12
|
||||
fi
|
||||
fi
|
||||
|
||||
yarn workspace loot-core build:node
|
||||
|
||||
# Get translations
|
||||
echo "Updating translations..."
|
||||
if ! [ -d packages/desktop-client/locale ]; then
|
||||
git clone https://github.com/actualbudget/translations packages/desktop-client/locale
|
||||
fi
|
||||
pushd packages/desktop-client/locale > /dev/null
|
||||
git checkout .
|
||||
git pull
|
||||
popd > /dev/null
|
||||
packages/desktop-client/bin/remove-untranslated-languages
|
||||
|
||||
yarn workspace loot-core build:node
|
||||
yarn workspace @actual-app/web build --mode=desktop # electron specific build
|
||||
|
||||
# required for running the sync-server server
|
||||
yarn workspace loot-core build:browser
|
||||
yarn workspace @actual-app/web build:browser
|
||||
|
||||
yarn workspace desktop-electron update-client
|
||||
|
||||
(
|
||||
cd packages/desktop-electron;
|
||||
yarn clean;
|
||||
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build
|
||||
|
||||
echo "\nCreated release"
|
||||
if [ $SKIP_EXE_BUILD == true ]; then
|
||||
echo "Building the dist"
|
||||
yarn build:dist
|
||||
echo "Skipping exe build"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build
|
||||
if [ "$RELEASE" == "production" ]; then
|
||||
if [ -f ../../.secret-tokens ]; then
|
||||
source ../../.secret-tokens
|
||||
fi
|
||||
yarn build
|
||||
|
||||
echo "Created release"
|
||||
else
|
||||
SKIP_NOTARIZATION=true yarn build
|
||||
fi
|
||||
fi
|
||||
)
|
||||
|
||||
182
bin/release-note-generator.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { exec } from 'node:child_process';
|
||||
import { existsSync, writeFile } from 'node:fs';
|
||||
import { exit } from 'node:process';
|
||||
|
||||
import prompts from 'prompts';
|
||||
|
||||
async function run() {
|
||||
const username = await execAsync(
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
"gh api user --jq '.login'",
|
||||
'To avoid having to enter your username, consider installing the official GitHub CLI (https://github.com/cli/cli) and logging in with `gh auth login`.',
|
||||
);
|
||||
const activePr = await getActivePr(username);
|
||||
if (activePr) {
|
||||
console.log(
|
||||
`Found potentially matching PR ${activePr.number}: ${activePr.title}`,
|
||||
);
|
||||
}
|
||||
const prNumber = activePr?.number ?? (await getNextPrNumber());
|
||||
|
||||
const result = await prompts([
|
||||
{
|
||||
name: 'githubUsername',
|
||||
message: 'Comma-separated GitHub username(s)',
|
||||
type: 'text',
|
||||
initial: username,
|
||||
},
|
||||
{
|
||||
name: 'pullRequestNumber',
|
||||
message: 'PR Number',
|
||||
type: 'number',
|
||||
initial: prNumber,
|
||||
},
|
||||
{
|
||||
name: 'releaseNoteType',
|
||||
message: 'Release Note Type',
|
||||
type: 'select',
|
||||
choices: [
|
||||
{ title: 'Features', value: 'Features' },
|
||||
{ title: 'Enhancements', value: 'Enhancements' },
|
||||
{ title: 'Bugfix', value: 'Bugfix' },
|
||||
{ title: 'Maintenance', value: 'Maintenance' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'oneLineSummary',
|
||||
message: 'Brief Summary',
|
||||
type: 'text',
|
||||
initial: activePr?.title,
|
||||
},
|
||||
]);
|
||||
|
||||
if (
|
||||
!result.githubUsername ||
|
||||
!result.oneLineSummary ||
|
||||
!result.releaseNoteType
|
||||
) {
|
||||
console.log('All questions must be answered. Exiting');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const fileContents = getFileContents(
|
||||
result.releaseNoteType,
|
||||
result.githubUsername,
|
||||
result.oneLineSummary,
|
||||
);
|
||||
|
||||
const filepath = `./upcoming-release-notes/${prNumber}.md`;
|
||||
if (existsSync(filepath)) {
|
||||
const { confirm } = await prompts({
|
||||
name: 'confirm',
|
||||
type: 'confirm',
|
||||
message: `This will overwrite the existing release note ${filepath} Are you sure?`,
|
||||
});
|
||||
if (!confirm) {
|
||||
console.log('Exiting');
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(filepath, fileContents, err => {
|
||||
if (err) {
|
||||
console.error('Failed to write release note file:', err);
|
||||
exit(1);
|
||||
} else {
|
||||
console.log(
|
||||
`Release note generated successfully: ./upcoming-release-notes/${prNumber}.md`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// makes an attempt to find an existing open PR from <username>:<branch>
|
||||
async function getActivePr(
|
||||
username: string,
|
||||
): Promise<{ number: number; title: string } | undefined> {
|
||||
if (!username) {
|
||||
return undefined;
|
||||
}
|
||||
const branchName = await execAsync('git rev-parse --abbrev-ref HEAD');
|
||||
if (!branchName) {
|
||||
return undefined;
|
||||
}
|
||||
const forkHead = `${username}:${branchName}`;
|
||||
return getPrNumberFromHead(forkHead);
|
||||
}
|
||||
|
||||
async function getPrNumberFromHead(
|
||||
head: string,
|
||||
): Promise<{ number: number; title: string } | undefined> {
|
||||
try {
|
||||
// head is a weird query parameter in this API call. If nothing matches, it
|
||||
// will return as if the head query parameter doesn't exist. To get around
|
||||
// this, we make the page size 2 and only return the number if the length.
|
||||
const resp = await fetch(
|
||||
'https://api.github.com/repos/actualbudget/actual/pulls?state=open&per_page=2&head=' +
|
||||
head,
|
||||
);
|
||||
if (!resp.ok) {
|
||||
console.warn('error fetching from github pulls api:', resp.status);
|
||||
return undefined;
|
||||
}
|
||||
const ghResponse = await resp.json();
|
||||
if (ghResponse?.length === 1) {
|
||||
return ghResponse[0];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('error fetching from github pulls api:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function getNextPrNumber(): Promise<number> {
|
||||
try {
|
||||
const resp = await fetch(
|
||||
'https://api.github.com/repos/actualbudget/actual/issues?state=all&per_page=1',
|
||||
);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`API responded with status: ${resp.status}`);
|
||||
}
|
||||
const ghResponse = await resp.json();
|
||||
const latestPrNumber = ghResponse?.[0]?.number;
|
||||
if (!latestPrNumber) {
|
||||
console.error(
|
||||
'Could not find latest issue number in GitHub API response',
|
||||
ghResponse,
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
return latestPrNumber + 1;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch next PR number:', error);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getFileContents(type: string, username: string, summary: string) {
|
||||
return `---
|
||||
category: ${type}
|
||||
authors: [${username}]
|
||||
---
|
||||
|
||||
${summary}
|
||||
`;
|
||||
}
|
||||
|
||||
// simple exec that fails silently and returns an empty string on failure
|
||||
async function execAsync(cmd: string, errorLog?: string): Promise<string> {
|
||||
return new Promise<string>(res => {
|
||||
exec(cmd, (error, stdout) => {
|
||||
if (error) {
|
||||
console.log(errorLog);
|
||||
res('');
|
||||
} else {
|
||||
res(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -15,4 +15,3 @@ services:
|
||||
volumes:
|
||||
- '.:/app'
|
||||
restart: 'no'
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import globals from 'globals';
|
||||
|
||||
import pluginImport from 'eslint-plugin-import';
|
||||
import pluginJSXA11y from 'eslint-plugin-jsx-a11y';
|
||||
import pluginPrettier from 'eslint-plugin-prettier/recommended';
|
||||
import pluginReact from 'eslint-plugin-react';
|
||||
import pluginReactHooks from 'eslint-plugin-react-hooks';
|
||||
import pluginRulesDir from 'eslint-plugin-rulesdir';
|
||||
@@ -90,6 +89,7 @@ export default [
|
||||
{
|
||||
ignores: [
|
||||
'packages/api/app/bundle.api.js',
|
||||
'packages/api/app/stats.json',
|
||||
'packages/api/dist',
|
||||
'packages/api/@types',
|
||||
'packages/api/migrations',
|
||||
@@ -117,6 +117,26 @@ export default [
|
||||
'.github/*',
|
||||
],
|
||||
},
|
||||
{
|
||||
// Temporary until the sync-server is migrated to TypeScript
|
||||
files: [
|
||||
'packages/sync-server/**/*.spec.js?(x)',
|
||||
'packages/sync-server/**/*.test.js?(x)',
|
||||
],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
vi: true,
|
||||
describe: true,
|
||||
expect: true,
|
||||
it: true,
|
||||
beforeAll: true,
|
||||
beforeEach: true,
|
||||
afterAll: true,
|
||||
afterEach: true,
|
||||
test: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: true,
|
||||
@@ -125,7 +145,6 @@ export default [
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.commonjs,
|
||||
...globals.jest,
|
||||
...globals.node,
|
||||
globalThis: false,
|
||||
vi: true,
|
||||
@@ -145,7 +164,6 @@ export default [
|
||||
},
|
||||
pluginReact.configs.flat.recommended,
|
||||
pluginReact.configs.flat['jsx-runtime'],
|
||||
pluginPrettier,
|
||||
...pluginTypescript.configs.recommended,
|
||||
pluginImport.flatConfigs.recommended,
|
||||
{
|
||||
@@ -529,7 +547,7 @@ export default [
|
||||
sourceType: 'module',
|
||||
|
||||
parserOptions: {
|
||||
project: [path.join(__dirname, './tsconfig.json')],
|
||||
projectService: true,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
@@ -549,6 +567,13 @@ export default [
|
||||
// 'tsc' already handles this (https://github.com/typescript-eslint/typescript-eslint/issues/477)
|
||||
'no-undef': 'off',
|
||||
|
||||
// TypeScript already handles these (https://typescript-eslint.io/troubleshooting/typed-linting/performance/#eslint-plugin-import)
|
||||
'import/named': 'off',
|
||||
'import/namespace': 'off',
|
||||
'import/default': 'off',
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-unresolved': 'off',
|
||||
|
||||
// Add TypeScript specific rules (and turn off ESLint equivalents)
|
||||
'@typescript-eslint/consistent-type-assertions': 'warn',
|
||||
'no-array-constructor': 'off',
|
||||
@@ -746,6 +771,16 @@ export default [
|
||||
'import/no-unresolved': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
// Allow configuring vitest with default exports (recommended as per vitest docs)
|
||||
{
|
||||
files: ['**/vitest.config.ts', '**/vitest.web.config.ts'],
|
||||
rules: {
|
||||
'import/no-anonymous-default-export': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{},
|
||||
{
|
||||
// TODO: fix the issues in these files
|
||||
|
||||
53
package.json
@@ -22,53 +22,61 @@
|
||||
"start:server": "yarn workspace @actual-app/sync-server start",
|
||||
"start:server-monitor": "yarn workspace @actual-app/sync-server start-monitor",
|
||||
"start:server-dev": "NODE_ENV=development BROWSER_OPEN=localhost:5006 yarn npm-run-all --parallel 'start:server-monitor' 'start'",
|
||||
"start:desktop": "yarn rebuild-electron && npm-run-all --parallel 'start:desktop-*'",
|
||||
"start:desktop": "yarn desktop-dependencies && npm-run-all --parallel 'start:desktop-*'",
|
||||
"desktop-dependencies": "yarn rebuild-electron && yarn workspace loot-core build:browser",
|
||||
"start:desktop-node": "yarn workspace loot-core watch:node",
|
||||
"start:desktop-client": "yarn workspace @actual-app/web watch",
|
||||
"start:desktop-server-client": "yarn workspace @actual-app/web build:browser",
|
||||
"start:desktop-electron": "yarn workspace desktop-electron watch",
|
||||
"start:electron": "yarn start:desktop",
|
||||
"start:browser": "npm-run-all --parallel 'start:browser-*'",
|
||||
"start:browser-backend": "yarn workspace loot-core watch:browser",
|
||||
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
|
||||
"build:server": "yarn build:browser",
|
||||
"build:browser": "./bin/package-browser",
|
||||
"build:desktop": "./bin/package-electron",
|
||||
"build:api": "yarn workspace @actual-app/api build",
|
||||
"generate:i18n": "yarn workspace @actual-app/web generate:i18n",
|
||||
"generate:release-notes": "ts-node ./bin/release-note-generator.ts",
|
||||
"test": "yarn workspaces foreach --all --parallel --verbose run test",
|
||||
"test:debug": "yarn workspaces foreach --all --verbose run test",
|
||||
"e2e": "yarn workspaces foreach --all --parallel --verbose run e2e",
|
||||
"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",
|
||||
"vrt": "yarn workspaces foreach --all --parallel --verbose run vrt",
|
||||
"vrt:docker": "./bin/run-vrt",
|
||||
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
|
||||
"rebuild-node": "yarn workspace loot-core rebuild",
|
||||
"lint": "eslint . --max-warnings 0",
|
||||
"lint:verbose": "DEBUG=eslint:cli-engine eslint . --max-warnings 0",
|
||||
"lint": "prettier --check . && eslint . --max-warnings 0",
|
||||
"lint:fix": "prettier --check --write . && eslint . --max-warnings 0 --fix",
|
||||
"install:server": "yarn workspaces focus @actual-app/sync-server --production",
|
||||
"typecheck": "yarn tsc && tsc-strict",
|
||||
"typecheck": "yarn tsc --incremental && tsc-strict",
|
||||
"jq": "./node_modules/node-jq/bin/jq",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/prompts": "^2.4.9",
|
||||
"@typescript-eslint/parser": "^8.26.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-import-resolver-typescript": "^4.2.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"globals": "^15.13.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.9",
|
||||
"node-jq": "^4.0.1",
|
||||
"html-to-image": "^1.11.11",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.5.0",
|
||||
"node-jq": "^4.0.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier": "^3.5.3",
|
||||
"prompts": "^2.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript-eslint": "^8.18.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"typescript-strict-plugin": "^2.4.4"
|
||||
},
|
||||
"resolutions": {
|
||||
@@ -78,9 +86,12 @@
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,json}": "prettier --write"
|
||||
"*.{js,jsx,ts,tsx,md,json,yml}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@4.3.1",
|
||||
"packageManager": "yarn@4.7.0",
|
||||
"browserslist": [
|
||||
"electron 24.0",
|
||||
"defaults"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`API setup and teardown successfully loads budget 1`] = `
|
||||
Array [
|
||||
exports[`API setup and teardown > successfully loads budget 1`] = `
|
||||
[
|
||||
"2016-10",
|
||||
"2016-11",
|
||||
"2016-12",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
module.exports = {
|
||||
moduleFileExtensions: [
|
||||
'testing.js',
|
||||
'testing.ts',
|
||||
'api.js',
|
||||
'api.ts',
|
||||
'api.tsx',
|
||||
'electron.js',
|
||||
'electron.ts',
|
||||
'mjs',
|
||||
'js',
|
||||
'ts',
|
||||
'tsx',
|
||||
'json',
|
||||
],
|
||||
testEnvironment: 'node',
|
||||
testPathIgnorePatterns: ['/node_modules/'],
|
||||
watchPathIgnorePatterns: ['<rootDir>/mocks/budgets/'],
|
||||
setupFilesAfterEnv: ['<rootDir>/../loot-core/src/mocks/setup.ts'],
|
||||
transformIgnorePatterns: ['/node_modules/'],
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
};
|
||||
@@ -6,10 +6,9 @@ import * as api from './index';
|
||||
|
||||
const budgetName = 'test-budget';
|
||||
|
||||
beforeEach(async () => {
|
||||
// we need real datetime if we are going to mix new timestamps with our mock data
|
||||
global.restoreDateNow();
|
||||
global.IS_TESTING = true;
|
||||
|
||||
beforeEach(async () => {
|
||||
const budgetPath = path.join(__dirname, '/mocks/budgets/', budgetName);
|
||||
await fs.rm(budgetPath, { force: true, recursive: true });
|
||||
|
||||
@@ -654,4 +653,60 @@ describe('API CRUD operations', () => {
|
||||
);
|
||||
expect(transactions).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('Transactions: import notes are preserved when importing', async () => {
|
||||
const accountId = await api.createAccount({ name: 'test-account' }, 0);
|
||||
|
||||
// Test with notes
|
||||
const transactionsWithNotes = [
|
||||
{
|
||||
date: '2023-11-03',
|
||||
imported_id: '11',
|
||||
amount: 100,
|
||||
notes: 'test note',
|
||||
},
|
||||
];
|
||||
|
||||
const addResultWithNotes = await api.addTransactions(
|
||||
accountId,
|
||||
transactionsWithNotes,
|
||||
{
|
||||
learnCategories: true,
|
||||
runTransfers: true,
|
||||
},
|
||||
);
|
||||
expect(addResultWithNotes).toBe('ok');
|
||||
|
||||
let transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions[0].notes).toBe('test note');
|
||||
|
||||
// Clear transactions
|
||||
await api.deleteTransaction(transactions[0].id);
|
||||
|
||||
// Test without notes
|
||||
const transactionsWithoutNotes = [
|
||||
{ date: '2023-11-03', imported_id: '11', amount: 100, notes: null },
|
||||
];
|
||||
|
||||
const addResultWithoutNotes = await api.addTransactions(
|
||||
accountId,
|
||||
transactionsWithoutNotes,
|
||||
{
|
||||
learnCategories: true,
|
||||
runTransfers: true,
|
||||
},
|
||||
);
|
||||
expect(addResultWithoutNotes).toBe('ok');
|
||||
|
||||
transactions = await api.getTransactions(
|
||||
accountId,
|
||||
'2023-11-01',
|
||||
'2023-11-30',
|
||||
);
|
||||
expect(transactions[0].notes).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "25.3.1",
|
||||
"version": "25.5.0",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
@@ -14,27 +14,25 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build:app": "yarn workspace loot-core build:api",
|
||||
"build:crdt": "yarn workspace @actual-app/crdt build",
|
||||
"build:node": "tsc --p tsconfig.dist.json && tsc-alias -p tsconfig.dist.json",
|
||||
"build:migrations": "cp migrations/*.sql dist/migrations",
|
||||
"build:default-db": "cp default-db.sqlite dist/",
|
||||
"build": "yarn run clean && yarn run build:app && yarn run build:node && yarn run build:migrations && yarn run build:default-db",
|
||||
"test": "yarn run build:app && jest -c jest.config.js",
|
||||
"test": "yarn run build:app && yarn run build:crdt && vitest",
|
||||
"clean": "rm -rf dist @types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actual-app/crdt": "workspace:^",
|
||||
"better-sqlite3": "^11.7.0",
|
||||
"compare-versions": "^6.1.0",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.5.3",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"tsc-alias": "^1.8.8",
|
||||
"typescript": "^5.5.4"
|
||||
"@types/uuid": "^9.0.8",
|
||||
"tsc-alias": "^1.8.11",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
}
|
||||
},
|
||||
"include": ["."],
|
||||
"exclude": ["**/node_modules/*", "dist", "@types"]
|
||||
"exclude": ["**/node_modules/*", "dist", "@types", "*.test.ts"]
|
||||
}
|
||||
|
||||
9
packages/api/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
test: {
|
||||
globals: true,
|
||||
onConsoleLog(log: string, type: 'stdout' | 'stderr'): boolean | void {
|
||||
// print only console.error
|
||||
return type === 'stderr';
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -3,17 +3,19 @@
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=18.2"
|
||||
"react": ">=18.2",
|
||||
"react-dom": ">=18.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/css": "^11.13.4",
|
||||
"react-aria-components": "^1.7.0",
|
||||
"usehooks-ts": "^3.0.1"
|
||||
"@emotion/css": "^11.13.5",
|
||||
"react-aria-components": "^1.7.1",
|
||||
"usehooks-ts": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/cli": "^8.1.0",
|
||||
"@types/react": "^18.2.0",
|
||||
"react": "18.2.0"
|
||||
"@types/react": "^19.1.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"exports": {
|
||||
"./hooks/*": "./src/hooks/*.ts",
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, {
|
||||
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import { useResponsive } from './hooks/useResponsive';
|
||||
import { styles, type CSSProperties } from './styles';
|
||||
import { theme } from './theme';
|
||||
|
||||
@@ -19,7 +20,7 @@ export const defaultInputStyle = {
|
||||
border: '1px solid ' + theme.formInputBorder,
|
||||
};
|
||||
|
||||
type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
export type InputProps = InputHTMLAttributes<HTMLInputElement> & {
|
||||
style?: CSSProperties;
|
||||
inputRef?: Ref<HTMLInputElement>;
|
||||
onEnter?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
@@ -101,3 +102,9 @@ export function BigInput(props: InputProps) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResponsiveInput(props: InputProps) {
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
|
||||
return isNarrowWidth ? <BigInput {...props} /> : <Input {...props} />;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@ import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type ComponentProps,
|
||||
type ComponentType,
|
||||
type SVGProps,
|
||||
type CSSProperties,
|
||||
} from 'react';
|
||||
|
||||
import { Button } from './Button';
|
||||
import { Text } from './Text';
|
||||
import { theme } from './theme';
|
||||
import { Toggle } from './Toggle';
|
||||
@@ -61,6 +63,7 @@ type MenuProps<NameType> = {
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
getItemStyle?: (item: MenuItemObject<NameType>) => CSSProperties;
|
||||
slot?: ComponentProps<typeof Button>['slot'];
|
||||
};
|
||||
|
||||
export function Menu<const NameType = string>({
|
||||
@@ -71,6 +74,7 @@ export function Menu<const NameType = string>({
|
||||
style,
|
||||
className,
|
||||
getItemStyle,
|
||||
slot,
|
||||
}: MenuProps<NameType>) {
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const items = allItems.filter(x => x);
|
||||
@@ -161,9 +165,10 @@ export function Menu<const NameType = string>({
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<View
|
||||
role="button"
|
||||
<Button
|
||||
key={String(item.name)}
|
||||
variant="bare"
|
||||
slot={slot}
|
||||
style={{
|
||||
cursor: 'default',
|
||||
padding: 10,
|
||||
@@ -179,11 +184,9 @@ export function Menu<const NameType = string>({
|
||||
}),
|
||||
...(!isLabel(item) && getItemStyle?.(item)),
|
||||
}}
|
||||
onPointerEnter={() => setHoveredIndex(idx)}
|
||||
onPointerLeave={() => setHoveredIndex(null)}
|
||||
onPointerUp={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
onHoverStart={() => setHoveredIndex(idx)}
|
||||
onHoverEnd={() => setHoveredIndex(null)}
|
||||
onPress={() => {
|
||||
if (
|
||||
!item.disabled &&
|
||||
item.toggle === undefined &&
|
||||
@@ -232,7 +235,7 @@ export function Menu<const NameType = string>({
|
||||
</View>
|
||||
)}
|
||||
{item.key && <Keybinding keyName={item.key} />}
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{footer}
|
||||
|
||||
@@ -21,7 +21,14 @@ function getChildren(key, children) {
|
||||
'type' in child &&
|
||||
child.type === Fragment
|
||||
) {
|
||||
return list.concat(getChildren(child.key, child.props.children));
|
||||
return list.concat(
|
||||
getChildren(
|
||||
child.key,
|
||||
typeof child.props === 'object' && 'children' in child.props
|
||||
? child.props.children
|
||||
: [],
|
||||
),
|
||||
);
|
||||
}
|
||||
list.push({ key: key + child['key'], child });
|
||||
return list;
|
||||
|
||||
@@ -26,7 +26,7 @@ export const Tooltip = ({
|
||||
const triggerRef = useRef(null);
|
||||
const [isHovered, setIsHover] = useState(false);
|
||||
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
|
||||
|
||||
const handlePointerEnter = useCallback(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import type { SVGProps } from 'react';
|
||||
export const SvgArrowButtonSingleLeft1 = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
color: 'inherit',
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M.25 12a2.643 2.643 0 0 1 .775-1.875L10.566.584a1.768 1.768 0 0 1 2.5 2.5l-8.739 8.739a.25.25 0 0 0 0 .354l8.739 8.739a1.768 1.768 0 0 1-2.5 2.5l-9.541-9.541A2.643 2.643 0 0 1 .25 12Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
<svg id="Bold" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>arrow-button-single-left-1</title><path d="M.25,12a2.643,2.643,0,0,1,.775-1.875L10.566.584a1.768,1.768,0,0,1,2.5,2.5L4.327,11.823a.25.25,0,0,0,0,.354l8.739,8.739a1.768,1.768,0,0,1-2.5,2.5L1.025,13.875A2.643,2.643,0,0,1,.25,12Z"/></svg>
|
||||
|
After Width: | Height: | Size: 312 B |
@@ -2,6 +2,7 @@ export { SvgAlertTriangle } from './AlertTriangle';
|
||||
export { SvgArrowButtonDown1 } from './ArrowButtonDown1';
|
||||
export { SvgArrowButtonLeft1 } from './ArrowButtonLeft1';
|
||||
export { SvgArrowButtonRight1 } from './ArrowButtonRight1';
|
||||
export { SvgArrowButtonSingleLeft1 } from './ArrowButtonSingleLeft1';
|
||||
export { SvgArrowButtonUp1 } from './ArrowButtonUp1';
|
||||
export { SvgArrowsExpand3 } from './ArrowsExpand3';
|
||||
export { SvgArrowsShrink3 } from './ArrowsShrink3';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './src/main';
|
||||
export * from './src';
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
};
|
||||
@@ -12,20 +12,17 @@
|
||||
"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/",
|
||||
"test": "jest -c jest.config.js"
|
||||
"test": "vitest --globals"
|
||||
},
|
||||
"dependencies": {
|
||||
"google-protobuf": "^3.12.0-rc.1",
|
||||
"google-protobuf": "^3.12.4",
|
||||
"murmurhash": "^2.0.1",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/core": "^1.5.3",
|
||||
"@swc/jest": "^0.2.36",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/uuid": "^9.0.2",
|
||||
"jest": "^27.5.1",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"ts-protoc-gen": "^0.15.0",
|
||||
"typescript": "^5.5.4"
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`merkle trie adding an item works 1`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
exports[`merkle trie > adding an item works 1`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
"hash": 1983295247,
|
||||
},
|
||||
"hash": 1983295247,
|
||||
@@ -34,14 +34,14 @@ Object {
|
||||
},
|
||||
"hash": 1983295247,
|
||||
},
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
"0": {
|
||||
"hash": 1469038940,
|
||||
},
|
||||
"hash": 1469038940,
|
||||
@@ -78,33 +78,33 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`merkle trie pruning works and keeps correct hashes 1`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
exports[`merkle trie > pruning works and keeps correct hashes 1`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"0": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1000,
|
||||
},
|
||||
"hash": 1000,
|
||||
},
|
||||
"hash": 1000,
|
||||
},
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1100,
|
||||
},
|
||||
"hash": 1100,
|
||||
@@ -113,28 +113,28 @@ Object {
|
||||
},
|
||||
"hash": 1956,
|
||||
},
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1200,
|
||||
},
|
||||
"hash": 1200,
|
||||
},
|
||||
"hash": 1200,
|
||||
},
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1400,
|
||||
},
|
||||
"hash": 1400,
|
||||
@@ -143,28 +143,28 @@ Object {
|
||||
},
|
||||
"hash": 1244,
|
||||
},
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": {
|
||||
"0": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1500,
|
||||
},
|
||||
"hash": 1500,
|
||||
},
|
||||
"hash": 1500,
|
||||
},
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1700,
|
||||
},
|
||||
"hash": 1700,
|
||||
@@ -175,29 +175,29 @@ Object {
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"hash": 1800,
|
||||
},
|
||||
"hash": 1800,
|
||||
},
|
||||
"hash": 1800,
|
||||
},
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": {
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"2": {
|
||||
"1": {
|
||||
"1": {
|
||||
"hash": 2000,
|
||||
},
|
||||
"hash": 2000,
|
||||
@@ -206,10 +206,10 @@ Object {
|
||||
},
|
||||
"hash": 1972,
|
||||
},
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"hash": 2100,
|
||||
},
|
||||
"hash": 2100,
|
||||
@@ -246,33 +246,33 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`merkle trie pruning works and keeps correct hashes 2`] = `
|
||||
Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"0": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
exports[`merkle trie > pruning works and keeps correct hashes 2`] = `
|
||||
{
|
||||
"1": {
|
||||
"2": {
|
||||
"1": {
|
||||
"0": {
|
||||
"0": {
|
||||
"2": {
|
||||
"2": {
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"hash": 1300,
|
||||
},
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1400,
|
||||
},
|
||||
"hash": 1400,
|
||||
@@ -281,19 +281,19 @@ Object {
|
||||
},
|
||||
"hash": 1244,
|
||||
},
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": {
|
||||
"1": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"2": Object {
|
||||
"2": Object {
|
||||
"0": Object {
|
||||
"2": {
|
||||
"2": {
|
||||
"0": {
|
||||
"hash": 1700,
|
||||
},
|
||||
"hash": 1700,
|
||||
@@ -304,20 +304,20 @@ Object {
|
||||
},
|
||||
"hash": 1600,
|
||||
},
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"1": {
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"hash": 1900,
|
||||
},
|
||||
"2": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"2": {
|
||||
"1": {
|
||||
"1": {
|
||||
"hash": 2000,
|
||||
},
|
||||
"hash": 2000,
|
||||
@@ -326,10 +326,10 @@ Object {
|
||||
},
|
||||
"hash": 1972,
|
||||
},
|
||||
"1": Object {
|
||||
"0": Object {
|
||||
"1": Object {
|
||||
"1": Object {
|
||||
"1": {
|
||||
"0": {
|
||||
"1": {
|
||||
"1": {
|
||||
"hash": 2100,
|
||||
},
|
||||
"hash": 2100,
|
||||
|
||||
@@ -39,6 +39,7 @@ HTTPS=true yarn start
|
||||
```
|
||||
|
||||
or using the dev container:
|
||||
|
||||
```
|
||||
HTTPS=true docker compose up --build
|
||||
```
|
||||
@@ -82,6 +83,7 @@ E2E_START_URL=https://ip:port yarn vrt
|
||||
You can also run the tests against a remote server by passing the URL:
|
||||
|
||||
Run in standardized docker container:
|
||||
|
||||
```sh
|
||||
E2E_START_URL=https://my-remote-server.com yarn vrt:docker
|
||||
|
||||
@@ -90,6 +92,7 @@ E2E_START_URL=https://my-remote-server.com yarn vrt:docker
|
||||
```
|
||||
|
||||
Run locally:
|
||||
|
||||
```sh
|
||||
E2E_START_URL=https://my-remote-server.com yarn vrt
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ const processTranslations = () => {
|
||||
|
||||
console.log(`en.json has ${enKeysCount} keys.`);
|
||||
|
||||
files.forEach((file) => {
|
||||
files.forEach(file => {
|
||||
if (file === 'en.json' || path.extname(file) !== '.json') return;
|
||||
|
||||
if (file.startsWith('en-')) {
|
||||
@@ -34,7 +34,9 @@ const processTranslations = () => {
|
||||
|
||||
// Calculate the percentage of keys present compared to en.json
|
||||
const percentage = (fileKeysCount / enKeysCount) * 100;
|
||||
console.log(`${file} has ${fileKeysCount} keys (${percentage.toFixed(2)}%).`);
|
||||
console.log(
|
||||
`${file} has ${fileKeysCount} keys (${percentage.toFixed(2)}%).`,
|
||||
);
|
||||
|
||||
if (percentage < 50) {
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
@@ -161,5 +161,28 @@ test.describe('Accounts', () => {
|
||||
|
||||
await expect(importButton).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('import notes checkbox is not shown for CSV files', async () => {
|
||||
const fileChooserPromise = page.waitForEvent('filechooser');
|
||||
await accountPage.page.getByRole('button', { name: 'Import' }).click();
|
||||
|
||||
const fileChooser = await fileChooserPromise;
|
||||
await fileChooser.setFiles(join(__dirname, 'data/test.csv'));
|
||||
|
||||
// Verify the import notes checkbox is not visible for CSV files
|
||||
const importNotesCheckbox = page.getByRole('checkbox', {
|
||||
name: 'Import notes from file',
|
||||
});
|
||||
await expect(importNotesCheckbox).not.toBeVisible();
|
||||
|
||||
// Import the transactions
|
||||
const importButton = page.getByRole('button', {
|
||||
name: /Import \d+ transactions/,
|
||||
});
|
||||
await importButton.click();
|
||||
|
||||
// Verify the transactions were imported
|
||||
await expect(importButton).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 29 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: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 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: 32 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: 33 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 31 KiB |
|
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: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |