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}
+
+ ) : 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
+}