diff --git a/packages/ketcher-core/src/application/actions/action.types.ts b/packages/ketcher-core/src/application/actions/action.types.ts index 394d0f87f7..d77e83c5c5 100644 --- a/packages/ketcher-core/src/application/actions/action.types.ts +++ b/packages/ketcher-core/src/application/actions/action.types.ts @@ -14,6 +14,13 @@ * limitations under the License. ***************************************************************************/ +// import { Operation } from "application/operations" +// import { ReStruct } from "application/render" + export interface Action { perform: () => void + // operations: Array + // mergeWith(action: Action): Action + // addOp(operation: Operation, restruct?: ReStruct): Operation + // isDummy(restruct?: ReStruct): boolean } diff --git a/packages/ketcher-core/src/application/editor/editor.types.ts b/packages/ketcher-core/src/application/editor/editor.types.ts index 436f43180e..35497fcbc4 100644 --- a/packages/ketcher-core/src/application/editor/editor.types.ts +++ b/packages/ketcher-core/src/application/editor/editor.types.ts @@ -52,4 +52,5 @@ export interface Editor { setOptions: (opts: string) => any zoom: (value?: any) => any structSelected: () => Struct + // update: (action: Action | true, ignoreHistory?: boolean) => void } diff --git a/packages/ketcher-core/src/application/formatters/formatProperties.ts b/packages/ketcher-core/src/application/formatters/formatProperties.ts index 6091eec922..8f34ca8d92 100644 --- a/packages/ketcher-core/src/application/formatters/formatProperties.ts +++ b/packages/ketcher-core/src/application/formatters/formatProperties.ts @@ -100,6 +100,12 @@ const formatProperties: FormatPropertiesMap = { ChemicalMimeType.CDX, ['.cdx'], true + ), + unknown: new SupportedFormatProperties( + 'Unknown', + ChemicalMimeType.UNKNOWN, + ['.'], + true ) } diff --git a/packages/ketcher-core/src/application/formatters/formatterFactory.ts b/packages/ketcher-core/src/application/formatters/formatterFactory.ts index 76d7a810a7..1eb8cd2992 100644 --- a/packages/ketcher-core/src/application/formatters/formatterFactory.ts +++ b/packages/ketcher-core/src/application/formatters/formatterFactory.ts @@ -92,6 +92,7 @@ export class FormatterFactory { case SupportedFormat.smarts: case SupportedFormat.cdxml: case SupportedFormat.cdx: + case SupportedFormat.unknown: default: formatter = new ServerFormatter( this.#structService, diff --git a/packages/ketcher-core/src/application/formatters/identifyStructFormat.ts b/packages/ketcher-core/src/application/formatters/identifyStructFormat.ts index cad099b4a7..883903bd68 100644 --- a/packages/ketcher-core/src/application/formatters/identifyStructFormat.ts +++ b/packages/ketcher-core/src/application/formatters/identifyStructFormat.ts @@ -32,6 +32,10 @@ export function identifyStructFormat( return SupportedFormat.rxn } + if (sanitizedString.indexOf('V2000') !== -1) { + return SupportedFormat.mol + } + if (sanitizedString.indexOf('V3000') !== -1) { return SupportedFormat.molV3000 } @@ -55,12 +59,16 @@ export function identifyStructFormat( return SupportedFormat.cml } - const clearStr = sanitizedString.replace(/\s/g, '') - const anyLetterAnyDigitContainsSlashesEndsWithEqualSign = - /^[a-zA-Z0-9+/]*={0,2}$/ + const clearStr = sanitizedString + .replace(/\s/g, '') + .replace(/(\\r)|(\\n)/g, '') + const isBase64String = + /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/ + const cdxHeader = 'VjCD0100' if ( - anyLetterAnyDigitContainsSlashesEndsWithEqualSign.test(clearStr) && - clearStr.length % 4 === 0 + clearStr.length % 4 === 0 && + isBase64String.test(clearStr) && + window.atob(clearStr).startsWith(cdxHeader) ) { return SupportedFormat.cdx } @@ -69,10 +77,7 @@ export function identifyStructFormat( return SupportedFormat.inChI } - if ( - sanitizedString.indexOf('\n') === -1 && - sanitizedString === sanitizedString.toUpperCase() - ) { + if (sanitizedString.indexOf('\n') === -1) { // TODO: smiles regexp return SupportedFormat.smiles } @@ -80,6 +85,6 @@ export function identifyStructFormat( if (sanitizedString.indexOf(' PerformOperationResult + invert(): Operation + isDummy(_restruct: ReStruct): boolean + execute(_restruct: ReStruct): void } export type PerformOperationResult = { diff --git a/packages/ketcher-core/src/application/render/restruct/restruct.ts b/packages/ketcher-core/src/application/render/restruct/restruct.ts index 1f875b7944..8ae3d79bd8 100644 --- a/packages/ketcher-core/src/application/render/restruct/restruct.ts +++ b/packages/ketcher-core/src/application/render/restruct/restruct.ts @@ -605,10 +605,14 @@ class ReStruct { item.selected = sGroupAtoms.length > 0 && sGroupAtoms[0].selected } - const selected = selection?.[map] + let selected = selection?.[map] ? selection[map].indexOf(id) > -1 : item.selected + if (selection === null) { + selected = false + } + this.showItemSelection(item, selected) }) } diff --git a/packages/ketcher-core/src/domain/entities/functionalGroup.ts b/packages/ketcher-core/src/domain/entities/functionalGroup.ts index 22ad8860f6..bb1c2424c5 100644 --- a/packages/ketcher-core/src/domain/entities/functionalGroup.ts +++ b/packages/ketcher-core/src/domain/entities/functionalGroup.ts @@ -60,9 +60,14 @@ export class FunctionalGroup { static getFunctionalGroupByName(searchName: string): Struct | null { const provider = FunctionalGroupsProvider.getInstance() const functionalGroups = provider.getFunctionalGroupsList() - const foundGroup = functionalGroups.find(({ name, abbreviation }) => { - return name === searchName || abbreviation === searchName - }) + + let foundGroup + if (searchName) { + foundGroup = functionalGroups.find(({ name, abbreviation }) => { + return name === searchName || abbreviation === searchName + }) + } + return foundGroup || null } diff --git a/packages/ketcher-core/src/domain/services/struct/structService.types.ts b/packages/ketcher-core/src/domain/services/struct/structService.types.ts index cce91b2ac4..7d26c019f9 100644 --- a/packages/ketcher-core/src/domain/services/struct/structService.types.ts +++ b/packages/ketcher-core/src/domain/services/struct/structService.types.ts @@ -25,7 +25,8 @@ export enum ChemicalMimeType { CDX = 'chemical/x-cdx', CDXML = 'chemical/x-cdxml', CML = 'chemical/x-cml', - KET = 'chemical/x-indigo-ket' + KET = 'chemical/x-indigo-ket', + UNKNOWN = 'chemical/x-unknown' } export interface WithStruct { diff --git a/packages/ketcher-react/package.json b/packages/ketcher-react/package.json index c3f91fe803..f55aa7bc93 100644 --- a/packages/ketcher-react/package.json +++ b/packages/ketcher-react/package.json @@ -126,6 +126,7 @@ "rollup-plugin-delete": "^2.0.0", "rollup-plugin-peer-deps-external": "^2.2.4", "rollup-plugin-postcss": "^4.0.2", + "rollup-plugin-string": "^3.0.0", "rollup-plugin-typescript2": "^0.31.1", "rollup-plugin-visualizer": "^5.5.2", "stylelint": "13.13.1", diff --git a/packages/ketcher-react/rollup.config.js b/packages/ketcher-react/rollup.config.js index a85bc85c35..8d313b9e6d 100644 --- a/packages/ketcher-react/rollup.config.js +++ b/packages/ketcher-react/rollup.config.js @@ -16,6 +16,7 @@ import strip from '@rollup/plugin-strip' import svgr from '@svgr/rollup' import typescript from 'rollup-plugin-typescript2' import { license } from '../../license.ts' +import { string } from 'rollup-plugin-string' const mode = { PRODUCTION: 'production', @@ -98,7 +99,10 @@ const config = { comments: 'none', include: includePattern }), - ...(isProduction ? [strip({ include: includePattern })] : []) + ...(isProduction ? [strip({ include: includePattern })] : []), + string({ + include: '**/*.sdf' + }) ] } diff --git a/packages/ketcher-react/src/script/editor/Editor.ts b/packages/ketcher-react/src/script/editor/Editor.ts index 8ddbf03fed..b2026a67b9 100644 --- a/packages/ketcher-react/src/script/editor/Editor.ts +++ b/packages/ketcher-react/src/script/editor/Editor.ts @@ -387,7 +387,7 @@ class Editor implements KetcherEditor { } } - update(action: Action | true, ignoreHistory?) { + update(action: Action | true, ignoreHistory?: boolean) { if (action === true) { this.render.update(true) // force } else { @@ -606,6 +606,10 @@ function domEventSetup(editor: Editor, clientArea) { editor.lastEvent = event if (EditorTool && eventName in EditorTool) { EditorTool[eventName](event) + return true + } + if (eventName === 'mouseup') { + editor.selection(null) } return true }, -1) diff --git a/packages/ketcher-react/src/script/editor/tool/bond.ts b/packages/ketcher-react/src/script/editor/tool/bond.ts index ea58033434..5107da7f8a 100644 --- a/packages/ketcher-react/src/script/editor/tool/bond.ts +++ b/packages/ketcher-react/src/script/editor/tool/bond.ts @@ -284,7 +284,7 @@ class BondTool { this.editor.update(bondAddition[0]) } else if (dragCtx.item.map === 'atoms') { - // when does it hapend? + // click on atom this.editor.update( fromBondAddition( rnd.ctab, diff --git a/packages/ketcher-react/src/script/editor/tool/select.ts b/packages/ketcher-react/src/script/editor/tool/select.ts index 091dae5897..acebb0b07b 100644 --- a/packages/ketcher-react/src/script/editor/tool/select.ts +++ b/packages/ketcher-react/src/script/editor/tool/select.ts @@ -34,7 +34,7 @@ import { import LassoHelper from './helper/lasso' import { atomLongtapEvent } from './atom' -import { sgroupDialog } from './sgroup' +import SGroupTool from './sgroup' import utils from '../shared/utils' import { xor } from 'lodash/fp' import { Editor } from '../Editor' @@ -193,6 +193,7 @@ class SelectTool { if (event.shiftKey) { this.editor.selection(selMerge(sel, selection, true)) } else { + this.editor.selection(null) this.editor.selection(isSelected(selection, ci) ? selection : sel) } return true @@ -535,7 +536,7 @@ class SelectTool { ci.map === 'sgroupData' ) { editor.selection(closestToSel(ci)) - sgroupDialog(editor, ci.id, null) + SGroupTool.sgroupDialog(editor, ci.id, null) } else if (ci.map === 'texts') { editor.selection(closestToSel(ci)) const text = molecule.texts.get(ci.id) diff --git a/packages/ketcher-react/src/script/editor/tool/sgroup.ts b/packages/ketcher-react/src/script/editor/tool/sgroup.ts index 5ffb47dfe9..4be43ec1f9 100644 --- a/packages/ketcher-react/src/script/editor/tool/sgroup.ts +++ b/packages/ketcher-react/src/script/editor/tool/sgroup.ts @@ -143,7 +143,7 @@ class SGroupTool { return } - sgroupDialog(this.editor, id !== undefined ? id : null, this.type) + SGroupTool.sgroupDialog(this.editor, id ?? null, this.type) this.isNotActiveTool = true } } @@ -443,7 +443,7 @@ class SGroupTool { // TODO: handle click on an existing group? if (id !== null || (selection && selection.atoms)) - sgroupDialog(this.editor, id, this.type) + SGroupTool.sgroupDialog(this.editor, id, this.type) } cancel() { @@ -452,75 +452,75 @@ class SGroupTool { } this.editor.selection(null) } -} -export function sgroupDialog(editor, id, defaultType) { - const restruct = editor.render.ctab - const struct = restruct.molecule - const selection = editor.selection() || {} - const sg = id !== null ? struct.sgroups.get(id) : null - const type = sg ? sg.type : defaultType - const eventName = type === 'DAT' ? 'sdataEdit' : 'sgroupEdit' - - if (!selection.atoms && !selection.bonds && !sg) { - console.info('There is no selection or sgroup') - return - } + static sgroupDialog(editor, id, defaultType) { + const restruct = editor.render.ctab + const struct = restruct.molecule + const selection = editor.selection() || {} + const sg = id !== null ? struct.sgroups.get(id) : null + const type = sg ? sg.type : defaultType + const eventName = type === 'DAT' ? 'sdataEdit' : 'sgroupEdit' - let attrs - if (sg) { - attrs = sg.getAttrs() - if (!attrs.context) attrs.context = getContextBySgroup(restruct, sg.atoms) - } else { - attrs = { - context: getContextBySelection(restruct, selection) + if (!selection.atoms && !selection.bonds && !sg) { + console.info('There is no selection or sgroup') + return } - } - const res = editor.event[eventName].dispatch({ - type, - attrs - }) + let attrs + if (sg) { + attrs = sg.getAttrs() + if (!attrs.context) attrs.context = getContextBySgroup(restruct, sg.atoms) + } else { + attrs = { + context: getContextBySelection(restruct, selection) + } + } - Promise.resolve(res) - .then((newSg) => { - // TODO: check before signal - if ( - newSg.type !== 'DAT' && // when data s-group separates - checkOverlapping(struct, selection.atoms || []) - ) { - editor.event.message.dispatch({ - error: 'Partial S-group overlapping is not allowed.' - }) - } else { + const res = editor.event[eventName].dispatch({ + type, + attrs + }) + + Promise.resolve(res) + .then((newSg) => { + // TODO: check before signal if ( - !sg && - newSg.type !== 'DAT' && - (!selection.atoms || selection.atoms.length === 0) - ) - return + newSg.type !== 'DAT' && // when data s-group separates + checkOverlapping(struct, selection.atoms || []) + ) { + editor.event.message.dispatch({ + error: 'Partial S-group overlapping is not allowed.' + }) + } else { + if ( + !sg && + newSg.type !== 'DAT' && + (!selection.atoms || selection.atoms.length === 0) + ) + return - const isDataSg = sg && sg.getAttrs().context === newSg.attrs.context + const isDataSg = sg && sg.getAttrs().context === newSg.attrs.context - if (isDataSg) { - const action = fromSeveralSgroupAddition( - restruct, - newSg.type, - sg.atoms, - newSg.attrs - ).mergeWith(fromSgroupDeletion(restruct, id)) + if (isDataSg) { + const action = fromSeveralSgroupAddition( + restruct, + newSg.type, + sg.atoms, + newSg.attrs + ).mergeWith(fromSgroupDeletion(restruct, id)) - editor.update(action) - editor.selection(selection) - return - } + editor.update(action) + editor.selection(selection) + return + } - const result = fromContextType(id, editor, newSg, selection) - editor.update(result.action) - editor.selection(null) - } - }) - .catch(() => null) + const result = fromContextType(id, editor, newSg, selection) + editor.update(result.action) + editor.selection(null) + } + }) + .catch(() => null) + } } function getContextBySgroup(restruct, sgAtoms) { diff --git a/packages/ketcher-react/src/script/ui/App/App.tsx b/packages/ketcher-react/src/script/ui/App/App.tsx index cc1b5b1a85..ae43126354 100644 --- a/packages/ketcher-react/src/script/ui/App/App.tsx +++ b/packages/ketcher-react/src/script/ui/App/App.tsx @@ -30,7 +30,7 @@ import Editor from '../views/Editor' import classes from './App.module.less' import { initFGTemplates } from '../state/functionalGroups' import { initSaltsAndSolventsTemplates } from '../state/saltsAndSolvents' -import { useSettingsContext, useSubscriptionOnEvents } from '../../../hooks' +import { useSubscriptionOnEvents } from '../../../hooks' interface AppCallProps { checkServer: () => void @@ -51,14 +51,13 @@ type Props = AppCallProps const App = (props: Props) => { const dispatch = useDispatch() const { checkServer } = props - const { staticResourcesUrl } = useSettingsContext() useSubscriptionOnEvents() useEffect(() => { checkServer() - dispatch(initFGTemplates(staticResourcesUrl)) - dispatch(initSaltsAndSolventsTemplates(staticResourcesUrl)) + dispatch(initFGTemplates()) + dispatch(initSaltsAndSolventsTemplates()) window.scrollTo(0, 0) }, []) diff --git a/packages/ketcher-react/src/script/ui/action/tools.js b/packages/ketcher-react/src/script/ui/action/tools.js index 6d16db26f6..1ae087fb04 100644 --- a/packages/ketcher-react/src/script/ui/action/tools.js +++ b/packages/ketcher-react/src/script/ui/action/tools.js @@ -23,7 +23,7 @@ import { toBondType } from '../data/convert/structconv' const toolActions = { hand: { title: 'Hand tool', - shortcut: 'Mod+h', + shortcut: 'Mod+Alt+h', action: { tool: 'hand' }, hidden: (options) => isHidden(options, 'hand') }, diff --git a/packages/ketcher-react/src/script/ui/component/form/input.jsx b/packages/ketcher-react/src/script/ui/component/form/input.tsx similarity index 74% rename from packages/ketcher-react/src/script/ui/component/form/input.jsx rename to packages/ketcher-react/src/script/ui/component/form/input.tsx index 4178a04b70..024ab42c35 100644 --- a/packages/ketcher-react/src/script/ui/component/form/input.jsx +++ b/packages/ketcher-react/src/script/ui/component/form/input.tsx @@ -14,23 +14,46 @@ * limitations under the License. ***************************************************************************/ -import { Component, useRef, useEffect } from 'react' +import React, { PureComponent, ComponentType, useRef, useEffect } from 'react' -import { omit } from 'lodash/fp' import classes from './input.module.less' import clsx from 'clsx' +type Props = { + component?: ComponentType + children?: React.ReactNode + className?: string + type: string + value: number | string | boolean + onChange: (val: any) => void + placeholder?: string + isFocused?: boolean + innerRef?: React.Ref + schema?: any + multiple?: boolean +} + export function GenericInput({ schema, value = '', onChange, + innerRef, type = 'text', isFocused = false, ...props }) { - const inputRef = useRef(null) + const inputRef = useRef(null) + useEffect(() => { - if (inputRef.current && isFocused) inputRef.current.focus() + if (inputRef.current) { + innerRef.current = inputRef.current + } + }, [innerRef]) + + useEffect(() => { + if (inputRef.current && isFocused) { + inputRef.current.focus() + } }, [inputRef, isFocused]) return ( @@ -72,7 +95,7 @@ function CheckBox({ schema, value = '', onChange, ...rest }) {
(!o.selected ? res : [enumSchema(schema, i), ...res]), - [] + const options = select.options + + return Array.from(options).reduce( + (res, o: HTMLOptionElement, i) => + !o.selected ? res : [enumSchema(schema, i), ...res], + [] as HTMLOptionElement[] ) } @@ -158,19 +183,29 @@ function FieldSet({ } FieldSet.val = function (ev, schema) { - const input = ev.target + const input = ev.target as HTMLInputElement + if (ev.target.tagName !== 'INPUT') { ev.stopPropagation() return undefined } + // Hm.. looks like premature optimization // should we inline this? - const fieldset = input.parentNode.parentNode.parentNode - const result = [].reduce.call( - fieldset.querySelectorAll('input'), - (res, inp, i) => (!inp.checked ? res : [enumSchema(schema, i), ...res]), - [] - ) + + const fieldset = input?.parentNode?.parentNode?.parentNode + const inputCollection = fieldset?.querySelectorAll('input') + let result + + if (inputCollection?.length) { + result = Array.from(inputCollection).reduce( + (res, inp: HTMLInputElement, i) => + !inp.checked ? res : [enumSchema(schema, i), ...res], + + [] as HTMLInputElement[] + ) + } + return input.type === 'radio' ? result[0] : result } @@ -261,7 +296,8 @@ function multipleSelectCtrl(component, schema, onChange) { } } -function ctrlMap(component, { schema, multiple, onChange }) { +function ctrlMap(component, props: Props) { + const { schema, multiple, onChange } = props if ( !schema || (!schema.enum && !schema.items && !Array.isArray(schema)) || @@ -275,11 +311,8 @@ function ctrlMap(component, { schema, multiple, onChange }) { return singleSelectCtrl(component, schema, onChange) } -function componentMap({ schema, type, multiple }) { - if (schema?.type === 'boolean' && schema?.description === 'slider') { - return Slider - } - +function componentMap(props: Props) { + const { schema, type, multiple } = props if (!schema || (!schema.enum && !schema.items && !Array.isArray(schema))) { if (type === 'checkbox' || (schema && schema.type === 'boolean')) { return CheckBox @@ -287,35 +320,53 @@ function componentMap({ schema, type, multiple }) { return type === 'textarea' ? TextArea : GenericInput } + + if (schema?.type === 'boolean' && schema?.description === 'slider') { + return Slider + } + if (multiple || schema.type === 'array') return type === 'checkbox' ? FieldSet : Select return type === 'radio' ? FieldSet : Select } -function shallowCompare(a, b) { - for (const key in a) { - if (!(key in b)) return true - } - for (const key in b) { - if (a[key] !== b[key]) return true +class Input extends PureComponent< + Props & { innerRef: React.Ref } +> { + component: any + ctrl: { + type?: string + onChange?: (val: any) => void + onSelect?: (ev, values) => void + selected?: (testVal: any, value: any) => boolean + multiple?: boolean } - return false -} -export default class Input extends Component { - shouldComponentUpdate({ children, onChange, style, ...nextProps }) { - const oldProps = omit(this.props, ['children', 'onChange', 'style']) - return shallowCompare(oldProps, nextProps) + constructor(props: Props & { innerRef: React.Ref }) { + super(props) + this.component = props.component || componentMap(props) + this.ctrl = ctrlMap(this.component, props) } render() { - const { component } = this.props - this.component = component || componentMap(this.props) - this.ctrl = ctrlMap(this.component, this.props) - - const { children, onChange, ...props } = this.props + const { children, onChange, ...restProps } = this.props const Component = this.component - return + + const ComponentWithRef = React.forwardRef((props, ref) => ( + + )) + + return ( + + ) } } + +export default React.forwardRef((props: Props, ref) => ( + +)) diff --git a/packages/ketcher-react/src/script/ui/component/structrender.tsx b/packages/ketcher-react/src/script/ui/component/structrender.tsx index ddc47664b9..44844f5640 100644 --- a/packages/ketcher-react/src/script/ui/component/structrender.tsx +++ b/packages/ketcher-react/src/script/ui/component/structrender.tsx @@ -15,54 +15,8 @@ ***************************************************************************/ import { Component, ComponentType, createRef } from 'react' -import { MolSerializer, Render, Struct } from 'ketcher-core' - -/** - * for S-Groups we want to show expanded structure - * without brackets - */ -function prepareStruct(struct: Struct) { - if (struct.sgroups.size > 0) { - const newStruct = struct.clone() - newStruct.sgroups.delete(0) - return newStruct - } - return struct -} - -/** - * Is used to improve search and opening tab performance in Template Dialog - * Rendering a lot of structures causes great delay - */ -const renderCache = new Map() - -function renderStruct( - el: HTMLElement | null, - struct: Struct | null, - options: any = {} -) { - if (el && struct) { - const { cachePrefix = '' } = options - const cacheKey = `${cachePrefix}${struct.name}` - if (renderCache.has(cacheKey)) { - el.innerHTML = renderCache.get(cacheKey) - return - } - const preparedStruct = prepareStruct(struct) - preparedStruct.initHalfBonds() - preparedStruct.initNeighbors() - preparedStruct.setImplicitHydrogen() - preparedStruct.markFragments() - const rnd = new Render(el, { - autoScale: true, - ...options - }) - rnd.setMolecule(preparedStruct) - rnd.update(true, options.viewSz) - renderCache.set(cacheKey, rnd.clientArea.innerHTML) - } -} - +import { MolSerializer, Struct } from 'ketcher-core' +import { RenderStruct } from '../utils' interface StructRenderProps { struct: Struct options: any @@ -100,7 +54,7 @@ class StructRender extends Component { el?.childNodes.forEach((node) => { node.remove() }) - renderStruct(el, parsedStruct, options) + RenderStruct.render(el, parsedStruct, options) } componentDidMount() { diff --git a/packages/ketcher-react/src/script/ui/dialog/template/TemplateDialog.tsx b/packages/ketcher-react/src/script/ui/dialog/template/TemplateDialog.tsx index e6934530a4..17a53f1881 100644 --- a/packages/ketcher-react/src/script/ui/dialog/template/TemplateDialog.tsx +++ b/packages/ketcher-react/src/script/ui/dialog/template/TemplateDialog.tsx @@ -14,14 +14,15 @@ * limitations under the License. ***************************************************************************/ -import { Dispatch, FC, useState, useEffect } from 'react' +import { Dispatch, FC, useState, useEffect, useRef } from 'react' import TemplateTable, { Template } from './TemplateTable' import { changeFilter, changeGroup, deleteTmpl, editTmpl, - selectTmpl + selectTmpl, + changeTab } from '../../state/templates' import { filterLib, filterFGLib, greekify } from '../../utils' import Accordion from '@mui/material/Accordion' @@ -44,6 +45,7 @@ import EmptySearchResult from '../../../ui/dialog/template/EmptySearchResult' import Tabs from '@mui/material/Tabs' import Tab from '@mui/material/Tab' +import useSaltsAndSolvents from './useSaltsAndSolvets' function TabPanel(props) { const { children, value, index, ...other } = props @@ -74,7 +76,7 @@ interface TemplateLibProps { lib: Array