diff --git a/.github/workflows/update-vrt.yml b/.github/workflows/update-vrt.yml new file mode 100644 index 00000000000..1806255dd52 --- /dev/null +++ b/.github/workflows/update-vrt.yml @@ -0,0 +1,50 @@ +name: /update-vrt +on: + issue_comment: + types: [ created ] + +permissions: + pull-requests: write + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + update-vrt: + name: Update VRT + runs-on: ubuntu-latest + if: | + github.event.issue.pull_request && + contains(github.event.comment.body, '/update-vrt') + steps: + - name: Get PR branch + uses: xt0rted/pull-request-comment-branch@v2 + id: comment-branch + - uses: actions/checkout@v4 + with: + ref: ${{ steps.comment-branch.outputs.head_ref }} + - name: Set up environment + uses: ./.github/actions/setup + - name: Wait for Netlify build to finish + id: netlify + env: + COMMIT_SHA: ${{ steps.comment-branch.outputs.head_sha }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./.github/actions/netlify-wait-for-build + - name: Run VRT Tests on Netlify URL + run: yarn vrt --update-snapshots + env: + E2E_START_URL: ${{ steps.netlify.outputs.url }} + - name: Commit and push changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add "**/*.png" + if git diff --staged --quiet; then + echo "No changes to commit" + exit 0 + fi + git commit -m "Update VRT" + git push origin HEAD:${{ steps.comment-branch.outputs.head_ref }} diff --git a/packages/api/package.json b/packages/api/package.json index afba87e9574..b44b0152ff5 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@actual-app/api", - "version": "24.10.1", + "version": "24.11.0", "license": "MIT", "description": "An API for Actual", "engines": { diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png index fa2f9434209..d7fdcf4f754 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-10-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png index ba91cd0fa05..a70a6870cda 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-11-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png index 5bc5d5a483f..11a107a25f3 100644 Binary files a/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png and b/packages/desktop-client/e2e/schedules.test.js-snapshots/Schedules-creates-a-new-schedule-posts-the-transaction-and-later-completes-it-12-chromium-linux.png differ diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json index bc81bc77bad..657fb96bb4f 100644 --- a/packages/desktop-client/package.json +++ b/packages/desktop-client/package.json @@ -1,6 +1,6 @@ { "name": "@actual-app/web", - "version": "24.10.1", + "version": "24.11.0", "license": "MIT", "files": [ "build" diff --git a/packages/desktop-client/src/browser-preload.browser.js b/packages/desktop-client/src/browser-preload.browser.js index 4a04b0e23c0..95c048247c8 100644 --- a/packages/desktop-client/src/browser-preload.browser.js +++ b/packages/desktop-client/src/browser-preload.browser.js @@ -1,4 +1,5 @@ import { initBackend as initSQLBackend } from 'absurd-sql/dist/indexeddb-main-thread'; +import { registerSW } from 'virtual:pwa-register'; import * as Platform from 'loot-core/src/client/platform'; @@ -39,6 +40,19 @@ function createBackendWorker() { createBackendWorker(); +let isUpdateReadyForDownload = false; +let markUpdateReadyForDownload; +const isUpdateReadyForDownloadPromise = new Promise(resolve => { + markUpdateReadyForDownload = () => { + isUpdateReadyForDownload = true; + resolve(true); + }; +}); +const updateSW = registerSW({ + immediate: true, + onNeedRefresh: markUpdateReadyForDownload, +}); + global.Actual = { IS_DEV, ACTUAL_VERSION, @@ -140,7 +154,14 @@ global.Actual = { window.open(url, '_blank'); }, onEventFromMain: () => {}, - applyAppUpdate: () => {}, + isUpdateReadyForDownload: () => isUpdateReadyForDownload, + waitForUpdateReadyForDownload: () => isUpdateReadyForDownloadPromise, + applyAppUpdate: async () => { + updateSW(); + + // Wait for the app to reload + await new Promise(() => {}); + }, updateAppMenu: () => {}, ipcConnect: () => {}, diff --git a/packages/desktop-client/src/components/App.tsx b/packages/desktop-client/src/components/App.tsx index f7785d58df4..75841207b0c 100644 --- a/packages/desktop-client/src/components/App.tsx +++ b/packages/desktop-client/src/components/App.tsx @@ -51,8 +51,20 @@ function AppInner() { const { showBoundary: showErrorBoundary } = useErrorBoundary(); const dispatch = useDispatch(); + const maybeUpdate = async (cb?: () => T): Promise => { + if (global.Actual.isUpdateReadyForDownload()) { + dispatch( + setAppState({ + loadingText: t('Downloading and applying update...'), + }), + ); + await global.Actual.applyAppUpdate(); + } + return cb?.(); + }; + async function init() { - const socketName = await global.Actual.getServerSocket(); + const socketName = await maybeUpdate(() => global.Actual.getServerSocket()); dispatch( setAppState({ @@ -86,14 +98,16 @@ function AppInner() { loadingText: t('Retrieving remote files...'), }), ); - send('get-remote-files').then(files => { - if (files) { - const remoteFile = files.find(f => f.fileId === cloudFileId); - if (remoteFile && remoteFile.deleted) { - dispatch(closeBudget()); - } + + const files = await send('get-remote-files'); + if (files) { + const remoteFile = files.find(f => f.fileId === cloudFileId); + if (remoteFile && remoteFile.deleted) { + dispatch(closeBudget()); } - }); + } + + await maybeUpdate(); } } diff --git a/packages/desktop-client/src/components/FinancesApp.tsx b/packages/desktop-client/src/components/FinancesApp.tsx index b585d5d30ee..387b8bbcac5 100644 --- a/packages/desktop-client/src/components/FinancesApp.tsx +++ b/packages/desktop-client/src/components/FinancesApp.tsx @@ -100,6 +100,29 @@ export function FinancesApp() { }, 100); }, []); + useEffect(() => { + async function run() { + await global.Actual.waitForUpdateReadyForDownload(); + dispatch( + addNotification({ + type: 'message', + title: t('A new version of Actual is available!'), + message: t('Click the button below to reload and apply the update.'), + sticky: true, + id: 'update-reload-notification', + button: { + title: t('Update now'), + action: async () => { + await global.Actual.applyAppUpdate(); + }, + }, + }), + ); + } + + run(); + }, []); + useEffect(() => { async function run() { const latestVersion = await getLatestVersion(); diff --git a/packages/desktop-client/src/components/ManageRules.tsx b/packages/desktop-client/src/components/ManageRules.tsx index f3eb583005a..bb9ce717ae5 100644 --- a/packages/desktop-client/src/components/ManageRules.tsx +++ b/packages/desktop-client/src/components/ManageRules.tsx @@ -204,6 +204,13 @@ export function ManageRules({ setLoading(false); } + async function onDeleteRule(id: string) { + setLoading(true); + await send('rule-delete', id); + await loadRules(); + setLoading(false); + } + const onEditRule = useCallback(rule => { dispatch( pushModal('edit-rule', { @@ -306,6 +313,7 @@ export function ManageRules({ hoveredRule={hoveredRule} onHover={onHover} onEditRule={onEditRule} + onDeleteRule={rule => onDeleteRule(rule.id)} /> )} diff --git a/packages/desktop-client/src/components/accounts/Account.tsx b/packages/desktop-client/src/components/accounts/Account.tsx index 1a8009db0b5..37936e10c7d 100644 --- a/packages/desktop-client/src/components/accounts/Account.tsx +++ b/packages/desktop-client/src/components/accounts/Account.tsx @@ -1803,6 +1803,15 @@ class AccountInternal extends PureComponent< sortField={this.state.sort?.field} ascDesc={this.state.sort?.ascDesc} onChange={this.onTransactionsChange} + onBatchDelete={this.onBatchDelete} + onBatchDuplicate={this.onBatchDuplicate} + onBatchLinkSchedule={this.onBatchLinkSchedule} + onBatchUnlinkSchedule={this.onBatchUnlinkSchedule} + onCreateRule={this.onCreateRule} + onScheduleAction={this.onScheduleAction} + onMakeAsNonSplitTransactions={ + this.onMakeAsNonSplitTransactions + } onRefetch={this.refetchTransactions} onCloseAddTransaction={() => this.setState({ isAdding: false }) diff --git a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx b/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx similarity index 84% rename from packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx rename to packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx index 29da40e1357..04c59a44ace 100644 --- a/packages/desktop-client/src/components/accounts/AccountSyncCheck.jsx +++ b/packages/desktop-client/src/components/accounts/AccountSyncCheck.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { Trans } from 'react-i18next'; import { useDispatch, useSelector } from 'react-redux'; import { useParams } from 'react-router-dom'; @@ -6,6 +6,7 @@ import { useParams } from 'react-router-dom'; import { t } from 'i18next'; import { unlinkAccount } from 'loot-core/client/actions'; +import { type AccountEntity } from 'loot-core/types/models'; import { authorizeBank } from '../../gocardless'; import { useAccounts } from '../../hooks/useAccounts'; @@ -16,7 +17,7 @@ import { Link } from '../common/Link'; import { Popover } from '../common/Popover'; import { View } from '../common/View'; -function getErrorMessage(type, code) { +function getErrorMessage(type: string, code: string) { switch (type.toUpperCase()) { case 'ITEM_ERROR': switch (code.toUpperCase()) { @@ -81,7 +82,29 @@ export function AccountSyncCheck() { const [open, setOpen] = useState(false); const triggerRef = useRef(null); - if (!failedAccounts) { + const reauth = useCallback( + (acc: AccountEntity) => { + setOpen(false); + + if (acc.account_id) { + authorizeBank(dispatch, { upgradingAccountId: acc.account_id }); + } + }, + [dispatch], + ); + + const unlink = useCallback( + (acc: AccountEntity) => { + if (acc.id) { + dispatch(unlinkAccount(acc.id)); + } + + setOpen(false); + }, + [dispatch], + ); + + if (!failedAccounts || !id) { return null; } @@ -91,22 +114,15 @@ export function AccountSyncCheck() { } const account = accounts.find(account => account.id === id); + if (!account) { + return null; + } + const { type, code } = error; const showAuth = (type === 'ITEM_ERROR' && code === 'ITEM_LOGIN_REQUIRED') || (type === 'INVALID_INPUT' && code === 'INVALID_ACCESS_TOKEN'); - function reauth() { - setOpen(false); - - authorizeBank(dispatch, { upgradingAccountId: account.account_id }); - } - - async function unlink() { - dispatch(unlinkAccount(account.id)); - setOpen(false); - } - return ( ) : ( - )} diff --git a/packages/desktop-client/src/components/budget/SidebarCategory.tsx b/packages/desktop-client/src/components/budget/SidebarCategory.tsx index 7e20348176b..4d3b53e7a04 100644 --- a/packages/desktop-client/src/components/budget/SidebarCategory.tsx +++ b/packages/desktop-client/src/components/budget/SidebarCategory.tsx @@ -7,6 +7,7 @@ import { type CategoryEntity, } from 'loot-core/src/types/models'; +import { useFeatureFlag } from '../../hooks/useFeatureFlag'; import { SvgCheveronDown } from '../../icons/v1'; import { theme } from '../../style'; import { Button } from '../common/Button2'; @@ -51,6 +52,7 @@ export function SidebarCategory({ const temporary = category.id === 'new'; const [menuOpen, setMenuOpen] = useState(false); const triggerRef = useRef(null); + const contextMenusEnabled = useFeatureFlag('contextMenus'); const displayed = ( { + if (!contextMenusEnabled) return; + e.preventDefault(); + setMenuOpen(true); }} >
{category.name}
- +