ToolJet/frontend/src/Editor/CodeEditor/PreviewBox.jsx
Kiran Ashok 8675b3186a
Bugfixes: Table crash fixes for LTS / Zindex issue for datepicker (#9984)
* fix : Table breaking for nested array of objects

* fix : table crash on selecting time only datepicker sing add new row

* remove logs

* update zindex

* Fix: Entity reference mapping and performance issues on codehinter (#10004)

* Fix: Entity reference mapping and performance issues on codehinter (#430)

* trial

* fixes:
- entity mapping on version switch
- entity mapping on page switch
- entity mapping on viewer on page switch

* fixes: performance of codehinter for rendering nested json data

* trial

* fixes:
- entity mapping on version switch
- entity mapping on page switch
- entity mapping on viewer on page switch

* Revert "fixes:"

This reverts commit 6ca9921731468aca4132c0aff3aeba02845bcd8a.

* Revert "trial"

This reverts commit 5bfa40c36aeb84abe98c0f2ab2418148772a048d.

* fixes: editor crash on resolving workspace variables

* fixes: populate JS hints

* chore : version bump

* version bump

---------

Co-authored-by: Arpit <arpitnath42@gmail.com>
2024-06-06 17:19:46 +05:30

402 lines
12 KiB
JavaScript

import React, { useEffect, useState } from 'react';
import { computeCoercion, getCurrentNodeType, hasDeepChildren, resolveReferences } from './utils';
import CodeHinter from '.';
import { copyToClipboard } from '@/_helpers/appUtils';
import { Alert } from '@/_ui/Alert/Alert';
import _, { isEmpty } from 'lodash';
import { handleCircularStructureToJSON, hasCircularDependency } from '@/_helpers/utils';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import Card from 'react-bootstrap/Card';
// eslint-disable-next-line import/no-unresolved
import { JsonViewer } from '@textea/json-viewer';
export const PreviewBox = ({
currentValue,
validationSchema,
setErrorStateActive,
setErrorMessage,
customVariables,
isWorkspaceVariable,
}) => {
const [resolvedValue, setResolvedValue] = useState('');
const [error, setError] = useState(null);
const [coersionData, setCoersionData] = useState(null);
const getPreviewContent = (content, type) => {
if (content === undefined || content === null) return currentValue;
try {
switch (type) {
case 'Object':
case 'Array':
return JSON.stringify(content);
case 'Boolean':
return content.toString();
default:
return content;
}
} catch (e) {
return undefined;
}
};
let previewType = getCurrentNodeType(resolvedValue);
let previewContent = resolvedValue;
if (hasCircularDependency(resolvedValue)) {
previewContent = JSON.stringify(resolvedValue, handleCircularStructureToJSON());
previewType = typeof previewContent;
}
const ifCoersionErrorHasCircularDependency = (value) => {
if (hasCircularDependency(value)) {
return JSON.stringify(value, handleCircularStructureToJSON());
}
return value;
};
const content = getPreviewContent(previewContent, previewType);
useEffect(() => {
if (error) {
setErrorStateActive(true);
setErrorMessage(error.message);
} else {
setErrorStateActive(false);
setErrorMessage(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
useEffect(() => {
const [valid, _error, newValue, resolvedValue] = resolveReferences(currentValue, validationSchema, customVariables);
if (isWorkspaceVariable || !validationSchema || isEmpty(validationSchema)) {
return setResolvedValue(newValue);
}
if (valid) {
const [coercionPreview, typeAfterCoercion, typeBeforeCoercion] = computeCoercion(resolvedValue, newValue);
setResolvedValue(resolvedValue);
setCoersionData({
coercionPreview,
typeAfterCoercion,
typeBeforeCoercion,
});
setError(null);
} else if (!valid && !newValue && !resolvedValue) {
const err = !error ? `Invalid value for ${validationSchema?.schema?.type}` : `${_error}`;
setError({ message: err, value: resolvedValue, type: 'Invalid' });
} else {
const jsErrorType = _error?.includes('ReferenceError')
? 'ReferenceError'
: _error?.includes('TypeError')
? 'TypeError'
: _error?.includes('SyntaxError')
? 'SyntaxError'
: 'Invalid';
const errValue = ifCoersionErrorHasCircularDependency(resolvedValue);
setError({
message: _error,
value: jsErrorType === 'Invalid' ? JSON.stringify(errValue) : resolvedValue,
type: jsErrorType,
});
setCoersionData(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentValue]);
return (
<>
<PreviewBox.RenderResolvedValue
error={error}
currentValue={currentValue}
previewType={previewType}
resolvedValue={content}
coersionData={coersionData}
withValidation={!isEmpty(validationSchema)}
isWorkspaceVariable={isWorkspaceVariable}
/>
<CodeHinter.PopupIcon
callback={() => copyToClipboard(error ? error?.value : content)}
icon={'copy'}
tip={'Copy to clipboard'}
/>
</>
);
};
const RenderResolvedValue = ({
error,
previewType,
resolvedValue,
coersionData,
withValidation,
isWorkspaceVariable,
}) => {
const computeCoersionPreview = (resolvedValue, coersionData) => {
if (coersionData?.typeBeforeCoercion === coersionData?.typeAfterCoercion) return resolvedValue;
if (coersionData?.typeBeforeCoercion === 'array') {
return '[...]' + coersionData?.coercionPreview;
}
if (coersionData?.typeBeforeCoercion === 'object') {
return '{...}' + coersionData?.coercionPreview;
}
return resolvedValue + coersionData?.coercionPreview;
};
const previewValueType = isWorkspaceVariable
? previewType
: withValidation || (coersionData && coersionData?.typeBeforeCoercion)
? `${coersionData?.typeBeforeCoercion} ${
coersionData?.coercionPreview ? `${coersionData?.typeAfterCoercion}` : ''
}`
: previewType;
const previewContent = !withValidation ? resolvedValue : computeCoersionPreview(resolvedValue, coersionData);
const cls = error ? 'codehinter-error-banner' : 'codehinter-success-banner';
return (
<div className={`d-flex flex-column align-content-between flex-wrap`}>
<div className="p-2">
<span className={`badge text-capitalize font-500 ${cls}`}> {error ? error.type : previewValueType}</span>
</div>
<PreviewBox.CodeBlock code={error ? error.value : previewContent} />
</div>
);
};
const PreviewContainer = ({
children,
isFocused,
enablePreview,
setCursorInsidePreview,
isPortalOpen,
...restProps
}) => {
const { validationSchema, isWorkspaceVariable, errorStateActive, previewPlacement } = restProps;
const [errorMessage, setErrorMessage] = useState('');
const typeofError = getCurrentNodeType(errorMessage);
const errorMsg = typeofError === 'Array' ? errorMessage[0] : errorMessage;
const darkMode = localStorage.getItem('darkMode') === 'true';
const popover = (
<Popover
bsPrefix="codehinter-preview-popover"
id="popover-basic"
className={`${darkMode && 'dark-theme'}`}
style={{
width: '250px',
maxWidth: '350px',
marginRight: 2,
zIndex: 1400,
}}
onMouseEnter={() => setCursorInsidePreview(true)}
onMouseLeave={() => setCursorInsidePreview(false)}
>
<Popover.Body
style={{
border: !isEmpty(validationSchema) && '1px solid var(--slate6)',
padding: isEmpty(validationSchema) && '0px',
boxShadow: ' 0px 4px 8px 0px #3032331A, 0px 0px 1px 0px #3032330D',
}}
>
<div>
{errorStateActive && (
<div className="mb-2">
<Alert
svg="tj-info-error"
cls={`codehinter preview-alert-banner p-2 mb-0 mt-2 bg-red-lt`}
iconCls="align-items-start"
data-cy={``}
imgHeight={18}
imgWidth={18}
>
<div className="d-flex align-items-center">
<div className="">{errorMsg !== 'null' ? errorMsg : 'Invalid'}</div>
</div>
</Alert>
</div>
)}
{!isEmpty(validationSchema) && (
<>
<div className="mb-1">
<span
style={{
fontSize: '11px',
fontWeight: '500',
lineHeight: '16px',
letterSpacing: '0em',
color: '#6A727C',
}}
>
Expected
</span>
</div>
<Card className={darkMode && 'bg-slate2'}>
<Card.Body
className="p-1"
style={{
minHeight: '60px',
maxHeight: '100px',
}}
>
<div className="d-flex flex-column align-content-between flex-wrap p-0">
<div className="p-2">
<span
className={`badge bg-light-gray font-500 mute-text text-capitalize`}
style={{ fontSize: '12px', background: 'var(--interactive-default)' }}
>
{validationSchema?.schema?.type}
</span>
</div>
<PreviewBox.CodeBlock code={validationSchema?.defaultValue} isExpectValue={true} />
</div>
</Card.Body>
</Card>
</>
)}
</div>
<div className={`${!isEmpty(validationSchema) && 'mt-2'}`}>
{!isEmpty(validationSchema) && (
<div className={`mb-1`}>
<span
style={{
fontSize: '11px',
fontWeight: '500',
lineHeight: '16px',
letterSpacing: '0em',
color: '#6A727C',
}}
>
Current
</span>
</div>
)}
<Card
className={darkMode && 'bg-slate2'}
style={{
borderColor: errorStateActive ? 'var(--tomato8)' : 'var(--slate6)',
}}
>
<Card.Body
className="p-1 code-hinter-preview-card-body"
style={{
minHeight: '60px',
maxHeight: '240px',
overflowY: 'auto',
}}
>
<PreviewBox isFocused={isFocused} setErrorMessage={setErrorMessage} {...restProps} />
</Card.Body>
</Card>
</div>
{isWorkspaceVariable && <CodeHinter.DepericatedAlert text={'Deprecating soon'} />}
</Popover.Body>
</Popover>
);
return (
<OverlayTrigger
trigger="click"
show={enablePreview && isFocused && !isPortalOpen}
placement={previewPlacement}
overlay={popover}
>
{children}
</OverlayTrigger>
);
};
const PreviewCodeBlock = ({ code, isExpectValue = false }) => {
let preview = code && code.trim ? code?.trim() : `${code}`;
const shouldTrim = preview.length > 35;
let showJSONTree = false;
if (isExpectValue && shouldTrim) {
preview = preview.substring(0, 35) + '...' + preview.substring(preview.length - 2, preview.length);
}
let prettyPrintedJson = preview;
try {
prettyPrintedJson = JSON.parse(preview);
const typeOfValue = typeof prettyPrintedJson;
if (typeOfValue === 'object' || typeOfValue === 'array') {
showJSONTree = true;
} else {
prettyPrintedJson = preview;
showJSONTree = false;
}
} catch (e) {
prettyPrintedJson = preview;
showJSONTree = false;
}
if (showJSONTree) {
const darkMode = localStorage.getItem('darkMode') === 'true';
const hasDeepChild = hasDeepChildren(prettyPrintedJson);
return (
<div className="preview-json">
<JsonViewer
value={prettyPrintedJson}
displayDataTypes={false}
displaySize={false}
displayObjectSize={false}
enableClipboard={false}
rootName={false}
theme={darkMode ? 'dark' : 'light'}
groupArraysAfterLength={hasDeepChild ? 10 : 100}
/>
</div>
);
}
return (
<div className="p-2 pt-0">
<pre
className="text-secondary"
style={{
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
overflowWrap: 'break-word',
display: 'block',
background: 'transparent',
border: 'none',
lineHeight: '1.5',
maxHeight: 'none',
overflow: 'auto',
width: '100%',
fontSize: '12px',
overflowY: 'auto',
padding: '0',
margin: '0',
}}
>
{prettyPrintedJson?.startsWith('{{') && prettyPrintedJson?.endsWith('{{')
? prettyPrintedJson?.replace(/{{/g, '').replace(/}}/g, '')
: prettyPrintedJson}
</pre>
</div>
);
};
PreviewBox.RenderResolvedValue = RenderResolvedValue;
PreviewBox.Container = PreviewContainer;
PreviewBox.CodeBlock = PreviewCodeBlock;