ToolJet/frontend/src/Editor/CodeBuilder/CodeHinter.jsx

295 lines
9.1 KiB
React
Raw Normal View History

import React, { useEffect, useMemo, useState } 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';
2021-05-03 11:26:31 +00:00
import 'codemirror/mode/handlebars/handlebars';
2021-05-09 04:25:17 +00:00
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';
2021-05-03 14:27:32 +00:00
import 'codemirror/addon/hint/show-hint.css';
import 'codemirror/theme/base16-light.css';
import 'codemirror/theme/duotone-light.css';
2021-07-03 17:07:50 +00:00
import 'codemirror/theme/monokai.css';
2021-05-03 16:04:54 +00:00
import { getSuggestionKeys, onBeforeChange, handleChange } from './utils';
import { resolveReferences } 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 FxButton from './Elements/FxButton';
const AllElements = {
Color,
Json,
Toggle,
Select,
AlignButtons,
Number,
};
2021-05-03 14:27:32 +00:00
export function CodeHinter({
initialValue,
onChange,
currentState,
mode,
theme,
lineNumbers,
placeholder,
ignoreBraces,
enablePreview,
2021-08-21 04:09:04 +00:00
height,
minHeight,
lineWrapping,
componentName = null,
usePortalEditor = true,
className,
[improvement] Query panel redesign (#1947) * query panel headers styles * create query button style update * create query button style update * create query button style update: mixins * querypanel header nav-links updated * querypanel header buttons * restapi: url hinter styles * fixes querypane header width, undo prev styles applied * querypanel header icons margin fix * restapi: header tab * restapi: params tab * restapi: body tab * base url style fixed * added alert component to @/_ui * adds margin top to query-panel tabs * bumped font weight of preview * on hover query style updated * selected query style update and along with dark themed * adds new searchbox component, added search queries * fixes query panel query lists icons margin * query selected hover bg * back icon should not be render if callback is a function * airtable: queries redesigned * stripe: queries redesigned * fixed query-pane header responsiveness * fixes tab title typo * undp/redo select datasource only when mode='create' * removes comments * refactor restapi url codehinter styles * fixes white spaces in query pane and query pane header * minor updates for restapi query options styles * removes tool-tip for lens svg icon for search: query-pane header * adds button loading spinner with primary color * fixes hover bg color for queries * update: query hover bg color to lighter tint * update query list icons: trash and play svgs * adds icons to the selection component * fixes tabs alignments restapi * fixes codehonter text margin: restapi url * fixes advanced-options-container margintop * new UI query: dynamodb * new UI query: elasticsearch * new UI query: firestore * new UI query: mongodb * new UI query: mysql * new UI query: psql * new UI query: typesenseapi * new UI query: gsheet * new UI query: sendgrid * new UI query: twillio * new UI query: gcs * new UI query: minio * new UI query: aws * update query bg color and hover bg color * fixes run query icon for dark theme * fixes input query title text alignment * update query header title * adds search queries placeholder and add icon when search box is displayed * updated No results query text * undo/redo rest-api tabs: fixes whote spaces * undo/redo: rest-api tabs conde hinter placeholder paddinleft * adding new queries when search box component is mounted:fix * fixes creating a new query after filtering queries * fixes dark theme for select search components * reverts query pane header search box with add icon * fixes input left margin * fixes bottom padding rest api tabs * fixes toggle button:not selected in dark theme * fixes typos * fixes preview button spinner size * undo/redo: query-trash-icon.svg for queries and trash.svg for components * query icons badge bg update[options] * icon badge fix * move static styles to theme.scss * restapi: url field height increment * fixes query name when adding a new query with filtered querylist * fixes alignment issues of the query header icon * revert back to 32px code hinter height * adds apdding to code mirror line * updates to react-select * remove component unmounts and mounts side effect
2022-02-02 16:59:57 +00:00
width = '100%',
paramName,
paramLabel,
type,
fieldMeta,
onFxPress,
fxActive,
}) {
const darkMode = localStorage.getItem('darkMode') === 'true';
2021-05-03 14:27:32 +00:00
const options = {
lineNumbers: lineNumbers ?? false,
lineWrapping: lineWrapping ?? true,
2021-05-03 14:27:32 +00:00
singleLine: true,
mode: mode || 'handlebars',
2021-05-03 14:27:32 +00:00
tabSize: 2,
theme: darkMode ? 'monokai' : 'default',
2021-05-03 14:27:32 +00:00
readOnly: false,
highlightSelectionMatches: true,
placeholder,
};
const [realState, setRealState] = useState(currentState);
const [currentValue, setCurrentValue] = useState(initialValue);
const [isFocused, setFocused] = useState(false);
const [heightRef, currentHeight] = useHeight();
const slideInStyles = useSpring({
config: { ...config.stiff },
from: { opacity: 0, height: 0 },
to: {
opacity: isFocused ? 1 : 0,
height: isFocused ? currentHeight : 0,
},
});
useEffect(() => {
setRealState(currentState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentState.components]);
let suggestions = useMemo(() => {
return getSuggestionKeys(realState);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [realState.components, realState.queries]);
2021-05-03 16:04:54 +00:00
function valueChanged(editor, onChange, suggestions, ignoreBraces) {
handleChange(editor, onChange, suggestions, ignoreBraces);
setCurrentValue(editor.getValue());
}
const getPreviewContent = (content, type) => {
switch (type) {
case 'object':
return JSON.stringify(content);
case 'boolean':
return content.toString();
default:
return content;
}
};
const getPreview = () => {
const [preview, error] = resolveReferences(currentValue, realState, null, {}, true);
const themeCls = darkMode ? 'bg-dark py-1' : 'bg-light py-1';
if (error) {
return (
<animated.div className={isOpen ? themeCls : null} style={{ ...slideInStyles, overflow: 'hidden' }}>
<div ref={heightRef} className="dynamic-variable-preview bg-red-lt px-1 py-1">
<div>
<div className="heading my-1">
<span>Error</span>
</div>
{error.toString()}
</div>
</div>
</animated.div>
);
}
const previewType = typeof preview;
const content = getPreviewContent(preview, previewType);
return (
<animated.div className={isOpen ? themeCls : null} style={{ ...slideInStyles, overflow: 'hidden' }}>
<div ref={heightRef} className="dynamic-variable-preview bg-green-lt px-1 py-1">
<div>
<div className="heading my-1">
<span>{previewType}</span>
</div>
{content}
</div>
</div>
</animated.div>
);
};
enablePreview = enablePreview ?? true;
const [isOpen, setIsOpen] = React.useState(false);
const handleToggle = () => {
if (!isOpen) {
setIsOpen(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]);
}
setIsOpen(false);
resolve();
}
}).then(() => {
setIsOpen(true);
forceUpdate();
});
};
const [, forceUpdate] = React.useReducer((x) => x + 1, 0);
const defaultClassName = className === 'query-hinter' || undefined ? '' : 'code-hinter';
const ElementToRender = AllElements[TypeMapping[type]];
const [forceCodeBox, setForceCodeBox] = useState(fxActive);
const codeShow = (type ?? 'code') === 'code' || forceCodeBox;
return (
<>
<div className="row" style={{ width: width, display: codeShow ? 'flex' : 'none' }}>
<div className={`col-${(type ?? 'code') === 'code' ? 12 : 10}`} style={{ marginBottom: '16px' }}>
<div className="code-hinter-wrapper" style={{ width: '100%', backgroundColor: darkMode && '#272822' }}>
<div
className={`${defaultClassName} ${className || 'codehinter-default-input'}`}
key={suggestions.length}
style={{
height: height || 'auto',
minHeight,
maxHeight: '320px',
overflow: 'auto',
padding: '0.18rem 0.75rem',
fontSize: ' .875rem',
}}
>
{usePortalEditor && <CodeHinter.PopupIcon callback={handleToggle} />}
<CodeHinter.Portal
isOpen={isOpen}
callback={setIsOpen}
componentName={componentName}
key={suggestions.length}
customComponent={getPreview}
forceUpdate={forceUpdate}
optionalProps={{ styles: { height: 300 }, cls: className }}
darkMode={darkMode}
selectors={{ className: 'preview-block-portal' }}
>
<CodeMirror
value={typeof initialValue === 'string' ? initialValue : ''}
realState={realState}
scrollbarStyle={null}
height={height || 'auto'}
onFocus={() => setFocused(true)}
onBlur={(editor) => {
const value = editor.getValue();
onChange(value);
setFocused(false);
}}
onChange={(editor) => valueChanged(editor, onChange, suggestions, ignoreBraces)}
onBeforeChange={(editor, change) => onBeforeChange(editor, change, ignoreBraces)}
options={options}
viewportMargin={Infinity}
/>
</CodeHinter.Portal>
</div>
{enablePreview && !isOpen && getPreview()}
</div>
</div>
<div className={`col-2 ${(type ?? 'code') === 'code' ? 'd-none' : ''} pt-2`}>
<FxButton
active={true}
onPress={() => {
setForceCodeBox(false);
onFxPress(false);
}}
/>
</div>
</div>
{!codeShow && (
<div style={{ display: !codeShow ? 'block' : 'none' }}>
<ElementToRender
value={resolveReferences(initialValue, realState)}
onChange={onChange}
paramName={paramName}
paramLabel={paramLabel}
forceCodeBox={() => {
setForceCodeBox(true);
onFxPress(true);
}}
meta={fieldMeta}
/>
</div>
)}
</>
2021-05-03 14:27:32 +00:00
);
}
const PopupIcon = ({ callback }) => {
return (
<div className="d-flex justify-content-end" style={{ position: 'relative' }}>
<OverlayTrigger
trigger={['hover', 'focus']}
placement="top"
delay={{ show: 800, hide: 100 }}
overlay={<Tooltip id="button-tooltip">{'Pop out code editor into a new window'}</Tooltip>}
>
<img
className="svg-icon m-2 popup-btn"
src="/assets/images/icons/portal-open.svg"
width="12"
height="12"
onClick={(e) => {
e.stopPropagation();
callback();
}}
/>
</OverlayTrigger>
</div>
);
};
const Portal = ({ children, ...restProps }) => {
const renderPortal = usePortal({ children, ...restProps });
return <React.Fragment>{renderPortal}</React.Fragment>;
};
CodeHinter.PopupIcon = PopupIcon;
CodeHinter.Portal = Portal;