Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions packages/insomnia/src/ui/stores/settings-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect } from 'react';

import type { ChangeBufferEvent, Settings } from '~/insomnia-data';
import { models, services } from '~/insomnia-data';
import type { KeyboardShortcut, KeyCombination, PluginConfigMap } from '~/insomnia-data/common';
import { getPlatformKeyCombinations, newDefaultRegistry } from '~/insomnia-data/common';
import { AnalyticsEvent } from '~/ui/analytics';
import { useOptimisticMutation } from '~/ui/stores/use-optimistic-mutation';

export const SETTINGS_STORE_KEY = ['settings'];

let registerCount = 0;
let unsubscribe: (() => void) | null = null;

function useDBListener() {
const queryClient = useQueryClient();

useEffect(() => {
registerCount += 1;

if (registerCount === 1) {
unsubscribe = window.main.on('db.changes', (_, changes: ChangeBufferEvent[]) => {
const hasSettingsChange = changes.some(([_, doc]) => doc.type === models.settings.type);
if (hasSettingsChange) {
queryClient.invalidateQueries({ queryKey: SETTINGS_STORE_KEY });
}
});
}

return () => {
registerCount -= 1;

if (registerCount === 0 && unsubscribe) {
unsubscribe();
unsubscribe = null;
}
};
}, [queryClient]);
}

export function useSettingsStore(selector?: (settings: Settings) => any) {
const { data: settings, ...rest } = useQuery({
queryKey: SETTINGS_STORE_KEY,
queryFn: async () => {
const settings = await services.settings.get();
return settings;
},
selector,
});

// computed
const httpProxyHasCredentials = settings?.httpProxy?.includes('@');
const isProxyConfigured = Boolean(settings?.proxyEnabled && (settings.httpProxy || settings.httpsProxy));
const hasCustomPluginPath = Boolean(settings?.pluginPath);
const hasDataFolderRestrictions = Boolean(settings?.dataFolders && settings.dataFolders.length > 0);
Comment on lines +42 to +56

const { mutateResult, ...mutation } = useOptimisticMutation({
mutationFn: async (patch: Partial<Settings>, { client }) => {
const updatedSettings = await services.settings.patch(patch);
client.setQueryData(SETTINGS_STORE_KEY, updatedSettings);
if ('enableAnalytics' in patch && !patch.enableAnalytics) {
window.main.trackAnalyticsEvent({ event: AnalyticsEvent.analyticsDisabled });
}
return updatedSettings;
},
invalidateKey: SETTINGS_STORE_KEY,
});
Comment on lines +58 to +68

// --- Semantic actions ---
const { mutate } = mutation;

const resetHotKeys = useCallback(() => {
mutate({ hotKeyRegistry: newDefaultRegistry() });
}, [mutate]);

const resetSingleHotKey = useCallback(
(shortcut: KeyboardShortcut) => {
mutateResult((settings: Settings) => {
if (!settings?.hotKeyRegistry) {
return settings;
}
const hotKeyRegistry = { ...settings.hotKeyRegistry, [shortcut]: newDefaultRegistry()[shortcut] };
return { hotKeyRegistry };
});
},
[mutateResult],
);

const addKeyCombination = useCallback(
(shortcut: KeyboardShortcut, keyComb: KeyCombination) => {
mutateResult((settings: Settings) => {
if (!settings?.hotKeyRegistry) {
return settings;
}

const hotKeyRegistry = structuredClone(settings.hotKeyRegistry);
const keyCombs = getPlatformKeyCombinations(hotKeyRegistry[shortcut]);
keyCombs.push(keyComb);
return { hotKeyRegistry };
});
},
[mutateResult],
);

const removeKeyCombination = useCallback(
(shortcut: KeyboardShortcut, keyComb: KeyCombination) => {
mutateResult((settings: Settings) => {
if (!settings?.hotKeyRegistry) {
return settings;
}
const hotKeyRegistry = structuredClone(settings.hotKeyRegistry);
const keyCombs = getPlatformKeyCombinations(hotKeyRegistry[shortcut]);
const idx = keyCombs.findIndex(
k =>
k.keyCode === keyComb.keyCode &&
Boolean(k.alt) === Boolean(keyComb.alt) &&
Boolean(k.shift) === Boolean(keyComb.shift) &&
Boolean(k.ctrl) === Boolean(keyComb.ctrl) &&
Boolean(k.meta) === Boolean(keyComb.meta),
);
if (idx !== -1) {
keyCombs.splice(idx, 1);
return { hotKeyRegistry };
}
return settings;
});
},
[mutateResult],
);

const toggleVariableSourceAndValue = useCallback(() => {
mutateResult((settings: Settings) => {
if (!settings) {
return settings;
}
return { showVariableSourceAndValue: !settings.showVariableSourceAndValue };
});
}, [mutateResult]);

const togglePlugin = useCallback(
(pluginName: string, enabled: boolean) => {
mutateResult((settings: Settings) => {
if (!settings?.pluginConfig) {
return settings;
}
const pluginConfig: PluginConfigMap = {
...settings.pluginConfig,
[pluginName]: { ...settings.pluginConfig[pluginName], disabled: !enabled },
};
return { pluginConfig };
});
},
[mutateResult],
);

const toggleBulkHeaderEditor = useCallback(() => {
mutateResult((settings: Settings) => {
if (!settings) {
return settings;
}
return { useBulkHeaderEditor: !settings.useBulkHeaderEditor };
});
}, [mutateResult]);

const toggleBulkParametersEditor = useCallback(() => {
mutateResult((settings: Settings) => {
if (!settings) {
return settings;
}
return { useBulkParametersEditor: !settings.useBulkParametersEditor };
});
}, [mutateResult]);

useDBListener();

return {
settings,
...rest,
// computed
httpProxyHasCredentials,
isProxyConfigured,
hasCustomPluginPath,
hasDataFolderRestrictions,
// generic update
mutation,
// semantic actions
resetHotKeys,
resetSingleHotKey,
addKeyCombination,
removeKeyCombination,
toggleVariableSourceAndValue,
togglePlugin,
toggleBulkHeaderEditor,
toggleBulkParametersEditor,
};
}
64 changes: 64 additions & 0 deletions packages/insomnia/src/ui/stores/use-optimistic-mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useCallback } from 'react';

