diff --git a/i18n/en.pot b/i18n/en.pot index 486f629e2..68c6cb165 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-08-27T11:56:55.720Z\n" -"PO-Revision-Date: 2026-08-27T11:56:55.720Z\n" +"POT-Creation-Date: 2026-09-07T09:22:41.250Z\n" +"PO-Revision-Date: 2026-09-07T09:22:41.251Z\n" msgid "view only" msgstr "view only" @@ -79,24 +79,27 @@ msgstr "This app could not retrieve required data." msgid "Network error" msgstr "Network error" +msgid "Could not validate the formula" +msgstr "Could not validate the formula" + +msgid "The formula is valid" +msgstr "The formula is valid" + msgid "Data / Edit calculation" msgstr "Data / Edit calculation" msgid "Data / New calculation" msgstr "Data / New calculation" -msgid "Remove item" -msgstr "Remove item" - -msgid "Check formula" -msgstr "Check formula" - msgid "Calculation name" msgstr "Calculation name" msgid "Shown in table headers and chart axes/legends" msgstr "Shown in table headers and chart axes/legends" +msgid "Formula" +msgstr "Formula" + msgid "Delete calculation" msgstr "Delete calculation" @@ -144,14 +147,17 @@ msgid "No data elements found" msgstr "No data elements found" msgid "" -"Drag items here, or double click in the list, to start building a " -"calculation formula" +"Drag a data element or operator here, or click one, to start building a " +"formula" msgstr "" -"Drag items here, or double click in the list, to start building a " -"calculation formula" +"Drag a data element or operator here, or click one, to start building a " +"formula" -msgid "Math operators" -msgstr "Math operators" +msgid "Remove item" +msgstr "Remove item" + +msgid "Check formula" +msgstr "Check formula" msgid "Expression description" msgstr "Expression description" @@ -1193,8 +1199,8 @@ msgstr "Option" msgid "Number" msgstr "Number" -msgid "Formula is empty. Add items to the formula from the lists on the left." -msgstr "Formula is empty. Add items to the formula from the lists on the left." +msgid "Formula is empty. Add a data element or operator to the formula." +msgstr "Formula is empty. Add a data element or operator to the formula." msgid "Consecutive math operators" msgstr "Consecutive math operators" diff --git a/src/__demo__/CalculationModal.stories.js b/src/__demo__/CalculationModal.stories.js index dfab5179e..1d4605839 100644 --- a/src/__demo__/CalculationModal.stories.js +++ b/src/__demo__/CalculationModal.stories.js @@ -208,6 +208,17 @@ const DATA_ELEMENT_GROUPS = { ], } +const VALIDATION_OK = { + status: 'OK', +} + +const providerData = { + dataElements: DATA_ELEMENTS, + dataElementGroups: DATA_ELEMENT_GROUPS, + dataElementOperands: DATA_ELEMENT_OPERANDS, + 'indicators/expression/description': VALIDATION_OK, +} + const calculation = { id: 'calculationid', name: 'My calculation', @@ -226,13 +237,7 @@ export default { export const Default = () => { return ( - + { export const WithCalculation = () => { return ( - + { return ( - + { return ( css.resolve` + .content { + width: ${width}px; + } +` const CalculationModal = ({ calculation = CALCULATION_PROP_DEFAULT, @@ -61,7 +80,10 @@ const CalculationModal = ({ const [doBackendValidation, { loading: isValidating }] = useDataMutation( validateIndicatorExpressionMutation, { - onError: (error) => showError(error), + onError: (error) => + showError( + error?.message || i18n.t('Could not validate the formula') + ), } ) @@ -124,7 +146,8 @@ const CalculationModal = ({ } }, [data, calculation.expression]) - const [newIdCount, setNewIdCount] = useState(1) + const nextItemIdRef = useRef(1) + const latestRef = useRef() const [validationOutput, setValidationOutput] = useState(null) const [expressionArray, setExpressionArray] = useState() @@ -135,12 +158,30 @@ const CalculationModal = ({ const [focusItemId, setFocusItemId] = useState(null) const [selectedItemId, setSelectedItemId] = useState(null) + const modalContentWidth = useModalContentWidth({ + minWidth: MODAL_MIN_CONTENT_WIDTH, + maxWidth: MODAL_MAX_CONTENT_WIDTH, + }) + const modalContentCSS = useMemo( + () => getModalContentCSS(modalContentWidth), + [modalContentWidth] + ) + const expressionStatus = validationOutput?.status + const validationMessage = + expressionStatus === VALID_EXPRESSION + ? i18n.t('The formula is valid') + : validationOutput?.message - const selectItem = (itemId) => - setSelectedItemId((prevSelected) => - prevSelected !== itemId ? itemId : null - ) + const selectItem = (itemId) => { + const prevSelected = latestRef.current?.selectedItemId + const next = prevSelected !== itemId ? itemId : null + + if (latestRef.current) { + latestRef.current.selectedItemId = next + } + setSelectedItemId(next) + } const isLoading = isCreatingCalculation || @@ -149,49 +190,58 @@ const CalculationModal = ({ isSavingCalculation || isValidating - const addItem = ({ label, value, type, destIndex = LAST_POSITION }) => { - if (isLoading) { - return null + const addItem = ({ label, value, type, destIndex }) => { + if (isLoading || !expressionArray) { + return } - setValidationOutput() + setValidationOutput(null) const newItem = { - id: `${type}-${newIdCount}`, + id: `${type}-${nextItemIdRef.current++}`, value: type === EXPRESSION_TYPE_DATA ? `#{${value}}` : value, label, type, } - setNewIdCount(newIdCount + 1) + const selectedId = latestRef.current?.selectedItemId + setExpressionArray((prevArray) => { + let insertAt = destIndex + if (insertAt === undefined) { + const selectedIndex = prevArray.findIndex( + (item) => item.id === selectedId + ) + insertAt = + selectedIndex === -1 ? prevArray.length : selectedIndex + 1 + } else if (insertAt === LAST_POSITION) { + insertAt = prevArray.length + } - if (destIndex === LAST_POSITION) { - setExpressionArray((prevArray) => prevArray.concat([newItem])) - } else if (destIndex === FIRST_POSITION) { - setExpressionArray((prevArray) => [newItem].concat(prevArray)) - } else { - const items = Array.from(expressionArray) - const newFormulaItems = [ - ...items.slice(0, destIndex), + return [ + ...prevArray.slice(0, insertAt), newItem, - ...items.slice(destIndex), + ...prevArray.slice(insertAt), ] - setExpressionArray(newFormulaItems) - } + }) if (newItem.type === EXPRESSION_TYPE_NUMBER) { setFocusItemId(newItem.id) } + + setSelectedItemId(newItem.id) + latestRef.current.selectedItemId = newItem.id } const moveItem = ({ sourceIndex, destIndex }) => { if (isLoading) { - return null + return } - setValidationOutput() - const sourceList = Array.from(expressionArray) - const [moved] = sourceList.splice(sourceIndex, 1) - sourceList.splice(destIndex, 0, moved) - setExpressionArray(sourceList) + setValidationOutput(null) + setExpressionArray((prevArray) => { + const sourceList = Array.from(prevArray) + const [moved] = sourceList.splice(sourceIndex, 1) + sourceList.splice(destIndex, 0, moved) + return sourceList + }) } const setItemValue = ({ itemId, value }) => { @@ -203,7 +253,7 @@ const CalculationModal = ({ const removeItem = (itemId) => { if (!isLoading && itemId !== null) { - setValidationOutput() + setValidationOutput(null) const index = expressionArray.findIndex( (item) => item.id === itemId ) @@ -214,6 +264,79 @@ const CalculationModal = ({ } } + // Mirrored on every render so the keydown listener below, which is + // registered once on mount, still sees fresh values on every keystroke. + latestRef.current = { + isLoading, + showDeletePrompt, + selectedItemId, + expressionArray, + addItem, + moveItem, + } + + useEffect(() => { + const handleKeyDown = (event) => { + const { + isLoading, + showDeletePrompt, + selectedItemId, + expressionArray, + addItem, + moveItem, + } = latestRef.current + + if ( + isLoading || + showDeletePrompt || + event.metaKey || + event.ctrlKey || + event.altKey || + isInteractiveElement(event.target) + ) { + return + } + + const operator = getOperators().find( + (op) => + op.type === EXPRESSION_TYPE_OPERATOR && + op.value === event.key + ) + + if (operator) { + event.preventDefault() + addItem(operator) + return + } + + if (!selectedItemId || !expressionArray) { + return + } + + const index = expressionArray.findIndex( + (item) => item.id === selectedItemId + ) + if (index === -1) { + return + } + + if (event.key === 'ArrowLeft' && index > 0) { + event.preventDefault() + moveItem({ sourceIndex: index, destIndex: index - 1 }) + } else if ( + event.key === 'ArrowRight' && + index < expressionArray.length - 1 + ) { + event.preventDefault() + moveItem({ sourceIndex: index, destIndex: index + 1 }) + } + } + + document.addEventListener('keydown', handleKeyDown) + + return () => document.removeEventListener('keydown', handleKeyDown) + }, []) + const addOrMoveDraggedItem = ({ item, destination }) => { const destContainerId = destination.containerId @@ -238,14 +361,29 @@ const CalculationModal = ({ } const validate = async () => { - setValidationOutput() + setValidationOutput(null) const expression = parseArrayToExpression(expressionArray) let result = validateExpression(expression) + if (!result) { - result = await doBackendValidation({ + const backendResult = await doBackendValidation({ expression, }) + + if (!backendResult) { + return + } + + if (backendResult.status === INVALID_EXPRESSION) { + result = backendResult + } else { + result = { + ...backendResult, + status: VALID_EXPRESSION, + } + } } + setValidationOutput(result) return result?.status @@ -296,91 +434,103 @@ const CalculationModal = ({ return ( <> - + {calculation.id ? i18n.t('Data / Edit calculation') : i18n.t('Data / New calculation')} +
+ + setName(value.substr(0, 50)) + } + value={name} + dataTest="calculation-label" + dense + /> +
setFocusItemId(null)} onDragEnd={addOrMoveDraggedItem} > -
+
-
- -
-
-
- -
-
- -
+
+
+

