Skip to content

Commit

Permalink
feat: advanced search for analyses datasets (#2179)
Browse files Browse the repository at this point in the history
* Add additional options and converter for temperature at detail search

* Enable generic dataset search after rebase

* Add available options logic to dataset queries

* Add selection of numeric operator, fixes for queries

* Add new available options to search spec

* Fix query for fields with field name type for generic datasets

* Delete old search modal scss
  • Loading branch information
beque authored Nov 14, 2024
1 parent a47c6ac commit 2e624f7
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 36 deletions.
3 changes: 2 additions & 1 deletion app/api/chemotion/search_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ 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
requires :field, type: Hash
requires :value, type: String
optional :smiles, type: String
optional :sub_values, type: Array
optional :available_options, type: Array
end
optional :id_params, type: Hash do
requires :model_name, type: String, values: %w[
Expand Down
59 changes: 48 additions & 11 deletions app/packs/src/components/searchModal/forms/DetailSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import UIStore from 'src/stores/alt/stores/UIStore';
import { observer } from 'mobx-react';
import { StoreContext } from 'src/stores/mobx/RootStore';
import { ionic_liquids } from 'src/components/staticDropdownOptions/ionic_liquids';
import { convertTemperature } from 'src/utilities/UnitsConversion';
import * as FieldOptions from 'src/components/staticDropdownOptions/options';

const DetailSearch = () => {
Expand Down Expand Up @@ -45,6 +46,7 @@ const DetailSearch = () => {
smiles: '',
sub_values: [],
unit: '',
available_options: [],
validationState: null
}];

Expand Down Expand Up @@ -194,7 +196,7 @@ const DetailSearch = () => {
}
if (value[0] === 'analyses') {
fieldsByTab.push(...analysesData);
// pushDatasetsToAnalysesFields();
pushDatasetsToAnalysesFields();
}
if (value[0] === 'inventory') {
fieldsByTab.push(...inventoryData);
Expand Down Expand Up @@ -593,26 +595,24 @@ 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);

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';
}
Expand All @@ -637,8 +637,9 @@ const DetailSearch = () => {
let searchValue = searchValueByStoreOrDefaultValue(column);

if (optionField.value_system) {
let valueSystem =
searchValue.sub_values.length >= 1 && searchValue.sub_values[0][id] ? searchValue.sub_values[0][id].value_system : optionField.value_system;
let valueSystem = searchValue.sub_values.length >= 1 && searchValue.sub_values[0][id]
? searchValue.sub_values[0][id].value_system
: optionField.value_system;
subValue = { id: id, value: { value: value, value_system: valueSystem } };
} else {
subValue = { id: id, value: value };
Expand All @@ -664,25 +665,40 @@ 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);
}

if (type == 'system-defined' && searchValue.unit === '') {
let units = optionsForSelect(option);
searchValue.unit = units[0].label;
}

if (column.indexOf('temperature') !== -1 && value !== '' && value !== 0 && value !== "0") {
searchValue = availableOptionsForTemperature(searchValue, value, searchValue.unit);
}

if (value === 'others' && option.type === 'select') {
searchValue.available_options = [];
optionsForSelect(option).map((object) => {
if (object.value !== '' && object.value !== 'others') {
searchValue.available_options.push(object);
}
});
}

let searchSubValuesLength = searchValue.sub_values.length >= 1 ? Object.keys(searchValue.sub_values[0]).length : 0;
let typesWithSubValues = ['input-group', 'table'];

if (((value === '' || value === false) && !typesWithSubValues.includes(type)) || (searchSubValuesLength === 0 && typesWithSubValues.includes(type) && value === '')) {
if (((value === '' || value === false) && !typesWithSubValues.includes(type))
|| (searchSubValuesLength === 0 && typesWithSubValues.includes(type) && value === '')) {
searchStore.removeDetailSearchValue(column);
} else {
searchStore.addDetailSearchValue(column, searchValue);
Expand All @@ -706,6 +722,22 @@ const DetailSearch = () => {
return subValues;
}

const availableOptionsForTemperature = (searchValue, startValue, startUnit) => {
startValue = startValue.match(/^-?\d+(\.\d+)?$/g);

if (startValue === null || isNaN(Number(startValue))) { return searchValue; }

searchValue.available_options = [];
searchValue.available_options.push({ value: startValue[0], unit: startUnit });

let [convertedValue, convertedUnit] = convertTemperature(startValue[0], startUnit);
searchValue.available_options.push({ value: convertedValue.trim(), unit: convertedUnit });

[convertedValue, convertedUnit] = convertTemperature(convertedValue, convertedUnit);
searchValue.available_options.push({ value: convertedValue.trim(), unit: convertedUnit });
return searchValue;
}

const changeUnit = (units, value, column, option, subFieldId) => (e) => {
let activeUnitIndex = units.findIndex((f) => { return f.label.replace('°', '') === value || f.label === value });
let nextUnitIndex = activeUnitIndex === units.length - 1 ? 0 : activeUnitIndex + 1;
Expand All @@ -722,7 +754,12 @@ const DetailSearch = () => {
}
}

if (column.indexOf('temperature') !== -1 && searchValue.value !== '') {
const nextValue = searchValue.available_options.find((v) => newUnit.indexOf(v.unit) !== -1);
searchValue.value = nextValue.value;
}
searchValue.unit = newUnit;

searchStore.addDetailSearchValue(column, searchValue);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,23 @@ const searchValuesBySubFields = (val, table) => {
return searchValues;
}

const searchValuesByAvailableOptions = (val, table) => {
let searchValues = [];
let link = 'OR';
let match = val.match;

val.available_options.map((option, i) => {
if (val.field.column.indexOf('temperature') === -1) {
link = i === 0 ? 'OR' : 'AND';
match = 'NOT LIKE';
}
if (!option.unit || option.unit.replace('°', '') !== val.unit.replace('°', '')) {
searchValues.push([link, table, val.field.label.toLowerCase(), match, option.value, option.unit].join(" "));
}
});
return searchValues;
}

const searchValuesByFilters = (store) => {
const storedFilter = store.searchFilters;
const filters = storedFilter.length == 0 ? [] : storedFilter[0].filters;
Expand All @@ -120,11 +137,15 @@ const searchValuesByFilters = (store) => {
let table = val.field.table || val.table;
let value = val.value;
table = table.charAt(0).toUpperCase() + table.slice(1, -1).replace('_', ' ');
value = value != true ? value.replace(/[\n\r]/g, ' OR ') : value;
value = value && value !== true ? value.replace(/[\n\r]/g, ' OR ') : value;

if (val.field.sub_fields && val.field.sub_fields.length >= 1 && val.sub_values.length >= 1) {
let values = searchValuesBySubFields(val, table);
searchValues.push(...values);
} else if (val.available_options) {
let values = searchValuesByAvailableOptions(val, table);
searchValues.push([val.link, table, val.field.label.toLowerCase(), val.match, value, val.unit].join(" "));
searchValues.push(...values);
} else {
searchValues.push([val.link, table, val.field.label.toLowerCase(), val.match, value, val.unit].join(" "));
}
Expand Down
2 changes: 1 addition & 1 deletion app/packs/src/components/searchModal/forms/SearchResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const SearchResult = ({ handleClear }) => {
<OverlayTrigger delayShow={500} placement="top" overlay={tooltip}>
<div className="d-inline-flex align-items-center">
<i className={`${iconClass} pe-1`} />
<span class="fs-3">
<span className="fs-3">
({tabResult.total_elements})
</span>
</div>
Expand Down
43 changes: 36 additions & 7 deletions app/packs/src/components/searchModal/forms/TextSearch.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -177,7 +181,34 @@ const TextSearch = () => {
)
}
</div>
<SearchButtonToolbar store={searchStore} />
<Form className="d-flex align-items-center gap-5">
<SearchButtonToolbar store={searchStore} />
{
searchStore.searchType == 'detail' && (
<Form.Group className="d-flex align-items-baseline gap-4">
<span>Change search operator for numeric Fields:</span>
<Form.Check
type="radio"
name="equal_higher_or_lower"
id="equal_higher_or_lower_higher"
label=">="
value=">="
checked={searchStore.numeric_match === '>='}
onChange={handleNumericMatchChange}
/>
<Form.Check
type="radio"
name="equal_higher_or_lower"
id="equal_higher_or_lower_higher"
label="<="
value="<="
checked={searchStore.numeric_match === '<='}
onChange={handleNumericMatchChange}
/>
</Form.Group>
)
}
</Form>
</div>
</Accordion.Collapse>
</Accordion.Item>
Expand All @@ -191,9 +222,7 @@ const TextSearch = () => {
</h2>
<Accordion.Collapse eventKey={1}>
<div className="accordion-body">
<SearchResult
handleClear={() => handleClear(searchStore)}
/>
<SearchResult handleClear={() => handleClear(searchStore)} />
</div>
</Accordion.Collapse>
</Accordion.Item>
Expand Down
14 changes: 14 additions & 0 deletions app/packs/src/stores/mobx/SearchStore.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const defaultSearchValues = [{
smiles: '',
sub_values: [],
unit: '',
available_options: [],
validationState: null
}];

Expand Down Expand Up @@ -68,6 +69,7 @@ export const SearchStore = types
detail_search_values: types.optional(types.array(types.frozen({})), []),
ketcher_rails_values: types.optional(types.frozen({}), defaultKetcherValues),
publication_search_values: types.optional(types.array(types.frozen({})), defaultPublicationValues),
numeric_match: types.optional(types.string, '>='),
search_results: types.map(SearchResult),
tab_search_results: types.map(SearchResult),
search_accordion_active_key: types.optional(types.number, 0),
Expand Down Expand Up @@ -173,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;
Expand Down
3 changes: 2 additions & 1 deletion app/packs/src/utilities/UnitsConversion.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const convertTemperature = (valueToFormat, currentUnit) => {
}
const conversions = {
K: { convertedUnit: TEMPERATURE_UNITS.CELSIUS, conversionFunc: kelvinToCelsius },
'°K': { convertedUnit: TEMPERATURE_UNITS.CELSIUS, conversionFunc: kelvinToCelsius },
'°C': { convertedUnit: TEMPERATURE_UNITS.FAHRENHEIT, conversionFunc: celsiusToFahrenheit },
'°F': { convertedUnit: TEMPERATURE_UNITS.KELVIN, conversionFunc: fahrenheitToKelvin },
};
Expand Down Expand Up @@ -132,7 +133,7 @@ const calculateVolumeForFeedstockOrGas = (amountGram, molecularWeight, purity, g
};

const calculateGasMoles = (volume, ppm, temperatureInKelvin) => (ppm * volume)
/ (IDEAL_GAS_CONSTANT * temperatureInKelvin * PARTS_PER_MILLION_FACTOR);
/ (IDEAL_GAS_CONSTANT * temperatureInKelvin * PARTS_PER_MILLION_FACTOR);

const calculateFeedstockMoles = (volume, purity) => (volume * purity) / (
IDEAL_GAS_CONSTANT * DEFAULT_TEMPERATURE_IN_KELVIN);
Expand Down
Loading

0 comments on commit 2e624f7

Please sign in to comment.