Added Global Synced Prefs (#6234)

* Added Global Synced Prefs

* [autofix.ci] apply automated fixes

* Add release notes for PR #6234

* typecheck

* lint fix

* Refactor global synced preferences to server preferences

- Removed global synced preferences implementation and related files.
- Introduced server preferences with a new slice and hooks for managing user settings.
- Updated components and hooks to utilize server preferences instead of global synced preferences.
- Adjusted Redux store and mock configurations to reflect the changes.
- Enhanced user settings consistency across devices with the new server preferences structure.

* Implement server preferences for feature flags and enhance admin permissions

- Updated the Experimental component to conditionally display based on user permissions and login method.
- Refactored feature flag handling to use 'flags.plugins' instead of 'plugins'.
- Introduced server-side checks to restrict access to server preferences for admin users only.
- Added comprehensive tests for server preferences management, ensuring proper handling of user roles and preferences.

* Enhance error handling in saveServerPrefs thunk

- Updated the saveServerPrefs async thunk to handle potential errors from the server response.
- Added a check for the presence of an error in the result and return it accordingly.
- Ensured that preferences are still dispatched to the store upon successful save.

* Feedback: strict "flags.plugins" typing

* Feedback: move state slice

* Feedback: localstorage pref

* Feedback: move serverPrefsSlide into prefsSlice

* Refactor: Remove duplicate import of PostError in app.ts

* Rename serverPrefs state slice property to server (#6596)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
lelemm
2026-01-09 05:17:36 -03:00
committed by GitHub
parent da1a2457ba
commit fed1cd7d30
12 changed files with 442 additions and 18 deletions

View File

@@ -5,14 +5,22 @@ import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import type { FeatureFlag, SyncedPrefs } from 'loot-core/types/prefs';
import type { FeatureFlag, ServerPrefs } from 'loot-core/types/prefs';
import { Setting } from './UI';
import { useAuth } from '@desktop-client/auth/AuthProvider';
import { Permissions } from '@desktop-client/auth/types';
import { Link } from '@desktop-client/components/common/Link';
import { Checkbox } from '@desktop-client/components/forms';
import {
useLoginMethod,
useMultiuserEnabled,
} from '@desktop-client/components/ServerContext';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useServerPref } from '@desktop-client/hooks/useServerPref';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import { useSyncServerStatus } from '@desktop-client/hooks/useSyncServerStatus';
type FeatureToggleProps = {
flag: FeatureFlag;
@@ -68,22 +76,42 @@ function FeatureToggle({
);
}
type GlobalFeatureToggleProps = {
prefName: keyof SyncedPrefs;
type ServerFeatureToggleProps = {
prefName: keyof ServerPrefs;
disableToggle?: boolean;
error?: ReactNode;
children: ReactNode;
feedbackLink?: string;
};
function GlobalFeatureToggle({
function ServerFeatureToggle({
prefName,
disableToggle = false,
feedbackLink,
error,
children,
}: GlobalFeatureToggleProps) {
const [enabled, setEnabled] = useSyncedPref(prefName);
}: ServerFeatureToggleProps) {
const [enabled, setEnabled] = useServerPref(prefName);
const syncServerStatus = useSyncServerStatus();
const isUsingServer = syncServerStatus !== 'no-server';
const isServerOffline = syncServerStatus === 'offline';
const { hasPermission } = useAuth();
const loginMethod = useLoginMethod();
const multiuserEnabled = useMultiuserEnabled();
if (!isUsingServer || isServerOffline) {
return null;
}
// Show to admins if OIDC is enabled, or to everyone if multi-user is not enabled
const isAdmin = hasPermission(Permissions.ADMINISTRATOR);
const oidcEnabled = loginMethod === 'openid';
const shouldShow = (oidcEnabled && isAdmin) || !multiuserEnabled;
if (!shouldShow) {
return null;
}
return (
<label style={{ display: 'flex' }}>
@@ -131,6 +159,9 @@ export function ExperimentalFeatures() {
(goalTemplatesEnabled &&
localStorage.getItem('devEnableGoalTemplatesUI') === 'true');
const showServerPrefs =
localStorage.getItem('devEnableServerPrefs') === 'true';
return (
<Setting
primaryAction={
@@ -174,13 +205,15 @@ export function ExperimentalFeatures() {
<FeatureToggle flag="forceReload">
<Trans>Force reload app button</Trans>
</FeatureToggle>
<GlobalFeatureToggle
prefName="plugins"
disableToggle
feedbackLink="https://github.com/actualbudget/actual/issues/5950"
>
<Trans>Client-Side plugins (soon)</Trans>
</GlobalFeatureToggle>
{showServerPrefs && (
<ServerFeatureToggle
prefName="flags.plugins"
disableToggle
feedbackLink="https://github.com/actualbudget/actual/issues/5950"
>
<Trans>Client-Side plugins (soon)</Trans>
</ServerFeatureToggle>
)}
</View>
) : (
<Link

View File

@@ -9,7 +9,6 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
formulaMode: false,
currency: false,
crossoverReport: false,
plugins: false,
forceReload: false,
};

View File

@@ -0,0 +1,31 @@
import { useCallback } from 'react';
import { type ServerPrefs } from 'loot-core/types/prefs';
import { saveServerPrefs } from '@desktop-client/prefs/prefsSlice';
import { useDispatch, useSelector } from '@desktop-client/redux';
type SetServerPrefAction<K extends keyof ServerPrefs> = (
value: ServerPrefs[K],
) => void;
export function useServerPref<K extends keyof ServerPrefs>(
prefName: K,
): [ServerPrefs[K], SetServerPrefAction<K>] {
const dispatch = useDispatch();
const setPref = useCallback<SetServerPrefAction<K>>(
value => {
dispatch(
saveServerPrefs({
prefs: { [prefName]: value },
}),
);
},
[dispatch, prefName],
);
const pref = useSelector(state => state.prefs.server[prefName]);
return [pref, setPref];
}

View File

@@ -5,6 +5,7 @@ import { parseNumberFormat, setNumberFormat } from 'loot-core/shared/util';
import {
type GlobalPrefs,
type MetadataPrefs,
type ServerPrefs,
type SyncedPrefs,
} from 'loot-core/types/prefs';
@@ -12,6 +13,7 @@ import { resetApp } from '@desktop-client/app/appSlice';
import { setI18NextLanguage } from '@desktop-client/i18n';
import { closeModal } from '@desktop-client/modals/modalsSlice';
import { createAppAsyncThunk } from '@desktop-client/redux';
import { getUserData } from '@desktop-client/users/usersSlice';
const sliceName = 'prefs';
@@ -19,12 +21,14 @@ type PrefsState = {
local: MetadataPrefs;
global: GlobalPrefs;
synced: SyncedPrefs;
server: ServerPrefs;
};
const initialState: PrefsState = {
local: {},
global: {},
synced: {},
server: {},
};
export const loadPrefs = createAppAsyncThunk(
@@ -125,6 +129,23 @@ export const saveSyncedPrefs = createAppAsyncThunk(
},
);
type SaveServerPrefsPayload = {
prefs: ServerPrefs;
};
export const saveServerPrefs = createAppAsyncThunk(
`${sliceName}/saveServerPrefs`,
async ({ prefs }: SaveServerPrefsPayload, { dispatch }) => {
const result = await send('save-server-prefs', { prefs });
if (result && 'error' in result) {
return { error: result.error };
}
dispatch(mergeServerPrefs(prefs));
return {};
},
);
type SetPrefsPayload = {
local: MetadataPrefs;
global: GlobalPrefs;
@@ -134,6 +155,7 @@ type SetPrefsPayload = {
type MergeLocalPrefsPayload = MetadataPrefs;
type MergeGlobalPrefsPayload = GlobalPrefs;
type MergeSyncedPrefsPayload = SyncedPrefs;
type MergeServerPrefsPayload = ServerPrefs;
const prefsSlice = createSlice({
name: sliceName,
@@ -153,12 +175,34 @@ const prefsSlice = createSlice({
mergeSyncedPrefs(state, action: PayloadAction<MergeSyncedPrefsPayload>) {
state.synced = { ...state.synced, ...action.payload };
},
mergeServerPrefs(state, action: PayloadAction<MergeServerPrefsPayload>) {
state.server = { ...state.server, ...action.payload };
},
},
extraReducers: builder => {
builder.addCase(resetApp, state => ({
...initialState,
global: state.global || initialState.global,
server: state.server || initialState.server,
}));
builder.addCase(getUserData.fulfilled, (state, action) => {
if (!action.payload || typeof action.payload !== 'object') {
return state;
}
const { serverPrefs } = action.payload as {
serverPrefs?: ServerPrefs | null;
};
if (!serverPrefs) {
return state;
}
state.server = {
...state.server,
...serverPrefs,
};
});
},
});
@@ -171,7 +215,13 @@ export const actions = {
loadGlobalPrefs,
saveGlobalPrefs,
saveSyncedPrefs,
saveServerPrefs,
};
export const { mergeGlobalPrefs, mergeLocalPrefs, mergeSyncedPrefs, setPrefs } =
actions;
export const {
mergeGlobalPrefs,
mergeLocalPrefs,
mergeServerPrefs,
mergeSyncedPrefs,
setPrefs,
} = actions;