Compare commits
9 Commits
accounts-f
...
fix-exhaus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1ea12c3dd | ||
|
|
f522d18636 | ||
|
|
900b294975 | ||
|
|
5a34c06859 | ||
|
|
92c93b3f6e | ||
|
|
34ffc5c4b2 | ||
|
|
14b0cd7b1d | ||
|
|
daca767808 | ||
|
|
6111f94b51 |
21
.github/actions/setup/action.yml
vendored
@@ -1,5 +1,15 @@
|
||||
name: Setup
|
||||
|
||||
inputs:
|
||||
working-directory:
|
||||
description: 'Working directory to run in, default .'
|
||||
required: false
|
||||
default: '.'
|
||||
download-translations:
|
||||
description: 'Whether to download translations as part of setup, default true'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
@@ -15,9 +25,16 @@ runs:
|
||||
uses: actions/cache@v4
|
||||
id: cache
|
||||
with:
|
||||
path: '**/node_modules'
|
||||
key: yarn-v1-${{ runner.os }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('**/yarn.lock') }}
|
||||
path: ${{ format('{0}/**/node_modules', inputs.working-directory) }}
|
||||
key: yarn-v1-${{ runner.os }}-${{ hashFiles(format('{0}/.nvmrc', inputs.working-directory)) }}-${{ hashFiles(format('{0}/**/yarn.lock', inputs.working-directory)) }}
|
||||
- name: Install
|
||||
working-directory: ${{ inputs.working-directory }}
|
||||
run: yarn --immutable
|
||||
shell: bash
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
- name: Download translations
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: actualbudget/translations
|
||||
path: ${{ inputs.working-directory }}/packages/desktop-client/locale
|
||||
if: ${{ inputs.download-translations == 'true' }}
|
||||
|
||||
98
.github/workflows/i18n-string-extract-master.yml
vendored
@@ -1,36 +1,80 @@
|
||||
name: Extract and upload i18n strings
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
# 4am UTC
|
||||
- cron: "0 4 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
extract-and-upload-i18n-strings:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'actualbudget/actual'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up environment
|
||||
uses: ./.github/actions/setup
|
||||
- name: Configure i18n client
|
||||
run: |
|
||||
pip install wlc
|
||||
- name: Generate i18n strings
|
||||
run: yarn generate:i18n
|
||||
- name: Upload i18n strings
|
||||
run: |
|
||||
if [[ ! -f packages/desktop-client/locale/en.json ]]; then
|
||||
echo "File packages/desktop-client/locale/en.json not found. Ensure the file was generated correctly."
|
||||
exit 1
|
||||
fi
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
upload \
|
||||
--author-name "Actual Budget" \
|
||||
--author-email "dev@actualbudget.org" \
|
||||
--method add \
|
||||
--input packages/desktop-client/locale/en.json \
|
||||
actualbudget/actual/en
|
||||
echo "Translations uploaded"
|
||||
- name: Check out main repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: actual
|
||||
- name: Set up environment
|
||||
uses: ./actual/.github/actions/setup
|
||||
with:
|
||||
working-directory: actual
|
||||
download-translations: false # As we'll manually clone instead
|
||||
- name: Configure Git config
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
- name: Configure i18n client
|
||||
run: |
|
||||
pip install wlc
|
||||
|
||||
- name: Lock translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
lock \
|
||||
actualbudget/actual
|
||||
|
||||
- name: Update VCS with latest translations
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
pull \
|
||||
actualbudget/actual
|
||||
- name: Check out updated translations
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ssh-key: ${{ secrets.STRING_IMPORT_DEPLOY_KEY }}
|
||||
repository: actualbudget/translations
|
||||
path: translations
|
||||
- name: Generate i18n strings
|
||||
working-directory: actual
|
||||
run: |
|
||||
mkdir -p packages/desktop-client/locale/
|
||||
cp ../translations/en.json packages/desktop-client/locale/
|
||||
yarn generate:i18n
|
||||
if [[ ! -f packages/desktop-client/locale/en.json ]]; then
|
||||
echo "File packages/desktop-client/locale/en.json not found. Ensure the file was generated correctly."
|
||||
exit 1
|
||||
fi
|
||||
- name: Check in new i18n strings
|
||||
working-directory: translations
|
||||
run: |
|
||||
cp ../actual/packages/desktop-client/locale/en.json .
|
||||
git add .
|
||||
if git commit -m "Update source strings"; then
|
||||
git push
|
||||
else
|
||||
echo "No changes to commit"
|
||||
fi
|
||||
|
||||
- name: Unlock translations
|
||||
if: always() # Clean up even on failure
|
||||
run: |
|
||||
wlc \
|
||||
--url https://hosted.weblate.org/api/ \
|
||||
--key "${{ secrets.WEBLATE_API_KEY_CI_STRINGS }}" \
|
||||
unlock \
|
||||
actualbudget/actual
|
||||
|
||||
@@ -715,7 +715,6 @@ export default [
|
||||
'packages/desktop-client/src/components/accounts/Account.jsx',
|
||||
'packages/desktop-client/src/components/accounts/MobileAccount.jsx',
|
||||
'packages/desktop-client/src/components/accounts/MobileAccounts.jsx',
|
||||
'packages/desktop-client/src/components/App.tsx',
|
||||
'packages/desktop-client/src/components/budget/BudgetCategories.jsx',
|
||||
'packages/desktop-client/src/components/budget/BudgetSummaries.tsx',
|
||||
'packages/desktop-client/src/components/budget/DynamicBudgetTable.tsx',
|
||||
@@ -724,7 +723,6 @@ export default [
|
||||
'packages/desktop-client/src/components/budget/envelope/HoldMenu.tsx',
|
||||
'packages/desktop-client/src/components/budget/envelope/TransferMenu.tsx',
|
||||
'packages/desktop-client/src/components/common/Menu.tsx',
|
||||
'packages/desktop-client/src/components/FinancesApp.tsx',
|
||||
'packages/desktop-client/src/components/GlobalKeys.ts',
|
||||
'packages/desktop-client/src/components/LoggedInUser.tsx',
|
||||
'packages/desktop-client/src/components/manager/ManagementApp.jsx',
|
||||
|
||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 121 KiB |
@@ -53,74 +53,78 @@ function AppInner() {
|
||||
const userData = useSelector(state => state.user.data);
|
||||
const { signOut, addNotification } = useActions();
|
||||
|
||||
const maybeUpdate = async <T,>(cb?: () => T): Promise<T> => {
|
||||
if (global.Actual.isUpdateReadyForDownload()) {
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Downloading and applying update...'),
|
||||
}),
|
||||
);
|
||||
await global.Actual.applyAppUpdate();
|
||||
}
|
||||
return cb?.();
|
||||
};
|
||||
|
||||
async function init() {
|
||||
const socketName = await maybeUpdate(() => global.Actual.getServerSocket());
|
||||
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Initializing the connection to the local database...'),
|
||||
}),
|
||||
);
|
||||
await initConnection(socketName);
|
||||
|
||||
// Load any global prefs
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Loading global preferences...'),
|
||||
}),
|
||||
);
|
||||
await dispatch(loadGlobalPrefs());
|
||||
|
||||
// Open the last opened budget, if any
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Opening last budget...'),
|
||||
}),
|
||||
);
|
||||
const budgetId = await send('get-last-opened-backup');
|
||||
if (budgetId) {
|
||||
await dispatch(loadBudget(budgetId));
|
||||
|
||||
// Check to see if this file has been remotely deleted (but
|
||||
// don't block on this in case they are offline or something)
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Retrieving remote files...'),
|
||||
}),
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const maybeUpdate = async <T,>(cb?: () => T): Promise<T> => {
|
||||
if (global.Actual.isUpdateReadyForDownload()) {
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Downloading and applying update...'),
|
||||
}),
|
||||
);
|
||||
await global.Actual.applyAppUpdate();
|
||||
}
|
||||
return cb?.();
|
||||
};
|
||||
|
||||
async function init() {
|
||||
const socketName = await maybeUpdate(() =>
|
||||
global.Actual.getServerSocket(),
|
||||
);
|
||||
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t(
|
||||
'Initializing the connection to the local database...',
|
||||
),
|
||||
}),
|
||||
);
|
||||
await initConnection(socketName);
|
||||
|
||||
// Load any global prefs
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Loading global preferences...'),
|
||||
}),
|
||||
);
|
||||
await dispatch(loadGlobalPrefs());
|
||||
|
||||
// Open the last opened budget, if any
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Opening last budget...'),
|
||||
}),
|
||||
);
|
||||
const budgetId = await send('get-last-opened-backup');
|
||||
if (budgetId) {
|
||||
await dispatch(loadBudget(budgetId));
|
||||
|
||||
// Check to see if this file has been remotely deleted (but
|
||||
// don't block on this in case they are offline or something)
|
||||
dispatch(
|
||||
setAppState({
|
||||
loadingText: t('Retrieving remote files...'),
|
||||
}),
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
async function initAll() {
|
||||
await Promise.all([installPolyfills(), init()]);
|
||||
dispatch(setAppState({ loadingText: null }));
|
||||
}
|
||||
|
||||
initAll().catch(showErrorBoundary);
|
||||
}, []);
|
||||
}, [cloudFileId, dispatch, showErrorBoundary, t]);
|
||||
|
||||
useEffect(() => {
|
||||
global.Actual.updateAppMenu(budgetId);
|
||||
@@ -140,7 +144,7 @@ function AppInner() {
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [userData, userData?.tokenExpired]);
|
||||
}, [addNotification, signOut, t, userData?.tokenExpired]);
|
||||
|
||||
return budgetId ? <FinancesApp /> : <ManagementApp />;
|
||||
}
|
||||
@@ -185,7 +189,7 @@ export function App() {
|
||||
window.removeEventListener('focus', checkScrollbars);
|
||||
window.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
};
|
||||
}, [dispatch]);
|
||||
}, [dispatch, hiddenScrollbars]);
|
||||
|
||||
const [theme] = useTheme();
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ export function FinancesApp() {
|
||||
setTimeout(async () => {
|
||||
await dispatch(sync());
|
||||
}, 100);
|
||||
}, []);
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
@@ -126,7 +126,7 @@ export function FinancesApp() {
|
||||
}
|
||||
|
||||
run();
|
||||
}, []);
|
||||
}, [dispatch, t]);
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
@@ -159,7 +159,7 @@ export function FinancesApp() {
|
||||
}
|
||||
|
||||
run();
|
||||
}, [lastUsedVersion, setLastUsedVersion]);
|
||||
}, [dispatch, lastUsedVersion, setLastUsedVersion, t]);
|
||||
|
||||
const scrollableRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
@@ -117,8 +117,8 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen('sync-event', ({ type, subtype, syncDisabled }) => {
|
||||
if (type === 'start') {
|
||||
const unlisten = listen('sync-event', event => {
|
||||
if (event.type === 'start') {
|
||||
setSyncing(true);
|
||||
setSyncState(null);
|
||||
} else {
|
||||
@@ -130,19 +130,19 @@ function SyncButton({ style, isMobile = false }: SyncButtonProps) {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
if (type === 'error') {
|
||||
if (event.type === 'error') {
|
||||
// Use the offline state if either there is a network error or
|
||||
// if this file isn't a "cloud file". You can't sync a local
|
||||
// file.
|
||||
if (subtype === 'network') {
|
||||
if (event.subtype === 'network') {
|
||||
setSyncState('offline');
|
||||
} else if (!cloudFileId) {
|
||||
setSyncState('local');
|
||||
} else {
|
||||
setSyncState('error');
|
||||
}
|
||||
} else if (type === 'success') {
|
||||
setSyncState(syncDisabled ? 'disabled' : null);
|
||||
} else if (event.type === 'success') {
|
||||
setSyncState(event.syncDisabled ? 'disabled' : null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -108,14 +108,16 @@ function BudgetInner(props: BudgetInnerProps) {
|
||||
run();
|
||||
|
||||
const unlistens = [
|
||||
listen('sync-event', ({ type, tables }) => {
|
||||
if (
|
||||
type === 'success' &&
|
||||
(tables.includes('categories') ||
|
||||
listen('sync-event', event => {
|
||||
if (event.type === 'success') {
|
||||
const tables = event.tables;
|
||||
if (
|
||||
tables.includes('categories') ||
|
||||
tables.includes('category_mapping') ||
|
||||
tables.includes('category_groups'))
|
||||
) {
|
||||
loadCategories();
|
||||
tables.includes('category_groups')
|
||||
) {
|
||||
loadCategories();
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
@@ -267,8 +267,9 @@ function TransactionListWithPreviews({
|
||||
}, [accountId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
return listen('sync-event', ({ type, tables }) => {
|
||||
if (type === 'applied') {
|
||||
return listen('sync-event', event => {
|
||||
if (event.type === 'applied') {
|
||||
const tables = event.tables;
|
||||
if (
|
||||
tables.includes('transactions') ||
|
||||
tables.includes('category_mapping') ||
|
||||
|
||||
@@ -62,8 +62,9 @@ export function CategoryTransactions({
|
||||
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
|
||||
|
||||
useEffect(() => {
|
||||
return listen('sync-event', ({ type, tables }) => {
|
||||
if (type === 'applied') {
|
||||
return listen('sync-event', event => {
|
||||
if (event.type === 'applied') {
|
||||
const tables = event.tables;
|
||||
if (
|
||||
tables.includes('transactions') ||
|
||||
tables.includes('category_mapping') ||
|
||||
|
||||
@@ -69,15 +69,17 @@ export function Budget() {
|
||||
|
||||
init();
|
||||
|
||||
const unlisten = listen('sync-event', ({ type, tables }) => {
|
||||
if (
|
||||
type === 'success' &&
|
||||
(tables.includes('categories') ||
|
||||
const unlisten = listen('sync-event', event => {
|
||||
if (event.type === 'success') {
|
||||
const tables = event.tables;
|
||||
if (
|
||||
tables.includes('categories') ||
|
||||
tables.includes('category_mapping') ||
|
||||
tables.includes('category_groups'))
|
||||
) {
|
||||
// TODO: is this loading every time?
|
||||
dispatch(getCategories());
|
||||
tables.includes('category_groups')
|
||||
) {
|
||||
// TODO: is this loading every time?
|
||||
dispatch(getCategories());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -48,9 +48,9 @@ export function ManagePayeesWithData({
|
||||
}
|
||||
loadData();
|
||||
|
||||
const unlisten = listen('sync-event', async ({ type, tables }) => {
|
||||
if (type === 'applied') {
|
||||
if (tables.includes('rules')) {
|
||||
const unlisten = listen('sync-event', async event => {
|
||||
if (event.type === 'applied') {
|
||||
if (event.tables.includes('rules')) {
|
||||
await refetchRuleCounts();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// @ts-strict-ignore
|
||||
import React, { useState, type CSSProperties } from 'react';
|
||||
import React, { useMemo, useState, type CSSProperties } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { css } from '@emotion/css';
|
||||
@@ -203,6 +203,12 @@ export function BarGraph({
|
||||
|
||||
const leftMargin = Math.abs(largestValue) > 1000000 ? 20 : 0;
|
||||
|
||||
// Sort the data in the bar chart
|
||||
const unsortedData = data[splitData];
|
||||
const sortedData = useMemo(() => {
|
||||
return unsortedData.sort((a, b) => a[balanceTypeOp] - b[balanceTypeOp]);
|
||||
}, [unsortedData, balanceTypeOp]);
|
||||
|
||||
return (
|
||||
<Container
|
||||
style={{
|
||||
@@ -219,7 +225,7 @@ export function BarGraph({
|
||||
width={width}
|
||||
height={height}
|
||||
stackOffset="sign"
|
||||
data={data[splitData]}
|
||||
data={sortedData}
|
||||
style={{ cursor: pointer }}
|
||||
margin={{
|
||||
top: labelsMargin,
|
||||
|
||||
@@ -21,19 +21,20 @@ export function handleGlobalEvents(actions: BoundActions, store: Store<State>) {
|
||||
});
|
||||
});
|
||||
|
||||
listen('schedules-offline', ({ payees }) => {
|
||||
actions.pushModal('schedule-posts-offline-notification', { payees });
|
||||
listen('schedules-offline', () => {
|
||||
actions.pushModal('schedule-posts-offline-notification');
|
||||
});
|
||||
|
||||
// This is experimental: we sync data locally automatically when
|
||||
// data changes from the backend
|
||||
listen('sync-event', async ({ type, tables }) => {
|
||||
listen('sync-event', async event => {
|
||||
// We don't need to query anything until the file is loaded, and
|
||||
// sync events might come in if the file is being synced before
|
||||
// being loaded (happens when downloading)
|
||||
const prefs = store.getState().prefs.local;
|
||||
if (prefs && prefs.id) {
|
||||
if (type === 'applied') {
|
||||
if (event.type === 'applied') {
|
||||
const tables = event.tables;
|
||||
if (tables.includes('payees') || tables.includes('payee_mapping')) {
|
||||
actions.getPayees();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { isNonProductionEnvironment } from 'loot-core/src/shared/environment';
|
||||
@@ -42,7 +41,7 @@ export function usePreferredDarkTheme() {
|
||||
}
|
||||
|
||||
export function ThemeStyle() {
|
||||
const [theme] = useTheme();
|
||||
const [activeTheme] = useTheme();
|
||||
const [darkThemePreference] = usePreferredDarkTheme();
|
||||
const [themeColors, setThemeColors] = useState<
|
||||
| typeof lightTheme
|
||||
@@ -53,7 +52,7 @@ export function ThemeStyle() {
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === 'auto') {
|
||||
if (activeTheme === 'auto') {
|
||||
const darkTheme = themes[darkThemePreference];
|
||||
|
||||
function darkThemeMediaQueryListener(event: MediaQueryListEvent) {
|
||||
@@ -85,18 +84,218 @@ export function ThemeStyle() {
|
||||
);
|
||||
};
|
||||
} else {
|
||||
setThemeColors(themes[theme].colors);
|
||||
setThemeColors(themes[activeTheme]?.colors);
|
||||
}
|
||||
}, [theme, darkThemePreference]);
|
||||
}, [activeTheme, darkThemePreference]);
|
||||
|
||||
if (!themeColors) return null;
|
||||
|
||||
const css = Object.keys(themeColors)
|
||||
.map(key => ` --color-${key}: ${themeColors[key]};`)
|
||||
const css = Object.entries(themeColors)
|
||||
.map(([key, value]) => ` --color-${key}: ${value};`)
|
||||
.join('\n');
|
||||
return <style>{`:root {\n${css}}`}</style>;
|
||||
}
|
||||
|
||||
export const theme = Object.fromEntries(
|
||||
Object.keys(lightTheme).map(key => [key, `var(--color-${key})`]),
|
||||
) as Record<keyof typeof lightTheme, string>;
|
||||
export const theme = {
|
||||
pageBackground: 'var(--color-pageBackground)',
|
||||
pageBackgroundModalActive: 'var(--color-pageBackgroundModalActive)',
|
||||
pageBackgroundTopLeft: 'var(--color-pageBackgroundTopLeft)',
|
||||
pageBackgroundBottomRight: 'var(--color-pageBackgroundBottomRight)',
|
||||
pageBackgroundLineTop: 'var(--color-pageBackgroundLineTop)',
|
||||
pageBackgroundLineMid: 'var(--color-pageBackgroundLineMid)',
|
||||
pageBackgroundLineBottom: 'var(--color-pageBackgroundLineBottom)',
|
||||
pageText: 'var(--color-pageText)',
|
||||
pageTextLight: 'var(--color-pageTextLight)',
|
||||
pageTextSubdued: 'var(--color-pageTextSubdued)',
|
||||
pageTextDark: 'var(--color-pageTextDark)',
|
||||
pageTextPositive: 'var(--color-pageTextPositive)',
|
||||
pageTextLink: 'var(--color-pageTextLink)',
|
||||
pageTextLinkLight: 'var(--color-pageTextLinkLight)',
|
||||
cardBackground: 'var(--color-cardBackground)',
|
||||
cardBorder: 'var(--color-cardBorder)',
|
||||
cardShadow: 'var(--color-cardShadow)',
|
||||
tableBackground: 'var(--color-tableBackground)',
|
||||
tableRowBackgroundHover: 'var(--color-tableRowBackgroundHover)',
|
||||
tableText: 'var(--color-tableText)',
|
||||
tableTextLight: 'var(--color-tableTextLight)',
|
||||
tableTextSubdued: 'var(--color-tableTextSubdued)',
|
||||
tableTextSelected: 'var(--color-tableTextSelected)',
|
||||
tableTextHover: 'var(--color-tableTextHover)',
|
||||
tableTextInactive: 'var(--color-tableTextInactive)',
|
||||
tableHeaderText: 'var(--color-tableHeaderText)',
|
||||
tableHeaderBackground: 'var(--color-tableHeaderBackground)',
|
||||
tableBorder: 'var(--color-tableBorder)',
|
||||
tableBorderSelected: 'var(--color-tableBorderSelected)',
|
||||
tableBorderHover: 'var(--color-tableBorderHover)',
|
||||
tableBorderSeparator: 'var(--color-tableBorderSeparator)',
|
||||
tableRowBackgroundHighlight: 'var(--color-tableRowBackgroundHighlight)',
|
||||
tableRowBackgroundHighlightText:
|
||||
'var(--color-tableRowBackgroundHighlightText)',
|
||||
tableRowHeaderBackground: 'var(--color-tableRowHeaderBackground)',
|
||||
tableRowHeaderText: 'var(--color-tableRowHeaderText)',
|
||||
sidebarBackground: 'var(--color-sidebarBackground)',
|
||||
sidebarItemBackgroundPending: 'var(--color-sidebarItemBackgroundPending)',
|
||||
sidebarItemBackgroundPositive: 'var(--color-sidebarItemBackgroundPositive)',
|
||||
sidebarItemBackgroundFailed: 'var(--color-sidebarItemBackgroundFailed)',
|
||||
sidebarItemAccentSelected: 'var(--color-sidebarItemAccentSelected)',
|
||||
sidebarItemBackgroundHover: 'var(--color-sidebarItemBackgroundHover)',
|
||||
sidebarItemText: 'var(--color-sidebarItemText)',
|
||||
sidebarItemTextSelected: 'var(--color-sidebarItemTextSelected)',
|
||||
menuBackground: 'var(--color-menuBackground)',
|
||||
menuItemBackground: 'var(--color-menuItemBackground)',
|
||||
menuItemBackgroundHover: 'var(--color-menuItemBackgroundHover)',
|
||||
menuItemText: 'var(--color-menuItemText)',
|
||||
menuItemTextHover: 'var(--color-menuItemTextHover)',
|
||||
menuItemTextSelected: 'var(--color-menuItemTextSelected)',
|
||||
menuItemTextHeader: 'var(--color-menuItemTextHeader)',
|
||||
menuBorder: 'var(--color-menuBorder)',
|
||||
menuBorderHover: 'var(--color-menuBorderHover)',
|
||||
menuKeybindingText: 'var(--color-menuKeybindingText)',
|
||||
menuAutoCompleteBackground: 'var(--color-menuAutoCompleteBackground)',
|
||||
menuAutoCompleteBackgroundHover:
|
||||
'var(--color-menuAutoCompleteBackgroundHover)',
|
||||
menuAutoCompleteText: 'var(--color-menuAutoCompleteText)',
|
||||
menuAutoCompleteTextHover: 'var(--color-menuAutoCompleteTextHover)',
|
||||
menuAutoCompleteTextHeader: 'var(--color-menuAutoCompleteTextHeader)',
|
||||
menuAutoCompleteItemTextHover: 'var(--color-menuAutoCompleteItemTextHover)',
|
||||
menuAutoCompleteItemText: 'var(--color-menuAutoCompleteItemText)',
|
||||
modalBackground: 'var(--color-modalBackground)',
|
||||
modalBorder: 'var(--color-modalBorder)',
|
||||
mobileHeaderBackground: 'var(--color-mobileHeaderBackground)',
|
||||
mobileHeaderText: 'var(--color-mobileHeaderText)',
|
||||
mobileHeaderTextSubdued: 'var(--color-mobileHeaderTextSubdued)',
|
||||
mobileHeaderTextHover: 'var(--color-mobileHeaderTextHover)',
|
||||
mobilePageBackground: 'var(--color-mobilePageBackground)',
|
||||
mobileNavBackground: 'var(--color-mobileNavBackground)',
|
||||
mobileNavItem: 'var(--color-mobileNavItem)',
|
||||
mobileNavItemSelected: 'var(--color-mobileNavItemSelected)',
|
||||
mobileAccountShadow: 'var(--color-mobileAccountShadow)',
|
||||
mobileAccountText: 'var(--color-mobileAccountText)',
|
||||
mobileTransactionSelected: 'var(--color-mobileTransactionSelected)',
|
||||
mobileViewTheme: 'var(--color-mobileViewTheme)',
|
||||
mobileConfigServerViewTheme: 'var(--color-mobileConfigServerViewTheme)',
|
||||
markdownNormal: 'var(--color-markdownNormal)',
|
||||
markdownDark: 'var(--color-markdownDark)',
|
||||
markdownLight: 'var(--color-markdownLight)',
|
||||
buttonMenuText: 'var(--color-buttonMenuText)',
|
||||
buttonMenuTextHover: 'var(--color-buttonMenuTextHover)',
|
||||
buttonMenuBackground: 'var(--color-buttonMenuBackground)',
|
||||
buttonMenuBackgroundHover: 'var(--color-buttonMenuBackgroundHover)',
|
||||
buttonMenuBorder: 'var(--color-buttonMenuBorder)',
|
||||
buttonMenuSelectedText: 'var(--color-buttonMenuSelectedText)',
|
||||
buttonMenuSelectedTextHover: 'var(--color-buttonMenuSelectedTextHover)',
|
||||
buttonMenuSelectedBackground: 'var(--color-buttonMenuSelectedBackground)',
|
||||
buttonMenuSelectedBackgroundHover:
|
||||
'var(--color-buttonMenuSelectedBackgroundHover)',
|
||||
buttonMenuSelectedBorder: 'var(--color-buttonMenuSelectedBorder)',
|
||||
buttonPrimaryText: 'var(--color-buttonPrimaryText)',
|
||||
buttonPrimaryTextHover: 'var(--color-buttonPrimaryTextHover)',
|
||||
buttonPrimaryBackground: 'var(--color-buttonPrimaryBackground)',
|
||||
buttonPrimaryBackgroundHover: 'var(--color-buttonPrimaryBackgroundHover)',
|
||||
buttonPrimaryBorder: 'var(--color-buttonPrimaryBorder)',
|
||||
buttonPrimaryShadow: 'var(--color-buttonPrimaryShadow)',
|
||||
buttonPrimaryDisabledText: 'var(--color-buttonPrimaryDisabledText)',
|
||||
buttonPrimaryDisabledBackground:
|
||||
'var(--color-buttonPrimaryDisabledBackground)',
|
||||
buttonPrimaryDisabledBorder: 'var(--color-buttonPrimaryDisabledBorder)',
|
||||
buttonNormalText: 'var(--color-buttonNormalText)',
|
||||
buttonNormalTextHover: 'var(--color-buttonNormalTextHover)',
|
||||
buttonNormalBackground: 'var(--color-buttonNormalBackground)',
|
||||
buttonNormalBackgroundHover: 'var(--color-buttonNormalBackgroundHover)',
|
||||
buttonNormalBorder: 'var(--color-buttonNormalBorder)',
|
||||
buttonNormalShadow: 'var(--color-buttonNormalShadow)',
|
||||
buttonNormalSelectedText: 'var(--color-buttonNormalSelectedText)',
|
||||
buttonNormalSelectedBackground: 'var(--color-buttonNormalSelectedBackground)',
|
||||
buttonNormalDisabledText: 'var(--color-buttonNormalDisabledText)',
|
||||
buttonNormalDisabledBackground: 'var(--color-buttonNormalDisabledBackground)',
|
||||
buttonNormalDisabledBorder: 'var(--color-buttonNormalDisabledBorder)',
|
||||
buttonBareText: 'var(--color-buttonBareText)',
|
||||
buttonBareTextHover: 'var(--color-buttonBareTextHover)',
|
||||
buttonBareBackground: 'var(--color-buttonBareBackground)',
|
||||
buttonBareBackgroundHover: 'var(--color-buttonBareBackgroundHover)',
|
||||
buttonBareBackgroundActive: 'var(--color-buttonBareBackgroundActive)',
|
||||
buttonBareDisabledText: 'var(--color-buttonBareDisabledText)',
|
||||
buttonBareDisabledBackground: 'var(--color-buttonBareDisabledBackground)',
|
||||
calendarText: 'var(--color-calendarText)',
|
||||
calendarBackground: 'var(--color-calendarBackground)',
|
||||
calendarItemText: 'var(--color-calendarItemText)',
|
||||
calendarItemBackground: 'var(--color-calendarItemBackground)',
|
||||
calendarSelectedBackground: 'var(--color-calendarSelectedBackground)',
|
||||
noticeBackground: 'var(--color-noticeBackground)',
|
||||
noticeBackgroundLight: 'var(--color-noticeBackgroundLight)',
|
||||
noticeBackgroundDark: 'var(--color-noticeBackgroundDark)',
|
||||
noticeText: 'var(--color-noticeText)',
|
||||
noticeTextLight: 'var(--color-noticeTextLight)',
|
||||
noticeTextDark: 'var(--color-noticeTextDark)',
|
||||
noticeTextMenu: 'var(--color-noticeTextMenu)',
|
||||
noticeTextMenuHover: 'var(--color-noticeTextMenuHover)',
|
||||
noticeBorder: 'var(--color-noticeBorder)',
|
||||
warningBackground: 'var(--color-warningBackground)',
|
||||
warningText: 'var(--color-warningText)',
|
||||
warningTextLight: 'var(--color-warningTextLight)',
|
||||
warningTextDark: 'var(--color-warningTextDark)',
|
||||
warningBorder: 'var(--color-warningBorder)',
|
||||
errorBackground: 'var(--color-errorBackground)',
|
||||
errorText: 'var(--color-errorText)',
|
||||
errorTextDark: 'var(--color-errorTextDark)',
|
||||
errorTextDarker: 'var(--color-errorTextDarker)',
|
||||
errorTextMenu: 'var(--color-errorTextMenu)',
|
||||
errorBorder: 'var(--color-errorBorder)',
|
||||
upcomingBackground: 'var(--color-upcomingBackground)',
|
||||
upcomingText: 'var(--color-upcomingText)',
|
||||
upcomingBorder: 'var(--color-upcomingBorder)',
|
||||
formLabelText: 'var(--color-formLabelText)',
|
||||
formLabelBackground: 'var(--color-formLabelBackground)',
|
||||
formInputBackground: 'var(--color-formInputBackground)',
|
||||
formInputBackgroundSelected: 'var(--color-formInputBackgroundSelected)',
|
||||
formInputBackgroundSelection: 'var(--color-formInputBackgroundSelection)',
|
||||
formInputBorder: 'var(--color-formInputBorder)',
|
||||
formInputTextReadOnlySelection: 'var(--color-formInputTextReadOnlySelection)',
|
||||
formInputBorderSelected: 'var(--color-formInputBorderSelected)',
|
||||
formInputText: 'var(--color-formInputText)',
|
||||
formInputTextSelected: 'var(--color-formInputTextSelected)',
|
||||
formInputTextPlaceholder: 'var(--color-formInputTextPlaceholder)',
|
||||
formInputTextPlaceholderSelected:
|
||||
'var(--color-formInputTextPlaceholderSelected)',
|
||||
formInputTextSelection: 'var(--color-formInputTextSelection)',
|
||||
formInputShadowSelected: 'var(--color-formInputShadowSelected)',
|
||||
formInputTextHighlight: 'var(--color-formInputTextHighlight)',
|
||||
checkboxText: 'var(--color-checkboxText)',
|
||||
checkboxBackgroundSelected: 'var(--color-checkboxBackgroundSelected)',
|
||||
checkboxBorderSelected: 'var(--color-checkboxBorderSelected)',
|
||||
checkboxShadowSelected: 'var(--color-checkboxShadowSelected)',
|
||||
checkboxToggleBackground: 'var(--color-checkboxToggleBackground)',
|
||||
checkboxToggleBackgroundSelected:
|
||||
'var(--color-checkboxToggleBackgroundSelected)',
|
||||
checkboxToggleDisabled: 'var(--color-checkboxToggleDisabled)',
|
||||
pillBackground: 'var(--color-pillBackground)',
|
||||
pillBackgroundLight: 'var(--color-pillBackgroundLight)',
|
||||
pillText: 'var(--color-pillText)',
|
||||
pillTextHighlighted: 'var(--color-pillTextHighlighted)',
|
||||
pillBorder: 'var(--color-pillBorder)',
|
||||
pillBorderDark: 'var(--color-pillBorderDark)',
|
||||
pillBackgroundSelected: 'var(--color-pillBackgroundSelected)',
|
||||
pillTextSelected: 'var(--color-pillTextSelected)',
|
||||
pillBorderSelected: 'var(--color-pillBorderSelected)',
|
||||
pillTextSubdued: 'var(--color-pillTextSubdued)',
|
||||
reportsRed: 'var(--color-reportsRed)',
|
||||
reportsBlue: 'var(--color-reportsBlue)',
|
||||
reportsGreen: 'var(--color-reportsGreen)',
|
||||
reportsGray: 'var(--color-reportsGray)',
|
||||
reportsLabel: 'var(--color-reportsLabel)',
|
||||
reportsInnerLabel: 'var(--color-reportsInnerLabel)',
|
||||
noteTagBackground: 'var(--color-noteTagBackground)',
|
||||
noteTagBackgroundHover: 'var(--color-noteTagBackgroundHover)',
|
||||
noteTagText: 'var(--color-noteTagText)',
|
||||
budgetOtherMonth: 'var(--color-budgetOtherMonth)',
|
||||
budgetCurrentMonth: 'var(--color-budgetCurrentMonth)',
|
||||
budgetHeaderOtherMonth: 'var(--color-budgetHeaderOtherMonth)',
|
||||
budgetHeaderCurrentMonth: 'var(--color-budgetHeaderCurrentMonth)',
|
||||
floatingActionBarBackground: 'var(--color-floatingActionBarBackground)',
|
||||
floatingActionBarBorder: 'var(--color-floatingActionBarBorder)',
|
||||
floatingActionBarText: 'var(--color-floatingActionBarText)',
|
||||
tooltipText: 'var(--color-tooltipText)',
|
||||
tooltipBackground: 'var(--color-tooltipBackground)',
|
||||
tooltipBorder: 'var(--color-tooltipBorder)',
|
||||
calendarCellBackground: 'var(--color-calendarCellBackground)',
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ export class LiveQuery<TResponse = unknown> {
|
||||
private _data: Data<TResponse>;
|
||||
private _dependencies: Set<string>;
|
||||
private _listeners: Array<Listener<TResponse>>;
|
||||
private _supportedSyncTypes: Set<string>;
|
||||
private _supportedSyncTypes: Set<'applied' | 'success'>;
|
||||
private _query: Query;
|
||||
private _onError: (error: Error) => void;
|
||||
|
||||
@@ -107,8 +107,8 @@ export class LiveQuery<TResponse = unknown> {
|
||||
|
||||
// TODO: error types?
|
||||
this._supportedSyncTypes = options.onlySync
|
||||
? new Set<string>(['success'])
|
||||
: new Set<string>(['applied', 'success']);
|
||||
? new Set(['success'])
|
||||
: new Set(['applied', 'success']);
|
||||
|
||||
if (onData) {
|
||||
this.addListener(onData);
|
||||
@@ -162,15 +162,18 @@ export class LiveQuery<TResponse = unknown> {
|
||||
|
||||
protected subscribe = () => {
|
||||
if (this._unsubscribeSyncEvent == null) {
|
||||
this._unsubscribeSyncEvent = listen('sync-event', ({ type, tables }) => {
|
||||
this._unsubscribeSyncEvent = listen('sync-event', event => {
|
||||
// If the user is doing optimistic updates, they don't want to
|
||||
// always refetch whenever something changes because it would
|
||||
// refetch all data after they've already updated the UI. This
|
||||
// voids the perf benefits of optimistic updates. Allow querys
|
||||
// to only react to remote syncs. By default, queries will
|
||||
// always update to all changes.
|
||||
if (this._supportedSyncTypes.has(type)) {
|
||||
this.onUpdate(tables);
|
||||
if (
|
||||
(event.type === 'applied' || event.type === 'success') &&
|
||||
this._supportedSyncTypes.has(event.type)
|
||||
) {
|
||||
this.onUpdate(event.tables);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,16 +8,14 @@ import type { Notification } from './state-types/notifications';
|
||||
export function listenForSyncEvent(actions, store) {
|
||||
let attemptedSyncRepair = false;
|
||||
|
||||
listen('sync-event', info => {
|
||||
const { type, subtype, meta, tables } = info;
|
||||
|
||||
listen('sync-event', event => {
|
||||
const prefs = store.getState().prefs.local;
|
||||
if (!prefs || !prefs.id) {
|
||||
// Do nothing if no budget is loaded
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'success') {
|
||||
if (event.type === 'success') {
|
||||
if (attemptedSyncRepair) {
|
||||
attemptedSyncRepair = false;
|
||||
|
||||
@@ -28,6 +26,8 @@ export function listenForSyncEvent(actions, store) {
|
||||
});
|
||||
}
|
||||
|
||||
const tables = event.tables;
|
||||
|
||||
if (tables.includes('prefs')) {
|
||||
actions.loadPrefs();
|
||||
}
|
||||
@@ -47,13 +47,13 @@ export function listenForSyncEvent(actions, store) {
|
||||
if (tables.includes('accounts')) {
|
||||
actions.getAccounts();
|
||||
}
|
||||
} else if (type === 'error') {
|
||||
} else if (event.type === 'error') {
|
||||
let notif: Notification | null = null;
|
||||
const learnMore = `[${t('Learn more')}](https://actualbudget.org/docs/getting-started/sync/#debugging-sync-issues)`;
|
||||
const githubIssueLink =
|
||||
'https://github.com/actualbudget/actual/issues/new?assignees=&labels=bug&template=bug-report.yml&title=%5BBug%5D%3A+';
|
||||
|
||||
switch (subtype) {
|
||||
switch (event.subtype) {
|
||||
case 'out-of-sync':
|
||||
if (attemptedSyncRepair) {
|
||||
notif = {
|
||||
@@ -215,7 +215,7 @@ export function listenForSyncEvent(actions, store) {
|
||||
break;
|
||||
case 'encrypt-failure':
|
||||
case 'decrypt-failure':
|
||||
if (meta.isMissingKey) {
|
||||
if (event.meta.isMissingKey) {
|
||||
notif = {
|
||||
title: t('Missing encryption key'),
|
||||
message: t(
|
||||
@@ -252,7 +252,7 @@ export function listenForSyncEvent(actions, store) {
|
||||
}
|
||||
break;
|
||||
case 'invalid-schema':
|
||||
console.trace('invalid-schema', meta);
|
||||
console.trace('invalid-schema', event.meta);
|
||||
notif = {
|
||||
title: t('Update required'),
|
||||
message: t(
|
||||
@@ -263,7 +263,7 @@ export function listenForSyncEvent(actions, store) {
|
||||
};
|
||||
break;
|
||||
case 'apply-failure':
|
||||
console.trace('apply-failure', meta);
|
||||
console.trace('apply-failure', event.meta);
|
||||
notif = {
|
||||
message: t(
|
||||
'We couldn’t apply that change to the database. Please report this as a bug by [opening a Github issue]({{githubIssueLink}}).',
|
||||
@@ -287,7 +287,7 @@ export function listenForSyncEvent(actions, store) {
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.trace('unknown error', info);
|
||||
console.trace('unknown error', event);
|
||||
notif = {
|
||||
message: t(
|
||||
'We had problems syncing your changes. Please report this as a bug by [opening a Github issue]({{githubIssueLink}}).',
|
||||
|
||||
@@ -9,7 +9,7 @@ export type Init = typeof init;
|
||||
|
||||
export function send<K extends keyof ServerEvents>(
|
||||
type: K,
|
||||
args?: ServerEvents[k],
|
||||
args?: ServerEvents[K],
|
||||
): void;
|
||||
export type Send = typeof send;
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ describe('Condition', () => {
|
||||
|
||||
cond = new Condition('is', 'payee', null, null);
|
||||
expect(cond.eval({ payee: null })).toBe(true);
|
||||
|
||||
cond = new Condition('is', 'notes', '', null);
|
||||
expect(cond.eval({ notes: null })).toBe(true);
|
||||
});
|
||||
|
||||
test('ops handles undefined fields', () => {
|
||||
|
||||
@@ -360,6 +360,12 @@ export class Condition {
|
||||
|
||||
eval(object) {
|
||||
let fieldValue = object[this.field];
|
||||
const type = this.type;
|
||||
|
||||
if (type === 'string') {
|
||||
fieldValue ??= '';
|
||||
}
|
||||
|
||||
if (fieldValue === undefined) {
|
||||
return false;
|
||||
}
|
||||
@@ -368,8 +374,6 @@ export class Condition {
|
||||
fieldValue = fieldValue.toLowerCase();
|
||||
}
|
||||
|
||||
const type = this.type;
|
||||
|
||||
if (type === 'number' && this.options) {
|
||||
if (this.options.outflow) {
|
||||
if (fieldValue > 0) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-strict-ignore
|
||||
import * as connection from '../../platform/server/connection';
|
||||
import { Diff } from '../../shared/util';
|
||||
import { TransactionEntity } from '../../types/models';
|
||||
import { PayeeEntity, TransactionEntity } from '../../types/models';
|
||||
import * as db from '../db';
|
||||
import { incrFetch, whereIn } from '../db/util';
|
||||
import { batchMessages } from '../sync';
|
||||
@@ -55,7 +55,7 @@ export async function batchUpdateTransactions({
|
||||
? await idsWithChildren(deleted.map(d => d.id))
|
||||
: [];
|
||||
|
||||
const oldPayees = new Set();
|
||||
const oldPayees = new Set<PayeeEntity['id']>();
|
||||
const accounts = await db.all('SELECT * FROM accounts WHERE tombstone = 0');
|
||||
|
||||
// We need to get all the payees of updated transactions _before_
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
// @ts-strict-ignore
|
||||
import mitt from 'mitt';
|
||||
import mitt, { type Emitter } from 'mitt';
|
||||
|
||||
import { captureException } from '../platform/exceptions';
|
||||
import { ServerEvents } from '../types/server-events';
|
||||
|
||||
// This is a simple helper abstraction for defining methods exposed to
|
||||
// the client. It doesn't do much, but checks for naming conflicts and
|
||||
// makes it cleaner to combine methods. We call a group of related
|
||||
// methods an "app".
|
||||
|
||||
type Events = {
|
||||
sync: ServerEvents['sync-event'];
|
||||
'load-budget': { id: string };
|
||||
};
|
||||
|
||||
class App<Handlers> {
|
||||
events;
|
||||
events: Emitter<Events>;
|
||||
handlers: Handlers;
|
||||
services;
|
||||
unlistenServices;
|
||||
|
||||
@@ -35,11 +35,11 @@ repeat 'repeat interval'
|
||||
/ 'year'i { return { annual: true }}
|
||||
/ years: positive _ 'years'i { return { annual: true, repeat: +years }}
|
||||
|
||||
limit = _? upTo _ amount: amount _ 'per week starting'i _ start:date _? hold:hold?
|
||||
limit = _? upTo _ amount: amount _ 'per week starting'i _ start:date _? hold:hold?
|
||||
{ return {amount: amount, hold: hold, period: 'weekly', start: start }}
|
||||
/ _? upTo _ amount: amount _ 'per day'i _? hold: hold?
|
||||
{ return {amount: amount, hold: hold, period: 'daily', start:null }}
|
||||
/ _? upTo _ amount: amount _? hold: hold?
|
||||
/ _? upTo _ amount: amount _? hold: hold?
|
||||
{ return {amount: amount, hold: hold, period: 'monthly', start:null }}
|
||||
|
||||
percentOf = percent:percent _ of _ 'previous'i _ { return { percent: percent, prev: true}}
|
||||
|
||||
@@ -73,6 +73,26 @@ describe('storeTemplates', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Stores template when prefix is used with valid template notes',
|
||||
mockTemplateNotes: [
|
||||
{
|
||||
id: 'cat1',
|
||||
name: 'Category 1',
|
||||
note: 'test: #template 12',
|
||||
},
|
||||
],
|
||||
expectedTemplates: [
|
||||
{
|
||||
type: 'simple',
|
||||
monthly: 12,
|
||||
limit: null,
|
||||
priority: 0,
|
||||
directive: 'template',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
description:
|
||||
'Stores templates for categories with valid goal directive template notes',
|
||||
|
||||
@@ -79,7 +79,7 @@ async function getCategoriesWithTemplates(): Promise<CategoryWithTemplates[]> {
|
||||
const parsedTemplates: Template[] = [];
|
||||
|
||||
note.split('\n').forEach(line => {
|
||||
const trimmedLine = line.trim();
|
||||
const trimmedLine = line.substring(line.indexOf('#')).trim();
|
||||
|
||||
if (
|
||||
!trimmedLine.startsWith(TEMPLATE_PREFIX) &&
|
||||
|
||||
@@ -6,6 +6,6 @@ import { createApp } from './app';
|
||||
// Main app
|
||||
export const app = createApp<Handlers>();
|
||||
|
||||
app.events.on('sync', info => {
|
||||
connection.send('sync-event', info);
|
||||
app.events.on('sync', event => {
|
||||
connection.send('sync-event', event);
|
||||
});
|
||||
|
||||
@@ -533,7 +533,7 @@ async function advanceSchedulesService(syncSuccess) {
|
||||
}
|
||||
|
||||
if (failedToPost.length > 0) {
|
||||
connection.send('schedules-offline', { payees: failedToPost });
|
||||
connection.send('schedules-offline');
|
||||
} else if (didPost) {
|
||||
// This forces a full refresh of transactions because it
|
||||
// simulates them coming in from a full sync. This not a
|
||||
@@ -542,7 +542,7 @@ async function advanceSchedulesService(syncSuccess) {
|
||||
connection.send('sync-event', {
|
||||
type: 'success',
|
||||
tables: ['transactions'],
|
||||
syncDisabled: 'false',
|
||||
syncDisabled: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
97
packages/loot-core/src/types/server-events.d.ts
vendored
@@ -1,23 +1,86 @@
|
||||
import { type Backup } from '../server/backups';
|
||||
import { type UndoState } from '../server/undo';
|
||||
|
||||
type SyncSubtype =
|
||||
| 'out-of-sync'
|
||||
| 'apply-failure'
|
||||
| 'decrypt-failure'
|
||||
| 'encrypt-failure'
|
||||
| 'invalid-schema'
|
||||
| 'network'
|
||||
| 'file-old-version'
|
||||
| 'file-key-mismatch'
|
||||
| 'file-not-found'
|
||||
| 'file-needs-upload'
|
||||
| 'file-has-reset'
|
||||
| 'file-has-new-key'
|
||||
| 'token-expired'
|
||||
| string;
|
||||
|
||||
type SyncEvent = {
|
||||
meta?: Record<string, unknown>;
|
||||
} & (
|
||||
| {
|
||||
type: 'applied';
|
||||
tables: string[];
|
||||
data?: Map<string, unknown>;
|
||||
prevData?: Map<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: 'success';
|
||||
tables: string[];
|
||||
syncDisabled?: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'error';
|
||||
subtype?: SyncSubtype;
|
||||
}
|
||||
| {
|
||||
type: 'start';
|
||||
}
|
||||
| {
|
||||
type: 'unauthorized';
|
||||
}
|
||||
);
|
||||
|
||||
type BackupUpdatedEvent = Backup[];
|
||||
|
||||
type CellsChangedEvent = Array<{
|
||||
name: string;
|
||||
value: string | number | boolean;
|
||||
}>;
|
||||
|
||||
type FallbackWriteErrorEvent = undefined;
|
||||
type FinishImportEvent = undefined;
|
||||
type FinishLoadEvent = undefined;
|
||||
|
||||
type OrphanedPayeesEvent = {
|
||||
orphanedIds: string[];
|
||||
updatedPayeeIds: string[];
|
||||
};
|
||||
|
||||
type PrefsUpdatedEvent = undefined;
|
||||
type SchedulesOfflineEvent = undefined;
|
||||
type ServerErrorEvent = undefined;
|
||||
type ShowBudgetsEvent = undefined;
|
||||
type StartImportEvent = { budgetName: string };
|
||||
type StartLoadEvent = undefined;
|
||||
type ApiFetchRedirectedEvent = undefined;
|
||||
|
||||
export interface ServerEvents {
|
||||
'backups-updated': Backup[];
|
||||
'cells-changed': Array<{ name }>;
|
||||
'fallback-write-error': unknown;
|
||||
'finish-import': unknown;
|
||||
'finish-load': unknown;
|
||||
'orphaned-payees': {
|
||||
orphanedIds: string[];
|
||||
updatedPayeeIds: string[];
|
||||
};
|
||||
'prefs-updated': unknown;
|
||||
'schedules-offline': { payees: unknown[] };
|
||||
'server-error': unknown;
|
||||
'show-budgets': unknown;
|
||||
'start-import': unknown;
|
||||
'start-load': unknown;
|
||||
'sync-event': { type; subtype; meta; tables; syncDisabled };
|
||||
'backups-updated': BackupUpdatedEvent;
|
||||
'cells-changed': CellsChangedEvent;
|
||||
'fallback-write-error': FallbackWriteErrorEvent;
|
||||
'finish-import': FinishImportEvent;
|
||||
'finish-load': FinishLoadEvent;
|
||||
'orphaned-payees': OrphanedPayeesEvent;
|
||||
'prefs-updated': PrefsUpdatedEvent;
|
||||
'schedules-offline': SchedulesOfflineEvent;
|
||||
'server-error': ServerErrorEvent;
|
||||
'show-budgets': ShowBudgetsEvent;
|
||||
'start-import': StartImportEvent;
|
||||
'start-load': StartLoadEvent;
|
||||
'sync-event': SyncEvent;
|
||||
'undo-event': UndoState;
|
||||
'api-fetch-redirected': unknown;
|
||||
'api-fetch-redirected': ApiFetchRedirectedEvent;
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/3998.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [UnderKoen]
|
||||
---
|
||||
|
||||
Usage of notes is (nothing) on new transactions
|
||||
6
upcoming-release-notes/4032.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [UnderKoen]
|
||||
---
|
||||
|
||||
Allow note prefixes in budget templates.
|
||||
6
upcoming-release-notes/4072.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [matt-fidd]
|
||||
---
|
||||
|
||||
Sort barchart data
|
||||
6
upcoming-release-notes/4086.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatisJanis]
|
||||
---
|
||||
|
||||
Refactor `theme` variable to be statically defined.
|
||||
6
upcoming-release-notes/4089.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [jfdoming]
|
||||
---
|
||||
|
||||
Include translations in builds
|
||||
6
upcoming-release-notes/4110.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Add types to loot-core server events.
|
||||
6
upcoming-release-notes/4124.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
[Address suppressed ESLint errors] Fix exhaustive deps errors in App.tsx
|
||||