Compare commits

...

5 Commits

Author SHA1 Message Date
Matiss Janis Aboltins
d0fcfd2d5a Bump version to v24.10.1 2024-10-08 18:05:06 +01:00
Matiss Janis Aboltins
888d2e9cce (synced-prefs) preloading in redux state to improve perf (#3544) 2024-10-07 18:11:02 +01:00
Matiss Janis Aboltins
2e569355bc 🐛 (reports) fix reports page having empty blocks (#3566) 2024-10-07 18:10:51 +01:00
Joel Jeremy Marquez
4a1e1511bc [Mobile] Fix budget list on mobile auto selecting a budget file under the Switch budget file menu (#3574)
* Fix budget list on mobile

* Releast notes
2024-10-07 18:10:36 +01:00
Joel Jeremy Marquez
c456596417 [Mobile] Update to Budgeted to match the column name in mobile budget table (#3573)
* Update to Budgeted to match the column name in mobile budget table

* Release notes

* VRT

* VRT
2024-10-07 18:10:00 +01:00
25 changed files with 187 additions and 120 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@actual-app/api", "name": "@actual-app/api",
"version": "24.10.0", "version": "24.10.1",
"license": "MIT", "license": "MIT",
"description": "An API for Actual", "description": "An API for Actual",
"engines": { "engines": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@actual-app/web", "name": "@actual-app/web",
"version": "24.10.0", "version": "24.10.1",
"license": "MIT", "license": "MIT",
"files": [ "files": [
"build" "build"

View File

@@ -78,24 +78,8 @@ function FileMenu({
const { t } = useTranslation(); const { t } = useTranslation();
const items = [{ name: 'delete', text: t('Delete') }]; const items = [{ name: 'delete', text: t('Delete') }];
const { isNarrowWidth } = useResponsive();
const defaultMenuItemStyle = isNarrowWidth return <Menu onMenuSelect={onMenuSelect} items={items} />;
? {
...styles.mobileMenuItem,
color: theme.menuItemText,
borderRadius: 0,
borderTop: `1px solid ${theme.pillBorder}`,
}
: {};
return (
<Menu
getItemStyle={() => defaultMenuItemStyle}
onMenuSelect={onMenuSelect}
items={items}
/>
);
} }
function FileMenuButton({ onDelete }: { onDelete: () => void }) { function FileMenuButton({ onDelete }: { onDelete: () => void }) {
@@ -202,50 +186,60 @@ function FileItem({
} }
return ( return (
<View <Button
onClick={() => _onSelect(file)} onPress={() => _onSelect(file)}
title={getFileDescription(file, t) || ''}
style={{ style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
...styles.shadow, ...styles.shadow,
margin: 10, margin: 10,
padding: '12px 15px', padding: '12px 15px',
backgroundColor: theme.buttonNormalBackground,
borderRadius: 6,
flexShrink: 0,
cursor: 'pointer', cursor: 'pointer',
':hover': { borderRadius: 6,
backgroundColor: theme.menuItemBackgroundHover, borderColor: 'transparent',
},
}} }}
> >
<View style={{ alignItems: 'flex-start' }}>
<Text style={{ fontSize: 16, fontWeight: 700 }}>{file.name}</Text>
<FileState file={file} />
</View>
<View <View
style={{ flex: '0 0 auto', flexDirection: 'row', alignItems: 'center' }} style={{
flexDirection: 'row',
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
}}
> >
{file.encryptKeyId && ( <View
<SvgKey title={getFileDescription(file, t) || ''}
style={{ style={{ alignItems: 'flex-start' }}
width: 13, >
height: 13, <Text style={{ fontSize: 16, fontWeight: 700 }}>{file.name}</Text>
marginRight: 8,
color: file.hasKey
? theme.formLabelText
: theme.buttonNormalDisabledText,
}}
/>
)}
{!quickSwitchMode && <FileMenuButton onDelete={() => onDelete(file)} />} <FileState file={file} />
</View>
<View
style={{
flex: '0 0 auto',
flexDirection: 'row',
alignItems: 'center',
}}
>
{file.encryptKeyId && (
<SvgKey
style={{
width: 13,
height: 13,
marginRight: 8,
color: file.hasKey
? theme.formLabelText
: theme.buttonNormalDisabledText,
}}
/>
)}
{!quickSwitchMode && (
<FileMenuButton onDelete={() => onDelete(file)} />
)}
</View>
</View> </View>
</View> </Button>
); );
} }

View File

@@ -81,7 +81,7 @@ export function EnvelopeBudgetMenuModal({
fontWeight: 400, fontWeight: 400,
}} }}
> >
Budget Budgeted
</Text> </Text>
<FocusableAmountInput <FocusableAmountInput
value={integerToAmount(budgeted || 0)} value={integerToAmount(budgeted || 0)}

View File

@@ -81,7 +81,7 @@ export function TrackingBudgetMenuModal({
fontWeight: 400, fontWeight: 400,
}} }}
> >
Budget Budgeted
</Text> </Text>
<FocusableAmountInput <FocusableAmountInput
value={integerToAmount(budgeted || 0)} value={integerToAmount(budgeted || 0)}

View File

@@ -82,16 +82,26 @@ export function Overview() {
const isDashboardsFeatureEnabled = useFeatureFlag('dashboards'); const isDashboardsFeatureEnabled = useFeatureFlag('dashboards');
const spendingReportFeatureFlag = useFeatureFlag('spendingReport'); const spendingReportFeatureFlag = useFeatureFlag('spendingReport');
const baseLayout = widgets.map(widget => ({ const baseLayout = widgets
i: widget.id, .map(widget => ({
w: widget.width, i: widget.id,
h: widget.height, w: widget.width,
minW: h: widget.height,
isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 2 : 3, minW:
minH: isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 2 : 3,
isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 1 : 2, minH:
...widget, isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 1 : 2,
})); ...widget,
}))
.filter(item => {
if (isDashboardsFeatureEnabled) {
return true;
}
if (item.type === 'custom-report' && !customReportMap.has(item.meta.id)) {
return false;
}
return true;
});
const layout = const layout =
spendingReportFeatureFlag && spendingReportFeatureFlag &&

View File

@@ -1,8 +1,8 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useQuery } from 'loot-core/client/query-hooks'; import { saveSyncedPrefs } from 'loot-core/client/actions';
import { send } from 'loot-core/platform/client/fetch'; import { type State } from 'loot-core/client/state-types';
import { q } from 'loot-core/shared/query';
import { type SyncedPrefs } from 'loot-core/src/types/prefs'; import { type SyncedPrefs } from 'loot-core/src/types/prefs';
type SetSyncedPrefAction<K extends keyof SyncedPrefs> = ( type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
@@ -12,21 +12,14 @@ type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
export function useSyncedPref<K extends keyof SyncedPrefs>( export function useSyncedPref<K extends keyof SyncedPrefs>(
prefName: K, prefName: K,
): [SyncedPrefs[K], SetSyncedPrefAction<K>] { ): [SyncedPrefs[K], SetSyncedPrefAction<K>] {
const { data: queryData, overrideData: setQueryData } = useQuery< const dispatch = useDispatch();
[{ value: string | undefined }] const setPref = useCallback<SetSyncedPrefAction<K>>(
>( value => {
() => q('preferences').filter({ id: prefName }).select('value'), dispatch(saveSyncedPrefs({ [prefName]: value }));
[prefName],
);
const setLocalPref = useCallback<SetSyncedPrefAction<K>>(
newValue => {
const value = String(newValue);
setQueryData([{ value }]);
send('preferences/save', { id: prefName, value });
}, },
[prefName, setQueryData], [prefName, dispatch],
); );
const pref = useSelector((state: State) => state.prefs.synced[prefName]);
return [queryData?.[0]?.value, setLocalPref]; return [pref, setPref];
} }

View File

@@ -1,39 +1,22 @@
import { useCallback, useMemo } from 'react'; import { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useQuery } from 'loot-core/client/query-hooks'; import { saveSyncedPrefs } from 'loot-core/client/actions';
import { send } from 'loot-core/platform/client/fetch'; import { type State } from 'loot-core/client/state-types';
import { q } from 'loot-core/shared/query';
import { type SyncedPrefs } from 'loot-core/src/types/prefs'; import { type SyncedPrefs } from 'loot-core/src/types/prefs';
type SetSyncedPrefsAction = (value: Partial<SyncedPrefs>) => void; type SetSyncedPrefsAction = (value: Partial<SyncedPrefs>) => void;
/** @deprecated: please use `useSyncedPref` (singular) */ /** @deprecated: please use `useSyncedPref` (singular) */
export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] { export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] {
const { data: queryData } = useQuery<{ id: string; value: string }[]>( const dispatch = useDispatch();
() => q('preferences').select(['id', 'value']), const setPrefs = useCallback<SetSyncedPrefsAction>(
[], newValue => {
dispatch(saveSyncedPrefs(newValue));
},
[dispatch],
); );
const prefs = useSelector((state: State) => state.prefs.synced);
const prefs = useMemo<SyncedPrefs>(
() =>
(queryData ?? []).reduce(
(carry, { id, value }) => ({
...carry,
[id]: value,
}),
{},
),
[queryData],
);
const setPrefs = useCallback<SetSyncedPrefsAction>(newValue => {
Object.entries(newValue).forEach(([id, value]) => {
send('preferences/save', {
id: id as keyof SyncedPrefs,
value: String(value),
});
});
}, []);
return [prefs, setPrefs]; return [prefs, setPrefs];
} }

