@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
JavaScript
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)))))));
}