UNPKG

@adaptabletools/adaptable-cjs

Version:

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

268 lines (267 loc) 15.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BaseEditorInput = void 0; const tslib_1 = require("tslib"); const rebass_1 = require("rebass"); const parser = tslib_1.__importStar(require("../../parser/src")); const React = tslib_1.__importStar(require("react")); const ErrorBox_1 = tslib_1.__importDefault(require("../ErrorBox")); const useSelectionRange_1 = require("../utils/useSelectionRange"); const EditorButton_1 = tslib_1.__importDefault(require("./EditorButton")); const HelpBlock_1 = tslib_1.__importDefault(require("../HelpBlock")); const OverlayTrigger_1 = tslib_1.__importDefault(require("../OverlayTrigger")); const SimpleButton_1 = tslib_1.__importDefault(require("../SimpleButton")); const Textarea_1 = tslib_1.__importDefault(require("../Textarea")); const react_1 = require("react"); const ExpressionEvaluationError_1 = require("../../parser/src/ExpressionEvaluationError"); const ButtonInfo_1 = require("../../View/Components/Buttons/ButtonInfo"); const DocumentationLinkConstants_1 = require("../../Utilities/Constants/DocumentationLinkConstants"); const icons_1 = require("../icons"); const AdaptableContext_1 = require("../../View/AdaptableContext"); const join_1 = tslib_1.__importDefault(require("../utils/join")); const ExpressionFunctionDocumentation_1 = require("./ExpressionFunctionDocumentation"); const Tag_1 = require("../Tag"); const StringExtensions_1 = tslib_1.__importDefault(require("../../Utilities/Extensions/StringExtensions")); const Radio_1 = tslib_1.__importDefault(require("../Radio")); const filterableCategories = [ 'dates', 'logical', 'maths', 'strings', 'changes', 'comparison', 'observable', 'aggregation', 'cumulative', ]; const getCategoryOrder = (category) => { const predefinedOrder = { special: 1, conditional: 2, logical: 3, comparison: 4, strings: 5, maths: 6, dates: 7, changes: 8, }; return predefinedOrder[category] || 0; }; const VarEditorButton = () => { const adaptable = (0, AdaptableContext_1.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_1.default, { 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_1.default.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_1.default, { ref: dropdownRef, showEvent: "mouseenter", hideEvent: "mouseleave", targetOffset: 5, render: () => (React.createElement(rebass_1.Flex, { className: `${baseClassName}__dropdown-functions-list-wrapper`, flexDirection: "column", onMouseLeave: () => hidePopup() }, React.createElement(rebass_1.Flex, { pl: 2, style: { gap: 10 } }, categoryOptions.map((option, index) => { return (React.createElement(Radio_1.default, { key: option.value, onFocus: (event) => { event.preventDefault(); event.stopPropagation(); }, onClick: handleFunctionCategoryChange(option.value), checked: currentFunctionCategory === option.value }, option.label)); })), React.createElement(rebass_1.Flex, null, React.createElement(rebass_1.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(rebass_1.Box, { mb: 2, key: groupName }, React.createElement(Tag_1.Tag, { mb: 1 }, StringExtensions_1.default.Humanize(groupName)), functionsInGroup.map((functionName) => (React.createElement(rebass_1.Box, { key: functionName, onMouseEnter: () => setOverFunction(functionName), onClick: () => hidePopup() }, functionName === 'VAR' ? (React.createElement(VarEditorButton, { key: functionName })) : (React.createElement(EditorButton_1.default, { data: getEditorButtonData(functionName), key: functionName, mr: 1 }, functionName))))))); })), React.createElement(rebass_1.Box, { className: `${baseClassName}__dropdown-functions-description`, p: 2, width: 600, maxWidth: "30vw" }, overFunction ? (React.createElement(React.Fragment, null, React.createElement(Tag_1.Tag, null, overFunction), React.createElement(ExpressionFunctionDocumentation_1.ExpressionFunctionDocumentation, { expressionFunction: expressionFunctions[overFunction] }))) : (React.createElement(Tag_1.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_1.default, { "data-name": "expression-dropdown", icon: "arrow-down", iconPosition: 'end', mr: 1 }, React.createElement(rebass_1.Flex, { marginRight: 1, fontSize: 2 }, React.createElement(icons_1.Icon, { name: "equation" }))))); }; function BaseEditorInput(props) { const { expressionFunctions, testData, style, type } = props; const { ref: textAreaRefCallback, selectionStart, selectionEnd } = (0, useSelectionRange_1.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(rebass_1.Flex, { marginBottom: 1, as: "summary" }, parserExceptionSummary, React.createElement("i", null, " (click for more details)")), React.createElement(rebass_1.Text, { marginLeft: 3, style: { fontStyle: 'italic' } }, parserExceptionDetails))); }; const testRowNode = (0, react_1.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_1.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}`); } } (0, react_1.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_1.default, { 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(rebass_1.Flex, { className: baseClassName, "data-name": "expression-toolbar", py: 2, mb: 2, mt: 2, flexWrap: "wrap" }, React.createElement(rebass_1.Flex, { style: { flex: 1, marginLeft: 5 }, flexWrap: "wrap" }, React.createElement(FunctionsDropdown, { expressionFunctions: expressionFunctions, baseClassName: baseClassName }), operatorButtons), showDocumentationLink && (React.createElement(rebass_1.Flex, { alignItems: "flex-start" }, React.createElement(ButtonInfo_1.ButtonInfo, { mr: 2, tooltip: 'Learn how to use the Expression Editor', onClick: () => window.open(DocumentationLinkConstants_1.ExpressionEditorDocsLink, '_blank') })))), React.createElement(Textarea_1.default, { "data-name": `expression-input-${type}`, ref: textAreaRefCallback, value: props.value || '', placeholder: props.placeholder || 'Create Query', disabled: props.disabled || false, className: (0, join_1.default)('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_1.default, { 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_1.default, { width: "100%", style: { whiteSpace: 'pre-wrap' }, mt: 2 }, expressionError)), evaluationError && (React.createElement(ErrorBox_1.default, { style: { whiteSpace: 'pre-wrap' }, mt: 2 }, `${evaluationError.expressionFnName} ${evaluationError.message}`)), !props.hideResultPreview && result !== undefined && (React.createElement(rebass_1.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))))))); } exports.BaseEditorInput = BaseEditorInput;