Merge page-permission-fixes, keeping current submodule versions

This commit is contained in:
devanshu052000 2025-05-20 20:40:41 +05:30
commit daa85fe48a
23 changed files with 427 additions and 97 deletions

View file

@ -164,7 +164,7 @@ export const PageHandlerMenu = ({ darkMode }) => {
>
<div className="d-flex align-items-center">
<div>Page permission</div>
{!licenseValid && <SolidIcon name="enterprisesmall" />}
{!licenseValid && <SolidIcon name="enterprisecrown" />}
</div>
</ToolTip>
);

View file

@ -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 (
<div
onMouseEnter={() => setIsHovered(true)}
@ -200,7 +231,13 @@ export const PageMenuItem = withRouter(
</span>
</div>
<div style={{ marginLeft: '8px', marginRight: 'auto' }}>
{licenseValid && restricted && <SolidIcon width="16" name="lock" fill="var(--icon-strong)" />}
{licenseValid && restricted && (
<ToolTip message={getTooltip()}>
<div>
<SolidIcon width="16" name="lock" fill="var(--icon-strong)" />
</div>
</ToolTip>
)}
</div>
<div className={cx('right', { 'handler-menu-open': showEditingPopover })}>
{!shouldFreeze && (

View file

@ -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 && (
<span
onClick={(e) => {
setPageToDelete(editingPage?.id);
togglePagePermissionModal(false);
setShowConfirmDelete(true);
}}
>
<SolidIcon fill="var(--tomato10)" width="20" name="trash" />
</span>
)
}
>
<div className="page-permission">
{isPermissionsLoading ? (

View file

@ -374,4 +374,8 @@
.spinner-center {
min-height: 250px;
}
}
.modal-base .modal-footer .action-btn-page-permission svg path {
fill: var(--indigo1) !important;
}

View file

@ -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 : (

View file

@ -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;
@ -116,6 +117,9 @@ class HomePageComponent extends React.Component {
shouldAutoImportPlugin: false,
dependentPlugins: [],
dependentPluginsDetail: {},
showMissingGroupsModal: false,
missingGroups: [],
missingGroupsExpanded: false,
};
}
@ -356,17 +360,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 +399,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 +903,9 @@ class HomePageComponent extends React.Component {
showGroupMigrationBanner,
dependentPlugins,
dependentPluginsDetail,
showMissingGroupsModal,
missingGroups,
missingGroupsExpanded,
} = this.state;
const modalConfigs = {
create: {
@ -939,6 +957,12 @@ class HomePageComponent extends React.Component {
};
const isAdmin = authenticationService?.currentSessionValue?.admin;
const isBuilder = authenticationService?.currentSessionValue?.is_builder;
//import app missing groups modal config
const threshold = 3;
const isLong = missingGroups.length > threshold;
const displayedGroups = missingGroupsExpanded ? missingGroups : missingGroups.slice(0, threshold);
return (
<Layout switchDarkMode={this.props.switchDarkMode} darkMode={this.props.darkMode}>
<div className="wrapper home-page">
@ -953,6 +977,93 @@ class HomePageComponent extends React.Component {
configs={modalConfigs}
onCommitChange={this.handleCommitChange}
/>
<ModalBase
showHeader={false}
showFooter={false}
handleConfirm={() => this.importFile(fileContent, fileName, true)}
show={showMissingGroupsModal}
isLoading={importingApp}
handleClose={() => this.setState({ showMissingGroupsModal: false })}
confirmBtnProps={{
title: 'Import',
tooltipMessage: '',
}}
className="missing-groups-modal"
darkMode={this.props.darkMode}
>
<div className="missing-groups-modal-body">
<div className="flex items-start">
<SolidIcon name="warning" width="40px" fill="var(--icon-warning)" />
<div>
<div className="header">Warning: Missing user groups for permissions</div>
<p className="sub-header">
Permissions for the following user group(s) wont be applied since they do not exist in this
workspace.
</p>
</div>
</div>
<div className="groups-list">
<div
className={`border rounded text-sm container ${
missingGroupsExpanded ? 'max-h-48 overflow-y-auto' : ''
}`}
>
<div style={{ color: 'var(--text-placeholder)' }} className="tj-text-xsm font-weight-500">
User groups
</div>
<div className="mt-1">
{displayedGroups.map((group, idx) => (
<span className="tj-text-xsm font-weight-500" key={idx}>
{group}
{idx < displayedGroups.length - 1 ? ', ' : ''}
</span>
))}
{!missingGroupsExpanded && isLong && '...'}
</div>
</div>
{isLong && (
<button
class="toggle-button"
onClick={() => this.setState({ missingGroupsExpanded: !missingGroupsExpanded })}
>
<span className="chevron">
<SolidIcon
fill="var(--icon-brand)"
name={missingGroupsExpanded ? 'cheveronup' : 'cheverondown'}
/>
</span>
<span class="label">{missingGroupsExpanded ? 'See less' : 'See more'}</span>
</button>
)}
</div>
<p className="info">
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.
</p>
<div className="mt-6 d-flex justify-between action-btns">
<ButtonSolid
className="secondary-action"
variant={'tertiary'}
onClick={() => this.setState({ showMissingGroupsModal: false, isImportingApp: false })}
>
Cancel import
</ButtonSolid>
<ButtonSolid
isLoading={importingApp}
variant={'primary'}
onClick={() => this.importFile(fileContent, fileName, true)}
className="primary-action"
>
Import with limited permissions
</ButtonSolid>
</div>
</div>
</ModalBase>
{showRenameAppModal && (
<AppModal
show={() => this.setState({ showRenameAppModal: true })}

View file

@ -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;
}
}
}

View file

@ -18891,4 +18891,69 @@ section.ai-message-prompt-input-wrapper {
.cm-editor {
max-height: 100px !important;
}
}
.missing-groups-modal {
.modal-body {
padding: 16px;
.header {
padding-top: 12px;
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);
}
}
}

View file

@ -0,0 +1,19 @@
import React from 'react';
const EnterpriseCrown = ({ fill = '#FCA23F', width = '12', className = '', viewBox = '0 0 16 16' }) => (
<svg
width={width}
height={width}
viewBox="0 0 16 16"
fill={fill}
className={className}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.6474 6.49583L13.2967 12.6705C13.2195 13.1143 12.8143 13.4423 12.3705 13.4423H3.62954C3.16644 13.4423 2.78053 13.1143 2.70334 12.6705L1.35264 6.51512C1.25616 5.99414 1.60349 5.51175 2.12447 5.41527C2.4525 5.35738 2.78053 5.47315 3.01207 5.724L5.01883 7.88512L7.14136 3.08049C7.35361 2.59809 7.93248 2.38584 8.39558 2.61739C8.60783 2.71387 8.7622 2.86823 8.85867 3.08049L10.9812 7.86582L12.988 5.7047C13.3353 5.31879 13.9334 5.2802 14.3387 5.62752C14.5895 5.83977 14.7053 6.1871 14.6474 6.49583Z"
fill={fill}
/>
</svg>
);
export default EnterpriseCrown;

View file

@ -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';
import Moon from './Moon.jsx';
const Icon = (props) => {
@ -356,6 +357,8 @@ const Icon = (props) => {
return <EnterpriseNew {...props} />;
case 'enterprisev3':
return <EnterpriseV3 {...props} />;
case 'enterprisecrown':
return <EnterpriseCrown {...props} />;
case 'lockGradient':
return <LockGradient {...props} />;
case 'datasourceGradient':

View file

@ -18,6 +18,8 @@ export default function ModalBase({
className = '',
size = 'sm',
headerAction,
showHeader = true,
showFooter = true,
}) {
return (
<Modal
@ -27,15 +29,17 @@ export default function ModalBase({
centered={true}
contentClassName={`${className} ${darkMode ? 'theme-dark dark-theme modal-base' : 'modal-base'}`}
>
<Modal.Header>
<Modal.Title className="font-weight-500" data-cy="modal-title">
{title}
</Modal.Title>
<div onClick={handleClose} id="header-actions" className="cursor-pointer" data-cy="modal-close-button">
{headerAction && headerAction()}
<SolidIcon name="remove" width="20" />
</div>
</Modal.Header>
{showHeader && (
<Modal.Header>
<Modal.Title className="font-weight-500" data-cy="modal-title">
{title}
</Modal.Title>
<div onClick={handleClose} id="header-actions" className="cursor-pointer" data-cy="modal-close-button">
{headerAction && headerAction()}
<SolidIcon name="remove" width="20" />
</div>
</Modal.Header>
)}
<Modal.Body data-cy="modal-body">
{children ? (
children
@ -45,28 +49,30 @@ export default function ModalBase({
</div>
)}
</Modal.Body>
<Modal.Footer>
<ButtonSolid disabled={cancelDisabled} variant={'tertiary'} onClick={handleClose} data-cy="cancel-button">
Cancel
</ButtonSolid>
<ToolTip
show={confirmBtnProps?.tooltipMessage && confirmBtnProps?.disabled}
message={confirmBtnProps?.tooltipMessage}
>
<div>
<ButtonSolid
disabled={isLoading || confirmBtnProps?.disabled}
isLoading={isLoading}
variant={confirmBtnProps?.variant || 'primary'}
onClick={handleConfirm}
{...confirmBtnProps}
data-cy="confim-button"
>
{confirmBtnProps?.title || 'Continue'}
</ButtonSolid>
</div>
</ToolTip>
</Modal.Footer>
{showFooter && (
<Modal.Footer>
<ButtonSolid disabled={cancelDisabled} variant={'tertiary'} onClick={handleClose} data-cy="cancel-button">
Cancel
</ButtonSolid>
<ToolTip
show={confirmBtnProps?.tooltipMessage && confirmBtnProps?.disabled}
message={confirmBtnProps?.tooltipMessage}
>
<div>
<ButtonSolid
disabled={isLoading || confirmBtnProps?.disabled}
isLoading={isLoading}
variant={confirmBtnProps?.variant || 'primary'}
onClick={handleConfirm}
{...confirmBtnProps}
data-cy="confim-button"
>
{confirmBtnProps?.title || 'Continue'}
</ButtonSolid>
</div>
</ToolTip>
</Modal.Footer>
)}
</Modal>
);
}

View file

@ -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<void> {
if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
return;
}
await queryRunner.createTable(
new Table({
name: 'page_permissions',

View file

@ -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<void> {
if (getTooljetEdition() === TOOLJET_EDITIONS.CE) {
return;
}
await queryRunner.createTable(
new Table({
name: 'page_users',

View file

@ -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 {

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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',
},
};

View file

@ -39,15 +39,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),

View file

@ -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';
import { UsersUtilService } from '@modules/users/util.service';
interface AppResourceMappings {
defaultDataSourceIdMapping: Record<string, string>;
@ -186,13 +191,31 @@ 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
@ -214,7 +237,7 @@ export class AppImportExportService {
.getMany();
appToExport['components'] = components;
appToExport['pages'] = pages;
appToExport['pages'] = pagesWithPermissionGroups;
appToExport['events'] = events;
appToExport['dataQueries'] = dataQueries;
appToExport['dataSources'] = dataSources;
@ -812,6 +835,10 @@ export class AppImportExportService {
});
}
if (page.permissions) {
pageCreated.permissions = page.permissions;
}
appResourceMappings.pagesMapping[page.id] = pageCreated.id;
isHomePage = importingAppVersion.homePageId === page.id;
@ -820,6 +847,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 = {};
@ -936,6 +966,7 @@ export class AppImportExportService {
});
}
}
// relink page groups
const updateArr = [];
for (const { pageId, groupId } of pageGroupIdArr) {
@ -1327,6 +1358,76 @@ export class AppImportExportService {
return pageSettings;
}
async checkIfGroupPermissionsExist(pages, organizationId) {
const allGroupNames = new Set<string>();
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,

View file

@ -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);

View file

@ -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<DynamicModule> {
@ -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: [

View file

@ -71,7 +71,6 @@ export class WorkflowsModule {
WorkflowExecutionNode,
WorkflowExecutionNode,
WorkflowExecutionEdge,
RolesRepository,
]),
ThrottlerModule.forRootAsync({
imports: [ConfigModule],