View File

@@ -3,7 +3,7 @@
"author": "Actual", "author": "Actual",
"productName": "Actual", "productName": "Actual",
"description": "A simple and powerful personal finance system", "description": "A simple and powerful personal finance system",
"version": "24.10.0", "version": "24.10.1",
"scripts": { "scripts": {
"clean": "rm -rf dist", "clean": "rm -rf dist",
"update-client": "bin/update-client", "update-client": "bin/update-client",

View File

@@ -1,5 +1,9 @@
import { send } from '../../platform/client/fetch'; import { send } from '../../platform/client/fetch';
import { type GlobalPrefs, type MetadataPrefs } from '../../types/prefs'; import {
type GlobalPrefs,
type MetadataPrefs,
type SyncedPrefs,
} from '../../types/prefs';
import * as constants from '../constants'; import * as constants from '../constants';
import { closeModal } from './modals'; import { closeModal } from './modals';
@@ -19,6 +23,7 @@ export function loadPrefs() {
type: constants.SET_PREFS, type: constants.SET_PREFS,
prefs, prefs,
globalPrefs: await send('load-global-prefs'), globalPrefs: await send('load-global-prefs'),
syncedPrefs: await send('preferences/get'),
}); });
return prefs; return prefs;
@@ -42,6 +47,7 @@ export function loadGlobalPrefs() {
type: constants.SET_PREFS, type: constants.SET_PREFS,
prefs: getState().prefs.local, prefs: getState().prefs.local,
globalPrefs, globalPrefs,
syncedPrefs: getState().prefs.synced,
}); });
return globalPrefs; return globalPrefs;
}; };
@@ -60,3 +66,20 @@ export function saveGlobalPrefs(
onSaveGlobalPrefs?.(); onSaveGlobalPrefs?.();
}; };
} }
export function saveSyncedPrefs(prefs: SyncedPrefs) {
return async (dispatch: Dispatch) => {
await Promise.all(
Object.entries(prefs).map(([prefName, value]) =>
send('preferences/save', {
id: prefName as keyof SyncedPrefs,
value,
}),
),
);
dispatch({
type: constants.MERGE_SYNCED_PREFS,
syncedPrefs: prefs,
});
};
}

