diff --git a/frontend/ee b/frontend/ee index 387bb6e55b..9458c8d66f 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 387bb6e55bc6a7600b7125bb9e22ca2b17dfe65d +Subproject commit 9458c8d66f29f8334765b5757dd096139a8d53d2 diff --git a/frontend/src/AppBuilder/AppCanvas/Container.jsx b/frontend/src/AppBuilder/AppCanvas/Container.jsx index c9fe887257..ac22aff055 100644 --- a/frontend/src/AppBuilder/AppCanvas/Container.jsx +++ b/frontend/src/AppBuilder/AppCanvas/Container.jsx @@ -143,10 +143,11 @@ export const Container = React.memo( if (canvasWidth !== undefined) { if (componentType === 'Listview' && listViewMode == 'grid') return canvasWidth / columns - 2; if (id === 'canvas') return canvasWidth; - return getSubContainerWidthAfterPadding(canvasWidth, componentType, id); + return getSubContainerWidthAfterPadding(canvasWidth, componentType, id, realCanvasRef); } return realCanvasRef?.current?.offsetWidth; } + const gridWidth = getContainerCanvasWidth() / NO_OF_GRIDS; useEffect(() => { diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js index eab22333b2..955880a173 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasConstants.js @@ -39,3 +39,5 @@ export const DROPPABLE_PARENTS = new Set([ export const TAB_CANVAS_PADDING = 7.5; export const MODAL_CANVAS_PADDING = 5; + +export const LISTVIEW_CANVAS_PADDING = 7; diff --git a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js index 228af4225e..109b17207b 100644 --- a/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js +++ b/frontend/src/AppBuilder/AppCanvas/appCanvasUtils.js @@ -15,6 +15,7 @@ import { BOX_PADDING, TAB_CANVAS_PADDING, MODAL_CANVAS_PADDING, + LISTVIEW_CANVAS_PADDING, } from './appCanvasConstants'; export function snapToGrid(canvasWidth, x, y) { @@ -779,7 +780,7 @@ export const getSubContainerIdWithSlots = (parentId) => { return cleanParentId; }; -export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, componentId) => { +export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, componentId, realCanvasRef) => { let padding = 2; //Need to update this 2 to correct value for other subcontainers if (componentType === 'Container' || componentType === 'Form') { padding = 2 * CONTAINER_FORM_CANVAS_PADDING + 2 * SUBCONTAINER_CANVAS_BORDER_WIDTH + 2 * BOX_PADDING; @@ -797,5 +798,8 @@ export const getSubContainerWidthAfterPadding = (canvasWidth, componentType, com padding = 2 * MODAL_CANVAS_PADDING; } } + if (componentType === 'Listview') { + padding = 2 * LISTVIEW_CANVAS_PADDING + 5; // 5 is accounting for scrollbar + } return canvasWidth - padding; }; diff --git a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx index 98af1dc9e4..325c5c8ac5 100644 --- a/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/MultiLineCodeEditor.jsx @@ -58,7 +58,7 @@ const MultiLineCodeEditor = (props) => { const replaceIdsWithName = useStore((state) => state.replaceIdsWithName, shallow); const wrapperRef = useRef(null); const getSuggestions = useStore((state) => state.getSuggestions, shallow); - const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); + const getServerSideGlobalResolveSuggestions = useStore((state) => state.getServerSideGlobalResolveSuggestions, shallow); const isInsideQueryPane = !!document.querySelector('.code-hinter-wrapper')?.closest('.query-details'); const isInsideQueryManager = useMemo( @@ -116,7 +116,7 @@ const MultiLineCodeEditor = (props) => { const hints = getSuggestions(); - const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager); + const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager); const allHints = { ...hints, diff --git a/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx b/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx index 5c422b1eb3..a24c41543f 100644 --- a/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx +++ b/frontend/src/AppBuilder/CodeEditor/PreviewBox.jsx @@ -98,7 +98,7 @@ export const PreviewBox = ({ const [largeDataset, setLargeDataset] = useState(false); const globals = useStore((state) => state.getAllExposedValues(moduleId).constants || {}, shallow); const secrets = useStore((state) => state.getSecrets(), shallow); - const globalServerConstantsRegex = /^\{\{.*globals\.server.*\}\}$/; + const globalServerConstantsRegex = /\{\{.*globals\.server.*\}\}/; const getPreviewContent = (content, type) => { if (content === undefined || content === null) return currentValue; @@ -251,7 +251,10 @@ const RenderResolvedValue = ({ isServerConstant = false, isLargeDataset, }) => { - const isServerSideGlobalEnabled = useStore((state) => !!state?.license?.featureAccess?.serverSideGlobal, shallow); + const isServerSideGlobalResolveEnabled = useStore( + (state) => !!state?.license?.featureAccess?.serverSideGlobalResolve, + shallow + ); const computeCoersionPreview = (resolvedValue, coersionData) => { if (coersionData?.typeBeforeCoercion === coersionData?.typeAfterCoercion) return resolvedValue; @@ -276,7 +279,7 @@ const RenderResolvedValue = ({ : previewType; const previewContent = isServerConstant - ? isServerSideGlobalEnabled + ? isServerSideGlobalResolveEnabled ? 'Server variables would be resolved at runtime' : 'Server variables are only available in paid plans' : isSecretConstant diff --git a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx index 9c85e0bf43..fba7322bb2 100644 --- a/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx +++ b/frontend/src/AppBuilder/CodeEditor/SingleLineCodeEditor.jsx @@ -216,7 +216,7 @@ const EditorInput = ({ const getSuggestions = useStore((state) => state.getSuggestions, shallow); const [codeMirrorView, setCodeMirrorView] = useState(undefined); - const getServerSideGlobalSuggestions = useStore((state) => state.getServerSideGlobalSuggestions, shallow); + const getServerSideGlobalResolveSuggestions = useStore((state) => state.getServerSideGlobalResolveSuggestions, shallow); const { queryPanelKeybindings } = useQueryPanelKeyHooks(onBlurUpdate, currentValue, 'singleline'); @@ -226,7 +226,7 @@ const EditorInput = ({ ); function autoCompleteExtensionConfig(context) { const hintsWithoutParamHints = getSuggestions(); - const serverHints = getServerSideGlobalSuggestions(isInsideQueryManager); + const serverHints = getServerSideGlobalResolveSuggestions(isInsideQueryManager); let word = context.matchBefore(/\w*/); diff --git a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/index.jsx b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/index.jsx index d85917c797..9f8379f85c 100644 --- a/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/index.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/GlobalSettings/index.jsx @@ -10,12 +10,13 @@ import AppModeToggle from './AppModeToggle'; import { ThemeSelect } from '@/modules/Appbuilder/components'; import MaintenanceMode from './MaintenanceMode'; import HideHeaderToggle from './HideHeaderToggle'; +import { ModuleProvider } from '@/AppBuilder/_contexts/ModuleContext'; const GlobalSettings = ({ darkMode }) => { const shouldFreeze = useStore((state) => state.getShouldFreeze()); return ( - <> +
@@ -44,7 +45,7 @@ const GlobalSettings = ({ darkMode }) => {
- +
); }; diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx index d4b8573c7d..68a54c2ca4 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx @@ -12,11 +12,12 @@ import './style.scss'; import { SortableTree } from './Tree/SortableTree'; import { PageGroupMenu } from './AddPageButton'; import { PageHandlerMenu } from './PageHandlerMenu.jsx'; +import AppPermissionsModal from '@/modules/Appbuilder/components/AppPermissionsModal'; import { EditModal } from './EditModal'; import { SettingsModal } from './SettingsModal'; import { DeletePageConfirmationModal } from './DeletePageConfirmationModal'; import SolidIcon from '@/_ui/Icon/SolidIcons'; -import PagePermission from './PagePermission'; +import { appPermissionService } from '@/_services'; export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { const showAddNewPageInput = useStore((state) => state.showAddNewPageInput); @@ -27,6 +28,12 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { const shouldFreeze = useStore((state) => state.getShouldFreeze()); const enableReleasedVersionPopupState = useStore((state) => state.enableReleasedVersionPopupState); const closePageEditPopover = useStore((state) => state.closePageEditPopover); + const editingPageId = useStore((state) => state.editingPage?.id); + const editingPageName = useStore((state) => state.editingPage?.name); + const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); + const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); + const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions); + useEffect(() => { return () => { closePageEditPopover(); @@ -95,7 +102,21 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { >
- {isLicensed ? : <>} + {isLicensed && ( + appPermissionService.getPagePermission(appId, id)} + createPermission={(id, appId, body) => appPermissionService.createPagePermission(appId, id, body)} + updatePermission={(id, appId, body) => appPermissionService.updatePagePermission(appId, id, body)} + deletePermission={(id, appId) => appPermissionService.deletePagePermission(appId, id)} + onSuccess={(data) => updatePageWithPermissions(editingPageId, data)} + /> + )} diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx deleted file mode 100644 index 160a941ebe..0000000000 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx +++ /dev/null @@ -1,509 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { components } from 'react-select'; -import ModalBase from '@/_ui/Modal'; -import Select from '@/_ui/Select'; -import SolidIcon from '@/_ui/Icon/SolidIcons'; -import useStore from '@/AppBuilder/_stores/store'; -import { appPermissionService } from '@/_services'; -import { ConfirmDialog } from '@/_components'; -import toast from 'react-hot-toast'; -import Spinner from '@/_ui/Spinner'; -import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; - -const PERMISSION_TYPES = { - single: 'SINGLE', - group: 'GROUP', - all: 'ALL', -}; - -export default function PagePermission({ darkMode }) { - const { moduleId } = useModuleContext(); - const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); - const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); - const editingPage = useStore((state) => state.editingPage); - const appId = useStore((state) => state.appStore.modules[moduleId].app.appId); - const selectedUserGroups = useStore((state) => state.selectedUserGroups); - const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); - const selectedUsers = useStore((state) => state.selectedUsers); - const setSelectedUsers = useStore((state) => state.setSelectedUsers); - const pagePermission = useStore((state) => state.pagePermission); - const setPagePermission = useStore((state) => state.setPagePermission); - const updatePageWithPermissions = useStore((state) => state.updatePageWithPermissions); - - const [pagePermissionType, setPagePermissionType] = useState('all'); - const [showUserGroupSelect, toggleUserGroupSelect] = useState(false); - const [showUsersSelect, toggleUsersSelect] = useState(false); - const [showConfirmDelete, setShowConfirmDelete] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isPermissionsLoading, setPermissionsLoading] = useState(true); - const [initialSelectedGroups, setInitialSelectedGroups] = useState([]); - const [initialSelectedUsers, setInitialSelectedUsers] = useState([]); - const [initalPagePermissionType, setInitialPagePermissionType] = useState('all'); - - useEffect(() => { - if (!showPagePermissionModal) return; - const fetchPagePermission = () => { - appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => { - if (data) { - if (data[0] && data[0]?.type === PERMISSION_TYPES.group) { - const groups = - data[0]?.groups?.map((user) => ({ - label: user?.permissionGroup?.name, - value: user?.permissionGroup?.id, - count: user?.permissionGroup?.count, - })) ?? []; - setPagePermissionType(data[0]?.type?.toLowerCase()); - setInitialPagePermissionType(data[0]?.type?.toLowerCase()); - setPagePermission(data); - toggleUserGroupSelect(true); - setInitialSelectedGroups(groups); - data?.length && setSelectedUserGroups(groups); - } else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) { - const users = - data[0]?.users?.map(({ user }) => { - const firstName = user.firstName || ''; - const lastName = user.lastName || ''; - return { - value: user.id, - label: `${firstName} ${lastName}`.trim(), - email: user.email, - initials: `${firstName[0] || ''}${lastName[0] || ''}`.toUpperCase(), - }; - }) ?? []; - setPagePermissionType(data[0]?.type?.toLowerCase()); - setInitialPagePermissionType(data[0]?.type?.toLowerCase()); - setPagePermission(data); - toggleUsersSelect(true); - setInitialSelectedUsers(users); - data?.length && setSelectedUsers(users); - } - } - setPermissionsLoading(false); - }); - }; - fetchPagePermission(); - }, [showPagePermissionModal]); - - const isSelectionUnchanged = useMemo(() => { - if (pagePermissionType === 'group') { - if (!selectedUserGroups.length) return true; - const current = selectedUserGroups - .map((g) => g.value) - .sort() - .join(','); - const initial = initialSelectedGroups - .map((g) => g.value) - .sort() - .join(','); - return current === initial; - } else if (pagePermissionType === 'single') { - if (!selectedUsers.length) return true; - const current = selectedUsers - .map((u) => u.value) - .sort() - .join(','); - const initial = initialSelectedUsers - .map((u) => u.value) - .sort() - .join(','); - return current === initial; - } else { - if (!pagePermission?.length) { - return true; - } else { - return initalPagePermissionType == pagePermissionType; - } - } - }, [ - pagePermissionType, - selectedUserGroups, - initialSelectedGroups, - selectedUsers, - initialSelectedUsers, - initalPagePermissionType, - ]); - - const permissionTypeOptions = useMemo( - () => [ - { - label: 'All users with access to the app', - value: 'all', - icon: 'globe', - }, - { - label: 'Users', - value: 'single', - icon: 'user', - }, - { - label: 'User groups', - value: 'group', - icon: 'usergroup', - }, - ], - [] - ); - const handlePermissionTypeChange = (value) => { - switch (value) { - case 'group': { - toggleUserGroupSelect(true); - toggleUsersSelect(false); - setPagePermissionType('group'); - break; - } - case 'single': { - toggleUsersSelect(true); - toggleUserGroupSelect(false); - setPagePermissionType('single'); - break; - } - case 'all': { - toggleUsersSelect(false); - toggleUserGroupSelect(false); - setPagePermissionType('all'); - } - } - }; - - const handlePagePermissionModalClose = () => { - togglePagePermissionModal(false); - toggleUserGroupSelect(false); - toggleUsersSelect(false); - setPagePermissionType('all'); - setPagePermission(null); - setSelectedUsers([]); - setSelectedUserGroups([]); - setInitialSelectedGroups([]); - setInitialSelectedUsers([]); - }; - - const createPagePermission = () => { - const body = { - pageId: editingPage?.id, - type: PERMISSION_TYPES[pagePermissionType], - ...(pagePermissionType === 'group' - ? { groups: selectedUserGroups.map((group) => group?.value) } - : { users: selectedUsers.map((user) => user?.value) }), - }; - setIsLoading(true); - appPermissionService - .createPagePermission(appId, editingPage?.id, body) - .then((data) => { - toast.success('Permission successfully created!', { - className: 'text-nowrap w-auto mw-100', - }); - updatePageWithPermissions(editingPage?.id, data); - }) - .catch(() => { - toast.error('Permission could not be created. Please try again!', { - className: 'text-nowrap w-auto mw-100', - }); - }) - .finally(() => { - setIsLoading(false); - handlePagePermissionModalClose(); - }); - }; - - const updatePagePermission = () => { - const body = { - pageId: editingPage?.id, - type: PERMISSION_TYPES[pagePermissionType], - ...(pagePermissionType === 'group' - ? { groups: selectedUserGroups.map((group) => group?.value) } - : { users: selectedUsers.map((user) => user?.value) }), - }; - setIsLoading(true); - appPermissionService - .updatePagePermission(appId, editingPage?.id, body) - .then((data) => { - toast.success('Permission successfully updated!', { - className: 'text-nowrap w-auto mw-100', - }); - updatePageWithPermissions(editingPage?.id, data); - }) - .catch(() => { - toast.error('Permission could not be updated. Please try again!', { - className: 'text-nowrap w-auto mw-100', - }); - }) - .finally(() => { - setIsLoading(false); - handlePagePermissionModalClose(); - }); - }; - - const deletePagePermission = () => { - setIsLoading(true); - appPermissionService - .deletePagePermission(appId, editingPage?.id) - .then((data) => { - toast.success('Permission successfully deleted!', { - className: 'text-nowrap w-auto mw-100', - }); - updatePageWithPermissions(editingPage?.id, []); - }) - .catch(() => { - toast.error('Permission could not be deleted. Please try again!', { - className: 'text-nowrap w-auto mw-100', - }); - setShowConfirmDelete(false); - togglePagePermissionModal(true); - }) - .finally(() => { - setIsLoading(false); - setShowConfirmDelete(false); - }); - }; - - const renderPermissionTypeOptions = ({ label, icon }) => { - return ( -
-
- -
-
- {label} -
-
- ); - }; - - return ( - <> - - Page permission -
- } - handleConfirm={!pagePermission ? createPagePermission : updatePagePermission} - show={showPagePermissionModal} - isLoading={isLoading} - handleClose={handlePagePermissionModalClose} - confirmBtnProps={{ - title: pagePermission - ? 'Save changes' - : pagePermissionType === 'all' - ? 'Default permission' - : 'Create permission', - disabled: isPermissionsLoading || isSelectionUnchanged, - tooltipMessage: '', - leftIcon: pagePermission && 'save', - className: 'action-btn-page-permission', - }} - darkMode={darkMode} - className="page-permissions-modal" - > -
- {isPermissionsLoading ? ( -
- -
- ) : ( - <> -
-
- -
-
-
-

- Only selected users will be allowed to access this page. Read docs to know more. -

-
-
-
- - -
-
{data.label}
-
{data.count} users
-
-
- - ); - }; - - return ( -
- - -
{data.initials}
-
-
{data.label}
-
{data.email}
-
-
- - ); - }; - - const selectStyles = { - option: (base) => ({ - ...base, - padding: '8px 0px', - }), - }; - return ( -
- - { data-tooltip-dynamic="true" > {decodeEntities(dataQuery.name)} - {' '} + + +
+ {licenseValid && isRestricted && } +
+
{' '} {!isQueryRunnable(dataQuery) && Draft} {localDs && ( <> @@ -143,80 +172,24 @@ export const QueryCard = ({ dataQuery, darkMode = false, localDs }) => {
)} - {!shouldFreeze && isQuerySelected && ( -
-
setRenamingQuery(true)} - > - - - - - -
-
debouncedDuplicateQuery(dataQuery?.id, appId)} - > - - - -
-
- {isDeletingQueryInProcess ? ( -
-
-
- ) : ( - - - - - - - - )} -
- -
- )} +
+ 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..a9e3030b51 --- /dev/null +++ b/frontend/src/AppBuilder/QueryPanel/QueryCardMenu.jsx @@ -0,0 +1,174 @@ +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'; +import { useModuleContext } from '@/AppBuilder/_contexts/ModuleContext'; + +const QueryCardMenu = ({ darkMode }) => { + const { moduleId } = useModuleContext(); + const appId = useStore((state) => state.appStore.modules[moduleId].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 9ac052ae51..97b4daa68f 100644 --- a/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx +++ b/frontend/src/AppBuilder/QueryPanel/QueryDataPane.jsx @@ -16,6 +16,10 @@ import DataSourceSelect from '../QueryManager/Components/DataSourceSelect'; import { OverlayTrigger, Popover } from 'react-bootstrap'; import FolderEmpty from '@/_ui/Icon/solidIcons/FolderEmpty'; 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(); @@ -34,6 +38,12 @@ export const QueryDataPane = ({ darkMode }) => { function isDataSourceLocal(dataQuery) { return dataSources.some((dataSource) => dataSource.id === dataQuery.data_source_id); } + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; + const selectedQuery = useStore((state) => state.queryPanel.selectedQuery); + const showQueryPermissionModal = useStore((state) => state.queryPanel.showQueryPermissionModal); + const toggleQueryPermissionModal = useStore((state) => state.queryPanel.toggleQueryPermissionModal); + const setQueries = useStore((state) => state.dataQuery.setQueries); useEffect(() => { setQueryPanelSearchTerm(searchTermForFilters); @@ -171,6 +181,33 @@ export const QueryDataPane = ({ darkMode }) => { {filteredQueries.map((query) => ( ))} + + {licenseValid && ( + appPermissionService.getQueryPermission(appId, id)} + createPermission={(id, appId, body) => appPermissionService.createQueryPermission(appId, id, body)} + updatePermission={(id, appId, body) => appPermissionService.updateQueryPermission(appId, id, body)} + deletePermission={(id, appId) => appPermissionService.deleteQueryPermission(appId, id)} + onSuccess={(data) => { + const updatedDataQueries = dataQueries.map((query) => { + if (query.id === selectedQuery.id) { + return { + ...query, + permissions: data.length === 0 || data.length === undefined ? [] : [data[0]], + }; + } + return query; + }); + setQueries(updatedDataQueries); + }} + /> + )} ); diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/container.js b/frontend/src/AppBuilder/WidgetManager/widgets/container.js index 04ddf805d9..2bff0d84c5 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/container.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/container.js @@ -47,11 +47,6 @@ export const containerConfig = { defaultValue: true, }, }, - headerHeight: { - type: 'numberInput', - displayName: 'Header height', - validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, - }, }, defaultChildren: [ { @@ -61,10 +56,10 @@ export const containerConfig = { top: 20, left: 1, height: 40, + width: 20, }, displayName: 'ContainerText', properties: ['text'], - slotName: 'header', accessorKey: 'text', styles: ['fontWeight', 'textSize', 'textColor'], defaultValue: { diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js index 6c599bc2fc..c3ebdcfa19 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/listview.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/listview.js @@ -13,7 +13,7 @@ export const listviewConfig = { top: 15, left: 3, height: 100, - width: 7, + width: 4, }, properties: ['source'], accessorKey: 'imageURL', @@ -24,6 +24,7 @@ export const listviewConfig = { top: 50, left: 11, height: 30, + width: 4, }, properties: ['text'], accessorKey: 'text', @@ -49,12 +50,14 @@ export const listviewConfig = { data: { type: 'code', displayName: 'List data', - schema: { - type: 'union', - schemas: [ - { type: 'array', element: { type: 'object' } }, - { type: 'array', element: { type: 'string' } }, - ], + validation: { + schema: { + type: 'union', + schemas: [ + { type: 'array', element: { type: 'object' } }, + { type: 'array', element: { type: 'string' } }, + ], + }, defaultValue: "[{text: 'Sample text 1'}]", }, }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js index a3c89acf93..2318a986f8 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/modalV2.js @@ -92,18 +92,6 @@ export const modalV2Config = { accordian: 'Data', validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 400 }, }, - headerHeight: { - type: 'numberInput', - displayName: 'Header height', - accordian: 'Data', - validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, - }, - footerHeight: { - type: 'numberInput', - displayName: 'Footer height', - accordian: 'Data', - validation: { schema: { type: 'union', schemas: [{ type: 'string' }, { type: 'number' }] }, defaultValue: 80 }, - }, hideOnEsc: { type: 'toggle', displayName: 'Close on escape key', section: 'additionalActions' }, closeOnClickingOutside: { type: 'toggle', displayName: 'Close on clicking outside', section: 'additionalActions' }, hideCloseButton: { type: 'toggle', displayName: 'Hide close button', section: 'additionalActions' }, diff --git a/frontend/src/AppBuilder/WidgetManager/widgets/table.js b/frontend/src/AppBuilder/WidgetManager/widgets/table.js index 9f0c4fd723..facb62f7d4 100644 --- a/frontend/src/AppBuilder/WidgetManager/widgets/table.js +++ b/frontend/src/AppBuilder/WidgetManager/widgets/table.js @@ -275,7 +275,7 @@ export const tableConfig = { showOnMobile: { type: 'toggle', displayName: 'Show on mobile' }, }, defaultSize: { - width: 35, + width: 25, height: 456, }, events: { diff --git a/frontend/src/AppBuilder/Widgets/Container/Container.jsx b/frontend/src/AppBuilder/Widgets/Container/Container.jsx index a706d29069..07492d4415 100644 --- a/frontend/src/AppBuilder/Widgets/Container/Container.jsx +++ b/frontend/src/AppBuilder/Widgets/Container/Container.jsx @@ -9,6 +9,8 @@ import { } from '@/AppBuilder/AppCanvas/appCanvasConstants'; import useStore from '@/AppBuilder/_stores/store'; import './container.scss'; +import { useActiveSlot } from '@/AppBuilder/_hooks/useActiveSlot'; +import { HorizontalSlot } from '@/AppBuilder/Widgets/Form/Components/HorizontalSlot'; export const Container = ({ id, @@ -33,8 +35,13 @@ export const Container = ({ shallow ); + const isEditing = useStore((state) => state.currentMode === 'edit'); + const setComponentProperty = useStore((state) => state.setComponentProperty, shallow); + + const activeSlot = useActiveSlot(isEditing ? id : null); // Track the active slot for this widget const { borderRadius, borderColor, boxShadow } = styles; const { headerHeight = 80 } = properties; + const headerMaxHeight = parseInt(height, 10) - 100 - 10; const contentBgColor = useMemo(() => { return { backgroundColor: @@ -65,9 +72,9 @@ export const Container = ({ const containerHeaderStyles = { flexShrink: 0, padding: `${CONTAINER_FORM_CANVAS_PADDING}px ${CONTAINER_FORM_CANVAS_PADDING}px 3px ${CONTAINER_FORM_CANVAS_PADDING}px`, + maxHeight: `${headerMaxHeight}px`, ...headerBgColor, }; - const containerContentStyles = { overflow: 'hidden auto', display: 'flex', @@ -75,6 +82,11 @@ export const Container = ({ padding: `${CONTAINER_FORM_CANVAS_PADDING}px`, }; + const updateHeaderSizeInStore = ({ newHeight }) => { + const _height = parseInt(newHeight, 10); + setComponentProperty(id, `headerHeight`, _height, 'properties', 'value', false); + }; + return (
{properties.showHeader && ( -
- -
+ )}
{ const parsedHeight = parseInt(height, 10); - - const { getRootProps, getHandleProps, getResizeState } = useResizable({ + const { getRootProps, getHandleProps, getResizeState } = useSubContainerResizable({ initialHeight: parsedHeight, initialWidth: '100%', // Now respects parent's width minHeight: 10, @@ -34,12 +34,11 @@ export const HorizontalSlot = React.memo( }); const { height: resizedHeight, isDragging } = getResizeState(); - useEffect(() => { if (isDragging) { - showGridLinesOnSlot(id); + showGridLines(); } else { - hideGridLinesOnSlot(id); + hideGridLines(); } }, [isDragging, id]); @@ -50,7 +49,10 @@ export const HorizontalSlot = React.memo( }; return ( -
+
{isEditing &&
}
diff --git a/frontend/src/AppBuilder/Widgets/Form/Form.jsx b/frontend/src/AppBuilder/Widgets/Form/Form.jsx index 4dd8cc500b..cab5e0ea52 100644 --- a/frontend/src/AppBuilder/Widgets/Form/Form.jsx +++ b/frontend/src/AppBuilder/Widgets/Form/Form.jsx @@ -341,6 +341,7 @@ export const Form = function Form(props) { isDisabled={isDisabled} isActive={activeSlot === `${id}-header`} onResize={updateHeaderSizeInStore} + componentType="Form" /> )} @@ -417,6 +418,7 @@ export const Form = function Form(props) { isDisabled={isDisabled} onResize={updateFooterSizeInStore} isActive={activeSlot === `${id}-footer`} + componentType="Form" /> )} diff --git a/frontend/src/AppBuilder/Widgets/Kanban/Components/Item.jsx b/frontend/src/AppBuilder/Widgets/Kanban/Components/Item.jsx index 7b4b2a56cf..d147c1b135 100644 --- a/frontend/src/AppBuilder/Widgets/Kanban/Components/Item.jsx +++ b/frontend/src/AppBuilder/Widgets/Kanban/Components/Item.jsx @@ -81,7 +81,6 @@ export const Item = React.memo( >
e.stopPropagation()}>
diff --git a/frontend/src/AppBuilder/Widgets/Listview.jsx b/frontend/src/AppBuilder/Widgets/Listview.jsx index 856ef57e49..e08735eb2c 100644 --- a/frontend/src/AppBuilder/Widgets/Listview.jsx +++ b/frontend/src/AppBuilder/Widgets/Listview.jsx @@ -55,6 +55,8 @@ export const Listview = function Listview({ display: visibility ? 'flex' : 'none', borderRadius: borderRadius ?? 0, boxShadow, + padding: '7px 2px 7px 7px', + scrollbarGutter: 'stable', }; const computeCanvasBackgroundColor = useMemo(() => { @@ -235,7 +237,6 @@ export const Listview = function Listview({ // Update the customResolvables with the new listItems if (listItems.length > 0) updateCustomResolvables(id, listItems, 'listItem', moduleId); } - return (
containerProps.onComponentClick(id, component)} data-cy={dataCy} > -
+
{filteredData.map((listItem, index) => (
{ - const canvasFooterHeight = getCanvasHeight(footerHeight); - return ( - - - {isDisabled && ( -