From 5149a136af9061d8ef2aaa4879783a1f97c76323 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Wed, 30 Apr 2025 03:07:40 +0530 Subject: [PATCH 1/8] fix: page permissions demo bugs --- .../LeftSidebar/PageMenu/PageHandlerMenu.jsx | 2 +- .../LeftSidebar/PageMenu/PageMenuItem.jsx | 39 ++++++++++++++++++- .../LeftSidebar/PageMenu/PagePermission.jsx | 33 ++++++---------- .../LeftSidebar/PageMenu/style.scss | 4 ++ frontend/src/AppBuilder/Viewer/PageGroup.jsx | 2 - frontend/src/_styles/components.scss | 6 +++ .../_ui/Icon/solidIcons/EnterrpiseCrown.jsx | 19 +++++++++ frontend/src/_ui/Icon/solidIcons/index.js | 3 ++ server/ee | 2 +- .../page-permissions.repository.ts | 12 ++---- server/src/modules/versions/module.ts | 2 + 11 files changed, 89 insertions(+), 35 deletions(-) create mode 100644 frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx index 1c5c3124f0..497b697b25 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageHandlerMenu.jsx @@ -164,7 +164,7 @@ export const PageHandlerMenu = ({ darkMode }) => { >
Page permission
- {!licenseValid && } + {!licenseValid && }
); diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx index a113251b61..6c74e7b6af 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PageMenuItem.jsx @@ -17,6 +17,7 @@ import IconSelector from './IconSelector'; import { withRouter } from '@/_hoc/withRouter'; import OverflowTooltip from '@/_components/OverflowTooltip'; import { shallow } from 'zustand/shallow'; +import { ToolTip } from '@/_components/ToolTip'; export const PageMenuItem = withRouter( memo(({ darkMode, page, navigate }) => { @@ -151,6 +152,36 @@ export const PageMenuItem = withRouter( [popoverRef.current, page] ); + function getTooltip() { + const permission = page?.permissions?.length ? page?.permissions[0] : null; + if (!permission) return ''; + const users = permission.users || []; + const isSingle = permission.type === 'SINGLE'; + const isGroup = permission.type === 'GROUP'; + + if (users.length === 0) return null; + + if (isSingle) { + if (users.length === 1) { + const email = users[0].user.email; + return `Access restricted to ${email}`; + } else { + return `Access restricted to ${users.length} users`; + } + } + + if (isGroup) { + if (users.length === 1) { + const groupName = users[0].permissionGroup?.name ?? 'Group'; + return `Access restricted to ${groupName} group`; + } else { + return `Access restricted to ${users.length} groups`; + } + } + + return ''; + } + return (
setIsHovered(true)} @@ -200,7 +231,13 @@ export const PageMenuItem = withRouter(
- {licenseValid && restricted && } + {licenseValid && restricted && ( + +
+ +
+
+ )}
{!shouldFreeze && ( diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx index 6a4a1c516a..e82f251665 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/PagePermission.jsx @@ -34,7 +34,6 @@ export default function PagePermission({ darkMode }) { 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'); @@ -42,7 +41,7 @@ export default function PagePermission({ darkMode }) { useEffect(() => { if (!showPagePermissionModal) return; const fetchPagePermission = () => { - appPermissionService.getPagePermission(appId, editingPage?.id || pageToDelete).then((data) => { + appPermissionService.getPagePermission(appId, editingPage?.id).then((data) => { if (data) { if (data[0] && data[0]?.type === PERMISSION_TYPES.group) { const groups = @@ -55,7 +54,6 @@ export default function PagePermission({ darkMode }) { 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) { @@ -74,7 +72,6 @@ export default function PagePermission({ darkMode }) { setInitialPagePermissionType(data[0]?.type?.toLowerCase()); setPagePermission(data); toggleUsersSelect(true); - setPageToDelete(null); setInitialSelectedUsers(users); data?.length && setSelectedUsers(users); } @@ -83,7 +80,7 @@ export default function PagePermission({ darkMode }) { }); }; fetchPagePermission(); - }, [showPagePermissionModal, pageToDelete]); + }, [showPagePermissionModal]); const isSelectionUnchanged = useMemo(() => { if (pagePermissionType === 'group') { @@ -237,13 +234,12 @@ export default function PagePermission({ darkMode }) { const deletePagePermission = () => { setIsLoading(true); appPermissionService - .deletePagePermission(appId, pageToDelete) + .deletePagePermission(appId, editingPage?.id) .then((data) => { toast.success('Permission successfully deleted!', { className: 'text-nowrap w-auto mw-100', }); - updatePageWithPermissions(pageToDelete, []); - setPageToDelete(null); + updatePageWithPermissions(editingPage?.id, []); }) .catch(() => { toast.error('Permission could not be deleted. Please try again!', { @@ -284,25 +280,18 @@ export default function PagePermission({ darkMode }) { isLoading={isLoading} handleClose={handlePagePermissionModalClose} confirmBtnProps={{ - title: pagePermission ? 'Update' : pagePermissionType === 'all' ? 'Default permission' : 'Create permission', + 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" - headerAction={() => - pagePermission && ( - { - setPageToDelete(editingPage?.id); - togglePagePermissionModal(false); - setShowConfirmDelete(true); - }} - > - - - ) - } >
{isPermissionsLoading ? ( diff --git a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss index 968218b106..a3adc9ec20 100644 --- a/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss +++ b/frontend/src/AppBuilder/LeftSidebar/PageMenu/style.scss @@ -374,4 +374,8 @@ .spinner-center { min-height: 250px; } +} + +.modal-base .modal-footer .action-btn-page-permission svg path { + fill: var(--indigo1) !important; } \ No newline at end of file diff --git a/frontend/src/AppBuilder/Viewer/PageGroup.jsx b/frontend/src/AppBuilder/Viewer/PageGroup.jsx index 0311115b09..1739263fff 100644 --- a/frontend/src/AppBuilder/Viewer/PageGroup.jsx +++ b/frontend/src/AppBuilder/Viewer/PageGroup.jsx @@ -11,8 +11,6 @@ import cx from 'classnames'; const RenderPage = ({ page, currentPageId, switchPageWrapper, labelStyle, computeStyles, darkMode, homePageId }) => { const isHomePage = page.id === homePageId; - console.log({ page, homePageId }); - console.log({ isHomePage }); const iconName = isHomePage && !page.icon ? 'IconHome2' : page.icon; const IconElement = Icons?.[iconName] ?? Icons?.['IconFileDescription']; return (page.hidden || page.disabled) && page?.restricted ? null : ( diff --git a/frontend/src/_styles/components.scss b/frontend/src/_styles/components.scss index 074338602e..059841e6c7 100644 --- a/frontend/src/_styles/components.scss +++ b/frontend/src/_styles/components.scss @@ -242,10 +242,16 @@ $btn-dark-color: #FFFFFF; display: flex; align-items: baseline; gap: 5px; + cursor: pointer !important; + pointer-events: unset !important; &.disabled { opacity: 1 !important; } + + svg { + margin-left: 5px; + } } } diff --git a/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx b/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx new file mode 100644 index 0000000000..eed8dd5e8c --- /dev/null +++ b/frontend/src/_ui/Icon/solidIcons/EnterrpiseCrown.jsx @@ -0,0 +1,19 @@ +import React from 'react'; + +const EnterpriseCrown = ({ fill = '#FCA23F', width = '12', className = '', viewBox = '0 0 16 16' }) => ( + + + +); + +export default EnterpriseCrown; diff --git a/frontend/src/_ui/Icon/solidIcons/index.js b/frontend/src/_ui/Icon/solidIcons/index.js index 5daa4b7c91..a190dd4c52 100644 --- a/frontend/src/_ui/Icon/solidIcons/index.js +++ b/frontend/src/_ui/Icon/solidIcons/index.js @@ -234,6 +234,7 @@ import NewTabSmall from './NewTabSmall.jsx'; import Code from './Code.jsx'; import WorkflowV3 from './WorkflowV3.jsx'; import WorkspaceV3 from './WorkspaceV3.jsx'; +import EnterpriseCrown from './EnterrpiseCrown.jsx'; const Icon = (props) => { switch (props.name) { @@ -355,6 +356,8 @@ const Icon = (props) => { return ; case 'enterprisev3': return ; + case 'enterprisecrown': + return ; case 'lockGradient': return ; case 'datasourceGradient': diff --git a/server/ee b/server/ee index 84ec48d0f6..bd6745ee0a 160000 --- a/server/ee +++ b/server/ee @@ -1 +1 @@ -Subproject commit 84ec48d0f64fd6dc5f7677f71a5119219cc4ada4 +Subproject commit bd6745ee0a9344d5fcf42a79a50b8185ec43643a diff --git a/server/src/modules/app-permissions/repositories/page-permissions.repository.ts b/server/src/modules/app-permissions/repositories/page-permissions.repository.ts index 7caa5f4318..a36be08bc7 100644 --- a/server/src/modules/app-permissions/repositories/page-permissions.repository.ts +++ b/server/src/modules/app-permissions/repositories/page-permissions.repository.ts @@ -19,14 +19,10 @@ export class PagePermissionsRepository extends Repository { }); return pagePermissions.map((permission) => { - if (permission.type === PAGE_PERMISSION_TYPE.GROUP) { - return { - ...permission, - groups: permission.users, - users: undefined, - }; - } - return permission; + return { + ...permission, + users: permission.users, + }; }); }, manager || this.manager); } diff --git a/server/src/modules/versions/module.ts b/server/src/modules/versions/module.ts index 1f08dc76bb..80929f4a3a 100644 --- a/server/src/modules/versions/module.ts +++ b/server/src/modules/versions/module.ts @@ -9,6 +9,7 @@ import { DataSourcesModule } from '@modules/data-sources/module'; import { AppsRepository } from '@modules/apps/repository'; import { FeatureAbilityFactory } from './ability'; import { getImportPath } from '@modules/app/constants'; +import { AppPermissionsModule } from '@modules/app-permissions/module'; export class VersionModule { static async register(configs?: { IS_GET_CONTEXT: boolean }): Promise { @@ -33,6 +34,7 @@ export class VersionModule { await DataSourcesModule.register(configs), await AppEnvironmentsModule.register(configs), await ThemesModule.register(configs), + await AppPermissionsModule.register(configs), ], controllers: [ComponentsController, EventsController, PagesController, VersionController, VersionControllerV2], providers: [ From 20dacb0d0d0c57a8b5b76fe7a93727ef3fe3efeb Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Mon, 12 May 2025 13:30:39 +0530 Subject: [PATCH 2/8] fix: import bugs --- frontend/src/HomePage/HomePage.jsx | 44 ++++- server/src/dto/import-resources.dto.ts | 6 +- server/src/helpers/error_type.constant.ts | 2 + .../page-permissions.repository.ts | 12 +- .../services/app-import-export.service.ts | 155 +++++++++++++++--- .../import-export-resources/service.ts | 12 ++ 6 files changed, 201 insertions(+), 30 deletions(-) diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 0e04b4b2a3..0e40e0078c 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -116,6 +116,8 @@ class HomePageComponent extends React.Component { shouldAutoImportPlugin: false, dependentPlugins: [], dependentPluginsDetail: {}, + showMissingGroupsModal: false, + missingGroups: [], }; } @@ -356,17 +358,24 @@ class HomePageComponent extends React.Component { } }; - importFile = async (importJSON, appName) => { + importFile = async (importJSON, appName, skipPagePermissionsGroupCheck = false) => { this.setState({ isImportingApp: true }); // For backward compatibility with legacy app import const organization_id = this.state.currentUser?.organization_id; const isLegacyImport = isEmpty(importJSON.tooljet_version); if (isLegacyImport) { - importJSON = { app: [{ definition: importJSON, appName: appName }], tooljet_version: importJSON.tooljetVersion }; + importJSON = { + app: [{ definition: importJSON, appName: appName }], + tooljet_version: importJSON.tooljetVersion, + }; } else { importJSON.app[0].appName = appName; } - const requestBody = { organization_id, ...importJSON }; + const requestBody = { + organization_id, + ...importJSON, + skip_page_permissions_group_check: skipPagePermissionsGroupCheck, + }; let installedPluginsInfo = []; try { if (this.state.dependentPlugins.length) { @@ -388,6 +397,10 @@ class HomePageComponent extends React.Component { this.props.navigate(`/${getWorkspaceId()}/database`); } } catch (error) { + if (error?.error?.type === 'permission-check') { + this.setState({ showMissingGroupsModal: true, missingGroups: error?.error?.data }); + return; + } if (installedPluginsInfo.length) { const pluginsId = installedPluginsInfo.map((pluginInfo) => pluginInfo.id); await pluginsService.uninstallPlugins(pluginsId); @@ -888,6 +901,8 @@ class HomePageComponent extends React.Component { showGroupMigrationBanner, dependentPlugins, dependentPluginsDetail, + showMissingGroupsModal, + missingGroups, } = this.state; const modalConfigs = { create: { @@ -953,6 +968,29 @@ class HomePageComponent extends React.Component { configs={modalConfigs} onCommitChange={this.handleCommitChange} /> + this.importFile(fileContent, fileName, true)} + show={showMissingGroupsModal} + isLoading={importingApp} + handleClose={() => this.setState({ showMissingGroupsModal: false })} + confirmBtnProps={{ + title: 'Import', + tooltipMessage: '', + }} + darkMode={this.props.darkMode} + > +

+ The following group permissions are missing for page permissions. Are you sure you want to continue? +

+
+ {missingGroups.map((item, index) => ( +
+ {`${index + 1}. ${item}`} +
+ ))} +
+
{showRenameAppModal && ( this.setState({ showRenameAppModal: true })} diff --git a/server/src/dto/import-resources.dto.ts b/server/src/dto/import-resources.dto.ts index f00992213e..89b3ee182c 100644 --- a/server/src/dto/import-resources.dto.ts +++ b/server/src/dto/import-resources.dto.ts @@ -1,4 +1,4 @@ -import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested } from 'class-validator'; +import { IsUUID, IsOptional, IsString, IsDefined, ValidateNested, IsBoolean } from 'class-validator'; import { Transform, Type } from 'class-transformer'; import { ValidateTooljetDatabaseSchema } from './validators/tooljet-database.validator'; import { TjdbSchemaToLatestVersion } from './transformers/resource-transformer'; @@ -28,6 +28,10 @@ export class ImportResourcesDto { // and instantiated data @ValidateTooljetDatabaseSchema({ each: true }) tooljet_database: ImportTooljetDatabaseDto[]; + + @IsOptional() + @IsBoolean() + skip_page_permissions_group_check?: boolean; } export class ImportAppDto { diff --git a/server/src/helpers/error_type.constant.ts b/server/src/helpers/error_type.constant.ts index 4b968f002e..cb483e8896 100644 --- a/server/src/helpers/error_type.constant.ts +++ b/server/src/helpers/error_type.constant.ts @@ -1,5 +1,7 @@ export const APP_ERROR_TYPE = { IMPORT_EXPORT_SERVICE: { UNSUPPORTED_VERSION_ERROR: 'Apps built on later versions of ToolJet cannot be imported', + PAGE_PERMISSION_GROUP_ERROR: 'Following groups are missing from the workspace', + PERMISSION_CHECK: 'permission-check', }, }; diff --git a/server/src/modules/app-permissions/repositories/page-permissions.repository.ts b/server/src/modules/app-permissions/repositories/page-permissions.repository.ts index a36be08bc7..7caa5f4318 100644 --- a/server/src/modules/app-permissions/repositories/page-permissions.repository.ts +++ b/server/src/modules/app-permissions/repositories/page-permissions.repository.ts @@ -19,10 +19,14 @@ export class PagePermissionsRepository extends Repository { }); return pagePermissions.map((permission) => { - return { - ...permission, - users: permission.users, - }; + if (permission.type === PAGE_PERMISSION_TYPE.GROUP) { + return { + ...permission, + groups: permission.users, + users: undefined, + }; + } + return permission; }); }, manager || this.manager); } diff --git a/server/src/modules/apps/services/app-import-export.service.ts b/server/src/modules/apps/services/app-import-export.service.ts index 6cd6b3ce8f..5b1d09f4c8 100644 --- a/server/src/modules/apps/services/app-import-export.service.ts +++ b/server/src/modules/apps/services/app-import-export.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { isEmpty, set } from 'lodash'; import { App } from 'src/entities/app.entity'; import { AppEnvironment } from 'src/entities/app_environments.entity'; @@ -33,6 +33,11 @@ import { DataSourcesUtilService } from '@modules/data-sources/util.service'; import { DataSourcesRepository } from '@modules/data-sources/repository'; import { AppEnvironmentUtilService } from '@modules/app-environments/util.service'; import { ComponentsService } from './component.service'; +import { GroupPermissions } from '@entities/group_permissions.entity'; +import { APP_ERROR_TYPE } from '@helpers/error_type.constant'; +import { PAGE_PERMISSION_TYPE } from '@modules/app-permissions/constants'; +import { PagePermission } from '@entities/page_permissions.entity'; +import { PageUser } from '@entities/page_users.entity'; interface AppResourceMappings { defaultDataSourceIdMapping: Record; dataQueryMapping: Record; @@ -51,7 +56,17 @@ type DefaultDataSourceName = | 'tooljetdbdefault' | 'workflowsdefault'; -type NewRevampedComponent = 'Text' | 'TextInput' | 'PasswordInput' | 'NumberInput' | 'Table' | 'Button' | 'Checkbox' | 'Divider' | 'VerticalDivider' | 'Link'; +type NewRevampedComponent = + | 'Text' + | 'TextInput' + | 'PasswordInput' + | 'NumberInput' + | 'Table' + | 'Button' + | 'Checkbox' + | 'Divider' + | 'VerticalDivider' + | 'Link'; const DefaultDataSourceNames: DefaultDataSourceName[] = [ 'restapidefault', @@ -82,7 +97,7 @@ export class AppImportExportService { protected appEnvironmentUtilService: AppEnvironmentUtilService, protected readonly entityManager: EntityManager, protected componentsService: ComponentsService - ) { } + ) {} async export(user: User, id: string, searchParams: any = {}): Promise<{ appV2: App }> { // https://github.com/typeorm/typeorm/issues/3857 @@ -174,23 +189,41 @@ export class AppImportExportService { } const pages = await manager - .createQueryBuilder(Page, 'pages') - .where('pages.appVersionId IN(:...versionId)', { + .createQueryBuilder(Page, 'page') + .leftJoinAndSelect('page.permissions', 'permission') + .leftJoinAndSelect('permission.users', 'pageUser') + .leftJoinAndSelect('pageUser.permissionGroup', 'permissionGroup') + .where('page.appVersionId IN(:...versionId)', { versionId: appVersions.map((v) => v.id), }) - .orderBy('pages.created_at', 'ASC') + .orderBy('page.created_at', 'ASC') .getMany(); + const pagesWithPermissionGroups = pages.map((page) => { + const groupPermission = page.permissions.find((perm) => perm.type === 'GROUP'); + + return { + ...page, + permissions: groupPermission + ? { + permissionGroup: groupPermission.users + .map((user) => user.permissionGroup?.name) + .filter((name): name is string => Boolean(name)), + } + : undefined, + }; + }); + const components = pages.length > 0 ? await manager - .createQueryBuilder(Component, 'components') - .leftJoinAndSelect('components.layouts', 'layouts') - .where('components.pageId IN(:...pageId)', { - pageId: pages.map((v) => v.id), - }) - .orderBy('components.created_at', 'ASC') - .getMany() + .createQueryBuilder(Component, 'components') + .leftJoinAndSelect('components.layouts', 'layouts') + .where('components.pageId IN(:...pageId)', { + pageId: pages.map((v) => v.id), + }) + .orderBy('components.created_at', 'ASC') + .getMany() : []; const events = await manager @@ -202,7 +235,7 @@ export class AppImportExportService { .getMany(); appToExport['components'] = components; - appToExport['pages'] = pages; + appToExport['pages'] = pagesWithPermissionGroups; appToExport['events'] = events; appToExport['dataQueries'] = dataQueries; appToExport['dataSources'] = dataSources; @@ -800,6 +833,10 @@ export class AppImportExportService { }); } + if (page.permissions) { + pageCreated.permissions = page.permissions; + } + appResourceMappings.pagesMapping[page.id] = pageCreated.id; isHomePage = importingAppVersion.homePageId === page.id; @@ -808,6 +845,9 @@ export class AppImportExportService { updateHomepageId = pageCreated.id; } + //create page permissions of page if flag enabled in dto + await this.createPagePermissionsForGroups(pageCreated, user.organizationId, manager); + const pageComponents = importingComponents.filter((component) => component.pageId === page.id); const newComponentIdsMap = {}; @@ -924,6 +964,7 @@ export class AppImportExportService { }); } } + // relink page groups const updateArr = []; for (const { pageId, groupId } of pageGroupIdArr) { @@ -1059,10 +1100,10 @@ export class AppImportExportService { const options = importingDataSource.kind === 'tooljetdb' ? this.replaceTooljetDbTableIds( - importingQuery.options, - externalResourceMappings['tooljet_database'], - organizationId - ) + importingQuery.options, + externalResourceMappings['tooljet_database'], + organizationId + ) : importingQuery.options; const newQuery = manager.create(DataQuery, { @@ -1315,6 +1356,76 @@ export class AppImportExportService { return pageSettings; } + async checkIfGroupPermissionsExist(pages, organizationId) { + const allGroupNames = new Set(); + + for (const page of pages) { + const groupNames = page.permissions?.permissionGroup || []; + for (const name of groupNames) { + allGroupNames.add(name); + } + } + + if (!allGroupNames.size) return; + + return await dbTransactionWrap(async (manager: EntityManager) => { + const existingGroups = await manager + .createQueryBuilder(GroupPermissions, 'gp') + .where('gp.name IN (:...names)', { names: Array.from(allGroupNames) }) + .andWhere('gp.organizationId = :organizationId', { organizationId }) + .select(['gp.name']) + .getMany(); + + const existingGroupNames = new Set(existingGroups.map((g) => g.name)); + + const missingGroups = Array.from(allGroupNames).filter((name) => !existingGroupNames.has(name)); + + if (missingGroups.length > 0) { + throw new HttpException( + { + message: { type: APP_ERROR_TYPE.IMPORT_EXPORT_SERVICE.PERMISSION_CHECK, data: missingGroups }, + }, + HttpStatus.BAD_REQUEST + ); + } + }); + } + + async createPagePermissionsForGroups(page, organizationId: string, manager: EntityManager) { + const groupNames = page.permissions?.permissionGroup || []; + if (!groupNames.length) return; + + const existingGroups = await manager + .createQueryBuilder(GroupPermissions, 'gp') + .where('gp.name IN (:...names)', { names: groupNames }) + .andWhere('gp.organizationId = :organizationId', { organizationId }) + .getMany(); + + const groupMap = new Map(existingGroups.map((g) => [g.name, g])); + + // Filter to only existing group names + const validGroupNames = groupNames.filter((name) => groupMap.has(name)); + + // If no valid group names exist, do not create permissions + if (!validGroupNames.length) return; + + const permission = manager.create(PagePermission, { + pageId: page.id, + type: PAGE_PERMISSION_TYPE.GROUP, + }); + + const savedPermission = await manager.save(permission); + + const pageUsers = validGroupNames.map((name) => + manager.create(PageUser, { + pagePermissionsId: savedPermission.id, + permissionGroupsId: groupMap.get(name).id, + }) + ); + + await manager.save(pageUsers); + } + async createAppVersionsForImportedApp( manager: EntityManager, user: User, @@ -1627,10 +1738,10 @@ export class AppImportExportService { options: dataSourceId == defaultDataSourceIds['tooljetdb'] ? this.replaceTooljetDbTableIds( - query.options, - externalResourceMappings['tooljet_database'], - user.organizationId - ) + query.options, + externalResourceMappings['tooljet_database'], + user.organizationId + ) : query.options, }); await manager.save(newQuery); diff --git a/server/src/modules/import-export-resources/service.ts b/server/src/modules/import-export-resources/service.ts index e8292c8fd7..e47205258e 100644 --- a/server/src/modules/import-export-resources/service.ts +++ b/server/src/modules/import-export-resources/service.ts @@ -69,6 +69,18 @@ export class ImportExportResourcesService { let tableNameMapping = {}; const imports = { app: [], tooljet_database: [], tableNameMapping: {} }; const importingVersion = importResourcesDto.tooljet_version; + const skipPagePermissionsGroupCheck = importResourcesDto.skip_page_permissions_group_check; + + if (!isEmpty(importResourcesDto.app) && !skipPagePermissionsGroupCheck) { + for (const appImportDto of importResourcesDto.app) { + let appParams = appImportDto.definition; + if (appParams?.appV2) { + appParams = { ...appParams.appV2 }; + const pages = appParams?.pages; + pages?.length && (await this.appImportExportService.checkIfGroupPermissionsExist(pages, user.organizationId)); + } + } + } if (!isEmpty(importResourcesDto.tooljet_database)) { const res = await this.tooljetDbImportExportService.bulkImport(importResourcesDto, importingVersion, cloning); From d52959572b8772d0b9007831159b90bc6e97ae30 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 9 May 2025 18:19:07 +0530 Subject: [PATCH 3/8] Revert "Added edition check in migrations." This reverts commit 4025d2305044ef1fa4f1910aad6ce14f99bd7d3c. --- server/migrations/1744610362161-CreatePagePermissions.ts | 6 ------ server/migrations/1744611380594-CreatePageUsers.ts | 6 ------ 2 files changed, 12 deletions(-) diff --git a/server/migrations/1744610362161-CreatePagePermissions.ts b/server/migrations/1744610362161-CreatePagePermissions.ts index ca4afbac66..ebf622da8b 100644 --- a/server/migrations/1744610362161-CreatePagePermissions.ts +++ b/server/migrations/1744610362161-CreatePagePermissions.ts @@ -1,13 +1,7 @@ import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; -import { TOOLJET_EDITIONS } from '@modules/app/constants'; -import { getTooljetEdition } from '@helpers/utils.helper'; export class CreatePagePermissions1744610362161 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - if (getTooljetEdition() === TOOLJET_EDITIONS.CE) { - return; - } - await queryRunner.createTable( new Table({ name: 'page_permissions', diff --git a/server/migrations/1744611380594-CreatePageUsers.ts b/server/migrations/1744611380594-CreatePageUsers.ts index 5fe4d126c7..f1c6c89beb 100644 --- a/server/migrations/1744611380594-CreatePageUsers.ts +++ b/server/migrations/1744611380594-CreatePageUsers.ts @@ -1,13 +1,7 @@ import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; -import { TOOLJET_EDITIONS } from '@modules/app/constants'; -import { getTooljetEdition } from '@helpers/utils.helper'; export class CreatePageUsers1744611380594 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - if (getTooljetEdition() === TOOLJET_EDITIONS.CE) { - return; - } - await queryRunner.createTable( new Table({ name: 'page_users', From 317f18b4fa67b4b613575ec7e1e6fd6aab129e92 Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 9 May 2025 18:19:17 +0530 Subject: [PATCH 4/8] Revert "Added useful indexes to the entities for query optimization" This reverts commit 962b7dc8b14078b9c8afd54f118fa855fe915c40. --- server/src/entities/group_permissions.entity.ts | 2 -- server/src/entities/group_users.entity.ts | 3 --- server/src/entities/page_users.entity.ts | 5 +---- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/server/src/entities/group_permissions.entity.ts b/server/src/entities/group_permissions.entity.ts index 92868d7510..693f4f930c 100644 --- a/server/src/entities/group_permissions.entity.ts +++ b/server/src/entities/group_permissions.entity.ts @@ -3,7 +3,6 @@ import { Column, CreateDateColumn, Entity, - Index, JoinColumn, ManyToOne, OneToMany, @@ -21,7 +20,6 @@ export class GroupPermissions extends BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Index() @Column({ name: 'organization_id', nullable: false }) organizationId: string; diff --git a/server/src/entities/group_users.entity.ts b/server/src/entities/group_users.entity.ts index 29771a5557..03ac55386b 100644 --- a/server/src/entities/group_users.entity.ts +++ b/server/src/entities/group_users.entity.ts @@ -3,7 +3,6 @@ import { Column, CreateDateColumn, Entity, - Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, @@ -17,11 +16,9 @@ export class GroupUsers extends BaseEntity { @PrimaryGeneratedColumn('uuid') id: string; - @Index() @Column({ name: 'user_id', nullable: false }) userId: string; - @Index() @Column({ name: 'group_id', nullable: false }) groupId: string; diff --git a/server/src/entities/page_users.entity.ts b/server/src/entities/page_users.entity.ts index ca3ef77c65..960be5b32f 100644 --- a/server/src/entities/page_users.entity.ts +++ b/server/src/entities/page_users.entity.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, Index } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn } from 'typeorm'; import { User } from './user.entity'; import { PagePermission } from './page_permissions.entity'; import { GroupPermissions } from './group_permissions.entity'; @@ -8,15 +8,12 @@ export class PageUser { @PrimaryGeneratedColumn('uuid') id: string; - @Index() @Column({ name: 'page_permissions_id', type: 'uuid' }) pagePermissionsId: string; - @Index() @Column({ name: 'user_id', type: 'uuid', nullable: true }) userId: string | null; - @Index() @Column({ name: 'permission_groups_id', type: 'uuid', nullable: true }) permissionGroupsId: string | null; From e41ce8a5ab66766dd3db246cb4c9288f5b1881ee Mon Sep 17 00:00:00 2001 From: devanshu052000 Date: Fri, 9 May 2025 18:21:45 +0530 Subject: [PATCH 5/8] Remove repositories from module imports --- server/src/modules/apps/module.ts | 10 +--------- server/src/modules/workflows/module.ts | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/server/src/modules/apps/module.ts b/server/src/modules/apps/module.ts index 15b5903fb2..5da2bd5992 100644 --- a/server/src/modules/apps/module.ts +++ b/server/src/modules/apps/module.ts @@ -38,15 +38,7 @@ export class AppsModule { return { module: AppsModule, imports: [ - TypeOrmModule.forFeature([ - App, - Page, - EventHandler, - Organization, - Component, - VersionRepository, - RolesRepository, - ]), + TypeOrmModule.forFeature([App, Page, EventHandler, Organization, Component, VersionRepository]), await FolderAppsModule.register(configs), await ThemesModule.register(configs), await FoldersModule.register(configs), diff --git a/server/src/modules/workflows/module.ts b/server/src/modules/workflows/module.ts index 77dfbd0af3..389a754701 100644 --- a/server/src/modules/workflows/module.ts +++ b/server/src/modules/workflows/module.ts @@ -71,7 +71,6 @@ export class WorkflowsModule { WorkflowExecutionNode, WorkflowExecutionNode, WorkflowExecutionEdge, - RolesRepository, ]), ThrottlerModule.forRootAsync({ imports: [ConfigModule], From ab6463f80706642c6cb62d95105ea35543295f07 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Fri, 16 May 2025 14:01:23 +0530 Subject: [PATCH 6/8] fix: modal UI --- frontend/src/HomePage/HomePage.jsx | 119 ++++++++++++++++++++++++++--- frontend/src/_styles/theme.scss | 66 ++++++++++++++++ frontend/src/_ui/Modal/index.jsx | 68 +++++++++-------- 3 files changed, 213 insertions(+), 40 deletions(-) diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 0e40e0078c..2b9586029a 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -47,6 +47,7 @@ import { ConsultationBanner, } from '@/modules/dashboard/components'; import CreateAppWithPrompt from '@/modules/AiBuilder/components/CreateAppWithPrompt'; +import SolidIcon from '@/_ui/Icon/SolidIcons'; const { iconList, defaultIcon } = configs; @@ -118,6 +119,7 @@ class HomePageComponent extends React.Component { dependentPluginsDetail: {}, showMissingGroupsModal: false, missingGroups: [], + missingGroupsExpanded: false, }; } @@ -903,6 +905,7 @@ class HomePageComponent extends React.Component { dependentPluginsDetail, showMissingGroupsModal, missingGroups, + missingGroupsExpanded, } = this.state; const modalConfigs = { create: { @@ -954,6 +957,40 @@ class HomePageComponent extends React.Component { }; const isAdmin = authenticationService?.currentSessionValue?.admin; const isBuilder = authenticationService?.currentSessionValue?.is_builder; + + const testGroups = [ + { name: 'Group 1' }, + { name: 'Group 2 long name' }, + { name: 'Group 3 med' }, + { name: 'Group 4 med' }, + { name: 'Group 4 really long name' }, + { name: 'Group 1' }, + { name: 'Group 2 long name' }, + { name: 'Group 3 med' }, + { name: 'Group 4 med' }, + { name: 'Group 4 really long name' }, + { name: 'Group 1' }, + { name: 'Group 2 long name' }, + { name: 'Group 3 med' }, + { name: 'Group 4 med' }, + { name: 'Group 4 really long name' }, + { name: 'Group 1' }, + { name: 'Group 2 long name' }, + { name: 'Group 3 med' }, + { name: 'Group 4 med' }, + { name: 'Group 4 really long name' }, + { name: 'Group 1' }, + { name: 'Group 2 long name' }, + { name: 'Group 3 med' }, + { name: 'Group 4 med' }, + { name: 'Group 4 really long name' }, + ]; + + //import app missing groups modal config + const threshold = 3; + const isLong = missingGroups.length > threshold; + const displayedGroups = missingGroupsExpanded ? missingGroups : missingGroups.slice(0, threshold); + return (
@@ -969,7 +1006,8 @@ class HomePageComponent extends React.Component { onCommitChange={this.handleCommitChange} /> this.importFile(fileContent, fileName, true)} show={showMissingGroupsModal} isLoading={importingApp} @@ -978,17 +1016,80 @@ class HomePageComponent extends React.Component { title: 'Import', tooltipMessage: '', }} + className="missing-groups-modal" darkMode={this.props.darkMode} > -

- The following group permissions are missing for page permissions. Are you sure you want to continue? -

-
- {missingGroups.map((item, index) => ( -
- {`${index + 1}. ${item}`} +
+
+ +
+
Warning: Missing user groups for permissions
+

+ Permissions for the following user group(s) won’t be applied since they do not exist in this + workspace. +

- ))} +
+ +
+
+
+ User groups +
+
+ {displayedGroups.map((group, idx) => ( + + {group} + {idx < displayedGroups.length - 1 ? ', ' : ''} + + ))} + {!missingGroupsExpanded && isLong && '...'} +
+
+ + {isLong && ( + + )} +
+ +

+ Restricted pages, queries, or components will become accessible to all users or to existing groups with + permissions. To avoid this, create the missing groups before importing, or reconfigure permissions after + import. +

+ +
+ this.setState({ showMissingGroupsModal: false, isImportingApp: false })} + > + Cancel import + + this.importFile(fileContent, fileName, true)} + className="primary-action" + > + Import with limited permissions + +
{showRenameAppModal && ( diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index e464a7b86f..50894aefbf 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -18875,4 +18875,70 @@ section.ai-message-prompt-input-wrapper { .cm-editor { max-height: 100px !important; } +} + +.missing-groups-modal { + .modal-body { + padding: 16px; + + .header { + padding-top: 12px; + padding-left: 4px; + font-weight: 500; + font-size: 14px; + } + + .sub-header { + margin-bottom: 0px; + font-size: 12px; + } + + .groups-list { + padding-top: 16px; + padding-bottom: 16px; + + .container { + padding: 12px; + } + } + + .info { + margin-bottom: 0px; + font-size: 12px; + padding-bottom: 24px; + } + + .action-btns { + justify-content: space-between; + } + + .primary-action, .secondary-action { + padding: 8px !important; + font-size: 12px; + } + + .toggle-button { + display: inline-flex; + align-items: center; + font-size: 14px; + color: var(--icon-brand); + background: none; + border: none; + cursor: pointer; + padding: 0; + font-family: inherit; + } + + .toggle-button:hover { + text-decoration: underline; + } + + .toggle-button .chevron { + transition: transform 0.2s ease; + } + + .toggle-button.expanded .chevron { + transform: rotate(180deg); + } + } } \ No newline at end of file diff --git a/frontend/src/_ui/Modal/index.jsx b/frontend/src/_ui/Modal/index.jsx index b9d48f0d88..6ed7fa04db 100644 --- a/frontend/src/_ui/Modal/index.jsx +++ b/frontend/src/_ui/Modal/index.jsx @@ -18,6 +18,8 @@ export default function ModalBase({ className = '', size = 'sm', headerAction, + showHeader = true, + showFooter = true, }) { return ( - - - {title} - -
- {headerAction && headerAction()} - -
-
+ {showHeader && ( + + + {title} + +
+ {headerAction && headerAction()} + +
+
+ )} {children ? ( children @@ -45,28 +49,30 @@ export default function ModalBase({
)} - - - Cancel - - -
- - {confirmBtnProps?.title || 'Continue'} - -
-
-
+ {showFooter && ( + + + Cancel + + +
+ + {confirmBtnProps?.title || 'Continue'} + +
+
+
+ )} ); } From 689fc0106a44318849c8c2969d9cd3160f101e25 Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Fri, 16 May 2025 15:22:36 +0530 Subject: [PATCH 7/8] fix: header alignment --- frontend/src/HomePage/HomePage.jsx | 28 ---------------------------- frontend/src/_styles/theme.scss | 1 - 2 files changed, 29 deletions(-) diff --git a/frontend/src/HomePage/HomePage.jsx b/frontend/src/HomePage/HomePage.jsx index 2b9586029a..3af41903aa 100644 --- a/frontend/src/HomePage/HomePage.jsx +++ b/frontend/src/HomePage/HomePage.jsx @@ -958,34 +958,6 @@ class HomePageComponent extends React.Component { const isAdmin = authenticationService?.currentSessionValue?.admin; const isBuilder = authenticationService?.currentSessionValue?.is_builder; - const testGroups = [ - { name: 'Group 1' }, - { name: 'Group 2 long name' }, - { name: 'Group 3 med' }, - { name: 'Group 4 med' }, - { name: 'Group 4 really long name' }, - { name: 'Group 1' }, - { name: 'Group 2 long name' }, - { name: 'Group 3 med' }, - { name: 'Group 4 med' }, - { name: 'Group 4 really long name' }, - { name: 'Group 1' }, - { name: 'Group 2 long name' }, - { name: 'Group 3 med' }, - { name: 'Group 4 med' }, - { name: 'Group 4 really long name' }, - { name: 'Group 1' }, - { name: 'Group 2 long name' }, - { name: 'Group 3 med' }, - { name: 'Group 4 med' }, - { name: 'Group 4 really long name' }, - { name: 'Group 1' }, - { name: 'Group 2 long name' }, - { name: 'Group 3 med' }, - { name: 'Group 4 med' }, - { name: 'Group 4 really long name' }, - ]; - //import app missing groups modal config const threshold = 3; const isLong = missingGroups.length > threshold; diff --git a/frontend/src/_styles/theme.scss b/frontend/src/_styles/theme.scss index 50894aefbf..1b0c48850c 100644 --- a/frontend/src/_styles/theme.scss +++ b/frontend/src/_styles/theme.scss @@ -18883,7 +18883,6 @@ section.ai-message-prompt-input-wrapper { .header { padding-top: 12px; - padding-left: 4px; font-weight: 500; font-size: 14px; } From 1a1040c2f052ca0325132d77cdfe26f316c366ac Mon Sep 17 00:00:00 2001 From: Vijaykant Yadav Date: Mon, 19 May 2025 10:53:53 +0530 Subject: [PATCH 8/8] update submodules --- frontend/ee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/ee b/frontend/ee index 1b77a55670..518f3334b1 160000 --- a/frontend/ee +++ b/frontend/ee @@ -1 +1 @@ -Subproject commit 1b77a556709211daed8924821383db9dccc95eb5 +Subproject commit 518f3334b12a83785fd37dd53b0245d72848211a