View File

@@ -1,4 +1,3 @@
// @ts-strict-ignore
import { send } from '../../platform/client/fetch'; import { send } from '../../platform/client/fetch';
import { getUploadError } from '../../shared/errors'; import { getUploadError } from '../../shared/errors';
@@ -32,7 +31,6 @@ export function resetSync() {
} }
} else { } else {
await dispatch(sync()); await dispatch(sync());
await dispatch(loadPrefs());
} }
}; };
} }
@@ -45,8 +43,12 @@ export function sync() {
if ('error' in result) { if ('error' in result) {
return { error: result.error }; return { error: result.error };
} }
return {};
// Update the prefs
await dispatch(loadPrefs());
} }
return {};
}; };
} }

View File

@@ -10,6 +10,7 @@ export const LOAD_PAYEES = 'LOAD_PAYEES';
export const SET_PREFS = 'SET_PREFS'; export const SET_PREFS = 'SET_PREFS';
export const MERGE_LOCAL_PREFS = 'MERGE_LOCAL_PREFS'; export const MERGE_LOCAL_PREFS = 'MERGE_LOCAL_PREFS';
export const MERGE_GLOBAL_PREFS = 'MERGE_GLOBAL_PREFS'; export const MERGE_GLOBAL_PREFS = 'MERGE_GLOBAL_PREFS';
export const MERGE_SYNCED_PREFS = 'MERGE_SYNCED_PREFS';
export const SET_BUDGETS = 'SET_BUDGETS'; export const SET_BUDGETS = 'SET_BUDGETS';
export const SET_REMOTE_FILES = 'SET_REMOTE_FILES'; export const SET_REMOTE_FILES = 'SET_REMOTE_FILES';
export const SET_ALL_FILES = 'SET_ALL_FILES'; export const SET_ALL_FILES = 'SET_ALL_FILES';

