diff --git a/gui/src/components/search/FilterContext.js b/gui/src/components/search/FilterContext.js index 875b8cf4f09c8bb4f537acea49499208cafe104f..f70a5fb479cc121ced1b9498f03bb8f7a5c56d0e 100644 --- a/gui/src/components/search/FilterContext.js +++ b/gui/src/components/search/FilterContext.js @@ -140,6 +140,7 @@ registerQuantity('results.material.structural_type', labelMaterial, 'terms') registerQuantity('results.material.functional_type', labelMaterial, 'terms') registerQuantity('results.material.compound_type', labelMaterial, 'terms') registerQuantity('results.material.material_name', labelMaterial) +registerQuantity('results.material.elements', labelElements, 'terms') registerQuantity('results.material.chemical_formula_hill', labelElements) registerQuantity('results.material.chemical_formula_anonymous', labelElements) registerQuantity('results.material.n_elements', labelElements, 'min_max', undefined, false) @@ -171,13 +172,6 @@ registerQuantity('upload_id', labelIDs) registerQuantity('results.material.material_id', labelIDs) registerQuantity('datasets.dataset_id', labelIDs) -// In regular element query we use the 'all'-postfix. -registerQuantity( - 'results.material.elements', - labelElements, - 'terms', - { set: (query, value) => (query['results.material.elements:all'] = value) } -) // In exclusive element query the elements names are sorted and concatenated // into a single string. registerQuantity( @@ -238,6 +232,17 @@ registerQuantity( }, false ) +// Restricted: controls whether materiarls search is done in a restricted mode. +registerQuantity( + 'restricted', + 'Restricted', + undefined, + { + set: () => {}, + get: () => {} + }, + false +) // Material and entry queries target slightly different fields. Here we prebuild // the mapping. @@ -415,6 +420,7 @@ export function useResetFilters() { for (let filter of quantities) { reset(queryFamily(filter)) } + reset(exclusive) }, []) return reset } @@ -557,13 +563,14 @@ export function useUpdateQueryString() { * datatypes that are directly compatible with the filter components. */ function qsToQuery(queryString) { - const query = qs.parse(queryString, { comma: true }) + const query = qs.parse(queryString, {comma: true}) const newQuery = {} for (let [key, value] of Object.entries(query)) { const split = key.split(':') key = split[0] let newKey = quantityFullnames.get(key) || key - const {type, parser} = parseMeta(newKey) + let multiple = quantityData[newKey].multiple + const {parser} = parseMeta(newKey) if (split.length !== 1) { const op = split[1] const oldValue = newQuery[newKey] @@ -574,7 +581,7 @@ function qsToQuery(queryString) { } } else { if (isArray(value)) { - value = new Set(value) + value = new Set(value.map(parser)) } else if (isPlainObject(value)) { if (!isNil(value.gte)) { value.gte = parser(value.gte) @@ -584,7 +591,7 @@ function qsToQuery(queryString) { } } else { value = parser(value) - if (type !== 'number' && type !== 'timestamp' && key !== 'visibility') { + if (multiple) { value = new Set([value]) } } @@ -663,13 +670,12 @@ export function useAgg(quantity, restrict = false, update = true, delay = 500) { const [results, setResults] = useState(undefined) const initialAggs = useRecoilValue(initialAggsState) const query = useQuery() - const exclusive = useExclusive() const firstLoad = useRef(true) // Pretty much all of the required pre-processing etc. should be done in this // function, as it is the final one that gets called after the debounce // interval. - const apiCall = useCallback((query, exclusive) => { + const apiCall = useCallback((query) => { // If the restrict option is enabled, the filters targeting the specified // quantity will be removed. This way all possible options pre-selection can // be returned. @@ -677,7 +683,7 @@ export function useAgg(quantity, restrict = false, update = true, delay = 500) { if (restrict && query && quantity in query) { delete queryCleaned[quantity] } - queryCleaned = toAPIQuery(queryCleaned, resource) + queryCleaned = toAPIQuery(queryCleaned, resource, query.restricted) const aggRequest = {} toAPIAgg(aggRequest, quantity, resource) const search = { @@ -713,12 +719,12 @@ export function useAgg(quantity, restrict = false, update = true, delay = 500) { // Make an immediate request for the aggregation values if query has been // specified. } else { - apiCall(query, exclusive) + apiCall(query) } } else { - debounced(query, exclusive) + debounced(query) } - }, [apiCall, quantity, debounced, query, exclusive, update, initialAggs]) + }, [apiCall, quantity, debounced, query, update, initialAggs]) return results } @@ -752,9 +758,10 @@ export function useScrollResults(pageSize, orderBy, order, exclusive, delay = 50 // The results are fetched as a side effect in order to not block the // rendering. This causes two renders: first one without the data, the second // one with the data. - const apiCall = useCallback((query, pageSize, orderBy, order, exclusive) => { + const apiCall = useCallback((query, pageSize, orderBy, order) => { pageAfterValue.current = undefined - const cleanedQuery = toAPIQuery(query, resource) + const restricted = query.restricted + const cleanedQuery = toAPIQuery(query, resource, restricted) const search = { owner: query.visibility || 'visible', query: cleanedQuery, @@ -814,12 +821,12 @@ export function useScrollResults(pageSize, orderBy, order, exclusive, delay = 50 return } if (firstRender.current) { - apiCall(query, pageSize, orderBy, order, exclusive) + apiCall(query, pageSize, orderBy, order) firstRender.current = false } else { - debounced(query, pageSize, orderBy, order, exclusive) + debounced(query, pageSize, orderBy, order) } - }, [apiCall, debounced, query, exclusive, pageSize, order, orderBy]) + }, [apiCall, debounced, query, pageSize, order, orderBy]) // Whenever the ordering changes, we perform a single API call that fetches // results in the new order. The amount of fetched results is based on the @@ -846,7 +853,7 @@ export function useScrollResults(pageSize, orderBy, order, exclusive, delay = 50 * @returns {object} A copy of the object with certain items cleaned into a * format that is supported by the API. */ -export function toAPIQuery(query, resource) { +export function toAPIQuery(query, resource, restricted) { // Perform custom transformations let queryCustomized = {} for (let [k, v] of Object.entries(query)) { @@ -858,10 +865,10 @@ export function toAPIQuery(query, resource) { } } - // Transform sets into lists and Quantities into SI values and modify keys - // according to target resource (entries/materials). let queryNormalized = {} - for (let [k, v] of Object.entries(queryCustomized)) { + for (const [k, v] of Object.entries(queryCustomized)) { + // Transform sets into lists and Quantities into SI values and modify keys + // according to target resource (entries/materials). let newValue if (isPlainObject(v)) { newValue = {} @@ -874,8 +881,61 @@ export function toAPIQuery(query, resource) { } else { newValue = toAPIQueryValue(v) } - k = resource === 'materials' ? quantityMaterialNames[k.split(':')[0]] : k - queryNormalized[k] = newValue + + // The query key postfixes and key remapping is done here. By default query + // items with array values get the 'any'-postfix. + let postfix + if (isArray(v)) { + const quantityPostfixMap = { + 'results.properties.available_properties': 'all', + 'results.material.elements': 'all' + } + postfix = quantityPostfixMap[k] || 'any' + } + + // For material query the keys are remapped. + let newKey = resource === 'materials' ? quantityMaterialNames[k] : k + newKey = postfix ? `${newKey}:${postfix}` : newKey + queryNormalized[newKey] = newValue + } + + if (resource === 'materials') { + // In restricted search we simply move all method/properties filters + // inside a single entries-subsection. + if (restricted) { + const entrySearch = {} + for (const [k, v] of Object.entries(queryNormalized)) { + if (k.startsWith('entries.')) { + const name = k.split('entries.').pop() + entrySearch[name] = v + delete queryNormalized[k] + } + } + if (!isEmpty(entrySearch)) { + queryNormalized.entries = entrySearch + } + // In unrestricted search we have to split each filter and each filter value + // into it's own separate entries query. These queries are then joined with + // 'and'. + } else { + const entrySearch = [] + for (const [k, v] of Object.entries(queryNormalized)) { + if (k.startsWith('entries.')) { + const newKey = k.split(':')[0] + if (isArray(v)) { + for (const item of v) { + entrySearch.push({[newKey]: item}) + } + } else { + entrySearch.push({[newKey]: v}) + } + delete queryNormalized[k] + } + } + if (entrySearch.length > 0) { + queryNormalized.and = entrySearch + } + } } return queryNormalized diff --git a/gui/src/components/search/input/InputSlider.js b/gui/src/components/search/input/InputSlider.js index 356303c7f4a29a6a46f8bfe9d54fac1b37783cf4..dc9ae0d7df8c171b9229bc6910776bfe95c82ec1 100644 --- a/gui/src/components/search/input/InputSlider.js +++ b/gui/src/components/search/input/InputSlider.js @@ -144,15 +144,10 @@ const InputSlider = React.memo(({ min = minGlobalSI max = maxGlobalSI } else { - if (filter instanceof Quantity) { - gte = filter.toSI() - lte = filter.toSI() - } else { - gte = filter.gte ? filter.gte.toSI() : minGlobalSI - lte = filter.lte ? filter.lte.toSI() : maxGlobalSI - } - min = Math.min(gte, minGlobalSI) - max = Math.max(lte, maxGlobalSI) + gte = filter.gte instanceof Quantity ? filter.gte.toSI() : filter.gte + lte = filter.lte instanceof Quantity ? filter.lte.toSI() : filter.lte + min = isNil(gte) ? minGlobalSI : Math.min(gte, minGlobalSI) + max = isNil(lte) ? maxGlobalSI : Math.max(lte, maxGlobalSI) } setMinLocal(min) setMaxLocal(max) diff --git a/gui/src/components/search/menus/FilterMainMenu.js b/gui/src/components/search/menus/FilterMainMenu.js index 0647536d38bd9f044afe8bee38ba2a0503c07cca..adc8882336bc4783482579752890c930f304e0c5 100644 --- a/gui/src/components/search/menus/FilterMainMenu.js +++ b/gui/src/components/search/menus/FilterMainMenu.js @@ -23,7 +23,7 @@ import { FilterMenuItems, FilterSubMenus } from './FilterMenu' - +import { makeStyles } from '@material-ui/core/styles' import FilterSubMenuMaterial from './FilterSubMenuMaterial' import FilterSubMenuElements from './FilterSubMenuElements' import FilterSubMenuSymmetry from './FilterSubMenuSymmetry' @@ -51,13 +51,20 @@ import { labelAuthor, labelDataset, labelIDs, - labelAccess + labelAccess, + useSearchContext } from '../FilterContext' +import InputCheckbox from '../input/InputCheckbox' /** * Swipable menu that shows the available filters on the left side of the * screen. */ +const useStyles = makeStyles(theme => ({ + restricted: { + paddingLeft: theme.spacing(2) + } +})) const FilterMainMenu = React.memo(({ open, onOpenChange, @@ -67,6 +74,8 @@ const FilterMainMenu = React.memo(({ onResultTypeChange }) => { const [value, setValue] = React.useState() + const {resource} = useSearchContext() + const styles = useStyles() return <FilterMenu selected={value} @@ -91,6 +100,15 @@ const FilterMainMenu = React.memo(({ <FilterMenuItem value={labelDataset} depth={0}/> <FilterMenuItem value={labelAccess} depth={0}/> <FilterMenuItem value={labelIDs} depth={0}/> + {resource === 'materials' && + <InputCheckbox + quantity="restricted" + label="Restricted" + description="If selected, the query will return materials that have individual calculations simultaneously matching your methodology and properties criteria." + initialValue={true} + className={styles.restricted} + ></InputCheckbox> + } </FilterMenuItems> <FilterSubMenus> <FilterSubMenuMaterial value={labelMaterial}/> diff --git a/gui/src/utils.js b/gui/src/utils.js index 4efcc30c84902237487be6bd82b42a58bb9e5efb..d3a3befd042d0e85547a0742977db82d2bb18964 100644 --- a/gui/src/utils.js +++ b/gui/src/utils.js @@ -479,7 +479,16 @@ export function parseMeta(quantity, pretty = true) { } } else { type = 'unknown' - parser = (value) => value + parser = (value) => { + const keywords = { + true: true, + false: false + } + if (value in keywords) { + return keywords[value] + } + return value + } } return {type, parser} }