packages/ketcher-macromolecules/src/components/modal/Open/Open.tsx (407 lines of code) (raw):

/**************************************************************************** * Copyright 2021 EPAM Systems * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ***************************************************************************/ import { Modal } from 'components/shared/modal'; import { useCallback, useEffect, useState } from 'react'; import { ViewSwitcher } from './ViewSwitcher'; import { ActionButton } from 'components/shared/actionButton'; import { FileOpener, fileOpener } from './fileOpener'; import { ChemicalMimeType, KetSerializer, StructService, CoreEditor, KetcherLogger, EditorHistory, SequenceMode, macromoleculesFilesInputFormats, ModeTypes, SnakeMode, FlexMode, } from 'ketcher-core'; import { IndigoProvider } from 'ketcher-react'; import { RequiredModalProps } from '../modalContainer'; import { OpenFileWrapper } from './Open.styles'; import { Loader, StyledDropdown as SaveDropdown, stylesForExpanded, } from '../save/Save.styles'; import { LoadingCircles } from './AnalyzingFile/LoadingCircles'; import { useAppDispatch } from 'hooks'; import { openErrorModal } from 'state/modal'; import { AnyAction, Dispatch } from 'redux'; import styled from '@emotion/styled'; import { Option } from 'components/shared/dropDown/dropDown'; export interface Props { onClose: () => void; isModalOpen: boolean; } const OpenModal = styled(Modal)( ({ modalWidth }) => ` .MuiPaper-root { width: ${modalWidth}; max-width: ${modalWidth}; }`, ); const OpenFooter = styled.div({ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }); const FooterSelectorContainer = styled.div({ display: 'flex', height: '24px', fontSize: '12px', }); const StyledDropdown = styled(SaveDropdown)({ padding: 0, fontSize: '12px', '& .MuiSelect-select': { display: 'flex', alignItems: 'center', padding: '0 20px 0 8px', paddingRight: '20px !important', // override MUI styles height: '100%', }, '& span': { fontSize: '12px', }, }); const FooterFormatSelector = styled(StyledDropdown)(() => ({ width: '140px', })); const FooterSequenceSelector = styled(StyledDropdown)({ width: '76px', marginLeft: '8px', }); const FooterPeptideLettersSelector = styled(StyledDropdown)({ width: '105px', marginLeft: '8px', }); const FooterButtonContainer = styled('div')({ display: 'flex', gap: '10px', }); const FooterButton = styled(ActionButton)({ width: 'min-content', }); const KET = 'ket'; const SEQ = 'seq'; const RNA = 'rna'; const PEPTIDE = 'peptide'; const FASTA = 'fasta'; const ONE_LETTER = 'one-letter'; const THREE_LETTER = 'three-letter'; const options: Array<Option> = [ { id: 'ket', label: 'Ket' }, { id: 'mol', label: 'MDL Molfile V3000' }, { id: 'seq', label: 'Sequence' }, { id: 'fasta', label: 'FASTA' }, { id: 'idt', label: 'IDT' }, { id: 'helm', label: 'HELM' }, ]; const additionalOptions: Array<Option> = [ { id: RNA, label: 'RNA' }, { id: 'dna', label: 'DNA' }, { id: PEPTIDE, label: 'Peptide' }, ]; const peptideLettersFormatOptions: Array<Option> = [ { id: ONE_LETTER, label: '1-letter code' }, { id: THREE_LETTER, label: '3-letter code' }, ]; const inputFormats = macromoleculesFilesInputFormats; export const MODAL_STATES = { openOptions: 'openOptions', textEditor: 'textEditor', } as const; export type MODAL_STATES_VALUES = typeof MODAL_STATES[keyof typeof MODAL_STATES]; const addToCanvas = ({ ketSerializer, editor, struct, }: { ketSerializer: KetSerializer; editor: CoreEditor; struct: string; }) => { const isCanvasEmptyBeforeOpenStructure = !editor.drawingEntitiesManager.hasDrawingEntities; const deserialisedKet = ketSerializer.deserializeToDrawingEntities(struct); if (!deserialisedKet) { throw new Error('Error during parsing file'); } deserialisedKet.drawingEntitiesManager.centerMacroStructure(); const { command: modelChanges } = deserialisedKet.drawingEntitiesManager.mergeInto( editor.drawingEntitiesManager, ); const editorHistory = new EditorHistory(editor); const isSequenceMode = editor.mode instanceof SequenceMode; const isSnakeMode = editor.mode instanceof SnakeMode; const isFlexMode = editor.mode instanceof FlexMode; if (isFlexMode) { editor.drawingEntitiesManager.recalculateAntisenseChains(); if (editor.drawingEntitiesManager.hasAntisenseChains) { modelChanges.merge( editor.drawingEntitiesManager.applySnakeLayout( editor.canvas.width.baseVal.value, true, true, true, ), ); modelChanges.setUndoOperationsByPriority(); } } editor.renderersContainer.update(modelChanges); editorHistory.update(modelChanges); if (isSequenceMode) { modelChanges.setUndoOperationReverse(); editor.events.selectMode.dispatch({ mode: ModeTypes.sequence, mergeWithLatestHistoryCommand: true, }); } if (isSnakeMode) { modelChanges.setUndoOperationReverse(); editor.events.selectMode.dispatch({ mode: ModeTypes.snake, mergeWithLatestHistoryCommand: true, }); } if (isCanvasEmptyBeforeOpenStructure) { editor.zoomToStructuresIfNeeded(); } }; // TODO: replace after the implementation of the function for processing the structure from the file const onOk = async ({ struct, formatSelection, additionalSelection, peptideLettersFormatSelection, onCloseCallback, setIsLoading, dispatch, }: { struct: string; formatSelection: string; additionalSelection: string; peptideLettersFormatSelection: string; onCloseCallback: () => void; setIsLoading: (isLoading: boolean) => void; dispatch: Dispatch<AnyAction>; }) => { const isKet = formatSelection === KET; const isSeq = formatSelection === SEQ; const isFasta = formatSelection === FASTA; const ketSerializer = new KetSerializer(); const editor = CoreEditor.provideEditorInstance(); let inputFormat; let fileData = struct; const showParsingError = (stringError) => { const errorMessage = 'Convert error! ' + stringError; dispatch( openErrorModal({ errorMessage, errorTitle: isSeq || isFasta ? 'Unsupported symbols' : '', }), ); }; if (isKet) { try { addToCanvas({ struct, ketSerializer, editor }); onCloseCallback(); } catch (e) { showParsingError('Error during file parsing.'); } return; } else if ( isFasta || (isSeq && peptideLettersFormatSelection !== THREE_LETTER) ) { inputFormat = inputFormats[formatSelection][additionalSelection]; fileData = fileData.toUpperCase(); } else if (isSeq && peptideLettersFormatSelection === THREE_LETTER) { inputFormat = inputFormats.seq.peptide3Letter; } else { inputFormat = inputFormats[formatSelection]; } const indigo = IndigoProvider.getIndigo() as StructService; try { setIsLoading(true); const ketStruct = await indigo.convert({ struct: fileData, output_format: ChemicalMimeType.KET, input_format: inputFormat, }); addToCanvas({ struct: ketStruct.struct, ketSerializer, editor }); onCloseCallback(); } catch (error) { const stringError = typeof error === 'string' ? error : JSON.stringify(error); showParsingError(stringError); KetcherLogger.error(error); } finally { setIsLoading(false); } }; const isAnalyzingFile = false; const errorHandler = (error) => console.log(error); const Open = ({ isModalOpen, onClose }: RequiredModalProps) => { const dispatch = useAppDispatch(); const [structStr, setStructStr] = useState<string>(''); const [fileName, setFileName] = useState<string>(''); const [isLoading, setIsLoading] = useState(false); const [opener, setOpener] = useState< { chosenOpener: FileOpener } | undefined >(); const [currentState, setCurrentState] = useState<MODAL_STATES_VALUES>( MODAL_STATES.openOptions, ); const [formatSelection, setFormatSelection] = useState(KET); const [additionalSelection, setAdditionalSelection] = useState(RNA); const [peptideLettersFormatSelection, setPeptideLettersFormatSelection] = useState(ONE_LETTER); useEffect(() => { const splittedFilenameByDot = fileName?.split('.'); const fileExtension = splittedFilenameByDot[splittedFilenameByDot.length - 1]; if (fileExtension) { const option = options.find((el) => el.id === fileExtension); const id = option?.id ? option.id : SEQ; setFormatSelection(id); } }, [fileName]); useEffect(() => { fileOpener().then((chosenOpener) => { setOpener({ chosenOpener }); }); }, []); const onCloseCallback = useCallback(() => { setCurrentState(MODAL_STATES.openOptions); setStructStr(''); setFormatSelection(KET); setAdditionalSelection(RNA); onClose(); }, [onClose]); const onFileLoad = (files: File[]) => { const onLoad = (fileContent) => { setStructStr(fileContent); setCurrentState(MODAL_STATES.textEditor); }; const onError = () => errorHandler('Error processing file'); setFileName(files[0].name); opener?.chosenOpener(files[0]).then(onLoad, onError); }; const addToCanvasHandler = () => { onOk({ struct: structStr, formatSelection, additionalSelection, peptideLettersFormatSelection, onCloseCallback, setIsLoading, dispatch, }); }; const openHandler = () => { const editor = CoreEditor.provideEditorInstance(); const history = new EditorHistory(editor); const modelChanges = editor.drawingEntitiesManager.deleteAllEntities(); history.update(modelChanges); editor.renderersContainer.update(modelChanges); editor.zoomToStructuresIfNeeded(); onOk({ struct: structStr, formatSelection, additionalSelection, peptideLettersFormatSelection, onCloseCallback, setIsLoading, dispatch, }); }; const renderFooter = () => ( <OpenFooter> <FooterSelectorContainer> <FooterFormatSelector options={options} currentSelection={formatSelection} selectionHandler={setFormatSelection} customStylesForExpanded={stylesForExpanded} key={formatSelection} /> {formatSelection === SEQ || formatSelection === FASTA ? ( <FooterSequenceSelector options={additionalOptions} currentSelection={additionalSelection} selectionHandler={setAdditionalSelection} customStylesForExpanded={stylesForExpanded} key={additionalSelection} testId="dropdown-select-type" /> ) : null} {formatSelection === SEQ && additionalSelection === PEPTIDE ? ( <FooterPeptideLettersSelector options={peptideLettersFormatOptions} currentSelection={peptideLettersFormatSelection} selectionHandler={setPeptideLettersFormatSelection} testId="dropdown-select-peptide-letters-format" /> ) : null} </FooterSelectorContainer> <FooterButtonContainer> <FooterButton key="openButton" disabled={!structStr} clickHandler={openHandler} label="Open as New" styleType="secondary" /> <FooterButton key="copyButton" disabled={!structStr} clickHandler={addToCanvasHandler} label="Add to Canvas" title="Structure will be loaded as fragment and added to Clipboard" data-testid="add-to-canvas-button" /> </FooterButtonContainer> </OpenFooter> ); return ( <OpenModal isOpen={isModalOpen} title="Open Structure" onClose={onCloseCallback} modalWidth={currentState === MODAL_STATES.textEditor ? '620px' : ''} > <Modal.Content> <OpenFileWrapper currentState={currentState}> <ViewSwitcher isAnalyzingFile={isAnalyzingFile} fileName={fileName} currentState={currentState} states={MODAL_STATES} selectClipboard={() => setCurrentState(MODAL_STATES.textEditor)} fileLoadHandler={onFileLoad} errorHandler={errorHandler} value={structStr} inputHandler={setStructStr} /> {isLoading && ( <Loader> <LoadingCircles /> </Loader> )} </OpenFileWrapper> </Modal.Content> {currentState === MODAL_STATES.textEditor && !isAnalyzingFile ? ( <Modal.Footer withborder="true">{renderFooter()}</Modal.Footer> ) : ( <></> )} </OpenModal> ); }; export { Open };