/* eslint-disable import/no-unresolved */ import React, { useContext, useEffect, useRef, useState } from 'react'; import { PreviewBox } from './PreviewBox'; import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip'; import { useTranslation } from 'react-i18next'; import { camelCase, isEmpty, noop } from 'lodash'; import CodeMirror from '@uiw/react-codemirror'; import { javascript } from '@codemirror/lang-javascript'; import { autocompletion, completionKeymap, completionStatus, acceptCompletion, startCompletion, } from '@codemirror/autocomplete'; import { defaultKeymap } from '@codemirror/commands'; import { keymap } from '@codemirror/view'; import FxButton from '../CodeBuilder/Elements/FxButton'; import cx from 'classnames'; import { DynamicFxTypeRenderer } from './DynamicFxTypeRenderer'; import { resolveReferences } from './utils'; import { okaidia } from '@uiw/codemirror-theme-okaidia'; import { githubLight } from '@uiw/codemirror-theme-github'; import { getAutocompletion } from './autocompleteExtensionConfig'; import ErrorBoundary from '@/_ui/ErrorBoundary'; import CodeHinter from './CodeHinter'; // import { EditorContext } from '../Context/EditorContextWrapper'; import { removeNestedDoubleCurlyBraces } from '@/_helpers/utils'; import useStore from '@/AppBuilder/_stores/store'; import { shallow } from 'zustand/shallow'; import { CodeHinterContext } from '../CodeBuilder/CodeHinterContext'; import { createReferencesLookup } from '@/_stores/utils'; const SingleLineCodeEditor = ({ componentName, fieldMeta = {}, componentId, ...restProps }) => { const { initialValue, onChange, enablePreview = true, portalProps } = restProps; const { validation = {} } = fieldMeta; const [isFocused, setIsFocused] = useState(false); const [currentValue, setCurrentValue] = useState(''); const [errorStateActive, setErrorStateActive] = useState(false); const [cursorInsidePreview, setCursorInsidePreview] = useState(false); const componentDefinition = useStore((state) => state.getComponentDefinition(componentId), shallow); const parentId = componentDefinition?.component?.parent; const customResolvables = useStore((state) => state.resolvedStore.modules.canvas?.customResolvables, shallow); const customVariables = customResolvables?.[parentId]?.[0] || {}; const isPreviewFocused = useRef(false); const wrapperRef = useRef(null); const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow); let newInitialValue = initialValue; if (typeof initialValue === 'string' && (initialValue?.includes('components') || initialValue?.includes('queries'))) { newInitialValue = replaceIdsWithName(initialValue); } //! Re render the component when the componentName changes as the initialValue is not updated // const { variablesExposedForPreview } = useContext(EditorContext) || {}; // const customVariables = variablesExposedForPreview?.[componentId] ?? {}; useEffect(() => { if (typeof newInitialValue !== 'string') return; // const [valid, _error] = !isEmpty(validation) // ? resolveReferences(newInitialValue, validation, customVariables) // : [true, null]; //!TODO use the updated new resolver const [valid, _error] = !isEmpty(validation) ? resolveReferences(newInitialValue, validation, customVariables) : [true, null]; if (!valid) { setErrorStateActive(true); } setCurrentValue(newInitialValue); // eslint-disable-next-line react-hooks/exhaustive-deps }, [componentName, newInitialValue]); useEffect(() => { const handleClickOutside = (event) => { if (cursorInsidePreview || portalProps?.isOpen || event.target.closest('.cm-tooltip-autocomplete')) { return; } if (wrapperRef.current && isFocused && !wrapperRef.current.contains(event.target)) { isPreviewFocused.current = false; setIsFocused(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [wrapperRef, isFocused, isPreviewFocused, currentValue, portalProps?.isOpen, cursorInsidePreview]); const isWorkspaceVariable = typeof currentValue === 'string' && (currentValue.includes('%%client') || currentValue.includes('%%server')); return (
); }; const EditorInput = ({ currentValue, setCurrentValue, setFocus, validationType, onBlurUpdate, placeholder = '', error, cyLabel = '', componentName, usePortalEditor = true, renderPreview, portalProps, lang, isFocused, componentId, type, delayOnChange = true, // Added this prop to immediately update the onBlurUpdate callback paramLabel = '', disabled = false, }) => { const codeHinterContext = useContext(CodeHinterContext); const { suggestionList: paramHints } = createReferencesLookup(codeHinterContext, true); const getSuggestions = useStore((state) => state.getSuggestions, shallow); const [codeMirrorView, setCodeMirrorView] = useState(undefined); function autoCompleteExtensionConfig(context) { const hintsWithoutParamHints = getSuggestions(); let word = context.matchBefore(/\w*/); const hints = { ...hintsWithoutParamHints, appHints: [...hintsWithoutParamHints.appHints, ...paramHints], }; const totalReferences = (context.state.doc.toString().match(/{{/g) || []).length; let queryInput = context.state.doc.toString(); const originalQueryInput = queryInput; if (totalReferences > 0) { const currentCursor = context.state.selection.main.head; const currentCursorPos = context.pos; let currentWord = queryInput.substring(currentCursor, currentCursorPos); if (currentWord?.length === 0) { const lastBracesFromPos = queryInput.lastIndexOf('{{', currentCursorPos); currentWord = queryInput.substring(lastBracesFromPos, currentCursorPos); //remove curly braces from the current word as will append it later currentWord = removeNestedDoubleCurlyBraces(currentWord); } if (currentWord.includes(' ')) { currentWord = currentWord.split(' ').pop(); } // remove \n from the current word if it is present currentWord = currentWord.replace(/\n/g, ''); queryInput = '{{' + currentWord + '}}'; } let completions = getAutocompletion(queryInput, validationType, hints, totalReferences, originalQueryInput); return { from: word.from, options: completions, validFor: /^\{\{.*\}\}$/, filter: false, }; } // eslint-disable-next-line react-hooks/exhaustive-deps const overRideFunction = React.useCallback((context) => autoCompleteExtensionConfig(context), [paramHints]); const autoCompleteConfig = autocompletion({ override: [overRideFunction], compareCompletions: (a, b) => { return a.section.rank - b.section.rank && a.label.localeCompare(b.label); }, aboveCursor: false, defaultKeymap: true, positionInfo: () => { return { class: 'cm-completionInfo-top cm-custom-completion-info', }; }, maxRenderedOptions: 10, }); const customKeyMaps = [...defaultKeymap, ...completionKeymap]; const customTabKeymap = keymap.of([ { key: 'Tab', run: (view) => { if (completionStatus(view.state)) { return acceptCompletion(view); } if (isOpen) { const { state } = view; const { selection } = state; const { anchor } = selection.main; const tabSize = 2; view?.dispatch({ changes: { from: anchor, insert: ' '.repeat(tabSize) }, selection: { anchor: anchor + tabSize }, }); return true; } }, }, ]); const handleOnChange = React.useCallback((val) => { setCurrentValue(val); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleOnBlur = () => { if (!delayOnChange) { setFirstTimeFocus(false); return onBlurUpdate(currentValue); } setTimeout(() => { setFirstTimeFocus(false); onBlurUpdate(currentValue); }, 0); }; const darkMode = localStorage.getItem('darkMode') === 'true'; const theme = darkMode ? okaidia : githubLight; const { handleTogglePopupExapand, isOpen, setIsOpen, forceUpdate } = portalProps; const [firstTimeFocus, setFirstTimeFocus] = useState(false); const customClassNames = cx('codehinter-input', { 'border-danger': error, focused: isFocused, 'focus-box-shadow-active': firstTimeFocus, 'widget-code-editor': componentId, 'disabled-pointerevents': disabled, }); const currentEditorHeightRef = useRef(null); const handleFocus = () => { setFirstTimeFocus(true); setTimeout(() => { setFocus(true); }, 50); }; const showLineNumbers = lang == 'jsx' || type === 'extendedSingleLine' || false; cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : cyLabel; return (
{usePortalEditor && ( )} { setCodeMirrorView(view); }} value={currentValue} placeholder={placeholder} height={showLineNumbers ? '400px' : '100%'} width="100%" extensions={[ javascript({ jsx: lang === 'jsx' }), autoCompleteConfig, keymap.of([...customKeyMaps]), customTabKeymap, ]} onChange={(val) => { setFirstTimeFocus(false); handleOnChange(val); }} basicSetup={{ lineNumbers: showLineNumbers, syntaxHighlighting: true, bracketMatching: true, foldGutter: false, highlightActiveLine: false, autocompletion: true, completionKeymap: true, searchKeymap: false, }} onMouseDown={() => handleFocus()} onBlur={() => handleOnBlur()} className={customClassNames} theme={theme} indentWithTab={false} readOnly={disabled} onKeyDown={(event) => { if (event.key === 'Backspace') { startCompletion(codeMirrorView); } }} />
); }; const DynamicEditorBridge = (props) => { const { initialValue, type, fxActive, paramType = 'code', paramLabel, paramName, fieldMeta, darkMode, className, onFxPress = noop, onChange, styleDefinition, component, onVisibilityChange, isEventManagerParam = false, } = props; const [forceCodeBox, setForceCodeBox] = React.useState(fxActive); const codeShow = paramType === 'code' || forceCodeBox; const HIDDEN_CODE_HINTER_LABELS = ['Table data', 'Column data', 'Text Format']; const { isFxNotRequired } = fieldMeta; const { t } = useTranslation(); const [_, error, value] = type === 'fxEditor' ? resolveReferences(initialValue) : []; let cyLabel = paramLabel ? paramLabel.toLowerCase().trim().replace(/\s+/g, '-') : props.cyLabel; useEffect(() => { setForceCodeBox(fxActive); }, [component, fxActive]); const fxClass = isEventManagerParam ? 'justify-content-start' : 'justify-content-end'; return (
{paramLabel !== ' ' && !HIDDEN_CODE_HINTER_LABELS.includes(paramLabel) && (
)}
{paramLabel !== 'Type' && isFxNotRequired === undefined && (
{ if (codeShow) { setForceCodeBox(false); onFxPress(false); } else { setForceCodeBox(true); onFxPress(true); } }} dataCy={cyLabel} />
)} {!codeShow && ( { setForceCodeBox(true); onFxPress(true); }} meta={fieldMeta} cyLabel={cyLabel} styleDefinition={styleDefinition} component={component} onVisibilityChange={onVisibilityChange} /> )}
{codeShow && (
)}
); }; SingleLineCodeEditor.Editor = EditorInput; SingleLineCodeEditor.EditorBridge = DynamicEditorBridge; export default SingleLineCodeEditor;