diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts index c098e0f15f..dcb78a94a5 100644 --- a/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts +++ b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts @@ -291,6 +291,26 @@ test.describe('RNA Library', () => { await takePresetsScreenshot(page); }); + test('Add Custom preset to Presets section and display after page reload', async ({ + page, + }) => { + await expandCollapseRnaBuilder(page); + await selectMonomer(page, Sugars.TwelveddR); + await selectMonomer(page, Bases.Adenine); + await selectMonomer(page, Phosphates.Test6Ph); + await page.getByTestId('add-to-presets-btn').click(); + await page.getByTestId('12ddR(A)Test-6-Ph_A_12ddR_Test-6-Ph').click(); + await expandCollapseRnaBuilder(page); + await takePresetsScreenshot(page); + await page.reload(); + await waitForPageInit(page); + await turnOnMacromoleculesEditor(page); + await page.getByTestId('RNA-TAB').click(); + await page.getByTestId('12ddR(A)Test-6-Ph_A_12ddR_Test-6-Ph').click(); + await expandCollapseRnaBuilder(page); + await takePresetsScreenshot(page); + }); + test('Add Custom preset to Canvas', async ({ page }) => { /* Test case: #2507 - Add RNA monomers to canvas diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-1-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-1-chromium-linux.png new file mode 100644 index 0000000000..ede12c3f8e Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-1-chromium-linux.png differ diff --git a/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-2-chromium-linux.png b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-2-chromium-linux.png new file mode 100644 index 0000000000..ede12c3f8e Binary files /dev/null and b/ketcher-autotests/tests/Macromolecule-editor/RNA-Builder/rna-library.spec.ts-snapshots/RNA-Library-Add-Custom-preset-to-Presets-section-and-display-after-page-reload-2-chromium-linux.png differ diff --git a/packages/ketcher-core/src/application/editor/tools/Tool.ts b/packages/ketcher-core/src/application/editor/tools/Tool.ts index 6ec4279e5a..2825284994 100644 --- a/packages/ketcher-core/src/application/editor/tools/Tool.ts +++ b/packages/ketcher-core/src/application/editor/tools/Tool.ts @@ -70,10 +70,20 @@ interface ToolEventHandler { export interface IRnaPreset { name?: string; + nameInList?: string; base?: MonomerItemType; sugar?: MonomerItemType; phosphate?: MonomerItemType; - presetInList?: IRnaPreset; + default?: boolean; + favorite?: boolean; + editedName?: boolean; +} + +export interface IRnaLabeledPreset + extends Omit { + base?: string; + sugar?: string; + phosphate?: string; } export type LabeledNodesWithPositionInSequence = { diff --git a/packages/ketcher-core/src/domain/entities/Command.ts b/packages/ketcher-core/src/domain/entities/Command.ts index ee944694ed..8deaa875d0 100644 --- a/packages/ketcher-core/src/domain/entities/Command.ts +++ b/packages/ketcher-core/src/domain/entities/Command.ts @@ -32,4 +32,8 @@ export class Command { ); renderersManagers.runPostRenderMethods(); } + + public clear() { + this.operations = []; + } } diff --git a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts index d643efa7f7..a7160fc961 100644 --- a/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts +++ b/packages/ketcher-core/src/domain/entities/DrawingEntitiesManager.ts @@ -1,3 +1,5 @@ +import pick from 'lodash/pick'; +import omit from 'lodash/omit'; import { AttachmentPointName, MonomerItemType } from 'domain/types'; import { Vec2 } from 'domain/entities/vec2'; import { Command } from 'domain/entities/Command'; @@ -53,7 +55,9 @@ import { ChainsCollection } from 'domain/entities/monomer-chains/ChainsCollectio import { SequenceRenderer } from 'application/render/renderers/sequence/SequenceRenderer'; import { Nucleoside } from './Nucleoside'; import { Nucleotide } from './Nucleotide'; -import { SequenceMode } from 'application/editor'; +import { IRnaPreset, SequenceMode } from 'application/editor'; +import { IRnaLabeledPreset } from 'application/editor/tools'; +import { getRnaPartLibraryItem } from 'domain/helpers/rna'; const HORIZONTAL_DISTANCE_FROM_MONOMER = 25; const VERTICAL_DISTANCE_FROM_MONOMER = 30; @@ -887,7 +891,9 @@ export class DrawingEntitiesManager { if (rnaBase && rnaBasePosition) { monomersToAdd.push([rnaBase, rnaBasePosition]); } - monomersToAdd.push([sugar, sugarPosition]); + if (sugar && sugarPosition) { + monomersToAdd.push([sugar, sugarPosition]); + } if (phosphate && phosphatePosition) { monomersToAdd.push([phosphate, phosphatePosition]); } @@ -909,6 +915,10 @@ export class DrawingEntitiesManager { const attPointEnd = monomer.getValidSourcePoint(previousMonomer); assert(attPointStart); + if (!attPointEnd) { + command.clear(); + return; + } assert(attPointEnd); const operation = new PolymerBondFinishCreationOperation( @@ -929,6 +939,53 @@ export class DrawingEntitiesManager { return { command, monomers }; } + public createRnaPresetFromLabeledRnaPreset(preset: IRnaLabeledPreset) { + const editor = CoreEditor.provideEditorInstance(); + const modifiedPreset: Partial = omit(preset, [ + 'sugar', + 'base', + 'phosphate', + ]); + const position = Coordinates.canvasToModel(new Vec2(0, 0)); + let rnaBaseLibraryItem; + let phosphateLibraryItem; + let sugarLibraryItem; + + if (preset.base) { + rnaBaseLibraryItem = getRnaPartLibraryItem(editor, preset.base); + rnaBaseLibraryItem && assert(rnaBaseLibraryItem); + } + if (preset.phosphate) { + phosphateLibraryItem = getRnaPartLibraryItem(editor, preset.phosphate); + phosphateLibraryItem && assert(phosphateLibraryItem); + } + if (preset.sugar) { + sugarLibraryItem = getRnaPartLibraryItem(editor, preset.sugar); + sugarLibraryItem && assert(sugarLibraryItem); + } + + const { monomers } = editor.drawingEntitiesManager.addRnaPreset({ + sugar: sugarLibraryItem, + sugarPosition: position, + rnaBase: rnaBaseLibraryItem, + rnaBasePosition: position, + phosphate: phosphateLibraryItem, + phosphatePosition: position, + }); + for (const monomer of monomers) { + const props = pick(monomer.monomerItem, ['label', 'props', 'struct']); + if (monomer instanceof RNABase) { + modifiedPreset.base = { ...props }; + } else if (monomer instanceof Sugar) { + modifiedPreset.sugar = { ...props }; + } else if (monomer instanceof Phosphate) { + modifiedPreset.phosphate = { ...props }; + } + } + + return modifiedPreset; + } + private findChainByMonomer( monomer: BaseMonomer, monomerChain: BaseMonomer[] = [], diff --git a/packages/ketcher-macromolecules/src/Editor.tsx b/packages/ketcher-macromolecules/src/Editor.tsx index 896d69c0a6..e0f508caca 100644 --- a/packages/ketcher-macromolecules/src/Editor.tsx +++ b/packages/ketcher-macromolecules/src/Editor.tsx @@ -47,10 +47,6 @@ import { selectTool, showPreview, } from 'state/common'; -import { - loadMonomerLibrary, - setFavoriteMonomersFromLocalStorage, -} from 'state/library'; import { useAppDispatch, useAppSelector, @@ -87,15 +83,9 @@ import { calculatePreviewPosition } from 'helpers'; import { ErrorModal } from 'components/modal/Error'; import { EditorWrapper, TogglerComponentWrapper } from './styledComponents'; import { useLoading } from './hooks/useLoading'; +import useSetRnaPresets from './hooks/useSetRnaPresets'; import { Loader } from 'components/Loader'; import { FullscreenButton } from 'components/FullscreenButton'; -import { getDefaultPresets } from 'src/helpers/getDefaultPreset'; -import { - setDefaultPresets, - setFavoritePresetsFromLocalStorage, - clearFavorites, -} from 'state/rna-builder'; -import { IRnaPreset } from 'components/monomerLibrary/RnaBuilder/types'; import { LayoutModeButton } from 'components/LayoutModeButton'; import { useContextMenu } from 'react-contexify'; import { CONTEXT_MENU_ID } from 'components/contextMenu/types'; @@ -174,28 +164,7 @@ function Editor({ theme, togglerComponent }: EditorProps) { }; }, [dispatch]); - useEffect(() => { - if (editor) { - const monomersLibrary = editor.monomersLibrary; - const defaultPresetsTemplates = editor.defaultRnaPresetsLibraryItems; - - dispatch(loadMonomerLibrary(monomersLibrary)); - dispatch(setFavoriteMonomersFromLocalStorage(null)); - - const defaultPresets: IRnaPreset[] = getDefaultPresets( - monomersLibrary, - defaultPresetsTemplates, - ); - - dispatch(setDefaultPresets(defaultPresets)); - dispatch(setFavoritePresetsFromLocalStorage()); - } - - return () => { - dispatch(loadMonomerLibrary([])); - dispatch(clearFavorites()); - }; - }, [editor]); + useSetRnaPresets(); const dispatchShowPreview = useCallback( (payload) => dispatch(showPreview(payload)), diff --git a/packages/ketcher-macromolecules/src/components/contextMenu/RNAContextMenu.test.tsx b/packages/ketcher-macromolecules/src/components/contextMenu/RNAContextMenu.test.tsx index 422869d659..ec2b89d0e4 100644 --- a/packages/ketcher-macromolecules/src/components/contextMenu/RNAContextMenu.test.tsx +++ b/packages/ketcher-macromolecules/src/components/contextMenu/RNAContextMenu.test.tsx @@ -85,7 +85,8 @@ describe('RNA ContextMenu', () => { monomers: monomerData, }, rnaBuilder: { - presets: mockedPresets, + presetsDefault: mockedPresets, + presetsCustom: [], }, }, ), @@ -112,7 +113,8 @@ describe('RNA ContextMenu', () => { monomers: monomerData, }, rnaBuilder: { - presets: mockedPresets, + presetsDefault: mockedPresets, + presetsCustom: [], }, }, ), @@ -141,7 +143,8 @@ describe('RNA ContextMenu', () => { monomers: monomerData, }, rnaBuilder: { - presets: mockedPresets, + presetsDefault: mockedPresets, + presetsCustom: [], }, }, ), diff --git a/packages/ketcher-macromolecules/src/components/modal/Delete/Delete.test.tsx b/packages/ketcher-macromolecules/src/components/modal/Delete/Delete.test.tsx index 569041c99a..e5db6fa7e9 100644 --- a/packages/ketcher-macromolecules/src/components/modal/Delete/Delete.test.tsx +++ b/packages/ketcher-macromolecules/src/components/modal/Delete/Delete.test.tsx @@ -37,13 +37,28 @@ describe('Delete component', () => { }, name: 'A', }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const presetCustom: any = { + base: { + label: '25A', + }, + phosphate: { + label: 'P', + }, + sugar: { + label: 'R', + }, + name: 'MyRna', + nameInList: 'MyRna', + }; it('should render correctly', () => { expect( render( withThemeAndStoreProvider(, { rnaBuilder: { - activePresetForContextMenu: { presetInList: preset, name: 'name' }, - presets: [preset], + activePresetForContextMenu: { nameInList: 'name', name: 'name' }, + presetsDefault: [preset], + presetsCustom: [], }, }), ), @@ -53,8 +68,9 @@ describe('Delete component', () => { render( withThemeAndStoreProvider(, { rnaBuilder: { - activePresetForContextMenu: { presetInList: preset, name: 'name' }, - presets: [preset], + activePresetForContextMenu: { nameInList: 'name', name: 'name' }, + presetsDefault: [preset], + presetsCustom: [], }, }), ); @@ -67,8 +83,9 @@ describe('Delete component', () => { render( withThemeAndStoreProvider(, { rnaBuilder: { - activePresetForContextMenu: { presetInList: preset, name: 'name' }, - presets: [preset], + activePresetForContextMenu: { nameInList: 'name', name: 'name' }, + presetsDefault: [preset], + presetsCustom: [presetCustom], }, editor: { editor: { diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/MonomerLibrary.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/MonomerLibrary.tsx index 6878ae00a9..a29487afda 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/MonomerLibrary.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/MonomerLibrary.tsx @@ -26,7 +26,7 @@ import { setSearchFilter } from 'state/library'; import { Icon } from 'ketcher-react'; import { IRnaPreset } from './RnaBuilder/types'; import { - selectPresets, + selectAllPresets, setActivePreset, setIsEditMode, setUniqueNameError, @@ -50,7 +50,7 @@ const MonomerLibrary = React.memo(() => { const isDisabledTabsPanels = isSequenceMode && !isSequenceEditInRNABuilderMode; - useAppSelector(selectPresets, (presets) => { + useAppSelector(selectAllPresets, (presets) => { presetsRef.current = presets; return true; }); @@ -68,10 +68,11 @@ const MonomerLibrary = React.memo(() => { return; } + const nameToSet = presetWithSameName ? `${name}_Copy` : name; const duplicatedPreset = { ...preset, - presetInList: undefined, - name: presetWithSameName ? `${name}_Copy` : name, + name: nameToSet, + nameInList: nameToSet, default: false, favorite: false, }; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaBuilder.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaBuilder.tsx index 163ca38402..807ed57442 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaBuilder.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaBuilder.tsx @@ -42,8 +42,10 @@ export const RnaBuilder = ({ libraryName, duplicatePreset, editPreset }) => { onClose={closeErrorModal} > - Preset with name "{uniqueNameError}" already exists. Please choose - another name. +
+ Preset with name "{uniqueNameError}" already exists. Please choose + another name. +
Close diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditor.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditor.tsx index 53e4a9a44e..ab2f73f33c 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditor.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditor.tsx @@ -69,7 +69,7 @@ export const RnaEditor = ({ duplicatePreset }) => { const expandEditor = () => { setExpanded(!expanded); - if (!activePreset?.presetInList) { + if (!activePreset?.nameInList) { dispatch(setIsEditMode(true)); } }; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.test.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.test.tsx index 5d4d059e65..1de79f4bda 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.test.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.test.tsx @@ -20,22 +20,11 @@ describe('Test Rna Editor Expanded component', () => { { rnaBuilder: { activePreset: { - name: 'MyRna', - sugar: { - props: { - MonomerName: '', - }, - }, - phosphate: { - props: { - MonomerName: '', - }, - }, - base: { - props: { - MonomerName: '', - }, - }, + name: '', + nameInList: '', + sugar: undefined, + phosphate: undefined, + base: undefined, }, }, }, @@ -79,6 +68,8 @@ describe('Test Rna Editor Expanded component', () => { hasR1Connection: true, }, ], + presetsDefault: [], + presetsCustom: [], }, }, ), @@ -117,8 +108,10 @@ describe('Test Rna Editor Expanded component', () => { MonomerName: '', }, }, - presetInList: {}, + nameInList: 'MyRna', }, + presetsDefault: [], + presetsCustom: [], }, }, ), diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.tsx index a742801b39..949f789008 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/RnaEditor/RnaEditorExpanded/RnaEditorExpanded.tsx @@ -40,7 +40,7 @@ import { selectActivePresetMonomerGroup, selectActiveRnaBuilderItem, selectIsPresetReadyToSave, - selectPresets, + selectAllPresets, setActivePreset, setActiveRnaBuilderItem, setIsEditMode, @@ -49,6 +49,7 @@ import { setUniqueNameError, setSequenceSelection, setSequenceSelectionName, + selectIsActivePresetNewAndEmpty, } from 'state/rna-builder'; import { useAppSelector, useSequenceEditInRNABuilderMode } from 'hooks'; import { @@ -95,9 +96,10 @@ export const RnaEditorExpanded = ({ const dispatch = useDispatch(); const activePreset = useAppSelector(selectActivePreset); + const isActivePresetEmpty = useAppSelector(selectIsActivePresetNewAndEmpty); const activeMonomerGroup = useAppSelector(selectActiveRnaBuilderItem); const editor = useAppSelector(selectEditor); - const presets = useAppSelector(selectPresets); + const presets = useAppSelector(selectAllPresets); const activePresetMonomerGroup = useAppSelector( selectActivePresetMonomerGroup, ); @@ -246,13 +248,13 @@ export const RnaEditorExpanded = ({ ); if ( presetWithSameName && - activePreset.presetInList !== presetWithSameName + activePreset.nameInList !== presetWithSameName.name ) { dispatch(setUniqueNameError(newPreset.name)); return; } - dispatch(setActivePreset(newPreset)); dispatch(savePreset(newPreset)); + dispatch(setActivePreset(newPreset)); editor.events.selectPreset.dispatch(newPreset); setTimeout(() => { scrollToSelectedPreset(newPreset.name); @@ -266,6 +268,8 @@ export const RnaEditorExpanded = ({ resetAfterSequenceUpdate(); } else { setNewPreset(activePreset); + dispatch(setIsEditMode(false)); + dispatch(setActivePresetMonomerGroup(null)); } }; @@ -294,7 +298,7 @@ export const RnaEditorExpanded = ({ let mainButton; - if (!activePreset.presetInList && !isSequenceEditInRNABuilderMode) { + if (isActivePresetEmpty && !isSequenceEditInRNABuilderMode) { mainButton = ( diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/types.ts b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/types.ts index 3431c304fd..80e51c2673 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/types.ts +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaBuilder/types.ts @@ -21,10 +21,10 @@ export interface IExpandIconProps { export interface IRnaPreset { name?: string; + nameInList?: string; base?: MonomerItemType; sugar?: MonomerItemType; phosphate?: MonomerItemType; - presetInList?: IRnaPreset; default?: boolean; favorite?: boolean; editedName?: boolean; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx index 1355646d1a..7140209b01 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetGroup/RnaPresetGroup.tsx @@ -45,7 +45,7 @@ export const RnaPresetGroup = ({ presets, duplicatePreset, editPreset }) => { const selectPreset = (preset: IRnaPreset) => () => { dispatch(setActivePreset(preset)); editor.events.selectPreset.dispatch(preset); - if (preset === activePreset?.presetInList) return; + if (preset.name === activePreset.name) return; dispatch(setIsEditMode(false)); }; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetItem/RnaPresetItem.test.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetItem/RnaPresetItem.test.tsx index 48dd80bee4..ea4993fdaa 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetItem/RnaPresetItem.test.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/RnaPresetItem/RnaPresetItem.test.tsx @@ -9,7 +9,7 @@ describe('Test Rna Preset Item component', () => { base: undefined, name: 'MyRna', phosphate: undefined, - presetInList: undefined, + nameInList: undefined, sugar: undefined, }; diff --git a/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryList/MonomerList.test.tsx b/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryList/MonomerList.test.tsx index 41347b96be..5cdd205ebb 100644 --- a/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryList/MonomerList.test.tsx +++ b/packages/ketcher-macromolecules/src/components/monomerLibrary/monomerLibraryList/MonomerList.test.tsx @@ -56,7 +56,8 @@ describe('Monomer List', () => { ], }, rnaBuilder: { - presets: [preset], + presetsDefault: [preset], + presetsCustom: [], }, }; diff --git a/packages/ketcher-macromolecules/src/constants.ts b/packages/ketcher-macromolecules/src/constants.ts index 193ac74ca1..989461c35d 100644 --- a/packages/ketcher-macromolecules/src/constants.ts +++ b/packages/ketcher-macromolecules/src/constants.ts @@ -62,3 +62,4 @@ export const MonomerCodeToGroup: Record = { } as const; export const FAVORITE_ITEMS_UNIQUE_KEYS = 'favoriteItemsUniqueKeys'; +export const CUSTOM_PRESETS = 'ketcher_custom_presets'; diff --git a/packages/ketcher-macromolecules/src/helpers/localStorage.ts b/packages/ketcher-macromolecules/src/helpers/localStorage.ts index 42a3d7bdd9..e9bf787750 100644 --- a/packages/ketcher-macromolecules/src/helpers/localStorage.ts +++ b/packages/ketcher-macromolecules/src/helpers/localStorage.ts @@ -18,6 +18,10 @@ class LocalStorageWrapper { setItem(key: string, item: unknown) { this.localStorage.setItem(key, JSON.stringify(item)); } + + removeItem(key: string) { + this.localStorage.removeItem(key); + } } export const localStorageWrapper = new LocalStorageWrapper(); diff --git a/packages/ketcher-macromolecules/src/helpers/manipulateCachedRnaPresets.ts b/packages/ketcher-macromolecules/src/helpers/manipulateCachedRnaPresets.ts new file mode 100644 index 0000000000..426e9282d3 --- /dev/null +++ b/packages/ketcher-macromolecules/src/helpers/manipulateCachedRnaPresets.ts @@ -0,0 +1,81 @@ +import { IRnaLabeledPreset, IRnaPreset } from 'ketcher-core'; +import { localStorageWrapper } from './localStorage'; +import { CUSTOM_PRESETS } from '../constants'; +import omit from 'lodash/omit'; + +// Get custom presets from LocalStorage +export const getCachedCustomRnaPresets = (): IRnaLabeledPreset[] | undefined => + localStorageWrapper.getItem(CUSTOM_PRESETS); + +const getPresetIndexInList = (name?: string): number => { + const presets = getCachedCustomRnaPresets(); + return presets?.findIndex((cachedPreset) => cachedPreset.name === name) ?? -1; +}; + +// Save or update custom preset in LocalStorage +export const setCachedCustomRnaPreset = ( + preset: IRnaPreset | IRnaLabeledPreset, +) => { + const presetToSet = { ...preset }; + const cachedPresets = getCachedCustomRnaPresets() || []; + const isLabeledPreset = + typeof presetToSet.sugar === 'string' || + typeof presetToSet.base === 'string' || + typeof presetToSet.phosphate === 'string'; + const fieldsToLabel = ['sugar', 'base', 'phosphate']; + const newLabeledPreset = isLabeledPreset + ? (presetToSet as IRnaLabeledPreset) + : (omit(presetToSet, fieldsToLabel) as Partial); + + if (!isLabeledPreset) { + for (const monomerName of fieldsToLabel) { + newLabeledPreset[monomerName] = presetToSet[monomerName]?.label; + } + } + + const presetIndexInCachedList = getPresetIndexInList( + newLabeledPreset.nameInList, + ); + + newLabeledPreset.nameInList = newLabeledPreset.name; + + if (presetIndexInCachedList > -1) { + cachedPresets.splice(presetIndexInCachedList, 1, newLabeledPreset); + localStorageWrapper.setItem(CUSTOM_PRESETS, cachedPresets); + } else { + localStorageWrapper.setItem(CUSTOM_PRESETS, [ + ...cachedPresets, + newLabeledPreset, + ]); + } +}; + +// Delete custom preset from LocalStorage +export const deleteCachedCustomRnaPreset = (presetName?: string) => { + if (!presetName) return; + + const cachedPresets = getCachedCustomRnaPresets(); + const presetIndexInCachedList = getPresetIndexInList(presetName); + + if (cachedPresets) { + cachedPresets.splice(presetIndexInCachedList, 1); + + if (cachedPresets.length) + localStorageWrapper.setItem(CUSTOM_PRESETS, cachedPresets); + else localStorageWrapper.removeItem(CUSTOM_PRESETS); + } +}; + +// Toggle 'favorite' field in custom preset from LocalStorage +export const toggleCachedCustomRnaPresetFavorites = (presetName?: string) => { + if (!presetName) return; + + const cachedPresets = getCachedCustomRnaPresets(); + const presetIndexInCachedList = getPresetIndexInList(presetName); + + if (cachedPresets && presetIndexInCachedList > -1) { + cachedPresets[presetIndexInCachedList].favorite = + !cachedPresets[presetIndexInCachedList].favorite; + localStorageWrapper.setItem(CUSTOM_PRESETS, cachedPresets); + } +}; diff --git a/packages/ketcher-macromolecules/src/hooks/useSetRnaPresets.ts b/packages/ketcher-macromolecules/src/hooks/useSetRnaPresets.ts new file mode 100644 index 0000000000..a320696e56 --- /dev/null +++ b/packages/ketcher-macromolecules/src/hooks/useSetRnaPresets.ts @@ -0,0 +1,83 @@ +import { useEffect } from 'react'; +import { useAppDispatch, useAppSelector } from './stateHooks'; +import { selectEditor } from 'state/common'; +import { IRnaPreset } from 'components/monomerLibrary/RnaBuilder/types'; +import { getDefaultPresets } from 'helpers'; +import { + getCachedCustomRnaPresets, + setCachedCustomRnaPreset, +} from 'helpers/manipulateCachedRnaPresets'; +import { + clearFavorites, + setCustomPresets, + setDefaultPresets, + setFavoritePresetsFromLocalStorage, +} from 'state/rna-builder'; +import { + loadMonomerLibrary, + setFavoriteMonomersFromLocalStorage, +} from 'state/library'; + +function useSetRnaPresets() { + const dispatch = useAppDispatch(); + const editor = useAppSelector(selectEditor); + + useEffect(() => { + if (!editor) return; + + const monomersLibrary = editor.monomersLibrary; + const defaultPresetsTemplates = editor.defaultRnaPresetsLibraryItems; + const defaultPresets: IRnaPreset[] = getDefaultPresets( + monomersLibrary, + defaultPresetsTemplates, + ); + let customLabeledPresets = getCachedCustomRnaPresets(); + const customPresets: IRnaPreset[] = []; + const presetsDefaultNames = defaultPresets.map((preset) => preset.name); + + if (customLabeledPresets) { + // If preset with the same name already exists: + // add '_Copy' (again and again) to custom preset, and update in LocalStorage + for (const customLabeledPreset of customLabeledPresets) { + let i = 0; + let presetUniqName = customLabeledPreset.name; + + while (presetsDefaultNames.includes(presetUniqName)) { + i++; + presetUniqName = `${customLabeledPreset.name}${'_Copy'.repeat(i)}`; + } + + if (presetUniqName !== customLabeledPreset.name) { + setCachedCustomRnaPreset({ + ...customLabeledPreset, + name: presetUniqName, + }); + } + } + + // Transform IRnaLabeledPreset[] to IRnaPreset[] + customLabeledPresets = getCachedCustomRnaPresets()!; + for (const preset of customLabeledPresets) { + const rnaPreset = + editor.drawingEntitiesManager.createRnaPresetFromLabeledRnaPreset( + preset, + ); + if (rnaPreset) customPresets.push(rnaPreset); + } + } + + dispatch(loadMonomerLibrary(monomersLibrary)); + dispatch(setFavoriteMonomersFromLocalStorage(null)); + + dispatch(setDefaultPresets(defaultPresets)); + customLabeledPresets && dispatch(setCustomPresets(customPresets)); + dispatch(setFavoritePresetsFromLocalStorage()); + + return () => { + dispatch(loadMonomerLibrary([])); + dispatch(clearFavorites()); + }; + }, [editor]); +} + +export default useSetRnaPresets; diff --git a/packages/ketcher-macromolecules/src/state/rna-builder/rnaBuilderSlice.ts b/packages/ketcher-macromolecules/src/state/rna-builder/rnaBuilderSlice.ts index 9b93d70e1f..5ff15c40ff 100644 --- a/packages/ketcher-macromolecules/src/state/rna-builder/rnaBuilderSlice.ts +++ b/packages/ketcher-macromolecules/src/state/rna-builder/rnaBuilderSlice.ts @@ -17,14 +17,18 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { IRnaPreset } from 'components/monomerLibrary/RnaBuilder/types'; import { RootState } from 'state'; -import { MonomerGroups } from '../../constants'; import { MonomerItemType, MONOMER_CONST, LabeledNodesWithPositionInSequence, } from 'ketcher-core'; import { localStorageWrapper } from 'helpers/localStorage'; -import { FAVORITE_ITEMS_UNIQUE_KEYS } from 'src/constants'; +import { FAVORITE_ITEMS_UNIQUE_KEYS, MonomerGroups } from 'src/constants'; +import { + deleteCachedCustomRnaPreset, + setCachedCustomRnaPreset, + toggleCachedCustomRnaPresetFavorites, +} from 'helpers/manipulateCachedRnaPresets'; export enum RnaBuilderPresetsItem { Presets = 'Presets', @@ -41,7 +45,8 @@ interface IRnaBuilderState { groupName: MonomerGroups; groupItem: MonomerItemType; } | null; - presets: IRnaPreset[]; + presetsDefault: IRnaPreset[]; + presetsCustom: IRnaPreset[]; activeRnaBuilderItem?: RnaBuilderItem | null; isEditMode: boolean; uniqueNameError: string; @@ -54,7 +59,8 @@ const initialState: IRnaBuilderState = { sequenceSelectionName: undefined, isSequenceFirstsOnlyNucleoelementsSelected: undefined, activePresetMonomerGroup: null, - presets: [], + presetsDefault: [], + presetsCustom: [], activeRnaBuilderItem: null, isEditMode: false, uniqueNameError: '', @@ -76,12 +82,13 @@ export const rnaBuilderSlice = createSlice({ sugar: undefined, phosphate: undefined, name: '', + nameInList: '', }; }, setActivePreset: (state, action: PayloadAction) => { state.activePreset = { ...action.payload, - presetInList: action.payload, + nameInList: action.payload.name, }; }, setSequenceSelection: ( @@ -127,28 +134,36 @@ export const rnaBuilderSlice = createSlice({ const preset = action.payload; const newPreset = { ...preset }; - if (preset.presetInList) { - const presetIndexInList = state.presets.findIndex( - (presetInList) => presetInList.name === preset.presetInList?.name, + setCachedCustomRnaPreset(newPreset); + + // Save or update preset in Store + if (newPreset.nameInList) { + const presetIndexInList = state.presetsCustom.findIndex( + (presetInList) => presetInList.name === newPreset.nameInList, ); + newPreset.nameInList = newPreset.name; presetIndexInList === -1 - ? state.presets.push(newPreset) - : state.presets.splice(presetIndexInList, 1, newPreset); + ? state.presetsCustom.push(newPreset) + : state.presetsCustom.splice(presetIndexInList, 1, newPreset); } else { - state.presets.push(newPreset); + state.presetsCustom.push(newPreset); } + if (!state.activePreset) return; - state.activePreset.presetInList = newPreset; + state.activePreset.nameInList = newPreset.name; }, deletePreset: (state, action: PayloadAction) => { const preset = action.payload; - const presetIndexInList = state.presets.findIndex( + deleteCachedCustomRnaPreset(preset.name); + + // Delete preset from Store + const presetIndexInList = state.presetsCustom.findIndex( (presetInList) => presetInList.name === preset.name, ); - state.presets.splice(presetIndexInList, 1); + state.presetsCustom.splice(presetIndexInList, 1); - if (preset.presetInList) { + if (preset.nameInList) { state.activePreset = null; } }, @@ -166,13 +181,19 @@ export const rnaBuilderSlice = createSlice({ if (!defaultNucleotide) { return; } - const presetExists = state.presets.find( + const presetExists = state.presetsDefault.find( (item: IRnaPreset) => item.name === defaultNucleotide.name, ); if (presetExists) { return; } - state.presets = action.payload; + state.presetsDefault = action.payload; + }, + setCustomPresets: ( + state: RootState, + action: PayloadAction, + ) => { + state.presetsCustom = action.payload; }, setFavoritePresetsFromLocalStorage: (state: RootState) => { @@ -183,7 +204,7 @@ export const rnaBuilderSlice = createSlice({ return; } - state.presets = state.presets.map((preset) => { + state.presetsDefault = state.presetsDefault.map((preset) => { const uniqueKey = `${preset.name}_${MONOMER_CONST.RNA}`; const favoriteItem = favoritesInLocalStorage.find( @@ -202,21 +223,34 @@ export const rnaBuilderSlice = createSlice({ }, clearFavorites: (state: RootState) => { - state.presets = []; + state.presetsDefault = []; }, togglePresetFavorites: (state, action: PayloadAction) => { - const presetIndex = state.presets.findIndex( + // Find preset to update in default presets + const presetIndex = state.presetsDefault.findIndex( + (presetInList) => presetInList.name === action.payload.name, + ); + // Find preset to update in custom presets + const presetCustomIndex = state.presetsCustom.findIndex( (presetInList) => presetInList.name === action.payload.name, ); - const uniquePresetKey = `${action.payload.name}_${MONOMER_CONST.RNA}`; - + // If updating default preset if (presetIndex >= 0) { - const favorite = state.presets[presetIndex].favorite; - state.presets[presetIndex].favorite = !favorite; + const favorite = state.presetsDefault[presetIndex].favorite; + state.presetsDefault[presetIndex].favorite = !favorite; + // If updating custom preset + } else if (presetCustomIndex >= 0) { + toggleCachedCustomRnaPresetFavorites( + state.presetsCustom[presetCustomIndex].name, + ); + const favorite = state.presetsCustom[presetCustomIndex].favorite; + state.presetsCustom[presetCustomIndex].favorite = !favorite; + return; } + const uniquePresetKey = `${action.payload.name}_${MONOMER_CONST.RNA}`; const favoriteItemsUniqueKeys = (localStorageWrapper.getItem( FAVORITE_ITEMS_UNIQUE_KEYS, ) || []) as string[]; @@ -260,10 +294,6 @@ export const selectIsSequenceFirstsOnlyNucleotidesSelected = ( state: RootState, ): boolean => state.rnaBuilder.isSequenceFirstsOnlyNucleoelementsSelected; -export const selectPresets = (state: RootState): IRnaPreset[] => { - return state.rnaBuilder.presets; -}; - export const selectCurrentMonomerGroup = ( preset: IRnaPreset, groupName: MonomerGroups | string, @@ -314,7 +344,7 @@ export const selectIsActivePresetNewAndEmpty = (state: RootState): boolean => { const activePreset = state.rnaBuilder.activePreset; return ( activePreset && - !activePreset.presetInList && + !activePreset.nameInList && !activePreset.name && !activePreset.sugar && !activePreset.base && @@ -329,12 +359,19 @@ export const selectActivePresetForContextMenu = (state: RootState) => { export const selectPresetsInFavorites = (items: IRnaPreset[]) => items.filter((item) => item.favorite); +// Return custom and default presets +export const selectAllPresets = ( + state, +): Array => { + const { presetsDefault = [], presetsCustom = [] } = state.rnaBuilder; + return [...presetsDefault, ...presetsCustom]; +}; export const selectFilteredPresets = ( state, ): Array => { const { searchFilter } = state.library; - const { presets } = state.rnaBuilder; - return presets.filter((item: IRnaPreset) => { + const presetsAll = selectAllPresets(state); + return presetsAll.filter((item: IRnaPreset) => { const name = item.name?.toLowerCase(); const sugarName = item.sugar?.label?.toLowerCase(); const phosphateName = item.phosphate?.label?.toLowerCase(); @@ -363,6 +400,7 @@ export const { setIsEditMode, setUniqueNameError, setDefaultPresets, + setCustomPresets, setActivePresetForContextMenu, togglePresetFavorites, setFavoritePresetsFromLocalStorage,