forked from enviPath/enviPy
Current Dev State
This commit is contained in:
70
static/js/ketcher2/script/ui/dialog/about.jsx
Normal file
70
static/js/ketcher2/script/ui/dialog/about.jsx
Normal file
@ -0,0 +1,70 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
|
||||
function About(props) {
|
||||
return (
|
||||
<Dialog title="About"
|
||||
className="about" params={props}
|
||||
buttons={["Close"]}>
|
||||
<a href="http://lifescience.opensource.epam.com/ketcher/" target="_blank">
|
||||
<img src="images/ketcher-logo.svg"/>
|
||||
</a>
|
||||
<dl>
|
||||
<dt>
|
||||
<a href="http://lifescience.opensource.epam.com/ketcher/help.html" target="_blank">Ketcher</a>
|
||||
</dt>
|
||||
<dd>
|
||||
version <var>{props.version}</var>
|
||||
</dd>
|
||||
{
|
||||
props.buildNumber ? (
|
||||
<dd>
|
||||
build #<var>{props.buildNumber}</var>
|
||||
{" at "}
|
||||
<time>{props.buildDate}</time>
|
||||
</dd> ) : null
|
||||
}
|
||||
{
|
||||
props.indigoVersion ? (
|
||||
<div>
|
||||
<dt>
|
||||
<a href="http://lifescience.opensource.epam.com/indigo/" target="_blank">Indigo
|
||||
Toolkit</a>
|
||||
</dt>
|
||||
<dd>version <var>{props.indigoVersion}</var></dd>
|
||||
</div>
|
||||
) : ( <dd>standalone</dd> )
|
||||
}
|
||||
<dt>
|
||||
<a href="http://lifescience.opensource.epam.com/" target="_blank">EPAM Life Sciences</a>
|
||||
</dt>
|
||||
<dd>
|
||||
<a href="http://lifescience.opensource.epam.com/ketcher/#feedback" target="_blank">Feedback</a>
|
||||
</dd>
|
||||
</dl>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({ ...store.options.app })
|
||||
)(About);
|
||||
136
static/js/ketcher2/script/ui/dialog/analyse.jsx
Normal file
136
static/js/ketcher2/script/ui/dialog/analyse.jsx
Normal file
@ -0,0 +1,136 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { range } from 'lodash/fp';
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import keyName from 'w3c-keyname';
|
||||
import Dialog from '../component/dialog';
|
||||
import Input from '../component/input';
|
||||
|
||||
import { changeRound } from '../state/options';
|
||||
import { analyse } from '../state/server';
|
||||
|
||||
function FrozenInput({value}) {
|
||||
return (
|
||||
<input type="text" spellCheck={false} value={value}
|
||||
onKeyDown={ev => allowMovement(ev)}/>
|
||||
);
|
||||
}
|
||||
|
||||
const formulaRegexp = /\b([A-Z][a-z]{0,3})(\d*)\s*\b/g;
|
||||
const errorRegexp = /error:.*/g;
|
||||
|
||||
function formulaInputMarkdown(value) {
|
||||
return (
|
||||
<div className="chem-input" spellCheck={false} contentEditable={true}
|
||||
onKeyDown={ev => allowMovement(ev)}>{value}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormulaInput({value}) {
|
||||
if (errorRegexp.test(value)) {
|
||||
return formulaInputMarkdown(value);
|
||||
}
|
||||
|
||||
const content = [];
|
||||
|
||||
var cnd;
|
||||
var pos = 0;
|
||||
|
||||
while (cnd = formulaRegexp.exec(value)) {
|
||||
content.push(value.substring(pos, cnd.index) + cnd[1]);
|
||||
if (cnd[2].length > 0) content.push(<sub>{cnd[2]}</sub>);
|
||||
pos = cnd.index + cnd[0].length;
|
||||
}
|
||||
|
||||
if (pos === 0) content.push(value);
|
||||
else content.push(value.substring(pos, value.length));
|
||||
|
||||
return formulaInputMarkdown(content);
|
||||
}
|
||||
|
||||
class Analyse extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
props.onAnalyse();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { values, round, onAnalyse, onChangeRound, ...props } = this.props;
|
||||
return (
|
||||
<Dialog title="Calculated Values" className="analyse"
|
||||
buttons={["Close"]} params={props}>
|
||||
<ul>{[
|
||||
{ name: 'Chemical Formula', key: 'gross' },
|
||||
{ name: 'Molecular Weight', key: 'molecular-weight', round: 'roundWeight' },
|
||||
{ name: 'Exact Mass', key: 'monoisotopic-mass', round: 'roundMass' },
|
||||
{ name: 'Elemental Analysis', key: 'mass-composition' }
|
||||
].map(item => (
|
||||
<li>
|
||||
<label>{item.name}:</label>
|
||||
{item.key === 'gross'
|
||||
? <FormulaInput value={values ? values[item.key] : 0}/>
|
||||
: <FrozenInput value={values ? roundOff(values[item.key], round[item.round]) : 0}/>
|
||||
}
|
||||
{item.round
|
||||
? <Input schema={{
|
||||
enum: range(0, 8),
|
||||
enumNames: range(0, 8).map(i => `${i} decimal places`)
|
||||
}} value={round[item.round]} onChange={val => onChangeRound(item.round, val)}/>
|
||||
: null
|
||||
}
|
||||
</li>
|
||||
))
|
||||
}</ul>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function allowMovement(event) {
|
||||
const movementKeys = ['Tab', 'ArrowLeft', 'ArrowRight', 'Home', 'End'];
|
||||
const key = keyName(event);
|
||||
|
||||
if (movementKeys.indexOf(key) === -1)
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function roundOff(value, round) {
|
||||
if (typeof value === 'number')
|
||||
return value.toFixed(round);
|
||||
|
||||
return value.replace(/[0-9]*\.[0-9]+/g, (str) => (
|
||||
(+str).toFixed(round)
|
||||
));
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({
|
||||
values: store.options.analyse.values,
|
||||
round: {
|
||||
roundWeight: store.options.analyse.roundWeight,
|
||||
roundMass: store.options.analyse.roundMass
|
||||
}
|
||||
}),
|
||||
dispatch => ({
|
||||
onAnalyse: () => dispatch(analyse()),
|
||||
onChangeRound: (roundName, val) => dispatch(changeRound(roundName, val))
|
||||
})
|
||||
)(Analyse);
|
||||
78
static/js/ketcher2/script/ui/dialog/atom.jsx
Normal file
78
static/js/ketcher2/script/ui/dialog/atom.jsx
Normal file
@ -0,0 +1,78 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { capitalize } from 'lodash/fp';
|
||||
|
||||
import { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import { atom as atomSchema } from '../structschema';
|
||||
import { Form, Field } from '../component/form';
|
||||
import Dialog from '../component/dialog';
|
||||
|
||||
import element from '../../chem/element';
|
||||
|
||||
function ElementNumber(props, {stateStore}) {
|
||||
let { result } = stateStore.props;
|
||||
return (
|
||||
<label>Number:
|
||||
<input className="number" type="text" readOnly={true}
|
||||
value={element.map[capitalize(result.label)] || ''}/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function Atom(props) {
|
||||
let { formState, ...prop } = props;
|
||||
return (
|
||||
<Dialog title="Atom Properties" className="atom-props"
|
||||
result={() => formState.result} valid={() => formState.valid} params={prop}>
|
||||
<Form schema={atomSchema} customValid={{ label: l => atomValid(l) }}
|
||||
init={prop} {...formState}>
|
||||
<fieldset className="main">
|
||||
<Field name="label"/>
|
||||
<Field name="alias"/>
|
||||
<ElementNumber/>
|
||||
<Field name="charge" maxlength="5"/>
|
||||
<Field name="explicitValence"/>
|
||||
<Field name="isotope"/>
|
||||
<Field name="radical"/>
|
||||
</fieldset>
|
||||
<fieldset className="query">
|
||||
<legend>Query specific</legend>
|
||||
<Field name="ringBondCount"/>
|
||||
<Field name="hCount"/>
|
||||
<Field name="substitutionCount"/>
|
||||
<Field name="unsaturatedAtom"/>
|
||||
</fieldset>
|
||||
<fieldset className="reaction">
|
||||
<legend>Reaction flags</legend>
|
||||
<Field name="invRet"/>
|
||||
<Field name="exactChangeFlag"/>
|
||||
</fieldset>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function atomValid(label) {
|
||||
return label && !!element.map[capitalize(label)];
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(store) => ({ formState: store.modal.form })
|
||||
)(Atom);
|
||||
40
static/js/ketcher2/script/ui/dialog/attach.jsx
Normal file
40
static/js/ketcher2/script/ui/dialog/attach.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import { attachmentPoints as attachmentPointsSchema } from '../structschema';
|
||||
import { Form, Field } from '../component/form';
|
||||
import Dialog from '../component/dialog';
|
||||
|
||||
function AttachmentPoints (props) {
|
||||
let { formState, ...prop} = props;
|
||||
return (
|
||||
<Dialog title="Attachment Points" className="attach-points"
|
||||
result={() => formState.result} valid={() => formState.valid} params={prop}>
|
||||
<Form schema={attachmentPointsSchema} init={prop} {...formState}>
|
||||
<Field name="primary"/>
|
||||
<Field name="secondary"/>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(store) => ({ formState: store.modal.form })
|
||||
)(AttachmentPoints);
|
||||
59
static/js/ketcher2/script/ui/dialog/automap.jsx
Normal file
59
static/js/ketcher2/script/ui/dialog/automap.jsx
Normal file
@ -0,0 +1,59 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import { Form, Field } from '../component/form';
|
||||
import Dialog from '../component/dialog';
|
||||
import { automap } from '../state/server';
|
||||
|
||||
export const automapSchema = {
|
||||
title: "Reaction Auto-Mapping",
|
||||
type: "object",
|
||||
required: ["mode"],
|
||||
properties: {
|
||||
mode: {
|
||||
title: "Mode",
|
||||
enum: ["discard", "keep", "alter", "clear"],
|
||||
enumNames: ["Discard", "Keep", "Alter", "Clear"],
|
||||
default: "discard"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function Automap (props) {
|
||||
let { formState, ...prop} = props;
|
||||
return (
|
||||
<Dialog title="Reaction Auto-Mapping" className="automap"
|
||||
result={() => formState.result} valid={() => formState.valid} params={prop}>
|
||||
<Form schema={automapSchema} {...formState}>
|
||||
<Field name="mode"/>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(store) => ({ formState: store.modal.form }),
|
||||
(dispatch, props) => ({
|
||||
onOk: (res) => {
|
||||
dispatch(automap(res));
|
||||
props.onOk(res);
|
||||
}
|
||||
})
|
||||
)(Automap);
|
||||
41
static/js/ketcher2/script/ui/dialog/bond.jsx
Normal file
41
static/js/ketcher2/script/ui/dialog/bond.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import { bond as bondSchema } from '../structschema';
|
||||
import { Form, Field } from '../component/form';
|
||||
import Dialog from '../component/dialog';
|
||||
|
||||
function Bond(props) {
|
||||
let { formState, ...prop} = props;
|
||||
return (
|
||||
<Dialog title="Bond Properties" className="bond"
|
||||
result={() => formState.result} valid={() => formState.valid} params={prop} >
|
||||
<Form schema={bondSchema} init={prop} {...formState}>
|
||||
<Field name="type"/>
|
||||
<Field name="topology"/>
|
||||
<Field name="center"/>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(store) => ({ formState: store.modal.form })
|
||||
)(Bond);
|
||||
89
static/js/ketcher2/script/ui/dialog/check.jsx
Normal file
89
static/js/ketcher2/script/ui/dialog/check.jsx
Normal file
@ -0,0 +1,89 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
import Tabs from '../component/tabs';
|
||||
import { Form, Field } from '../component/form';
|
||||
import { check } from '../state/server';
|
||||
|
||||
const checkSchema = {
|
||||
title: 'Check',
|
||||
type: 'object',
|
||||
properties: {
|
||||
checkOptions: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: "string",
|
||||
enum: ['valence', 'radicals', 'pseudoatoms', 'stereo', 'query', 'overlapping_atoms',
|
||||
'overlapping_bonds', 'rgroups', 'chiral', '3d'],
|
||||
enumNames: ['Valence', 'Radical', 'Pseudoatom', 'Stereochemistry', 'Query', 'Overlapping Atoms',
|
||||
'Overlapping Bonds', 'R-Groups', 'Chirality', '3D Structure']
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
function getOptionName(opt) {
|
||||
const d = checkSchema.properties.checkOptions.items;
|
||||
return d.enumNames[d.enum.indexOf(opt)];
|
||||
}
|
||||
|
||||
function Check(props) {
|
||||
const tabs = ['Check', 'Settings'];
|
||||
const { formState, onCheck, ...prop } = props;
|
||||
const { result, moleculeErrors } = formState;
|
||||
|
||||
return (
|
||||
<Dialog title="Structure Check" className="check"
|
||||
result={() => result} params={prop}>
|
||||
<Form schema={checkSchema} {...formState}>
|
||||
<Tabs className="tabs" captions={tabs}
|
||||
changeTab={(i) => i === 0 ? onCheck(result.checkOptions) : null}>
|
||||
<ErrorsCheck moleculeErrors={moleculeErrors}/>
|
||||
<Field name="checkOptions" multiple={true} type="checkbox"/>
|
||||
</Tabs>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorsCheck(props) {
|
||||
const { moleculeErrors } = props;
|
||||
const moleculeErrorsTypes = Object.keys(moleculeErrors);
|
||||
|
||||
return (
|
||||
<fieldset {...props}>
|
||||
{moleculeErrorsTypes.length === 0 ?
|
||||
<dt>No errors found</dt> :
|
||||
moleculeErrorsTypes.map(type => (
|
||||
<div>
|
||||
<dt>{getOptionName(type)} error :</dt>
|
||||
<dd>{moleculeErrors[type]}</dd>
|
||||
</div>
|
||||
))}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({ formState: store.modal.form }),
|
||||
dispatch => ({
|
||||
onCheck: (opts) => dispatch(check(opts))
|
||||
})
|
||||
)(Check);
|
||||
134
static/js/ketcher2/script/ui/dialog/generic-groups.jsx
Normal file
134
static/js/ketcher2/script/ui/dialog/generic-groups.jsx
Normal file
@ -0,0 +1,134 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
/** @jsx h */
|
||||
|
||||
import generics from '../../chem/generics';
|
||||
|
||||
const viewSchema = {
|
||||
'atom': {
|
||||
caption: 'Atom Generics',
|
||||
order: ['any', 'no-carbon', 'metal', 'halogen']
|
||||
},
|
||||
'group': {
|
||||
caption: 'Group Generics',
|
||||
order: ['acyclic', 'cyclic']
|
||||
},
|
||||
'special': {
|
||||
caption: 'Special Nodes',
|
||||
order: []
|
||||
},
|
||||
'group/acyclic': {
|
||||
caption: 'Acyclic',
|
||||
order: ['carbo', 'hetero']
|
||||
},
|
||||
'group/cyclic': {
|
||||
caption: 'Cyclic',
|
||||
order: ['no-carbon', 'carbo', 'hetero']
|
||||
},
|
||||
'group/acyclic/carbo': {
|
||||
caption: 'Carbo',
|
||||
order: ['alkynyl', 'alkyl', 'alkenyl']
|
||||
},
|
||||
'group/acyclic/hetero': {
|
||||
caption: 'Hetero',
|
||||
order: ['alkoxy']
|
||||
},
|
||||
'group/cyclic/carbo': {
|
||||
caption: 'Carbo',
|
||||
order: ['aryl', 'cycloalkyl', 'cycloalkenyl']
|
||||
},
|
||||
'group/cyclic/hetero': {
|
||||
caption: 'Hetero',
|
||||
order: ['aryl']
|
||||
},
|
||||
'atom/any': 'any atom',
|
||||
'atom/no-carbon': 'except C or H',
|
||||
'atom/metal': 'any metal',
|
||||
'atom/halogen': 'any halogen',
|
||||
'group/cyclic/no-carbon': 'no carbon',
|
||||
'group/cyclic/hetero/aryl': 'hetero aryl'
|
||||
};
|
||||
|
||||
function GenSet({labels, caption='', selected, onSelect, ...props}) {
|
||||
return (
|
||||
<fieldset {...props}>
|
||||
{
|
||||
labels.map(label => (
|
||||
<button onClick={e => onSelect(label)}
|
||||
className={selected(label) ? 'selected' : ''}>
|
||||
{label}</button>
|
||||
))
|
||||
}
|
||||
{
|
||||
caption ? (
|
||||
<legend>{caption}</legend>
|
||||
) : null
|
||||
}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
function GenGroup({gen, name, path, selected, onSelect}) {
|
||||
const group = gen[name];
|
||||
const pk = path ? `${path}/${name}` : name;
|
||||
const schema = viewSchema[pk];
|
||||
|
||||
return (schema && schema.caption) ? (
|
||||
<fieldset className={name}>
|
||||
<legend>{schema.caption}</legend>
|
||||
{
|
||||
group.labels ? (
|
||||
<GenSet labels={group.labels}
|
||||
selected={selected} onSelect={onSelect} />
|
||||
) : null
|
||||
}
|
||||
{
|
||||
schema.order.map(child => ( // TODO:order = Object.keys ifndef
|
||||
<GenGroup gen={group} name={child} path={pk}
|
||||
selected={selected} onSelect={onSelect}/>
|
||||
))
|
||||
}
|
||||
</fieldset>
|
||||
) : (
|
||||
<GenSet labels={group.labels}
|
||||
caption={schema || name} className={name}
|
||||
selected={selected} onSelect={onSelect} />
|
||||
);
|
||||
}
|
||||
|
||||
function GenericGroups({ selected, onSelect, ...props }) {
|
||||
return (
|
||||
<div summary="Generic Groups" {...props}>
|
||||
<div className="col">
|
||||
<GenGroup gen={generics} name='atom'
|
||||
selected={l => selected(l)}
|
||||
onSelect={l => onSelect(l)}/>
|
||||
<GenGroup gen={generics} name='special'
|
||||
selected={l => selected(l)}
|
||||
onSelect={l => onSelect(l)}/>
|
||||
</div>
|
||||
<div className="col">
|
||||
<GenGroup gen={generics} name='group'
|
||||
selected={l => selected(l)}
|
||||
onSelect={l => onSelect(l)}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenericGroups;
|
||||
32
static/js/ketcher2/script/ui/dialog/help.jsx
Normal file
32
static/js/ketcher2/script/ui/dialog/help.jsx
Normal file
@ -0,0 +1,32 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
/** @jsx h */
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
|
||||
function Help(props) {
|
||||
return (
|
||||
<Dialog title="Help"
|
||||
className="help" params={props}
|
||||
buttons={["Close"]}>
|
||||
<iframe className="help" src="doc/help.html"></iframe>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default Help;
|
||||
64
static/js/ketcher2/script/ui/dialog/index.js
Normal file
64
static/js/ketcher2/script/ui/dialog/index.js
Normal file
@ -0,0 +1,64 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 Open from './open';
|
||||
import Save from './save';
|
||||
import Analyse from './analyse';
|
||||
import Recognize from './recognize';
|
||||
import PeriodTable from './period-table';
|
||||
import Rgroup from './rgroup';
|
||||
import TemplateAttach from './template-attach';
|
||||
import TemplatesLib from './template-lib';
|
||||
import About from './about';
|
||||
import Help from './help';
|
||||
import Miew from './miew';
|
||||
|
||||
// schemify dialogs
|
||||
import Atom from './atom';
|
||||
import AttachPoints from './attach';
|
||||
import Automap from './automap';
|
||||
import Bond from './bond';
|
||||
import Check from './check';
|
||||
import LabelEdit from './labeledit';
|
||||
import RgroupLogic from './rgroup-logic';
|
||||
import Settings from './options';
|
||||
import Sgroup from './sgroup';
|
||||
import Sdata from './sdata';
|
||||
|
||||
export default {
|
||||
open: Open,
|
||||
save: Save,
|
||||
analyse: Analyse,
|
||||
recognize: Recognize,
|
||||
'period-table': PeriodTable,
|
||||
rgroup: Rgroup,
|
||||
attach: TemplateAttach,
|
||||
templates: TemplatesLib,
|
||||
about: About,
|
||||
help: Help,
|
||||
miew: Miew,
|
||||
|
||||
atomProps: Atom,
|
||||
attachmentPoints: AttachPoints,
|
||||
automap: Automap,
|
||||
bondProps: Bond,
|
||||
check: Check,
|
||||
labelEdit: LabelEdit,
|
||||
rgroupLogic: RgroupLogic,
|
||||
settings: Settings,
|
||||
sgroup: Sgroup,
|
||||
sdata: Sdata
|
||||
};
|
||||
96
static/js/ketcher2/script/ui/dialog/labeledit.jsx
Normal file
96
static/js/ketcher2/script/ui/dialog/labeledit.jsx
Normal file
@ -0,0 +1,96 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { capitalize } from 'lodash/fp';
|
||||
|
||||
import { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import element from '../../chem/element';
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
import { Form, Field } from '../component/form';
|
||||
|
||||
export const labelEditSchema = {
|
||||
title: "Label Edit",
|
||||
type: "object",
|
||||
required: ["label"],
|
||||
properties: {
|
||||
label: {
|
||||
title: "Atom",
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function serialize(lc) {
|
||||
const charge = Math.abs(lc.charge);
|
||||
const radical = ['', ':', '.', '^^'][lc.radical] || '';
|
||||
let sign = '';
|
||||
if (charge)
|
||||
sign = lc.charge < 0 ? '-' : '+';
|
||||
return (lc.isotope || '') + lc.label + radical +
|
||||
(charge > 1 ? charge: '') + sign;
|
||||
}
|
||||
|
||||
function deserialize(value) {
|
||||
const match = value.match(/^(\d+)?([a-z*]{1,3})(\.|:|\^\^)?(\d+[-+]|[-+])?$/i); // TODO: radical on last place
|
||||
if (match) {
|
||||
const label = match[2] === '*' ? 'A' : capitalize(match[2]);
|
||||
let charge = 0;
|
||||
let isotope = 0;
|
||||
let radical = 0;
|
||||
|
||||
if (match[1])
|
||||
isotope = parseInt(match[1]);
|
||||
|
||||
if (match[3])
|
||||
radical = { ':': 1, '.': 2, '^^': 3 }[match[3]];
|
||||
|
||||
if (match[4]) {
|
||||
charge = parseInt(match[4]);
|
||||
if (isNaN(charge)) // NaN => [-+]
|
||||
charge = 1;
|
||||
if (match[4].endsWith('-'))
|
||||
charge = -charge;
|
||||
}
|
||||
// Not consistant
|
||||
if (label === 'A' || label === 'Q' || label === 'X' || label === 'M' || element.map[label])
|
||||
return { label, charge, isotope, radical };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function LabelEdit(props) {
|
||||
const init = { label: props.letter || serialize(props) };
|
||||
const { formState, ...prop} = props;
|
||||
const { result, valid } = formState;
|
||||
|
||||
return (
|
||||
<Dialog title="Label Edit" className="labeledit" valid={() => valid}
|
||||
result={() => deserialize(result.label)} params={prop}>
|
||||
<Form schema={labelEditSchema} customValid={{label: l => deserialize(l)}}
|
||||
init={init} {...formState}>
|
||||
<Field name="label" maxlength="20" size="10"/>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(store) => ({ formState: store.modal.form })
|
||||
)(LabelEdit);
|
||||
223
static/js/ketcher2/script/ui/dialog/miew.jsx
Normal file
223
static/js/ketcher2/script/ui/dialog/miew.jsx
Normal file
@ -0,0 +1,223 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { camelCase } from 'lodash/fp';
|
||||
import { h, Component } from 'preact';
|
||||
/** @jsx h */
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
import { storage } from '../utils';
|
||||
|
||||
const MIEW_PATH = '__MIEW_PATH__';
|
||||
const MIEW_OPTIONS = {
|
||||
preset: 'small',
|
||||
settings: {
|
||||
theme: 'light',
|
||||
atomLabel: 'bright',
|
||||
autoPreset: false,
|
||||
inversePanning: true
|
||||
},
|
||||
reps: [{
|
||||
mode: 'LN',
|
||||
colorer: 'AT',
|
||||
selector: 'all'
|
||||
}]
|
||||
};
|
||||
|
||||
const MIEW_WINDOW = {
|
||||
location: 'no',
|
||||
menubar: 'no',
|
||||
toolbar: 'no',
|
||||
directories: 'no',
|
||||
modal: 'yes',
|
||||
alwaysRaised: 'yes'
|
||||
};
|
||||
|
||||
const MIEW_MODES = {
|
||||
'lines': 'LN',
|
||||
'ballsAndSticks': 'BS',
|
||||
'licorice': 'LC'
|
||||
};
|
||||
|
||||
function getLocalMiewOpts() {
|
||||
let userOpts = storage.getItem("ketcher-opts");
|
||||
if (!userOpts)
|
||||
return MIEW_OPTIONS;
|
||||
|
||||
const opts = MIEW_OPTIONS;
|
||||
|
||||
if (userOpts.miewTheme)
|
||||
opts.settings.theme = camelCase(userOpts.miewTheme);
|
||||
|
||||
if (userOpts.miewAtomLabel)
|
||||
opts.settings.atomLabel = camelCase(userOpts.miewAtomLabel);
|
||||
|
||||
if (userOpts.miewMode)
|
||||
opts.reps[0].mode = MIEW_MODES[camelCase(userOpts.miewMode)];
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
function origin (url) {
|
||||
let loc = url;
|
||||
|
||||
if (!loc.href) {
|
||||
loc = document.createElement('a');
|
||||
loc.href = url;
|
||||
}
|
||||
|
||||
if (loc.origin)
|
||||
return loc.origin;
|
||||
|
||||
if (!loc.hostname) // relative url, IE
|
||||
loc = document.location;
|
||||
|
||||
return loc.protocol + '//' + loc.hostname +
|
||||
(!loc.port ? '' : ':' + loc.port);
|
||||
}
|
||||
|
||||
function queryOptions(options, sep='&') {
|
||||
if (Array.isArray(options)) {
|
||||
return options.reduce((res, item) => {
|
||||
let value = queryOptions(item);
|
||||
if (value !== null)
|
||||
res.push(value);
|
||||
return res;
|
||||
}, []).join(sep);
|
||||
} else if (typeof options === 'object') {
|
||||
return Object.keys(options).reduce((res, item) => {
|
||||
let value = options[item];
|
||||
res.push(typeof value === 'object' ?
|
||||
queryOptions(value) :
|
||||
encodeURIComponent(item) + '=' +
|
||||
encodeURIComponent(value));
|
||||
return res;
|
||||
}, []).join(sep);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function miewLoad(wnd, url, options={}) { // TODO: timeout
|
||||
return new Promise(function (resolve, reject) {
|
||||
addEventListener('message', function onload(event) {
|
||||
if (event.origin === origin(url) && event.data === 'miewLoadComplete') {
|
||||
window.removeEventListener('message', onload);
|
||||
let miew = wnd.MIEWS[0];
|
||||
miew._opts.load = false; // setOptions({ load: '' })
|
||||
miew._menuDisabled = true; // no way to disable menu after constructor return
|
||||
if (miew.init()) {
|
||||
miew.setOptions(options);
|
||||
miew.benchmarkGfx().then(() => {
|
||||
miew.run();
|
||||
setTimeout(() => resolve(miew), 10);
|
||||
// see setOptions message handler
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function miewSave(miew, url) {
|
||||
miew.saveData();
|
||||
return new Promise(function (resolve, reject) {
|
||||
addEventListener('message', function onsave(event) {
|
||||
if (event.origin === origin(url) && event.data.startsWith('CML:')) {
|
||||
window.removeEventListener('message', onsave);
|
||||
resolve(atob(event.data.slice(4)));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class Miew extends Component {
|
||||
constructor(props) {
|
||||
console.info('init');
|
||||
super(props);
|
||||
this.opts = getLocalMiewOpts();
|
||||
}
|
||||
load(ev) {
|
||||
let miew = miewLoad(ev.target.contentWindow,
|
||||
MIEW_PATH, this.opts);
|
||||
this.setState({ miew });
|
||||
this.state.miew.then(miew => {
|
||||
miew.parse(this.props.structStr, {
|
||||
fileType: 'cml',
|
||||
loaded: true
|
||||
});
|
||||
this.setState({ miew });
|
||||
});
|
||||
}
|
||||
save(ev) {
|
||||
if (this.props.onOk) {
|
||||
let structStr = miewSave(this.state.miew, MIEW_PATH);
|
||||
this.setState({ structStr });
|
||||
this.state.structStr.then(structStr => {
|
||||
this.props.onOk({ structStr });
|
||||
});
|
||||
}
|
||||
}
|
||||
window() {
|
||||
let opts = {
|
||||
...this.opts,
|
||||
load: `CML:${btoa(this.props.structStr)}`,
|
||||
sourceType: 'message'
|
||||
};
|
||||
let br = this.base.getBoundingClientRect(); // Preact specifiec
|
||||
// see: epa.ms/1NAYWp
|
||||
let wndProps = {
|
||||
...MIEW_WINDOW,
|
||||
top: Math.round(br.top),
|
||||
left: Math.round(br.left),
|
||||
width: Math.round(br.width),
|
||||
height: Math.round(br.height)
|
||||
};
|
||||
let wnd = window.open(`${MIEW_PATH}?${queryOptions(opts)}`,
|
||||
'miew', queryOptions(wndProps, ','));
|
||||
if (wnd) {
|
||||
this.props.onCancel && this.props.onCancel();
|
||||
wnd.onload = function () {
|
||||
console.info('windowed');
|
||||
};
|
||||
}
|
||||
}
|
||||
render(props) {
|
||||
let {miew, structStr} = this.state;
|
||||
return (
|
||||
<Dialog title="3D View"
|
||||
className="miew" params={props}
|
||||
buttons={[
|
||||
"Close",
|
||||
<button disabled={miew instanceof Promise || structStr instanceof Promise}
|
||||
onClick={ ev => this.save(ev) }>
|
||||
Apply
|
||||
</button>,
|
||||
<button className="window"
|
||||
disabled={/MSIE|rv:11/i.test(navigator.userAgent)}
|
||||
onClick={ ev => this.window() }>
|
||||
Detach to new window
|
||||
</button>
|
||||
]}>
|
||||
<iframe id="miew-iframe"
|
||||
src={MIEW_PATH}
|
||||
onLoad={ev => this.load(ev) }></iframe>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Miew;
|
||||
95
static/js/ketcher2/script/ui/dialog/open.jsx
Normal file
95
static/js/ketcher2/script/ui/dialog/open.jsx
Normal file
@ -0,0 +1,95 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h, Component } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import { map as formatMap } from '../structformat';
|
||||
import Dialog from '../component/dialog';
|
||||
import OpenButton from '../component/openbutton';
|
||||
import ClipArea, { exec } from '../component/cliparea';
|
||||
|
||||
import { load } from '../state';
|
||||
|
||||
class Open extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
structStr: '',
|
||||
fragment: false
|
||||
};
|
||||
}
|
||||
result() {
|
||||
let { structStr, fragment } = this.state;
|
||||
return structStr ? { structStr, fragment } : null;
|
||||
}
|
||||
changeStructStr(structStr) {
|
||||
this.setState({ structStr });
|
||||
}
|
||||
changeFragment(target) {
|
||||
this.setState({
|
||||
fragment: target.checked
|
||||
});
|
||||
}
|
||||
render () {
|
||||
let { structStr, fragment } = this.state;
|
||||
return (
|
||||
<Dialog title="Open Structure"
|
||||
className="open" result={() => this.result()}
|
||||
params={this.props}
|
||||
buttons={[(
|
||||
<OpenButton className="open" server={this.props.server}
|
||||
type={structAcceptMimes()}
|
||||
onLoad={s => this.changeStructStr(s)}>
|
||||
Open From File…
|
||||
</OpenButton>
|
||||
), "Cancel", "OK"]}>
|
||||
<textarea value={structStr}
|
||||
onInput={ev => this.changeStructStr(ev.target.value)}/>
|
||||
<label>
|
||||
<input type="checkbox" checked={fragment}
|
||||
onClick={ev => this.changeFragment(ev.target)}/>
|
||||
Load as a fragment and copy to the Clipboard
|
||||
</label>
|
||||
<ClipArea focused={() => true}
|
||||
onCopy={() => ({ 'text/plain': structStr })}/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function structAcceptMimes() {
|
||||
return Object.keys(formatMap).reduce((res, key) => (
|
||||
res.concat(formatMap[key].mime, ...formatMap[key].ext)
|
||||
), []).join(',');
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({ server: store.server }),
|
||||
(dispatch, props) => ({
|
||||
onOk: (res) => {
|
||||
if (res.fragment) exec('copy');
|
||||
dispatch(
|
||||
load(res.structStr, {
|
||||
badHeaderRecover: true,
|
||||
fragment: res.fragment
|
||||
})
|
||||
);
|
||||
props.onOk(res);
|
||||
}
|
||||
})
|
||||
)(Open);
|
||||
129
static/js/ketcher2/script/ui/dialog/options.jsx
Normal file
129
static/js/ketcher2/script/ui/dialog/options.jsx
Normal file
@ -0,0 +1,129 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
import { updateFormState, setDefaultSettings } from '../state/form';
|
||||
import { saveSettings } from '../state/options';
|
||||
|
||||
import settingsSchema from '../data/options-schema';
|
||||
import { Form, Field } from '../component/form';
|
||||
import { storage } from '../utils';
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
import Accordion from '../component/accordion';
|
||||
import SystemFonts from '../component/systemfonts';
|
||||
import SaveButton from '../component/savebutton';
|
||||
import OpenButton from '../component/openbutton';
|
||||
import MeasureInput from '../component/measure-input';
|
||||
|
||||
function Settings(props) {
|
||||
const { initState, formState, server, onOpenFile, onReset, appOpts, ...prop } = props;
|
||||
const tabs = ['Rendering customization options', 'Atoms', 'Bonds', 'Server', '3D Viewer', 'Options for debugging'];
|
||||
const activeTabs = { 0: true, 1: false, 2: false, 3: false, 4: false, 5: false };
|
||||
|
||||
return (
|
||||
<Dialog title="Settings" className="settings"
|
||||
result={() => formState.result} valid={() => formState.valid} params={prop}
|
||||
buttons={[
|
||||
<OpenButton className="open" server={ server } onLoad={ onOpenFile }>
|
||||
Open From File…
|
||||
</OpenButton>,
|
||||
<SaveButton className="save" data={JSON.stringify(formState.result)} filename={'ketcher-settings'}>
|
||||
Save To File…
|
||||
</SaveButton>,
|
||||
<button onClick={ onReset }>Reset</button>,
|
||||
"OK", "Cancel"]} >
|
||||
<Form schema={settingsSchema} init={initState} {...formState}>
|
||||
<Accordion className="accordion" captions={tabs} active={activeTabs}>
|
||||
<fieldset className="render">
|
||||
<Field name="resetToSelect"/>
|
||||
<Field name="rotationStep"/>
|
||||
<SelectCheckbox name="showValenceWarnings"/>
|
||||
<SelectCheckbox name="atomColoring"/>
|
||||
<SelectCheckbox name="hideChiralFlag"/>
|
||||
<Field name="font" component={SystemFonts}/>
|
||||
<FieldMeasure name="fontsz"/>
|
||||
<FieldMeasure name="fontszsub"/>
|
||||
</fieldset>
|
||||
<fieldset className="atoms">
|
||||
<SelectCheckbox name="carbonExplicitly"/>
|
||||
<SelectCheckbox name="showCharge"/>
|
||||
<SelectCheckbox name="showValence"/>
|
||||
<Field name="showHydrogenLabels"/>
|
||||
</fieldset>
|
||||
<fieldset className="bonds">
|
||||
<SelectCheckbox name="aromaticCircle"/>
|
||||
<FieldMeasure name="doubleBondWidth"/>
|
||||
<FieldMeasure name="bondThickness"/>
|
||||
<FieldMeasure name="stereoBondWidth"/>
|
||||
</fieldset>
|
||||
<fieldset className="server" disabled={!appOpts.server}>
|
||||
<SelectCheckbox name="smart-layout"/>
|
||||
<SelectCheckbox name="ignore-stereochemistry-errors"/>
|
||||
<SelectCheckbox name="mass-skip-error-on-pseudoatoms"/>
|
||||
<SelectCheckbox name="gross-formula-add-rsites"/>
|
||||
</fieldset>
|
||||
<fieldset className="3dView" disabled={!appOpts.miewPath}>
|
||||
<Field name="miewMode"/>
|
||||
<Field name="miewTheme"/>
|
||||
<Field name="miewAtomLabel"/>
|
||||
</fieldset>
|
||||
<fieldset className="debug">
|
||||
<SelectCheckbox name="showAtomIds"/>
|
||||
<SelectCheckbox name="showBondIds"/>
|
||||
<SelectCheckbox name="showHalfBondIds"/>
|
||||
<SelectCheckbox name="showLoopIds"/>
|
||||
</fieldset>
|
||||
</Accordion>
|
||||
{ !storage.isAvailable() ? <div className="warning">{storage.warningMessage}</div> : null }
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectCheckbox(props, {schema}) {
|
||||
const desc = {
|
||||
title: schema.properties[props.name].title,
|
||||
enum: [true, false],
|
||||
enumNames: ['on', 'off'],
|
||||
};
|
||||
return <Field schema={desc} {...props}/>;
|
||||
}
|
||||
|
||||
function FieldMeasure(props, {schema}) {
|
||||
return <Field schema={schema.properties[props.name]} component={MeasureInput} {...props}/>
|
||||
}
|
||||
|
||||
export default connect(store => ({
|
||||
appOpts: store.options.app,
|
||||
initState: store.options.settings,
|
||||
formState: store.modal.form
|
||||
}), (dispatch, props) => ({
|
||||
onOpenFile: newOpts => {
|
||||
try {
|
||||
dispatch(updateFormState({ result: JSON.parse(newOpts) }));
|
||||
} catch (ex) {
|
||||
console.info('Bad file');
|
||||
}
|
||||
},
|
||||
onReset: () => dispatch(setDefaultSettings()),
|
||||
onOk: (res) => {
|
||||
dispatch(saveSettings(res));
|
||||
props.onOk(res);
|
||||
}
|
||||
}))(Settings);
|
||||
272
static/js/ketcher2/script/ui/dialog/period-table.jsx
Normal file
272
static/js/ketcher2/script/ui/dialog/period-table.jsx
Normal file
@ -0,0 +1,272 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { range } from 'lodash/fp';
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import element from '../../chem/element';
|
||||
import Dialog from '../component/dialog';
|
||||
import Atom from '../component/atom';
|
||||
import Tabs from '../component/tabs';
|
||||
|
||||
import GenericGroups from './generic-groups';
|
||||
|
||||
import { fromElement, toElement } from '../structconv';
|
||||
import { onAction } from '../state';
|
||||
import { addAtoms } from '../state/toolbar';
|
||||
|
||||
const typeSchema = [
|
||||
{ title: 'Single', value: 'atom' },
|
||||
{ title: 'List', value: 'list'},
|
||||
{ title: 'Not List', value: 'not-list'}
|
||||
];
|
||||
|
||||
const beforeSpan = {
|
||||
'He': 16,
|
||||
'B': 10,
|
||||
'Al': 10,
|
||||
'Hf': 1,
|
||||
'Rf': 1
|
||||
};
|
||||
|
||||
const main = rowPartition(element.filter(el => el && el.type !== 'actinide' &&
|
||||
el.type !== 'lanthanide'));
|
||||
const lanthanides = element.filter(el => el && el.type === 'lanthanide');
|
||||
const actinides = element.filter(el => el && el.type === 'actinide');
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<tr>
|
||||
{
|
||||
range(0, 19).map(i => (
|
||||
<th>{i || ''}</th>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function TypeChoise({value, onChange, ...props}) {
|
||||
return (
|
||||
<fieldset>
|
||||
{
|
||||
typeSchema.map(sc => (
|
||||
<label>
|
||||
<input type="radio" value={sc.value}
|
||||
checked={sc.value === value}
|
||||
onClick={ev => onChange(sc.value) } {...props}/>
|
||||
{sc.title}
|
||||
</label>
|
||||
))
|
||||
}
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
|
||||
function MainRow({row, caption, refer, selected, onSelect, curEvents}) {
|
||||
return (
|
||||
<tr>
|
||||
<th>{caption}</th>
|
||||
{
|
||||
row.map(el => (typeof el !== 'number') ? (
|
||||
<td>
|
||||
<Atom el={el}
|
||||
className={selected(el.label) ? 'selected' : ''}
|
||||
onClick={ev => onSelect(el.label)} {...curEvents(el)}/>
|
||||
</td>
|
||||
) : (
|
||||
refer(el) ? ( <td className="ref">{refer(el)}</td> ) :
|
||||
( <td colspan={el}/> )
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function OutinerRow({row, caption, selected, onSelect, curEvents}) {
|
||||
return (
|
||||
<tr>
|
||||
<th colspan="3" className="ref">{caption}</th>
|
||||
{
|
||||
row.map(el => (
|
||||
<td>
|
||||
<Atom el={el}
|
||||
className={selected(el.label) ? 'selected' : ''}
|
||||
onClick={ev => onSelect(el.label)} {...curEvents(el)}/>
|
||||
</td>
|
||||
))
|
||||
}
|
||||
<td></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function AtomInfo({el, isInfo}) {
|
||||
const numberStyle = { color: el.color || 'black', 'font-size': '1.2em' };
|
||||
const elemStyle = { color: el.color || 'black', 'font-weight': 'bold', 'font-size': '2em' };
|
||||
return (
|
||||
<div className={`atom-info ${isInfo ? '' : 'none'}`}>
|
||||
<div style={numberStyle}>{element.map[el.label]}</div>
|
||||
<span style={elemStyle}>{el.label}</span><br/>
|
||||
{el.title}<br/>
|
||||
{el.atomic_mass}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
class PeriodTable extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let genType = !!this.props.pseudo ? 'gen' : null;
|
||||
this.state = {
|
||||
type: props.type || genType || 'atom',
|
||||
value: props.values || props.label || null,
|
||||
cur: element[2],
|
||||
isInfo: false
|
||||
};
|
||||
this.firstType = true;
|
||||
}
|
||||
changeType(type) {
|
||||
if (this.firstType)
|
||||
return this.firstType = false;
|
||||
let pl = this.state.type === 'list' || this.state.type === 'not-list';
|
||||
let l = type === 'list' || type === 'not-list';
|
||||
if (l && pl)
|
||||
this.setState({type});
|
||||
else
|
||||
this.setState({
|
||||
type,
|
||||
value: type === 'atom' || type === 'gen' ? null : []
|
||||
});
|
||||
}
|
||||
selected(label) {
|
||||
let {type, value} = this.state;
|
||||
return (type === 'atom' || type === 'gen') ? value === label :
|
||||
value.includes(label);
|
||||
}
|
||||
onSelect(label) {
|
||||
let {type, value} = this.state;
|
||||
if (type === 'atom' || type === 'gen')
|
||||
this.setState({ value: label });
|
||||
else {
|
||||
let i = value.indexOf(label);
|
||||
if (i < 0)
|
||||
value.push(label);
|
||||
else
|
||||
value.splice(i, 1);
|
||||
this.setState({ value });
|
||||
}
|
||||
}
|
||||
result() {
|
||||
let {type, value} = this.state;
|
||||
if (type === 'atom')
|
||||
return value ? { label: value, pseudo: null } : null;
|
||||
else if (type === 'gen')
|
||||
return value ? { type, label: value, pseudo: value} : null;
|
||||
else
|
||||
return value.length ? { type, values: value } : null;
|
||||
}
|
||||
curEvents = (el) => {
|
||||
return {
|
||||
onMouseEnter: () => this.setState({ cur: el, isInfo: true }),
|
||||
onMouseLeave: () => this.setState({ isInfo: false })
|
||||
};
|
||||
};
|
||||
render () {
|
||||
const tabs = ['Table', 'Extended'];
|
||||
let { type } = this.state;
|
||||
return (
|
||||
<Dialog title="Periodic table" className="elements-table"
|
||||
params={this.props} result={() => this.result()}>
|
||||
<Tabs className="tabs" captions={tabs} tabIndex={type !== 'gen' ? 0 : 1}
|
||||
changeTab={(i) => this.changeType(i === 0 ? 'atom' : 'gen')}>
|
||||
<div className="period-table">
|
||||
<table summary="Periodic table of the chemical elements">
|
||||
<Header/>
|
||||
<AtomInfo el={this.state.cur} isInfo={this.state.isInfo}/>
|
||||
{
|
||||
main.map((row, i) => (
|
||||
<MainRow row={row} caption={i + 1}
|
||||
refer={o => o === 1 && (i === 5 ? '*' : '**')}
|
||||
curEvents={this.curEvents}
|
||||
selected={l => this.selected(l)}
|
||||
onSelect={l => this.onSelect(l)}/>
|
||||
))
|
||||
}
|
||||
<OutinerRow row={lanthanides} caption="*"
|
||||
curEvents={this.curEvents}
|
||||
selected={l => this.selected(l)}
|
||||
onSelect={l => this.onSelect(l)}/>
|
||||
<OutinerRow row={actinides} caption="**"
|
||||
curEvents={this.curEvents}
|
||||
selected={l => this.selected(l)}
|
||||
onSelect={l => this.onSelect(l)}/>
|
||||
</table>
|
||||
<TypeChoise value={type}
|
||||
onChange={t => this.changeType(t) }/>
|
||||
</div>
|
||||
<GenericGroups className="generic-groups"
|
||||
selected={this.selected.bind(this)}
|
||||
onSelect={this.onSelect.bind(this)}/>
|
||||
</Tabs>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function rowPartition(elements) {
|
||||
return elements.reduce(function (res, el) {
|
||||
let row = res[el.period - 1];
|
||||
if (!row)
|
||||
res.push([el]);
|
||||
else {
|
||||
if (beforeSpan[el.label])
|
||||
row.push(beforeSpan[el.label]);
|
||||
row.push(el);
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function mapSelectionToProps(editor) {
|
||||
const selection = editor.selection();
|
||||
|
||||
if (selection && Object.keys(selection).length === 1 &&
|
||||
selection.atoms && Object.keys(selection.atoms).length === 1) {
|
||||
let struct = editor.struct();
|
||||
let atom = struct.atoms.get(selection.atoms[0]);
|
||||
return { ...fromElement(atom) }
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export default connect(
|
||||
(store, props) => {
|
||||
if (props.values || props.label) return {};
|
||||
return mapSelectionToProps(store.editor);
|
||||
},
|
||||
(dispatch, props) => ({
|
||||
onOk: (res) => {
|
||||
if (!res.type || res.type === 'atom') dispatch(addAtoms(res.label));
|
||||
dispatch(onAction({ tool: 'atom', opts: toElement(res) }));
|
||||
props.onOk(res);
|
||||
}
|
||||
})
|
||||
)(PeriodTable);
|
||||
104
static/js/ketcher2/script/ui/dialog/recognize.jsx
Normal file
104
static/js/ketcher2/script/ui/dialog/recognize.jsx
Normal file
@ -0,0 +1,104 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import { changeImage, shouldFragment } from '../state/options';
|
||||
import { load } from '../state';
|
||||
import { recognize } from '../state/server';
|
||||
import Dialog from '../component/dialog';
|
||||
import Input from '../component/input';
|
||||
import StructRender from '../component/structrender';
|
||||
import OpenButton from '../component/openbutton';
|
||||
import Spin from '../component/spin';
|
||||
|
||||
function Recognize(prop) {
|
||||
const {file, structStr, fragment, onRecognize, isFragment, onImage, ...props} = prop;
|
||||
|
||||
const result = () =>
|
||||
structStr && !(structStr instanceof Promise) ? {structStr, fragment} : null;
|
||||
|
||||
return (
|
||||
<Dialog title="Import From Image" className="recognize"
|
||||
params={props} result={() => result(structStr, fragment) }
|
||||
buttons={[
|
||||
<OpenButton className="open" onLoad={onImage} type="image/*">
|
||||
Choose file…
|
||||
</OpenButton>,
|
||||
<span className="open-filename">{file ? file.name : null}</span>,
|
||||
file && !structStr ? (
|
||||
<button onClick={() => onRecognize(file) }>Recognize</button>
|
||||
) : null,
|
||||
"Cancel",
|
||||
"OK"
|
||||
]}>
|
||||
<div className="picture">
|
||||
{
|
||||
file ? (
|
||||
<img id="pic" src={url(file) || ""}
|
||||
onError={() => {
|
||||
onImage(null);
|
||||
alert("Error, it isn't a picture");
|
||||
}}/>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
<div className="output">
|
||||
{
|
||||
structStr ? (
|
||||
structStr instanceof Promise || typeof structStr !== 'string' ? // in Edge 38:
|
||||
( <Spin/> ) : // instanceof Promise always `false`
|
||||
( <StructRender className="struct" struct={structStr}/> )
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
<label>
|
||||
<Input type="checkbox" value={fragment} onChange={v => isFragment(v)}/>
|
||||
Load as a fragment
|
||||
</label>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function url(file) {
|
||||
if (!file) return null;
|
||||
const URL = window.URL || window.webkitURL;
|
||||
return URL ? URL.createObjectURL(file) : "No preview";
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({
|
||||
file: store.options.recognize.file,
|
||||
structStr: store.options.recognize.structStr,
|
||||
fragment: store.options.recognize.fragment
|
||||
}),
|
||||
(dispatch, props) => ({
|
||||
isFragment: (v) => dispatch(shouldFragment(v)),
|
||||
onImage: (file) => dispatch(changeImage(file)),
|
||||
onRecognize: (file) => dispatch(recognize(file)),
|
||||
onOk: (res) => {
|
||||
dispatch(
|
||||
load(res.structStr, {
|
||||
rescale: true,
|
||||
fragment: res.fragment
|
||||
})
|
||||
);
|
||||
props.onOk(res);
|
||||
}
|
||||
})
|
||||
)(Recognize);
|
||||
72
static/js/ketcher2/script/ui/dialog/rgroup-logic.jsx
Normal file
72
static/js/ketcher2/script/ui/dialog/rgroup-logic.jsx
Normal file
@ -0,0 +1,72 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import { rgroup as rgroupSchema } from '../structschema';
|
||||
import { Form, Field } from '../component/form';
|
||||
import Dialog from '../component/dialog';
|
||||
|
||||
function IfThenSelect(props, { schema }) {
|
||||
const { name, rgids } = props;
|
||||
|
||||
const desc = {
|
||||
title: schema.properties[name].title,
|
||||
enum: [0],
|
||||
enumNames: ['Always']
|
||||
};
|
||||
|
||||
rgids.forEach(label => {
|
||||
if (props.label !== label) {
|
||||
desc.enum.push(label);
|
||||
desc.enumNames.push(`IF R${props.label} THEN R${label}`);
|
||||
}
|
||||
});
|
||||
|
||||
return <Field name={name} schema={desc} {...props}/>;
|
||||
}
|
||||
|
||||
function RgroupLogic (props) {
|
||||
const { formState, label, rgroupLabels, ...prop } = props;
|
||||
|
||||
return (
|
||||
<Dialog title="R-Group Logic" className="rgroup-logic"
|
||||
result={() => formState.result} valid={() => formState.valid} params={prop}>
|
||||
<Form schema={rgroupSchema}
|
||||
customValid={{range: r => rangeConv(r)}} init={prop} {...formState}>
|
||||
<Field name="range"/>
|
||||
<Field name="resth"/>
|
||||
<IfThenSelect name="ifthen" className="cond" label={label} rgids={rgroupLabels}/>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function rangeConv(range) { // structConv
|
||||
const res = range.replace(/\s*/g, '').replace(/,+/g, ',')
|
||||
.replace(/^,/, '').replace(/,$/, '');
|
||||
|
||||
return res.split(',').every(function (s) {
|
||||
return s.match(/^[>,<=]?[0-9]+$/g) ||
|
||||
s.match(/^[0-9]+-[0-9]+$/g);
|
||||
});
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({ formState: store.modal.form })
|
||||
)(RgroupLogic);
|
||||
101
static/js/ketcher2/script/ui/dialog/rgroup.jsx
Normal file
101
static/js/ketcher2/script/ui/dialog/rgroup.jsx
Normal file
@ -0,0 +1,101 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { range } from 'lodash/fp';
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
/** @jsx h */
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
|
||||
function RGroup({ selected, onSelect, result, ...props }) {
|
||||
return (
|
||||
<Dialog title="R-Group"
|
||||
className="rgroup" params={props}
|
||||
result={() => result()}>
|
||||
<ul>
|
||||
{ range(1, 33).map(i => (
|
||||
<li>
|
||||
<button
|
||||
className={ selected(i) ? 'selected' : ''}
|
||||
onClick={ev => onSelect(i)}>
|
||||
{`R${i}`}
|
||||
</button>
|
||||
</li>
|
||||
)) }
|
||||
</ul>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
class RGroupFragment extends Component {
|
||||
constructor({label}) {
|
||||
super();
|
||||
this.state.label = label || null;
|
||||
}
|
||||
onSelect(label) {
|
||||
this.setState({
|
||||
label: label !== this.state.label ? label : null
|
||||
});
|
||||
}
|
||||
selected(label) {
|
||||
return label === this.state.label;
|
||||
}
|
||||
result() {
|
||||
return { label: this.state.label };
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<RGroup selected={i => this.selected(i)}
|
||||
onSelect={i => this.onSelect(i)}
|
||||
result={() => this.result()} {...this.props}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RGroupAtom extends Component {
|
||||
constructor({values}) {
|
||||
super();
|
||||
this.state.values = values || [];
|
||||
}
|
||||
onSelect(index) {
|
||||
const {values} = this.state;
|
||||
const i = values.indexOf(index);
|
||||
if (i < 0)
|
||||
values.push(index);
|
||||
else
|
||||
values.splice(i, 1);
|
||||
this.setState({ values });
|
||||
}
|
||||
selected(index) {
|
||||
return this.state.values.includes(index);
|
||||
}
|
||||
result() {
|
||||
return {
|
||||
type: 'rlabel',
|
||||
values: this.state.values
|
||||
};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<RGroup selected={i => this.selected(i)}
|
||||
onSelect={i => this.onSelect(i)}
|
||||
result={() => this.result() } {...this.props}/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default params => params.type === 'rlabel' ? (<RGroupAtom {...params}/>) : (<RGroupFragment {...params}/>);
|
||||
90
static/js/ketcher2/script/ui/dialog/save.jsx
Normal file
90
static/js/ketcher2/script/ui/dialog/save.jsx
Normal file
@ -0,0 +1,90 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h, Component } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
import * as structFormat from '../structformat';
|
||||
import { saveUserTmpl } from '../state/templates';
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
import SaveButton from '../component/savebutton';
|
||||
|
||||
class Save extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { type: props.struct.hasRxnArrow() ? 'rxn' : 'mol' };
|
||||
this.changeType().catch(props.onCancel);
|
||||
}
|
||||
|
||||
changeType(ev) {
|
||||
let { type } = this.state;
|
||||
if (ev) {
|
||||
type = ev.target.value;
|
||||
ev.preventDefault();
|
||||
}
|
||||
let converted = structFormat.toString(this.props.struct, type, this.props.server, this.props.options);
|
||||
return converted.then(structStr => this.setState({ type, structStr }),
|
||||
e => { alert(e); });
|
||||
}
|
||||
|
||||
render () {
|
||||
// $('[value=inchi]').disabled = ui.standalone;
|
||||
let { type, structStr } = this.state;
|
||||
let format = structFormat.map[type];
|
||||
console.assert(format, "Unknown chemical file type");
|
||||
|
||||
return (
|
||||
<Dialog title="Save Structure"
|
||||
className="save" params={this.props}
|
||||
buttons={[(
|
||||
<SaveButton className="save"
|
||||
data={structStr}
|
||||
filename={'ketcher' + format.ext[0]}
|
||||
type={format.mime}
|
||||
server={this.props.server}
|
||||
onSave={ () => this.props.onOk() }>
|
||||
Save To File…
|
||||
</SaveButton>
|
||||
), (
|
||||
<button className="save-tmpl"
|
||||
onClick={ () => this.props.onTmplSave(structStr) }>
|
||||
Save to Templates</button>
|
||||
), "Close"]}>
|
||||
<label>Format:
|
||||
<select value={type} onChange={ev => this.changeType(ev)}>{
|
||||
[this.props.struct.hasRxnArrow() ? 'rxn' : 'mol', 'smiles', 'smarts', 'cml', 'inchi'].map(type => (
|
||||
<option value={type}>{structFormat.map[type].name}</option>
|
||||
))
|
||||
}</select>
|
||||
</label>
|
||||
<textarea className={type} value={structStr} readonly
|
||||
ref={ el => el && setTimeout(() => el.select(), 10) }/>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({
|
||||
server: store.server,
|
||||
struct: store.editor.struct(),
|
||||
options: store.options.getServerSettings()
|
||||
}),
|
||||
dispatch => ({
|
||||
onTmplSave: struct => dispatch(saveUserTmpl(struct))
|
||||
})
|
||||
)(Save);
|
||||
89
static/js/ketcher2/script/ui/dialog/sdata.jsx
Normal file
89
static/js/ketcher2/script/ui/dialog/sdata.jsx
Normal file
@ -0,0 +1,89 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
import { Form, Field, SelectOneOf } from '../component/form';
|
||||
import Dialog from '../component/dialog';
|
||||
import ComboBox from '../component/combobox';
|
||||
import { sdataSchema, sdataCustomSchema, getSdataDefault } from '../data/sdata-schema'
|
||||
/** @jsx h */
|
||||
|
||||
function SelectInput({ title, name, schema, ...prop }) {
|
||||
const inputSelect = Object.keys(schema).reduce((acc, item) => {
|
||||
acc.enum.push(item);
|
||||
acc.enumNames.push(schema[item].title || item);
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
title: title,
|
||||
type: 'string',
|
||||
default: '',
|
||||
minLength: 1,
|
||||
enum: [],
|
||||
enumNames: []
|
||||
}
|
||||
);
|
||||
|
||||
return <Field name={name} schema={inputSelect} component={ComboBox} {...prop} />
|
||||
}
|
||||
|
||||
function SData({ context, fieldName, fieldValue, type, radiobuttons, formState, ...prop }) {
|
||||
const { result, valid } = formState;
|
||||
|
||||
const init = {
|
||||
context,
|
||||
fieldName: fieldName || getSdataDefault(context),
|
||||
type,
|
||||
radiobuttons
|
||||
};
|
||||
|
||||
init.fieldValue = fieldValue || getSdataDefault(context, init.fieldName);
|
||||
|
||||
const formSchema = sdataSchema[result.context][result.fieldName] || sdataCustomSchema;
|
||||
|
||||
const serialize = {
|
||||
context: result.context.trim(),
|
||||
fieldName: result.fieldName.trim(),
|
||||
fieldValue: typeof (result.fieldValue) === 'string' ? result.fieldValue.trim() : result.fieldValue
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog title={"S-Group Properties"} className="sgroup"
|
||||
result={() => result} valid={() => valid} params={prop}>
|
||||
<Form serialize={serialize} schema={formSchema} init={init} {...formState}>
|
||||
<SelectOneOf title="Context" name="context" schema={sdataSchema}/>
|
||||
<fieldset className={"data"}>
|
||||
<SelectInput title="Field name" name="fieldName" schema={sdataSchema[result.context]}/>
|
||||
{
|
||||
content(formSchema, result.context, result.fieldName)
|
||||
}
|
||||
</fieldset>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const content = (schema, context, fieldName) => Object.keys(schema.properties)
|
||||
.filter(prop => prop !== "type" && prop !== "context" && prop !== "fieldName")
|
||||
.map(prop => prop === "radiobuttons" ?
|
||||
<Field name={prop} type="radio" key={`${context}-${fieldName}-${prop}-radio`}/> :
|
||||
<Field name={prop} type="textarea" multiple={true} size="10" key={`${context}-${fieldName}-${prop}-select`}/>
|
||||
);
|
||||
|
||||
export default connect(
|
||||
store => ({ formState: store.modal.form })
|
||||
)(SData);
|
||||
61
static/js/ketcher2/script/ui/dialog/sgroup.jsx
Normal file
61
static/js/ketcher2/script/ui/dialog/sgroup.jsx
Normal file
@ -0,0 +1,61 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import { sgroup as sgroupSchema } from '../structschema';
|
||||
import { Form, Field, SelectOneOf } from '../component/form';
|
||||
import { mapOf } from '../utils';
|
||||
import Dialog from '../component/dialog';
|
||||
|
||||
const schemes = mapOf(sgroupSchema, 'type');
|
||||
|
||||
function Sgroup({ formState, ...prop }) {
|
||||
const { result, valid } = formState;
|
||||
|
||||
const type = result.type;
|
||||
|
||||
return (
|
||||
<Dialog title="S-Group Properties" className="sgroup"
|
||||
result={() => result} valid={() => valid} params={prop}>
|
||||
<Form schema={schemes[type]} init={prop} {...formState}>
|
||||
<SelectOneOf title="Type" name="type" schema={schemes}/>
|
||||
<fieldset className={type === 'DAT' ? 'data' : 'base'}>
|
||||
{ content(type) }
|
||||
</fieldset>
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const content = type => Object.keys(schemes[type].properties)
|
||||
.filter(prop => prop !== 'type')
|
||||
.map(prop => {
|
||||
let props = {};
|
||||
if (prop === 'name') props.maxlength = 15;
|
||||
if (prop === 'fieldName') props.maxlength = 30;
|
||||
if (prop === 'fieldValue') props.type = 'textarea';
|
||||
if (prop === 'radiobuttons') props.type = 'radio';
|
||||
|
||||
return <Field name={prop} key={`${type}-${prop}`} {...props}/>;
|
||||
}
|
||||
);
|
||||
|
||||
export default connect(
|
||||
(store) => ({ formState: store.modal.form })
|
||||
)(Sgroup);
|
||||
114
static/js/ketcher2/script/ui/dialog/template-attach.jsx
Normal file
114
static/js/ketcher2/script/ui/dialog/template-attach.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { h, Component } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import Dialog from '../component/dialog';
|
||||
import Input from '../component/input';
|
||||
import StructEditor from '../component/structeditor';
|
||||
import { storage } from '../utils';
|
||||
|
||||
import { initAttach, setAttachPoints, setTmplName } from '../state/templates';
|
||||
|
||||
const EDITOR_STYLES = {
|
||||
selectionStyle: { fill: '#47b3ec', stroke: 'none' },
|
||||
highlightStyle: { stroke: '#1a7090', 'stroke-width': 1.2 }
|
||||
};
|
||||
|
||||
class Attach extends Component {
|
||||
constructor({ onInit, ...props }) {
|
||||
super();
|
||||
this.tmpl = initTmpl(props.tmpl);
|
||||
onInit(this.tmpl.struct.name, this.tmpl.props);
|
||||
this.onResult = this.onResult.bind(this);
|
||||
}
|
||||
|
||||
onResult() {
|
||||
const { name, atomid, bondid } = this.props;
|
||||
return name && (
|
||||
name !== this.tmpl.struct.name ||
|
||||
atomid !== this.tmpl.props.atomid ||
|
||||
bondid !== this.tmpl.props.bondid
|
||||
) ? { name, attach: { atomid, bondid } } : null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
name, atomid, bondid,
|
||||
onNameEdit, onAttachEdit, ...prop
|
||||
} = this.props;
|
||||
const struct = this.tmpl.struct;
|
||||
const options = Object.assign(EDITOR_STYLES, { scale: getScale(struct) });
|
||||
|
||||
return (
|
||||
<Dialog title="Template Edit" className="attach"
|
||||
result={this.onResult} params={prop}>
|
||||
<label>Template name:
|
||||
<Input value={name} onChange={onNameEdit}/>
|
||||
</label>
|
||||
<label>Choose attachment atom and bond:</label>
|
||||
<StructEditor className="editor"
|
||||
struct={struct}
|
||||
onAttachEdit={onAttachEdit}
|
||||
tool="attach" toolOpts={{ atomid, bondid }}
|
||||
options={options}/>
|
||||
{!storage.isAvailable() ? <div className="warning">{storage.warningMessage}</div> : null}
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({ ...store.templates.attach }),
|
||||
dispatch => ({
|
||||
onInit: (name, ap) => dispatch(initAttach(name, ap)),
|
||||
onAttachEdit: ap => dispatch(setAttachPoints(ap)),
|
||||
onNameEdit: name => dispatch(setTmplName(name))
|
||||
})
|
||||
)(Attach);
|
||||
|
||||
function initTmpl(tmpl) {
|
||||
const normTmpl = {
|
||||
struct: structNormalization(tmpl.struct),
|
||||
props: {
|
||||
atomid: +tmpl.props.atomid || 0,
|
||||
bondid: +tmpl.props.bondid || 0
|
||||
}
|
||||
};
|
||||
normTmpl.struct.name = tmpl.struct.name;
|
||||
return normTmpl;
|
||||
}
|
||||
|
||||
function structNormalization(struct) {
|
||||
const normStruct = struct.clone();
|
||||
const cbb = normStruct.getCoordBoundingBox();
|
||||
normStruct.atoms.each(function (aid, atom) { // only atoms ?? mb arrow etc ...
|
||||
atom.pp = atom.pp.sub(cbb.min);
|
||||
});
|
||||
return normStruct;
|
||||
}
|
||||
|
||||
function getScale(struct) {
|
||||
const cbb = struct.getCoordBoundingBox();
|
||||
const VIEW_SIZE = 200;
|
||||
let scale = VIEW_SIZE / Math.max(cbb.max.y - cbb.min.y, cbb.max.x - cbb.min.x);
|
||||
|
||||
if (scale < 35) scale = 35;
|
||||
if (scale > 75) scale = 75;
|
||||
return scale;
|
||||
}
|
||||
174
static/js/ketcher2/script/ui/dialog/template-lib.jsx
Normal file
174
static/js/ketcher2/script/ui/dialog/template-lib.jsx
Normal file
@ -0,0 +1,174 @@
|
||||
/****************************************************************************
|
||||
* Copyright 2017 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 { escapeRegExp, chunk, flow, filter as _filter, reduce, omit } from 'lodash/fp';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx h */
|
||||
|
||||
import sdf from '../../chem/sdf';
|
||||
|
||||
import VisibleView from '../component/visibleview';
|
||||
import StructRender from '../component/structrender';
|
||||
import Dialog from '../component/dialog';
|
||||
import SaveButton from '../component/savebutton';
|
||||
import Input from '../component/input';
|
||||
import SelectList from '../component/select';
|
||||
|
||||
import { changeFilter, changeGroup, selectTmpl, editTmpl } from '../state/templates';
|
||||
import { onAction } from "../state/";
|
||||
|
||||
const GREEK_SIMBOLS = {
|
||||
'Alpha': 'A', 'alpha': 'α',
|
||||
'Beta': 'B', 'beta': 'β',
|
||||
'Gamma': 'Г', 'gamma': 'γ'
|
||||
};
|
||||
|
||||
function tmplName(tmpl, i) {
|
||||
console.assert(tmpl.props && tmpl.props.group, "No group");
|
||||
return tmpl.struct.name || `${tmpl.props.group} template ${i + 1}`;
|
||||
}
|
||||
|
||||
function partition(n, array) {
|
||||
console.warn('partition', n);
|
||||
return chunk(n)(array);
|
||||
}
|
||||
|
||||
const greekRe = new RegExp('\\b' + Object.keys(GREEK_SIMBOLS).join('\\b|\\b') + '\\b', 'g');
|
||||
function greekify(str) {
|
||||
return str.replace(greekRe, sym => GREEK_SIMBOLS[sym]);
|
||||
}
|
||||
|
||||
const filterLibSelector = createSelector(
|
||||
(props) => props.lib,
|
||||
(props) => props.filter,
|
||||
filterLib
|
||||
);
|
||||
|
||||
function filterLib(lib, filter) {
|
||||
console.warn('Filter', filter);
|
||||
let re = new RegExp(escapeRegExp(greekify(filter)), 'i');
|
||||
return flow(
|
||||
_filter(item => !filter || re.test(greekify(item.struct.name)) || re.test(greekify(item.props.group))),
|
||||
reduce((res, item) => {
|
||||
!res[item.props.group] ? res[item.props.group] = [item] : res[item.props.group].push(item);
|
||||
return res;
|
||||
}, {})
|
||||
)(lib)
|
||||
}
|
||||
|
||||
const libRowsSelector = createSelector(
|
||||
(props) => props.lib,
|
||||
(props) => props.group,
|
||||
(props) => props.COLS,
|
||||
libRows
|
||||
);
|
||||
|
||||
function libRows(lib, group, COLS) {
|
||||
console.warn("Group", group);
|
||||
return partition(COLS, lib[group])
|
||||
}
|
||||
|
||||
function RenderTmpl({ tmpl, ...props }) {
|
||||
return tmpl.props && tmpl.props.prerender ?
|
||||
( <svg {...props}><use xlinkHref={tmpl.props.prerender}/></svg> ) :
|
||||
( <StructRender struct={tmpl.struct} options={{ autoScaleMargin: 15 }} {...props}/> );
|
||||
}
|
||||
|
||||
class TemplateLib extends Component {
|
||||
select(tmpl) {
|
||||
if (tmpl === this.props.selected)
|
||||
this.props.onOk(this.result());
|
||||
else
|
||||
this.props.onSelect(tmpl);
|
||||
}
|
||||
|
||||
result() {
|
||||
const tmpl = this.props.selected;
|
||||
console.assert(!tmpl || tmpl.props, 'Incorrect SDF parse');
|
||||
return tmpl ? {
|
||||
struct: tmpl.struct,
|
||||
aid: parseInt(tmpl.props.atomid) || null,
|
||||
bid: parseInt(tmpl.props.bondid) || null
|
||||
} : null;
|
||||
}
|
||||
|
||||
renderRow(row, index, COLS) {
|
||||
return (
|
||||
<div className="tr" key={index}>{ row.map((tmpl, i) => (
|
||||
<div className={tmpl === this.props.selected ? 'td selected' : 'td'}
|
||||
title={greekify(tmplName(tmpl, index * COLS + i))}>
|
||||
<RenderTmpl tmpl={tmpl} className="struct" onClick={() => this.select(tmpl)}/>
|
||||
<button className="attach-button" onClick={() => this.props.onAttach(tmpl)}>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
))}</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const COLS = 3;
|
||||
let { group, filter, onFilter, onChangeGroup, ...props } = this.props;
|
||||
const lib = filterLibSelector(this.props);
|
||||
group = lib[group] ? group : Object.keys(lib)[0];
|
||||
|
||||
return (
|
||||
<Dialog title="Template Library"
|
||||
className="template-lib" params={props}
|
||||
result={() => this.result()}
|
||||
buttons={[
|
||||
<SaveButton className="save"
|
||||
data={ sdf.stringify(this.props.lib) }
|
||||
filename={'ketcher-tmpls.sdf'}>
|
||||
Save To SDF…
|
||||
</SaveButton>,
|
||||
"OK", "Cancel"]}>
|
||||
<label>
|
||||
<Input type="search" placeholder="Filter"
|
||||
value={ filter } onChange={value => onFilter(value)}/>
|
||||
</label>
|
||||
<Input className="groups" component={SelectList}
|
||||
splitIndexes={[Object.keys(lib).indexOf('User Templates')]}
|
||||
value={ group } onChange={g => onChangeGroup(g)}
|
||||
schema={{
|
||||
enum: Object.keys(lib),
|
||||
enumNames: Object.keys(lib).map(g => greekify(g))
|
||||
}}/>
|
||||
<VisibleView data={libRowsSelector({ lib, group, COLS })}
|
||||
rowHeight={120} className="table">
|
||||
{ (row, i) => this.renderRow(row, i, COLS) }
|
||||
</VisibleView>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
store => ({ ...omit(['attach'], store.templates) }),
|
||||
(dispatch, props) => ({
|
||||
onFilter: filter => dispatch(changeFilter(filter)),
|
||||
onSelect: tmpl => dispatch(selectTmpl(tmpl)),
|
||||
onChangeGroup: group => dispatch(changeGroup(group)),
|
||||
onAttach: tmpl => dispatch(editTmpl(tmpl)),
|
||||
onOk: res => {
|
||||
dispatch(onAction({ tool: 'template', opts: res }));
|
||||
props.onOk(res);
|
||||
}
|
||||
})
|
||||
)(TemplateLib);
|
||||
Reference in New Issue
Block a user