diff --git a/apps/readest-app/src/__tests__/utils/serializer.test.ts b/apps/readest-app/src/__tests__/utils/serializer.test.ts index ca958664..4117ddb9 100644 --- a/apps/readest-app/src/__tests__/utils/serializer.test.ts +++ b/apps/readest-app/src/__tests__/utils/serializer.test.ts @@ -108,6 +108,43 @@ describe('BookConfig serialization', () => { expect(tombstone.deletedAt).toBeTruthy(); }); + it('does not persist an array view setting that equals the global value', () => { + // Array/object view settings must be compared by value, not reference — + // otherwise annotationToolbarItems (an array) is stored as a per-book override on + // every save, shadowing later global changes (the customize-toolbar bug). + const global = { + ...globalViewSettings, + annotationToolbarItems: ['highlight', 'annotate', 'copy'], + } as unknown as ViewSettings; + const config: BookConfig = { + updatedAt: 1, + // Same content as global but a distinct array reference, as produced by the + // load -> merge -> serialize round-trip. + viewSettings: { + annotationToolbarItems: ['highlight', 'annotate', 'copy'], + } as Partial, + }; + + const parsed = JSON.parse(serializeConfig(config, global, defaultSearchConfig)); + + expect(parsed.viewSettings.annotationToolbarItems).toBeUndefined(); + }); + + it('persists an array view setting that differs from the global value', () => { + const global = { + ...globalViewSettings, + annotationToolbarItems: ['highlight', 'annotate', 'copy'], + } as unknown as ViewSettings; + const config: BookConfig = { + updatedAt: 1, + viewSettings: { annotationToolbarItems: ['copy'] } as Partial, + }; + + const parsed = JSON.parse(serializeConfig(config, global, defaultSearchConfig)); + + expect(parsed.viewSettings.annotationToolbarItems).toEqual(['copy']); + }); + it('does not migrate annotations when schemaVersion is already 2', () => { const config = deserializeConfig( JSON.stringify({ diff --git a/apps/readest-app/src/utils/serializer.ts b/apps/readest-app/src/utils/serializer.ts index 615f0a24..e2531fc5 100644 --- a/apps/readest-app/src/utils/serializer.ts +++ b/apps/readest-app/src/utils/serializer.ts @@ -14,6 +14,15 @@ export const serializeRawConfig = (config: Partial): string => { return JSON.stringify(stampBookConfigSchema(config)); }; +// Compare a per-book view-setting value with the global one by value, not +// reference. serializeConfig deep-clones the config first, so array/object +// settings (e.g. annotationToolbarItems) are always fresh references; a reference +// check would persist them as per-book overrides even when identical to global, +// which then shadows later global changes on reopen. Primitives short-circuit; +// the JSON compare covers arrays and plain config objects (all JSON-serializable). +const isSameViewSettingValue = (a: unknown, b: unknown): boolean => + a === b || JSON.stringify(a) === JSON.stringify(b); + export const serializeConfig = ( config: BookConfig, globalViewSettings: ViewSettings, @@ -35,7 +44,7 @@ export const serializeConfig = ( const searchConfig = (config.searchConfig ?? {}) as Partial; config.viewSettings = Object.entries(viewSettings).reduce( (acc: Partial>, [key, value]) => { - if (globalViewSettings[key as keyof ViewSettings] !== value) { + if (!isSameViewSettingValue(globalViewSettings[key as keyof ViewSettings], value)) { acc[key as keyof ViewSettings] = value; } return acc;