ToolJet/frontend/src/AppBuilder/QueryManager/Components/Preview.jsx
Nakul Nagargade 433e1bd4c4
Enhance TypeScript support in frontend configuration (#15576)
* test: verify pre-commit hook

* fix: clean up code formatting and improve readability across multiple components

* chore: update subproject commit reference in frontend/ee

* chore: update eslint to version 9.26.0 and remove unused dependencies from package.json

fix: update submodule reference in server/ee

* chore: refactor ESLint configuration and add quiet linting script; update components to disable specific ESLint rules

* chore: add GitHub Copilot review instructions for App Builder team

Covers backward compatibility rules, styling conventions, state management,
resolution system, widget definitions, and common review flags.

* chore: add review instructions for App Builder, Data Migrations, Server Widget Config, Widget Components, and Widget Config

* Enhance TypeScript support in frontend configuration

- Added TypeScript parser and linting rules to ESLint configuration.
- Updated Babel configuration to include TypeScript preset.
- Modified package.json and package-lock.json to include TypeScript and related dependencies.
- Introduced tsconfig.json for TypeScript compiler options.
- Updated Webpack configuration to support .ts and .tsx file extensions.
- Adjusted linting and formatting scripts to include TypeScript files.

* chore: update TypeScript ESLint packages and subproject commits

---------

Co-authored-by: kavinvenkatachalam <kavin.saratha@gmail.com>
Co-authored-by: Johnson Cherian <johnsonc.dev@gmail.com>
2026-03-19 12:41:32 +05:30

251 lines
9.1 KiB
JavaScript

import React, { useEffect, useLayoutEffect, useRef, useState, useMemo } from 'react';
import { JSONTree } from 'react-json-tree';
import { Tab, ListGroup, Row, Col } from 'react-bootstrap';
import { getTheme, tabs } from '../constants';
import ArrowDownTriangle from '@/_ui/Icon/solidIcons/ArrowDownTriangle';
import { useEventListener } from '@/_hooks/use-event-listener';
import { reservedKeywordReplacer } from '@/_lib/reserved-keyword-replacer';
import useStore from '@/AppBuilder/_stores/store';
import { generateCypressDataCy } from '@/modules/common/helpers/cypressHelpers';
const Preview = ({ darkMode, calculatePreviewHeight }) => {
const [key, setKey] = useState('raw');
const [isJson, setIsJson] = useState(false);
const [isDragging, setDragging] = useState(false);
const [isTopOfPreviewPanel, setIsTopOfPreviewPanel] = useState(false);
const storedHeight = useStore((state) => state.queryPanel.previewPanelHeight);
// initialize height with stored height if present in state
const heightSetOnce = useRef(!!storedHeight);
const previewPanelExpanded = useStore((state) => state.queryPanel.previewPanelExpanded);
const setPreviewPanelExpanded = useStore((state) => state.queryPanel.setPreviewPanelExpanded);
const [height, setHeight] = useState(storedHeight);
const [theme, setTheme] = useState(() => getTheme(darkMode));
const queryPreviewData = useStore((state) => state.queryPanel.queryPreviewData);
const previewLoading = useStore((state) => state.queryPanel.previewLoading);
const previewPanelRef = useRef();
const queryPanelHeight = useStore((state) => state.queryPanel.queryPanelHeight);
useEffect(() => {
calculatePreviewHeight(height, previewPanelExpanded);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setTheme(() => getTheme(darkMode));
}, [darkMode]);
useLayoutEffect(() => {
if (queryPreviewData || previewLoading) {
previewPanelRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest',
});
}
}, [queryPreviewData, previewLoading]);
useEffect(() => {
if (queryPreviewData !== null && typeof queryPreviewData === 'object') {
setKey('json');
} else {
setKey('raw');
}
setIsJson(queryPreviewData !== null && typeof queryPreviewData === 'object');
}, [queryPreviewData]);
const renderRawData = () => {
if (!queryPreviewData) {
return queryPreviewData === null ? '' : `${queryPreviewData}`;
} else {
return isJson
? JSON.stringify(queryPreviewData, reservedKeywordReplacer).toString()
: queryPreviewData.toString();
}
};
useEffect(() => {
// query panel collapse scenario
if (queryPanelHeight === 95 || queryPanelHeight === 0) {
return;
}
if (queryPanelHeight - 85 < 40) {
setHeight(40);
return;
}
if (queryPanelHeight - 85 < height) {
setHeight((queryPanelHeight - 85) * 0.7);
} else if (!heightSetOnce.current) {
setHeight((queryPanelHeight - 85) * 0.7);
heightSetOnce.current = true;
}
}, [queryPanelHeight]);
const onMouseMove = (e) => {
if (previewPanelRef.current) {
const componentTop = Math.round(previewPanelRef.current.getBoundingClientRect().top);
const clientY = e.clientY;
if ((clientY >= componentTop - 12) & (clientY <= componentTop + 1)) {
setIsTopOfPreviewPanel(true);
} else if (isTopOfPreviewPanel) {
setIsTopOfPreviewPanel(false);
}
if (isDragging) {
const parentHeight = queryPanelHeight;
const shift = componentTop - clientY;
const currentHeight = previewPanelRef.current.offsetHeight;
const newHeight = currentHeight + shift;
if (newHeight < 50) {
setPreviewPanelExpanded(false);
setHeight((queryPanelHeight - 85) * 0.7);
return;
}
if (newHeight > parentHeight - 95) {
return;
}
setHeight(newHeight);
}
}
};
const onMouseUp = () => {
setDragging(false);
calculatePreviewHeight(height, previewPanelExpanded);
};
const onMouseDown = () => {
isTopOfPreviewPanel && setDragging(true);
};
useEventListener('mousemove', onMouseMove);
useEventListener('mouseup', onMouseUp);
const queryPreviewDataWithCircularDependenciesRemoved = useMemo(() => {
const stringifiedValue = JSON.stringify(queryPreviewData, reservedKeywordReplacer);
return stringifiedValue ? JSON.parse(stringifiedValue) : undefined;
}, [queryPreviewData]);
return (
<div
className={`
preview-header preview-section d-flex flex-column align-items-baseline font-weight-500 ${
previewPanelExpanded ? 'expanded' : ''
}`}
ref={previewPanelRef}
onMouseDown={onMouseDown}
style={{
cursor: previewPanelExpanded && (isDragging || isTopOfPreviewPanel) ? 'row-resize' : 'default',
height: `${height}px`,
...(!previewPanelExpanded && { height: '29px' }),
...(isDragging && {
transition: 'none',
}),
}}
>
<div className="preview-toggle">
<div
onClick={() => {
setPreviewPanelExpanded(!previewPanelExpanded);
calculatePreviewHeight(height, !previewPanelExpanded);
}}
className="left"
data-cy="preview-toggle-button"
>
<ArrowDownTriangle
width={15}
style={{
transform: !previewPanelExpanded ? 'rotate(180deg)' : '',
transition: 'transform 0.2s ease-in-out',
marginRight: '4px',
}}
/>
<span data-cy="preview-section-label">Preview</span>
</div>
{previewPanelExpanded && (
<div className="right" data-cy="preview-tabs-container">
<Tab.Container activeKey={key} onSelect={(k) => setKey(k)} defaultActiveKey="raw">
<Row className="m-0">
<Col className="keys text-center d-flex align-items-center">
<ListGroup
className={`query-preview-list-group rounded ${darkMode ? 'dark' : ''}`}
variant="flush"
style={{ backgroundColor: '#ECEEF0', padding: '2px' }}
data-cy="preview-tabs-list"
>
{tabs.map((tab) => (
<ListGroup.Item
key={tab}
eventKey={tab.toLowerCase()}
disabled={!queryPreviewData || (tab == 'JSON' && !isJson)}
style={{ minWidth: '74px', textAlign: 'center' }}
className="rounded"
data-cy={`preview-tab-${generateCypressDataCy(tab)}-item`}
>
<span
data-cy={`preview-tab-${generateCypressDataCy(tab)}`}
style={{ width: '100%' }}
className="rounded"
>
{tab}
</span>
</ListGroup.Item>
))}
</ListGroup>
</Col>
</Row>
</Tab.Container>
</div>
)}
</div>
<div className="preview-content" data-cy="preview-content">
<Tab.Container activeKey={key} onSelect={(k) => setKey(k)} defaultActiveKey="raw">
<div className="position-relative h-100">
{previewLoading && (
<center
style={{ display: 'grid', placeItems: 'center' }}
className="position-absolute w-100 h-100"
data-cy="preview-loading-container"
>
<div className="spinner-border text-azure" role="status" data-cy="preview-loading-spinner"></div>
</center>
)}
<Tab.Content
style={{
overflowWrap: 'anywhere',
padding: 0,
border: '1px solid var(--slate5)',
height: '100%',
}}
data-cy="preview-tab-content"
>
<Tab.Pane eventKey="json" transition={false} data-cy="preview-json-pane">
<div className="w-100 preview-data-container" data-cy="preview-json-data-container">
<JSONTree
theme={theme}
data={queryPreviewDataWithCircularDependenciesRemoved}
invertTheme={!darkMode}
collectionLimit={100}
hideRoot={true}
/>
</div>
</Tab.Pane>
<Tab.Pane eventKey="raw" transition={false} data-cy="preview-raw-pane">
<div
style={{ padding: '1rem' }}
className={`raw-container preview-data-container`}
data-cy="preview-raw-data-container"
>
{renderRawData()}
</div>
</Tab.Pane>
</Tab.Content>
</div>
</Tab.Container>
</div>
</div>
);
};
export default Preview;