UNPKG

@adaptabletools/adaptable

Version:

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

228 lines (227 loc) 15.7 kB
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import * as parser from '../../parser/src'; import { cn } from '../../lib/utils'; 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'; import { Box, Flex } from '../Flex'; import { AdaptableFormControlTextClear } from '../../View/Components/Forms/AdaptableFormControlTextClear'; 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 = ({ className }) => { const adaptable = useAdaptable(); const customQueryVariables = adaptable.api.optionsApi.getExpressionOptions()?.customQueryVariables; if (!customQueryVariables || Object.keys(customQueryVariables).length === 0) { return _jsx(_Fragment, {}); } return (_jsx(_Fragment, { children: Object.keys(customQueryVariables).map((varOption) => { const varString = `VAR('${varOption}')`; return (_jsx(EditorButton, { data: varString, className: className, children: varString }, varOption)); }) })); }; 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]; 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); }; const [searchInputValue, setSearchInputValue] = React.useState(''); return (_jsx(OverlayTrigger, { ref: dropdownRef, showEvent: "mouseenter", hideEvent: "mouseleave", targetOffset: 5, render: () => (_jsxs(Flex, { className: `${baseClassName}__dropdown-functions-list-wrapper twa:max-w-[60vw] twa:shadow-md twa:bg-background twa:text-foreground twa:text-3 twa:rounded-standard twa:p-3`, flexDirection: "column", onMouseLeave: () => hidePopup(), children: [_jsx(Flex, { className: "twa:md:gap-3 twa:gap-2 twa:pb-2 twa:md:justify-between twa:justify-start twa:flex-wrap twa:border-b twa:border-primarydark/30 twa:mb-2", children: categoryOptions.map((option, index) => { return (_jsx(Radio, { onFocus: (event) => { event.preventDefault(); event.stopPropagation(); }, onClick: handleFunctionCategoryChange(option.value), checked: currentFunctionCategory === option.value, children: option.label }, option.value)); }) }), _jsxs(Flex, { className: "twa:gap-2 twa:max-h-[60vh]", flexDirection: "row", children: [_jsxs(Flex, { flexDirection: "column", className: "twa:gap-2 twa:rounded-standard", children: [_jsx(Box, { children: _jsx(AdaptableFormControlTextClear, { className: "twa:w-full", value: searchInputValue, OnTextChange: setSearchInputValue, placeholder: "Search Functions" }) }), _jsx(Flex, { className: `${baseClassName}__dropdown-functions-list twa:overflow-auto`, "data-name": "expression-dropdown-functions-list", flexDirection: "column", children: orderedGroupNames .filter((groupName) => !!groupedFunctions[groupName]) .map((groupName) => { const functionsInGroup = Object.keys(groupedFunctions[groupName]); if (functionsInGroup.length === 0) { return _jsx(React.Fragment, {}, groupName); } const getEditorButtonData = (functionName) => { 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}()`; }; const fns = functionsInGroup .map((functionName) => { if (searchInputValue && !functionName.toLowerCase().includes(searchInputValue.toLowerCase())) { return null; } const buttonClassName = 'twa:mr-1 twa:hover:bg-accent twa:hover:text-accent-foreground'; return (_jsx(Box, { onMouseEnter: () => setOverFunction(functionName), onClick: () => hidePopup(), children: functionName === 'VAR' ? (_jsx(VarEditorButton, { className: buttonClassName }, functionName)) : (_jsx(EditorButton, { data: getEditorButtonData(functionName), className: buttonClassName, children: functionName }, functionName)) }, functionName)); }) .filter(Boolean); return (_jsxs(Box, { className: cn('twa:mb-2 twa:mx-1', fns.length === 0 ? 'twa:hidden' : ''), children: [_jsx(Tag, { className: "twa:mb-1 twa:bg-primarylight twa:text-primary-foreground twa:w-full", children: StringExtensions.Humanize(groupName) }), fns] }, groupName)); }) })] }), _jsx(Box, { className: cn(`${baseClassName}__dropdown-functions-description `, `twa:flex-1 twa:p-2 twa:w-[600px] twa:max-w-[60vw]`, `twa:rounded-standard twa:shadow-sm twa:overflow-auto twa:h-auto`, 'twa:bg-primarylight twa:text-primary-foreground'), children: overFunction ? (_jsxs(_Fragment, { children: [_jsx(Tag, { className: "twa:bg-accent twa:text-accent-foreground", children: overFunction }), _jsx(ExpressionFunctionDocumentation, { expressionFunction: expressionFunctions[overFunction] })] })) : (_jsx(Flex, { className: "twa:size-full twa:items-center twa:justify-center twa:text-2 twa:italic", children: _jsxs("ul", { children: [_jsxs("li", { children: ["Hover over a Function to learn more", _jsx("br", {}), _jsx("br", {})] }), _jsx("li", { children: "Click a Function to add it to the Expression in the Editor" })] }) })) })] })] })), children: _jsx(SimpleButton, { "data-name": "expression-dropdown", icon: "arrow-down", iconPosition: 'end', className: "twa:mr-1", children: _jsx(Flex, { className: "twa:mr-1 twa:text-2", children: _jsx(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 (_jsxs("details", { children: [_jsxs(Flex, { className: "twa:mb-1", as: "summary", children: [parserExceptionSummary, _jsx("i", { children: " (click for more details)" })] }), _jsx(Box, { className: "twa:ml-3 twa:italic", children: parserExceptionDetails })] })); }; const testRowNode = useMemo(() => { const firstRowNode = props.api.gridApi.getFirstRowNode(); if (!firstRowNode || !firstRowNode.data) { return {}; } return Object.assign(Object.create(Object.getPrototypeOf(firstRowNode)), firstRowNode); }, []); const scopeColumnId = useMemo(() => { return props.api.columnScopeApi.getAnyColumnIdForScope(props.columnScope); }, [props.columnScope]); try { const expr = parser.parse(props.value || ''); try { testRowNode.data = testData; const dataChangedEvent = { newValue: 100, oldValue: 150, }; result = expr.evaluate({ node: testRowNode, functions: expressionFunctions, evaluateCustomQueryVariable: props.api.internalApi.getQueryLanguageService().evaluateCustomQueryVariable, dataChangedEvent, columnScope: scopeColumnId, ...props.api.internalApi.buildBaseContext(), }); } catch (err) { if (err instanceof ExpressionEvaluationError) { evaluationError = err; } else { throw err; } } const path = parser.findPathTo(expr.ast, cursor); selectedFunctionName = path[0] ? path[0].type : null; } catch (e) { const isParserException = !!e.hash; if (isParserException) { expressionError = buildParserExceptionMessage(e); } else { props.api.logWarn(`Unexpected error while evaluating '${props.value}': ${e}`); } } useEffect(() => { if (cursor != undefined) { props.onSelectedFunctionChange(selectedFunctionName ? expressionFunctions[selectedFunctionName] : null); } }, [selectedFunctionName]); const operatorButtons = props.editorButtons .filter((editorButtonDef) => !!expressionFunctions[editorButtonDef.functionName]) .map((editorButtonDef) => (_jsx(EditorButton, { data: editorButtonDef.data, icon: editorButtonDef.icon, children: !editorButtonDef.icon && (editorButtonDef.text || editorButtonDef.functionName) }, `${editorButtonDef.functionName}-operator`))); const showDocumentationLink = props.api.internalApi.isDocumentationLinksDisplayed(); return (_jsxs(_Fragment, { children: [_jsxs(Flex, { "data-name": "expression-toolbar", className: `${baseClassName} twa:py-2 twa:mb-2 twa:mt-2 twa:flex-wrap twa:w-full`, children: [_jsxs(Flex, { className: "twa:flex-1 twa:ml-1 twa:flex-wrap", children: [_jsx(FunctionsDropdown, { expressionFunctions: expressionFunctions, baseClassName: baseClassName }), operatorButtons] }), showDocumentationLink && (_jsx(Flex, { alignItems: "flex-start", children: _jsx(ButtonInfo, { className: "twa:mr-2", tooltip: 'Learn how to use the Expression Editor', onClick: () => window.open(ExpressionEditorDocsLink, '_blank') }) }))] }), _jsx(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', `${baseClassName}__textarea`, 'twa:w-full twa:min-h-[100px] twa:p-2 twa:text-4 twa:resize-y twa:font-mono'), autoFocus: true, spellCheck: "false", onChange: (event) => { props.onChange(event.target.value); }, style: style }), props.isFullExpression !== true && (_jsxs(HelpBlock, { className: "twa:my-2 twa:p-2 twa:text-3", children: ["This Expression must resolve to a ", _jsx("b", { children: "boolean " }), "(i.e. true / false) value"] })), expressionError && (_jsx(ErrorBox, { className: "twa:mt-2 twa:whitespace-pre-wrap twa:w-full", children: expressionError })), evaluationError && (_jsx(ErrorBox, { className: "twa:mt-2 twa:whitespace-pre-wrap", children: `${evaluationError.expressionFnName} ${evaluationError.message}` })), !props.hideResultPreview && result !== undefined && (_jsx(Box, { className: `${baseClassName}__editor-feedback twa:mt-2 twa:p-2 twa:rounded-standard`, "data-name": "expression-editor-feedback", children: _jsxs("pre", { className: "twa:whitespace-pre-wrap twa:m-0", children: ["Result (using test data):", ' ', _jsx("b", { className: "twa:bg-accentlight twa:text-accent twa:rounded-full twa:p-1 twa:px-2", children: JSON.stringify(result) })] }) }))] })); }