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: ( - 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={ - - - {QUERY_MENU_OPTIONS.map((option) => ( - -
{ - e.stopPropagation(); - handleQueryMenuActions(option.value); - }} - > -
{option.icon}
-
- {option?.label} -
- {option.trailingIcon && option.trailingIcon} -
-
- ))} -
-
- } - > - 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: ( + 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) => ( + + + {QUERY_MENU_OPTIONS.map((option) => { + const optionBody = ( +
{ + e.stopPropagation(); + handleQueryMenuActions(option.value); + }} + > +
{option.icon}
+
+ {option?.label} +
+ {option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon} +
+ ); + + return option.value === 'permission' ? ( + + {optionBody} + + ) : ( + optionBody + ); + })} +
+
+ )} +
+ ); +}; + +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; + }), }, });