+ {i18n.t('Formula')} +

- + removeItem(selectedItemId) + } + onValidate={validate} + canRemove={Boolean(selectedItemId)} + isValidating={isValidating} + isLoading={isLoading} + /> +
- {validationOutput?.message} - -
- - setName(value.substr(0, 50)) - } - value={name} - dataTest="calculation-label" - dense + + {validationMessage && ( +
+ + {expressionStatus === + VALID_EXPRESSION ? ( + + ) : ( + + )} + + {validationMessage} + + +
+ )}
@@ -471,6 +621,7 @@ const CalculationModal = ({ )} + {modalContentCSS.styles} ) diff --git a/src/components/DataDimension/Calculation/DataElementOption.js b/src/components/DataDimension/Calculation/DataElementOption.js index 7743ff3b3..5839aef4a 100644 --- a/src/components/DataDimension/Calculation/DataElementOption.js +++ b/src/components/DataDimension/Calculation/DataElementOption.js @@ -6,8 +6,9 @@ import { DIMENSION_TYPE_DATA_ELEMENT } from '../../../modules/dataTypes.js' import { getIcon } from '../../../modules/dimensionListItem.js' import { EXPRESSION_TYPE_DATA } from '../../../modules/expressions.js' import styles from './styles/DataElementOption.style.js' +import draggableChipButtonStyles from './styles/DraggableChipButton.style.js' -const DataElementOption = ({ label, value, onDoubleClick }) => { +const DataElementOption = ({ label, value, onClick }) => { const data = { label, value, type: EXPRESSION_TYPE_DATA } const { attributes, listeners, setNodeRef, transform } = useSortable({ id: value, @@ -19,24 +20,24 @@ const DataElementOption = ({ label, value, onDoubleClick }) => { return (
-
onClick(data)} > -
onDoubleClick(data)} - data-test="data-element-option" - > +
{getIcon(DIMENSION_TYPE_DATA_ELEMENT)} {label}
-
+ +
) @@ -45,7 +46,7 @@ const DataElementOption = ({ label, value, onDoubleClick }) => { DataElementOption.propTypes = { label: PropTypes.string, value: PropTypes.string, - onDoubleClick: PropTypes.func, + onClick: PropTypes.func, } export default DataElementOption diff --git a/src/components/DataDimension/Calculation/DataElementSelector.js b/src/components/DataDimension/Calculation/DataElementSelector.js index 36a5b4539..0964d668d 100644 --- a/src/components/DataDimension/Calculation/DataElementSelector.js +++ b/src/components/DataDimension/Calculation/DataElementSelector.js @@ -127,7 +127,7 @@ DisaggregationSelector.propTypes = { const DataElementSelector = ({ displayNameProp, - onDoubleClick, + onClick, height = SCROLLBOX_HEIGHT, }) => { const dataEngine = useDataEngine() @@ -296,7 +296,7 @@ const DataElementSelector = ({ key={value} label={label} value={value} - onDoubleClick={onDoubleClick} + onClick={onClick} /> ))} {!loading && !options.length && ( @@ -325,7 +325,7 @@ const DataElementSelector = ({ DataElementSelector.propTypes = { displayNameProp: PropTypes.string.isRequired, - onDoubleClick: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, height: PropTypes.string, } diff --git a/src/components/DataDimension/Calculation/DndContext.js b/src/components/DataDimension/Calculation/DndContext.js index 846835894..574e9f1a4 100644 --- a/src/components/DataDimension/Calculation/DndContext.js +++ b/src/components/DataDimension/Calculation/DndContext.js @@ -79,20 +79,27 @@ const rectIntersectionCustom = ({ return collisions.sort(sortCollisionsDesc) } -const isInteractiveElement = (el) => { - const interactiveElements = [ - 'button', - 'input', - 'textarea', - 'select', - 'option', - ] +const INTERACTIVE_SELECTOR = 'button, input, textarea, select, option' - if (interactiveElements.includes(el.tagName.toLowerCase())) { - return true - } +// Chips are real + )} + + +
+ +
+) + +FormulaToolbar.propTypes = { + onAddOperator: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, + onValidate: PropTypes.func.isRequired, + canRemove: PropTypes.bool, + isLoading: PropTypes.bool, + isValidating: PropTypes.bool, +} + +export default FormulaToolbar diff --git a/src/components/DataDimension/Calculation/MathOperatorSelector.js b/src/components/DataDimension/Calculation/MathOperatorSelector.js index d87689f6a..97c06c781 100644 --- a/src/components/DataDimension/Calculation/MathOperatorSelector.js +++ b/src/components/DataDimension/Calculation/MathOperatorSelector.js @@ -1,33 +1,29 @@ import PropTypes from 'prop-types' import React from 'react' -import i18n from '../../../locales/index.js' import { getOperators } from '../../../modules/expressions.js' import DraggableOperator from './Operator.js' import styles from './styles/MathOperatorSelector.style.js' -const MathOperatorSelector = ({ onDoubleClick }) => ( +const MathOperatorSelector = ({ onClick }) => ( <> -
-

{i18n.t('Math operators')}

-
- {getOperators().map(({ label, value, type }, index) => ( - - ))} -
+
+ {getOperators().map(({ label, value, type }, index) => ( + + ))}
) MathOperatorSelector.propTypes = { - onDoubleClick: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, } export default MathOperatorSelector diff --git a/src/components/DataDimension/Calculation/Operator.js b/src/components/DataDimension/Calculation/Operator.js index 2bd46479b..f8ef39149 100644 --- a/src/components/DataDimension/Calculation/Operator.js +++ b/src/components/DataDimension/Calculation/Operator.js @@ -7,10 +7,11 @@ import { EXPRESSION_TYPE_NUMBER, EXPRESSION_TYPE_OPERATOR, } from '../../../modules/expressions.js' +import draggableChipButtonStyles from './styles/DraggableChipButton.style.js' import formulaItemStyles from './styles/FormulaItem.style.js' import styles from './styles/Operator.style.js' -const Operator = ({ label, value, type, onDoubleClick }) => { +const Operator = ({ label, value, type, onClick }) => { const data = { label, value, type } const { attributes, listeners, setNodeRef, transform } = useSortable({ id: `operator-${label}`, @@ -21,20 +22,29 @@ const Operator = ({ label, value, type, onDoubleClick }) => { } return ( -
+
+ ) } @@ -42,7 +52,7 @@ Operator.propTypes = { label: PropTypes.string.isRequired, type: PropTypes.string.isRequired, value: PropTypes.string.isRequired, - onDoubleClick: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, } export default Operator diff --git a/src/components/DataDimension/Calculation/styles/CalculationModal.style.js b/src/components/DataDimension/Calculation/styles/CalculationModal.style.js index a15c19c14..70407b600 100644 --- a/src/components/DataDimension/Calculation/styles/CalculationModal.style.js +++ b/src/components/DataDimension/Calculation/styles/CalculationModal.style.js @@ -2,34 +2,36 @@ import { colors, spacers } from '@dhis2/ui' import css from 'styled-jsx/css' export default css` - .header { - background: ${colors.grey200}; - padding: ${spacers.dp16}; - font-weight: normal; - } - - .header-icon { - padding: 0 ${spacers.dp8}; - vertical-align: text-bottom; - line-height: 14px; - } - - .actions-wrapper { - margin-top: ${spacers.dp16}; - margin-bottom: ${spacers.dp16}; - margin-left: ${spacers.dp4}; + .formula-section { + /* Match left column height; FormulaField scrolls inside. */ + position: absolute; + inset: 0; + background: ${colors.white}; + border: 1px solid ${colors.grey400}; + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; } - .button-container { - display: inline-flex; + .formula-box { + position: relative; + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + border-top: 1px solid ${colors.grey400}; } - .validate-button { - margin-bottom: ${spacers.dp4}; + .formula-box.valid { + border-top-color: transparent; + box-shadow: inset 0 0 0 1px ${colors.green500}; } - .remove-button { - margin-right: ${spacers.dp8}; + .formula-box.invalid { + border-top-color: transparent; + box-shadow: inset 0 0 0 1px ${colors.red500}; } .delete-button { @@ -38,31 +40,84 @@ export default css` .content { display: flex; + gap: ${spacers.dp12}; + align-items: stretch; } .left-section { - width: 45%; + width: 40%; + flex-shrink: 0; } .right-section { - width: 55%; - padding-left: ${spacers.dp8}; + width: 60%; font-size: 14px; + position: relative; + min-height: 0; } - .validation-message { - margin-left: ${spacers.dp8}; + .sub-header-row { + display: flex; + align-items: center; + gap: ${spacers.dp8}; + padding: ${spacers.dp8} ${spacers.dp8} 0; + box-sizing: border-box; + flex-shrink: 0; } - .validation-error { - color: ${colors.red500}; + .sub-header { + font-size: 14px; + font-weight: normal; + margin: 0; + flex-shrink: 0; } - .validation-success { - color: ${colors.green500}; + /* Clear chips under the overlay bar when validation is shown */ + .formula-box.valid :global(.formula-field), + .formula-box.invalid :global(.formula-field) { + padding-bottom: 40px; } - .name-input { - margin-top: ${spacers.dp12}; + .validation-bar { + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + display: flex; + align-items: center; + gap: ${spacers.dp4}; + padding: ${spacers.dp8} ${spacers.dp12}; + box-sizing: border-box; + background: ${colors.red050}; + outline: 1px solid ${colors.red500}; + outline-offset: -1px; + } + + .formula-box.valid .validation-bar { + background: ${colors.green050}; + outline-color: ${colors.green500}; + } + + .status { + display: inline-flex; + align-items: center; + gap: ${spacers.dp4}; + min-width: 0; + } + + .status-text { + color: ${colors.red700}; + font-size: 14px; + line-height: 19px; + min-width: 0; + } + + .formula-box.valid .status-text { + color: ${colors.green700}; + } + + .name-field { + margin-bottom: ${spacers.dp16}; } ` diff --git a/src/components/DataDimension/Calculation/styles/DataElementOption.style.js b/src/components/DataDimension/Calculation/styles/DataElementOption.style.js index f3333d726..eb09ccf13 100644 --- a/src/components/DataDimension/Calculation/styles/DataElementOption.style.js +++ b/src/components/DataDimension/Calculation/styles/DataElementOption.style.js @@ -9,11 +9,6 @@ export default css` .wrapper:last-child { margin-bottom: ${spacers.dp4}; } - .draggable-item { - cursor: pointer; - display: inline-flex; - } - .chip { display: inline-flex; background: ${colors.grey200}; diff --git a/src/components/DataDimension/Calculation/styles/DraggableChipButton.style.js b/src/components/DataDimension/Calculation/styles/DraggableChipButton.style.js new file mode 100644 index 000000000..330f948e3 --- /dev/null +++ b/src/components/DataDimension/Calculation/styles/DraggableChipButton.style.js @@ -0,0 +1,21 @@ +import { theme } from '@dhis2/ui' +import css from 'styled-jsx/css' + +export default css` + .draggable-item { + all: unset; + display: inline-flex; + cursor: pointer; + border-radius: 3px; + } + + .draggable-item:focus { + outline: 2px solid ${theme.focus}; + outline-offset: -2px; + } + + /* Prevent focus styles when mouse clicking */ + .draggable-item:focus:not(:focus-visible) { + outline: none; + } +` diff --git a/src/components/DataDimension/Calculation/styles/FormulaField.style.js b/src/components/DataDimension/Calculation/styles/FormulaField.style.js index 24b120ec5..47bee19d4 100644 --- a/src/components/DataDimension/Calculation/styles/FormulaField.style.js +++ b/src/components/DataDimension/Calculation/styles/FormulaField.style.js @@ -3,8 +3,8 @@ import css from 'styled-jsx/css' export default css` .formula-field { - border-right: 2px solid ${colors.grey200}; - height: 180px; + flex: 1; + min-height: 0; overflow: auto; padding: 6px 12px; position: relative; @@ -16,23 +16,9 @@ export default css` width: 100%; } - .container { - position: relative; - } - - .border { - position: absolute; - top: 0; - left: 6px; - height: 180px; - width: calc(100% - 6px); - border-left: 2px solid ${colors.grey200}; - border-top: 2px solid ${colors.grey200}; - border-bottom: 2px solid ${colors.grey200}; - } - .placeholder { height: 100%; + width: 100%; display: flex; flex-direction: column; gap: ${spacers.dp8}; @@ -40,12 +26,14 @@ export default css` justify-content: center; margin-top: -28px; padding: 0 ${spacers.dp32}; + text-align: center; } .help-text { color: ${colors.grey600}; font-size: 14px; line-height: 19px; + text-align: center; user-select: none; } ` diff --git a/src/components/DataDimension/Calculation/styles/FormulaItem.style.js b/src/components/DataDimension/Calculation/styles/FormulaItem.style.js index 6b0998960..d9ba3ca2c 100644 --- a/src/components/DataDimension/Calculation/styles/FormulaItem.style.js +++ b/src/components/DataDimension/Calculation/styles/FormulaItem.style.js @@ -45,9 +45,6 @@ export default css` } .data .label { - max-width: 280px; - text-overflow: ellipsis; - overflow: hidden; white-space: nowrap; } diff --git a/src/components/DataDimension/Calculation/styles/FormulaToolbar.style.js b/src/components/DataDimension/Calculation/styles/FormulaToolbar.style.js new file mode 100644 index 000000000..01b048ae1 --- /dev/null +++ b/src/components/DataDimension/Calculation/styles/FormulaToolbar.style.js @@ -0,0 +1,17 @@ +import { spacers } from '@dhis2/ui' +import css from 'styled-jsx/css' + +export default css` + .formula-toolbar { + padding: ${spacers.dp8}; + flex-shrink: 0; + } + + .buttons-row { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: ${spacers.dp8}; + } +` diff --git a/src/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js b/src/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js index 98c2214ee..dbac349ee 100644 --- a/src/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js +++ b/src/components/DataDimension/Calculation/styles/MathOperatorSelector.style.js @@ -1,23 +1,10 @@ -import { colors, spacers } from '@dhis2/ui' +import { spacers } from '@dhis2/ui' import css from 'styled-jsx/css' export default css` - .wrapper { - border: 1px solid ${colors.grey400}; - margin-top: ${spacers.dp8}; - } - .operators { display: flex; flex-wrap: wrap; gap: ${spacers.dp4}; - padding: ${spacers.dp4}; - border-top: 1px solid ${colors.grey400}; - } - - .sub-header { - font-size: 14px; - font-weight: normal; - margin: ${spacers.dp4} ${spacers.dp8}; } ` diff --git a/src/components/Interpretations/InterpretationModal/InterpretationModal.js b/src/components/Interpretations/InterpretationModal/InterpretationModal.js index 73bc13c8c..689cc9780 100644 --- a/src/components/Interpretations/InterpretationModal/InterpretationModal.js +++ b/src/components/Interpretations/InterpretationModal/InterpretationModal.js @@ -15,12 +15,12 @@ import cx from 'classnames' import PropTypes from 'prop-types' import React, { useMemo } from 'react' import css from 'styled-jsx/css' +import { useModalContentWidth } from '../../../modules/useModalContentWidth.js' import { useActiveInterpretation, useInterpretationsCurrentUser, } from '../InterpretationsProvider/hooks.js' import { InterpretationThread } from './InterpretationThread.js' -import { useModalContentWidth } from './useModalContentWidth.js' const modalCSS = css.resolve` aside { diff --git a/src/modules/__tests__/expressions.spec.js b/src/modules/__tests__/expressions.spec.js index 4208a1eba..804e696fa 100644 --- a/src/modules/__tests__/expressions.spec.js +++ b/src/modules/__tests__/expressions.spec.js @@ -12,7 +12,7 @@ import { const invalidTestExpressions = [ { message: - 'Formula is empty. Add items to the formula from the lists on the left.', + 'Formula is empty. Add a data element or operator to the formula.', expressions: [''], }, // { diff --git a/src/modules/expressions.js b/src/modules/expressions.js index 99eba9dcf..9ea589c88 100644 --- a/src/modules/expressions.js +++ b/src/modules/expressions.js @@ -67,7 +67,7 @@ export const validateExpression = (expression) => { result = { status: INVALID_EXPRESSION, message: i18n.t( - 'Formula is empty. Add items to the formula from the lists on the left.' + 'Formula is empty. Add a data element or operator to the formula.' ), } // TODO: reimplement this but allow negative values, e.g. 10 / -5 diff --git a/src/components/Interpretations/InterpretationModal/useModalContentWidth.js b/src/modules/useModalContentWidth.js similarity index 52% rename from src/components/Interpretations/InterpretationModal/useModalContentWidth.js rename to src/modules/useModalContentWidth.js index 9ce9e1fde..9bc3c390a 100644 --- a/src/components/Interpretations/InterpretationModal/useModalContentWidth.js +++ b/src/modules/useModalContentWidth.js @@ -1,18 +1,22 @@ import { useState, useEffect } from 'react' -import { useDebounce } from '../../../modules/utils.js' +import { useDebounce } from './utils.js' const MODAL_SIDE_PADDING = 2 * 24 const MODAL_SIDE_MARGINS = 2 * 128 -const computeModalContentWidth = (windowWidth) => { - return windowWidth - MODAL_SIDE_MARGINS - MODAL_SIDE_PADDING +const computeModalContentWidth = (windowWidth, minWidth, maxWidth) => { + const width = windowWidth - MODAL_SIDE_MARGINS - MODAL_SIDE_PADDING + return Math.min(Math.max(width, minWidth), maxWidth) } -export const useModalContentWidth = () => { +export const useModalContentWidth = ({ + minWidth = 0, + maxWidth = Infinity, +} = {}) => { const [windowWidth, setWindowWidth] = useState(window.innerWidth) const debouncedWindowWidth = useDebounce(windowWidth, 150) const [modalContentWidth, setModalContentWidth] = useState( - computeModalContentWidth(windowWidth) + computeModalContentWidth(windowWidth, minWidth, maxWidth) ) useEffect(() => { @@ -27,8 +31,10 @@ export const useModalContentWidth = () => { }, []) useEffect(() => { - setModalContentWidth(computeModalContentWidth(debouncedWindowWidth)) - }, [debouncedWindowWidth]) + setModalContentWidth( + computeModalContentWidth(debouncedWindowWidth, minWidth, maxWidth) + ) + }, [debouncedWindowWidth, minWidth, maxWidth]) return modalContentWidth }