From 7fcec3e7432fca9d2356ba5fedf6d794e1d02eed Mon Sep 17 00:00:00 2001 From: DannyM300 Date: Tue, 4 Aug 2026 13:26:02 +0100 Subject: [PATCH] fix: trim saved profiles to only their non-default filters useStorage.getState() was saved verbatim, including every filter key at its default value - the whole point of a profile backup is the handful a user actually changed, not the full menu. The task filter feature just added a large new set of possible keys on top of the existing rocket/invasion/quest ones, making this far more likely to hit the existing 413 "backup too large" handling than before. createBackupData() diffs the current filters against the server's live defaults (useMemory.filters) and keeps only what differs. Safe on load: the backup write already clears localStorage and reloads the page, so useMapData's existing deepMerge(serverDefaults, loadedState) fills back in anything omitted because it matched the default at save time. Also adds error feedback to the Update button, which previously failed silently on the same 413 this was written to reduce. Co-Authored-By: Claude Opus 5 --- src/features/profile/Backups.jsx | 152 +++++++++++++++++------------ src/features/profile/backupData.js | 63 ++++++++++++ 2 files changed, 154 insertions(+), 61 deletions(-) create mode 100644 src/features/profile/backupData.js diff --git a/src/features/profile/Backups.jsx b/src/features/profile/Backups.jsx index 98b656456..78b4ce951 100644 --- a/src/features/profile/Backups.jsx +++ b/src/features/profile/Backups.jsx @@ -21,6 +21,28 @@ import Box from '@mui/material/Box' import { useMemory } from '@store/useMemory' import { useStorage } from '@store/useStorage' import { Query } from '@services/queries' +import { createBackupData } from './backupData' + +/** @param {unknown} err @param {(key: string) => string} t */ +function getBackupErrorMessage(err, t) { + let message = t('backup_error_generic') + if (err instanceof ApolloError) { + const { networkError } = err + if ( + networkError && + 'statusCode' in networkError && + networkError.statusCode === 413 + ) { + message = t('backup_error_too_large') + } else if (err.message) { + message = err.message + } + } + return message +} + +const getCurrentBackupData = () => + createBackupData(useStorage.getState(), useMemory.getState().filters) export function UserBackups() { const { t } = useTranslation() @@ -73,24 +95,11 @@ function CreateNew({ backups }) { setErrorMessage('') try { await create({ - variables: { backup: { name, data: useStorage.getState() } }, + variables: { backup: { name, data: getCurrentBackupData() } }, }) setName('') } catch (err) { - let message = t('backup_error_generic') - if (err instanceof ApolloError) { - const { networkError } = err - if ( - networkError && - 'statusCode' in networkError && - networkError.statusCode === 413 - ) { - message = t('backup_error_too_large') - } else if (err.message) { - message = err.message - } - } - setErrorMessage(message) + setErrorMessage(getBackupErrorMessage(err, t)) } }, [backups, create, loading, name, t, userBackupLimits]) @@ -139,6 +148,7 @@ function BackupItem({ backup }) { const { t } = useTranslation() const [name, setName] = React.useState(backup.name) const [loading, setLoading] = React.useState(false) + const [errorMessage, setErrorMessage] = React.useState('') const [update, { loading: l1 }] = useMutation(Query.user('UPDATE_BACKUP'), { refetchQueries: ['GetBackups'], @@ -153,6 +163,24 @@ function BackupItem({ backup }) { React.useEffect(() => setName(backup.name), [backup]) React.useEffect(() => setLoading(l1 || l2 || l3), [l1, l2, l3]) + const handleUpdate = React.useCallback(async () => { + if (loading) return + setErrorMessage('') + try { + await update({ + variables: { + backup: { + id: backup.id, + name, + data: getCurrentBackupData(), + }, + }, + }) + } catch (err) { + setErrorMessage(getBackupErrorMessage(err, t)) + } + }, [backup.id, loading, name, t, update]) + React.useEffect(() => { if (fullBackup?.backup?.data) { try { @@ -180,54 +208,56 @@ function BackupItem({ backup }) { }, [fullBackup]) return ( - - setName(e.target.value)} - variant="outlined" - sx={{ mr: 2 }} - /> - - - - + + + + + {errorMessage ? ( + - {t('delete')} - - + {errorMessage} + + ) : null} ) } diff --git a/src/features/profile/backupData.js b/src/features/profile/backupData.js new file mode 100644 index 000000000..97f13713b --- /dev/null +++ b/src/features/profile/backupData.js @@ -0,0 +1,63 @@ +// @ts-check + +/** @param {unknown} value */ +const isPlainObject = (value) => + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + Object.getPrototypeOf(value) === Object.prototype + +/** + * Returns only values that differ from the matching defaults. + * Unknown keys are deliberately retained for forwards/backwards compatibility. + * + * @param {unknown} value + * @param {unknown} defaults + * @returns {unknown} + */ +function getDifference(value, defaults) { + if (Object.is(value, defaults)) return undefined + + if (Array.isArray(value) && Array.isArray(defaults)) { + if ( + value.length === defaults.length && + value.every((entry, index) => + Object.is(getDifference(entry, defaults[index]), undefined), + ) + ) { + return undefined + } + return value + } + + if (isPlainObject(value) && isPlainObject(defaults)) { + const difference = {} + Object.entries(value).forEach(([key, entry]) => { + const entryDifference = Object.prototype.hasOwnProperty.call( + defaults, + key, + ) + ? getDifference(entry, defaults[key]) + : entry + if (entryDifference !== undefined) difference[key] = entryDifference + }) + return Object.keys(difference).length ? difference : undefined + } + + return value +} + +/** + * Produces a JSON-safe profile payload. Filter values matching the current + * server defaults are omitted because useMapData merges those defaults back in + * when a profile is loaded. + * + * @param {Record} state + * @param {Record} defaultFilters + */ +export function createBackupData(state, defaultFilters) { + const backup = JSON.parse(JSON.stringify(state)) + backup.filters = + getDifference(backup.filters || {}, defaultFilters || {}) || {} + return backup +}