mirror of
https://github.com/ToolJet/ToolJet
synced 2026-04-21 21:47:17 +00:00
* 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>
251 lines
9.1 KiB
JavaScript
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;
|