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