Skip to content

Commit

Permalink
Form designer: fixed constant value to expression switch (min/max cou…
Browse files Browse the repository at this point in the history
…nt expressions) (#3713)

* fixing expressions editor layout

* code cleanup

* code cleanup

* code cleanup

* code cleanup

* code cleanup

---------

Co-authored-by: Stefano Ricci <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 23, 2025
1 parent 10bafbb commit 662ab03
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 86 deletions.
1 change: 1 addition & 0 deletions core/stringUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const removeSuffix = (suffix) => (text) =>

export const quote = (text) => (isBlank(text) ? '' : `'${text}'`)
export const unquote = R.pipe(removePrefix(`'`), removeSuffix(`'`))
export const unquoteDouble = R.pipe(removePrefix(`"`), removeSuffix(`"`))

export const hashCode = (str) => {
let hash = 0
Expand Down
5 changes: 3 additions & 2 deletions core/survey/nodeDefExpression.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ export const getSeverity = R.propOr(ValidationResult.severity.error, keys.severi

export const isPlaceholder = R.propEq(keys.placeholder, true)

export const isEmpty = (expression = {}) =>
StringUtils.isBlank(getExpression(expression)) && StringUtils.isBlank(getApplyIf(expression))
export const isExpressionEmpty = (expression = {}) => StringUtils.isBlank(getExpression(expression))
export const isApplyIfEmpty = (expression = {}) => StringUtils.isBlank(getApplyIf(expression))
export const isEmpty = (expression = {}) => isExpressionEmpty(expression) && isApplyIfEmpty(expression)

export const isSimilarTo = (expressionA) => (expressionB) => {
if (isEmpty(expressionA) && isEmpty(expressionB)) return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const ExpressionProp = (props) => {
}))

const isPlaceholder = NodeDefExpression.isPlaceholder(expression)
const expressionEmpty = NodeDefExpression.isEmpty(expression)

const expressionEditorTypes = [ExpressionEditorType.basic, ...(hideAdvanced ? [] : [ExpressionEditorType.advanced])]

Expand Down Expand Up @@ -82,7 +83,7 @@ const ExpressionProp = (props) => {
/>
</div>

{applyIf && !isPlaceholder && (
{applyIf && !isPlaceholder && !expressionEmpty && (
<div className="expression-item">
<div className="label">{i18n.t('nodeDefEdit.expressionsProp.applyIf')}</div>

Expand Down Expand Up @@ -111,7 +112,7 @@ const ExpressionProp = (props) => {
selectedItemKey={NodeDefExpression.getSeverity(expression)}
onChange={(severityVal) => onUpdate(NodeDefExpression.assocSeverity(severityVal)(expression))}
items={severityItems}
disabled={NodeDefExpression.isEmpty(expression)}
disabled={expressionEmpty}
/>
</div>
)}
Expand All @@ -120,7 +121,7 @@ const ExpressionProp = (props) => {
formLabelKey="common.errorMessage"
labels={NodeDefExpression.getMessages(expression)}
onChange={(messages) => onUpdate(NodeDefExpression.assocMessages(messages)(expression))}
readOnly={NodeDefExpression.isEmpty(expression)}
readOnly={expressionEmpty}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import './ExpressionsProp.scss'

import React, { useCallback, useEffect, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import { useDispatch } from 'react-redux'
import * as R from 'ramda'
Expand All @@ -11,9 +11,11 @@ import { Objects } from '@openforis/arena-core'
import * as NodeDefExpression from '@core/survey/nodeDefExpression'
import * as Validation from '@core/validation/validation'
import * as Expression from '@core/expressionParser/expression'
import * as StringUtils from '@core/stringUtils'

import { DialogConfirmActions } from '@webapp/store/ui'

import { useConfirmAsync } from '@webapp/components/hooks'
import { ButtonGroup } from '@webapp/components/form'
import { FormItem, Input } from '@webapp/components/form/Input'
import ValidationTooltip from '@webapp/components/validationTooltip'
Expand All @@ -30,6 +32,22 @@ const valueTypeItems = Object.keys(ValueType).map((valueType) => ({
label: `expressionEditor.valueType.${valueType}`,
}))

const extractConstantValue = ({ values }) => {
const nodeDefExpr = values[0]
if (
NodeDefExpression.isPlaceholder(nodeDefExpr) ||
NodeDefExpression.isExpressionEmpty(nodeDefExpr) ||
!NodeDefExpression.isApplyIfEmpty(nodeDefExpr)
) {
return null
}
const expression = NodeDefExpression.getExpression(nodeDefExpr)
const stringValue = typeof expression === 'string' ? expression : null
return Expression.isLiteral(Expression.fromString(stringValue))
? R.pipe(StringUtils.unquote, StringUtils.unquoteDouble)(stringValue)
: null
}

const ExpressionsWrapper = (props) => {
const { validation, children } = props

Expand Down Expand Up @@ -77,26 +95,40 @@ const ExpressionsProp = (props) => {
} = props

const dispatch = useDispatch()
const confirm = useConfirmAsync()

const [valueType, setValueType] = useState(determineValueType?.())

useEffect(() => {
if (!valueTypeSelection || !determineValueType) return
const valueTypeNext = determineValueType()
if (valueTypeNext !== valueType) {
setValueType(valueTypeNext)
}
}, [determineValueType, valueType, valueTypeSelection])
const valuesIsEmpty = R.isEmpty(values) || values.every(NodeDefExpression.isEmpty)

const onValueTypeChange = useCallback(
(valueTypeNext) => {
async (valueTypeNext) => {
if (valueTypeNext === ValueType.expression) {
// switch from constant to expression: convert constant value (if specified) to expression
const constantValue = values?.[0]?.expression
onChange([NodeDefExpression.createExpression({ expression: constantValue })])
if (Objects.isNotEmpty(constantValue)) {
onChange([NodeDefExpression.createExpression({ expression: constantValue })])
}
setValueType(valueTypeNext)
} else if (valuesIsEmpty) {
// converting from expression to constant
// no expressions to convert; just switch type
setValueType(valueTypeNext)
} else {
// converting from expression to constant
// expressions defined: try to convert them into a constant value
const constantValue = extractConstantValue({ values })
if (!Objects.isEmpty(constantValue)) {
onChange(constantValue)
setValueType(valueTypeNext)
} else if (await confirm({ key: 'nodeDefEdit.expressionsProp.confirmDelete' })) {
// expression cannot be converted into constant: clear it
onChange(null)
setValueType(valueTypeNext)
}
}
setValueType(valueTypeNext)
},
[onChange, values]
[confirm, onChange, values, valuesIsEmpty]
)

const getExpressionIndex = useCallback(
Expand Down Expand Up @@ -135,80 +167,60 @@ const ExpressionsProp = (props) => {
[getExpressionIndex, onChange, onDelete, values]
)

const createExpressionProp = useCallback(
({ index, expression, validation }) => (
<ExpressionProp
key={index}
applyIf={applyIf}
canBeConstant={canBeConstant}
excludeCurrentNodeDef={excludeCurrentNodeDef}
expression={expression}
hideAdvanced={hideAdvanced}
index={index}
isBoolean={isBoolean}
isContextParent={isContextParent}
mode={mode}
qualifier={qualifier}
nodeDefUuidContext={nodeDefUuidContext}
nodeDefUuidCurrent={nodeDefUuidCurrent}
onDelete={onDelete}
onUpdate={onUpdate}
readOnly={readOnly}
severity={severity}
showLabels={showLabels}
validation={validation}
/>
),
[
applyIf,
canBeConstant,
excludeCurrentNodeDef,
hideAdvanced,
isBoolean,
isContextParent,
mode,
nodeDefUuidContext,
nodeDefUuidCurrent,
onDelete,
onUpdate,
qualifier,
readOnly,
severity,
showLabels,
]
)
const uiValues = useMemo(() => {
const _uiValues = [...values]
if (!readOnly && (multiple || valuesIsEmpty)) {
_uiValues.push(NodeDefExpression.createExpressionPlaceholder())
}
return _uiValues
}, [multiple, readOnly, values, valuesIsEmpty])

return (
<FormItem info={info} label={label} className={classNames({ error: Validation.isNotValid(validation) })}>
<ExpressionsWrapper validation={validation}>
{valueType === ValueType.constant ? (
<>
<ButtonGroup items={valueTypeItems} onChange={onValueTypeChange} selectedItemKey={valueType} />
<Input
className="node-def-edit_constant-value"
disabled={readOnly}
numberFormat={valueConstantEditorNumberFormat}
onChange={onChange}
validation={validation}
value={values?.[0]?.expression}
/>
</>
) : (
{valueTypeSelection && (
<ButtonGroup
disabled={readOnly}
items={valueTypeItems}
onChange={onValueTypeChange}
selectedItemKey={valueType}
/>
)}
{valueType === ValueType.constant && (
<Input
className="node-def-edit_constant-value"
disabled={readOnly}
numberFormat={valueConstantEditorNumberFormat}
onChange={onChange}
validation={validation}
value={values?.[0]?.expression}
/>
)}
{(!valueTypeSelection || valueType === ValueType.expression) && (
<div className="node-def-edit__expressions">
{values.map((value, index) =>
createExpressionProp({
index,
expression: value,
validation: Validation.getFieldValidation(index)(validation),
})
)}
{!readOnly &&
(multiple || R.isEmpty(values)) &&
createExpressionProp({
index: values.length,
expression: NodeDefExpression.createExpressionPlaceholder(),
validation: {},
})}
{uiValues.map((value, index) => (
<ExpressionProp
key={NodeDefExpression.getUuid(value)}
applyIf={applyIf}
canBeConstant={canBeConstant}
excludeCurrentNodeDef={excludeCurrentNodeDef}
expression={value}
hideAdvanced={hideAdvanced}
index={index}
isBoolean={isBoolean}
isContextParent={isContextParent}
mode={mode}
qualifier={qualifier}
nodeDefUuidContext={nodeDefUuidContext}
nodeDefUuidCurrent={nodeDefUuidCurrent}
onDelete={onDelete}
onUpdate={onUpdate}
readOnly={readOnly}
severity={severity}
showLabels={showLabels}
validation={Validation.getFieldValidation(index)(validation)}
/>
))}
</div>
)}
</ExpressionsWrapper>
Expand Down

0 comments on commit 662ab03

Please sign in to comment.