diff --git a/.github/workflows/update-vrt.yml b/.github/workflows/update-vrt.yml new file mode 100644 index 00000000000..e044d1e81cf --- /dev/null +++ b/.github/workflows/update-vrt.yml @@ -0,0 +1,55 @@ +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') + container: + 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 + uses: gotson/pull-request-comment-branch@head-repo-owner-dist + id: comment-branch + - uses: actions/checkout@v4 + with: + repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }} + 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 --system --add safe.directory "*" + 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/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/BudgetCategories.jsx b/packages/desktop-client/src/components/budget/BudgetCategories.jsx index d6a1a52f796..12efffd809b 100644 --- a/packages/desktop-client/src/components/budget/BudgetCategories.jsx +++ b/packages/desktop-client/src/components/budget/BudgetCategories.jsx @@ -28,6 +28,7 @@ export const BudgetCategories = memo( onSaveGroup, onDeleteCategory, onDeleteGroup, + onApplyBudgetTemplatesInGroup, onReorderCategory, onReorderGroup, }) => { @@ -245,6 +246,7 @@ export const BudgetCategories = memo( onReorderCategory={onReorderCategory} onToggleCollapse={onToggleCollapse} onShowNewCategory={onShowNewCategory} + onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} /> ); break; diff --git a/packages/desktop-client/src/components/budget/BudgetTable.jsx b/packages/desktop-client/src/components/budget/BudgetTable.jsx index 24d72bf8718..785457f2ab5 100644 --- a/packages/desktop-client/src/components/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/budget/BudgetTable.jsx @@ -28,6 +28,7 @@ export function BudgetTable(props) { onDeleteCategory, onSaveGroup, onDeleteGroup, + onApplyBudgetTemplatesInGroup, onReorderCategory, onReorderGroup, onShowActivity, @@ -235,6 +236,7 @@ export function BudgetTable(props) { onReorderGroup={_onReorderGroup} onBudgetAction={onBudgetAction} onShowActivity={onShowActivity} + onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} /> diff --git a/packages/desktop-client/src/components/budget/ExpenseGroup.tsx b/packages/desktop-client/src/components/budget/ExpenseGroup.tsx index d30e7c4f3d3..5fdbf16d848 100644 --- a/packages/desktop-client/src/components/budget/ExpenseGroup.tsx +++ b/packages/desktop-client/src/components/budget/ExpenseGroup.tsx @@ -25,6 +25,9 @@ type ExpenseGroupProps = { onEditName?: ComponentProps['onEdit']; onSave?: ComponentProps['onSave']; onDelete?: ComponentProps['onDelete']; + onApplyBudgetTemplatesInGroup?: ComponentProps< + typeof SidebarGroup + >['onApplyBudgetTemplatesInGroup']; onDragChange: OnDragChangeCallback< ComponentProps['group'] >; @@ -43,6 +46,7 @@ export function ExpenseGroup({ onEditName, onSave, onDelete, + onApplyBudgetTemplatesInGroup, onDragChange, onReorderGroup, onReorderCategory, @@ -125,6 +129,7 @@ export function ExpenseGroup({ onEdit={onEditName} onSave={onSave} onDelete={onDelete} + onApplyBudgetTemplatesInGroup={onApplyBudgetTemplatesInGroup} onShowNewCategory={onShowNewCategory} /> 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}
- +