diff --git a/.gitignore b/.gitignore index 29a1ff2210..b3b4fdbb7a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,14 +58,14 @@ /public/images/* !/public/images/wild_card/ -/uploads/attachments/* -/uploads/thumbnails +/uploads/* !/public/attachments/.keep + /public/docx/* !/public/docx/.keep -/public/assets/*.js +/public/assets/* /coverage/ diff --git a/.rubocop.yml b/.rubocop.yml index d07f98f099..991149d200 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,7 @@ +AllCops: + DisplayCopNames: true + TargetRubyVersion: 2.3 + Style/BlockDelimiters: EnforcedStyle: semantic FunctionalMethods: @@ -15,6 +19,8 @@ Metrics/ModuleLength: Metrics/BlockLength: Exclude: - "**/*_spec.rb" - Metrics/LineLength: Max: 100 + +Metrics/MethodLength: + Max: 20 diff --git a/Gemfile b/Gemfile index 2da25e10c6..9bc3b6ca8b 100644 --- a/Gemfile +++ b/Gemfile @@ -106,6 +106,7 @@ gem 'rubocop', require: false gem 'yaml_db' gem 'ruby-ole' +gem 'ruby-geometry', require: 'geometry' # CI gem 'coveralls', require: false @@ -114,8 +115,8 @@ gem 'coveralls', require: false # to compile from github/openbabel/openbabel master # gem 'openbabel', '2.4.1.2', git: 'https://github.com/ComPlat/openbabel-gem' # to compile from github/openbabel/openbabel branch openbabel-2-4-x -gem 'openbabel', '2.4.0.1', git: 'https://github.com/ComPlat/openbabel-gem', - branch: 'openbabel-2-4-x' +gem 'openbabel', '2.4.1.2', git: 'https://github.com/ComPlat/openbabel-gem', + branch: 'cdx-extraction' gem 'barby' gem 'prawn' diff --git a/Gemfile.lock b/Gemfile.lock index 683d0f70b4..cc64ab8755 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -15,10 +15,10 @@ GIT GIT remote: https://github.com/ComPlat/openbabel-gem - revision: 837eec9e660b6dbc9d2517889de39a6e93bb4b98 - branch: openbabel-2-4-x + revision: b651909dc84ea33ab79a8e95dc50486ba272a3d4 + branch: cdx-extraction specs: - openbabel (2.4.0.1) + openbabel (2.4.1.2) GIT remote: https://github.com/ComPlat/sablon @@ -333,6 +333,7 @@ GEM skinny (~> 0.2.3) sqlite3 (~> 1.3) thin (~> 1.5.0) + memoist (0.16.0) memory_profiler (0.9.7) meta_request (0.4.3) callsite (~> 0.0, >= 0.0.11) @@ -469,6 +470,9 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.7) unicode-display_width (~> 1.0, >= 1.0.1) + ruby-geometry (0.0.6) + activesupport + memoist ruby-mailchecker (3.0.27) ruby-ole (1.2.12) ruby-progressbar (1.8.1) @@ -632,7 +636,7 @@ DEPENDENCIES net-sftp net-ssh nokogiri - openbabel (= 2.4.0.1)! + openbabel (= 2.4.1.2)! paranoia (~> 2.0) pg (~> 0.20.0) pg_search @@ -648,6 +652,7 @@ DEPENDENCIES rspec-rails rtf rubocop + ruby-geometry ruby-mailchecker ruby-ole rubyXL (= 3.3.26) diff --git a/app/api/api.rb b/app/api/api.rb index 848a2607fc..4cbe29f817 100644 --- a/app/api/api.rb +++ b/app/api/api.rb @@ -190,7 +190,7 @@ def to_molecule_array(hash_groups) mount Chemotion::DevicesAnalysisAPI mount Chemotion::GeneralAPI mount Chemotion::V1PublicAPI - mount Chemotion::DocxAPI mount Chemotion::GateAPI mount Chemotion::ElementAPI + mount Chemotion::ChemReadAPI end diff --git a/app/api/chemotion/chem_read_api.rb b/app/api/chemotion/chem_read_api.rb new file mode 100644 index 0000000000..0d3c2958a2 --- /dev/null +++ b/app/api/chemotion/chem_read_api.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# Belong to Chemotion module +module Chemotion + require 'open3' + require 'ole/storage' + + # API for ChemRead manipulation + class ChemReadAPI < Grape::API + helpers ChemReadHelpers + + resource :chemread do + resource :embedded do + desc 'Upload import files' + params do + requires :get_mol, type: Boolean, default: false, desc: '' + end + + post 'upload' do + smi_arr = [] + + get_mol = params[:get_mol] + params.delete('get_mol') + + params.each do |uid, file| + temp_file = file.tempfile + temp_path = temp_file.to_path + extn = File.extname temp_path + tmp_dir = Dir.mktmpdir([uid, File.basename(temp_path, extn)]) + + info = read_uploaded_file(temp_file, tmp_dir, get_mol) + smi_obj = { uid: uid, name: file.filename, info: info } + + temp_file.close + temp_file.unlink + FileUtils.remove_dir tmp_dir, true + smi_arr.push(smi_obj) + end + + smi_arr + end + end + + resource :svg do + desc 'Convert svg from smi' + params do + requires :smiArr, type: Array, desc: 'Files and uids' + end + + post 'smi' do + smi_arr = params[:smiArr] + res = [] + smi_arr.each do |smi| + res.push( + uid: smi[:uid], + smiIdx: smi[:smiIdx], + svg: SVG::ReactionComposer.cr_reaction_svg_from_rsmi( + smi[:newSmi], + ChemReadHelpers::SOLVENTS_SMI, + ChemReadHelpers::REAGENTS_SMI + ) + ) + end + res + end + end + end + end +end diff --git a/app/api/chemotion/docx_api.rb b/app/api/chemotion/docx_api.rb deleted file mode 100644 index 7140bd4b00..0000000000 --- a/app/api/chemotion/docx_api.rb +++ /dev/null @@ -1,75 +0,0 @@ -# Belong to Chemotion module -module Chemotion - require 'open3' - - # API for Docx manipulation - class DocxAPI < Grape::API - resource :docx do - resource :embedded do - desc 'Upload cdx' - post 'upload' do - rsmi_arr = [] - - params.each do |uid, file| - rsmi_obj = { uid: uid, name: file.filename, rsmi: [] } - tmp = file.tempfile - extn = File.extname tmp.to_path - filename = File.basename tmp.to_path, extn - tmp_dir = Dir.mktmpdir([uid, filename]) - - if extn == '.docx' - cmd = "unzip #{tmp.to_path} -d #{tmp_dir}" - Open3.popen3(cmd) do |_, _, _, wait_thr| wait_thr.value end - - cmd = "for file in #{tmp_dir}/word/embeddings/*.bin; " - cmd += 'do DIR="${file%.*}"; mkdir $DIR; 7z x -o$DIR/ $file; ' - cmd += 'mv $DIR/CONTENTS $DIR.cdx; done' - Open3.popen3(cmd) do |_, _, _, wait_thr| wait_thr.value end - file_dir = "#{tmp_dir}/word/embeddings/*.cdx" - else - file_dir = tmp.to_path - end - - Dir[file_dir].each do |cdx_path| - cmd = Gem.loaded_specs['openbabel'].full_gem_path - cmd += "/openbabel/bin/obabel -icdx #{cdx_path} -orsmi" - Open3.popen3(cmd) do |_, stdout, _, wait_thr| - rsmi = (stdout.gets || '').delete("\n").strip - res = {} - unless rsmi.empty? - res[:svg] = SVG::ReactionComposer.reaction_svg_from_rsmi rsmi - res[:smi] = rsmi - rsmi_obj[:rsmi].push(res) - end - wait_thr.value - end - end - - tmp.close - tmp.unlink - FileUtils.remove_dir tmp_dir, true - rsmi_arr.push(rsmi_obj) - end - - rsmi_arr - end - end - - resource :svg do - desc 'Convert svg from smi' - post 'smi' do - smi_arr = params[:smiArr] - res = [] - smi_arr.each do |smi| - res.push( - uid: smi[:uid], - rsmiIdx: smi[:rsmiIdx], - svg: SVG::ReactionComposer.reaction_svg_from_rsmi(smi[:newSmi]) - ) - end - res - end - end - end - end -end diff --git a/app/api/helpers/chem_read_helpers.rb b/app/api/helpers/chem_read_helpers.rb new file mode 100644 index 0000000000..4f8628d06f --- /dev/null +++ b/app/api/helpers/chem_read_helpers.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +# Helpers function for CDX parser +module ChemReadHelpers + extend Grape::API::Helpers + include ChemReadTextHelpers + + reagents_paths = Dir.glob('lib/cdx/below_arrow/*.yaml') + .reject { |f| f.end_with?('solvents.yaml') } + .map { |f| Rails.root + f } + REAGENTS_SMI = reagents_paths.reduce([]) { |acc, val| + acc.concat(YAML.safe_load(File.open(val)).values) + } + + solvents_path = Rails.root + 'lib/cdx/below_arrow/solvents.yaml' + SOLVENTS_SMI = YAML.safe_load(File.open(solvents_path)).values + + def read_docx(path, dir, get_mol) + Open3.popen3("unzip #{path} -d #{dir}") do |_, _, _, wait_thr| + wait_thr.value + end + + cmd = "for file in #{dir}/word/embeddings/*.bin; " + cmd += 'do DIR="${file%.*}"; mkdir $DIR; 7z x -o$DIR/ $file; ' + cmd += 'mv $DIR/CONTENTS $DIR.cdx; done' + Open3.popen3(cmd) do |_, _, _, wait_thr| wait_thr.value end + + # sort by order of cdx within docx + cdx_files = Dir["#{dir}/word/embeddings/*.cdx"]&.sort { |x, y| + File.basename(x)[/\d+/].to_i <=> File.basename(y)[/\d+/].to_i + } + + infos = [] + cdx_files.each do |cdx| + info = read_cdx(cdx, get_mol) + next if info.nil? + infos = infos.concat(info) + end + + infos + end + + def read_doc(path, get_mol) + ole = Ole::Storage.open(path).root['ObjectPool'] + cdx_arr = ole.children.map { |x| x['CONTENTS'] && x['CONTENTS'].read }.compact + + infos = [] + cdx_arr.each do |cdx| + info = read_cdx(cdx, get_mol, false) + next if info.nil? + infos = infos.concat(info) + end + + infos + end + + def read_cdx(cdx, get_mol, is_path = true) + parser = Cdx::Parser::CDXParser.new + parser.read(cdx, is_path) + + info_from_parser(parser, get_mol) + end + + def read_xml(path, get_mol) + parser = Cdx::Parser::ExmlParser.new + parser.read(path) + + info_from_parser(parser, get_mol) + end + + def read_cdxml(path, get_mol) + parser = Cdx::Parser::CdxmlParser.new + parser.read(path) + + info_from_parser(parser, get_mol) + end + + def info_from_parser(parser, get_mol) + objs = get_mol ? parser.molmap.values : parser.reaction + return [] if objs.empty? + + infos = [] + objs.each do |obj| + info = extract_info(obj, get_mol) + infos.push(info) + end + + infos + end + + def read_uploaded_file(file, dir, get_mol) + filepath = file.to_path + extn = File.extname(filepath) + + begin + return case extn + when '.docx' then read_docx(filepath, dir, get_mol) + when '.doc' then read_doc(file, get_mol) + when '.cdx' then read_cdx(file, get_mol) + when '.xml' then read_xml(file, get_mol) + when '.cdxml' then read_cdxml(file, get_mol) + else raise 'Uploaded file type is not supported' + end + rescue StandardError => e + Rails.logger.error("Error while parsing: #{e}") + end + end + + def refine_text(obj) + t = obj[:text] + return if t.nil? || t.empty? + + extract_text_info(obj) + + expand_abb(obj) + end + + def extract_info(obj, get_mol) + info = get_mol ? mol_info(obj) : reaction_info(obj) + return nil if info.nil? || info[:smi].nil? || info[:smi].empty? + + svg = if get_mol + Chemotion::OpenBabelService.smi_to_trans_svg(info[:smi]) + else + SVG::ReactionComposer.cr_reaction_svg_from_rsmi( + info[:smi], + SOLVENTS_SMI, + REAGENTS_SMI + ) + end + + info.merge(svg: svg) + end + + def extract_molecules(react, group) + return { smis: [], desc: {} } unless react[group.to_sym] + + smi_array = [] + desc = {} + react[group.to_sym].each_with_index do |m, idx| + smi_array << m[:smi] if m[:smi] + if m[:text] + # Scan the text, try to extract/parse it as a molecule if there are + # any abbreviations in the list + smis = refine_text(m) + smi_array.concat(smis) unless smis.nil? || smis.empty? + end + + desc[idx] = { + text: m[:text], + time: m[:time], + yield: m[:yield], + detail: m[:detail], + temperature: m[:temperature] + } + end + + { smis: smi_array, desc: desc } + end + + def reaction_info(reaction) + smi_array = [] + desc = { detail: reaction[:detail] } + + %w[reactants reagents products].each do |group| + info = extract_molecules(reaction, group) + desc[group.to_sym] = info[:desc] + smi_array << info[:smis].compact.reject(&:empty?).join('.') + end + + rinfo = desc[:reagents] + desc[:reagents] = {} + rinfo.values.compact.each do |val| + %w[text time yield temperature].each do |field| + field_s = field.to_sym + next if val[field_s].nil? + desc[:reagents][field_s] = '' if desc[:reagents][field_s].nil? + desc[:reagents][field_s] += ' ' + val[field_s] + desc[:reagents][field_s].strip! + end + end + + %w[reactants products].each do |group| + desc[group.to_sym].values.compact.each do |val| + %w[time yield temperature].each do |field| + field_s = field.to_sym + next if val[field_s].nil? + desc[:reagents][field_s] = '' if desc[:reagents][field_s].nil? + desc[:reagents][field_s] += ' ' + val[field_s] + desc[:reagents][field_s].strip! + val[field_s] = nil + end + end + end + rinfo = desc[:reagents] + desc[:reagents] = { '0': rinfo } + + { smi: smi_array.join('>'), desc: desc } + end + + def mol_info(obj) + return if obj[:mol].nil? + + smi = obj[:smi].nil? ? '' : obj[:smi] + res = { smi: smi, desc: {} } + + extract_text_info(obj) unless obj[:text].nil? + + res[:desc] = { + detail: obj[:detail], + text: obj[:text], + time: obj[:time], + yield: obj[:yield], + temperature: obj[:temperature] + } + + res + end +end diff --git a/app/api/helpers/chem_read_text_helpers.rb b/app/api/helpers/chem_read_text_helpers.rb new file mode 100644 index 0000000000..5bac3ed2f6 --- /dev/null +++ b/app/api/helpers/chem_read_text_helpers.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +# Helpers function for manipulate extracted text from CDX +module ChemReadTextHelpers + extend Grape::API::Helpers + + yml_path = Rails.root + 'lib/cdx/parser/abbreviations.yaml' + ABB = YAML.safe_load(File.open(yml_path)) + + # @ad = OpenBabel::AliasData.new + + # Expand any aliases after molecule constructed + # def expand_mol(mol, atom, cid, alias_text) + # return if alias_text.empty? + # @ad.set_alias(alias_text) + # # Make chemically meaningful, if possible. + # is_expanded = @ad.expand(mol, atom.get_idx) + # return if is_expanded + # @textmap[cid] = { + # text: alias_text, + # x: atom.get_vector.get_x, + # y: atom.get_vector.get_y + # } + # end + + def ending_regex + '(\s|,|\n|\r|\)|\]|\.|\z|$)' + end + + def beginning_regex + '(\s|,|\n|\r|\(|\[|\.|\A|^)' + end + + def unicode(uni_str) + uni_str.encode(Encoding::UTF_8) + end + + def degree_regex + degree = unicode("\u00B0") + dcelcius = unicode("\u2103") + dfarenheit = unicode("\u2109") + + "((#{degree} *(C|F))|(#{dcelcius}|#{dfarenheit}))" + end + + def range_regex + hyphen1 = unicode("\u002D") + hyphen2 = unicode("\u2010") + minus = unicode("\u2212") + ndash = unicode("\u2013") + mdash = unicode("\u2014") + + "(#{hyphen1}|#{hyphen2}|#{minus}|#{ndash}|#{mdash}|~)" + end + + def range_number_regex(unit_regex, can_negative) + sign = can_negative ? '-?\\s*' : '' + real_number = '(\d+|\d+\.\d+)' + + "#{sign}(#{real_number}\\s*#{unit_regex}?\\s*#{range_regex})?#{real_number}\\s*#{unit_regex}" + end + + def expand_abb(obj) + smis = [] + + ABB.each_key do |k| + regex = Regexp.new("#{Regexp.escape(k)}#{ending_regex}", true) + next unless obj[:text] =~ regex + smis.push(ABB[k]) + end + + smis.uniq + end + + def time_regex + day = '(days?|d)' + hour = '(hours?|h)' + minute = '(minutes?|mins?|m)' + second = '(seconds?|secs?|s)' + + time_unit = "(#{day}|#{hour}|#{minute}|#{second})" + time_regex_str = range_number_regex(time_unit, false) + + /#{beginning_regex}+#{time_regex_str}#{ending_regex}+/i + end + + def extract_time_info(obj) + matches = [] + + obj[:text].scan(time_regex) do + m = Regexp.last_match[0] + matches << m.gsub(/#{ending_regex}/, ' ').strip + end + + ovn_regex = Regexp.union(%w[overnight ovn o/n]).source + m = obj[:text].match(/#{beginning_regex}+#{ovn_regex}?#{ending_regex}+/i) + matches << '12h ~ 20h' unless m.nil? || m[0].empty? + + return if matches.size.zero? + + obj[:time] = matches.join(' ').gsub(/[()]/, '') + end + + def extract_text_info(obj) + unless obj[:text].encoding == Encoding::UTF_8 + t = obj[:text].force_encoding(Encoding::CP1252).encode(Encoding::UTF_8) + obj[:text] = t + end + + yield_regex_str = range_number_regex('%', false) + yield_regex = /#{beginning_regex}+#{yield_regex_str}#{ending_regex}+/ + text_regex(obj, yield_regex, 'yield') + + extract_temperature(obj) + extract_time_info(obj) + end + + def extract_temperature(obj) + temperature_regex_str = range_number_regex(degree_regex, true) + temperature_regex = /#{beginning_regex}+#{temperature_regex_str}#{ending_regex}+/ + + text_regex(obj, temperature_regex, 'temperature') + + m = obj[:text].match(/#{beginning_regex}+r\.?t\.?#{ending_regex}+/i) + return if m.nil? || m[0].empty? + + degree = unicode("\u00B0") + obj[:temperature] = (obj[:temperature] || '') + "20#{degree}C ~ 25#{degree}C" + end + + def text_regex(obj, regex, field) + m = obj[:text].match(regex) + return if m.nil? + matched = m[0] + return if matched.empty? + + obj[field.to_sym] = matched.gsub(/#{ending_regex}/, ' ').strip + obj[field.to_sym] = matched.gsub(/#{beginning_regex}/, ' ').strip + end +end diff --git a/app/assets/javascripts/components.js b/app/assets/javascripts/components.js index eb0cc6ed2c..7d07f910f1 100644 --- a/app/assets/javascripts/components.js +++ b/app/assets/javascripts/components.js @@ -2,8 +2,8 @@ var React = require('react'); var Home = require('./libHome/Home'); -var Docx = require('./components/docx/DocxContainer'); var CnC = require('./libCnC/CnC'); +var ChemRead = require('./components/chemread/ChemReadContainer'); var App = require('./components/App'); //= require_self diff --git a/app/assets/javascripts/components/chemread/ChemRead.js b/app/assets/javascripts/components/chemread/ChemRead.js new file mode 100644 index 0000000000..45b8910303 --- /dev/null +++ b/app/assets/javascripts/components/chemread/ChemRead.js @@ -0,0 +1,189 @@ +import React from 'react'; +import { + PanelGroup, Panel, ListGroup, ListGroupItem, Grid, Col, Radio +} from 'react-bootstrap'; +import Dropzone from 'react-dropzone'; + +import SmilesEditing from './SmilesEditing'; +import RsmiItemContainer from './RsmiItemContainer'; +import DeleteBtn from './DeleteBtn'; + +class ClickableBtn extends React.Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + } + + onClick(e) { + const { obj, onClick } = this.props; + e.preventDefault(); + onClick(obj); + } + + render() { + return ( + + ); + } +} + +ClickableBtn.propTypes = { + onClick: React.PropTypes.func.isRequired, + text: React.PropTypes.string.isRequired, + style: React.PropTypes.object, + obj: React.PropTypes.object.isRequired +}; + +ClickableBtn.defaultProps = { + style: {} +}; + +function ChemRead({ + files, selected, getMol, addFile, removeFile, changeType, + selectSmi, removeSmi, editSmiles, exportSmi +}) { + let listItems = ; + let fileContents = ; + let disabled = true; + let selectedEditSmiles = ''; + + if (selected.length > 0) disabled = false; + if (selected.length === 1) { + const s = selected[0]; + const file = files.filter(x => x.uid === s.uid); + if (file && file[0] && file[0].info && file[0].info[s.smiIdx]) { + selectedEditSmiles = file[0].info[s.smiIdx].editedSmi || ''; + } + } + + if (files.length > 0) { + listItems = files.map(x => ( + +
+ +
{x.name}
+
+
+ )); + + fileContents = ( + + {files.map((x, index) => ( + + + {x.info.map((i, idx) => ( + + ))} + + + ))} + + ); + } + + return ( + + + +
+ Export +
+ + +
+ +
+
+ SMILES Editing +
+ + + +
+ +
+
+ + Molecules + + + Reactions + +
+
+ addFile(file)} + > + Drop Files, or Click to add File. + +
+ {listItems} +
+ + +
+ {fileContents} +
+ +
+ ); +} + +ChemRead.propTypes = { + files: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + selected: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, + addFile: React.PropTypes.func.isRequired, + removeFile: React.PropTypes.func.isRequired, + selectSmi: React.PropTypes.func.isRequired, + removeSmi: React.PropTypes.func.isRequired, + changeType: React.PropTypes.func.isRequired, + editSmiles: React.PropTypes.func.isRequired, + exportSmi: React.PropTypes.func.isRequired, + getMol: React.PropTypes.bool.isRequired +}; + +export default ChemRead; diff --git a/app/assets/javascripts/components/chemread/ChemReadContainer.js b/app/assets/javascripts/components/chemread/ChemReadContainer.js new file mode 100644 index 0000000000..3c6b41b88a --- /dev/null +++ b/app/assets/javascripts/components/chemread/ChemReadContainer.js @@ -0,0 +1,335 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import uuid from 'uuid'; +import _ from 'lodash'; +import 'whatwg-fetch'; +import XLSX from 'xlsx'; + +import { solvents } from '../staticDropdownOptions/reagents/solvents'; + +import ChemRead from './ChemRead'; +import NotificationActions from '../actions/NotificationActions'; + +function fetchInfo(files, getMol) { + const data = new FormData(); + data.append('get_mol', getMol); + files.forEach(file => data.append(file.uid, file.file)); + + return fetch('/api/v1/chemread/embedded/upload', { + credentials: 'same-origin', + method: 'post', + body: data + }).then((response) => { + if (response.ok === false) { + let msg = 'Files uploading failed: '; + if (response.status === 413) { + msg += 'File size limit exceeded. Max size is 50MB'; + } else { + msg += response.statusText; + } + + NotificationActions.add({ + message: msg, + level: 'error' + }); + } + return response.json(); + }); +} + +function fetchSvgFromSmis(smiArr) { + return fetch('/api/v1/chemread/svg/smi', { + credentials: 'same-origin', + method: 'post', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ smiArr }) + }).then((response) => { + if (response.ok === false) { + let msg = 'Files uploading failed: '; + if (response.status === 413) { + msg += 'File size limit exceeded. Max size is 50MB'; + } else { + msg += response.statusText; + } + + NotificationActions.add({ + message: msg, + level: 'error' + }); + } + return response.json(); + }); +} + +function generateTextFromInfo(name, info) { + if (!info) return ''; + + const descArr = []; + + Object.keys(info).forEach((key) => { + const desc = info[key]; + if (!desc) return; + descArr.push(`${name} ${key}:`); + + Object.keys(desc).forEach((x) => { + const dProp = desc[x]; + if (!dProp) return; + + if (x === 'detail') { + Object.keys(dProp).forEach((propKey) => { + if (propKey === 'ID' || propKey === 'parentID' || !dProp[propKey]) return; + descArr.push(` - ${propKey}: ${dProp[propKey]}`); + }); + } else { + if (!desc[x]) return; + descArr.push(` - ${x}: ${desc[x]}`); + } + }); + }); + + return descArr.join('\n'); +} + +function generateExportRow(info) { + const row = []; + const smiArr = info.smi.split('>'); + let solventsAdded = ''; + + if (info.editedSmi && info.editedSmi !== '') { + const editedSmiArr = info.editedSmi.split(','); + solventsAdded = editedSmiArr.filter(x => ( + Object.values(solvents).indexOf(x) > -1 + )).join(','); + + const allSolvents = smiArr[1].split('.').concat(editedSmiArr); + smiArr[1] = allSolvents.filter(x => x).join('.'); + } + + const temperature = []; + const time = []; + const reactionDesc = []; + const reactionYield = []; + + let reactantDescs = ''; + let productDescs = ''; + + if (info.desc) { + if (info.desc.reagents) { + Object.keys(info.desc.reagents).forEach((key) => { + const desc = info.desc.reagents[key]; + temperature.push(desc.temperature); + time.push(desc.time); + reactionYield.push(desc.yield); + reactionDesc.push(`- Description: ${desc.text}`); + }); + } + + if (info.desc.detail) { + Object.keys(info.desc.detail).forEach((k) => { + const details = info.desc.detail[k]; + + details.forEach((detail, idx) => { + const detailKey = details.length === 1 ? k : `${k} ${idx + 1}`; + const dconstructor = detail.constructor; + + if (dconstructor === Object) { + Object.keys(detail).forEach((dkey) => { + if (!detail[dkey]) return; + reactionDesc.push(`- ${dkey}: ${detail[dkey]}`); + }); + } else if (dconstructor === String) { + if (!detail) return; + reactionDesc.push(`- ${detailKey}: ${detail}`); + } + }); + }); + } + + reactantDescs = generateTextFromInfo('Reactant', info.desc.reactants); + productDescs = generateTextFromInfo('Product', info.desc.products); + } + + row.push(smiArr.join('>')); + row.push(solventsAdded); + + row.push(temperature.filter(x => x).join(';')); + row.push(reactionYield.filter(x => x).join(';')); + row.push(time.filter(x => x).join(';')); + row.push(reactionDesc.filter(x => x).join('\n')); + + row.push(reactantDescs); + row.push(productDescs); + + return row; +} + +class ChemReadContainer extends React.Component { + constructor() { + super(); + this.state = { + files: [], + getMol: false, + selected: [] + }; + + this.addFile = this.addFile.bind(this); + this.removeFile = this.removeFile.bind(this); + this.selectSmi = this.selectSmi.bind(this); + this.removeSmi = this.removeSmi.bind(this); + this.exportSmi = this.exportSmi.bind(this); + this.editSmiles = this.editSmiles.bind(this); + this.changeType = this.changeType.bind(this); + } + + addFile(files) { + const fileArr = []; + files.forEach((file) => { + const fileObj = {}; + fileObj.uid = uuid.v1(); + fileObj.name = file.name; + fileObj.file = file; + fileArr.push(fileObj); + }); + + let rsmis = []; + fetchInfo(fileArr, this.state.getMol).then((res) => { + rsmis = [].concat(res.embedded); + this.setState({ files: this.state.files.concat(rsmis) }); + }); + } + + removeFile(file) { + let files = _.cloneDeep(this.state.files); + let selected = _.cloneDeep(this.state.selected); + files = files.filter(x => x.uid !== file.uid); + selected = selected.filter(x => x.uid !== file.uid); + + this.setState({ files, selected }); + } + + selectSmi(uid, smiIdx) { + const selected = [...this.state.selected]; + const s = selected.findIndex(x => x.uid === uid && x.smiIdx === smiIdx); + if (s >= 0) { + selected.splice(s, 1); + } else { + selected.push({ uid, smiIdx }); + } + + this.setState({ selected }); + } + + removeSmi(obj) { + const { uid, idx } = obj; + const files = [...this.state.files]; + const fileIdx = files.findIndex(x => x.uid === uid); + files[fileIdx].info.splice(idx, 1); + this.setState({ files }); + } + + exportSmi(obj) { + const exportAll = obj.all; + const files = [...this.state.files]; + + const headerRow = [ + 'ReactionSmiles', 'Solvents', + 'Temperature', 'Yield', 'Time', 'Description', + 'StartingMaterialsDescription', 'ProductsDescription' + ]; + let rows = [headerRow]; + + if (exportAll) { + files.forEach((val) => { + const smis = val.info.map(x => generateExportRow(x)); + rows = rows.concat(smis); + }, []); + } else { + const selected = [...this.state.selected]; + selected.forEach((x) => { + const fileIdx = files.findIndex(f => f.uid === x.uid); + const smis = generateExportRow(files[fileIdx].info[x.smiIdx]); + rows.push(smis); + }); + } + + const wb = {}; + const ws = XLSX.utils.aoa_to_sheet(rows); + const wsName = 'ChemRead'; + + wb.SheetNames = []; + wb.Sheets = {}; + wb.SheetNames.push(wsName); + wb.Sheets[wsName] = ws; + + XLSX.writeFile(wb, 'chemread.xlsx', { bookSST: true }); + } + + changeType() { + this.setState({ getMol: !this.state.getMol }); + } + + editSmiles(solventSmi) { + const files = _.cloneDeep(this.state.files); + const { selected } = this.state; + const addedSmi = []; + + selected.forEach((s) => { + const file = files.filter(x => x.uid === s.uid); + const info = file[0].info[s.smiIdx]; + const solventSmiArr = solventSmi.split(',').filter(x => x); + + let newEditedSmi = solventSmiArr; + if (selected.length > 1) { + newEditedSmi = (info.editedSmi || '').split(',').concat(solventSmiArr); + } + newEditedSmi = [...new Set(newEditedSmi)]; + info.editedSmi = newEditedSmi.filter(x => x).join(','); + + const smiArr = info.smi.split('>'); + let newSolvents = smiArr[1].split('.').concat(newEditedSmi); + newSolvents = [...new Set(newSolvents)]; + smiArr[1] = newSolvents.filter(x => x).join('.'); + const newSmi = smiArr.join('>'); + addedSmi.push({ uid: s.uid, smiIdx: s.smiIdx, newSmi }); + }); + + fetchSvgFromSmis(addedSmi).then((r) => { + r.svg.forEach((svgInfo) => { + const file = files.filter(x => x.uid === svgInfo.uid)[0]; + if (!file) return; + file.info[svgInfo.smiIdx].svg = svgInfo.svg; + }); + + this.setState({ files }); + }); + } + + render() { + const { files, selected, getMol } = this.state; + + return ( +
+ +
+ ); + } +} + +document.addEventListener('DOMContentLoaded', () => { + const chemReadDOM = document.getElementById('ChemRead'); + if (chemReadDOM) ReactDOM.render(, chemReadDOM); +}); diff --git a/app/assets/javascripts/components/chemread/ChemReadObjectHelper.js b/app/assets/javascripts/components/chemread/ChemReadObjectHelper.js new file mode 100644 index 0000000000..1cb6ae4092 --- /dev/null +++ b/app/assets/javascripts/components/chemread/ChemReadObjectHelper.js @@ -0,0 +1,59 @@ +const ChemReadObjectHelper = { + renderSvg(svg) { + let newSvg = svg.replace(//, ''); + const viewBox = svg.match(/viewBox="(.*)"/)[1]; + newSvg = newSvg.replace(//, ''); + newSvg = newSvg.replace(/<\/svg><\/svg>/, ''); + const svgDOM = new DOMParser().parseFromString(newSvg, 'image/svg+xml'); + const editedSvg = svgDOM.documentElement; + editedSvg.removeAttribute('width'); + editedSvg.removeAttribute('height'); + editedSvg.setAttribute('viewBox', viewBox); + editedSvg.setAttribute('width', '100%'); + return editedSvg.outerHTML; + }, + extractDetails(desc) { + if (Object.keys(desc).includes('time')) { + return { description: desc.detail }; + } + + const details = {}; + Object.keys(desc).forEach((k) => { + if (!desc[k]) return; + + if (k === 'detail') { + const detailOutline = desc[k]; + + Object.keys(detailOutline).forEach((dk) => { + const detailList = detailOutline[dk]; + + detailList.forEach((detail, idx) => { + const detailKey = detailList.length === 1 ? dk : `${dk} ${idx + 1}`; + const dconstructor = detail.constructor; + + if (dconstructor === Object) { + details[detailKey] = detail; + } else if (dconstructor === String) { + const trimmedDetail = detail.trim(); + if (trimmedDetail) { + const dobj = {}; + dobj[detailKey] = trimmedDetail; + details[dk] = Object.assign(details[dk] || {}, dobj); + } + } + }); + }); + } else { + Object.keys(desc[k]).forEach((d) => { + const dk = k.endsWith('s') ? k.slice(0, -1) : k; + const dkey = `${dk} ${parseInt(d, 10) + 1}`; + details[dkey] = desc[k][d].detail; + }); + } + }); + + return details; + } +}; + +module.exports = ChemReadObjectHelper; diff --git a/app/assets/javascripts/components/chemread/DeleteBtn.js b/app/assets/javascripts/components/chemread/DeleteBtn.js new file mode 100644 index 0000000000..20b54724c0 --- /dev/null +++ b/app/assets/javascripts/components/chemread/DeleteBtn.js @@ -0,0 +1,29 @@ +import React from 'react'; + +export default class DeleteBtn extends React.Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + } + + onClick() { + const { obj, onClick } = this.props; + onClick(obj); + } + + render() { + return ( + + ); + } +} + +DeleteBtn.propTypes = { + onClick: React.PropTypes.func.isRequired, + obj: React.PropTypes.object.isRequired +}; diff --git a/app/assets/javascripts/components/chemread/ListProps.js b/app/assets/javascripts/components/chemread/ListProps.js new file mode 100644 index 0000000000..fe5f1fe3d8 --- /dev/null +++ b/app/assets/javascripts/components/chemread/ListProps.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { Label } from 'react-bootstrap'; + +function ListProps({ label, listProps }) { + if (!listProps) return ; + if (Object.values(listProps).filter(x => x).length === 0) return (); + + const list = Object.keys(listProps).filter(x => ( + x !== 'ID' && x !== 'parentID' && listProps[x] && typeof listProps[x] !== 'object' + )); + if (list.length === 0) return ; + + const propsList = list.map(k => ( +
  • + {`${k}: `} + {listProps[k]} +
  • + )); + + return ( +
    + +
      + {propsList} +
    +
    + ); +} + +ListProps.propTypes = { + label: React.PropTypes.string.isRequired, + listProps: React.PropTypes.object.isRequired +}; + +export default ListProps; diff --git a/app/assets/javascripts/components/chemread/RsmiItem.js b/app/assets/javascripts/components/chemread/RsmiItem.js new file mode 100644 index 0000000000..960b495ffd --- /dev/null +++ b/app/assets/javascripts/components/chemread/RsmiItem.js @@ -0,0 +1,81 @@ +import React from 'react'; +import { ListGroupItem } from 'react-bootstrap'; +import SvgFileZoomPan from 'react-svg-file-zoom-pan'; + +import DeleteBtn from './DeleteBtn'; +import ListProps from './ListProps'; +import XmlDetails from './XmlDetails'; + +function RsmiItem({ + desc, details, idx, removeSmi, svg, smi, selectSmi, selected, uid +}) { + const className = selected ? 'list-group-item-info' : ''; + + const descList = []; + if (Object.keys(desc).includes('time')) { + const el = ; + descList.push(el); + } else { + Object.keys(desc).forEach((group) => { + if (group === 'detail') return; + + Object.keys(desc[group]).forEach((d) => { + const dgroup = group.endsWith('s') ? group.slice(0, -1) : group; + let label = dgroup; + if (group !== 'reagents') { + label = `${dgroup} ${parseInt(d, 10) + 1}`; + } + + const list = ( + + ); + descList.push(list); + }); + }); + } + + return ( + + +
    + +
    + {smi} +
    +
    +
    + {descList} +
    +
    + +
    +
    +
    +
    + ); +} + +RsmiItem.propTypes = { + desc: React.PropTypes.object.isRequired, + details: React.PropTypes.object.isRequired, + selectSmi: React.PropTypes.func.isRequired, + removeSmi: React.PropTypes.func.isRequired, + uid: React.PropTypes.string.isRequired, + svg: React.PropTypes.string.isRequired, + smi: React.PropTypes.string.isRequired, + idx: React.PropTypes.number.isRequired, + selected: React.PropTypes.bool.isRequired +}; + +export default RsmiItem; diff --git a/app/assets/javascripts/components/chemread/RsmiItemContainer.js b/app/assets/javascripts/components/chemread/RsmiItemContainer.js new file mode 100644 index 0000000000..1fbf314763 --- /dev/null +++ b/app/assets/javascripts/components/chemread/RsmiItemContainer.js @@ -0,0 +1,61 @@ +import React from 'react'; + +import RsmiItem from './RsmiItem'; +import { renderSvg, extractDetails } from './ChemReadObjectHelper'; + +export default class RsmiItemContainer extends React.Component { + constructor() { + super(); + + this.selectSmi = this.selectSmi.bind(this); + } + + selectSmi() { + const { uid, idx } = this.props; + this.props.selectSmi(uid, idx); + } + + render() { + const { + uid, idx, selected, content, removeSmi + } = this.props; + const { + svg, smi, desc, editedSmi + } = content; + + const isSelected = selected.filter(x => ( + x.uid === uid && x.smiIdx === idx + )).length > 0; + + let displayedSmi = smi; + if (editedSmi && editedSmi !== '') { + const smiArr = smi.split('>'); + const allSolvents = smiArr[1].split('.').concat(editedSmi.split(',')); + smiArr[1] = allSolvents.filter(x => x).join('.'); + displayedSmi = smiArr.join('>'); + } + + return ( + + ); + } +} + +RsmiItemContainer.propTypes = { + selectSmi: React.PropTypes.func.isRequired, + removeSmi: React.PropTypes.func.isRequired, + uid: React.PropTypes.string.isRequired, + content: React.PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types + idx: React.PropTypes.number.isRequired, + selected: React.PropTypes.arrayOf(React.PropTypes.shape).isRequired +}; diff --git a/app/assets/javascripts/components/chemread/SelectWrapper.js b/app/assets/javascripts/components/chemread/SelectWrapper.js new file mode 100644 index 0000000000..7b7cdf5541 --- /dev/null +++ b/app/assets/javascripts/components/chemread/SelectWrapper.js @@ -0,0 +1,66 @@ +import React from 'react'; +import VirtualizedSelect from 'react-virtualized-select'; + +class SelectWrapper extends React.Component { + constructor(props) { + super(props); + this.state = { value: props.value || '' }; + + this.onSelect = this.onSelect.bind(this); + } + + componentWillReceiveProps(nextProps) { + const { value } = nextProps; + this.setState({ value }); + } + + onSelect(selected) { + const { onSelect, title } = this.props; + onSelect({ value: selected, type: title }); + } + + render() { + const { obj, title, disabled } = this.props; + const { value } = this.state; + const options = []; + const editedSmi = []; + + Object.keys(obj).forEach((k) => { + const opt = { label: k, value: obj[k] }; + options.push(opt); + + const valueArr = value.split(','); + if (valueArr.indexOf(obj[k]) > -1) { + editedSmi.push(obj[k]); + } + }); + + return ( + + ); + } +} + +SelectWrapper.propTypes = { + obj: React.PropTypes.object.isRequired, + title: React.PropTypes.string, + value: React.PropTypes.string, + disabled: React.PropTypes.bool, + onSelect: React.PropTypes.func.isRequired +}; + +SelectWrapper.defaultProps = { + title: '', + disabled: true +}; + +export default SelectWrapper; diff --git a/app/assets/javascripts/components/chemread/SmilesEditing.js b/app/assets/javascripts/components/chemread/SmilesEditing.js new file mode 100644 index 0000000000..c1c62ea859 --- /dev/null +++ b/app/assets/javascripts/components/chemread/SmilesEditing.js @@ -0,0 +1,129 @@ +import React from 'react'; +import { FormGroup, ControlLabel } from 'react-bootstrap'; + +import { catalyst } from '../staticDropdownOptions/reagents/catalyst'; +import { chiralAuxiliaries } from '../staticDropdownOptions/reagents/chiral_auxiliaries'; +import { couplingReagents } from '../staticDropdownOptions/reagents/coupling_reagents'; +import { fluorination } from '../staticDropdownOptions/reagents/fluorination'; +import { halogenationBrClI } from '../staticDropdownOptions/reagents/halogenation_BrClI'; +import { ionicLiquids } from '../staticDropdownOptions/reagents/ionic_liquids'; +import { lewisAcids } from '../staticDropdownOptions/reagents/lewis_acids'; +import { ligands } from '../staticDropdownOptions/reagents/ligands'; +import { metallorganics } from '../staticDropdownOptions/reagents/metallorganics'; +import { orgBases } from '../staticDropdownOptions/reagents/org_bases'; +import { organoboron } from '../staticDropdownOptions/reagents/organoboron'; +import { organocatalysts } from '../staticDropdownOptions/reagents/organocatalysts'; +import { oxidation } from '../staticDropdownOptions/reagents/oxidation'; +import { phaseTransferReagents } from '../staticDropdownOptions/reagents/phase_transfer_reagents'; +import { reducingReagents } from '../staticDropdownOptions/reagents/reducing_reagents'; +import { solvents } from '../staticDropdownOptions/reagents/solvents'; + +import SelectWrapper from './SelectWrapper'; + +function partitionSolventsReagents(value) { + const selectedSolvents = []; + const selectedReagents = []; + + value.split(',').forEach((x) => { + if (Object.values(solvents).indexOf(x) > -1) { + selectedSolvents.push(x); + } else { + selectedReagents.push(x); + } + }); + + return { + solvents: selectedSolvents, + reagents: selectedReagents + }; +} + +class SmilesEditing extends React.Component { + constructor(props) { + super(props); + + this.reagentsOpts = Object.assign( + {}, ionicLiquids, catalyst, ligands, + chiralAuxiliaries, couplingReagents, halogenationBrClI, fluorination, + orgBases, lewisAcids, organocatalysts, organoboron, metallorganics, + oxidation, reducingReagents, phaseTransferReagents + ); + + const partition = partitionSolventsReagents(props.value); + this.state = { + selectedReagents: partition.reagents, + selectedSolvents: partition.solvents + }; + + this.onSelect = this.onSelect.bind(this); + } + + componentWillReceiveProps(nextProps) { + const { value } = nextProps; + + const partition = partitionSolventsReagents(value); + this.setState({ + selectedReagents: partition.reagents, + selectedSolvents: partition.solvents + }); + } + + onSelect(selected) { + const { editFunc } = this.props; + const { selectedReagents, selectedSolvents } = this.state; + + let selectedArr = selected.value.split(','); + if (selected.type === 'Solvents') { + selectedArr = selectedArr.concat(selectedReagents); + } else { + selectedArr = selectedArr.concat(selectedSolvents); + } + + selectedArr = [...new Set(selectedArr.filter(x => x))]; + editFunc(selectedArr.join(',')); + } + + render() { + const { disabled } = this.props; + const { selectedReagents, selectedSolvents } = this.state; + + return ( +
    + + Solvents + + + + Reagents + + +
    + ); + } +} + +SmilesEditing.propTypes = { + editFunc: React.PropTypes.func.isRequired, + value: React.PropTypes.string, + disabled: React.PropTypes.bool +}; + +SmilesEditing.defaultProps = { + value: '', + disabled: true +}; + +export default SmilesEditing; diff --git a/app/assets/javascripts/components/chemread/XmlDetails.js b/app/assets/javascripts/components/chemread/XmlDetails.js new file mode 100644 index 0000000000..513934c602 --- /dev/null +++ b/app/assets/javascripts/components/chemread/XmlDetails.js @@ -0,0 +1,74 @@ +import React from 'react'; + +import ListProps from './ListProps'; + +export default class XmlDetails extends React.Component { + constructor() { + super(); + this.state = { + expand: false + }; + + this.onClick = this.onClick.bind(this); + } + + onClick() { + const { expand } = this.state; + this.setState({ expand: !expand }); + } + + render() { + const { details } = this.props; + if (!details) return (); + if (Object.values(details).filter(x => x).length === 0) return (); + + const { expand } = this.state; + const iconClass = expand ? 'fa-angle-down' : 'fa-angle-right'; + const iconText = expand ? 'Hide' : 'More'; + const expandBtn = ( + + ); + + const detailsList = Object.keys(details).filter(x => x && details[x]).reduce((acc, k) => { + const val = details[k]; + if (val instanceof Array) { + const els = val.map((v, idx) => { + const label = `${k} ${idx + 1}`; + return ( + + ); + }); + return acc.concat(els); + } + + const el = ( + + ); + + acc.push(el); + return acc; + }, []); + + return ( +
    +
    + {expandBtn} +
    +
    + {expand ? detailsList : } +
    +
    + ); + } +} + +XmlDetails.propTypes = { + details: React.PropTypes.object.isRequired +}; diff --git a/app/assets/javascripts/components/docx/Docx.js b/app/assets/javascripts/components/docx/Docx.js deleted file mode 100644 index c724080bf7..0000000000 --- a/app/assets/javascripts/components/docx/Docx.js +++ /dev/null @@ -1,139 +0,0 @@ -import React from 'react'; -import { - PanelGroup, ListGroup, ListGroupItem, Grid, Row, Col, Panel -} from 'react-bootstrap'; -import Dropzone from 'react-dropzone'; - -import Navigation from '../Navigation'; -import SmilesEditing from './SmilesEditing'; -import RsmiItem from './RsmiItem'; - -class CloseBtn extends React.Component { - constructor() { - super(); - this.onClick = this.onClick.bind(this); - } - - onClick() { - const { obj, onClick } = this.props; - onClick(obj); - } - - render() { - return ( - - ); - } -} - -function Docx({ - files, selected, addFile, removeFile, selectSmi, editSmiles -}) { - let listItems = ; - let fileContents = ; - let disabled = true; - if (selected.length > 0) disabled = false; - - if (files.length > 0) { - listItems = files.map(x => ( - -
    - -
    {x.name}
    -
    -
    - )); - - fileContents = ( - - {files.map((x, index) => ( - - - {x.rsmi.map((i, idx) => ( - - ))} - - - ))} - - ); - } - - return ( - -
    - - - -
    - addFile(file)} - > - Drop Files, or Click to add File. - -
    - {listItems} -
    - - - -
    - SMILES Editing -
    - - - -
    - -
    - - -
    - {fileContents} -
    - -
    -
    - ); -} - -Docx.propTypes = { - files: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - selected: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - addFile: React.PropTypes.func.isRequired, - removeFile: React.PropTypes.func.isRequired, - selectSmi: React.PropTypes.func.isRequired, - editSmiles: React.PropTypes.func.isRequired -}; - -CloseBtn.propTypes = { - onClick: React.PropTypes.func.isRequired, - obj: React.PropTypes.object.isRequired -}; - - -export default Docx; diff --git a/app/assets/javascripts/components/docx/DocxContainer.js b/app/assets/javascripts/components/docx/DocxContainer.js deleted file mode 100644 index e1e6c25dad..0000000000 --- a/app/assets/javascripts/components/docx/DocxContainer.js +++ /dev/null @@ -1,161 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import uuid from 'uuid'; -import _ from 'lodash'; -import 'whatwg-fetch'; - -import Docx from './Docx'; -import NotificationActions from '../actions/NotificationActions'; - -function fetchRsmiAndSvg(files) { - const data = new FormData(); - files.forEach(file => data.append(file.uid, file.file)); - - return fetch('/api/v1/docx/embedded/upload', { - credentials: 'same-origin', - method: 'post', - body: data - }).then((response) => { - if (response.ok === false) { - let msg = 'Files uploading failed: '; - if (response.status === 413) { - msg += 'File size limit exceeded. Max size is 50MB'; - } else { - msg += response.statusText; - } - - NotificationActions.add({ - message: msg, - level: 'error' - }); - } - return response.json(); - }); -} - -function fetchSvgFromSmis(smiArr) { - return fetch('/api/v1/docx/svg/smi', { - credentials: 'same-origin', - method: 'post', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ smiArr }) - }).then((response) => { - if (response.ok === false) { - let msg = 'Files uploading failed: '; - if (response.status === 413) { - msg += 'File size limit exceeded. Max size is 50MB'; - } else { - msg += response.statusText; - } - - NotificationActions.add({ - message: msg, - level: 'error' - }); - } - return response.json(); - }); -} - -class DocxContainer extends React.Component { - constructor() { - super(); - this.state = { - files: [], - selected: [] - }; - - this.addFile = this.addFile.bind(this); - this.removeFile = this.removeFile.bind(this); - this.selectSmi = this.selectSmi.bind(this); - this.editSmiles = this.editSmiles.bind(this); - } - - addFile(files) { - const fileArr = []; - files.forEach((file) => { - const fileObj = {}; - fileObj.uid = uuid.v1(); - fileObj.name = file.name; - fileObj.file = file; - fileArr.push(fileObj); - }); - - let rsmis = []; - fetchRsmiAndSvg(fileArr).then((res) => { - rsmis = [].concat(res.embedded); - this.setState({ files: this.state.files.concat(rsmis) }); - }); - } - - removeFile(file) { - let files = [...this.state.files]; - files = files.filter(x => x.uid !== file.uid); - this.setState({ files }); - } - - selectSmi(uid, rsmiIdx) { - const selected = [...this.state.selected]; - const s = selected.findIndex(x => x.uid === uid && x.rsmiIdx === rsmiIdx); - if (s >= 0) { - selected.splice(s, 1); - } else { - selected.push({ uid, rsmiIdx }); - } - - this.setState({ selected }); - } - - editSmiles(solventSmi) { - const files = _.cloneDeep(this.state.files); - const { selected } = this.state; - const addedSmi = []; - - selected.forEach((s) => { - const file = files.filter(x => x.uid === s.uid); - if (!file) return; - const smiArr = file[0].rsmi[s.rsmiIdx].smi.split('>'); - /* const curSolventSmi = smiArr[1] === '' ? '' : `${smiArr[1]}.`; - * smiArr[1] = curSolventSmi.concat(`${solventSmi}`); */ - smiArr[1] = solventSmi; - const newSmi = smiArr.join('>'); - file[0].rsmi[s.rsmiIdx].smi = newSmi; - addedSmi.push({ uid: s.uid, rsmiIdx: s.rsmiIdx, newSmi }); - }); - - fetchSvgFromSmis(addedSmi).then((r) => { - r.svg.forEach((svgInfo) => { - const file = files.filter(x => x.uid === svgInfo.uid)[0]; - if (!file) return; - file.rsmi[svgInfo.rsmiIdx].svg = svgInfo.svg; - }); - - this.setState({ files }); - }); - } - - render() { - const { files, selected } = this.state; - - return ( -
    - -
    - ); - } -} - -document.addEventListener('DOMContentLoaded', () => { - const docxDOM = document.getElementById('Docx'); - if (docxDOM) ReactDOM.render(, docxDOM); -}); diff --git a/app/assets/javascripts/components/docx/RsmiItem.js b/app/assets/javascripts/components/docx/RsmiItem.js deleted file mode 100644 index 349074f979..0000000000 --- a/app/assets/javascripts/components/docx/RsmiItem.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { ListGroupItem } from 'react-bootstrap'; -import SvgFileZoomPan from 'react-svg-file-zoom-pan'; - -function renderSvg(svg) { - let newSvg = svg.replace(//, ''); - const viewBox = svg.match(/viewBox="(.*)"/)[1]; - newSvg = newSvg.replace(//, ''); - newSvg = newSvg.replace(/<\/svg><\/svg>/, ''); - const svgDOM = new DOMParser().parseFromString(newSvg, 'image/svg+xml'); - const editedSvg = svgDOM.documentElement; - editedSvg.removeAttribute('width'); - editedSvg.removeAttribute('height'); - editedSvg.setAttribute('viewBox', viewBox); - editedSvg.setAttribute('width', '100%'); - return editedSvg.outerHTML; -} - -export default class RsmiItem extends React.Component { - constructor() { - super(); - - this.selectSmi = this.selectSmi.bind(this); - } - - selectSmi() { - const { uid, idx } = this.props; - this.props.selectSmi(uid, idx); - } - - render() { - const { - uid, idx, selected, svg, smi - } = this.props; - const sel = selected.filter(x => x.uid === uid && x.rsmiIdx === idx); - const className = sel.length > 0 ? 'list-group-item-info' : ''; - - return ( - - -
    - {smi} -
    -
    - ); - } -} - -RsmiItem.propTypes = { - selectSmi: React.PropTypes.func.isRequired, - uid: React.PropTypes.string.isRequired, - smi: React.PropTypes.string.isRequired, - svg: React.PropTypes.string.isRequired, - idx: React.PropTypes.number.isRequired, - selected: React.PropTypes.arrayOf(React.PropTypes.object).isRequired, - children: React.PropTypes.node -}; diff --git a/app/assets/javascripts/components/docx/SmilesEditing.js b/app/assets/javascripts/components/docx/SmilesEditing.js deleted file mode 100644 index f844dd5a76..0000000000 --- a/app/assets/javascripts/components/docx/SmilesEditing.js +++ /dev/null @@ -1,244 +0,0 @@ -import React from 'react'; -import { FormGroup, ControlLabel } from 'react-bootstrap'; -import Select from 'react-select'; - -import { catalyst } from '../staticDropdownOptions/reagents/catalyst'; -import { chiralAuxiliaries } from '../staticDropdownOptions/reagents/chiral_auxiliaries'; -import { couplingReagents } from '../staticDropdownOptions/reagents/coupling_reagents'; -import { fluorination } from '../staticDropdownOptions/reagents/fluorination'; -import { halogenationBrClI } from '../staticDropdownOptions/reagents/halogenation_BrClI'; -import { ionicLiquids } from '../staticDropdownOptions/reagents/ionic_liquids'; -import { lewisAcids } from '../staticDropdownOptions/reagents/lewis_acids'; -import { ligands } from '../staticDropdownOptions/reagents/ligands'; -import { metallorganics } from '../staticDropdownOptions/reagents/metallorganics'; -import { orgBases } from '../staticDropdownOptions/reagents/org_bases'; -import { organoboron } from '../staticDropdownOptions/reagents/organoboron'; -import { organocatalysts } from '../staticDropdownOptions/reagents/organocatalysts'; -import { oxidation } from '../staticDropdownOptions/reagents/oxidation'; -import { phaseTransferReagents } from '../staticDropdownOptions/reagents/phase_transfer_reagents'; -import { reducingReagents } from '../staticDropdownOptions/reagents/reducing_reagents'; - -const solvents = { - THF: 'C1CCCO1', - DMF: 'CN(C)C=O', - DMSO: 'CS(C)=O', - Chloroform: 'ClC(Cl)Cl', - 'methylene chloride': 'ClCCl', - acetone: 'CC(C)=O', - '1,4-dioxane': 'C1COCCO1', - 'ethyl acetate': 'CC(OCC)=O', - 'n-hexane': 'CCCCCC', - cyclohexane: 'C1CCCCC1', - 'diethyl ether': 'CCOCC', - methanol: 'CO', - ethanol: 'OCC', - water: '[H]O[H]' -}; - -class SelectWrapper extends React.Component { - constructor() { - super(); - this.state = { value: '' }; - - this.onSelect = this.onSelect.bind(this); - } - - onSelect(selected) { - const { onSelect } = this.props; - this.setState( - { value: selected }, - onSelect(selected) - ); - } - - render() { - const { obj, title, disabled } = this.props; - const options = Object.keys(obj).map(k => ({ label: k, value: obj[k] })); - - return ( -