export type Variables<T> = Partial<T> | ((variables: T) => Promise<Partial<T>> | Partial<T>);

export function useOptimisticMutation(
options: Parameters<typeof useMutation>[0] & { invalidateKey: any[]; patch: boolean },
) {
const { invalidateKey, mutationFn, onMutate, onSettled, patch, ...rest } = options;
Comment on lines +6 to +9
const mutation = useMutation({
...rest,
mutationFn: (...args: any) => {
const [variables, { client }] = args;
const previousSettings = client.getQueryData(invalidateKey);
client.setQueryData(invalidateKey, patch ? { ...previousSettings, ...variables } : variables);

return mutationFn?.(...args);
},
Comment on lines +12 to +18
// before the mutation
onMutate: (...args: any) => {
const [_, { client }] = args;
const previous = client.getQueryData(invalidateKey);
const result = onMutate?.(...args) || {};
return { _previous: previous, ...result };
},
Comment on lines +20 to +25
onSettled: (...args: any) => {
const [data, error, variables, { _previous }, { client }] = args;
if (error && _previous) {
// rollback first
client.setQueryData(invalidateKey, _previous);
}
onSettled?.(...args);
// Always refetch after error or success:
client.invalidateQueries({ queryKey: invalidateKey });
},
Comment on lines +26 to +35
});

const client = useQueryClient();
const { mutate } = mutation;

const mutateResult = useCallback(
<T>(doMutation: Variables<T>) => {
const settings = client.getQueryData(invalidateKey);
let result;
if (typeof doMutation === 'function') {
result = doMutation(settings);
if (result instanceof Promise) {
result.then(res => {
if (settings !== res) {
mutate(res);
}
});
return;
}
}
if (settings !== result) {
mutate(result);
}
},
[client, mutate, invalidateKey], // invalidateKey is not expected to change, but we include it for completeness
);

return { ...mutation, mutateResult };
}
Loading