Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ import styled from '@emotion/styled';
import { ButtonGroup, Button, Box } from '@mui/material';
import { setSelectedTabIndex } from 'state/library';
import { LIBRARY_TAB_INDEX, MONOMER_TYPES } from 'src/constants';
import {
getPersistedSequenceType,
persistSequenceType,
} from 'helpers/sequenceTypeStorage';

const SequenceTypeButton = styled(Button)(({ theme, variant }) => ({
color:
Expand Down Expand Up @@ -87,8 +91,7 @@ export const SequenceTypeGroupButton = () => {
};

useEffect(() => {
editor?.events.selectMode.add(onToggleSequenceMode);
editor?.events.changeSequenceTypeEnterMode.add((mode: SequenceType) => {
const onChangeSequenceType = (mode: SequenceType) => {
dispatch(
setSelectedTabIndex(
mode === MONOMER_TYPES.PEPTIDE
Expand All @@ -97,11 +100,17 @@ export const SequenceTypeGroupButton = () => {
),
);
setActiveSequenceType(mode);
});
editor?.events.changeSequenceTypeEnterMode.dispatch(SequenceType.RNA);
persistSequenceType(mode);
Comment thread
mariam-khutuashvili marked this conversation as resolved.
};
editor?.events.selectMode.add(onToggleSequenceMode);
editor?.events.changeSequenceTypeEnterMode.add(onChangeSequenceType);
editor?.events.changeSequenceTypeEnterMode.dispatch(
getPersistedSequenceType(),
);

return () => {
editor?.events.selectMode.remove(onToggleSequenceMode);
editor?.events.changeSequenceTypeEnterMode.remove(onChangeSequenceType);
};
}, [editor]);

Expand Down
1 change: 1 addition & 0 deletions packages/ketcher-macromolecules/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const FAVORITE_ITEMS_UNIQUE_KEYS = 'favoriteItemsUniqueKeys';
export const CUSTOM_PRESETS = 'ketcher_custom_presets';
export const PRESET_PHOSPHATE_FILTER_STORAGE_KEY =
'ketcher_preset_phosphate_filter';
export const SEQUENCE_TYPE_STORAGE_KEY = 'ketcher_polymer_sequence_type';

// It's set as Z, so it will always be put in the end when alphabetically sorting groups by code
export const NoNaturalAnalogueGroupCode = 'Z';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { KetcherLogger, SequenceType } from 'ketcher-core';
import { SEQUENCE_TYPE_STORAGE_KEY } from 'src/constants';
import {
getPersistedSequenceType,
persistSequenceType,
} from './sequenceTypeStorage';

describe('sequenceTypeStorage', () => {
afterEach(() => {
window.localStorage.clear();
});

it('defaults to RNA when nothing is persisted', () => {
expect(getPersistedSequenceType()).toBe(SequenceType.RNA);
});

it('persists and restores the selected polymer type', () => {
persistSequenceType(SequenceType.PEPTIDE);
expect(getPersistedSequenceType()).toBe(SequenceType.PEPTIDE);

persistSequenceType(SequenceType.DNA);
expect(getPersistedSequenceType()).toBe(SequenceType.DNA);
});

it('falls back to RNA when the stored value is invalid', () => {
window.localStorage.setItem(
SEQUENCE_TYPE_STORAGE_KEY,
JSON.stringify('NOT_A_TYPE'),
);
expect(getPersistedSequenceType()).toBe(SequenceType.RNA);
});

it('falls back to RNA when the stored value is malformed JSON', () => {
const loggerSpy = jest
.spyOn(KetcherLogger, 'error')
.mockImplementation(() => undefined);
// Raw, non-JSON string (bypassing JSON.stringify) simulates a corrupted or
// manually-edited value that makes JSON.parse throw inside getItem.
window.localStorage.setItem(SEQUENCE_TYPE_STORAGE_KEY, 'not-json{');

expect(getPersistedSequenceType()).toBe(SequenceType.RNA);
expect(loggerSpy).toHaveBeenCalled();

loggerSpy.mockRestore();
});
});
31 changes: 31 additions & 0 deletions packages/ketcher-macromolecules/src/helpers/sequenceTypeStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { KetcherLogger, SequenceType } from 'ketcher-core';
import { localStorageWrapper } from './localStorage';
import { SEQUENCE_TYPE_STORAGE_KEY } from '../constants';

const isSequenceType = (value: unknown): value is SequenceType =>
value === SequenceType.RNA ||
value === SequenceType.DNA ||
value === SequenceType.PEPTIDE;

// Read the last polymer type chosen on the sequence-type switcher from
// localStorage, falling back to RNA when nothing valid is stored (#9780).
// `localStorageWrapper.getItem` runs `JSON.parse`, which throws on a malformed
// value (manual edit / partial write), so the read is guarded to avoid
// crashing the switcher's mount effect.
export const getPersistedSequenceType = (): SequenceType => {
let stored: unknown;
try {
stored = localStorageWrapper.getItem(SEQUENCE_TYPE_STORAGE_KEY);
} catch (error) {
KetcherLogger.error(
'sequenceTypeStorage.ts::getPersistedSequenceType',
error,
);
return SequenceType.RNA;
}
return isSequenceType(stored) ? stored : SequenceType.RNA;
};
Comment thread
mariam-khutuashvili marked this conversation as resolved.

export const persistSequenceType = (sequenceType: SequenceType): void => {
localStorageWrapper.setItem(SEQUENCE_TYPE_STORAGE_KEY, sequenceType);
};
Loading