Merge pull request 'Fix legacy settings migration' (#3562) from fix/config-migration-3559 into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3562
This commit is contained in:
commit
99c7d06758
3 changed files with 295 additions and 74 deletions
1
changelog.d/config-migration.fix
Normal file
1
changelog.d/config-migration.fix
Normal file
|
|
@ -0,0 +1 @@
|
|||
Fix legacy settings migration being discarded after it ran.
|
||||
|
|
@ -687,83 +687,13 @@ export const useSyncConfigStore = defineStore('sync_config', {
|
|||
)
|
||||
recent = _wrapData({
|
||||
flagStorage: { ...flagsTemplate },
|
||||
prefsStorage: { ...defaultState.prefsStorage },
|
||||
prefsStorage: cloneDeep(defaultState.prefsStorage),
|
||||
})
|
||||
}
|
||||
|
||||
recent = recent && (await _doMigrations(recent, this.setPreference))
|
||||
stale = stale && (await _doMigrations(stale, this.setPreference))
|
||||
|
||||
// Various migrations
|
||||
console.debug('Migrating from old config')
|
||||
const vuexState = (await storage.getItem('vuex-lz')) ?? {}
|
||||
const config = vuexState.config ?? {}
|
||||
|
||||
const migratedEntries = new Set(config._syncMigration ?? [])
|
||||
console.debug(
|
||||
`Already migrated Values: ${[...migratedEntries].join() || '[none]'}`,
|
||||
)
|
||||
|
||||
Object.entries(oldDefaultConfigSync).forEach(([key, value]) => {
|
||||
const oldValue = config[key]
|
||||
const defaultValue = value
|
||||
|
||||
const present = oldValue !== undefined
|
||||
const migrated = migratedEntries.has(key)
|
||||
const different = !isEqual(oldValue, defaultValue)
|
||||
|
||||
if (present && !migrated && different) {
|
||||
console.debug(`Migrating config ${key}: ${oldValue}`)
|
||||
if (key === 'theme3hacks') {
|
||||
useLocalConfigStore().set({
|
||||
path: 'fontInterface',
|
||||
value: oldValue.fonts.interface,
|
||||
})
|
||||
useLocalConfigStore().set({
|
||||
path: 'fontInput',
|
||||
value: oldValue.fonts.input,
|
||||
})
|
||||
useLocalConfigStore().set({
|
||||
path: 'fontPost',
|
||||
value: oldValue.fonts.post,
|
||||
})
|
||||
useLocalConfigStore().set({
|
||||
path: 'fontMonospace',
|
||||
value: oldValue.fonts.monospace,
|
||||
})
|
||||
useSyncConfigStore().setSimplePrefAndSave({
|
||||
path: 'underlay',
|
||||
value: oldValue.underlay,
|
||||
})
|
||||
} else if (key == 'muteWords') {
|
||||
oldValue.forEach((word, order) => {
|
||||
const uniqueId = uuidv4()
|
||||
|
||||
useSyncConfigStore().setPreference({
|
||||
path: 'simple.muteFilters.' + uniqueId,
|
||||
value: {
|
||||
type: 'word',
|
||||
value: word,
|
||||
name: word,
|
||||
enabled: true,
|
||||
expires: null,
|
||||
hide: false,
|
||||
order,
|
||||
},
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.setPreference({ path: `simple.${key}`, value: oldValue })
|
||||
}
|
||||
migratedEntries.add(key)
|
||||
needUpload = true
|
||||
}
|
||||
})
|
||||
|
||||
config._syncMigration = [...migratedEntries]
|
||||
vuexState.config = config
|
||||
storage.setItem('vuex-lz', vuexState)
|
||||
|
||||
if (!needUpload && recent && stale) {
|
||||
console.debug('Checking if data needs merging...')
|
||||
// discarding timestamps and versions
|
||||
|
|
@ -794,14 +724,99 @@ export const useSyncConfigStore = defineStore('sync_config', {
|
|||
recent.flagStorage = { ...flagsTemplate, ...totalFlags }
|
||||
recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
|
||||
|
||||
this.dirty = dirty || needUpload
|
||||
this.cache = recent
|
||||
this.flagStorage = this.cache.flagStorage
|
||||
this.prefsStorage = this.cache.prefsStorage
|
||||
if (!Array.isArray(this.prefsStorage._journal)) {
|
||||
this.prefsStorage._journal = []
|
||||
}
|
||||
|
||||
// Various migrations
|
||||
console.debug('Migrating from old config')
|
||||
const vuexState = (await storage.getItem('vuex-lz')) ?? {}
|
||||
const config = vuexState.config ?? {}
|
||||
|
||||
const migratedEntries = new Set(config._syncMigration ?? [])
|
||||
console.debug(
|
||||
`Already migrated Values: ${[...migratedEntries].join() || '[none]'}`,
|
||||
)
|
||||
|
||||
Object.entries(oldDefaultConfigSync).forEach(([key, value]) => {
|
||||
const oldValue = config[key]
|
||||
const defaultValue = value
|
||||
const migrated = migratedEntries.has(key)
|
||||
const preferencePath =
|
||||
key === 'theme3hacks' ? 'simple.underlay' : `simple.${key}`
|
||||
|
||||
const present = oldValue !== undefined
|
||||
const different = !isEqual(oldValue, defaultValue)
|
||||
const preferenceHandled =
|
||||
get(this.prefsStorage, preferencePath) !== undefined ||
|
||||
this.prefsStorage._journal.some(
|
||||
(entry) =>
|
||||
entry?.path === preferencePath ||
|
||||
entry?.path?.startsWith?.(`${preferencePath}.`),
|
||||
)
|
||||
|
||||
if (present && different && !preferenceHandled) {
|
||||
console.debug(`Migrating config ${key}: ${oldValue}`)
|
||||
if (key === 'theme3hacks') {
|
||||
if (!migrated) {
|
||||
useLocalConfigStore().set({
|
||||
path: 'fontInterface',
|
||||
value: oldValue.fonts.interface,
|
||||
})
|
||||
useLocalConfigStore().set({
|
||||
path: 'fontInput',
|
||||
value: oldValue.fonts.input,
|
||||
})
|
||||
useLocalConfigStore().set({
|
||||
path: 'fontPosts',
|
||||
value: oldValue.fonts.post,
|
||||
})
|
||||
useLocalConfigStore().set({
|
||||
path: 'fontMonospace',
|
||||
value: oldValue.fonts.monospace,
|
||||
})
|
||||
}
|
||||
this.setPreference({
|
||||
path: 'simple.underlay',
|
||||
value: oldValue.underlay,
|
||||
})
|
||||
} else if (key == 'muteWords') {
|
||||
oldValue.forEach((word, order) => {
|
||||
const uniqueId = uuidv4()
|
||||
|
||||
this.setPreference({
|
||||
path: 'simple.muteFilters.' + uniqueId,
|
||||
value: {
|
||||
type: 'word',
|
||||
value: word,
|
||||
name: word,
|
||||
enabled: true,
|
||||
expires: null,
|
||||
hide: false,
|
||||
order,
|
||||
},
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.setPreference({ path: preferencePath, value: oldValue })
|
||||
}
|
||||
migratedEntries.add(key)
|
||||
needUpload = true
|
||||
}
|
||||
})
|
||||
|
||||
config._syncMigration = [...migratedEntries]
|
||||
vuexState.config = config
|
||||
storage.setItem('vuex-lz', vuexState)
|
||||
|
||||
this.dirty = dirty || needUpload
|
||||
// set local timestamp to smaller one if we don't have any changes
|
||||
if (stale && recent && !this.dirty) {
|
||||
this.cache._timestamp = Math.min(stale._timestamp, recent._timestamp)
|
||||
}
|
||||
this.flagStorage = this.cache.flagStorage
|
||||
this.prefsStorage = this.cache.prefsStorage
|
||||
this.pushSyncConfig()
|
||||
},
|
||||
pushSyncConfig({ force = false } = {}) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { cloneDeep } from 'lodash'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
|
||||
import { useLocalConfigStore } from 'src/stores/local_config.js'
|
||||
import {
|
||||
_getAllFlags,
|
||||
_getRecentData,
|
||||
|
|
@ -16,11 +17,17 @@ import {
|
|||
VERSION,
|
||||
} from 'src/stores/sync_config.js'
|
||||
|
||||
import { storage } from 'src/lib/storage.js'
|
||||
|
||||
describe('The SyncConfig store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('mutations', () => {
|
||||
describe('initSyncConfig', () => {
|
||||
const user = {
|
||||
|
|
@ -163,6 +170,204 @@ describe('The SyncConfig store', () => {
|
|||
expect(store.cache._timestamp).to.be.a('number')
|
||||
expect(store.cache.flagStorage).to.eql(defaultState.flagStorage)
|
||||
})
|
||||
|
||||
it('should recover legacy preferences marked as migrated', async () => {
|
||||
vi.spyOn(storage, 'getItem').mockResolvedValue({
|
||||
config: {
|
||||
_syncMigration: ['modalOnUnfollow'],
|
||||
modalOnUnfollow: true,
|
||||
},
|
||||
})
|
||||
vi.spyOn(storage, 'setItem').mockResolvedValue()
|
||||
const store = useSyncConfigStore()
|
||||
store.pushSyncConfig = vi.fn()
|
||||
|
||||
await store.initSyncConfig({ ...user })
|
||||
|
||||
expect(store.prefsStorage.simple.modalOnUnfollow).to.eql(true)
|
||||
expect(store.cache.prefsStorage.simple.modalOnUnfollow).to.eql(true)
|
||||
expect(store.prefsStorage._journal).to.deep.include({
|
||||
path: 'simple.modalOnUnfollow',
|
||||
operation: 'set',
|
||||
args: [true],
|
||||
timestamp: store.prefsStorage._journal[0].timestamp,
|
||||
})
|
||||
expect(store.dirty).to.eql(true)
|
||||
expect(store.pushSyncConfig).toHaveBeenCalledOnce()
|
||||
expect(defaultState.prefsStorage.simple.modalOnUnfollow).to.eql(
|
||||
undefined,
|
||||
)
|
||||
})
|
||||
|
||||
it('should preserve an explicit synced preference during recovery', async () => {
|
||||
vi.spyOn(storage, 'getItem').mockResolvedValue({
|
||||
config: {
|
||||
_syncMigration: ['modalOnRepeat'],
|
||||
modalOnRepeat: true,
|
||||
},
|
||||
})
|
||||
vi.spyOn(storage, 'setItem').mockResolvedValue()
|
||||
const store = useSyncConfigStore()
|
||||
store.pushSyncConfig = () => {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
await store.initSyncConfig({
|
||||
...user,
|
||||
storage: {
|
||||
_timestamp: 1,
|
||||
_version: VERSION,
|
||||
flagStorage: cloneDeep(defaultState.flagStorage),
|
||||
prefsStorage: {
|
||||
...cloneDeep(defaultState.prefsStorage),
|
||||
simple: {
|
||||
...cloneDeep(defaultState.prefsStorage.simple),
|
||||
modalOnRepeat: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(store.prefsStorage.simple.modalOnRepeat).to.eql(false)
|
||||
})
|
||||
|
||||
it('should preserve a journaled preference removal during recovery', async () => {
|
||||
vi.spyOn(storage, 'getItem').mockResolvedValue({
|
||||
config: {
|
||||
_syncMigration: ['modalOnMute'],
|
||||
modalOnMute: true,
|
||||
},
|
||||
})
|
||||
vi.spyOn(storage, 'setItem').mockResolvedValue()
|
||||
const store = useSyncConfigStore()
|
||||
store.pushSyncConfig = () => {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
await store.initSyncConfig({
|
||||
...user,
|
||||
storage: {
|
||||
_timestamp: 1,
|
||||
_version: VERSION,
|
||||
flagStorage: cloneDeep(defaultState.flagStorage),
|
||||
prefsStorage: {
|
||||
...cloneDeep(defaultState.prefsStorage),
|
||||
_journal: [
|
||||
{
|
||||
path: 'simple.modalOnMute',
|
||||
operation: 'unset',
|
||||
args: [],
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(store.prefsStorage.simple.modalOnMute).to.eql(undefined)
|
||||
})
|
||||
|
||||
it('should preserve a journaled nested preference during recovery', async () => {
|
||||
vi.spyOn(storage, 'getItem').mockResolvedValue({
|
||||
config: {
|
||||
_syncMigration: ['notificationVisibility'],
|
||||
notificationVisibility: {
|
||||
...cloneDeep(
|
||||
defaultState.prefsStorage.simple.notificationVisibility,
|
||||
),
|
||||
likes: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
vi.spyOn(storage, 'setItem').mockResolvedValue()
|
||||
const store = useSyncConfigStore()
|
||||
const setPreference = vi.spyOn(store, 'setPreference')
|
||||
store.pushSyncConfig = () => {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
await store.initSyncConfig({
|
||||
...user,
|
||||
storage: {
|
||||
_timestamp: 1,
|
||||
_version: VERSION,
|
||||
flagStorage: cloneDeep(defaultState.flagStorage),
|
||||
prefsStorage: {
|
||||
...cloneDeep(defaultState.prefsStorage),
|
||||
_journal: [
|
||||
{
|
||||
path: 'simple.notificationVisibility.likes',
|
||||
operation: 'unset',
|
||||
args: [],
|
||||
timestamp: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(setPreference).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should recover with a malformed synced journal', async () => {
|
||||
vi.spyOn(storage, 'getItem').mockResolvedValue({
|
||||
config: {
|
||||
_syncMigration: ['modalOnUnfollow'],
|
||||
modalOnUnfollow: true,
|
||||
},
|
||||
})
|
||||
vi.spyOn(storage, 'setItem').mockResolvedValue()
|
||||
const store = useSyncConfigStore()
|
||||
store.pushSyncConfig = () => {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
await store.initSyncConfig({
|
||||
...user,
|
||||
storage: {
|
||||
_timestamp: 1,
|
||||
_version: VERSION,
|
||||
flagStorage: cloneDeep(defaultState.flagStorage),
|
||||
prefsStorage: {
|
||||
...cloneDeep(defaultState.prefsStorage),
|
||||
_journal: [{ path: 1, operation: 'set' }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(store.prefsStorage.simple.modalOnUnfollow).to.eql(true)
|
||||
})
|
||||
|
||||
it('should not replay local theme migration during recovery', async () => {
|
||||
vi.spyOn(storage, 'getItem').mockResolvedValue({
|
||||
config: {
|
||||
_syncMigration: ['theme3hacks'],
|
||||
theme3hacks: {
|
||||
underlay: 'grid',
|
||||
fonts: {
|
||||
interface: 'Legacy interface',
|
||||
input: 'Legacy input',
|
||||
post: 'Legacy posts',
|
||||
monospace: 'Legacy monospace',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
vi.spyOn(storage, 'setItem').mockResolvedValue()
|
||||
const localStore = useLocalConfigStore()
|
||||
localStore.set({ path: 'fontInterface', value: 'Current interface' })
|
||||
const store = useSyncConfigStore()
|
||||
store.pushSyncConfig = () => {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
await store.initSyncConfig({ ...user })
|
||||
|
||||
expect(store.prefsStorage.simple.underlay).to.eql('grid')
|
||||
expect(localStore.prefsStorage.fontInterface).to.eql(
|
||||
'Current interface',
|
||||
)
|
||||
})
|
||||
})
|
||||
describe('setPreference', () => {
|
||||
it('should set preference and update journal log accordingly', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue