diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx
index 318ac86736..2d192ec666 100644
--- a/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx
+++ b/frontend/src/AppBuilder/QueryPanel/QueryCard.jsx
@@ -1,11 +1,10 @@
-import React, { useState, useCallback } from 'react';
+import React, { useState } from 'react';
import { Tooltip } from 'react-tooltip';
import { ToolTip } from '@/_components/ToolTip';
import { updateQuerySuggestions } from '@/_helpers/appUtils';
// import { Confirm } from '../Viewer/Confirm';
import { toast } from 'react-hot-toast';
import { shallow } from 'zustand/shallow';
-import Copy from '@/_ui/Icon/solidIcons/Copy';
import DataSourceIcon from '../QueryManager/Components/DataSourceIcon';
import { isQueryRunnable, decodeEntities } from '@/_helpers/utils';
import { canDeleteDataSource, canReadDataSource, canUpdateDataSource } from '@/_helpers';
@@ -13,17 +12,10 @@ import useStore from '@/AppBuilder/_stores/store';
//TODO: Remove this
import { Confirm } from '@/Editor/Viewer/Confirm';
// TODO: enable delete query confirmation popup
-import { debounce } from 'lodash';
import { Button as ButtonComponent } from '@/components/ui/Button/Button.jsx';
-import Edit from '@/_ui/Icon/bulkIcons/Edit';
-import Trash from '@/_ui/Icon/solidIcons/Trash';
-import { OverlayTrigger, Popover } from 'react-bootstrap';
-import classNames from 'classnames';
import SolidIcon from '@/_ui/Icon/SolidIcons';
export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
- const appId = useStore((state) => state.app.appId);
-
const isQuerySelected = useStore((state) => state.queryPanel.isQuerySelected(dataQuery.id), shallow);
const setSelectedQuery = useStore((state) => state.queryPanel.setSelectedQuery);
const checkExistingQueryName = useStore((state) => state.dataQuery.checkExistingQueryName);
@@ -31,11 +23,16 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
const isDeletingQueryInProcess = useStore((state) => state.dataQuery.isDeletingQueryInProcess);
const renameQuery = useStore((state) => state.dataQuery.renameQuery);
const deleteDataQueries = useStore((state) => state.dataQuery.deleteDataQueries);
- const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery);
const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
- const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
- const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
- const [showQueryMenu, setShowQueryMenu] = useState(false);
+ const shouldFreeze = useStore((state) => state.getShouldFreeze());
+
+ const renamingQueryId = useStore((state) => state.queryPanel.renamingQueryId);
+ const deletingQueryId = useStore((state) => state.queryPanel.deletingQueryId);
+ const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery);
+ const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery);
+ const isRenaming = renamingQueryId === dataQuery.id;
+ const isDeleting = deletingQueryId === dataQuery.id;
+
const hasPermissions =
selectedDataSourceScope === 'global'
? canUpdateDataSource(dataQuery?.data_source_id) ||
@@ -43,104 +40,35 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
canDeleteDataSource()
: true;
+ const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
const isRestricted = dataQuery.permissions && dataQuery.permissions.length !== 0;
- const shouldFreeze = useStore((state) => state.getShouldFreeze());
-
- const QUERY_MENU_OPTIONS = [
- {
- label: 'Rename',
- value: 'rename',
- icon: ,
- showTooltip: false,
- },
- {
- label: 'Duplicate',
- value: 'duplicate',
- icon: ,
- showTooltip: false,
- },
- {
- label: 'Query permission',
- value: 'permission',
- icon: (
-
- ),
- trailingIcon: !licenseValid ? : undefined,
- tooltipText: 'Query permissions are available only in paid plans',
- showTooltip: !licenseValid,
- },
- {
- label: 'Delete',
- value: 'delete',
- icon: ,
- showTooltip: false,
- },
- ];
-
- const handleQueryMenuActions = (value) => {
- if (value === 'rename') {
- setRenamingQuery(true);
- }
- if (value === 'duplicate') {
- debouncedDuplicateQuery(dataQuery?.id, appId);
- }
- if (value === 'permission') {
- if (!licenseValid) return;
- toggleQueryPermissionModal(true);
- }
- if (value === 'delete') {
- deleteDataQuery();
- }
- setShowQueryMenu(false);
- };
-
- const [renamingQuery, setRenamingQuery] = useState(false);
-
- const deleteDataQuery = () => {
- setShowDeleteConfirmation(true);
- };
-
const updateQueryName = (dataQuery, newName) => {
const { name } = dataQuery;
if (name === newName) {
- return setRenamingQuery(false);
+ return setRenamingQuery(null);
}
const isNewQueryNameAlreadyExists = checkExistingQueryName(newName);
if (newName && !isNewQueryNameAlreadyExists) {
renameQuery(dataQuery?.id, newName);
- setRenamingQuery(false);
+ setRenamingQuery(null);
updateQuerySuggestions(name, newName);
} else {
if (isNewQueryNameAlreadyExists) {
toast.error('Query name already exists');
}
- setRenamingQuery(false);
+ setRenamingQuery(null);
}
};
const executeDataQueryDeletion = () => {
- setShowDeleteConfirmation(false);
+ deleteDataQuery(null);
deleteDataQueries(dataQuery?.id);
setPreviewData(null);
};
- // To prevent user clicking from continuous clicks
- const debouncedDuplicateQuery = useCallback(
- debounce((queryId, appId) => {
- duplicateQuery(queryId, appId);
- setPreviewData(null);
- }, 500),
- [duplicateQuery]
- );
-
const getTooltip = () => {
const permission = dataQuery.permissions?.[0];
if (!permission) return null;
@@ -171,10 +99,18 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
{
+ onClick={(e) => {
if (isQuerySelected) return;
- setSelectedQuery(dataQuery?.id);
- setPreviewData(null);
+ const menuBtn = document.getElementById(`query-handler-menu-${dataQuery?.id}`);
+ if (menuBtn.contains(e.target)) {
+ e.stopPropagation();
+ } else {
+ toggleQueryHandlerMenu(false);
+ }
+ setTimeout(() => {
+ setSelectedQuery(dataQuery?.id);
+ setPreviewData(null);
+ }, 0);
}}
role="button"
>
@@ -182,7 +118,7 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
- {renamingQuery ? (
+ {isRenaming ? (
{
)}
- setShowQueryMenu(false)}
- show={showQueryMenu && isQuerySelected}
- popperConfig={{
- modifiers: [
- {
- name: 'offset',
- options: {
- offset: [0, 3],
- },
- },
- ],
- }}
- overlay={
-
- }
- >
- setShowQueryMenu(!showQueryMenu)}
- size="small"
- variant="outline"
- className=""
- />
-
+ toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)}
+ size="small"
+ variant="outline"
+ className=""
+ id={`query-handler-menu-${dataQuery?.id}`}
+ />
setShowDeleteConfirmation(false)}
+ onCancel={() => deleteDataQuery(null)}
darkMode={darkMode}
/>
>
diff --git a/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx
new file mode 100644
index 0000000000..760adb8075
--- /dev/null
+++ b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx
@@ -0,0 +1,172 @@
+import React, { useCallback } from 'react';
+import { Overlay, Popover } from 'react-bootstrap';
+import useStore from '@/AppBuilder/_stores/store';
+import classNames from 'classnames';
+import Edit from '@/_ui/Icon/bulkIcons/Edit';
+import Trash from '@/_ui/Icon/solidIcons/Trash';
+import Copy from '@/_ui/Icon/solidIcons/Copy';
+import SolidIcon from '@/_ui/Icon/SolidIcons';
+import { shallow } from 'zustand/shallow';
+import { ToolTip } from '@/_components/ToolTip';
+import { debounce } from 'lodash';
+import usePopoverObserver from '@/AppBuilder/_hooks/usePopoverObserver';
+
+const QueryCardMenu = ({ darkMode }) => {
+ const appId = useStore((state) => state.app.appId);
+ const selectedQuery = useStore((state) => state.queryPanel.selectedQuery);
+ const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal);
+ const featureAccess = useStore((state) => state?.license?.featureAccess, shallow);
+ const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid;
+ const targetBtnForMenu = useStore((state) => state.queryPanel.targetBtnForMenu);
+ const targetElement = document.getElementById(targetBtnForMenu);
+ const showQueryHandlerMenu = useStore((state) => state.queryPanel.showQueryHandlerMenu);
+ const toggleQueryHandlerMenu = useStore((state) => state.queryPanel.toggleQueryHandlerMenu);
+ const duplicateQuery = useStore((state) => state.dataQuery.duplicateQuery);
+ const setPreviewData = useStore((state) => state.queryPanel.setPreviewData);
+ const setRenamingQuery = useStore((state) => state.queryPanel.setRenamingQuery);
+ const deleteDataQuery = useStore((state) => state.queryPanel.deleteDataQuery);
+
+ const QUERY_MENU_OPTIONS = [
+ {
+ label: 'Rename',
+ value: 'rename',
+ icon: ,
+ showTooltip: false,
+ },
+ {
+ label: 'Duplicate',
+ value: 'duplicate',
+ icon: ,
+ showTooltip: false,
+ },
+ {
+ label: 'Query permission',
+ value: 'permission',
+ icon: (
+
+ ),
+ trailingIcon: ,
+ },
+ {
+ label: 'Delete',
+ value: 'delete',
+ icon: ,
+ showTooltip: false,
+ },
+ ];
+
+ // To prevent user clicking from continuous clicks
+ const debouncedDuplicateQuery = useCallback(
+ debounce((queryId, appId) => {
+ duplicateQuery(queryId, appId);
+ setPreviewData(null);
+ }, 500),
+ [duplicateQuery]
+ );
+
+ const handleQueryMenuActions = (value) => {
+ if (value === 'rename') {
+ setRenamingQuery(selectedQuery?.id);
+ }
+ if (value === 'duplicate') {
+ debouncedDuplicateQuery(selectedQuery?.id, appId);
+ }
+ if (value === 'permission') {
+ if (!licenseValid) return;
+ toggleQueryPermissionModal(true);
+ }
+ if (value === 'delete') {
+ deleteDataQuery(selectedQuery?.id);
+ }
+ toggleQueryHandlerMenu(false);
+ };
+
+ usePopoverObserver(
+ document.getElementsByClassName('query-list')[0],
+ targetElement,
+ document.getElementById('query-list-menu'),
+ showQueryHandlerMenu,
+ () => (document.getElementById('query-list-menu').style.display = 'block'),
+ () => (document.getElementById('query-list-menu').style.display = 'none')
+ );
+
+ return (
+ toggleQueryHandlerMenu(false)}
+ popperConfig={{
+ modifiers: [
+ {
+ name: 'flip',
+ options: {
+ fallbackPlacements: ['top-start'],
+ flipVariations: true,
+ allowedAutoPlacements: ['top', 'bottom'],
+ boundary: 'viewport',
+ },
+ },
+ {
+ name: 'offset',
+ options: {
+ offset: [0, 3],
+ },
+ },
+ ],
+ }}
+ >
+ {(props) => (
+
+ )}
+
+ );
+};
+
+export default QueryCardMenu;
diff --git a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx
index 62f27197ba..12e8fb3171 100644
--- a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx
+++ b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx
@@ -19,6 +19,7 @@ import useStore from '@/AppBuilder/_stores/store';
import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal';
import { shallow } from 'zustand/shallow';
import { appPermissionService } from '@/_services';
+import QueryCardMenu from './QueryCardMenu';
export const QueryDataPane = ({ darkMode }) => {
const { t } = useTranslation();
@@ -180,6 +181,7 @@ export const QueryDataPane = ({ darkMode }) => {
{filteredQueries.map((query) => (
))}
+
{licenseValid && (
({
@@ -1141,5 +1146,19 @@ export const createQueryPanelSlice = (set, get) => ({
state.queryPanel.showQueryPermissionModal = show;
});
},
+ toggleQueryHandlerMenu: (show, id) => {
+ set((state) => {
+ if (show) state.queryPanel.targetBtnForMenu = id;
+ state.queryPanel.showQueryHandlerMenu = show;
+ });
+ },
+ setRenamingQuery: (queryId) =>
+ set((state) => {
+ state.queryPanel.renamingQueryId = queryId;
+ }),
+ deleteDataQuery: (queryId) =>
+ set((state) => {
+ state.queryPanel.deletingQueryId = queryId;
+ }),
},
});