mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 12:43:09 -05:00
♻️ (synced-prefs) moving the prefs from metadata.json to the db (#3423)
This commit is contained in:
committed by
GitHub
parent
5b685ecc64
commit
6c87d85920
@@ -31,6 +31,9 @@ vi.mock('loot-core/src/platform/client/fetch');
|
||||
vi.mock('../../hooks/useFeatureFlag', () => ({
|
||||
default: vi.fn().mockReturnValue(false),
|
||||
}));
|
||||
vi.mock('../../hooks/useSyncedPref', () => ({
|
||||
useSyncedPref: vi.fn().mockReturnValue([undefined, vi.fn()]),
|
||||
}));
|
||||
|
||||
const accounts = [generateAccount('Bank of America')];
|
||||
const payees = [
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { type State } from 'loot-core/src/client/state-types';
|
||||
|
||||
export function useLocalPrefs() {
|
||||
return useSelector((state: State) => state.prefs.local);
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { type SyncedPrefs } from 'loot-core/src/types/prefs';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useLocalPref } from './useLocalPref';
|
||||
import { useQuery } from 'loot-core/client/query-hooks';
|
||||
import { send } from 'loot-core/platform/client/fetch';
|
||||
import { q } from 'loot-core/shared/query';
|
||||
import { type SyncedPrefs } from 'loot-core/src/types/prefs';
|
||||
|
||||
type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
|
||||
value: SyncedPrefs[K],
|
||||
@@ -9,7 +12,21 @@ type SetSyncedPrefAction<K extends keyof SyncedPrefs> = (
|
||||
export function useSyncedPref<K extends keyof SyncedPrefs>(
|
||||
prefName: K,
|
||||
): [SyncedPrefs[K], SetSyncedPrefAction<K>] {
|
||||
// TODO: implement logic for fetching the pref exclusively from the
|
||||
// database (in follow-up PR)
|
||||
return useLocalPref(prefName);
|
||||
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 });
|
||||
},
|
||||
[prefName, setQueryData],
|
||||
);
|
||||
|
||||
return [queryData?.[0]?.value, setLocalPref];
|
||||
}
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { savePrefs } from 'loot-core/client/actions';
|
||||
import { useQuery } from 'loot-core/client/query-hooks';
|
||||
import { send } from 'loot-core/platform/client/fetch';
|
||||
import { q } from 'loot-core/shared/query';
|
||||
import { type SyncedPrefs } from 'loot-core/src/types/prefs';
|
||||
|
||||
import { useLocalPrefs } from './useLocalPrefs';
|
||||
|
||||
type SetSyncedPrefsAction = (value: Partial<SyncedPrefs>) => void;
|
||||
|
||||
/** @deprecated: please use `useSyncedPref` (singular) */
|
||||
export function useSyncedPrefs(): [SyncedPrefs, SetSyncedPrefsAction] {
|
||||
// TODO: implement real logic (follow-up PR)
|
||||
const dispatch = useDispatch();
|
||||
const setPrefs = useCallback<SetSyncedPrefsAction>(
|
||||
newPrefs => {
|
||||
dispatch(savePrefs(newPrefs));
|
||||
},
|
||||
[dispatch],
|
||||
const { data: queryData } = useQuery<{ id: string; value: string }[]>(
|
||||
() => q('preferences').select(['id', 'value']),
|
||||
[],
|
||||
);
|
||||
|
||||
return [useLocalPrefs(), setPrefs];
|
||||
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];
|
||||
}
|
||||
|
||||
59
packages/loot-core/migrations/1723665565000_prefs.js
Normal file
59
packages/loot-core/migrations/1723665565000_prefs.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const SYNCED_PREF_KEYS = [
|
||||
'firstDayOfWeekIdx',
|
||||
'dateFormat',
|
||||
'numberFormat',
|
||||
'hideFraction',
|
||||
'isPrivacyEnabled',
|
||||
/^show-extra-balances-/,
|
||||
/^hide-cleared-/,
|
||||
/^parse-date-/,
|
||||
/^csv-mappings-/,
|
||||
/^csv-delimiter-/,
|
||||
/^csv-has-header-/,
|
||||
/^ofx-fallback-missing-payee-/,
|
||||
/^flip-amount-/,
|
||||
// 'budgetType', // TODO: uncomment when `budgetType` moves from metadata to synced prefs
|
||||
/^flags\./,
|
||||
];
|
||||
|
||||
export default async function runMigration(db, { fs, fileId }) {
|
||||
await db.execQuery(`
|
||||
CREATE TABLE preferences
|
||||
(id TEXT PRIMARY KEY,
|
||||
value TEXT);
|
||||
`);
|
||||
|
||||
try {
|
||||
const budgetDir = fs.getBudgetDir(fileId);
|
||||
const fullpath = fs.join(budgetDir, 'metadata.json');
|
||||
|
||||
const prefs = JSON.parse(await fs.readFile(fullpath));
|
||||
|
||||
if (typeof prefs !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(prefs).map(async key => {
|
||||
// Check if the current key is of synced-keys type
|
||||
if (
|
||||
!SYNCED_PREF_KEYS.find(keyMatcher =>
|
||||
keyMatcher instanceof RegExp
|
||||
? keyMatcher.test(key)
|
||||
: keyMatcher === key,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// insert the synced prefs in the new table
|
||||
await db.runQuery('INSERT INTO preferences (id, value) VALUES (?, ?)', [
|
||||
key,
|
||||
String(prefs[key]),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,12 @@ export async function runQuery(query) {
|
||||
return send('query', query.serialize());
|
||||
}
|
||||
|
||||
export function liveQuery(query, onData?, opts?): LiveQuery {
|
||||
const q = new LiveQuery(query, onData, opts);
|
||||
export function liveQuery<Response = unknown>(
|
||||
query,
|
||||
onData?: (response: Response) => void,
|
||||
opts?,
|
||||
): LiveQuery {
|
||||
const q = new LiveQuery<Response>(query, onData, opts);
|
||||
q.run();
|
||||
return q;
|
||||
}
|
||||
@@ -20,7 +24,7 @@ export function pagedQuery(query, onData?, opts?): PagedQuery {
|
||||
}
|
||||
|
||||
// Subscribe and refetch
|
||||
export class LiveQuery {
|
||||
export class LiveQuery<Response = unknown> {
|
||||
_unsubscribe;
|
||||
data;
|
||||
dependencies;
|
||||
@@ -36,7 +40,11 @@ export class LiveQuery {
|
||||
inflightRequestId;
|
||||
restart;
|
||||
|
||||
constructor(query, onData?, opts: { mapper?; onlySync?: boolean } = {}) {
|
||||
constructor(
|
||||
query,
|
||||
onData?: (response: Response) => void,
|
||||
opts: { mapper?; onlySync?: boolean } = {},
|
||||
) {
|
||||
this.error = new Error();
|
||||
this.query = query;
|
||||
this.data = null;
|
||||
|
||||
@@ -74,21 +74,42 @@ export function useLiveQuery<Response = unknown>(
|
||||
makeQuery: () => Query,
|
||||
deps: DependencyList,
|
||||
): Response {
|
||||
const [data, setData] = useState(null);
|
||||
const { data } = useQuery<Response>(makeQuery, deps);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function useQuery<Response = unknown>(
|
||||
makeQuery: () => Query,
|
||||
deps: DependencyList,
|
||||
): {
|
||||
data: Response;
|
||||
overrideData: (newData: Response) => void;
|
||||
isLoading: boolean;
|
||||
} {
|
||||
const [data, setData] = useState<null | Response>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const query = useMemo(makeQuery, deps);
|
||||
|
||||
useEffect(() => {
|
||||
let live = liveQuery(query, async data => {
|
||||
setIsLoading(true);
|
||||
|
||||
let live = liveQuery<Response>(query, async data => {
|
||||
if (live) {
|
||||
setIsLoading(false);
|
||||
setData(data);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
setIsLoading(false);
|
||||
live.unsubscribe();
|
||||
live = null;
|
||||
};
|
||||
}, [query]);
|
||||
|
||||
return data;
|
||||
return {
|
||||
data,
|
||||
overrideData: setData,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -122,6 +122,10 @@ export const schema = {
|
||||
id: f('id'),
|
||||
note: f('string'),
|
||||
},
|
||||
preferences: {
|
||||
id: f('id'),
|
||||
value: f('string'),
|
||||
},
|
||||
transaction_filters: {
|
||||
id: f('id'),
|
||||
name: f('string'),
|
||||
|
||||
@@ -53,6 +53,7 @@ import { mutator, runHandler } from './mutators';
|
||||
import { app as notesApp } from './notes/app';
|
||||
import * as Platform from './platform';
|
||||
import { get, post } from './post';
|
||||
import { app as preferencesApp } from './preferences/app';
|
||||
import * as prefs from './prefs';
|
||||
import { app as reportsApp } from './reports/app';
|
||||
import { app as rulesApp } from './rules/app';
|
||||
@@ -2079,6 +2080,7 @@ app.combine(
|
||||
budgetApp,
|
||||
dashboardApp,
|
||||
notesApp,
|
||||
preferencesApp,
|
||||
toolsApp,
|
||||
filtersApp,
|
||||
reportsApp,
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
// them which doesn't play well with CSP. There isn't great, and eventually
|
||||
// we can remove this migration.
|
||||
import { Database } from '@jlongster/sql.js';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import m1632571489012 from '../../../migrations/1632571489012_remove_cache';
|
||||
import m1722717601000 from '../../../migrations/1722717601000_reports_move_selected_categories';
|
||||
import m1722804019000 from '../../../migrations/1722804019000_create_dashboard_table';
|
||||
import m1723665565000 from '../../../migrations/1723665565000_prefs';
|
||||
import * as fs from '../../platform/server/fs';
|
||||
import * as sqlite from '../../platform/server/sqlite';
|
||||
import * as prefs from '../prefs';
|
||||
|
||||
let MIGRATIONS_DIR = fs.migrationsPath;
|
||||
|
||||
@@ -17,6 +18,7 @@ const javascriptMigrations = {
|
||||
1632571489012: m1632571489012,
|
||||
1722717601000: m1722717601000,
|
||||
1722804019000: m1722804019000,
|
||||
1723665565000: m1723665565000,
|
||||
};
|
||||
|
||||
export async function withMigrationsDir(
|
||||
@@ -107,7 +109,10 @@ async function applyJavaScript(db, id) {
|
||||
}
|
||||
|
||||
const run = javascriptMigrations[id];
|
||||
return run(dbInterface, () => uuidv4());
|
||||
return run(dbInterface, {
|
||||
fs,
|
||||
fileId: prefs.getPrefs()?.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function applySql(db, sql) {
|
||||
|
||||
21
packages/loot-core/src/server/preferences/app.ts
Normal file
21
packages/loot-core/src/server/preferences/app.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { type SyncedPrefs } from '../../types/prefs';
|
||||
import { createApp } from '../app';
|
||||
import * as db from '../db';
|
||||
import { mutator } from '../mutators';
|
||||
import { undoable } from '../undo';
|
||||
|
||||
import { PreferencesHandlers } from './types/handlers';
|
||||
|
||||
export const app = createApp<PreferencesHandlers>();
|
||||
|
||||
const savePreferences = async ({
|
||||
id,
|
||||
value,
|
||||
}: {
|
||||
id: keyof SyncedPrefs;
|
||||
value: string | undefined;
|
||||
}) => {
|
||||
await db.update('preferences', { id, value });
|
||||
};
|
||||
|
||||
app.method('preferences/save', mutator(undoable(savePreferences)));
|
||||
8
packages/loot-core/src/server/preferences/types/handlers.d.ts
vendored
Normal file
8
packages/loot-core/src/server/preferences/types/handlers.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { type SyncedPrefs } from '../../../types/prefs';
|
||||
|
||||
export interface PreferencesHandlers {
|
||||
'preferences/save': (arg: {
|
||||
id: keyof SyncedPrefs;
|
||||
value: string | undefined;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
@@ -28,19 +28,6 @@ export async function loadPrefs(id?: string): Promise<MetadataPrefs> {
|
||||
prefs = { id, budgetName: id };
|
||||
}
|
||||
|
||||
// delete released feature flags
|
||||
const releasedFeatures = ['syncAccount'];
|
||||
for (const feature of releasedFeatures) {
|
||||
delete prefs[`flags.${feature}`];
|
||||
}
|
||||
|
||||
// delete legacy notifications
|
||||
for (const key of Object.keys(prefs)) {
|
||||
if (key.startsWith('notifications.')) {
|
||||
delete prefs[key];
|
||||
}
|
||||
}
|
||||
|
||||
// No matter what is in `id` field, force it to be the current id.
|
||||
// This makes it resilient to users moving around folders, etc
|
||||
prefs.id = id;
|
||||
|
||||
2
packages/loot-core/src/types/handlers.d.ts
vendored
2
packages/loot-core/src/types/handlers.d.ts
vendored
@@ -2,6 +2,7 @@ import type { BudgetHandlers } from '../server/budget/types/handlers';
|
||||
import type { DashboardHandlers } from '../server/dashboard/types/handlers';
|
||||
import type { FiltersHandlers } from '../server/filters/types/handlers';
|
||||
import type { NotesHandlers } from '../server/notes/types/handlers';
|
||||
import type { PreferencesHandlers } from '../server/preferences/types/handlers';
|
||||
import type { ReportsHandlers } from '../server/reports/types/handlers';
|
||||
import type { RulesHandlers } from '../server/rules/types/handlers';
|
||||
import type { SchedulesHandlers } from '../server/schedules/types/handlers';
|
||||
@@ -17,6 +18,7 @@ export interface Handlers
|
||||
DashboardHandlers,
|
||||
FiltersHandlers,
|
||||
NotesHandlers,
|
||||
PreferencesHandlers,
|
||||
ReportsHandlers,
|
||||
RulesHandlers,
|
||||
SchedulesHandlers,
|
||||
|
||||
5
packages/loot-core/src/types/prefs.d.ts
vendored
5
packages/loot-core/src/types/prefs.d.ts
vendored
@@ -55,11 +55,10 @@ export type MetadataPrefs = Partial<{
|
||||
|
||||
/**
|
||||
* Local preferences applicable to a single device. Stored in local storage.
|
||||
* TODO: eventually `LocalPrefs` type should not use `SyncedPrefs` or `MetadataPrefs`;
|
||||
* TODO: eventually `LocalPrefs` type should not use `MetadataPrefs`;
|
||||
* this is only a stop-gap solution.
|
||||
*/
|
||||
export type LocalPrefs = SyncedPrefs &
|
||||
MetadataPrefs &
|
||||
export type LocalPrefs = MetadataPrefs &
|
||||
Partial<{
|
||||
'ui.showClosedAccounts': boolean;
|
||||
'expand-splits': boolean;
|
||||
|
||||
6
upcoming-release-notes/3423.md
Normal file
6
upcoming-release-notes/3423.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [MatissJanis]
|
||||
---
|
||||
|
||||
SyncedPrefs: move synced-preferences from metadata.json to the database.
|
||||
Reference in New Issue
Block a user