diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx index ca193084fb..7968184633 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,6 +28,8 @@ 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 { definition: { styles, properties }, @@ -195,8 +198,11 @@ export const PageMenuItem = withRouter( {isHidden && !isDisabled && 'Hidden'} - {!shouldFreeze && ( -
+
+ {licenseValid && page?.restricted && } +
+
+ {!shouldFreeze && ( -
- )} + )} +
)} diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx index ce7c78e6f3..095c33fd18 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx @@ -18,6 +18,7 @@ export default function PagePermission({ darkMode }) { const showPagePermissionModal = useStore((state) => state.showPagePermissionModal); const togglePagePermissionModal = useStore((state) => state.togglePagePermissionModal); const editingPage = useStore((state) => state.editingPage); + const setEditingPage = useStore((state) => state.setEditingPage); const appId = useStore((state) => state.app.appId); const selectedUserGroups = useStore((state) => state.selectedUserGroups); const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); @@ -31,15 +32,14 @@ export default function PagePermission({ darkMode }) { const [showUsersSelect, toggleUsersSelect] = useState(false); const [showConfirmDelete, setShowConfirmDelete] = useState(false); const [isLoading, setIsLoading] = useState(false); - - console.log({ editingPage, showUserGroupSelect }); + const [pageToDelete, setPageToDelete] = useState(null); useEffect(() => { if (!editingPage?.id && !showPagePermissionModal) return; const fetchPagePermission = () => { appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => { if (data) { - if (data[0]) { + if (data[0] && data[0]?.type === PERMISSION_TYPES.group) { setPagePermissionType(data[0]?.type?.toLowerCase()); setPagePermission(data); toggleUserGroupSelect(true); @@ -48,14 +48,32 @@ export default function PagePermission({ darkMode }) { data[0]?.users?.map((user) => ({ label: user?.permissionGroup?.name, value: user?.permissionGroup?.id, + count: user?.permissionGroup?.count, })) ); + } else if (data[0] && data[0]?.type === PERMISSION_TYPES.single) { + setPagePermissionType(data[0]?.type?.toLowerCase()); + setPagePermission(data); + toggleUsersSelect(true); + data?.length && + setSelectedUsers( + 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(), + }; + }) + ); } } }); }; fetchPagePermission(); - }, [appId, editingPage, setPagePermission, setSelectedUserGroups, showPagePermissionModal]); + }, [editingPage]); const permissionTypeOptions = useMemo( () => [ @@ -77,9 +95,7 @@ export default function PagePermission({ darkMode }) { ], [] ); - console.log({ pagePermission }); const handlePermissionTypeChange = (value) => { - console.log({ value }); switch (value) { case 'group': { toggleUserGroupSelect(true); @@ -107,6 +123,9 @@ export default function PagePermission({ darkMode }) { toggleUsersSelect(false); setPagePermissionType('all'); setPagePermission(null); + setEditingPage(null); + setSelectedUsers([]); + setSelectedUserGroups([]); }; const createPagePermission = () => { @@ -121,7 +140,7 @@ export default function PagePermission({ darkMode }) { appPermissionService .createPagePermission(appId, editingPage?.id, body) .then((data) => { - console.log({ data }); + toast.success('Permission successfully created!'); }) .catch(() => { toast.error('Permission could not be created. Please try again!'); @@ -129,7 +148,6 @@ export default function PagePermission({ darkMode }) { .finally(() => { setIsLoading(false); handlePagePermissionModalClose(); - toast.success('Permission successfully created!'); }); }; @@ -145,7 +163,7 @@ export default function PagePermission({ darkMode }) { appPermissionService .updatePagePermission(appId, editingPage?.id, body) .then((data) => { - console.log({ data }); + toast.success('Permission successfully updated!'); }) .catch(() => { toast.error('Permission could not be updated. Please try again!'); @@ -153,16 +171,15 @@ export default function PagePermission({ darkMode }) { .finally(() => { setIsLoading(false); handlePagePermissionModalClose(); - toast.success('Permission successfully updated!'); }); }; const deletePagePermission = () => { setIsLoading(true); appPermissionService - .deletePagePermission(appId, editingPage?.id) + .deletePagePermission(appId, pageToDelete) .then((data) => { - console.log({ data }); + toast.success('Permission successfully deleted!'); }) .catch(() => { toast.error('Permission could not be deleted. Please try again!'); @@ -170,14 +187,14 @@ export default function PagePermission({ darkMode }) { .finally(() => { setIsLoading(false); setShowConfirmDelete(false); + setPageToDelete(null); handlePagePermissionModalClose(); - toast.success('Permission successfully deleted!'); }); }; const renderPermissionTypeOptions = ({ label, icon }) => { return ( -
+
@@ -211,6 +228,7 @@ export default function PagePermission({ darkMode }) { pagePermission && ( { + setPageToDelete(editingPage?.id); togglePagePermissionModal(false); setShowConfirmDelete(true); }} @@ -268,20 +286,17 @@ export default function PagePermission({ darkMode }) { } const UserGroupSelect = () => { - console.log('rendering'); const appId = useStore((state) => state.app.appId); - const editingPage = useStore((state) => state.editingPage); const selectedUserGroups = useStore((state) => state.selectedUserGroups); const setSelectedUserGroups = useStore((state) => state.setSelectedUserGroups); const [userGroups, setUserGroups] = useState([]); useEffect(() => { const fetchUserGroups = () => { appPermissionService.getUsers(appId, 'user-groups').then((data) => { - console.log({ data }); if (data?.length) { const groups = []; data.map((group) => { - groups.push({ value: group.id, label: group.name }); + groups.push({ value: group.id, label: group.name, count: group.count }); }); setUserGroups(groups); } @@ -290,7 +305,26 @@ const UserGroupSelect = () => { fetchUserGroups(); }, []); - console.log({ selectedUserGroups, userGroups }); + const CustomOption = (props) => { + const { data, isFocused, isSelected } = props; + + return ( + +
+ +
+
{data.label}
+
{data.count} users
+
+
+
+ ); + }; return (
@@ -300,10 +334,12 @@ const UserGroupSelect = () => { options={userGroups} value={selectedUserGroups} width={'100%'} - // customOption={renderPermissionTypeOptions} + closeMenuOnSelect={false} + components={{ Option: CustomOption, MenuList: CustomMenuList }} useMenuPortal={false} - // menuIsOpen={true} + hideSelectedOptions={false} onChange={(groups) => setSelectedUserGroups(groups)} + info="Only user groups with access to this application can be selected" />
); @@ -318,7 +354,6 @@ const UserSelect = () => { useEffect(() => { const fetchUsers = () => { appPermissionService.getUsers(appId, 'users').then((data) => { - console.log({ data }); if (data?.length) { const users = []; data.map((user) => { @@ -343,6 +378,12 @@ const UserSelect = () => { return (
+
{data.initials}
{data.label}
@@ -353,8 +394,12 @@ const UserSelect = () => { ); }; - console.log({ users }); - + const selectStyles = { + option: (base) => ({ + ...base, + padding: '8px 0px', + }), + }; return (
@@ -363,15 +408,35 @@ const UserSelect = () => { options={users} value={selectedUsers} width={'100%'} - // customOption={renderUserSelectOptions} useMenuPortal={false} - components={{ Option: CustomOption }} - // menuIsOpen={true} + closeMenuOnSelect={false} + components={{ Option: CustomOption, MenuList: CustomMenuList }} + styles={selectStyles} + hideSelectedOptions={false} + info="Only user with access to this application can be selected" onChange={(users) => { - console.log({ userstemp: users }); setSelectedUsers(users); }} />
); }; + +const CustomMenuList = (props) => { + const { info } = props.selectProps; + return ( + +
+
+ +
+
+
+

{info}

+
+
+
+ {props.children} +
+ ); +}; diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss index 203b68ec71..b3799e09ce 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss @@ -297,6 +297,14 @@ gap: 12px; } + .react-select__option { + padding: 8px 0px; + + input { + margin-right: 10px; + } + } + .user-select-option { display: flex; align-items: center; @@ -327,15 +335,32 @@ flex-direction: column; .name { - font-weight: 600; + font-weight: 500; font-size: 14px; color: var(--slate12); } .email { font-size: 12px; - color: var(--slate10); // gray-500 + color: var(--slate10); } } + + .group-info { + display: flex; + flex-direction: row; + gap: 8px; + + .name { + font-weight: 400; + font-size: 14px; + color: var(--slate12); + } + + .count { + font-size: 12px; + color: var(--slate9); + } + } } } \ No newline at end of file diff --git a/frontend/src/AppBuilder/Viewer/PageGroup.jsx b/frontend/src/AppBuilder/Viewer/PageGroup.jsx index 4a47d350f4..120b5dfc68 100644 --- a/frontend/src/AppBuilder/Viewer/PageGroup.jsx +++ b/frontend/src/AppBuilder/Viewer/PageGroup.jsx @@ -142,8 +142,7 @@ const RenderPageGroup = ({ export const RenderPageAndPageGroup = ({ pages, labelStyle, computeStyles, darkMode, switchPageWrapper }) => { // Don't render empty folders if displaying only icons const tree = buildTree(pages, !!labelStyle?.label?.hidden); - const filteredPages = tree.filter((page) => !page?.isPageGroup || page.children?.length > 0); - + const filteredPages = tree.filter((page) => (!page?.isPageGroup || page.children?.length > 0) && !page?.restricted); const currentPageId = useStore((state) => state.currentPageId); const currentPage = pages.find((page) => page.id === currentPageId); const homePageId = useStore((state) => state.app.homePageId); diff --git a/frontend/src/AppBuilder/_hooks/useAppData.js b/frontend/src/AppBuilder/_hooks/useAppData.js index bbd4308403..d10e4343b9 100644 --- a/frontend/src/AppBuilder/_hooks/useAppData.js +++ b/frontend/src/AppBuilder/_hooks/useAppData.js @@ -329,25 +329,23 @@ const useAppData = (appId, moduleId, darkMode, mode = 'edit', { environmentId, v if (initialLoadRef.current) { // if initial load, check if the path has a page handle and set that as the starting page - const initialLoadPath = location.pathname.split('/')[3]; + const initialLoadPath = location.pathname.split('/').pop(); const page = appData.pages.find((page) => page.handle === initialLoadPath && !page.isPageGroup); if (page) { // if page is disabled, and not editing redirect to home page - if (mode !== 'edit' && page?.disabled) { - const currentUrl = window.location.href; - const replacedUrl = currentUrl.replace(initialLoadPath, startingPage.handle); - window.history.replaceState(null, null, replacedUrl); + const shouldRedirect = page?.restricted || (mode !== 'edit' && page?.disabled); + + if (shouldRedirect) { + const newUrl = window.location.href.replace(initialLoadPath, startingPage.handle); + window.history.replaceState(null, null, newUrl); + + if (page?.restricted) { + toast.error('Access to this page is restricted. Contact admin to know more.'); + } } else { startingPage = page; } - } else { - if (mode !== 'edit' && initialLoadPath) { - const currentUrl = window.location.href; - const replacedUrl = currentUrl.replace(initialLoadPath, startingPage.handle); - window.history.replaceState(null, null, replacedUrl); - toast.error('Access to this page is restricted. Contact admin to know more.'); - } } // navigate(`/${getWorkspaceId()}/apps/${slug ?? appId}/${startingPage.handle}`); diff --git a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js index 60fbef8b52..f93f64b1c5 100644 --- a/frontend/src/AppBuilder/_stores/slices/eventsSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/eventsSlice.js @@ -860,7 +860,9 @@ export const createEventsSlice = (set, get) => ({ const { switchPage } = get(); const page = get().modules.canvas.pages.find((page) => page.id === event.pageId); const queryParams = event.queryParams || []; - if (!page.disabled) { + if (page.restricted && mode !== 'edit') { + toast.error('Access to this page is restricted. Contact admin to know more.'); + } else if (!page.disabled) { const resolvedQueryParams = []; queryParams.forEach((param) => { resolvedQueryParams.push([ @@ -1118,10 +1120,6 @@ export const createEventsSlice = (set, get) => ({ toast('Valid page handle is required', { icon: '⚠️', }); - mode === 'view' && - toast.error('Access to this page is restricted. Contact admin to know more.', { - icon: '⚠️', - }); return Promise.resolve(); } diff --git a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js index ea17cc27e2..1fea8b7090 100644 --- a/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js +++ b/frontend/src/AppBuilder/_stores/slices/pageMenuSlice.js @@ -444,5 +444,9 @@ export const createPageMenuSlice = (set, get) => { set((state) => { state.selectedUsers = users; }), + setEditingPage: (page) => + set((state) => { + state.editingPage = page; + }), }; }; diff --git a/server/ee b/server/ee index d14468b695..7d46f023ce 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit d14468b6954cc33616b02802c49db2deac6be105 +Subproject commit 7d46f023cea42533c4a8ff387dcc1149553c4671 diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts index 38b1ee1968..15b5903fb2 100644 --- a/server/src/modules/apps/module.ts +++ b/server/src/modules/apps/module.ts @@ -20,6 +20,7 @@ import { DataSourcesModule } from '@modules/data-sources/module'; import { AppsSubscriber } from './subscribers/apps.subscriber'; import { AiModule } from '@modules/ai/module'; import { AppPermissionsModule } from '@modules/app-permissions/module'; +import { RolesRepository } from '@modules/roles/repository'; @Module({}) export class AppsModule { static async register(configs: { IS_GET_CONTEXT: boolean }): Promise { @@ -37,7 +38,15 @@ export class AppsModule { return { module: AppsModule, imports: [ - TypeOrmModule.forFeature([App, Page, EventHandler, Organization, Component, VersionRepository]), + TypeOrmModule.forFeature([ + App, + Page, + EventHandler, + Organization, + Component, + VersionRepository, + RolesRepository, + ]), await FolderAppsModule.register(configs), await ThemesModule.register(configs), await FoldersModule.register(configs), @@ -63,6 +72,7 @@ export class AppsModule { AppsSubscriber, DataSourcesRepository, AppImportExportService, + RolesRepository, ], exports: [AppsUtilService], }; diff --git a/server/src/modules/apps/service.ts b/server/src/modules/apps/service.ts index 60c6f8bc15..81708667f9 100644 --- a/server/src/modules/apps/service.ts +++ b/server/src/modules/apps/service.ts @@ -18,7 +18,7 @@ import { VersionReleaseDto, } from './dto'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { APP_TYPES, FEATURE_KEY } from './constants'; +import { FEATURE_KEY } from './constants'; import { camelizeKeys, decamelizeKeys } from 'humps'; import { App } from '@entities/app.entity'; import { AppsUtilService } from './util.service'; diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts index 65721a264e..77dfbd0af3 100644 --- a/server/src/modules/workflows/module.ts +++ b/server/src/modules/workflows/module.ts @@ -30,6 +30,7 @@ import { App } from '@entities/app.entity'; import { AiModule } from '@modules/ai/module'; import { DataSourcesRepository } from '@modules/data-sources/repository'; import { AppPermissionsModule } from '@modules/app-permissions/module'; +import { RolesRepository } from '@modules/roles/repository'; export class WorkflowsModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { const importPath = await getImportPath(configs?.IS_GET_CONTEXT); @@ -70,6 +71,7 @@ export class WorkflowsModule { WorkflowExecutionNode, WorkflowExecutionNode, WorkflowExecutionEdge, + RolesRepository, ]), ThrottlerModule.forRootAsync({ imports: [ConfigModule], @@ -115,6 +117,7 @@ export class WorkflowsModule { WorkflowSchedulesService, TemporalService, FeatureAbilityFactory, + RolesRepository, ], controllers: [ WorkflowsController,