mirror of
https://github.com/ToolJet/ToolJet
synced 2026-05-24 01:18:23 +00:00
Fix: Query options menu
This commit is contained in:
parent
e6d800459b
commit
f1011dbd15
4 changed files with 231 additions and 154 deletions
|
|
@ -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: <Edit width={16} />,
|
||||
showTooltip: false,
|
||||
},
|
||||
{
|
||||
label: 'Duplicate',
|
||||
value: 'duplicate',
|
||||
icon: <Copy width={16} />,
|
||||
showTooltip: false,
|
||||
},
|
||||
{
|
||||
label: 'Query permission',
|
||||
value: 'permission',
|
||||
icon: (
|
||||
<img
|
||||
alt="permission-icon"
|
||||
src="assets/images/icons/editor/left-sidebar/authorization.svg"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
),
|
||||
trailingIcon: !licenseValid ? <SolidIcon width={16} name="enterprisecrown" className="mx-1" /> : undefined,
|
||||
tooltipText: 'Query permissions are available only in paid plans',
|
||||
showTooltip: !licenseValid,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
value: 'delete',
|
||||
icon: <Trash width={16} fill={'#E54D2E'} />,
|
||||
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 }) => {
|
|||
<div
|
||||
className={`row query-row pe-2 ${darkMode && 'dark-theme'}` + (isQuerySelected ? ' query-row-selected' : '')}
|
||||
key={dataQuery.id}
|
||||
onClick={() => {
|
||||
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 }) => {
|
|||
<DataSourceIcon source={dataQuery} height={16} />
|
||||
</div>
|
||||
<div className="col query-row-query-name">
|
||||
{renamingQuery ? (
|
||||
{isRenaming ? (
|
||||
<input
|
||||
data-cy={`query-edit-input-field`}
|
||||
className={`query-name query-name-input-field border-indigo-09 bg-transparent ${
|
||||
|
|
@ -237,75 +173,23 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
|
|||
)}
|
||||
</div>
|
||||
<div className={`col-auto query-rename-delete-btn ${!shouldFreeze && isQuerySelected ? 'd-flex' : 'd-none'}`}>
|
||||
<OverlayTrigger
|
||||
trigger={'click'}
|
||||
placement={'bottom-start'}
|
||||
rootClose
|
||||
onHide={() => setShowQueryMenu(false)}
|
||||
show={showQueryMenu && isQuerySelected}
|
||||
popperConfig={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 3],
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
overlay={
|
||||
<Popover id="list-menu" className={darkMode && 'dark-theme'}>
|
||||
<Popover.Body bsPrefix="list-item-popover-body">
|
||||
{QUERY_MENU_OPTIONS.map((option) => (
|
||||
<ToolTip
|
||||
key={option?.value}
|
||||
message={option?.tooltipText}
|
||||
placement="right"
|
||||
show={option?.showTooltip}
|
||||
>
|
||||
<div
|
||||
data-cy={`query-menu-${String(option?.value).toLowerCase()}-button`}
|
||||
className="list-item-popover-option"
|
||||
key={option?.value}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQueryMenuActions(option.value);
|
||||
}}
|
||||
>
|
||||
<div className="list-item-popover-menu-option-icon">{option.icon}</div>
|
||||
<div
|
||||
className={classNames('list-item-option-menu-label', {
|
||||
'color-tomato9': option.value === 'delete',
|
||||
'color-disabled': option.value === 'permission' && !licenseValid,
|
||||
})}
|
||||
>
|
||||
{option?.label}
|
||||
</div>
|
||||
{option.trailingIcon && option.trailingIcon}
|
||||
</div>
|
||||
</ToolTip>
|
||||
))}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<ButtonComponent
|
||||
iconOnly
|
||||
leadingIcon="morevertical01"
|
||||
onClick={() => setShowQueryMenu(!showQueryMenu)}
|
||||
size="small"
|
||||
variant="outline"
|
||||
className=""
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
<ButtonComponent
|
||||
iconOnly
|
||||
leadingIcon="morevertical01"
|
||||
onClick={(e) => toggleQueryHandlerMenu(true, `query-handler-menu-${dataQuery?.id}`)}
|
||||
size="small"
|
||||
variant="outline"
|
||||
className=""
|
||||
id={`query-handler-menu-${dataQuery?.id}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Confirm
|
||||
show={showDeleteConfirmation}
|
||||
show={isDeleting}
|
||||
message={'Do you really want to delete this query?'}
|
||||
confirmButtonLoading={isDeletingQueryInProcess}
|
||||
onConfirm={executeDataQueryDeletion}
|
||||
onCancel={() => setShowDeleteConfirmation(false)}
|
||||
onCancel={() => deleteDataQuery(null)}
|
||||
darkMode={darkMode}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
172
frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx
Normal file
172
frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx
Normal file
|
|
@ -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: <Edit width={16} />,
|
||||
showTooltip: false,
|
||||
},
|
||||
{
|
||||
label: 'Duplicate',
|
||||
value: 'duplicate',
|
||||
icon: <Copy width={16} />,
|
||||
showTooltip: false,
|
||||
},
|
||||
{
|
||||
label: 'Query permission',
|
||||
value: 'permission',
|
||||
icon: (
|
||||
<img
|
||||
alt="permission-icon"
|
||||
src="assets/images/icons/editor/left-sidebar/authorization.svg"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
),
|
||||
trailingIcon: <SolidIcon width={16} name="enterprisecrown" className="mx-1" />,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
value: 'delete',
|
||||
icon: <Trash width={16} fill={'#E54D2E'} />,
|
||||
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 (
|
||||
<Overlay
|
||||
placement="bottom-start"
|
||||
target={targetElement}
|
||||
show={showQueryHandlerMenu}
|
||||
rootClose
|
||||
onHide={() => toggleQueryHandlerMenu(false)}
|
||||
popperConfig={{
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
fallbackPlacements: ['top-start'],
|
||||
flipVariations: true,
|
||||
allowedAutoPlacements: ['top', 'bottom'],
|
||||
boundary: 'viewport',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [0, 3],
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
>
|
||||
{(props) => (
|
||||
<Popover {...props} id="query-list-menu" className={darkMode && 'dark-theme'}>
|
||||
<Popover.Body bsPrefix="list-item-popover-body">
|
||||
{QUERY_MENU_OPTIONS.map((option) => {
|
||||
const optionBody = (
|
||||
<div
|
||||
data-cy={`component-inspector-${String(option?.value).toLowerCase()}-button`}
|
||||
className="list-item-popover-option"
|
||||
key={option?.value}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleQueryMenuActions(option.value);
|
||||
}}
|
||||
>
|
||||
<div className="list-item-popover-menu-option-icon">{option.icon}</div>
|
||||
<div
|
||||
className={classNames('list-item-option-menu-label', {
|
||||
'color-tomato9': option.value === 'delete',
|
||||
'color-disabled': option.value === 'permission' && !licenseValid,
|
||||
})}
|
||||
>
|
||||
{option?.label}
|
||||
</div>
|
||||
{option.value === 'permission' && !licenseValid && option.trailingIcon && option.trailingIcon}
|
||||
</div>
|
||||
);
|
||||
|
||||
return option.value === 'permission' ? (
|
||||
<ToolTip
|
||||
key={option.value}
|
||||
message={'Component permissions are available only in paid plans'}
|
||||
placement="left"
|
||||
show={!licenseValid}
|
||||
>
|
||||
{optionBody}
|
||||
</ToolTip>
|
||||
) : (
|
||||
optionBody
|
||||
);
|
||||
})}
|
||||
</Popover.Body>
|
||||
</Popover>
|
||||
)}
|
||||
</Overlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default QueryCardMenu;
|
||||
|
|
@ -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) => (
|
||||
<QueryCard key={query.id} dataQuery={query} darkMode={darkMode} localDs={!!isDataSourceLocal(query)} />
|
||||
))}
|
||||
<QueryCardMenu darkMode={darkMode} />
|
||||
{licenseValid && (
|
||||
<AppPermissionsModal
|
||||
modalType="query"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@ const initialState = {
|
|||
isPreviewQueryLoading: false,
|
||||
queryPanelSearchTem: '',
|
||||
showQueryPermissionModal: false,
|
||||
targetBtnForMenu: null,
|
||||
showQueryHandlerMenu: false,
|
||||
showDeleteConfirmation: false,
|
||||
renamingQueryId: null,
|
||||
deletingQueryId: null,
|
||||
};
|
||||
|
||||
export const createQueryPanelSlice = (set, get) => ({
|
||||
|
|
@ -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;
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue