fix(reader): make annotation toolbar customization apply to all books (#4760)
The customized annotation toolbar only took effect in the book where it was changed, instead of applying to every book. Root cause: serializeConfig decided which per-book view settings to persist as overrides using a reference check (globalViewSettings[key] !== value). It deep-clones the config first, so array-valued settings like annotationToolbarItems are always a fresh reference and were stored as a per-book override on every save (each progress autosave). On reopen the per-book override shadowed the global value, so a global toolbar change never reached already-opened books. Compare view-setting values by content, not reference, so array/object settings equal to the global value are no longer persisted per-book. This also fixes the same latent issue for paragraphMode, proofreadRules, ttsHighlightOptions and noteExportConfig. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<ViewSettings>,
|
||||
};
|
||||
|
||||
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<ViewSettings>,
|
||||
};
|
||||
|
||||
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({
|
||||
|
||||
@@ -14,6 +14,15 @@ export const serializeRawConfig = (config: Partial<BookConfig>): 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<BookSearchConfig>;
|
||||
config.viewSettings = Object.entries(viewSettings).reduce(
|
||||
(acc: Partial<Record<keyof ViewSettings, unknown>>, [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;
|
||||
|
||||
Reference in New Issue
Block a user