fix: import bugs

This commit is contained in:
Vijaykant Yadav 2025-05-12 13:30:39 +05:30
parent 5149a136af
commit 20dacb0d0d
6 changed files with 201 additions and 30 deletions

View file

@ -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}
/>
<ModalBase
title={'Missing groups in workspace'}
handleConfirm={() => this.importFile(fileContent, fileName, true)}
show={showMissingGroupsModal}
isLoading={importingApp}
handleClose={() => this.setState({ showMissingGroupsModal: false })}
confirmBtnProps={{
title: 'Import',
tooltipMessage: '',
}}
darkMode={this.props.darkMode}
>
<p className="tj-text-sm">
The following group permissions are missing for page permissions. Are you sure you want to continue?
</p>
<div className="item-list">
{missingGroups.map((item, index) => (
<div key={index}>
<span className="tj-text-sm">{`${index + 1}. ${item}`}</span>
</div>
))}
</div>
</ModalBase>
{showRenameAppModal && (
<AppModal
show={() => this.setState({ showRenameAppModal: true })}

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

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

@ -19,10 +19,14 @@ export class PagePermissionsRepository extends Repository<PagePermission> {
});
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);
}

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';
interface AppResourceMappings {
defaultDataSourceIdMapping: Record<string, string>;
dataQueryMapping: Record<string, string>;
@ -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<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,
@ -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);

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