UNPKG

@adaptabletools/adaptable

Version:

Powerful data-agnostic HTML5 AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

261 lines (260 loc) 14.5 kB
import { Box, Flex, Text } from 'rebass'; import * as parser from '../../parser/src'; import * as React from 'react'; import ErrorBox from '../ErrorBox'; import { useSelectionRange } from '../utils/useSelectionRange'; import EditorButton from './EditorButton'; import HelpBlock from '../HelpBlock'; import OverlayTrigger from '../OverlayTrigger'; import SimpleButton from '../SimpleButton'; import Textarea from '../Textarea'; import { useEffect, useMemo } from 'react'; import { ExpressionEvaluationError } from '../../parser/src/ExpressionEvaluationError'; import { ButtonInfo } from '../../View/Components/Buttons/ButtonInfo'; import { ExpressionEditorDocsLink } from '../../Utilities/Constants/DocumentationLinkConstants'; import { Icon } from '../icons'; import { useAdaptable } from '../../View/AdaptableContext'; import join from '../utils/join'; import { ExpressionFunctionDocumentation } from './ExpressionFunctionDocumentation'; import { Tag } from '../Tag'; import StringExtensions from '../../Utilities/Extensions/StringExtensions'; import Radio from '../Radio'; const filterableCategories = [ 'dates', 'logical', 'maths', 'strings', 'comparison', 'observable', 'aggregation', 'cumulative', ]; const getCategoryOrder = (category) => { const predefinedOrder = { special: 1, conditional: 2, logical: 3, comparison: 4, strings: 5, maths: 6, dates: 7, }; return predefinedOrder[category] || 0; }; const VarEditorButton = () => { const adaptable = useAdaptable(); const customQueryVariables = adaptable.api.optionsApi.getExpressionOptions()?.customQueryVariables; if (!customQueryVariables || Object.keys(customQueryVariables).length === 0) { return React.createElement(React.Fragment, null); } return (React.createElement(React.Fragment, null, Object.keys(customQueryVariables).map((varOption) => { const varString = `VAR('${varOption}')`; return (React.createElement(EditorButton, { data: varString, key: varOption }, varString)); }))); }; const FunctionsDropdown = ({ expressionFunctions, baseClassName }) => { const [currentFunctionCategory, setCurrentFunctionCategory] = React.useState('all'); const dropdownRef = React.useRef(null); const [overFunction, setOverFunction] = React.useState(); React.useEffect(() => { setOverFunction(null); }, [currentFunctionCategory]); const groupedFunctions = React.useMemo(() => { return Object.keys(expressionFunctions).reduce((acc, key) => { const functionDef = expressionFunctions[key]; // filter if (currentFunctionCategory !== 'all' && functionDef.category !== currentFunctionCategory) { return acc; } if (functionDef.category) { acc[functionDef.category] = { ...acc[functionDef.category], [key]: functionDef, }; } else { acc.noCategory[key] = functionDef; } return acc; }, { noCategory: {} }); }, [currentFunctionCategory]); const orderedGroupNames = React.useMemo(() => { return Object.keys(groupedFunctions).sort((first, second) => getCategoryOrder(first) - getCategoryOrder(second)); }, [groupedFunctions]); const handleFunctionCategoryChange = React.useCallback((type) => (event) => { setCurrentFunctionCategory(type); }, [currentFunctionCategory]); const categoryOptions = React.useMemo(() => { const categoryOptions = Object.keys(expressionFunctions) .reduce((acc, functionName) => { const functionExpression = expressionFunctions[functionName]; if (!acc.includes(functionExpression.category)) { acc.push(functionExpression.category); } return acc; }, []) .filter((category) => filterableCategories.includes(category)) .sort((first, second) => getCategoryOrder(first) - getCategoryOrder(second)) .map((category) => ({ label: StringExtensions.Humanize(category), value: category, })); return [{ label: 'All', value: 'all' }, ...categoryOptions]; }, [expressionFunctions]); const hidePopup = () => { dropdownRef.current.hide(); setOverFunction(null); }; /** * Hide when: * - mouse leaves * - a function is inserted */ return (React.createElement(OverlayTrigger, { ref: dropdownRef, showEvent: "mouseenter", hideEvent: "mouseleave", targetOffset: 5, render: () => (React.createElement(Flex, { className: `${baseClassName}__dropdown-functions-list-wrapper`, flexDirection: "column", onMouseLeave: () => hidePopup() }, React.createElement(Flex, { pl: 2, style: { gap: 10 } }, categoryOptions.map((option, index) => { return (React.createElement(Radio, { key: option.value, onFocus: (event) => { event.preventDefault(); event.stopPropagation(); }, onClick: handleFunctionCategoryChange(option.value), checked: currentFunctionCategory === option.value }, option.label)); })), React.createElement(Flex, null, React.createElement(Flex, { className: `${baseClassName}__dropdown-functions-list`, "data-name": "expression-dropdown-functions-list", flexDirection: "column", p: 2, maxHeight: '50vh' }, orderedGroupNames .filter((groupName) => !!groupedFunctions[groupName]) .map((groupName) => { const functionsInGroup = Object.keys(groupedFunctions[groupName]); if (functionsInGroup.length === 0) { return React.createElement(React.Fragment, { key: groupName }); } const getEditorButtonData = (functionName) => { // handle special cases if (functionName === 'CASE') { return `CASE <caseValue> WHEN <whenValue> THEN <thenValue> ELSE <defaultValue> END`; } if (functionName === 'TRUE' || functionName === 'FALSE') { return functionName; } if (functionName === 'IF') { return `<condition_expr> ? <consequent_expr> : <alternative_expr>`; } return `${functionName}()`; }; return (React.createElement(Box, { mb: 2, key: groupName }, React.createElement(Tag, { mb: 1 }, StringExtensions.Humanize(groupName)), functionsInGroup.map((functionName) => (React.createElement(Box, { key: functionName, onMouseEnter: () => setOverFunction(functionName), onClick: () => hidePopup() }, functionName === 'VAR' ? (React.createElement(VarEditorButton, { key: functionName })) : (React.createElement(EditorButton, { data: getEditorButtonData(functionName), key: functionName, mr: 1 }, functionName))))))); })), React.createElement(Box, { className: `${baseClassName}__dropdown-functions-description`, p: 2, width: 600, maxWidth: "30vw" }, overFunction ? (React.createElement(React.Fragment, null, React.createElement(Tag, null, overFunction), React.createElement(ExpressionFunctionDocumentation, { expressionFunction: expressionFunctions[overFunction] }))) : (React.createElement(Tag, { padding: 20 }, React.createElement("ul", null, React.createElement("li", null, "Hover over a Function to learn more", React.createElement("br", null), React.createElement("br", null)), React.createElement("li", null, "Select a Function to add to the Expression in the Editor")))))))) }, React.createElement(SimpleButton, { "data-name": "expression-dropdown", icon: "arrow-down", iconPosition: 'end', mr: 1 }, React.createElement(Flex, { marginRight: 1, fontSize: 2 }, React.createElement(Icon, { name: "equation" }))))); }; export function BaseEditorInput(props) { const { expressionFunctions, testData, style, type } = props; const { ref: textAreaRefCallback, selectionStart, selectionEnd } = useSelectionRange(); const cursor = selectionStart === selectionEnd ? selectionStart : null; let result; let evaluationError; let expressionError; let selectedFunctionName; const baseClassName = 'ab-ExpressionEditorInput'; const buildParserExceptionMessage = (e) => { const parserExceptionSummary = 'Invalid expression is not parsable'; if (!e.message) { return parserExceptionSummary; } const parserExceptionDetails = e.message; return (React.createElement("details", null, React.createElement(Flex, { marginBottom: 1, as: "summary" }, parserExceptionSummary, React.createElement("i", null, " (click for more details)")), React.createElement(Text, { marginLeft: 3, style: { fontStyle: 'italic' } }, parserExceptionDetails))); }; const testRowNode = useMemo(() => { const firstRowNode = props.api.gridApi.getFirstRowNode(); if (!firstRowNode || !firstRowNode.data) { return {}; } // clone the class instance to still keep the prototype methods return Object.assign(Object.create(Object.getPrototypeOf(firstRowNode)), firstRowNode); }, []); try { // explicitly parsing & evaluating the expression because we need full control of the resulted AST const expr = parser.parse(props.value || ''); try { testRowNode.data = testData; // Mock datachangedevent for change-based functions e.g. PERCENT_CHANGE const dataChangedEvent = { newValue: 100, oldValue: 150, }; result = expr.evaluate({ // we need a fully-fledged rowNode as Adaptable accesses internal methods of it node: testRowNode, functions: expressionFunctions, evaluateCustomQueryVariable: props.api.internalApi.getQueryLanguageService().evaluateCustomQueryVariable, dataChangedEvent, ...props.api.internalApi.buildBaseContext(), }); } catch (err) { if (err instanceof ExpressionEvaluationError) { evaluationError = err; } else { // unexpected error, pass it on to the enclosing handler throw err; } } const path = parser.findPathTo(expr.ast, cursor); selectedFunctionName = path[0] ? path[0].type : null; } catch (e) { // parse errors should have a hash, otherwise it's an unexpected runtime error const isParserException = !!e.hash; if (isParserException) { expressionError = buildParserExceptionMessage(e); } else { props.api.logWarn(`Unexpected error while evaluating '${props.value}': ${e}`); } } useEffect(() => { // update selected function only for focused textareas (where cursor is present) if (cursor != undefined) { props.onSelectedFunctionChange(selectedFunctionName ? expressionFunctions[selectedFunctionName] : null); } }, [selectedFunctionName]); const operatorButtons = props.editorButtons .filter((editorButtonDef) => !!expressionFunctions[editorButtonDef.functionName]) .map((editorButtonDef) => (React.createElement(EditorButton, { key: `${editorButtonDef.functionName}-operator`, data: editorButtonDef.data, icon: editorButtonDef.icon }, !editorButtonDef.icon && (editorButtonDef.text || editorButtonDef.functionName)))); const showDocumentationLink = props.api.internalApi.isDocumentationLinksDisplayed(); return (React.createElement(React.Fragment, null, React.createElement(Flex, { className: baseClassName, "data-name": "expression-toolbar", py: 2, mb: 2, mt: 2, flexWrap: "wrap" }, React.createElement(Flex, { style: { flex: 1, marginLeft: 5 }, flexWrap: "wrap" }, React.createElement(FunctionsDropdown, { expressionFunctions: expressionFunctions, baseClassName: baseClassName }), operatorButtons), showDocumentationLink && (React.createElement(Flex, { alignItems: "flex-start" }, React.createElement(ButtonInfo, { mr: 2, tooltip: 'Learn how to use the Expression Editor', onClick: () => window.open(ExpressionEditorDocsLink, '_blank') })))), React.createElement(Textarea, { "data-name": `expression-input-${type}`, ref: textAreaRefCallback, value: props.value || '', placeholder: props.placeholder || 'Create Query', disabled: props.disabled || false, className: join('ab-ExpressionEditor__textarea', // left for backwards compatibility `${baseClassName}__textarea`), autoFocus: true, spellCheck: "false", onChange: (event) => { props.onChange(event.target.value); }, style: style }), props.isFullExpression !== true && (React.createElement(HelpBlock, { mt: 2, mb: 2, p: 2, fontSize: 3 }, "This Expression must resolve to a ", React.createElement("b", null, "boolean "), "(i.e. true / false) value")), expressionError && (React.createElement(ErrorBox, { width: "100%", style: { whiteSpace: 'pre-wrap' }, mt: 2 }, expressionError)), evaluationError && (React.createElement(ErrorBox, { style: { whiteSpace: 'pre-wrap' }, mt: 2 }, `${evaluationError.expressionFnName} ${evaluationError.message}`)), !props.hideResultPreview && result !== undefined && (React.createElement(Box, { className: `${baseClassName}__editor-feedback`, "data-name": "expression-editor-feedback", mt: 2, p: 2 }, React.createElement("pre", { style: { whiteSpace: 'pre-wrap', margin: 0 } }, "Result (using Test Data): ", React.createElement("b", null, JSON.stringify(result))))))); }