View File

@@ -1,17 +1,22 @@
// @ts-strict-ignore
import * as constants from '../constants'; import * as constants from '../constants';
import type { Action } from '../state-types'; import type { Action } from '../state-types';
import type { PrefsState } from '../state-types/prefs'; import type { PrefsState } from '../state-types/prefs';
const initialState: PrefsState = { const initialState: PrefsState = {
local: null, local: {},
global: null, global: {},
synced: {},
}; };
export function update(state = initialState, action: Action): PrefsState { export function update(state = initialState, action: Action): PrefsState {
switch (action.type) { switch (action.type) {
case constants.SET_PREFS: case constants.SET_PREFS:
return { local: action.prefs, global: action.globalPrefs }; return {
...state,
local: action.prefs,
global: action.globalPrefs,
synced: action.syncedPrefs,
};
case constants.MERGE_LOCAL_PREFS: case constants.MERGE_LOCAL_PREFS:
return { return {
...state, ...state,
@@ -22,6 +27,11 @@ export function update(state = initialState, action: Action): PrefsState {
...state, ...state,
global: { ...state.global, ...action.globalPrefs }, global: { ...state.global, ...action.globalPrefs },
}; };
case constants.MERGE_SYNCED_PREFS:
return {
...state,
synced: { ...state.synced, ...action.syncedPrefs },
};
default: default:
} }

View File

@@ -1,15 +1,21 @@
import type { GlobalPrefs, MetadataPrefs } from '../../types/prefs'; import type {
GlobalPrefs,
MetadataPrefs,
SyncedPrefs,
} from '../../types/prefs';
import type * as constants from '../constants'; import type * as constants from '../constants';
export type PrefsState = { export type PrefsState = {
local: MetadataPrefs; local: MetadataPrefs;
global: GlobalPrefs; global: GlobalPrefs;
synced: SyncedPrefs;
}; };
export type SetPrefsAction = { export type SetPrefsAction = {
type: typeof constants.SET_PREFS; type: typeof constants.SET_PREFS;
prefs: MetadataPrefs; prefs: MetadataPrefs;
globalPrefs: GlobalPrefs; globalPrefs: GlobalPrefs;
syncedPrefs: SyncedPrefs;
}; };
export type MergeLocalPrefsAction = { export type MergeLocalPrefsAction = {
@@ -22,7 +28,13 @@ export type MergeGlobalPrefsAction = {
globalPrefs: GlobalPrefs; globalPrefs: GlobalPrefs;
}; };
export type MergeSyncedPrefsAction = {
type: typeof constants.MERGE_SYNCED_PREFS;
syncedPrefs: SyncedPrefs;
};
export type PrefsActions = export type PrefsActions =
| SetPrefsAction | SetPrefsAction
| MergeLocalPrefsAction | MergeLocalPrefsAction
| MergeGlobalPrefsAction; | MergeGlobalPrefsAction
| MergeSyncedPrefsAction;

View File

@@ -18,4 +18,17 @@ const savePreferences = async ({
await db.update('preferences', { id, value }); await db.update('preferences', { id, value });
}; };
const getPreferences = async (): Promise<SyncedPrefs> => {
const prefs = (await db.all('SELECT id, value FROM preferences')) as Array<{
id: string;
value: string;
}>;
return prefs.reduce<SyncedPrefs>((carry, { value, id }) => {
carry[id as keyof SyncedPrefs] = value;
return carry;
}, {});
};
app.method('preferences/save', mutator(undoable(savePreferences))); app.method('preferences/save', mutator(undoable(savePreferences)));
app.method('preferences/get', getPreferences);

View File

@@ -5,4 +5,6 @@ export interface PreferencesHandlers {
id: keyof SyncedPrefs; id: keyof SyncedPrefs;
value: string | undefined; value: string | undefined;
}) => Promise<void>; }) => Promise<void>;
'preferences/get': () => Promise<SyncedPrefs>;
} }

View File

@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [MatissJanis]
---
SyncedPrefs: preload in redux state and fetch from there; improved performance.

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [MatissJanis]
---
Reports: fix old reports page having empty blocks.

View File

@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [joel-jeremy]
---
[Mobile] Update Budget to Budgeted to match the column name in mobile budget table

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [joel-jeremy]
---
[Mobile] Fix budget list on mobile auto selecting a budget file under the Switch budget file menu