diff --git a/frontend/assets/images/icons/editor/left-sidebar/authorization.svg b/frontend/assets/images/icons/editor/left-sidebar/authorization.svg new file mode 100644 index 0000000000..609f7a5910 --- /dev/null +++ b/frontend/assets/images/icons/editor/left-sidebar/authorization.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/ee b/frontend/ee index a1435b3b0e..dcd948d284 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit a1435b3b0e66a0c731812256d3d495d5bf48d5bb +Subproject commit dcd948d284b5f14a868480830e09b90496db8572 diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx index e2a395ecab..1c5c3124f0 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx @@ -2,6 +2,9 @@ import React from 'react'; import { Overlay, Popover } from 'react-bootstrap'; import { Button } from '@/_ui/LeftSidebar'; import useStore from '@/AppBuilder/_stores/store'; +import { shallow } from 'zustand/shallow'; +import { ToolTip } from '@/_components/ToolTip'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; export const PageHandlerMenu = ({ darkMode }) => { const setShowEditingPopover = useStore((state) => state.setShowEditingPopover); @@ -20,23 +23,9 @@ export const PageHandlerMenu = ({ darkMode }) => { const toggleDeleteConfirmationModal = useStore((state) => state.toggleDeleteConfirmationModal); const clonePage = useStore((state) => state.clonePage); const markAsHomePage = useStore((state) => state.markAsHomePage); - // const popoverTargetRef = null; - // console.log( - // { - // setShowEditingPopover, - // setShowRenameHandlerModal, - // setEditingPage, - // setShowPageEventsModal, - // popoverTargetRef, - // editingPage, - // showRenameHandlerModal, - // showPageEventsModal, - // setEditingPageName, - // showEditingPopover, - // closeEditingPopover, - // }, - // 'editingPage' - // ); + const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; const closeMenu = () => { closePageEditPopover(); @@ -119,7 +108,6 @@ export const PageHandlerMenu = ({ darkMode }) => { callback={() => markAsHomePage(editingPage.id)} /> )} - {!isDisabled && ( { disabled={isHomePage} /> )} - { clonePage(editingPage.id); }} /> - { }} disabled={isHomePage} /> + + { + return ( + +
+
Page permission
+ {!licenseValid && } +
+
+ ); + }} + customClass={'delete-btn'} + iconSrc={`assets/images/icons/editor/left-sidebar/authorization.svg`} + closeMenu={closeMenu} + callback={(id) => { + togglePagePermissionModal(true); + }} + /> { ); }; -const Field = ({ id, text, iconSrc, customClass = '', closeMenu, disabled = false, callback = () => null }) => { +const Field = ({ + id, + text, + iconSrc, + customClass = '', + classNames, + closeMenu, + disabled = false, + callback = () => null, +}) => { const handleOnClick = (e) => { e.preventDefault(); e.stopPropagation(); @@ -233,7 +254,12 @@ const Field = ({ id, text, iconSrc, customClass = '', closeMenu, disabled = fals return (
- +
diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx index 0669f4928b..d4b8573c7d 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenu.jsx @@ -16,6 +16,7 @@ import { EditModal } from './EditModal'; import { SettingsModal } from './SettingsModal'; import { DeletePageConfirmationModal } from './DeletePageConfirmationModal'; import SolidIcon from '@/_ui/Icon/SolidIcons'; +import PagePermission from './PagePermission'; export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { const showAddNewPageInput = useStore((state) => state.showAddNewPageInput); @@ -94,6 +95,7 @@ export const PageMenu = ({ darkMode, switchPage, pinned, setPinned }) => { >
+ {isLicensed ? : <>} diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx index ca193084fb..0f3b5d21a7 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx @@ -16,6 +16,7 @@ import { RenameInput } from './RenameInput'; import IconSelector from './IconSelector'; import { withRouter } from '@/_hoc/withRouter'; import OverflowTooltip from '@/_components/OverflowTooltip'; +import { shallow } from 'zustand/shallow'; export const PageMenuItem = withRouter( memo(({ darkMode, page, navigate }) => { @@ -27,7 +28,10 @@ export const PageMenuItem = withRouter( const isDisabled = page?.disabled ?? false; const [isHovered, setIsHovered] = useState(false); const shouldFreeze = useStore((state) => state.getShouldFreeze()); + const featureAccess = useStore((state) => state?.license?.featureAccess, shallow); + const licenseValid = !featureAccess?.licenseStatus?.isExpired && featureAccess?.licenseStatus?.isLicenseValid; const showEditingPopover = useStore((state) => state.showEditingPopover); + const restricted = page?.permissions && page?.permissions?.length > 0; const { definition: { styles, properties }, } = useStore((state) => state.pageSettings); @@ -195,8 +199,11 @@ export const PageMenuItem = withRouter( {isHidden && !isDisabled && 'Hidden'}
- {!shouldFreeze && ( -
+
+ {licenseValid && restricted && } +
+
+ {!shouldFreeze && ( -
- )} + )} +
)} diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx new file mode 100644 index 0000000000..6a4a1c516a --- /dev/null +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx @@ -0,0 +1,516 @@ +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'; + +const PERMISSION_TYPES = { + single: 'SINGLE', + group: 'GROUP', + all: 'ALL', +}; + +export default function PagePermission({ darkMode }) { + const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); + const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); + const editingPage = useStore((state) => state.editingPage); + const appId = useStore((state) => state.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 [pageToDelete, setPageToDelete] = useState(null); + const [initialSelectedGroups, setInitialSelectedGroups] = useState([]); + const [initialSelectedUsers, setInitialSelectedUsers] = useState([]); + const [initalPagePermissionType, setInitialPagePermissionType] = useState('all'); + + useEffect(() => { + if (!showPagePermissionModal) return; + const fetchPagePermission = () => { + appPermissionService.getPagePermission(appId, editingPage?.id || pageToDelete).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); + setPageToDelete(null); + 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); + setPageToDelete(null); + setInitialSelectedUsers(users); + data?.length && setSelectedUsers(users); + } + } + setPermissionsLoading(false); + }); + }; + fetchPagePermission(); + }, [showPagePermissionModal, pageToDelete]); + + 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, pageToDelete) + .then((data) => { + toast.success('Permission successfully deleted!', { + className: 'text-nowrap w-auto mw-100', + }); + updatePageWithPermissions(pageToDelete, []); + setPageToDelete(null); + }) + .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 ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission', + disabled: isPermissionsLoading || isSelectionUnchanged, + tooltipMessage: '', + }} + darkMode={darkMode} + className="page-permissions-modal" + headerAction={() => + pagePermission && ( + { + setPageToDelete(editingPage?.id); + togglePagePermissionModal(false); + setShowConfirmDelete(true); + }} + > + + + ) + } + > +
+ {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 ( +
+ +