diff --git a/app/api/chemotion/search_api.rb b/app/api/chemotion/search_api.rb index 2990660f41..8b791eb438 100644 --- a/app/api/chemotion/search_api.rb +++ b/app/api/chemotion/search_api.rb @@ -34,7 +34,7 @@ class SearchAPI < Grape::API optional :name, type: String optional :advanced_params, type: Array do optional :link, type: String, values: ['', 'AND', 'OR'], default: '' - optional :match, type: String, values: ['=', 'LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE', '>', '<', '>=', '@>', '<@'], default: 'LIKE' + optional :match, type: String, values: ['=', 'LIKE', 'ILIKE', 'NOT LIKE', 'NOT ILIKE', '>', '<', '>=', '<=', '@>', '<@'], default: 'LIKE' optional :table, type: String, values: %w[samples reactions wellplates screens research_plans elements segments literatures] optional :element_id, type: Integer optional :unit, type: String diff --git a/app/assets/stylesheets/search_modal.scss b/app/assets/stylesheets/search_modal.scss new file mode 100644 index 0000000000..e68a381479 --- /dev/null +++ b/app/assets/stylesheets/search_modal.scss @@ -0,0 +1,461 @@ +[dialogas=full-search].modal { + width: fit-content; + height: fit-content; + top: calc(100% - 95vh); + left: calc((100% - 90vw) / 2); +} + +[dialogas=full-search] .modal-dialog { + width: 90vw !important; + position: relative !important; + top: 0; + left: 0; + transform: none !important; + margin: 5px 20px 22px 15px; +} + +[dialogas=full-search] .modal-content { + border: 1px solid rgba(0, 0, 0, 0.4); + overflow: hidden; + height: fit-content; +} + +[dialogas=full-search] .modal-header { + background: #ddd; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); + padding: 10px 15px 10px 0; +} + +[dialogas=full-search] .modal-header .close { + opacity: 0.4; + font-size: 25px; + line-height: 1.3; +} + +[dialogas=full-search] .modal-header .form-group { + margin-bottom: 0; + font-size: 16px; +} + +[dialogas=full-search] .modal-header .move { + margin-right: 15px; +} + +[dialogas=full-search] .modal-header .modal-title { + padding: 5px 0; +} + +[dialogas=full-search] .modal-header .window-minimize { + color: rgba(0, 0, 0, 0.4); + float: right; + margin-top: -62px; + margin-right: 15px; +} + +[dialogas=full-search] .modal-header .window-minimize:hover { + cursor: pointer; +} + +[dialogas=full-search] .modal-body { + padding: 0; +} + +@media screen and (min-width: 768px) { + [dialogas=full-search] .modal-header .window-minimize { + margin-top: -22px; + } +} + +.search-selection.btn-group { + float: right; +} + +.search-selection .btn { + height: 35px; + color: #337ab7; + border: 1px solid rgba(0, 0, 0, 0.4); + margin-right: 12px; +} + +.search-selection .btn:hover { + background-color: #fff; + border: 1px solid rgba(0, 0, 0, 0.4); +} + +.search-selection .btn.active, .search-selection .btn.active:hover, .search-selection .btn.active:focus { + background-color: #5bc0de75; + box-shadow: none; + border: 1px solid rgba(0, 0, 0, 0.4); + outline: none; +} + +.search-icon { + float: left; + margin-right: 5px; +} +.search-icon i { + font-size: 1.5em; + padding-top: 1px; +} + +.form-container { + display: block; +} + +.form-container.minimized { + display: none; +} + +.collapsible-search-result.panel.panel-default { + border: none !important; + box-shadow: none !important; + margin-bottom: 0 !important; +} + +.collapsible-search-result.panel.panel-default .btn-toolbar { + margin-bottom: 8px; + margin-top: 3px; +} + +.collapsible-search-result.inactive { + display: none; +} + +.collapsible-search-result.panel-default > .panel-heading { + background: #5bc0de75; + border: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); +} + +.collapsible-search-result.panel-default > .panel-heading.inactive { + background: #a2bdc6; + border: none; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); +} + +.collapsible-search-result .panel-title a:hover, .collapsible-search-result .panel-title a:link { + text-decoration: none; + display: block; +} + +.collapsible-search-result #tabList { + height: 46.8vh; + @media screen and (min-width: 1600px) { + height: 51vh; + } +} + +.no-selected-search { + padding: 15px; +} + +.search-spinner, .tab-spinner { + color: #ddd; + position: absolute; + top: 0; + left: 50%; +} + +i.fa.icon-right { + float: right; +} + +.advanced-search { + margin-bottom: 39px; + padding-left: 4px; + height: 58.5vh; + position: relative; + + @media screen and (min-width: 1600px) { + height: 65vh; + } + + .scrollable-content { + clear: both; + overflow-y: auto; + height: calc(58.5vh - 65px); + margin-top: 23px; + .alert { + margin-left: 15px; + } + @media screen and (min-width: 1600px) { + height: calc(65vh - 65px); + } + } + + .Select-menu, .css-4ljt47-MenuList { + max-height: 250px; + } + .toggle-elements { + width: calc(100% - 5px); + } + .btn-group { + border-bottom: 1px solid rgba(0, 0, 0, 0.4); + } + .btn-group .btn { + padding: 0 25px; + border: 1px solid rgba(0, 0, 0, 0.4); + border-bottom: none; + border-radius: 0; + color: rgba(0, 0, 0, 0.5); + height: 45px; + } + .btn-group .btn:hover { + background-color: #fff; + } + .btn-group .btn:first-child { + border-top-left-radius: 4px; + } + .btn-group .btn:last-child { + border-top-right-radius: 4px; + } + .btn-group .btn.active { + background-color: #5bc0de75; + box-shadow: none; + } + .btn-group i { + font-size: 2.2em; + color: #337ab7; + } + .btn-group .btn:hover i { + color: rgba(0, 0, 0, 0.6); + } + .btn-group i.icon-research_plan { + font-size: 1.6em; + padding: 6px 0 5px 0; + display: inline-block; + } + .btn-group i.icon_generic_nav { + font-size: 1.8em; + padding: 10px 0 8px 0; + } + .css-1okebmr-indicatorSeparator { + background-color: #fff; + } + .css-18ng2q5-group { + color: #5bc0de; + font-size: 13px; + font-weight: bold; + } + .css-syji7d-Group .css-yt9ioa-option, .css-syji7d-Group .css-1n7v3ny-option { + padding-left: 30px; + } + .vertical-buttons { + -webkit-transform: rotate(-90deg) translate(-100%, 0); + -moz-transform: rotate(-90deg) translate(-100%, 0); + -o-transform: rotate(-90deg) translate(-100%, 0); + transform: rotate(-90deg) translate(-100%, 0); + -webkit-transform-origin: -17px 15px; + -moz-transform-origin: -17px 15px; + -o-transform-origin: -17px 15px; + transform-origin: -17px 15px; + width: 43.8vh; + border-bottom: none; + } + + .vertical-buttons .btn { + padding: 3px 25px; + width: 50%; + height: 33px; + font-size: 1.2em; + border-bottom: 1px solid rgba(0,0,0, 0.4); + background-color: #ddd; + } + .vertical-buttons .btn.active { + background-color: #fff; + color: #347ab8; + border-bottom: none; + } + + .advanced-detail-switch { + height: 0; + width: 0; + visibility: hidden; + } + + .advanced-detail-switch-label { + display: flex; + align-items: center; + justify-content: space-between; + cursor: pointer; + width: 115px; + height: 30px; + position: relative; + float: right; + margin-top: -40px; + margin-bottom: 25px; + margin-right: 6px; + padding: 0 34px 0 17px; + color: #347ab8; + background-color: #b7e2ef; + font-weight: normal; + border-radius: 100px; + } + + .advanced-detail-switch-label.active { + padding: 0 17px 0 36px; + } + + .advanced-detail-switch-label .advanced-detail-switch-button { + content: ''; + position: absolute; + top: 2px; + right: 2px; + width: 26px; + height: 26px; + border-radius: 26px; + transition: right 2s; + background: #fff; + box-shadow: 0 0 2px 0 rgba(10, 10, 10, 0.29); + } + + .advanced-detail-switch-label.active .advanced-detail-switch-button { + right: calc(115px - 28px); + } + + .detail-search { + overflow: visible !important; + margin-left: 15px; + } + .detail-search .form-group, .detail-search .checkbox, .detail-search .ant-select { + width: 49%; + margin-right: 1%; + } + .detail-search .sub-group-with-addon-2col { + width: 100%; + } + .detail-search .sub-group-with-addon-2col:last-child { + margin-right: 0; + } + @media screen and (min-width: 1023px) { + .detail-search .form-group, .detail-search .checkbox, .detail-search .ant-select { + width: 24%; + margin-right: 1%; + } + .detail-search .sub-group-with-addon-2col { + width: 49%; + } + } + .detail-search .ant-select { + width: 100%; + } + .detail-search .checkbox, .detail-search .radio + .radio, .detail-search .checkbox + .checkbox, .detail-search-headline + .checkbox { + padding-top: 5px; + margin-top: -5px; + } + .detail-search .form-group + .checkbox, .detail-search .form-group + .checkbox + .checkbox, .detail-search .detail-search-headline + .checkbox { + padding-top: 17px; + margin-top: 10px; + } + .detail-search-headline, .detail-search-segment-headline { + flex-basis: 100%; + font-weight: 700; + font-size: 1.15em; + color: rgba(0, 0, 0, 0.5); + border-bottom: 1px solid rgba(0, 0, 0, 0.5); + padding-bottom: 3px; + margin-bottom: 15px; + margin-top: 3px; + } + .detail-search-headline { + background-color: #f5f5f5; + color: #333; + padding: 5px; + padding-left: 10px; + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + } + .detail-search-headline + .detail-search-headline { + color: rgba(0, 0, 0, 0.5); + background-color: transparent; + border-top: none; + padding: 0; + padding-bottom: 3px; + border-bottom: 1px solid rgba(0, 0, 0, 0.5); + } + .detail-search-segment-headline { + font-size: 1.5em; + color: rgba(0, 0, 0, 0.6); + margin-top: 20px; + } + .css-yk16xz-control, .css-1pahdxg-control { + min-height: 34px; + height: 34px; + } + .css-1wa3eu0-placeholder, .css-1uccc91-singleValue { + top: 42%; + } + .css-26l3qy-menu { + margin-top: 0; + } + ul.ant-select-tree li .ant-select-tree-node-content-wrapper, .react-select-4-option-0 { + display: block; + height: 17px; + } + .detail-search ul.ant-select-tree li:first-child span.ant-select-tree-node-content-wrapper { + height: 17px; + } + .detail-search .css-26l3qy-menu { + z-index: 10000; + } + .detail-search hr.generic-spacer { + border-top: 1px solid rgba(0, 0, 0, 0.5); + margin-top: 3px; + margin-bottom: 15px; + flex-basis: 100%; + } + .detail-search .grey-field { + background-color: #555; + border: 1px solid #ccc; + } + .detail-search .grouped-sub-fields { + display: flex; + justify-content: space-between; + width: 100%; + } + .detail-search .subfields-with-addon-left-3 { + width: 32%; + margin-right: 1%; + } + .detail-search .subfields-with-addon-left-2 { + width: 49%; + margin-right: 1%; + } +} + +#detail-search-form-element-tabs, #detail-search-form-element-tabs .nav-tabs { + width: 100%; +} +#detail-search-form-element-tabs [id^=detail-search-form-element-tabs-pane-] { + display: none; + width: 100%; + padding-top: 15px; +} +#detail-search-form-element-tabs [id^=detail-search-form-element-tabs-pane-].active { + display: flex; + flex-wrap: wrap; +} +#detail-search-form-element-tabs .nav-tabs { + border-bottom: 1px solid rgba(0,0,0, 0.4); + margin-top: 2px; +} +#detail-search-form-element-tabs .nav-tabs li a { + margin-right: 2px; + padding-top: 8px; + padding-bottom: 8px; + color: #555; + border-bottom: 1px; +} +#detail-search-form-element-tabs .nav-tabs li.active a { + color: #337ab7; + border: 1px solid rgba(0,0,0, 0.4); + border-bottom-color: transparent; +} +.search-info-button { + display: inline-block; + margin-left: 5px; +} + +.result-error-message { + margin-top: 20px; +} diff --git a/app/packs/src/components/searchModal/forms/DetailSearch.js b/app/packs/src/components/searchModal/forms/DetailSearch.js index 1f58e7ce4d..c680157739 100644 --- a/app/packs/src/components/searchModal/forms/DetailSearch.js +++ b/app/packs/src/components/searchModal/forms/DetailSearch.js @@ -595,18 +595,16 @@ const DetailSearch = () => { case 'value_measurement': case 'solvent_ratio': case 'molecular_mass': - return '>='; + return searchStore.numeric_match; case 'unit_measurement': case 'solvent_smiles': return '='; default: - return type == 'system-defined' ? '>=' : 'ILIKE'; + return type == 'system-defined' ? searchStore.numeric_match : 'ILIKE'; } } const checkValueForNumber = (label, value) => { - if (value === '') { return null; } - let validationState = null; let message = `${label}: Only numbers are allowed`; searchStore.removeErrorMessage(message); @@ -614,7 +612,7 @@ const DetailSearch = () => { const regex = /^[0-9\s\-]+$/; let numericCheck = label.includes('point') ? !regex.test(value) : isNaN(Number(value)); - if (numericCheck) { + if (numericCheck && value !== '') { searchStore.addErrorMessage(message); validationState = 'error'; } @@ -667,14 +665,14 @@ const DetailSearch = () => { const setSearchStoreValues = (value, option, column, type, subValue, smiles) => { let searchValue = searchValueByStoreOrDefaultValue(column); - let cleanedValue = ['>=', '<@'].includes(searchValue.match) ? value.replace(/,/g, '.') : value; + let cleanedValue = ['>=', '<=', '<@'].includes(searchValue.match) ? value.replace(/,/g, '.') : value; searchValue.field = option; searchValue.value = cleanedValue; searchValue.sub_values = subValuesForSearchValue(searchValue, subValue, cleanedValue); searchValue.match = matchByField(column, type); searchValue.smiles = smiles; - if (['>=', '<@'].includes(searchValue.match)) { + if (['>=', '<=', '<@'].includes(searchValue.match)) { searchValue.validationState = checkValueForNumber(option.label, cleanedValue); } diff --git a/app/packs/src/components/searchModal/forms/SearchModalFunctions.js b/app/packs/src/components/searchModal/forms/SearchModalFunctions.js index b44af3b514..51858bd6c1 100644 --- a/app/packs/src/components/searchModal/forms/SearchModalFunctions.js +++ b/app/packs/src/components/searchModal/forms/SearchModalFunctions.js @@ -120,7 +120,7 @@ const searchValuesByAvailableOptions = (val, table) => { link = i === 0 ? 'OR' : 'AND'; match = 'NOT LIKE'; } - if (!option.unit || option.unit !== val.unit) { + if (!option.unit || option.unit.replace('°', '') !== val.unit.replace('°', '')) { searchValues.push([link, table, val.field.label.toLowerCase(), match, option.value, option.unit].join(" ")); } }); diff --git a/app/packs/src/components/searchModal/forms/TextSearch.js b/app/packs/src/components/searchModal/forms/TextSearch.js index 50c91614c7..ab83508612 100644 --- a/app/packs/src/components/searchModal/forms/TextSearch.js +++ b/app/packs/src/components/searchModal/forms/TextSearch.js @@ -1,8 +1,8 @@ import React, { useEffect, useContext } from 'react'; -import { ToggleButtonGroup, ToggleButton, Tooltip, OverlayTrigger, Stack, Accordion } from 'react-bootstrap'; +import { ToggleButtonGroup, ToggleButton, Tooltip, OverlayTrigger, Stack, Accordion, Form } from 'react-bootstrap'; import { - togglePanel, handleClear, showErrorMessage, handleSearch, - AccordeonHeaderButtonForSearchForm, SearchButtonToolbar, panelVariables + togglePanel, handleClear, showErrorMessage, panelVariables, + AccordeonHeaderButtonForSearchForm, SearchButtonToolbar } from './SearchModalFunctions'; import UserStore from 'src/stores/alt/stores/UserStore'; import AdvancedSearchRow from './AdvancedSearchRow'; @@ -55,6 +55,10 @@ const TextSearch = () => { searchStore.addAdvancedSearchValue(0, searchValues); } + const handleNumericMatchChange = (e) => { + searchStore.changeNumericMatchValue(e.target.value); + } + const SelectSearchTable = () => { const layout = UserStore.getState().profile.data.layout; @@ -191,9 +195,34 @@ const TextSearch = () => {
- handleClear(searchStore)} - /> +
+ handleClear(searchStore)} /> + { + searchStore.searchType == 'detail' && ( + + Change search operator for numeric Fields: + ='} + onChange={handleNumericMatchChange} + /> + ='), search_results: types.map(SearchResult), tab_search_results: types.map(SearchResult), search_accordion_active_key: types.optional(types.number, 0), @@ -174,6 +175,18 @@ export const SearchStore = types self.detail_search_values.splice(index, 1); } }, + changeNumericMatchValue(match) { + self.numeric_match = match; + self.detail_search_values.map((object, i) => { + if (['>=', '<='].includes(Object.values(object)[0].match)) { + Object.entries(self.detail_search_values[i]).forEach(([key, value]) => { + let values = { ...value }; + values.match = match; + self.detail_search_values[i] = { [key]: values }; + }); + } + }); + }, changeKetcherRailsValue(key, value) { let ketcherValues = { ...self.ketcher_rails_values }; ketcherValues[key] = value; diff --git a/app/usecases/search/conditions_for_advanced_search.rb b/app/usecases/search/conditions_for_advanced_search.rb index 2790e0a5e4..cda56d405c 100644 --- a/app/usecases/search/conditions_for_advanced_search.rb +++ b/app/usecases/search/conditions_for_advanced_search.rb @@ -155,13 +155,25 @@ def sanitize_words(filter) return [filter['smiles']] if filter['field']['column'] == 'solvent' return [filter['value'].to_f] if sanitize_float_fields(filter) - no_sanitizing_matches = ['=', '>='] + no_sanitizing_matches = ['=', '>=', '<='] sanitize = no_sanitizing_matches.exclude?(filter['match']) words = filter['value'].split(/(\r)?\n/).map!(&:strip) words = words.map { |e| "%#{ActiveRecord::Base.send(:sanitize_sql_like, e)}%" } if sanitize words end + def valid_temperature(prop, number) + regex_number = "'^-{0,1}\\d+(\\.\\d+){0,1}\\Z'" + "(#{prop} ->> '#{number}' ~ #{regex_number})" + end + + def temperature_field_specials(prop, number, unit) + @conditions[:words][0] = @conditions[:words][0].to_f.to_s + @conditions[:field] = "(#{prop} ->> '#{number}')::FLOAT" + @conditions[:first_condition] += + " (#{prop} ->> '#{unit}')::TEXT != '' AND #{valid_temperature(prop, number)} AND " + end + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity def special_non_generic_field_options(filter) @@ -187,12 +199,9 @@ def special_non_generic_field_options(filter) AND private_notes.noteable_id = #{@table}.id" @conditions[:condition_table] = 'private_notes.' when 'temperature' - regex_number = "'^-{0,1}\\d+(\\.\\d+){0,1}\\Z'" - is_data_valid = "(#{@table}.temperature ->> 'userText' ~ #{regex_number})" + # is_data_valid = valid_temperature("#{@table}.temperature", 'userText') field = filter['field']['column'] - @conditions[:field] = "(#{@table}.temperature ->> 'userText')::FLOAT" - @conditions[:first_condition] += - " (#{@table}.temperature ->> 'valueUnit')::TEXT != '' AND #{is_data_valid} AND " + temperature_field_specials("#{@table}.temperature", 'userText', 'valueUnit') unit_and_available_options_conditions(filter, "#{@table}.temperature", field, 'userText', 'valueUnit') @conditions[:condition_table] = '' when 'duration' @@ -379,6 +388,7 @@ def dataset_tab_options(filter, number) @conditions[:additional_condition] = "AND (#{prop} ->> 'field')::TEXT = '#{field}'" if filter['unit'].present? || filter['available_options'].present? + temperature_field_specials(prop, 'value', 'value_system') if field == 'temperature' unit_and_available_options_conditions(filter, prop, field, 'value', 'value_system') end end @@ -392,6 +402,7 @@ def remove_degree_from_property(prop, unit) "LOWER(replace((#{prop} ->> '#{unit}')::TEXT, '°', ''))" end + # rubocop:disable Metrics/AbcSize def unit_and_available_options_conditions(filter, prop, field, number, unit) if filter['unit'].present? @conditions[:additional_condition] += @@ -403,10 +414,11 @@ def unit_and_available_options_conditions(filter, prop, field, number, unit) conditions = '' filter['available_options'].each do |option| if field.include?('temperature') - next if option[:unit] == filter['unit'] + next if option[:unit].remove('°') == filter['unit'].remove('°') @conditions[:additional_condition] += - " OR ((#{prop} ->> '#{number}')::TEXT >= '#{option[:value]}' + " OR (#{valid_temperature(prop, number)} + AND (#{prop} ->> '#{number}')::FLOAT #{@match} '#{option[:value].to_f}' AND #{remove_degree_from_property(prop, unit)} = LOWER('#{remove_degree_from_unit(option)}'))" else conditions += " AND (#{prop} ->> '#{number}')::TEXT NOT ILIKE '%#{option[:value]}%'" @@ -417,6 +429,7 @@ def unit_and_available_options_conditions(filter, prop, field, number, unit) @conditions[:additional_condition] += " OR ((#{prop} ->> 'field')::TEXT = '#{field}'#{conditions})" end + # rubocop:enable Metrics/AbcSize def datasets_joins(filter, prop, key) datasets_join =