Merge pull request #12842 from ToolJet/fix/query-panel-overflow

Fix: Dropdown options go out of scope when scrolling through Query Manager
This commit is contained in:
Johnson Cherian 2025-05-16 09:48:22 +05:30 committed by GitHub
commit ecf1d824ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 116 additions and 8 deletions

@ -1 +1 @@
Subproject commit 518f3334b12a83785fd37dd53b0245d72848211a
Subproject commit 52e3f8b488ddc7701c44f2cb73c4cef3b5ddd9e1

View file

@ -1,8 +1,20 @@
import React from 'react';
import React, { useState } from 'react';
import Select from '@/_ui/Select';
import { decodeEntities } from '@/_helpers/utils';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
export const ChangeDataSource = ({ dataSources, onChange, value, isVersionReleased }) => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
usePopoverObserver(
document.getElementsByClassName('query-details')[0],
document.querySelector('.change-data-source-select.react-select__control'),
document.querySelector('.change-data-source-select.react-select__menu'),
isMenuOpen,
() => (document.querySelector('.change-data-source-select.react-select__menu').style.display = 'block'),
() => (document.querySelector('.change-data-source-select.react-select__menu').style.display = 'none')
);
return (
<Select
className="w-100"
@ -14,6 +26,13 @@ export const ChangeDataSource = ({ dataSources, onChange, value, isVersionReleas
}}
useMenuPortal={true}
isDisabled={isVersionReleased}
customClassPrefix="change-data-source-select"
onMenuOpen={() => {
setIsMenuOpen(true);
}}
onMenuClose={() => {
setIsMenuOpen(false);
}}
/>
);
};

View file

