mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 03:32:54 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0fcfd2d5a | ||
|
|
888d2e9cce | ||
|
|
2e569355bc | ||
|
|
4a1e1511bc | ||
|
|
c456596417 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/api",
|
||||
"version": "24.10.0",
|
||||
"version": "24.10.1",
|
||||
"license": "MIT",
|
||||
"description": "An API for Actual",
|
||||
"engines": {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actual-app/web",
|
||||
"version": "24.10.0",
|
||||
"version": "24.10.1",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"build"
|
||||
|
||||
@@ -78,24 +78,8 @@ function FileMenu({
|
||||
const { t } = useTranslation();
|
||||
|
||||
const items = [{ name: 'delete', text: t('Delete') }];
|
||||
const { isNarrowWidth } = useResponsive();
|
||||
|
||||
const defaultMenuItemStyle = isNarrowWidth
|
||||
? {
|
||||
...styles.mobileMenuItem,
|
||||
color: theme.menuItemText,
|
||||
borderRadius: 0,
|
||||
borderTop: `1px solid ${theme.pillBorder}`,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<Menu
|
||||
getItemStyle={() => defaultMenuItemStyle}
|
||||
onMenuSelect={onMenuSelect}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
return <Menu onMenuSelect={onMenuSelect} items={items} />;
|
||||
}
|
||||
|
||||
function FileMenuButton({ onDelete }: { onDelete: () => void }) {
|
||||
@@ -202,50 +186,60 @@ function FileItem({
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
onClick={() => _onSelect(file)}
|
||||
title={getFileDescription(file, t) || ''}
|
||||
<Button
|
||||
onPress={() => _onSelect(file)}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
...styles.shadow,
|
||||
margin: 10,
|
||||
padding: '12px 15px',
|
||||
backgroundColor: theme.buttonNormalBackground,
|
||||
borderRadius: 6,
|
||||
flexShrink: 0,
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
backgroundColor: theme.menuItemBackgroundHover,
|
||||
},
|
||||
borderRadius: 6,
|
||||
borderColor: 'transparent',
|
||||
}}
|
||||
>
|
||||
<View style={{ alignItems: 'flex-start' }}>
|
||||
<Text style={{ fontSize: 16, fontWeight: 700 }}>{file.name}</Text>
|
||||
|
||||
<FileState file={file} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{ flex: '0 0 auto', flexDirection: 'row', alignItems: 'center' }}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{file.encryptKeyId && (
|
||||
<SvgKey
|
||||
style={{
|
||||
width: 13,
|
||||
height: 13,
|
||||
marginRight: 8,
|
||||
color: file.hasKey
|
||||
? theme.formLabelText
|
||||
: theme.buttonNormalDisabledText,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
title={getFileDescription(file, t) || ''}
|
||||
style={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<Text style={{ fontSize: 16, fontWeight: 700 }}>{file.name}</Text>
|
||||
|
||||
{!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>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ export function EnvelopeBudgetMenuModal({
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
Budget
|
||||
Budgeted
|
||||
</Text>
|
||||
<FocusableAmountInput
|
||||
value={integerToAmount(budgeted || 0)}
|
||||
|
||||
@@ -81,7 +81,7 @@ export function TrackingBudgetMenuModal({
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
Budget
|
||||
Budgeted
|
||||
</Text>
|
||||
<FocusableAmountInput
|
||||
value={integerToAmount(budgeted || 0)}
|
||||
|
||||
@@ -82,16 +82,26 @@ export function Overview() {
|
||||
const isDashboardsFeatureEnabled = useFeatureFlag('dashboards');
|
||||
const spendingReportFeatureFlag = useFeatureFlag('spendingReport');
|
||||
|
||||
const baseLayout = widgets.map(widget => ({
|
||||
i: widget.id,
|
||||
w: widget.width,
|
||||
h: widget.height,
|
||||
minW:
|
||||
isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 2 : 3,
|
||||
minH:
|
||||
isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 1 : 2,
|
||||
...widget,
|
||||
}));
|
||||
const baseLayout = widgets
|
||||
.map(widget => ({
|
||||
i: widget.id,
|
||||
w: widget.width,
|
||||
h: widget.height,
|
||||
minW:
|
||||
isCustomReportWidget(widget) || widget.type === 'markdown-card' ? 2 : 3,
|
||||
minH:
|
||||
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 =
|
||||
spendingReportFeatureFlag &&
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useQuery } from 'loot-core/client/query-hooks';
|
||||
import { send } from 'loot-core/platform/client/fetch';
|
||||
import { q } from 'loot-core/shared/query';
|
||||
import { saveSyncedPrefs } from 'loot-core/client/actions';
|
||||
import { type State } from 'loot-core/client/state-types';
|
||||
import { type SyncedPrefs } from 'loot-core/src/types/prefs';
|
||||
|
||||
type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
|
||||
@@ -12,21 +12,14 @@ type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
|
||||
export function useSyncedPref<K extends keyof SyncedPrefs>(
|
||||
prefName: K,
|
||||
): [SyncedPrefs[K], SetSyncedPrefAction<K>] {
|
||||
const { data: queryData, overrideData: setQueryData } = useQuery<
|
||||
[{ value: string | undefined }]
|
||||
>(
|
||||
() => q('preferences').filter({ id: prefName }).select('value'),
|
||||
[prefName],
|
||||
);
|
||||
|
||||
const setLocalPref = useCallback<SetSyncedPrefAction<K>>(
|
||||
newValue => {
|
||||
const value = String(newValue);
|
||||
setQueryData([{ value }]);
|
||||
send('preferences/save', { id: prefName, value });
|
||||
const dispatch = useDispatch();
|
||||
const setPref = useCallback<SetSyncedPrefAction<K>>(
|
||||
value => {
|
||||
dispatch(saveSyncedPrefs({ [prefName]: value }));
|
||||
},
|
||||
[prefName, setQueryData],
|
||||
[prefName, dispatch],
|
||||
);
|
||||
const pref = useSelector((state: State) => state.prefs.synced[prefName]);
|
||||
|
||||
return [queryData?.[0]?.value, setLocalPref];
|
||||
return [pref, setPref];
|
||||
}
|
||||
|
||||
@@ -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 { send } from 'loot-core/platform/client/fetch';
|
||||
import { q } from 'loot-core/shared/query';
|
||||
import { saveSyncedPrefs } from 'loot-core/client/actions';
|
||||
import { type State } from 'loot-core/client/state-types';
|
||||
import { type SyncedPrefs } from 'loot-core/src/types/prefs';
|
||||
|
||||
type SetSyncedPrefsAction = (value: Partial<SyncedPrefs>) => void;
|
||||
|
||||
/** @deprecated: please use `useSyncedPref` (singular) */
|
||||
export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] {
|
||||
const { data: queryData } = useQuery<{ id: string; value: string }[]>(
|
||||
() => q('preferences').select(['id', 'value']),
|
||||
[],
|
||||
const dispatch = useDispatch();
|
||||
const setPrefs = useCallback<SetSyncedPrefsAction>(
|
||||
newValue => {
|
||||
dispatch(saveSyncedPrefs(newValue));
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
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),
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
const prefs = useSelector((state: State) => state.prefs.synced);
|
||||
|
||||
return [prefs, setPrefs];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"author": "Actual",
|
||||
"productName": "Actual",
|
||||
"description": "A simple and powerful personal finance system",
|
||||
"version": "24.10.0",
|
||||
"version": "24.10.1",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist",
|
||||
"update-client": "bin/update-client",
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
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 { closeModal } from './modals';
|
||||
@@ -19,6 +23,7 @@ export function loadPrefs() {
|
||||
type: constants.SET_PREFS,
|
||||
prefs,
|
||||
globalPrefs: await send('load-global-prefs'),
|
||||
syncedPrefs: await send('preferences/get'),
|
||||
});
|
||||
|
||||
return prefs;
|
||||
@@ -42,6 +47,7 @@ export function loadGlobalPrefs() {
|
||||
type: constants.SET_PREFS,
|
||||
prefs: getState().prefs.local,
|
||||
globalPrefs,
|
||||
syncedPrefs: getState().prefs.synced,
|
||||
});
|
||||
return globalPrefs;
|
||||
};
|
||||
@@ -60,3 +66,20 @@ export function saveGlobalPrefs(
|
||||
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,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// @ts-strict-ignore
|
||||
import { send } from '../../platform/client/fetch';
|
||||
import { getUploadError } from '../../shared/errors';
|
||||
|
||||
@@ -32,7 +31,6 @@ export function resetSync() {
|
||||
}
|
||||
} else {
|
||||
await dispatch(sync());
|
||||
await dispatch(loadPrefs());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -45,8 +43,12 @@ export function sync() {
|
||||
if ('error' in result) {
|
||||
return { error: result.error };
|
||||
}
|
||||
return {};
|
||||
|
||||
// Update the prefs
|
||||
await dispatch(loadPrefs());
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export const LOAD_PAYEES = 'LOAD_PAYEES';
|
||||
export const SET_PREFS = 'SET_PREFS';
|
||||
export const MERGE_LOCAL_PREFS = 'MERGE_LOCAL_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_REMOTE_FILES = 'SET_REMOTE_FILES';
|
||||
export const SET_ALL_FILES = 'SET_ALL_FILES';
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
// @ts-strict-ignore
|
||||
import * as constants from '../constants';
|
||||
import type { Action } from '../state-types';
|
||||
import type { PrefsState } from '../state-types/prefs';
|
||||
|
||||
const initialState: PrefsState = {
|
||||
local: null,
|
||||
global: null,
|
||||
local: {},
|
||||
global: {},
|
||||
synced: {},
|
||||
};
|
||||
|
||||
export function update(state = initialState, action: Action): PrefsState {
|
||||
switch (action.type) {
|
||||
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:
|
||||
return {
|
||||
...state,
|
||||
@@ -22,6 +27,11 @@ export function update(state = initialState, action: Action): PrefsState {
|
||||
...state,
|
||||
global: { ...state.global, ...action.globalPrefs },
|
||||
};
|
||||
case constants.MERGE_SYNCED_PREFS:
|
||||
return {
|
||||
...state,
|
||||
synced: { ...state.synced, ...action.syncedPrefs },
|
||||
};
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
export type PrefsState = {
|
||||
local: MetadataPrefs;
|
||||
global: GlobalPrefs;
|
||||
synced: SyncedPrefs;
|
||||
};
|
||||
|
||||
export type SetPrefsAction = {
|
||||
type: typeof constants.SET_PREFS;
|
||||
prefs: MetadataPrefs;
|
||||
globalPrefs: GlobalPrefs;
|
||||
syncedPrefs: SyncedPrefs;
|
||||
};
|
||||
|
||||
export type MergeLocalPrefsAction = {
|
||||
@@ -22,7 +28,13 @@ export type MergeGlobalPrefsAction = {
|
||||
globalPrefs: GlobalPrefs;
|
||||
};
|
||||
|
||||
export type MergeSyncedPrefsAction = {
|
||||
type: typeof constants.MERGE_SYNCED_PREFS;
|
||||
syncedPrefs: SyncedPrefs;
|
||||
};
|
||||
|
||||
export type PrefsActions =
|
||||
| SetPrefsAction
|
||||
| MergeLocalPrefsAction
|
||||
| MergeGlobalPrefsAction;
|
||||
| MergeGlobalPrefsAction
|
||||
| MergeSyncedPrefsAction;
|
||||
|
||||
@@ -18,4 +18,17 @@ const savePreferences = async ({
|
||||
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/get', getPreferences);
|
||||
|
||||
@@ -5,4 +5,6 @@ export interface PreferencesHandlers {
|
||||
id: keyof SyncedPrefs;
|
||||
value: string | undefined;
|
||||
}) => Promise<void>;
|
||||
|
||||
'preferences/get': () => Promise<SyncedPrefs>;
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/3544.md
Normal file
6
upcoming-release-notes/3544.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
SyncedPrefs: preload in redux state and fetch from there; improved performance.
|
||||
6
upcoming-release-notes/3566.md
Normal file
6
upcoming-release-notes/3566.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfix
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
Reports: fix old reports page having empty blocks.
|
||||
6
upcoming-release-notes/3573.md
Normal file
6
upcoming-release-notes/3573.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
[Mobile] Update Budget to Budgeted to match the column name in mobile budget table
|
||||
6
upcoming-release-notes/3574.md
Normal file
6
upcoming-release-notes/3574.md
Normal 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
|
||||
Reference in New Issue
Block a user