mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-21 15:36:50 -05:00
✨ 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:
@@ -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
|
||||
|
||||
@@ -9,7 +9,6 @@ const DEFAULT_FEATURE_FLAG_STATE: Record<FeatureFlag, boolean> = {
|
||||
formulaMode: false,
|
||||
currency: false,
|
||||
crossoverReport: false,
|
||||
plugins: false,
|
||||
forceReload: false,
|
||||
};
|
||||
|
||||
|
||||
31
packages/desktop-client/src/hooks/useServerPref.ts
Normal file
31
packages/desktop-client/src/hooks/useServerPref.ts
Normal 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];
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user