From a8bff395b01fe6557598f3d1c6f8afb66caa4e17 Mon Sep 17 00:00:00 2001 From: Stefano Ricci <1219739+SteRiccio@users.noreply.github.com> Date: Fri, 29 Nov 2024 17:07:46 +0100 Subject: [PATCH] Fix data explorer error when using function call as filter (#3675) * data explorer: support isNotEmpty function * sort editor: keep only first variable in composite attributes * fixed filtering using function call --------- Co-authored-by: Stefano Ricci Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- core/expressionParser/toSql/toSql.js | 3 ++- .../ButtonBar/ButtonFilter/ButtonFilter.js | 9 +++---- .../ButtonSort/SortEditor/SortEditor.js | 4 +--- .../SortEditor/store/useSortEditor.js | 11 +++++++-- .../components/expression/expressionParser.js | 24 +++++++++++-------- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/core/expressionParser/toSql/toSql.js b/core/expressionParser/toSql/toSql.js index b5adb97714..a049bf5918 100644 --- a/core/expressionParser/toSql/toSql.js +++ b/core/expressionParser/toSql/toSql.js @@ -13,6 +13,7 @@ const stdlib2sql = { sum: 'sum', avg: 'avg', isEmpty: (param) => `coalesce(${param}, '') = ''`, + isNotEmpty: (param) => `coalesce(${param}, '') <> ''`, '!': (param) => `NOT (${param})`, } @@ -129,7 +130,7 @@ export const identifier = (node, params) => { export const call = (node, params) => { const { callee, arguments: argumentsNode } = node - const { name } = callee + const name = callee.name ?? callee.value // callee can be literal or identifier const sqlFnNameOrFn = stdlib2sql[name] if (!sqlFnNameOrFn) { throw new SystemError('undefinedFunction', { name }) diff --git a/webapp/components/DataQuery/ButtonBar/ButtonFilter/ButtonFilter.js b/webapp/components/DataQuery/ButtonBar/ButtonFilter/ButtonFilter.js index 2f7a58075e..d879b835e8 100644 --- a/webapp/components/DataQuery/ButtonBar/ButtonFilter/ButtonFilter.js +++ b/webapp/components/DataQuery/ButtonBar/ButtonFilter/ButtonFilter.js @@ -6,16 +6,15 @@ import { Query } from '@common/model/query' import { ButtonIconFilter } from '@webapp/components/buttons' import ExpressionEditorPopup from '@webapp/components/expression/expressionEditorPopup' +import * as ExpressionParser from '@webapp/components/expression/expressionParser' import { DataExplorerHooks, DataExplorerSelectors } from '@webapp/store/dataExplorer' -import { useI18n } from '@webapp/store/system' import { State } from '../store' const ButtonFilter = (props) => { const { disabled, state, Actions } = props - const i18n = useI18n() const query = DataExplorerSelectors.useQuery() const onChangeQuery = DataExplorerHooks.useSetQuery() @@ -35,6 +34,7 @@ const ButtonFilter = (props) => { {State.isPanelFilterShown(state) && ( { excludeCurrentNodeDef={false} query={filter ? Expression.toString(filter) : ''} mode={Expression.modes.sql} - header={i18n.t('dataView.filterRecords.expressionEditorHeader')} + header="dataView.filterRecords.expressionEditorHeader" onChange={({ expr }) => { - onChangeQuery(Query.assocFilter(expr)(query)) + const exprNormalized = ExpressionParser.normalize({ expr, canBeCall: true }) + onChangeQuery(Query.assocFilter(exprNormalized)(query)) Actions.closePanels() }} onClose={Actions.closePanels} diff --git a/webapp/components/DataQuery/ButtonBar/ButtonSort/SortEditor/SortEditor.js b/webapp/components/DataQuery/ButtonBar/ButtonSort/SortEditor/SortEditor.js index 3363aebcf0..ad48fdaed8 100644 --- a/webapp/components/DataQuery/ButtonBar/ButtonSort/SortEditor/SortEditor.js +++ b/webapp/components/DataQuery/ButtonBar/ButtonSort/SortEditor/SortEditor.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types' import { Button } from '@webapp/components/buttons' import PanelRight from '@webapp/components/PanelRight' -import { useI18n } from '@webapp/store/system' import { Sort, SortCriteria } from '@common/model/query' import { useSortEditor } from './store' @@ -13,11 +12,10 @@ import SortCriteriaEditor from './SortCriteriaEditor' const SortEditor = (props) => { const { onChange, onClose, query } = props - const i18n = useI18n() const { draft, sort, sortDraft, setSortDraft, variables, variablesAvailable } = useSortEditor({ query }) return ( - +
{sortDraft.map((sortCriteria, idx) => ( }) if (Objects.isEmpty(attributeDefUuids)) return variables - const variablesByUuid = ObjectUtils.toUuidIndexedObj(variables) + const variablesByUuid = variables.reduce((acc, variable) => { + const { uuid } = variable + if (!acc[uuid]) { + // keep only first variable + // variables for composite attributes have same UUID for different properties (e.g. taxon) + acc[uuid] = variable + } + return acc + }, {}) return attributeDefUuids.map((uuid) => variablesByUuid[uuid]).filter(Boolean) } diff --git a/webapp/components/expression/expressionParser.js b/webapp/components/expression/expressionParser.js index 3f95b12d54..b39e767f9a 100644 --- a/webapp/components/expression/expressionParser.js +++ b/webapp/components/expression/expressionParser.js @@ -14,18 +14,22 @@ export const parseQuery = ({ query, mode, canBeConstant = false }) => { return Expression.newBinaryEmpty({ canBeConstant, exprQuery }) } +export const normalize = ({ expr, canBeConstant = false, canBeCall = false }) => { + if (canBeConstant || canBeCall) { + // expr can be a binary expression with an empty operator and right operand; + // formatting and parsing it again will keep only the left operand in the evaluation + const exprString = Expression.toString(expr) + if (isNotBlank(exprString)) { + return Expression.fromString(exprString) + } + } + return expr +} + export const isExprValid = ({ expr, canBeConstant = false, canBeCall = false }) => { try { - if (canBeConstant || canBeCall) { - // expr can be a binary expression with an empty operator and right operand; - // formatting and parsing it again allows to consider only the left operand in the evaluation - const exprString = Expression.toString(expr) - if (isNotBlank(exprString)) { - const exprToValidate = Expression.fromString(exprString) - return Expression.isValid(exprToValidate) - } - } - return Expression.isValid(expr) + const exprToValidate = normalize({ expr, canBeConstant, canBeCall }) + return Expression.isValid(exprToValidate) } catch (error) { return false }