import React, { useEffect, useState, useRef, useContext } from 'react'; import { useSpring, config, animated } from 'react-spring'; import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; import Tooltip from 'react-bootstrap/Tooltip'; import CodeMirror from '@uiw/react-codemirror'; import 'codemirror/mode/handlebars/handlebars'; import 'codemirror/mode/javascript/javascript'; import 'codemirror/mode/sql/sql'; import 'codemirror/addon/hint/show-hint'; import 'codemirror/addon/display/placeholder'; import 'codemirror/addon/search/match-highlighter'; import 'codemirror/addon/hint/show-hint.css'; import 'codemirror/theme/base16-light.css'; import 'codemirror/theme/duotone-light.css'; import 'codemirror/theme/monokai.css'; import { onBeforeChange, handleChange } from './utils'; import { resolveReferences, hasCircularDependency, handleCircularStructureToJSON } from '@/_helpers/utils'; import useHeight from '@/_hooks/use-height-transition'; import usePortal from '@/_hooks/use-portal'; import { Color } from './Elements/Color'; import { Json } from './Elements/Json'; import { Select } from './Elements/Select'; import { Toggle } from './Elements/Toggle'; import { AlignButtons } from './Elements/AlignButtons'; import { TypeMapping } from './TypeMapping'; import { Number } from './Elements/Number'; import { BoxShadow } from './Elements/BoxShadow'; import FxButton from './Elements/FxButton'; import { ToolTip } from '../Inspector/Elements/Components/ToolTip'; import { toast } from 'react-hot-toast'; import { EditorContext } from '@/Editor/Context/EditorContextWrapper'; import { camelCase } from 'lodash'; import { useTranslation } from 'react-i18next'; import cx from 'classnames'; import { useCurrentState } from '@/_stores/currentStateStore'; const AllElements = { Color, Json, Toggle, Select, AlignButtons, Number, BoxShadow, }; export function CodeHinter({ initialValue, onChange, mode, theme, lineNumbers, placeholder, ignoreBraces, enablePreview, height, minHeight, lineWrapping, componentName = null, usePortalEditor = true, className, width = '', paramName, paramLabel, type, fieldMeta, onFxPress, fxActive, component, popOverCallback, cyLabel = '', callgpt = () => null, isCopilotEnabled = false, currentState: _currentState, }) { const darkMode = localStorage.getItem('darkMode') === 'true'; const options = { lineNumbers: lineNumbers ?? false, lineWrapping: lineWrapping ?? true, singleLine: true, mode: mode || 'handlebars', tabSize: 2, theme: theme ? theme : darkMode ? 'monokai' : 'default', readOnly: false, highlightSelectionMatches: true, placeholder, }; const currentState = useCurrentState(); const [realState, setRealState] = useState(currentState); const [currentValue, setCurrentValue] = useState(initialValue); const [isFocused, setFocused] = useState(false); const [heightRef, currentHeight] = useHeight(); const isPreviewFocused = useRef(false); const wrapperRef = useRef(null); const slideInStyles = useSpring({ config: { ...config.stiff }, from: { opacity: 0, height: 0 }, to: { opacity: isFocused ? 1 : 0, height: isFocused ? currentHeight : 0, }, }); const { t } = useTranslation(); const { variablesExposedForPreview } = useContext(EditorContext); const prevCountRef = useRef(false); useEffect(() => { if (_currentState) { setRealState(_currentState); } else { setRealState(currentState); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentState.components, _currentState]); useEffect(() => { const handleClickOutside = (event) => { if (isOpen) { return; } if (wrapperRef.current && isFocused && !wrapperRef.current.contains(event.target) && prevCountRef.current) { isPreviewFocused.current = false; setFocused(false); prevCountRef.current = false; } else if (isFocused) { prevCountRef.current = true; } else if (!isFocused && prevCountRef.current) prevCountRef.current = false; }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [wrapperRef, isFocused, isPreviewFocused, currentValue, prevCountRef, isOpen]); function valueChanged(editor, onChange, ignoreBraces) { if (editor.getValue()?.trim() !== currentValue) { handleChange(editor, onChange, ignoreBraces, realState, componentName); setCurrentValue(editor.getValue()?.trim()); } } const getPreviewContent = (content, type) => { try { switch (type) { case 'object': return JSON.stringify(content); case 'boolean': return content.toString(); default: return content; } } catch (e) { return undefined; } }; const focusPreview = () => (isPreviewFocused.current = true); const unFocusPreview = () => (isPreviewFocused.current = false); const copyToClipboard = (text) => { navigator.clipboard.writeText(text); toast.success('Copied to clipboard'); }; const getCustomResolvables = () => { if (variablesExposedForPreview.hasOwnProperty(component?.id)) { if (component?.component?.component === 'Table' && fieldMeta?.name) { return { ...variablesExposedForPreview[component?.id], cellValue: variablesExposedForPreview[component?.id]?.rowData?.[fieldMeta?.name], rowData: { ...variablesExposedForPreview[component?.id]?.rowData }, }; } return variablesExposedForPreview[component.id]; } return {}; }; const getPreview = () => { if (!enablePreview) return; const customResolvables = getCustomResolvables(); const [preview, error] = resolveReferences(currentValue, realState, null, customResolvables, true, true); const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1'; if (error) { const err = String(error); const errorMessage = err.includes('.run()') ? `${err} in ${componentName.split('::')[0]}'s field` : err; return (
Error
{errorMessage}
); } let previewType = typeof preview; let previewContent = preview; if (hasCircularDependency(preview)) { previewContent = JSON.stringify(preview, handleCircularStructureToJSON()); previewType = typeof previewContent; } const content = getPreviewContent(previewContent, previewType); return ( focusPreview()} onMouseLeave={() => unFocusPreview()} >
{previewType}
{isFocused && (
copyToClipboard(content)} icon="copy" tip="Copy to clipboard" />
)}
{content}
); }; enablePreview = enablePreview ?? true; const [isOpen, setIsOpen] = React.useState(false); const handleToggle = () => { const changeOpen = (newOpen) => { setIsOpen(newOpen); if (typeof popOverCallback === 'function') popOverCallback(newOpen); }; if (!isOpen) { changeOpen(true); } return new Promise((resolve) => { const element = document.getElementsByClassName('portal-container'); if (element) { const checkPortalExits = element[0]?.classList.contains(componentName); if (checkPortalExits === false) { const parent = element[0].parentNode; parent.removeChild(element[0]); } changeOpen(false); resolve(); } }).then(() => { changeOpen(true); forceUpdate(); }); }; const [, forceUpdate] = React.useReducer((x) => x + 1, 0); const defaultClassName = className === 'query-hinter' || className === 'custom-component' || undefined ? '' : 'code-hinter'; const ElementToRender = AllElements[TypeMapping[type]]; const [forceCodeBox, setForceCodeBox] = useState(fxActive); const codeShow = (type ?? 'code') === 'code' || forceCodeBox; cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : cyLabel; return (
{paramLabel && (
)}
{ setForceCodeBox(false); onFxPress(false); }} dataCy={cyLabel} />
{usePortalEditor && ( )} setFocused(true)} onBlur={(editor, e) => { e.stopPropagation(); const value = editor.getValue()?.trimEnd(); onChange(value); if (!isPreviewFocused.current) { setFocused(false); } }} onChange={(editor) => valueChanged(editor, onChange, ignoreBraces)} onBeforeChange={(editor, change) => onBeforeChange(editor, change, ignoreBraces)} options={options} viewportMargin={Infinity} />
{enablePreview && !isOpen && getPreview()}
{!codeShow && (
{ if (value !== currentValue) { onChange(value); setCurrentValue(value); } }} paramName={paramName} paramLabel={paramLabel} forceCodeBox={() => { setForceCodeBox(true); onFxPress(true); }} meta={fieldMeta} cyLabel={cyLabel} />
)}
); } const PopupIcon = ({ callback, icon, tip, transformation = false }) => { const size = transformation ? 20 : 12; return (
{tip}} > { e.stopPropagation(); callback(); }} />
); }; const Portal = ({ children, ...restProps }) => { const renderPortal = usePortal({ children, ...restProps }); return {renderPortal}; }; CodeHinter.PopupIcon = PopupIcon; CodeHinter.Portal = Portal;