@ -31,6 +31,9 @@ class Restapi extends React.Component {
codeHinterHeight: 32, // Default height
};
this.codeHinterRef = React.createRef();
this.isMenuOpenRef = React.createRef();
this.prevIsMenuOpenRef = React.createRef(false);
this.intersectionObserver = null;
this.resizeObserver = null;
}
@ -47,6 +50,9 @@ class Restapi extends React.Component {
if (this.codeHinterRef.current && !this.resizeObserver) {
this.setupResizeObserver();
}
if (!this.intersectionObserver) {
this.setupIntersectionObserver();
}
}
componentDidMount() {
@ -75,6 +81,7 @@ class Restapi extends React.Component {
}, 1000);
this.setupResizeObserver();
this.setupIntersectionObserver();
} catch (error) {
console.log(error);
}
@ -84,6 +91,9 @@ class Restapi extends React.Component {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
}
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
}
setupResizeObserver() {
@ -132,6 +142,33 @@ class Restapi extends React.Component {
this.resizeObserver.observe(element);
}
setupIntersectionObserver() {
const container = document.getElementsByClassName('query-details')[0];
const trigger = document.querySelector('.restapi-method-select.react-select__control');
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
this.intersectionObserver = new IntersectionObserver(
([entry]) => {
const popover = document.querySelector('.restapi-method-select.react-select__menu');
if (entry.isIntersecting) {
if (this.prevIsMenuOpenRef.current) {
popover.style.display = 'block';
this.prevIsMenuOpenRef.current = false;
}
} else if (this.isMenuOpenRef.current) {
popover.style.display = 'none';
this.prevIsMenuOpenRef.current = true;
}
},
{ root: container, threshold: [0.5] }
);
this.intersectionObserver.observe(trigger);
}
initizalizeRetryNetworkErrorsToggle = () => {
const isRetryNetworkErrorToggleUnused = this.props.options.retry_network_errors === null;
if (isRetryNetworkErrorToggleUnused) {
@ -287,6 +324,13 @@ class Restapi extends React.Component {
height={32}
styles={this.customSelectStyles(this.props.darkMode, 91)}
useCustomStyles={true}
customClassPrefix="restapi-method-select"
onMenuOpen={() => {
this.isMenuOpenRef.current = true;
}}
onMenuClose={() => {
this.isMenuOpenRef.current = false;
}}
/>
</div>
<div

View file

@ -5,14 +5,25 @@ import CodeHinter from '@/AppBuilder/CodeEditor';
import './workflows-query.scss';
import { v4 as uuidv4 } from 'uuid';
import useStore from '@/AppBuilder/_stores/store';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
export function Workflows({ options, optionsChanged, currentState }) {
const [workflowOptions, setWorkflowOptions] = useState([]);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [_selectedWorkflowId, setSelectedWorkflowId] = useState(undefined);
const [params, setParams] = useState([...(options.params ?? [{ key: '', value: '' }])]);
const appId = useStore((state) => state.app.appId);
usePopoverObserver(
document.getElementsByClassName('query-details')[0],
document.querySelector('.workflow-select.react-select__control'),
document.querySelector('.workflow-select.react-select__menu'),
isMenuOpen,
() => (document.querySelector('.workflow-select.react-select__menu').style.display = 'block'),
() => (document.querySelector('.workflow-select.react-select__menu').style.display = 'none')
);
useEffect(() => {
appsService.getWorkflows(appId).then(({ workflows }) => {
setWorkflowOptions(
@ -50,6 +61,13 @@ export function Workflows({ options, optionsChanged, currentState }) {
customWrap={true}
width="300px"
menuPlacement="bottom"
customClassPrefix="workflow-select"
onMenuOpen={() => {
setIsMenuOpen(true);
}}
onMenuClose={() => {
setIsMenuOpen(false);
}}
/>
<label className="my-2">Params</label>
<div className="grid"></div>

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext } from 'react';
import React, { useState, useEffect, useContext, useRef } from 'react';
import { ActionTypes } from '@/Editor/ActionTypes';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
@ -32,6 +32,7 @@ import useStore from '@/AppBuilder/_stores/store';
import { useEventActions, useEvents } from '@/AppBuilder/_stores/slices/eventsSlice';
import ToggleGroup from '@/ToolJetUI/SwitchGroup/ToggleGroup';
import ToggleGroupItem from '@/ToolJetUI/SwitchGroup/ToggleGroupItem';
import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
import SolidIcon from '@/_ui/Icon/SolidIcons';
import { components as selectComponents } from 'react-select';
@ -84,6 +85,8 @@ export const EventManager = ({
const [events, setEvents] = useState([]);
const [focusedEventIndex, setFocusedEventIndex] = useState(null);
const lastFocusedEventIndex = useRef(null);
const shouldSkipOnToggle = useRef(null);
const { t } = useTranslation();
@ -1090,10 +1093,21 @@ export const EventManager = ({
placement={popoverPlacement || 'left'}
rootClose={true}
overlay={eventPopover(event.event, index)}
onHide={() => setFocusedEventIndex(null)}
onToggle={(showing) => {
// If the toggle action should be skipped (e.g., due to a previous state change), reset the flag and exit early.
if (shouldSkipOnToggle.current) {
shouldSkipOnToggle.current = false;
return;
}
// If there is already a focused event, set the skip flag to prevent unnecessary state updates.
if (focusedEventIndex !== null && showing) {
shouldSkipOnToggle.current = true;
}
if (showing) {
setFocusedEventIndex(index);
lastFocusedEventIndex.current = index;
} else {
setFocusedEventIndex(null);
}
@ -1102,6 +1116,7 @@ export const EventManager = ({
>
<div
key={index}
id={`${sourceId}-${index}`}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
@ -1145,6 +1160,17 @@ export const EventManager = ({
);
};
const shouldUsePopoverObserver = events.length !== 0 && eventSourceType === 'data_query';
usePopoverObserver(
shouldUsePopoverObserver ? document.getElementsByClassName('query-details')[0] : null,
document.getElementById(`${sourceId}-${lastFocusedEventIndex.current}`),
document.getElementById('popover-basic'),
focusedEventIndex !== null,
() => (document.getElementById('popover-basic').style.display = 'block'),
() => (document.getElementById('popover-basic').style.display = 'none')
);
if (events.length === 0) {
return (
<>

View file

@ -4,9 +4,9 @@ function usePopoverObserver(containerRef, triggerRef, popoverRef, show, onShow,
const prevShow = useRef(false);
// Check if it is a ref or a DOM element
const container = containerRef?.current ? containerRef.current : containerRef;
const trigger = triggerRef?.current ? triggerRef.current : triggerRef;
const popover = popoverRef?.current ? popoverRef.current : popoverRef;
const container = containerRef?.current !== undefined ? containerRef.current : containerRef;
const trigger = triggerRef?.current !== undefined ? triggerRef.current : triggerRef;
const popover = popoverRef?.current !== undefined ? popoverRef.current : popoverRef;
useEffect(() => {
if (!container || !trigger) return;

View file

@ -24,6 +24,7 @@ export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSele
isDisabled = false,
borderRadius,
openMenuOnFocus = false,
customClassPrefix = '',
} = restProps;
const customStyles = useCustomStyles ? styles : defaultStyles(isDarkMode, width, height, styles, borderRadius);
@ -74,7 +75,7 @@ export const SelectComponent = ({ options = [], value, onChange, closeMenuOnSele
maxMenuHeight={maxMenuHeight}
menuPortalTarget={useMenuPortal ? document.body : menuPortalTarget}
closeMenuOnSelect={closeMenuOnSelect ?? true}
classNamePrefix={`${isDarkMode && 'dark-theme'} ${'react-select'}`}
classNamePrefix={`${customClassPrefix} ${isDarkMode && 'dark-theme'} ${'react-select'}`}
/>
);
};